Ana içeriğe geç
Kesit SQL
İleri · Sorgu Optimizasyonu

Tür dönüşümü ve fonksiyon tuzakları: indeks var ama kullanılmıyor

Sütunu sargan fonksiyon ve dönüşümler, sabiti dönüştürmek, ifade indeksi, yeniden yazarken sonucu bozmamak.

Uygulamada aç, 8 alıştırmayı çözÜcretsiz, hesap gerekmez, kurulum yok.

Bu sayfa dersin okuma sürümüdür. Metin bir kutuyu çalıştırmanı ya da adıma geçmeni söylüyorsa dersi uygulamada aç.

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

TuzakSorunDüzeltme
WHERE date(txn_time) = '2024-12-31'Tarih fonksiyonuYarı açık aralık
WHERE amount * -1 > 1000Sütunda aritmetikSabit 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 = 100019Etkisiz işlemaccount_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?

PostgreSQL çıktılarıBu derslerdeki PostgreSQL planları elle yazılmadı. Aynı veriyle PostgreSQL 16.4 üzerinde (PGlite, tarayıcıda çalışan WASM sürümü) üretildi. 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 ServerKarşı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.
MySQLBelgeye 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.
PostgreSQLSayı 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.
SQLiteSü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

Alıştırmalara geç (8)