Ölçeklenebilir bir uygulama kurmak doğru veritabanını seçmekle bitmez. Veriyi nasıl yapılandırdığınız, sorguları nasıl kurduğunuz ve ilişkileri nasıl yönettiğiniz performansı, ölçeklenmeyi ve işletme yükünü doğrudan belirler. Bu yazı, yük altında ayakta kalan uygulamaların kullandığı veritabanı tasarım kalıplarını anlatıyor.
Tasarım neden önemli
Uygulama mimarilerinde darboğaz çoğu zaman veritabanıdır. Kötü tasarlanmış bir şema zincirleme sorunlar üretir: yavaş sorgular, yüksek gecikme, artan altyapı maliyeti ve kötüleşen kullanıcı deneyimi. Aşağıdaki kalıplar, zorlanan tek bir veritabanından ölçeklenen bir yapıya geçmenin yaygın yollarıdır. Ama sıra önemlidir: önce ölçün, sonra en basit kalıpla başlayın.
Kalıp 1: Parçalama (sharding)
Parçalama, veriyi yatay olarak birden fazla veritabanı sunucusuna böler. Her parça verinin bir alt kümesini tutar; hangi kaydın hangi parçada olduğunu bir parça anahtarı (müşteri kimliği, kiracı kimliği ya da bölge) belirler.
İki yaygın yöntem:
Bölgeye göre: Avrupa müşterileri → parça 1, Türkiye müşterileri → parça 2
Özete göre: parça = hash(customer_id) mod N (ya da tutarlı özetleme)
Bölgeye göre parçalama veri yerleşimi (data residency) gereksinimlerini karşılamaya yardım eder; özete göre parçalama yükü daha eşit dağıtır. Tutarlı özetleme (consistent hashing), parça eklendiğinde yalnız küçük bir veri kısmının taşınmasını sağlar.
Artıları:
- Veri büyüdükçe yatay ölçeklenme
- Parça başına daha küçük indeksler ve daha hızlı sorgular
- Veri yerleşimi gereksinimlerine uyum
Zorlukları:
- Yönlendirme mantığı uygulamaya ya da bir ara katmana girer
- Parçalar arası sorgular ve transaction’lar pahalıdır
- Parçaları yeniden dengelemek dikkatli planlama ister
Parçalama en son başvurulacak kalıplardandır; tek sunucuyu dikey büyütmek, okuma kopyaları ve bölümleme çoğu zaman uzun süre yeter.
Kalıp 2: Okuma kopyaları ve CQRS
Okuması ağır uygulamalarda yazma ana sunucuya, okuma bir ya da birden fazla kopyaya gider. Kopyalar asenkron çoğaltıldığı için ana sunucunun milisaniyeler ya da saniyeler gerisinde olabilir; “yazdığını hemen oku” gerektiren akışlar (kullanıcı formu kaydettikten sonra kendi kaydını görmek) ana sunucudan okumalıdır.
CQRS (Command Query Responsibility Segregation) bunu bir adım ileri götürür: yazma ve okuma için ayrı modeller. Yazmalar normalize, transaction’lı bir veritabanına gider; okumalar sorguya göre biçimlenmiş, normalize edilmemiş bir modelden yapılır.
Örnek: analitik platformu
- Yazma servisi gelen veriyi doğrular ve transaction’lı PostgreSQL’e yazar
- Okuma servisi panolar için önceden toplanmış verileri ayrı görünümlerden ya da bir veri ambarından okur
Yazma tarafı ana sunucunun dikey gücüyle, okuma tarafı kopya ekleyerek ölçeklenir. Yazmayı da yatay ölçeklemek gerekiyorsa sıradaki adım parçalamadır.
Kalıp 3: Normalize etmeme ve materyalize görünümler
Normalizasyon veri tekrarını önler ama sık sorgularda pahalı birleştirmeler (join) gerektirebilir. Bazı veriyi bilinçli olarak tekrarlamak okumayı hızlandırır.
Örnek: sosyal akış
CREATE TABLE posts (
id bigint PRIMARY KEY,
user_id bigint NOT NULL REFERENCES users(id),
content text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE likes (
post_id bigint NOT NULL REFERENCES posts(id),
user_id bigint NOT NULL REFERENCES users(id),
PRIMARY KEY (post_id, user_id)
);
Akışı her seferinde gönderi, kullanıcı ve beğeni sayısını birleştirerek üretmek ölçekte pahalıdır. Önceden hesaplanmış bir görünüm:
CREATE MATERIALIZED VIEW post_feed AS
SELECT p.id, p.user_id, p.content, p.created_at,
u.username, u.avatar_url,
count(l.user_id) AS like_count
FROM posts p
JOIN users u ON u.id = p.user_id
LEFT JOIN likes l ON l.post_id = p.id
GROUP BY p.id, u.username, u.avatar_url;
CREATE UNIQUE INDEX post_feed_id ON post_feed (id);
-- Okumaları kilitlemeden yenileme (benzersiz indeks gerektirir)
REFRESH MATERIALIZED VIEW CONCURRENTLY post_feed;
Materyalize görünüm, son yenilemedeki veriyi gösterir; ne kadar bayat olabileceğine karar verip yenilemeyi zamanlanmış bir işle yapın. Sürekli güncel bir sayaç gerekiyorsa beğeni sayısını gönderi satırında tutup her beğenide artırmak daha uygun olabilir.
Kalıp 4: Önbellek katmanları
Bellek içi önbellek veritabanı yükünü ciddi biçimde azaltır. En yaygın kalıp “önce önbelleğe bak, yoksa veritabanından oku ve önbelleğe yaz”dır (cache-aside):
import json
import redis
r = redis.Redis(host="redis", port=6379)
def get_user(conn, user_id: int) -> dict | None:
key = f"user:{user_id}"
cached = r.get(key)
if cached:
return json.loads(cached)
# Parametreli sorgu: kullanıcı girdisi asla SQL metnine eklenmez
row = conn.execute("SELECT id, name, email FROM users WHERE id = %s", (user_id,)).fetchone()
if row is None:
return None
user = {"id": row[0], "name": row[1], "email": row[2]}
r.setex(key, 3600, json.dumps(user)) # 1 saat
return user
Geçersiz kılma stratejileri:
- Süreye bağlı (TTL): belirli süre sonra kendiliğinden düşer; biraz bayatlığı kaldırabilen veride
- Olaya bağlı: veri değiştiğinde ilgili anahtar silinir; en doğru yöntem
- Yazarken güncelleme (write-through): veritabanı ve önbellek birlikte güncellenir
Kalıp 5: Bölümleme (partitioning)
Bölümleme büyük bir tabloyu aynı veritabanı içinde daha küçük parçalara ayırır. Sorgular yalnız ilgili bölümleri tarar, eski veriyi silmek ise tek bir bölümü düşürmek kadar ucuzdur.
Zamana göre bölümleme (PostgreSQL):
CREATE TABLE events (
id bigint GENERATED ALWAYS AS IDENTITY,
event_type text NOT NULL,
created_at timestamptz NOT NULL,
data jsonb,
PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (created_at);
CREATE TABLE events_2026_q3 PARTITION OF events
FOR VALUES FROM ('2026-07-01') TO ('2026-10-01');
CREATE TABLE events_2026_q4 PARTITION OF events
FOR VALUES FROM ('2026-10-01') TO ('2027-01-01');
-- Saklama süresi dolan dönemi silmek: tek komut, satır satır DELETE yok
DROP TABLE events_2025_q3;
Bölümler önceden oluşturulmalıdır; bunu pg_partman gibi bir araçla ya da zamanlanmış bir işle otomatikleştirin.
Kalıp 6: Belirli işler için farklı veri modelleri
İlişkisel veritabanları transaction tutarlılığında güçlüdür; bazı işlerde başka modeller daha uygundur:
- Belge veritabanları (MongoDB): esnek şema, iç içe yapılar
- Anahtar-değer depoları (Redis, DynamoDB): çok hızlı arama, oturum ve sayaçlar
- Graf veritabanları (Neo4j): karmaşık ilişkiler ve öneriler
- Zaman serisi (TimescaleDB, InfluxDB): metrik ve izleme verisi
Her yeni veritabanı türü işletilmesi, yedeklenmesi ve izlenmesi gereken bir sistem daha demektir. PostgreSQL’in jsonb, tam metin arama ve uzantıları çoğu ihtiyacı tek veritabanında karşılar.
Örnek: son bildirimler Redis’te
from datetime import datetime, timezone
def push_notification(user_id: int, message: str) -> None:
key = f"notifications:{user_id}"
r.lpush(key, json.dumps({"message": message,
"ts": datetime.now(timezone.utc).isoformat()}))
r.ltrim(key, 0, 99) # son 100 bildirim
r.expire(key, 7 * 24 * 3600) # 7 gün
Kalıp 7: Bağlantı havuzu
Veritabanı bağlantısı pahalı bir kaynaktır; her istekte yeni bağlantı açmak hem gecikme ekler hem de sunucunun bağlantı sınırını tüketir:
from sqlalchemy import create_engine
engine = create_engine(
"postgresql+psycopg://app@db/appdb",
pool_size=10, # havuzda tutulan bağlantı
max_overflow=10, # yoğunlukta açılabilecek ek bağlantı
pool_recycle=1800, # bağlantıları 30 dakikada bir yenile
pool_pre_ping=True, # kopmuş bağlantıyı kullanmadan önce yakala
)
Uygulamanın kopya sayısı × havuz boyutu, veritabanının max_connections değerini aşmamalıdır. Çok sayıda kopya varsa araya PgBouncer gibi bir bağlantı havuzlayıcı koyun.
Pratik öneriler
- Optimize etmeden önce ölçün:
EXPLAIN ANALYZEvepg_stat_statementsile gerçek darboğazı bulun - Basit başlayın: normalize şemayla başlayıp kalıpları ancak gerektiğinde ekleyin
- Sürekli izleyin: sorgu süresi, bağlantı sayısı, kopya gecikmesi, kaynak kullanımı
- Büyümeyi düşünün: veri hacmi bir büyüklük mertebesi artarsa hangi sorgunun bozulacağını önceden sorun
- Gerçekçi veriyle test edin: üretime benzer hacimde yük testi
- Kararları yazın: şema tercihlerinin gerekçesi kodun yanında dursun
Sonuç
Etkili veritabanı tasarımı, uygulamanın ihtiyacına göre seçilmiş birkaç kalıbın birleşimidir. Evrensel bir çözüm yoktur; doğru yaklaşım verinizin yapısına, sorgu örüntülerinize, tutarlılık gereksinimlerinize ve büyüme beklentinize bağlıdır. İş yükünüzü anlayın, başlangıç ölçümünü alın ve kalıpları adım adım uygulayın.
Veritabanı mimarinizi birlikte değerlendirmek isterseniz bize yazın.
ByteGurus
Yazar