Geçen hafta, üretimde 40 saniye süren bir sorguyu incelerken geliştirme ortamında 120 milisaniye aldığını öğrendik. Aynı sorgu, aynı SQL, farklı evren. Farkın sebebi indeks değildi; indekslerin yanlış sıralamasıydı. Bu yazı o günkü notlardan.

B-Tree, Matematiksel Garantisi

İlişkisel veritabanlarının çoğu, indeks için B-tree denen dengeli bir ağaç kullanır. Güzelliği matematiğinde: derinlik, kayıt sayısına logaritmik büyür. On milyon satırlı bir tabloda üç-dört seviyeye iner; yani indeksle bir kayda ulaşmak birkaç disk erişimi demektir. İndekssiz, yani tablo taraması (sequential scan) ile on milyon satırın hepsi okunur.

Asıl mesele genelde indeksin olmaması değil, sorgunun onu kullanamamasıdır. Klasik tuzaklar:

  • WHERE UPPER(email) = 'X' — fonksiyon sarmalanmış kolon, indeksi görmez.
  • WHERE created_at::date = '2026-08-26' — cast, aynı tuzak.
  • WHERE LEFT(isim, 3) = 'Ara' — soldan kesme, tüm satırı taratır.

Composite İndekste Sıralama Bir Kıyamettir

Çok kolonlu (composite) indeks bir telefon rehberi gibi çalışır: önce soyada, sonra ada göre sıralıdır. Soyadını bilmeden ada ararsanız, rehber size yardımcı olmaz. Aynı kural sorguda geçerli:

-- İndeks: (tenant_id, durum, created_at)
-- Çalışır: her üç kolon soldan doğru sırayla
SELECT * FROM siparisler
WHERE tenant_id = 7 AND durum = 'acik'
  AND created_at > '2026-08-01';

-- Çalışmaz: tenant_id yoksa indeks boşa gider
SELECT * FROM siparisler
WHERE durum = 'acik' AND created_at > '2026-08-01';

Birinci sorgu üç kolonu da kullanır; ikincisi ortadaki kolonla başladığı için indeksi okuyamaz. Bu kurala "en soldaki önek" (leftmost prefix) denir. Üretimdeki 40 saniyelik sorgunun ilacı da buydu: sorgu, (tenant_id, durum, created_at) sıralamasını bozuyordu.

Bir kolonun hem filtrede hem sıralamada (ORDER BY) geçtiği sorgularda indeks, sıralama işini de üstlenir; ORDER BY için ayrı bir sort adımı açılmaz. Ama sıralama kolonunun dizideki yeri değiştiğinde, indeksin sıralamadaki avantajı da buharlaşır.

EXPLAIN'i Okumak Bir Sanattır

Hangi sorgunun neden yavaşladığını kestirmek yok, ölçmek var. PostgreSQL'de:

EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM siparisler
WHERE tenant_id = 7 AND durum = 'acik';

Çıktıda aranacak satırlar şunlar: Seq Scan görüyorsanız indeks hiç kullanılmıyor; Index Scan varsa tamam; Bitmap Heap Scan görüyorsanız indeks kısmen kullanılıyor, satırların bir bölümü tablodan geri okunuyor. rows=... tahmini ile gerçek sayı arasındaki uçurum, istatistiklerin bayatladığını gösterir — ANALYZE siparisler; çalıştırmayı unutmayın.

BUFFERS satırlarını da ciddiye alın: shared hit sayısı verinin önbellekten geldiğini, read sayısı diskten okunduğunu gösterir. Aynı sorgu sabah hızlı, akşam yavaşsa ilk bakılacak yer burasıdır.

Sonuç. 40 saniyelik sorgu, tenant_id'yi başa alıp durum'u ortada bırakan tek satırlık bir indeks değişikliğiyle 80 milisaniyeye indi. Sorgu hızlandırma sihir değil, muhasebe: kaç satır taranıyor, hangi kolon filtrede, hangi kolon sıralamada? Üç soruya doğru cevap veren ekip, veritabanı danışmanına ödeme yapmaktan kurtulur.