Database2026-01-22

PostgreSQL Query Optimization: 40 Saniyeden 0.8 Saniyeye Düşüş Hikayesi

Müşterim 'Dashboard 40 saniyede açılıyor' dediğinde sihirli bir değnek kullanmadım. Sadece EXPLAIN ANALYZE okumayı biliyordum. İşte adım adım performans rehberi.

"Dashboard Açılmıyor, Sunucu mu Kiralasak?"

Geçen hafta bir müşterim panik halinde aradı. Dashboard ekranının açılması tam 40 saniye sürüyormuş. "Daha güçlü bir sunucuya mı geçsek?" diye soruyorlardı.

Onlara "Durun, cüzdanınızı kapatın" dedim. 15 dakikalık bir incelemeden sonra, kodda sadece tek bir satırı değiştirerek ve bir tane index ekleyerek süreyi 0.8 saniyeye düşürdük.

Bu bir sihir değil. Bu, PostgreSQL'in bize ne dediğini (EXPLAIN ANALYZE) anlamak ve veri tabanının nasıl düşündüğünü bilmekle ilgili. İşte size gerçek hayat senaryolarıyla dolu bir optimizasyon rehberi.

Bölüm 1: EXPLAIN ANALYZE Çıktısını Okuyabilmek

PostgreSQL'e bir sorgu gönderdiğinizde, "Planner" (Planlayıcı) en verimli yolu bulmaya çalışır. EXPLAIN ANALYZE komutu, planner'ın ne düşündüğünü ve gerçekten ne yaptığını (süresiyle birlikte) gösterir.

EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 9521;

Bu komutu çalıştırdığınızda karşınıza çıkan terimler korkutucu olabilir. En önemlileri şunlar:

  • Seq Scan (Sequential Scan): Veritabanı kitabı baştan sona okuyor demektir. Tablonuzda 1 milyon satır varsa ve index yoksa, PostgreSQL aradığınızı bulmak için 1 milyon satırı tek tek okur. Kırmızı alarm.
  • Index Scan: Kitabın arkasındaki "İndeks" bölümüne bakıp, direkt ilgili sayfayı açmasıdır. Hedefimiz budur.
  • Cost: Cost=0.00..12.50 gibi bir ifade görürsünüz. Bu, sorgunun tahmini maliyetidir (işlemci zamanı, disk I/O).

Bölüm 2: Klasik Performans Katilleri

Sorgularınızı yavaşlatan en yaygın 5 suçlu:

1. N+1 Problemi (ORM Tuzağı)

ORM'ler (Prisma, TypeORM, Hibernate) yazılım geliştirmeyi hızlandırır ama performans körlüğü yaratır.

Kötü Örnek:

// Önce tüm kullanıcıları çek
const users = await db.user.findMany();
// Sonra her kullanıcı için tek tek siparişleri çek (N tane query)
for (const user of users) {
  const orders = await db.order.findMany({ where: { userId: user.id } });
}

Bu kod 100 kullanıcı için veritabanına 101 kez gider.

İyi Örnek:

// JOIN (veya include) kullanarak tek seferde çek
const usersWithOrders = await db.user.findMany({
  include: { orders: true }
});

Sadece 1 kez veritabanına gider.

2. SELECT * Kullanımı

Neden ihtiyacınız olmayan veriyi RAM'e doldurup network trafiği yaratasınız? JSON kolonları veya TEXT gibi büyük alanlar varsa SELECT * performansı öldürür. Sadece ihtiyacınız olan sütunları seçin.

3. LIKE '%aranan%' Tuzağı

Bir metin araması yaparken % işaretini başa koyarsanız (LIKE '%elma%'), PostgreSQL index kullanamaz (B-Tree indexler soldan sağa çalışır). Tüm tabloyu taramak zorundadır.

Çözüm: Full Text Search veya pg_trgm eklentisi kullanın.

4. JOIN Sıralaması

PostgreSQL genellikle akıllıdır ama karmaşık querylerde kafası karışabilir. Her zaman en çok satır eleyecek (filtreleyecek) tabloyu önceye almaya çalışın veya WHERE koşullarınızın indexte olduğundan emin olun.

Bölüm 3: Index Stratejileri

Her kolona index eklemek çözüm değildir; yazma performansını (INSERT/UPDATE) düşürür. Stratejik olun.

  • B-Tree: Varsayılan index tipidir. =, >, < gibi operatörler için idealdir.
  • Composite Index: Eğer sorgunuzda WHERE category = 'A' AND status = 'active' varsa, bu iki alan için tek birleşik (composite) index oluşturun. Dikkat: Sıralama önemlidir! Sorguda kullandığınız sırayla index oluşturun.
  • Partial Index: Sadece aktif kullanıcıları sorguluyorsanız, tüm tabloyu indexlemeyin.
    CREATE INDEXidx_active_users ON users (email) WHERE status = 'active';
    Index boyutunu küçültür ve hızlandırır.

Bölüm 4: Monitoring ve Toollar

Kör uçuş yapmayın.

  • pg_stat_statements: Bu eklentiyi mutlaka aktif edin. Size "en çok zaman harcayan" ve "en sık çağrılan" sorguları listeler. Optimizasyona buradan başlayın.
  • pgBadger: Log dosyalarınızı analiz edip size HTML rapor sunar. "Saat 14:00'te hangi sorgular sistemi kilitlemiş?" sorusunun cevabıdır.
  • Vacuum: PostgreSQL sildiğiniz satırları hemen diskten atmaz, "ölü" olarak işaretler. VACUUM ANALYZE komutu hem evi temizler hem de istatistikleri günceller, böylece planner daha doğru kararlar verir.

Bölüm 5: Gerçek Bir Vaka Analizi

Senaryo: 10 milyon satırlık logs tablosunda belirli bir IP adresine ve tarih aralığına göre arama yapılıyor. Süre: 45 saniye.

Adım 1: Analiz

EXPLAIN ANALYZE SELECT * FROM logs WHERE ip = '192.168.1.1' AND created_at > NOW() - INTERVAL '1 day';

Çıktı: Seq Scan on logs. Yani tüm tablo taranıyor.

Adım 2: Index Ekleme

CREATE INDEX idx_logs_ip_date ON logs (ip, created_at);

Yeni Süre: 12 saniye. Daha iyi ama yetmez.

Adım 3: Sorgu Refactoring Index olmasına rağmen hala yavaştı çünkü created_at üzerinde fonksiyon (NOW() - ...) vardı ve veri dağılımı dengesizdi. Tabloyu partitionlara (aylık) böldük ve sorguyu buna göre düzenledik. Son Süre: 0.6 saniye.

Sonuç: Performans Bir Kültürdür

Performans optimizasyonu, proje bittikten sonra yapılan bir "cila" işlemi değildir. Geliştirme sürecinin bir parçasıdır.

Checklist:

  1. [ ] N+1 problemlerini ORM seviyesinde çözdüm mü?
  2. [ ] Gereksiz SELECT * var mı?
  3. [ ] WHERE koşullarındaki alanlarda index var mı?
  4. [ ] Indexlerim verimli kullanılıyor mu (EXPLAIN ile kontrol)?
  5. [ ] Ölü satır (bloat) oranım ne durumda?

Veri tabanı mimarinizde darboğazlar yaşıyorsanız ve "artık sunucu yetmiyor" diyorsanız, yeni donanım almadan önce bana ulaşabilirsiniz. Belki de çözüm sadece doğru bir index'te saklıdır.

İlginizi Çekebilir

👨‍💻

Yazar

Doğan Aydın

Kıdemli Yazılım Mühendisi. Karmaşık problemleri basit çözümlere dönüştürmeyi sever. Mimar, mentör ve sürekli öğrenci.