Senaryo: "İndeks var ama sorgu yine tarıyor"
Bir mühendis txn_time üzerine indeks eklemiş, rapor hızlanmamış. Sebep çoğu zaman indeksin kendisi değil, koşulun yazımıdır. İndeks sütunun ham değerlerine göre sıralıdır. Sütunu bir ifadenin içine koyarsan motor o sıradan yararlanamaz.
CREATE INDEX ix_txn_time ON bank_txns (txn_time); EXPLAIN QUERY PLAN SELECT COUNT(*) FROM bank_txns WHERE date(txn_time) = '2024-12-31'; EXPLAIN QUERY PLAN SELECT COUNT(*) FROM bank_txns WHERE txn_time >= '2024-12-31' AND txn_time < '2025-01-01';
İlkinde SCAN bank_txns USING COVERING INDEX görürsün: indeks baştan sona okunuyor, arama yok. "Index" sözcüğü seni yanıltmasın, önemli olan SCAN ve SEARCH ayrımıdır.
Sık görülen biçimler
| Tuzak | Sorun | Düzeltme |
|---|---|---|
WHERE date(txn_time) = '2024-12-31' | Tarih fonksiyonu | Yarı açık aralık |
WHERE amount * -1 > 1000 | Sütunda aritmetik | Sabit tarafa taşı: amount < -1000 |
WHERE COALESCE(merchant_category, '') = 'market' | NULL dönüşümü | merchant_category = 'market' (NULL zaten eşleşmez) |
WHERE CAST(account_id AS TEXT) = '100019' | Sütunda tür dönüşümü | account_id = 100019 |
WHERE account_id + 0 = 100019 | Etkisiz işlem | account_id = 100019 |
WHERE LOWER(channel) = 'atm' | Harf dönüşümü | Veriyi standart tut ya da ifade indeksi kur |
Gerçek çıktı: dönüşüm hangi tarafta?
VACUUM ANALYZE çalıştırıldı, planlama satırları kısalık için çıkarıldı. Sayılar kendi sunucunda farklı olur; okuma biçimi aynıdır.Sabit sütunun türüne uyunca:
EXPLAIN (ANALYZE, BUFFERS, TIMING OFF, SUMMARY OFF) SELECT COUNT(*) FROM bank_txns WHERE account_id = 100019;
Gerçek çıktı
Aggregate (cost=5.27..5.28 rows=1 width=8) (actual rows=1 loops=1)
Buffers: shared hit=3
-> Index Only Scan using bank_txns_account_id_idx on bank_txns (cost=0.29..5.15 rows=49 width=0) (actual rows=72 loops=1)
Index Cond: (account_id = 100019)
Heap Fetches: 0
Buffers: shared hit=3
Sütun metne dönüştürülünce aynı indeksin tamamı okunuyor, koşul Filter satırına düşüyor:
EXPLAIN (ANALYZE, BUFFERS, TIMING OFF, SUMMARY OFF) SELECT COUNT(*) FROM bank_txns WHERE account_id::text = '100019';
Gerçek çıktı
Aggregate (cost=1108.21..1108.22 rows=1 width=8) (actual rows=1 loops=1)
Buffers: shared hit=39
-> Index Only Scan using bank_txns_account_id_idx on bank_txns (cost=0.29..1107.68 rows=211 width=0) (actual rows=72 loops=1)
Filter: ((account_id)::text = '100019'::text)
Rows Removed by Filter: 42212
Heap Fetches: 0
Buffers: shared hit=39
Örtük dönüşüm (implicit conversion)
Dönüşümü sen yazmasan da motor yapabilir. SQL türleri burada çok farklıdır:
| SQL türü | Davranış |
|---|---|
| SQL Server | Karşılaştırmada veri türü önceliği (data type precedence) kuralı geçerlidir: düşük öncelikli taraf yüksek olana çevrilir. nvarchar, varchar'dan yüksektir. varchar sütunu N'...' ya da nvarchar parametreyle karşılaştırılırsa sütun çevrilir, arama yapılamayabilir. Planda CONVERT_IMPLICIT olarak görünür. Uygulama sürücüsünün metin parametrelerini nvarchar gönderip göndermediğine bak. |
| MySQL | Belgeye göre metin sütunu sayıyla karşılaştırılırsa o sütundaki indeks arama için kullanılamaz, çünkü farklı metinler aynı sayıya dönüşebilir ('1', ' 1', '1a'). Sabiti tırnak içinde yaz. |
| PostgreSQL | Sayı ve metin arasında örtük karşılaştırma yoktur, hata verir. Tırnaklı sabit ise sütunun türüne uyarlanır, indeks kullanılır. |
| SQLite | Sütun yakınlığı (type affinity) sabite uygulanır: account_id = '100019' yine arama yapar. Sütuna dönüşüm yazarsan yapamaz. |
| BigQuery | İndeks yerine bölüm kullanılır. Bölüm sütununu yalnız bırak: belgedeki desteklenen fonksiyonlar dışında bölüm eleme yapılmaz, tablonun tamamı taranır. |
Fonksiyon şartsa: ifade indeksi
Bazen sorguyu değiştiremezsin (hazır rapor aracı, ORM). O zaman indeksi ifadeye kurarsın. Sorgudaki ifade, indeksteki ifadeyle aynı yazılmalıdır.
CREATE INDEX ix_gun ON bank_txns (date(txn_time)); EXPLAIN QUERY PLAN SELECT COUNT(*) FROM bank_txns WHERE date(txn_time) = '2024-12-31';
CREATE INDEX ix_gun ON bank_txns ((txn_time::date)); EXPLAIN (ANALYZE, BUFFERS, TIMING OFF, SUMMARY OFF) SELECT COUNT(*) FROM bank_txns WHERE txn_time::date = '2024-11-01';
Gerçek çıktı
Aggregate (cost=10.35..10.36 rows=1 width=8) (actual rows=1 loops=1)
Buffers: shared hit=4
-> Index Scan using ix_gun on bank_txns (cost=0.29..10.09 rows=103 width=0) (actual rows=120 loops=1)
Index Cond: ((txn_time)::date = '2024-11-01'::date)
Buffers: shared hit=4
SQL Server'da aynı iş hesaplanmış sütun (computed column) üzerine indeksle, Oracle'da fonksiyon tabanlı indeksle yapılır. Her ifade indeksi bir indeks daha demektir: yazma bedeli aynen ödenir.
Yeniden yazarken sonucu bozma
- Tarih sınırı:
txn_time BETWEEN '2024-11-01' AND '2024-11-30', saatli sütunda 30 Kasım'ın saatli kayıtlarını kaçırır. Yarı açık aralık kullan. - NULL:
WHERE COALESCE(stok, 0) < 20NULL olanı da alır;stok < 20almaz. Eşdeğeristok < 20 OR stok IS NULLolur. - Negatif çarpım:
amount * -1 > 1000yeniden yazılırken eşitsizlik yön değiştirir:amount < -1000. - LIKE ve harf: SQLite varsayılan ayarda
LIKEharf duyarsız çalıştığı için normal indeksten yararlanamaz.name LIKE 'Ko%'diğer motorlarda aramaya dönebilse de buradaSCANgörürsün.LIKE '%ko'ise normal B-tree indeksle aramaya dönüşmez.