PostgreSQL Sorgu Performansı: İndeksler, Okuma/Yazma Etkisi ve İleri Konseptler
İyi tasarlanmış bir şema tek başına yetmez: sorgular büyüdükçe planlama, istatistik ve indeks katmanı devreye girer. Bu yazıda PostgreSQL’de sorgu performansını şekillendiren indeks türlerini, ne zaman işe yaradıklarını ve özellikle okuma ile yazma arasındaki takası net biçimde ele alıyoruz.
Sorgu Motoru Kısaca
PostgreSQL bir sorguyu çalıştırmadan önce maliyet modeli ile birkaç yol dener (çoğu zaman heuristik + istatistik). Sonuç:
- Seq Scan: Tabloyu baştan sona okur — küçük tablolar veya indeks kullanmanın faydasız olduğu durumlar için mantıklı.
- Index Scan / Index Only Scan: İndeks üzerinden satırlara veya yalnızca indekste bulunan sütunlara ulaşır.
- Bitmap Index Scan: Birden fazla indeks koşulunu birleştirmek için sık kullanılır.
Gerçek performansı görmek için EXPLAIN (ANALYZE, BUFFERS) şart; tahmin satırları (rows) ile gerçek (actual rows) farkı genelde istatistik veya plan kalitesi sorunudur.
B-tree: Varsayılan ve Çoğu Senaryonun Kazananı
PostgreSQL’de CREATE INDEX ile oluşturduğunuz indeks çoğu zaman B-tree’dir. Eşitlik (=) ve aralık (<, >, BETWEEN, ORDER BY) için uygundur.
İyi çalıştığı yerler:
- Birincil anahtar / benzersiz doğrulamalar (
UNIQUE) - Zaman damgası veya ID ile filtreleme
- Birleşimlerde (
JOIN) küçük tarafın anahtarı üzerinde indeks
Dikkat: Çok düşük seçicilik (ör. WHERE status = 'active' ve tablonun %90’ı active) tek başına B-tree ile seq scan’e yenilenebilir — planner haklıdır.
Bileşik İndeks (Composite)
Sütun sırası kritiktir. İndeks (a, b, c) ise:
WHERE a = ?→ kullanılabilirWHERE a = ? AND b = ?→ kullanılabilirWHERE b = ?→ genelde kullanılamaz (a eşitlik/ön koşul yok)
Kural: En seçici ve en sık filtrelenen sütunu öne alın; ORDER BY ile uyumlu sıra plan maliyetini düşürür.
Partial Index (Kısmi İndeks)
Yalnızca bir alt küme için indeks:
CREATE INDEX idx_orders_open ON orders (created_at)
WHERE status IN ('pending', 'processing');
Avantaj: Daha küçük indeks → daha az disk, daha hızlı bakım, sık sorgulanan alt kümeye odaklı okuma.
Uyarı: Sorgu koşulu indeksteki WHERE ile uyumlu olmalı; aksi halde planner indeksi seçmez.
Covering Index ve INCLUDE
Sorgu yalnızca birkaç sütun istiyorsa Index Only Scan mümkün olabilir. PostgreSQL 11+ ile:
CREATE INDEX idx_users_email ON users (email) INCLUDE (display_name, id);
INCLUDE sütunları indeks anahtarında sıralanmaz; yalnızca yapraklarda taşınır — geniş anahtar maliyetini sınırlar.
İleri Seviye: GIN, GiST, BRIN (Kısa Harita)
| Tür | Tipik kullanım |
|---|---|
| GIN | jsonb, full-text, diziler — çok değer içeren yapılar |
| GiST | geometri, bazı full-text ve aralık türleri |
| BRIN | çok büyük, doğal sıralı (zaman serisi) tablolar — minimal boyut |
BRIN “ucuz ama kaba” bir filtredir; her satır için hassas değildir, doğru veri düzeni şarttır.
Okuma Tarafında İndeks Etkisi
- Az satır seçiliyorsa indeks genelde gecikmeyi düşürür (rastgele disk erişimi maliyeti
heap fetchile artar). - Çok satır dönüyorsa seq scan + paralel tarama bazen daha ucuzdur.
- JOIN zincirinde doğru tarafta indeks, nested loop maliyetini kontrol altına alır.
Pratik: Üretimde EXPLAIN (ANALYZE, BUFFERS) ile shared hit / read oranına bakın; soğuk önbellek testleri de yapın.
Yazma Tarafında İndeks Etkisi
Her ek indeks:
- INSERT: Her indekse yeni giriş yazılır.
- UPDATE: Anahtar veya indekslenen sütunlar değiştiyse ilgili indeksler güncellenir.
- DELETE: Tüm ilgili indeks girişleri temizlenir.
Bu nedenle “her sütuna indeks” yaklaşımı yazma yoğun sistemlerde throughput’u düşürür ve VACUUM / autovacuum ile bloat baskısını artırır.
HOT (Heap-Only Tuple) Güncellemeleri
Güncellenen sütunlar indekslenmemişse ve yeni sürüm aynı sayfaya sığıyorsa PostgreSQL bazen HOT ile indeksi güncellemeden devam edebilir — yazma maliyetini düşürür. Bu yüzden gereksiz indekslerden kaçınmak sadece insert değil, update maliyetini de şekillendirir.
İstatistik ve Bakım
ANALYZE(veya autovacuum analiz): Planner’ın satır tahmini için gerekli.- Çok sık değişen tablolarda istatistik eskirse yanlış plan seçilir —
EXPLAIN’de tahmin/gerçek sapması büyür. - Index bloat: Yoğun update/delete sonrası
REINDEX CONCURRENTLY(uygun sürümde) veya bakım penceresi planlanmalı.
Sık Hatalar
- Aynı sütun kümesi için neredeyse aynı indeksleri çoğaltmak
- Partial indeks tanımı ile uygulama sorgusunun uyuşmaması
- ORM ile gelen “gizli” N+1 sorgular — indeks var olsa bile gecikme çarpanlaşır
- Büyük migration’da indeksi transaction dışında oluşturmamak (kilitlenme)
- Production’da
EXPLAIN ANALYZEolmadan “indeks ekledim, kesin hızlandı” varsayımı
Özet Denge
| Okuma | Yazma / Bakım |
|---|---|
| Daha az tam tarama, daha düşük gecikme | Her indeks ek yazar; vacuum/reindex maliyeti |
| Index only scan ile I/O azaltma | HOT fırsatını daraltan fazla indeks |
İndeks optimizasyon değil abonelik modelidir: her biri okumaya yardım ederken yazmaya ve bakıma ortak olur. Doğru strateji, ölçüm (EXPLAIN ANALYZE, gerçek trafik, p95/p99) ile şekillenir.
İyi sorgular.