---
title: 'Dizin Kullanımını Engelleyen Yazımlar'
source: 'https://academia.sh/tr/kurslar/ileri-sql/dizin-kullanimini-engelleyen-yazimlar'
course: 'İleri SQL'
language: tr
updated: '2026-08-17T18:08:55+00:00'
license: 'CC BY-SA 4.0'
---

# Dizin Kullanımını Engelleyen Yazımlar

Sütun üzerinde işlev kullanmanın dizini devre dışı bırakması, tarih süzmelerinin aralık koşuluna çevrilmesi, sol ucu açık kalıp eşlemenin sınırı, karşılaştırma kuralının dizinle uyuşması ve işlevden vazgeçilemediğinde ifade dizini.

Önceki iki derste dizin vardı ve kullanıldı. Uygulamada sık karşılaşılan durum bunun
tersidir: dizin vardır, sorgu tam da o sütunu süzer, plan yine `SCAN` der ve süre
düşmez.

Bunun nedeni çoğunlukla dizinde ya da veride değil, koşulun yazımındadır. Dizin belli bir
sütunun **saklandığı biçimdeki** değerlerini sıralı tutar. Koşul o değerin kendisini değil,
ondan hesaplanmış bir başka değeri sorduğunda dizin işe yaramaz hale gelir. Bu ders bu
yazımları ve aynı sonucu veren eşdeğerlerini ele alır.

## Veri Kümesi

```sh
rm -f kutuphane.db
cat > kurulum.sql <<'SQL'
CREATE TABLE sube (
  sube_id INTEGER PRIMARY KEY,
  ad      TEXT NOT NULL,
  sehir   TEXT NOT NULL
);
CREATE TABLE uye (
  uye_id       INTEGER PRIMARY KEY,
  ad           TEXT NOT NULL,
  sehir        TEXT NOT NULL,
  kayit_tarihi TEXT NOT NULL
);
CREATE TABLE kitap (
  kitap_id INTEGER PRIMARY KEY,
  baslik   TEXT NOT NULL,
  yazar    TEXT NOT NULL,
  yil      INTEGER NOT NULL,
  sube_id  INTEGER NOT NULL
);
CREATE TABLE odunc (
  odunc_id    INTEGER PRIMARY KEY,
  kitap_id    INTEGER NOT NULL,
  uye_id      INTEGER NOT NULL,
  alis_tarihi TEXT NOT NULL,
  iade_tarihi TEXT
);

INSERT INTO sube (sube_id, ad, sehir) VALUES
  (1,'Merkez','Ankara'),(2,'Bahcelievler','Ankara'),(3,'Kadikoy','Istanbul'),
  (4,'Beyoglu','Istanbul'),(5,'Konak','Izmir'),(6,'Nilufer','Bursa'),
  (7,'Selcuklu','Konya'),(8,'Cankaya','Ankara');

INSERT INTO uye (uye_id, ad, sehir, kayit_tarihi)
WITH RECURSIVE sayac(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM sayac WHERE n < 120000)
SELECT n, 'Uye ' || n,
       CASE n % 5 WHEN 0 THEN 'Ankara' WHEN 1 THEN 'Istanbul' WHEN 2 THEN 'Izmir'
                  WHEN 3 THEN 'Bursa' ELSE 'Konya' END,
       date('2015-01-01', '+' || (n % 3200) || ' days')
FROM sayac;

INSERT INTO kitap (kitap_id, baslik, yazar, yil, sube_id)
WITH RECURSIVE sayac(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM sayac WHERE n < 200000)
SELECT n, 'Kitap ' || n, 'Yazar ' || (n % 4000), 1950 + (n % 75), 1 + (n % 8)
FROM sayac;

INSERT INTO odunc (odunc_id, kitap_id, uye_id, alis_tarihi, iade_tarihi)
WITH RECURSIVE sayac(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM sayac WHERE n < 2000000)
SELECT n, 1 + ((n * 7) % 200000), 1 + ((n * 13) % 120000),
       date('2018-01-01', '+' || ((n * 37) % 2437) || ' days'),
       CASE WHEN n % 9 = 0 THEN NULL
            ELSE date('2018-01-01', '+' || (((n * 37) % 2437) + 14) || ' days') END
FROM sayac;
SQL
sqlite3 kutuphane.db < kurulum.sql
```

## Sütunun Üzerine Uygulanan İşlev

İki sütunda dizin var. Aynı iki soru, biri sütuna işlev uygulayarak, diğeri uygulamadan
soruluyor.

```sh
sqlite3 kutuphane.db <<'SQL'
CREATE INDEX kitap_yazar ON kitap(yazar);
CREATE INDEX odunc_uye  ON odunc(uye_id);
EXPLAIN QUERY PLAN SELECT count(*) FROM kitap WHERE upper(yazar) = 'YAZAR 7';
EXPLAIN QUERY PLAN SELECT count(*) FROM kitap WHERE yazar = 'Yazar 7';
EXPLAIN QUERY PLAN SELECT count(*) FROM odunc WHERE uye_id + 0 = 4242;
EXPLAIN QUERY PLAN SELECT count(*) FROM odunc WHERE uye_id = 4242;
.timer on
.stats vmstep
SELECT count(*) FROM kitap WHERE upper(yazar) = 'YAZAR 7';
SELECT count(*) FROM kitap WHERE yazar = 'Yazar 7';
SELECT count(*) FROM odunc WHERE uye_id + 0 = 4242;
SELECT count(*) FROM odunc WHERE uye_id = 4242;
SQL
```

```
QUERY PLAN
`--SCAN kitap USING COVERING INDEX kitap_yazar
QUERY PLAN
`--SEARCH kitap USING COVERING INDEX kitap_yazar (yazar=?)
QUERY PLAN
`--SCAN odunc USING COVERING INDEX odunc_uye
QUERY PLAN
`--SEARCH odunc USING COVERING INDEX odunc_uye (uye_id=?)
50
VM-steps: 800061
Run Time: real 0.013 user 0.012658 sys 0.000530
50
VM-steps: 161
Run Time: real 0.000 user 0.000019 sys 0.000018
17
VM-steps: 8000029
Run Time: real 0.029 user 0.026334 sys 0.002302
17
VM-steps: 62
Run Time: real 0.000 user 0.000027 sys 0.000018
```

Dört planın ikisi `SCAN`, ikisi `SEARCH`. Sonuçlar aynı: elli ve on yedi. Adım sayıları
sırasıyla 800.061'e karşı 161 ve 8.000.029'a karşı 62.

Nedeni tek cümleyle söylenebilir: dizin `yazar` değerlerini sıralı tutar, `upper(yazar)`
değerlerini değil. Veritabanı `upper(yazar)` için sıralı bir yapıya sahip olmadığından
işlevi her satır için ayrı ayrı hesaplamak ve sonucu karşılaştırmak zorunda kalır. Aynı
şey aritmetik için de geçerlidir: `uye_id + 0` bir dizin anahtarı değildir. `+ 0` hiçbir
şey değiştirmiyormuş gibi görünse de dizinin devre dışı kalması için yeterlidir.

## Aynı Hatanın Tarih Biçimi

Bu yazımın en sık görülen biçimi tarih süzmeleridir: bir yılın kayıtları istendiğinde
tarihten yıl çıkarmak doğal görünür.

```sh
sqlite3 kutuphane.db <<'SQL'
CREATE INDEX odunc_alis ON odunc(alis_tarihi);
EXPLAIN QUERY PLAN SELECT count(*) FROM odunc WHERE strftime('%Y', alis_tarihi) = '2019';
EXPLAIN QUERY PLAN
SELECT count(*) FROM odunc
WHERE alis_tarihi >= '2019-01-01' AND alis_tarihi < '2020-01-01';
.timer on
.stats vmstep
SELECT count(*) FROM odunc WHERE strftime('%Y', alis_tarihi) = '2019';
SELECT count(*) FROM odunc
WHERE alis_tarihi >= '2019-01-01' AND alis_tarihi < '2020-01-01';
SQL
```

```
QUERY PLAN
`--SCAN odunc USING COVERING INDEX odunc_alis
QUERY PLAN
`--SEARCH odunc USING COVERING INDEX odunc_alis (alis_tarihi>? AND alis_tarihi<?)
299551
VM-steps: 8299563
Run Time: real 0.213 user 0.208346 sys 0.004492
299551
VM-steps: 898666
Run Time: real 0.005 user 0.004441 sys 0.000663
```

İki sorgu da 299.551 döndürüyor: yeniden yazım eşdeğerdir. Adım sayısı dokuz kat, süre bu
ortamda kırk kattan fazla düştü.

Yeniden yazımın mantığı şudur: bir yılın kayıtları, sıralı bir tarih dizininde kesintisiz
bir aralıktır. Aralığın alt ucu kapalı, üst ucu açık yazılır — `< '2020-01-01'` biçimi,
`<= '2019-12-31'` yazımının aksine gün içindeki saat bilgisi taşıyan değerleri de doğru
kapsar. Aynı dönüşüm ay, hafta ve gün süzmeleri için de kurulabilir; her durumda işlev
koşulun sağ tarafına, sabit değerlerin içine taşınır.

## Kalıp Eşlemede Sol Uç

Sıralı bir yapıda önekle arama yapılabilir, sonekle yapılamaz.

```sh
sqlite3 kutuphane.db <<'SQL'
CREATE INDEX kitap_baslik ON kitap(baslik);
EXPLAIN QUERY PLAN SELECT count(*) FROM kitap WHERE baslik LIKE 'Kitap 1234%';
CREATE INDEX kitap_baslik_nc ON kitap(baslik COLLATE NOCASE);
EXPLAIN QUERY PLAN SELECT count(*) FROM kitap WHERE baslik LIKE 'Kitap 1234%';
EXPLAIN QUERY PLAN SELECT count(*) FROM kitap WHERE baslik LIKE '%1234';
.timer on
.stats vmstep
SELECT count(*) FROM kitap WHERE baslik LIKE 'Kitap 1234%';
SELECT count(*) FROM kitap WHERE baslik LIKE '%1234';
SQL
```

```
QUERY PLAN
`--SCAN kitap USING COVERING INDEX kitap_baslik
QUERY PLAN
`--SEARCH kitap USING COVERING INDEX kitap_baslik_nc (baslik>? AND baslik<?)
QUERY PLAN
`--SCAN kitap USING COVERING INDEX kitap_baslik_nc
111
VM-steps: 463
Run Time: real 0.000 user 0.000015 sys 0.000014
20
VM-steps: 800031
Run Time: real 0.007 user 0.006536 sys 0.000030
```

Çıktı iki ayrı şey söylüyor. Birincisi: `LIKE '%1234'` koşulu, hangi dizin varsa olsun
taramayla karşılanıyor. Sol ucu açık bir kalıp için sıralamadan yararlanılamaz; sözlükte
"ile biten" kelimeleri bulmanın yolu sözlüğü baştan sona okumaktır. Bu durumda çözüm
sorguyu yeniden yazmak değil, yapıyı değiştirmektir — tam metin dizini ya da sütunun ters
çevrilmiş bir kopyası üzerinde önek araması.

İkincisi daha incedir: önekli kalıp ilk dizinle de çalışmadı, ancak `COLLATE NOCASE`
tanımlı ikinci dizinle çalıştı. Nedeni, karşılaştırma kuralının uyuşmamasıdır. Bu motorda
`LIKE` varsayılan olarak büyük–küçük harf ayrımı yapmaz, birinci dizin ise ayrım yapan bir
karşılaştırmayla sıralanmıştır; iki sıra düzeni aynı olmadığı için dizin kullanılamaz.
Kural motordan bağımsızdır: **dizinin sıralama kuralı ile koşulun karşılaştırma kuralı
aynı olmalıdır.** Harf durumu, dil duyarlı sıralama ve boşluk davranışı bu kuralın
kapsamına girer.

## İşlevden Vazgeçilemiyorsa

Bazı durumlarda hesaplanmış değerin kendisi aranmak zorundadır. Bunun karşılığı, hesabı
sorgudan çıkarmak değil, dizine koymaktır: **ifade dizini (expression index)**, bir
sütunun değerlerini değil, o sütundan hesaplanan ifadenin değerlerini sıralı tutar.

```sh
sqlite3 kutuphane.db <<'SQL'
CREATE INDEX kitap_yazar_buyuk ON kitap(upper(yazar));
EXPLAIN QUERY PLAN SELECT count(*) FROM kitap WHERE upper(yazar) = 'YAZAR 7';
.timer on
.stats vmstep
SELECT count(*) FROM kitap WHERE upper(yazar) = 'YAZAR 7';
SQL
```

```
QUERY PLAN
`--SEARCH kitap USING COVERING INDEX kitap_yazar_buyuk (<expr>=?)
50
VM-steps: 212
Run Time: real 0.000 user 0.000014 sys 0.000011
```

Aynı sorgu, hiç değiştirilmeden, 800.061 adımdan 212 adıma indi. Planda anahtar sütun adı
yerine `<expr>` yazması, dizinin bir sütunu değil bir ifadeyi anahtarladığını gösterir.

İfade dizininin iki koşulu vardır. İfade **belirlenimci** olmalıdır: aynı satır için her
zaman aynı değeri üretmelidir; şimdiki zamanı ya da oturum ayarını okuyan bir ifade
dizinlenemez, çünkü dizindeki değer yazıldığı anda eskir. İkincisi, sorgudaki ifade
dizindeki ifadeyle eşleşmelidir; `upper(yazar)` dizini `lower(yazar)` koşuluna yaramaz.

## Kuralın Genel Biçimi

Bu derste dört ayrı yazım incelendi; hepsi tek bir kurala indirgenebilir. Bir koşulun
dizinden yararlanabilmesi için **dizinlenmiş sütun karşılaştırmanın bir tarafında, üzerine
hiçbir hesap uygulanmadan** durmalıdır. Literatürde bu biçimdeki koşullara *sargable
predicate* denir.

Uygulamada bu, koşulu şu üç biçimden birine getirmek demektir: sütunu bir sabitle
karşılaştırmak, sütunu bir aralığa sokmak, ya da hesabı dizinin tanımına taşımak. Hesabın
sorgudan tamamen kaybolması gerekmez — koşulun sağ tarafında, sabit değerlerin üzerinde
kalabilir. `alis_tarihi >= date('2019-01-01', '-7 days')` koşulu dizini kullanır, çünkü
hesap sorgu başında bir kez yapılır ve sonucu sabit bir değerdir.

## Özet

- Dizin, sütunun saklandığı biçimdeki değerlerini sıralı tutar; sütun üzerine uygulanan
  her işlev ya da aritmetik bu sırayı kullanılamaz kılar ve plan `SCAN` olur.
- Bir yılın kayıtlarını isteyen `strftime` koşulu, aynı sonucu veren yarı açık aralık
  koşuluna çevrildiğinde bu ortamda dokuz kat az adım ve kırk kat az süre harcadı.
- Sol ucu açık kalıp eşleme hiçbir sıralı dizinden yararlanamaz; çözüm sorguyu yeniden
  yazmak değil, farklı bir dizin yapısı kurmaktır.
- Dizinin sıralama kuralı ile koşulun karşılaştırma kuralı aynı olmalıdır; harf durumu
  farkı tek başına dizini devre dışı bırakabilir.
- Hesaplanmış değerin aranması zorunluysa ifade dizini kullanılır; ifade belirlenimci
  olmalı ve sorgudaki yazımıyla birebir eşleşmelidir.

## Sonraki Adım

Buraya kadarki bütün ölçümler tek tablo üzerindeydi ve karar tekti: dizin mi, tarama mı.
Birden çok tablo birleştiğinde karar sayısı artar — hangi tablonun önce okunacağı, her
adımda hangi erişim yolunun kullanılacağı ve satırların hangi yöntemle eşleştirileceği
ayrı ayrı seçilir. Sonraki ders birleştirme sırasının ve algoritmasının plandaki
karşılığını ve ölçülen etkisini ele alır.
