---
title: 'İleri SQL'
source: 'https://academia.sh/tr/kurslar/ileri-sql'
language: tr
updated: '2026-08-17T04:40:18+00:00'
license: 'CC BY-SA 4.0'
lessons: 20
outcomes:
    - 'Alt sorgu, ortak tablo ifadesi ve pencere işlevlerini yerinde kullanır'
    - 'İşlem ve yalıtım düzeylerini davranışlarıyla açıklar'
    - 'Sorgu planını okuyup sorgu yeniden yazımıyla iyileştirir'
---

# İleri SQL

Alt sorgu, ortak tablo ifadesi, özyinelemeli sorgu ve pencere işlevleriyle tek sorguda çok adımlı hesap kurmayı; işlem, yalıtım ve kilitleme davranışını; sorgu planını okuyup sorguyu yeniden yazmayı öğreten orta düzey kurs.

## Bileşik Sorgular

Skaler, satır ve tablo alt sorguları, ilişkili alt sorgunun satır başına maliyeti, ortak tablo ifadeleri, özyinelemeli sorgularla hiyerarşi gezinme, pencere ve sıralama işlevleri ile döndürme işlemleri.

### Alt Sorgular

Source: https://academia.sh/tr/kurslar/ileri-sql/alt-sorgular

Skaler, satır ve tablo alt sorgularının yazıldığı yerler, türetilmiş tablolar, IN ile EXISTS ayrımı ve boş değerlerin alt sorgu sonucunu sessizce boşaltması.

SQL Temelleri kursu, tek bir sonuç kümesi üreten deyimleri kurdu: satır seçmek, tabloları
birleştirmek, gruplayıp özetlemek. Bu deyimlerin ortak yanı, hesabın **tek adımda**
bitmesidir. Oysa gerçek sorular çoğu zaman iki adımlıdır: "ortalamanın üstünde ödünç alan
üyeler" sorusu, önce ortalamayı, sonra ondan büyük olanları ister.

Bu tür soruları iki ayrı sorguyla çözüp sonucu elde birleştirmek mümkündür, ancak veri
sorgular arasında değişebilir ve ara sonucun taşınması gereksiz iştir. SQL'in yanıtı, bir
sorgunun içine başka bir sorgu yerleştirmektir. Önceki kursta bu yerleştirme yalnız koşul
yazarken kullanıldı; burada kendi başına bir yapı olarak ele alınır. Bu ders, yerleştirmenin
biçimlerini ve her birinin nereye yazılabildiğini kurar.

#### Alt Sorgu ve Döndürdüğü Şekil

**Alt sorgu (subquery)**, başka bir deyimin içine parantezle gömülmüş bir `SELECT`
deyimidir. Nereye yazılabileceğini belirleyen tek şey, **döndürdüğü şekildir**:

| Şekil | Ne döndürür | Nerede kullanılır |
|---|---|---|
| Skaler alt sorgu | Tek satır, tek sütun | Bir değerin geçebildiği her yer |
| Satır alt sorgusu | Tek satır, çok sütun | Satır karşılaştırmalarında |
| Tablo alt sorgusu | Çok satır, çok sütun | `FROM`, `IN`, `EXISTS` |

Bu üçlü, dersin iskeletidir. Şekil uymadığında motor hata verir; şekil uyduğu hâlde
beklenmedik sonuç çıkıyorsa neredeyse her zaman sorumlu boş değerlerdir — dersin son
bölümü bunu ele alır.

Kurs boyunca aynı örnek üzerinde çalışılacak: bir kütüphanenin ödünç kayıtları. Dört tablo
vardır — `sube` (şubeler), `uye` (üyeler), `kitap` (kitaplar) ve `odunc` (ödünç işlemleri).
Her kod bloğu ihtiyaç duyduğu tabloları kendi içinde kurar; blok olduğu gibi kopyalanıp
çalıştırıldığında yazılan çıktıyı üretir.

#### Skaler Alt Sorgu

Skaler alt sorgu tek bir değer üretir; bu yüzden bir sabitin ya da sütun adının
yazılabildiği her yere yazılabilir. En sık kullanıldığı yer, karşılaştırmanın sağ
tarafıdır.

"Üye başına düşen ortalama ödünç sayısından fazla kitap alan üyeler" sorusu, `HAVING`
içinde bir skaler alt sorguyla ifade edilir:

```bash
sqlite3 -box -header <<'SQL'
CREATE TABLE uye(id INTEGER PRIMARY KEY, ad TEXT, sube_id INT);
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT);
INSERT INTO uye VALUES (1,'Ayse',4),(2,'Burak',4),(3,'Ceren',5),(4,'Deniz',5),
                       (5,'Emre',6),(6,'Fatma',6),(7,'Gokhan',4),(8,'Hale',5);
INSERT INTO odunc VALUES
  (1,1,1,'2024-03-01','2024-03-15'),(2,3,1,'2024-03-01','2024-03-20'),
  (3,5,1,'2024-03-04','2024-03-18'),(4,7,1,'2024-03-11',NULL),
  (5,9,1,'2024-03-18','2024-03-29'),(6,2,2,'2024-03-01','2024-03-12'),
  (7,4,2,'2024-03-06','2024-03-25'),(8,6,2,'2024-03-11','2024-03-19'),
  (9,8,2,'2024-03-21',NULL),(10,1,3,'2024-03-04','2024-03-10'),
  (11,3,3,'2024-03-06','2024-03-27'),(12,10,3,'2024-03-13','2024-03-22'),
  (13,2,3,'2024-03-25',NULL),(14,5,4,'2024-03-04','2024-03-09'),
  (15,7,4,'2024-03-13','2024-03-26'),(16,4,4,'2024-03-20',NULL),
  (17,6,5,'2024-03-06','2024-03-14'),(18,9,5,'2024-03-11','2024-03-23'),
  (19,1,5,'2024-03-25','2024-03-28'),(20,8,6,'2024-03-04','2024-03-17'),
  (21,10,6,'2024-03-13','2024-03-21'),(22,3,6,'2024-03-20',NULL),
  (23,2,7,'2024-03-11','2024-03-16'),(24,5,7,'2024-03-18','2024-03-24'),
  (25,4,8,'2024-03-13','2024-03-24');

SELECT u.ad, COUNT(*) AS adet
FROM odunc o JOIN uye u ON u.id = o.uye_id
GROUP BY u.id, u.ad
HAVING COUNT(*) > (SELECT COUNT(*) * 1.0 / COUNT(DISTINCT uye_id) FROM odunc)
ORDER BY adet DESC, u.ad;
SQL
```

```text
┌───────┬──────┐
│  ad   │ adet │
├───────┼──────┤
│ Ayse  │ 5    │
│ Burak │ 4    │
│ Ceren │ 4    │
└───────┴──────┘
```

Alt sorgu, dış sorgunun hiçbir sütununa başvurmaz. Bu bağımsızlık iki sonuç doğurur:
motor onu **bir kez** hesaplar ve sonucu sabit gibi kullanır; ayrıca alt sorgu tek başına
çalıştırılıp doğrulanabilir. Alt sorgu ile dış sorgu arasında bağ kurulduğunda tablo
değişir — sonraki dersin konusu budur.

Skaler alt sorgunun sözleşmesi katıdır: **en fazla bir satır** döndürmelidir. Sıfır satır
döndürürse sonuç boş değerdir; birden çok satır döndürürse standart bir hata gerektirir.
Bu yüzden skaler alt sorgular ya bir toplama işlevi içerir ya da anahtara göre süzülür.

#### Satır Alt Sorgusu

Karşılaştırma yalnız tek değerler arasında yapılmak zorunda değildir. Standart SQL, satır
değerlerini de karşılaştırır: soldaki parantezli sütun listesiyle sağdaki alt sorgunun
döndürdüğü satır, sütun sütun eşlenir.

```bash
sqlite3 -box -header <<'SQL'
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT);
INSERT INTO odunc VALUES
  (2,3,1,'2024-03-01','2024-03-20'),(11,3,3,'2024-03-06','2024-03-27'),
  (22,3,6,'2024-03-20',NULL),(3,5,1,'2024-03-04','2024-03-18');
SELECT id, kitap_id, uye_id, alis
FROM odunc
WHERE (kitap_id, alis) = (SELECT kitap_id, MIN(alis) FROM odunc WHERE kitap_id = 3);
SQL
```

```text
┌────┬──────────┬────────┬────────────┐
│ id │ kitap_id │ uye_id │    alis    │
├────┼──────────┼────────┼────────────┤
│ 2  │ 3        │ 1      │ 2024-03-01 │
└────┴──────────┴────────┴────────────┘
```

Satır karşılaştırması, "şu kitabın **ilk** ödünç kaydı" gibi iki koşulu birlikte tutan
soruları tek ifadeyle yazar. Aynı sonuç iki ayrı skaler alt sorguyla da elde edilebilir,
ancak o yazımda iki alt sorgunun aynı satırdan geldiği görsel olarak kaybolur.

Satır değeri karşılaştırması standarttadır, fakat desteklenme derecesi motora göre
değişir; sıralama işleçleriyle (`<`, `>`) birleştirildiğinde daha da değişir. Taşınabilir
kod yazılıyorsa hedef motorun desteğini doğrulamak gerekir.

#### Tablo Alt Sorgusu ve Türetilmiş Tablolar

Çok satır döndüren bir alt sorgu, bir tablo gibi davranır. İki kullanımı vardır.

Birincisi, `FROM` içinde **türetilmiş tablo (derived table)** olarak yer almasıdır. Bu,
gruplamanın sonucunu yeniden gruplamak gibi iki katmanlı hesapların standart yoludur:

```bash
sqlite3 -box -header <<'SQL'
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT);
INSERT INTO odunc VALUES
  (1,1,1,'2024-03-01','2024-03-15'),(2,3,1,'2024-03-01','2024-03-20'),
  (3,5,1,'2024-03-04','2024-03-18'),(4,7,1,'2024-03-11',NULL),
  (5,9,1,'2024-03-18','2024-03-29'),(6,2,2,'2024-03-01','2024-03-12'),
  (7,4,2,'2024-03-06','2024-03-25'),(8,6,2,'2024-03-11','2024-03-19'),
  (9,8,2,'2024-03-21',NULL),(10,1,3,'2024-03-04','2024-03-10'),
  (11,3,3,'2024-03-06','2024-03-27'),(12,10,3,'2024-03-13','2024-03-22'),
  (13,2,3,'2024-03-25',NULL),(14,5,4,'2024-03-04','2024-03-09'),
  (15,7,4,'2024-03-13','2024-03-26'),(16,4,4,'2024-03-20',NULL),
  (17,6,5,'2024-03-06','2024-03-14'),(18,9,5,'2024-03-11','2024-03-23'),
  (19,1,5,'2024-03-25','2024-03-28'),(20,8,6,'2024-03-04','2024-03-17'),
  (21,10,6,'2024-03-13','2024-03-21'),(22,3,6,'2024-03-20',NULL),
  (23,2,7,'2024-03-11','2024-03-16'),(24,5,7,'2024-03-18','2024-03-24'),
  (25,4,8,'2024-03-13','2024-03-24');

SELECT MIN(adet) AS en_az, MAX(adet) AS en_cok, ROUND(AVG(adet), 2) AS ortalama
FROM (SELECT uye_id, COUNT(*) AS adet FROM odunc GROUP BY uye_id) AS sayim;
SQL
```

```text
┌───────┬────────┬──────────┐
│ en_az │ en_cok │ ortalama │
├───────┼────────┼──────────┤
│ 1     │ 5      │ 3.13     │
└───────┴────────┴──────────┘
```

Toplama işlevi doğrudan iç içe yazılamaz; `AVG(COUNT(*))` geçersizdir. Türetilmiş tablo,
iç hesabı bir tabloya dönüştürerek dış hesabın önünü açar. Türetilmiş tabloya **takma ad
verilmesi** zorunludur (burada `sayim`), çünkü dış sorgu ona bir adla başvurur.

İkinci kullanım, tablo alt sorgusunun bir **liste** olarak süzmede yer almasıdır:
`WHERE id IN (SELECT …)`. Bu yazım okunaklıdır; ancak olumsuzu olan `NOT IN`, listeye
boş değer karıştığında sessizce yanlış davranır.

#### Boş Değerin Listeyi Boşaltması

Kütüphanede kayıp bildirimleri ayrı bir tabloda tutulsun. Bir bildirim, hangi kitaba ait
olduğu henüz belirlenememişse `kitap_id` sütunu boş bırakılarak kaydedilir. "Kayıp
bildirimi olan kitaplar" ve "olmayan kitaplar" sorgularının ikisi de `IN` ile yazılabilir:

```bash
sqlite3 -box -header <<'SQL'
CREATE TABLE kitap(id INTEGER PRIMARY KEY, baslik TEXT, tur TEXT);
INSERT INTO kitap VALUES (1,'Kayip Zaman','roman'),(2,'Sessiz Ev','roman'),
                         (3,'Sayilar Kurami','bilim'),(4,'Evrenin Yapisi','bilim');
CREATE TABLE kayip(id INTEGER PRIMARY KEY, kitap_id INT, bildirim TEXT);
INSERT INTO kayip VALUES (1,2,'2024-03-08'),(2,NULL,'2024-03-19');

SELECT baslik FROM kitap WHERE id IN (SELECT kitap_id FROM kayip);
SELECT baslik FROM kitap WHERE id NOT IN (SELECT kitap_id FROM kayip);
SQL
```

```text
┌───────────┐
│  baslik   │
├───────────┤
│ Sessiz Ev │
└───────────┘
```

İlk sorgu beklendiği gibi bir satır verdi. İkinci sorgu **hiçbir satır** vermedi; bu
yüzden çıktıda yalnız bir tablo görünüyor. Oysa üç kitabın kayıp bildirimi yok.

Nedeni, İlişkisel Kuram kursunda tanıtılan üç değerli mantıktır. `x NOT IN (a, b)` ifadesi
`x <> a AND x <> b` demektir. Listede boş değer varsa o karşılaştırma doğru ya da yanlış
değil, **bilinmeyen** olur; bir çarpımın bilinmeyen çarpanı sonucu bilinmeyene çeker ve
bilinmeyen satır süzgeçten geçmez:

```bash
sqlite3 -box -header <<'SQL'
CREATE TABLE kitap(id INTEGER PRIMARY KEY, baslik TEXT, tur TEXT);
INSERT INTO kitap VALUES (1,'Kayip Zaman','roman'),(2,'Sessiz Ev','roman'),
                         (3,'Sayilar Kurami','bilim'),(4,'Evrenin Yapisi','bilim');
CREATE TABLE kayip(id INTEGER PRIMARY KEY, kitap_id INT, bildirim TEXT);
INSERT INTO kayip VALUES (1,2,'2024-03-08'),(2,NULL,'2024-03-19');

SELECT 3 IN (2, NULL) AS "3 IN (2,NULL)", 3 NOT IN (2, NULL) AS "3 NOT IN (2,NULL)";
SELECT baslik FROM kitap k
WHERE NOT EXISTS (SELECT 1 FROM kayip y WHERE y.kitap_id = k.id);
SQL
```

```text
┌───────────────┬───────────────────┐
│ 3 IN (2,NULL) │ 3 NOT IN (2,NULL) │
├───────────────┼───────────────────┤
│               │                   │
└───────────────┴───────────────────┘
┌────────────────┐
│     baslik     │
├────────────────┤
│ Kayip Zaman    │
│ Sayilar Kurami │
│ Evrenin Yapisi │
└────────────────┘
```

İlk satırdaki iki boş hücre, iki ifadenin de boş değer ürettiğini gösterir: ne doğru ne
yanlış. `EXISTS` ise farklı bir soruyu sorar — "eşleşen satır **var mı**" — ve yanıtı her
zaman doğru ya da yanlıştır; boş değer üretmez. Bu yüzden `NOT EXISTS` üç kitabı da
döndürdü.

Kural olarak: alt sorgunun döndürdüğü sütunda boş değer olabiliyorsa `NOT IN` yerine
`NOT EXISTS` yazılır ya da alt sorguya `WHERE kitap_id IS NOT NULL` koşulu eklenir.
Sütunun boş değer alamayacağı şemada güvenceye alınmışsa `NOT IN` güvenlidir.

#### Alt Sorgu Nereye Yazılabilir

Alt sorgunun yazılabildiği dört konum ve her birinin tipik işi şudur:

- `SELECT` listesinde: satır başına bir ek değer üretmek.
- `FROM` içinde: ara sonucu tablo hâline getirmek.
- `WHERE` içinde: satırları başka bir sorgunun sonucuna göre süzmek.
- `HAVING` içinde: grupları başka bir sorgunun sonucuna göre süzmek.

`SELECT` listesindeki alt sorgu okunaklı olsa da maliyeti gizler:

```bash
sqlite3 -box -header <<'SQL'
CREATE TABLE uye(id INTEGER PRIMARY KEY, ad TEXT, sube_id INT);
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT);
INSERT INTO uye VALUES (1,'Ayse',4),(2,'Burak',4),(3,'Ceren',5),(4,'Deniz',5);
INSERT INTO odunc VALUES
  (1,1,1,'2024-03-01','2024-03-15'),(2,3,1,'2024-03-01','2024-03-20'),
  (4,7,1,'2024-03-11',NULL),(6,2,2,'2024-03-01','2024-03-12'),
  (9,8,2,'2024-03-21',NULL),(10,1,3,'2024-03-04','2024-03-10');

SELECT u.ad,
       (SELECT COUNT(*) FROM odunc o WHERE o.uye_id = u.id) AS toplam,
       (SELECT MAX(alis) FROM odunc) AS son_islem_tarihi
FROM uye u
ORDER BY u.id;
SQL
```

```text
┌───────┬────────┬──────────────────┐
│  ad   │ toplam │ son_islem_tarihi │
├───────┼────────┼──────────────────┤
│ Ayse  │ 3      │ 2024-03-21       │
│ Burak │ 2      │ 2024-03-21       │
│ Ceren │ 1      │ 2024-03-21       │
│ Deniz │ 0      │ 2024-03-21       │
└───────┴────────┴──────────────────┘
```

Çıktıdaki iki sütun aynı görünse de yapıları farklıdır. Üçüncü sütundaki alt sorgu dış
sorguya bağlı değildir; tek kez hesaplanıp her satıra kopyalanır. İkinci sütundaki alt
sorgu ise `u.id` üzerinden dış satıra bağlıdır: her üye için ayrıca değerlendirilmesi
gerekir. Bu bağ, alt sorgunun maliyetini satır sayısıyla çarpar.

Ayrıca bu yazımın bir yan yararı görünür: hiç ödünç kaydı olmayan `Deniz` için sıfır
çıktı. İç birleştirmeyle yazılsaydı bu üye sonuçtan tümüyle düşerdi ve onu geri getirmek
için dış birleştirme gerekirdi; alt sorgu eşleşme yokken sıfır üretti.

#### Özet

- Alt sorgu, döndürdüğü şekle göre skaler, satır ve tablo alt sorgusu olarak ayrılır;
  şekil, nereye yazılabileceğini belirler.
- Skaler alt sorgu en fazla bir satır döndürmelidir; sıfır satır boş değer üretir.
- `FROM` içindeki türetilmiş tablo, toplama işlevlerini katmanlamanın standart yoludur ve
  takma ad almak zorundadır.
- Alt sorgunun sütununda boş değer olabiliyorsa `NOT IN` sessizce boş sonuç verir;
  `NOT EXISTS` üç değerli mantıktan etkilenmediği için doğru yanıtı verir.
- Dış sorgunun sütununa başvurmayan alt sorgu bir kez hesaplanır; başvuran alt sorgu satır
  başına değerlendirilir.

#### Sonraki Adım

Bu dersin son örneğinde iki alt sorgu yan yana duruyordu: biri tek kez, diğeri satır
başına hesaplanan. Aradaki fark yazımda tek bir sütun başvurusundan ibaretti, ancak iş
miktarındaki fark katsayıyla ölçülür. Sonraki ders, dış satıra bağlı olan bu ikinci
biçimi — ilişkili alt sorguyu — tanımlayacak, kaç kez değerlendirildiğini sayarak
ölçecek ve aynı sonucu üreten birleştirmeli yazımla karşılaştıracak.

### İlişkili Alt Sorgular

Source: https://academia.sh/tr/kurslar/ileri-sql/iliskili-alt-sorgular

Dış satıra bağlı alt sorgunun satır başına değerlendirilmesi, iş miktarının sayılarak ölçülmesi, birleştirmeli yeniden yazımla karşılaştırma ve EXISTS ile birleştirmenin çoğaltma farkı.

Önceki dersin son örneğinde iki alt sorgu yan yana duruyordu. Biri tablonun tamamına
bakıp tek bir değer üretiyordu; diğeri dış sorgunun `u.id` sütununa başvuruyordu. Yazımda
aradaki fark tek bir sütun adıydı, ancak bu başvuru alt sorgunun ne zaman
değerlendirileceğini değiştirir.

Dış sorgunun bir sütununa başvuran alt sorguya **ilişkili alt sorgu (correlated
subquery)** denir. Bu ders, ilişkinin ne anlama geldiğini, iş miktarını nasıl büyüttüğünü
ve aynı soruyu daha az işle soran yazımın nasıl kurulduğunu ele alır.

#### Bağın Anlamı

Bağımsız bir alt sorgu tek başına çalıştırılabilir; sonucu dış sorgudan bağımsızdır. Bu
yüzden motor onu bir kez hesaplayıp sabit gibi kullanabilir.

İlişkili alt sorgu tek başına çalıştırılamaz: içinde çözülemeyen bir ad vardır. Anlamı
"dış sorgu her satırı ürettiğinde, o satırın değerleriyle yeniden değerlendirilen sorgu"
biçiminde tanımlanır. Kavramsal değerlendirme sırası şudur:

1. Dış sorgu bir aday satır üretir.
2. Alt sorgudaki dış sütun başvuruları o satırın değerleriyle değiştirilir.
3. Alt sorgu çalıştırılır.
4. Sonuç, dış satırın süzülmesinde ya da bir sütununda kullanılır.

Bu tanım **anlamsaldır**: sorgunun ne ürettiğini belirler. Motorun bunu gerçekten satır
satır çalıştırıp çalıştırmadığı ayrı bir sorudur ve dersin sonunda ele alınır.

İlişkili alt sorgunun tipik işi, "her grubun kendi içindeki uç değeri" gibi grup başına
karşılaştırma gerektiren sorulardır. "Her üyenin en son aldığı kitap" sorusu böyledir:

```bash
sqlite3 -box -header <<'SQL'
CREATE TABLE uye(id INTEGER PRIMARY KEY, ad TEXT, sube_id INT);
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT);
INSERT INTO uye VALUES (1,'Ayse',4),(2,'Burak',4),(3,'Ceren',5),(4,'Deniz',5);
INSERT INTO odunc VALUES
  (1,1,1,'2024-03-01','2024-03-15'),(2,3,1,'2024-03-01','2024-03-20'),
  (4,7,1,'2024-03-11',NULL),(6,2,2,'2024-03-01','2024-03-12'),
  (9,8,2,'2024-03-21',NULL),(10,1,3,'2024-03-04','2024-03-10');

SELECT o.id, o.uye_id, o.alis
FROM odunc o
WHERE o.alis = (SELECT MAX(i.alis) FROM odunc i WHERE i.uye_id = o.uye_id)
ORDER BY o.uye_id;
SQL
```

```text
┌────┬────────┬────────────┐
│ id │ uye_id │    alis    │
├────┼────────┼────────────┤
│ 4  │ 1      │ 2024-03-11 │
│ 9  │ 2      │ 2024-03-21 │
│ 10 │ 3      │ 2024-03-04 │
└────┴────────┴────────────┘
```

İçteki `odunc` tablosuna `i`, dıştakine `o` takma adı verilmesi zorunludur: aynı tablo iki
farklı rolde geçtiği için `uye_id` sütununun hangisine ait olduğu ancak takma adla
belirlenir. Bağı kuran satır `WHERE i.uye_id = o.uye_id` koşuludur; `o` içeriden
görülebildiği için alt sorgu ilişkilidir.

#### İş Miktarını Ölçmek

Satır başına değerlendirmenin maliyeti, sayılarak gösterilebilir. Bunun için sorguya bir
sayaç yerleştirilir: her çağrıldığında bir değişkeni artıran ve argümanını olduğu gibi
döndüren `gecis` adlı bir kullanıcı işlevi. İşlev iç tablonun **koşuluna** konur; böylece
iç tablonun taranan her satırında bir kez çalışır ve toplam çağrı sayısı, iç tabloya kaç
satır dokunulduğunu verir.

Aynı sonucu üreten iki yazım karşılaştırılır: ilişkili alt sorgu ve iç tabloyu bir kez
gruplayıp birleştiren yazım.

```bash
cat > olcum.mjs <<'JS'
import { DatabaseSync } from 'node:sqlite';

const db = new DatabaseSync(':memory:');
db.exec(`
  CREATE TABLE uye(id INTEGER PRIMARY KEY, ad TEXT);
  CREATE TABLE odunc(id INTEGER PRIMARY KEY, uye_id INT, alis TEXT);
  INSERT INTO uye VALUES (1,'Ayse'),(2,'Burak'),(3,'Ceren'),(4,'Deniz'),
                         (5,'Emre'),(6,'Fatma'),(7,'Gokhan'),(8,'Hale');
  INSERT INTO odunc VALUES
    (1,1,'2024-03-01'),(2,1,'2024-03-01'),(3,1,'2024-03-04'),(4,1,'2024-03-11'),
    (5,1,'2024-03-18'),(6,2,'2024-03-01'),(7,2,'2024-03-06'),(8,2,'2024-03-11'),
    (9,2,'2024-03-21'),(10,3,'2024-03-04'),(11,3,'2024-03-06'),(12,3,'2024-03-13'),
    (13,3,'2024-03-25'),(14,4,'2024-03-04'),(15,4,'2024-03-13'),(16,4,'2024-03-20'),
    (17,5,'2024-03-06'),(18,5,'2024-03-11'),(19,5,'2024-03-25'),(20,6,'2024-03-04'),
    (21,6,'2024-03-13'),(22,6,'2024-03-20'),(23,7,'2024-03-11'),(24,7,'2024-03-18'),
    (25,8,'2024-03-13');
`);

let ziyaret = 0;
db.function('gecis', (deger) => { ziyaret += 1; return deger; });

ziyaret = 0;
const iliskili = db.prepare(`
  SELECT u.ad, (SELECT COUNT(*) FROM odunc o WHERE gecis(o.uye_id) = u.id) AS adet
  FROM uye u ORDER BY u.id`).all();
console.log('ilişkili   :', iliskili.map((s) => s.adet).join(','), '| ziyaret:', ziyaret);

ziyaret = 0;
const birlestirmeli = db.prepare(`
  SELECT u.ad, COALESCE(s.adet, 0) AS adet
  FROM uye u
  LEFT JOIN (SELECT uye_id, COUNT(*) AS adet FROM odunc
             WHERE gecis(uye_id) IS NOT NULL GROUP BY uye_id) AS s
    ON s.uye_id = u.id
  ORDER BY u.id`).all();
console.log('birleştirme:', birlestirmeli.map((s) => s.adet).join(','),
            '| ziyaret:', ziyaret);
JS
node olcum.mjs
```

```text
ilişkili   : 5,4,4,3,3,3,2,1 | ziyaret: 200
birleştirme: 5,4,4,3,3,3,2,1 | ziyaret: 25
```

İki yazım aynı sekiz sayıyı üretti. İç tabloya dokunma sayısı ise 200'e karşı 25.

Sayı şaşırtıcı değildir: dış tabloda 8, iç tabloda 25 satır var. İlişkili yazımda iç tablo
her dış satır için baştan sona taranır, yani $8 \times 25 = 200$. Birleştirmeli yazımda iç
tablo **bir kez** taranıp gruplanır, yani 25; ardından sekiz satırlık iki küçük küme
eşleştirilir.

Genel olarak, dış tabloda $m$, iç tabloda $n$ satır varsa ilişkili yazım $O(m \cdot n)$,
önce gruplayıp sonra birleştiren yazım $O(m + n)$ iş yapar. Üye sayısı ile işlem sayısının
birlikte on katına çıktığı bir kütüphanede, ilk yazımın işi yüz katına, ikincisinin işi on
katına çıkar.

#### Sorgu Planında Görmek

Ölçüm, motorun ne yaptığını dışarıdan gösterir. Motorun ne yapmayı planladığı ise sorgu
planından okunur. `EXPLAIN QUERY PLAN`, sorguyu çalıştırmadan planı yazdırır:

```bash
sqlite3 <<'SQL'
CREATE TABLE uye(id INTEGER PRIMARY KEY, ad TEXT, sube_id INT);
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT);
.print -- dizinsiz --
EXPLAIN QUERY PLAN
SELECT u.ad, (SELECT COUNT(*) FROM odunc o WHERE o.uye_id = u.id) AS adet
FROM uye u ORDER BY u.id;
CREATE INDEX odunc_uye ON odunc(uye_id);
.print
.print -- dizinli --
EXPLAIN QUERY PLAN
SELECT u.ad, (SELECT COUNT(*) FROM odunc o WHERE o.uye_id = u.id) AS adet
FROM uye u ORDER BY u.id;
SQL
```

```text
-- dizinsiz --
QUERY PLAN
|--SCAN u
`--CORRELATED SCALAR SUBQUERY 1
   `--SCAN o

-- dizinli --
QUERY PLAN
|--SCAN u
`--CORRELATED SCALAR SUBQUERY 1
   `--SEARCH o USING COVERING INDEX odunc_uye (uye_id=?)
```

Plandaki `CORRELATED SCALAR SUBQUERY` satırı, alt sorgunun ilişkili olduğunu ve tek değer
döndürdüğünü söyler. Altındaki satır iç tabloya nasıl erişildiğini verir: dizin yokken
`SCAN o` — yani baştan sona tarama — dizin varken `SEARCH o USING … INDEX` — yani doğrudan
erişim. Ölçülen 200 sayısı, ilk plandaki taramanın sekiz kez yinelenmesidir.

İkinci plan, ilişkili alt sorgunun kendiliğinden pahalı **olmadığını** gösterir: pahalı
olan, uygun erişim yolu olmadan yinelenen taramadır. Dizin varken her yineleme tam tarama
değil, tek bir arama olur ve toplam iş satır sayısıyla değil, eşleşme sayısıyla orantılı
hâle gelir. Planın nasıl okunacağı ve dizinlerin bu tabloyu nasıl değiştirdiği, kursun
Sorgu Başarımı konusunun asıl işidir.

#### Motorun Yeniden Yazma Hakkı

Bir sorgu ne yapılacağını söyler, nasıl yapılacağını değil. Planlayıcı, sonucu değiştirmemek
koşuluyla ilişkili alt sorguyu birleştirmeye ya da yarı birleştirmeye çevirebilir; iç
sonucu bir kez maddeleştirip yeniden kullanabilir. Bu dönüşümlere **alt sorgu azaltma**
denir ve motora göre kapsamı değişir.

Sonuç iki yönlüdür. Bir yandan, ilişkili yazımı gördüğü her yerde "bu yavaştır" demek
yanlıştır — motor kurtarmış olabilir. Öte yandan, kurtarmanın **garanti olmadığı** da
doğrudur: alt sorguda toplama işlevi, `LIMIT`, boş değer duyarlı bir koşul ya da yan
etkili bir işlev varsa dönüşüm anlamı bozacağı için yapılmaz. Ölçmeden varsayım
yürütmemek, bu yüzden bir alışkanlıktır.

#### EXISTS ile Birleştirmenin Farkı

İlişkili alt sorgunun ikinci yaygın biçimi, değer değil **varlık** soran `EXISTS`
yüklemidir. Aynı soru birleştirmeyle de yazılabilir, ancak ikisi eşdeğer değildir:

```bash
sqlite3 -box -header <<'SQL'
CREATE TABLE uye(id INTEGER PRIMARY KEY, ad TEXT, sube_id INT);
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT);
INSERT INTO uye VALUES (1,'Ayse',4),(2,'Burak',4),(3,'Ceren',5),(4,'Deniz',5);
INSERT INTO odunc VALUES
  (4,7,1,'2024-03-11',NULL),(9,8,2,'2024-03-21',NULL),
  (13,2,3,'2024-03-25',NULL),(16,4,3,'2024-03-20',NULL);

SELECT u.ad FROM uye u
WHERE EXISTS (SELECT 1 FROM odunc o WHERE o.uye_id = u.id AND o.iade IS NULL)
ORDER BY u.id;

SELECT u.ad FROM uye u
JOIN odunc o ON o.uye_id = u.id AND o.iade IS NULL
ORDER BY u.id;
SQL
```

```text
┌───────┐
│  ad   │
├───────┤
│ Ayse  │
│ Burak │
│ Ceren │
└───────┘
┌───────┐
│  ad   │
├───────┤
│ Ayse  │
│ Burak │
│ Ceren │
│ Ceren │
└───────┘
```

`Ceren` ikinci sonuçta iki kez görünür, çünkü iade edilmemiş iki kaydı vardır ve
birleştirme her eşleşme için bir satır üretir. `EXISTS` ise "en az bir eşleşme var mı"
sorusunu sorar; ilk eşleşmede durur ve satırı çoğaltmaz.

Bu fark, birleştirmeli yeniden yazımın sınırıdır: satır sayısını değiştirmeyen bir
dönüşüm isteniyorsa birleştirmenin sonucuna `DISTINCT` ya da gruplama eklemek gerekir. Bu
eklemeler de bedava değildir; sıralama ya da geçici yapı maliyeti getirir. `EXISTS` ile
yazılmış bir yüklemi birleştirmeye çevirirken önce eşleşmenin tekil olup olmadığı
sorulmalıdır.

#### Özet

- İlişkili alt sorgu, dış sorgunun bir sütununa başvurur ve anlamsal olarak dış satır
  başına yeniden değerlendirilir.
- Dış tabloda $m$, iç tabloda $n$ satır için yinelenen tarama $O(m \cdot n)$, önce
  gruplayıp birleştiren yazım $O(m + n)$ iş yapar; ölçümde bu 200'e karşı 25 olarak çıktı.
- `EXPLAIN QUERY PLAN` çıktısındaki `CORRELATED SCALAR SUBQUERY` satırı ilişkiyi, altındaki
  `SCAN` ya da `SEARCH` satırı erişim yolunu gösterir.
- İlişkili yazım kendiliğinden pahalı değildir; uygun erişim yolu varsa her yineleme tam
  tarama yerine tek aramaya iner.
- `EXISTS` satırı çoğaltmaz, birleştirme çoğaltır; iki yazım ancak eşleşme tekilse
  aynı sonucu verir.

#### Sonraki Adım

Bu dersteki birleştirmeli yazım, ara sonucu `FROM` içine gömülü bir alt sorgu olarak
kurdu. İç içe iki üç katman sonra bu yazım okunmaz hâle gelir: ara sonucun adı sorgunun
ortasında kaybolur ve aynı ara sonuç iki yerde gerekiyorsa tekrar yazılır. Sonraki ders,
ara sonuçları sorgunun başında adlandırıp gövdeyi sade bırakan ortak tablo ifadelerini
tanıtacak.

### Ortak Tablo İfadeleri

Source: https://academia.sh/tr/kurslar/ileri-sql/ortak-tablo-ifadeleri

WITH ile ara sonuçları adlandırma, sütun listesi verme, ifadelerin zincirlenmesi, aynı ara sonucun yeniden kullanılması, maddeleştirme kararının ölçülmesi ve veri değiştiren deyimlerde kullanım.

Önceki dersin birleştirmeli yazımı ara sonucu `FROM` içine gömülü bir alt sorgu olarak
kurmuştu. Tek katmanda bu okunur; iki katmanda okunmaz. Ara sonucun tanımı sorgunun
ortasına gömülür, adı ancak kapanış parantezinden sonra görünür ve aynı ara sonuç iki
yerde gerekiyorsa tanımı iki kez yazılır.

**Ortak tablo ifadesi (common table expression)**, bu üç sorunu birden çözer: ara sonuca
sorgunun **başında** ad verir, gövdeyi o adı kullanan sade bir sorguya indirger ve aynı adı
gövde içinde birden çok kez kullanmaya izin verir.

#### Adlandırma

Bir ortak tablo ifadesi `WITH` anahtar sözcüğüyle başlar; ad, isteğe bağlı sütun listesi ve
parantez içinde bir sorgu içerir. Bu tanımdan sonra gelen deyimin gövdesinde ad, bir tablo
gibi kullanılır.

"Şube başına, üye başına düşen ortalama ödünç sayısı" sorusu iki katmanlıdır: önce üye
başına sayım, sonra şube içinde ortalama. Aşağıdaki blok aynı soruyu iki yazımla sorar —
biri gömülü alt sorguyla, diğeri adlandırılmış ifadeyle:

```bash
sqlite3 -box -header <<'SQL'
CREATE TABLE sube(id INTEGER PRIMARY KEY, ad TEXT, ust_id INT);
CREATE TABLE uye(id INTEGER PRIMARY KEY, ad TEXT, sube_id INT);
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT);
INSERT INTO sube VALUES (4,'Kadikoy',2),(5,'Uskudar',2),(6,'Besiktas',3);
INSERT INTO uye VALUES (1,'Ayse',4),(2,'Burak',4),(3,'Ceren',5),(4,'Deniz',5),
                       (5,'Emre',6),(6,'Fatma',6),(7,'Gokhan',4),(8,'Hale',5);
INSERT INTO odunc VALUES
  (1,1,1,'2024-03-01','2024-03-15'),(2,3,1,'2024-03-01','2024-03-20'),
  (3,5,1,'2024-03-04','2024-03-18'),(4,7,1,'2024-03-11',NULL),
  (5,9,1,'2024-03-18','2024-03-29'),(6,2,2,'2024-03-01','2024-03-12'),
  (7,4,2,'2024-03-06','2024-03-25'),(8,6,2,'2024-03-11','2024-03-19'),
  (9,8,2,'2024-03-21',NULL),(10,1,3,'2024-03-04','2024-03-10'),
  (11,3,3,'2024-03-06','2024-03-27'),(12,10,3,'2024-03-13','2024-03-22'),
  (13,2,3,'2024-03-25',NULL),(14,5,4,'2024-03-04','2024-03-09'),
  (15,7,4,'2024-03-13','2024-03-26'),(16,4,4,'2024-03-20',NULL),
  (17,6,5,'2024-03-06','2024-03-14'),(18,9,5,'2024-03-11','2024-03-23'),
  (19,1,5,'2024-03-25','2024-03-28'),(20,8,6,'2024-03-04','2024-03-17'),
  (21,10,6,'2024-03-13','2024-03-21'),(22,3,6,'2024-03-20',NULL),
  (23,2,7,'2024-03-11','2024-03-16'),(24,5,7,'2024-03-18','2024-03-24'),
  (25,4,8,'2024-03-13','2024-03-24');

.print -- gömülü alt sorgu --
SELECT s.ad AS sube, ROUND(AVG(t.adet), 2) AS uye_basina
FROM (SELECT u.sube_id, u.id, COUNT(o.id) AS adet
      FROM uye u LEFT JOIN odunc o ON o.uye_id = u.id
      GROUP BY u.sube_id, u.id) AS t
JOIN sube s ON s.id = t.sube_id
GROUP BY s.id, s.ad
ORDER BY uye_basina DESC;

.print -- ortak tablo ifadesi --
WITH uye_sayim AS (
  SELECT u.sube_id, u.id AS uye_id, COUNT(o.id) AS adet
  FROM uye u LEFT JOIN odunc o ON o.uye_id = u.id
  GROUP BY u.sube_id, u.id
)
SELECT s.ad AS sube, ROUND(AVG(t.adet), 2) AS uye_basina
FROM uye_sayim t JOIN sube s ON s.id = t.sube_id
GROUP BY s.id, s.ad
ORDER BY uye_basina DESC;
SQL
```

```text
-- gömülü alt sorgu --
┌──────────┬────────────┐
│   sube   │ uye_basina │
├──────────┼────────────┤
│ Kadikoy  │ 3.67       │
│ Besiktas │ 3.0        │
│ Uskudar  │ 2.67       │
└──────────┴────────────┘
-- ortak tablo ifadesi --
┌──────────┬────────────┐
│   sube   │ uye_basina │
├──────────┼────────────┤
│ Kadikoy  │ 3.67       │
│ Besiktas │ 3.0        │
│ Uskudar  │ 2.67       │
└──────────┴────────────┘
```

Sonuçlar aynıdır; değişen okunma sırasıdır. İlk yazımda okuyucu dış sorgunun ne yaptığını
anlamak için önce parantezin içine inip çıkmak zorundadır; `FROM` yan tümcesinin neye
karşılık geldiği ancak kapanış parantezinden sonraki `AS t` görüldüğünde belli olur.
İkincisinde tanım yukarıda bitmiştir ve gövde okunurken `uye_sayim` bir tablo gibi
düşünülebilir. `LEFT JOIN` her iki yazımda da hiç ödünç almamış üyeleri sıfırla korur.

Ortak tablo ifadesi bir **görünüm (view)** değildir. Görünüm şemada kalıcı bir nesnedir ve
başka sorgular da kullanabilir; ortak tablo ifadesi yalnız yazıldığı deyim boyunca vardır,
deyim bitince adı da kaybolur. Bu geçicilik bir kısıt değil, tercih sebebidir: tek bir
raporun ara adımı için şemaya kalıcı nesne eklemek, o nesneyi bakması gereken bir varlık
hâline getirir.

#### Sütun Adlarını Tanımda Vermek

Adın ardından parantez içinde sütun listesi yazılabilir. Bu liste, iç sorgunun ürettiği
sütunlara sırayla ad verir ve iç sorguda her ifadeye tek tek `AS` yazma gereğini kaldırır:

```bash
sqlite3 -box -header <<'SQL'
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT);
INSERT INTO odunc VALUES
  (1,1,1,'2024-03-01','2024-03-15'),(2,3,1,'2024-03-01','2024-03-20'),
  (4,7,1,'2024-03-11',NULL),(6,2,2,'2024-03-01','2024-03-12'),
  (9,8,2,'2024-03-21',NULL),(10,1,3,'2024-03-04','2024-03-10'),
  (13,2,3,'2024-03-25',NULL);

WITH sure(odunc_id, uye_id, gun) AS (
  SELECT id, uye_id, julianday(COALESCE(iade, '2024-03-31')) - julianday(alis)
  FROM odunc
)
SELECT uye_id, COUNT(*) AS kayit, CAST(AVG(gun) AS INT) AS ortalama_gun
FROM sure GROUP BY uye_id ORDER BY uye_id;
SQL
```

```text
┌────────┬───────┬──────────────┐
│ uye_id │ kayit │ ortalama_gun │
├────────┼───────┼──────────────┤
│ 1      │ 3     │ 17           │
│ 2      │ 2     │ 10           │
│ 3      │ 2     │ 6            │
└────────┴───────┴──────────────┘
```

Sütun listesi verilmişse iç sorgunun ürettiği sütun sayısı listeyle **birebir** uyuşmak
zorundadır; uyuşmazlık sözdizim değil anlam hatasıdır ve motor deyimi reddeder. Burada
`gun` sütunu, iade edilmemiş kayıtlar için sayım gününü varsayan bir ifadedir; adın tanımda
verilmesi, gövdenin bu ifadeyi bir daha görmemesini sağlar.

#### Zincirleme ve Yeniden Kullanım

`WITH` sonrası virgülle ayrılmış birden çok tanım yazılabilir. Sonraki tanımlar
öncekilere başvurabilir; böylece hesap adım adım kurulur. Aynı ad gövdede birden çok kez
de kullanılabilir — gömülü alt sorguda mümkün olmayan şey budur:

```bash
sqlite3 -box -header <<'SQL'
CREATE TABLE sube(id INTEGER PRIMARY KEY, ad TEXT, ust_id INT);
CREATE TABLE uye(id INTEGER PRIMARY KEY, ad TEXT, sube_id INT);
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT);
INSERT INTO sube VALUES (4,'Kadikoy',2),(5,'Uskudar',2),(6,'Besiktas',3);
INSERT INTO uye VALUES (1,'Ayse',4),(2,'Burak',4),(3,'Ceren',5),(4,'Deniz',5),
                       (5,'Emre',6),(6,'Fatma',6),(7,'Gokhan',4),(8,'Hale',5);
INSERT INTO odunc VALUES
  (1,1,1,'2024-03-01','2024-03-15'),(2,3,1,'2024-03-01','2024-03-20'),
  (3,5,1,'2024-03-04','2024-03-18'),(4,7,1,'2024-03-11',NULL),
  (5,9,1,'2024-03-18','2024-03-29'),(6,2,2,'2024-03-01','2024-03-12'),
  (7,4,2,'2024-03-06','2024-03-25'),(8,6,2,'2024-03-11','2024-03-19'),
  (9,8,2,'2024-03-21',NULL),(10,1,3,'2024-03-04','2024-03-10'),
  (11,3,3,'2024-03-06','2024-03-27'),(12,10,3,'2024-03-13','2024-03-22'),
  (13,2,3,'2024-03-25',NULL),(14,5,4,'2024-03-04','2024-03-09'),
  (15,7,4,'2024-03-13','2024-03-26'),(16,4,4,'2024-03-20',NULL),
  (17,6,5,'2024-03-06','2024-03-14'),(18,9,5,'2024-03-11','2024-03-23'),
  (19,1,5,'2024-03-25','2024-03-28'),(20,8,6,'2024-03-04','2024-03-17'),
  (21,10,6,'2024-03-13','2024-03-21'),(22,3,6,'2024-03-20',NULL),
  (23,2,7,'2024-03-11','2024-03-16'),(24,5,7,'2024-03-18','2024-03-24'),
  (25,4,8,'2024-03-13','2024-03-24');

WITH uye_sayim AS (
  SELECT u.sube_id, u.id AS uye_id, COUNT(o.id) AS adet
  FROM uye u LEFT JOIN odunc o ON o.uye_id = u.id
  GROUP BY u.sube_id, u.id
),
sube_toplam AS (
  SELECT sube_id, SUM(adet) AS toplam FROM uye_sayim GROUP BY sube_id
)
SELECT s.ad AS sube, t.toplam,
       ROUND(100.0 * t.toplam / (SELECT SUM(toplam) FROM sube_toplam), 1) AS yuzde
FROM sube_toplam t JOIN sube s ON s.id = t.sube_id
ORDER BY t.toplam DESC;
SQL
```

```text
┌──────────┬────────┬───────┐
│   sube   │ toplam │ yuzde │
├──────────┼────────┼───────┤
│ Kadikoy  │ 11     │ 44.0  │
│ Uskudar  │ 8      │ 32.0  │
│ Besiktas │ 6      │ 24.0  │
└──────────┴────────┴───────┘
```

Hesap üç adımda kuruldu: üye başına sayım, şube başına toplam, toplamın genel toplama
oranı. `sube_toplam` iki yerde geçiyor — bir kez `FROM` içinde satır kaynağı, bir kez
`SELECT` listesinde genel toplamı veren skaler alt sorgu olarak. Gömülü yazımda bu, aynı
tanımın iki kez kopyalanması demekti; kopyaların biri güncellenip diğeri unutulduğunda
yüzdeler sessizce tutarsız hâle gelirdi. Ad tek olduğu için böyle bir sapma imkânsızdır.

Zincir aynı zamanda bir hata ayıklama aracıdır. Gövde geçici olarak
`SELECT * FROM uye_sayim` ile değiştirilerek ilk adımın çıktısı doğrudan görülebilir;
sonra `sube_toplam` sınanır. Gömülü yazımda bunun için parantezin içi elle söküp
çalıştırılır, sonra geri konur.

Tanımların sırası bağlayıcıdır: bir ifade yalnız kendinden **önce** tanımlanmış ifadelere
başvurabilir. Kendi kendine başvuru ise özel bir durumdur ve ayrı bir anahtar sözcük
gerektirir — sonraki dersin konusu.

#### Maddeleştirme Kararı

Ad tek olsa da hesap tek olmak zorunda değildir. Motorun iki seçeneği vardır: ifadeyi bir
kez hesaplayıp sonucu geçici bir yapıda tutmak (**maddeleştirme**, materialization) ya da
tanımı her kullanım yerine yerinde açmak. İlki tekrar hesabı önler; ikincisi dış
koşulların ifadenin içine itilmesine ve dizinlerin kullanılmasına izin verir. İkisi de her
zaman iyi değildir, bu yüzden karar planlayıcıya bırakılmıştır.

Fark, önceki dersteki sayaçla ölçülebilir. Aşağıdaki üç sorgu aynı ifadeyi iki kez
kullanır; tek fark, motora verilen maddeleştirme yönergesidir:

```bash
cat > cte-olcum.mjs <<'JS'
import { DatabaseSync } from 'node:sqlite';

const db = new DatabaseSync(':memory:');
db.exec(`
  CREATE TABLE odunc(id INTEGER PRIMARY KEY, uye_id INT, alis TEXT);
  INSERT INTO odunc VALUES
    (1,1,'2024-03-01'),(2,1,'2024-03-01'),(3,1,'2024-03-04'),(4,1,'2024-03-11'),
    (5,1,'2024-03-18'),(6,2,'2024-03-01'),(7,2,'2024-03-06'),(8,2,'2024-03-11'),
    (9,2,'2024-03-21'),(10,3,'2024-03-04'),(11,3,'2024-03-06'),(12,3,'2024-03-13'),
    (13,3,'2024-03-25'),(14,4,'2024-03-04'),(15,4,'2024-03-13'),(16,4,'2024-03-20'),
    (17,5,'2024-03-06'),(18,5,'2024-03-11'),(19,5,'2024-03-25'),(20,6,'2024-03-04'),
    (21,6,'2024-03-13'),(22,6,'2024-03-20'),(23,7,'2024-03-11'),(24,7,'2024-03-18'),
    (25,8,'2024-03-13');
`);

let ziyaret = 0;
db.function('gecis', (deger) => { ziyaret += 1; return deger; });

const olc = (etiket, sorgu) => {
  ziyaret = 0;
  const satir = db.prepare(sorgu).get();
  console.log(etiket, JSON.stringify(satir), '| ziyaret:', ziyaret);
};

olc('varsayılan     ', `
  WITH sayim AS (SELECT uye_id, COUNT(*) AS adet FROM odunc
                 WHERE gecis(uye_id) IS NOT NULL GROUP BY uye_id)
  SELECT (SELECT MAX(adet) FROM sayim) AS en_cok,
         (SELECT MIN(adet) FROM sayim) AS en_az`);

olc('MATERIALIZED   ', `
  WITH sayim AS MATERIALIZED (SELECT uye_id, COUNT(*) AS adet FROM odunc
                 WHERE gecis(uye_id) IS NOT NULL GROUP BY uye_id)
  SELECT (SELECT MAX(adet) FROM sayim) AS en_cok,
         (SELECT MIN(adet) FROM sayim) AS en_az`);

olc('NOT MATERIALIZED', `
  WITH sayim AS NOT MATERIALIZED (SELECT uye_id, COUNT(*) AS adet FROM odunc
                 WHERE gecis(uye_id) IS NOT NULL GROUP BY uye_id)
  SELECT (SELECT MAX(adet) FROM sayim) AS en_cok,
         (SELECT MIN(adet) FROM sayim) AS en_az`);
JS
node cte-olcum.mjs
```

```text
varsayılan      {"en_cok":5,"en_az":1} | ziyaret: 25
MATERIALIZED    {"en_cok":5,"en_az":1} | ziyaret: 25
NOT MATERIALIZED {"en_cok":5,"en_az":1} | ziyaret: 50
```

Üç sonuç aynı, iş miktarı farklı. Maddeleştirildiğinde 25 satırlık tablo bir kez taranır;
yerinde açıldığında tanım iki kullanım yerine ayrı ayrı kopyalanır ve tablo iki kez, yani
toplam 50 satır taranır. Bu ölçümde varsayılan davranış maddeleştirmeyle aynı çıktı;
sorgunun biçimi değişseydi planlayıcı diğer kararı da verebilirdi.

Buradaki `MATERIALIZED` ve `NOT MATERIALIZED` yönergeleri **motora özgüdür**; standart SQL
böyle bir denetim tanımlamaz ve her motorda bulunmaz. Standart olan, ortak tablo ifadesinin
**anlamıdır**: hangi sonucu ürettiği tanımlıdır, kaç kez hesaplandığı planlayıcıya
bırakılmıştır. Bu yüzden "ortak tablo ifadesi kullanınca sonuç bir kez hesaplanır" cümlesi
taşınabilir bir varsayım değildir; bazı motorlarda ifade her zaman maddeleştirilir, bazı
motorlarda hiç maddeleştirilmez, bazılarında karar sorguya göre değişir. Bir ifade birden
çok yerde kullanılıyorsa ve tanımı pahalıysa, hedef motorun davranışının ölçülmesi gerekir.

#### Veri Değiştiren Deyimlerde

`WITH` yalnız `SELECT` deyiminin önüne değil, `INSERT`, `UPDATE` ve `DELETE` deyimlerinin
önüne de yazılabilir. Hangi satırların değiştirileceğini belirleyen mantık böylece
deyimden ayrılır ve tek başına okunabilir hâle gelir:

```bash
sqlite3 -box -header <<'SQL'
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT);
INSERT INTO odunc VALUES
  (4,7,1,'2024-03-11',NULL),(9,8,2,'2024-03-21',NULL),(13,2,3,'2024-03-25',NULL),
  (16,4,4,'2024-03-20',NULL),(22,3,6,'2024-03-20',NULL),(1,1,1,'2024-03-01','2024-03-15');

WITH gecikmis AS (
  SELECT id FROM odunc WHERE iade IS NULL AND alis <= '2024-03-20'
)
UPDATE odunc SET iade = '2024-03-31' WHERE id IN (SELECT id FROM gecikmis);
SELECT changes() AS guncellenen;
SELECT id, alis, iade FROM odunc WHERE iade IS NULL ORDER BY id;
SQL
```

```text
┌─────────────┐
│ guncellenen │
├─────────────┤
│ 3           │
└─────────────┘
┌────┬────────────┬──────┐
│ id │    alis    │ iade │
├────┼────────────┼──────┤
│ 9  │ 2024-03-21 │      │
│ 13 │ 2024-03-25 │      │
└────┴────────────┴──────┘
```

Üç satır güncellendi; iade tarihi 20 Martı geçen iki kayıt açık kaldı. Bu yazımın pratik
yararı, güncellemeden önce **aynı** ifadenin `SELECT` ile çalıştırılıp etkilenecek
satırların görülebilmesidir. Seçim ölçütü tek yerde tanımlı olduğu için denetleme ile
uygulama arasında sapma kalmaz — koşulu iki kez yazmanın en pahalı sonucu, güncellemenin
denetlenenden farklı bir kümeye çarpmasıdır.

#### Özet

- Ortak tablo ifadesi, ara sonuca deyimin başında ad verir; ad yalnız o deyim boyunca yaşar
  ve şemada kalıcı bir nesne oluşturmaz.
- Ada isteğe bağlı sütun listesi eklenebilir; liste verildiğinde iç sorgunun sütun sayısı
  listeyle birebir uyuşmalıdır.
- Birden çok ifade virgülle zincirlenir; her ifade yalnız kendinden önce tanımlananlara
  başvurabilir ve aynı ad gövdede birden çok kez kullanılabilir.
- Adın tek olması hesabın tek olduğunu göstermez: maddeleştirme kararı planlayıcıya aittir
  ve aynı ifade iki kez hesaplanabilir; ölçümde bu 25'e karşı 50 satır olarak çıktı.
- `WITH`, veri değiştiren deyimlerin önüne de yazılabilir; seçim ölçütü böylece uygulamadan
  önce `SELECT` ile denetlenebilir.

#### Sonraki Adım

Bir ortak tablo ifadesinin yalnız kendinden önce tanımlananlara başvurabildiği söylendi;
tek istisna, ifadenin **kendine** başvurmasıdır. Bu istisna küçük bir sözdizim ayrıntısı
değildir: sabit sayıda adımla ifade edilemeyen hesapları — şube ağacında köke kadar
yükselmeyi, bir düğümden erişilebilen tüm düğümleri bulmayı — SQL'in erişimine açar.
Sonraki ders özyinelemeli ortak tablo ifadesini tanımlayacak, hiyerarşi üzerinde derinlik
hesaplayacak ve döngü koruması olmadan ne olduğunu sınırlı adımla gösterecek.

### Özyinelemeli Sorgular

Source: https://academia.sh/tr/kurslar/ileri-sql/ozyinelemeli-sorgular

WITH RECURSIVE ile taban ve özyineleme adımlarının kurulması, ağaçta aşağı ve yukarı gezinme, alt ağaç toplamları, döngü koruması olmadan ne olduğu ve dizi üretimi.

Önceki ders, ortak tablo ifadesinin yalnız kendinden önce tanımlananlara başvurabildiğini
söyledi ve tek istisnayı belirtti: bir ifade **kendine** başvurabilir. Bu istisna,
SQL'in ifade gücünü niteliksel olarak büyütür.

Şubelerin ağaç biçiminde örgütlendiği bir kütüphane düşünülsün: her şubenin bir üst şubesi
vardır, kökte merkez durur. "Merkezin altındaki tüm şubeler" sorusu, ağacın derinliği
bilinmeden yazılamaz — üç seviye için üç birleştirme, dört seviye için dört birleştirme
gerekir. Oysa derinlik veriye bağlıdır ve zamanla değişir. Sabit sayıda birleştirmeyle
ifade edilemeyen bu tür hesaplar **özyinelemeli ortak tablo ifadesiyle** yazılır.

#### İki Adımlı Yapı

Özyinelemeli ifade `WITH RECURSIVE` ile başlar ve gövdesi `UNION ALL` ile ayrılmış iki
parçadan oluşur:

- **Taban adımı**: ifadeye başvurmayan sorgu. Başlangıç satırlarını üretir.
- **Özyineleme adımı**: ifadenin kendisine başvuran sorgu. Bir önceki turda üretilen
  satırlardan yenilerini türetir.

Değerlendirme şöyle ilerler: taban adımı çalıştırılır ve sonucu bir çalışma kümesine konur.
Sonra özyineleme adımı, yalnız **son turda eklenen** satırlar üzerinde çalıştırılır; ürettiği
satırlar sonuca eklenir ve bir sonraki turun girdisi olur. Yeni satır üretilmeyen turda
işlem durur. Bu, Veri Yapıları kursundaki enine aramanın sorgu dilindeki karşılığıdır:
seviye seviye ilerlenir.

Şube ağacında kökten aşağı inmek, derinlik ve yol hesaplamak bu yapıyla yazılır:

```bash
sqlite3 -box -header <<'SQL'
CREATE TABLE sube(id INTEGER PRIMARY KEY, ad TEXT NOT NULL, ust_id INT REFERENCES sube(id));
INSERT INTO sube VALUES (1,'Merkez',NULL),(2,'Anadolu',1),(3,'Avrupa',1),
  (4,'Kadikoy',2),(5,'Uskudar',2),(6,'Besiktas',3),(7,'Moda',4),(8,'Bostanci',4);

WITH RECURSIVE agac(id, ad, derinlik, yol) AS (
  SELECT id, ad, 0, ad FROM sube WHERE ust_id IS NULL
  UNION ALL
  SELECT s.id, s.ad, a.derinlik + 1, a.yol || ' > ' || s.ad
  FROM sube s JOIN agac a ON s.ust_id = a.id
)
SELECT derinlik, ad, yol FROM agac ORDER BY yol;
SQL
```

```text
┌──────────┬──────────┬───────────────────────────────────────┐
│ derinlik │    ad    │                  yol                  │
├──────────┼──────────┼───────────────────────────────────────┤
│ 0        │ Merkez   │ Merkez                                │
│ 1        │ Anadolu  │ Merkez > Anadolu                      │
│ 2        │ Kadikoy  │ Merkez > Anadolu > Kadikoy            │
│ 3        │ Bostanci │ Merkez > Anadolu > Kadikoy > Bostanci │
│ 3        │ Moda     │ Merkez > Anadolu > Kadikoy > Moda     │
│ 2        │ Uskudar  │ Merkez > Anadolu > Uskudar            │
│ 1        │ Avrupa   │ Merkez > Avrupa                       │
│ 2        │ Besiktas │ Merkez > Avrupa > Besiktas            │
└──────────┴──────────┴───────────────────────────────────────┘
```

Taban adımı kökü seçer: üst şubesi olmayan satır, derinlik sıfır. Özyineleme adımı, o ana
kadar bulunmuş her düğüm için çocuklarını bulur, derinliği bir artırır ve yol dizgisini
uzatır. Derinlik ve yol, tabloda **bulunmayan** ama gezinme sırasında biriken değerlerdir;
özyinelemeli ifadenin asıl gücü budur.

Sıralama `ORDER BY yol` ile yapıldığı için çıktı ağaç düzeninde okunur: her düğümün hemen
altında kendi alt ağacı gelir. Derinlik sütunu artıp azalarak ilerler, çünkü sıralama
ölçütü derinlik değil yoldur.

#### Yukarı Gezinme

Aynı yapı ters yönde de kurulur. Birleştirme koşulunun yönü değiştirilirse bir düğümden
köke doğru yürünür; bu, "bu şube hangi bölgeye bağlı" sorusunun yanıtıdır:

```bash
sqlite3 -box -header <<'SQL'
CREATE TABLE sube(id INTEGER PRIMARY KEY, ad TEXT NOT NULL, ust_id INT REFERENCES sube(id));
INSERT INTO sube VALUES (1,'Merkez',NULL),(2,'Anadolu',1),(3,'Avrupa',1),
  (4,'Kadikoy',2),(5,'Uskudar',2),(6,'Besiktas',3),(7,'Moda',4),(8,'Bostanci',4);

WITH RECURSIVE ust_zincir(id, ad, ust_id, adim) AS (
  SELECT id, ad, ust_id, 0 FROM sube WHERE ad = 'Moda'
  UNION ALL
  SELECT s.id, s.ad, s.ust_id, z.adim + 1
  FROM sube s JOIN ust_zincir z ON s.id = z.ust_id
)
SELECT adim, ad FROM ust_zincir ORDER BY adim;
SQL
```

```text
┌──────┬─────────┐
│ adim │   ad    │
├──────┼─────────┤
│ 0    │ Moda    │
│ 1    │ Kadikoy │
│ 2    │ Anadolu │
│ 3    │ Merkez  │
└──────┴─────────┘
```

Tek fark birleştirme koşulundadır: aşağı inerken `s.ust_id = a.id`, yukarı çıkarken
`s.id = z.ust_id` yazılır. Zincir kökte durur, çünkü kökün üst şubesi boş değerdir ve
hiçbir satırla eşleşmez.

#### Alt Ağaç Üzerinde Toplama

Özyinelemeli ifadenin sonucu bir tablodur; başka tablolarla birleştirilebilir, gruplanabilir.
"Her şubenin kendi alt ağacındaki toplam ödünç sayısı" sorusu, önce her düğüm için alt
ağacındaki düğümleri üretip sonra bu eşleştirme üzerinden toplayarak yanıtlanır:

```bash
sqlite3 -box -header <<'SQL'
CREATE TABLE sube(id INTEGER PRIMARY KEY, ad TEXT NOT NULL, ust_id INT REFERENCES sube(id));
INSERT INTO sube VALUES (1,'Merkez',NULL),(2,'Anadolu',1),(3,'Avrupa',1),
  (4,'Kadikoy',2),(5,'Uskudar',2),(6,'Besiktas',3),(7,'Moda',4),(8,'Bostanci',4);
CREATE TABLE uye(id INTEGER PRIMARY KEY, ad TEXT, sube_id INT);
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT);
INSERT INTO uye VALUES (1,'Ayse',7),(2,'Burak',8),(3,'Ceren',5),(4,'Deniz',5),
                       (5,'Emre',6),(6,'Fatma',6),(7,'Gokhan',7),(8,'Hale',5);
INSERT INTO odunc VALUES
  (1,1,1,'2024-03-01','2024-03-15'),(2,3,1,'2024-03-01','2024-03-20'),
  (3,5,1,'2024-03-04','2024-03-18'),(4,7,1,'2024-03-11',NULL),
  (5,9,1,'2024-03-18','2024-03-29'),(6,2,2,'2024-03-01','2024-03-12'),
  (7,4,2,'2024-03-06','2024-03-25'),(8,6,2,'2024-03-11','2024-03-19'),
  (9,8,2,'2024-03-21',NULL),(10,1,3,'2024-03-04','2024-03-10'),
  (11,3,3,'2024-03-06','2024-03-27'),(12,10,3,'2024-03-13','2024-03-22'),
  (13,2,3,'2024-03-25',NULL),(14,5,4,'2024-03-04','2024-03-09'),
  (15,7,4,'2024-03-13','2024-03-26'),(16,4,4,'2024-03-20',NULL),
  (17,6,5,'2024-03-06','2024-03-14'),(18,9,5,'2024-03-11','2024-03-23'),
  (19,1,5,'2024-03-25','2024-03-28'),(20,8,6,'2024-03-04','2024-03-17'),
  (21,10,6,'2024-03-13','2024-03-21'),(22,3,6,'2024-03-20',NULL),
  (23,2,7,'2024-03-11','2024-03-16'),(24,5,7,'2024-03-18','2024-03-24'),
  (25,4,8,'2024-03-13','2024-03-24');

WITH RECURSIVE altagac(kok_id, id) AS (
  SELECT id, id FROM sube
  UNION ALL
  SELECT a.kok_id, s.id FROM sube s JOIN altagac a ON s.ust_id = a.id
)
SELECT k.ad AS sube, COUNT(o.id) AS altagac_toplami
FROM altagac a
JOIN sube k ON k.id = a.kok_id
LEFT JOIN uye u ON u.sube_id = a.id
LEFT JOIN odunc o ON o.uye_id = u.id
GROUP BY a.kok_id, k.ad
ORDER BY altagac_toplami DESC, k.ad;
SQL
```

```text
┌──────────┬─────────────────┐
│   sube   │ altagac_toplami │
├──────────┼─────────────────┤
│ Merkez   │ 25              │
│ Anadolu  │ 19              │
│ Kadikoy  │ 11              │
│ Uskudar  │ 8               │
│ Moda     │ 7               │
│ Avrupa   │ 6               │
│ Besiktas │ 6               │
│ Bostanci │ 4               │
└──────────┴─────────────────┘
```

Taban adımı burada tek bir kök değil, **her** şubeyi kendi alt ağacının kökü olarak alır.
Sonuç, "hangi düğüm hangi alt ağaca ait" eşleştirmesidir. Sayılar iç tutarlıdır: Kadıköy'ün
11 işlemi, Moda'nın 7'si ile Bostancı'nın 4'ünün toplamıdır; Anadolu'nun 19'u, Kadıköy'ün
11'i ile Üsküdar'ın 8'inin toplamıdır. Kökteki 25, tablodaki tüm işlemlerdir.

#### Döngü Koruması Olmadan

Şube ağacının gerçekten ağaç olduğu, verinin doğruluğuna bağlıdır. Bir güncelleme
yanlışlıkla bir üst şubeyi kendi alt şubesine bağlarsa yapı çevrimli bir çizgeye dönüşür ve
özyineleme durmaz: her tur aynı düğümleri yeniden üretir.

Aşağıdaki blok bu durumu bilerek üretir — Anadolu, kendi torunu olan Moda'ya bağlanır.
Sorgunun sonsuza kadar çalışmaması için özyineleme adımına bir **derinlik sınırı** konur;
sınır, döngüyü çözmez, yalnızca sonucu görülebilir kılar:

```bash
sqlite3 -box -header <<'SQL'
CREATE TABLE sube(id INTEGER PRIMARY KEY, ad TEXT NOT NULL, ust_id INT REFERENCES sube(id));
INSERT INTO sube VALUES (1,'Merkez',NULL),(2,'Anadolu',1),(3,'Avrupa',1),
  (4,'Kadikoy',2),(5,'Uskudar',2),(6,'Besiktas',3),(7,'Moda',4),(8,'Bostanci',4);
UPDATE sube SET ust_id = 7 WHERE id = 2;

WITH RECURSIVE agac(id, ad, derinlik) AS (
  SELECT id, ad, 0 FROM sube WHERE id = 4
  UNION ALL
  SELECT s.id, s.ad, a.derinlik + 1
  FROM sube s JOIN agac a ON s.ust_id = a.id
  WHERE a.derinlik < 7
)
SELECT derinlik, id, ad FROM agac ORDER BY derinlik, id;
SQL
```

```text
┌──────────┬────┬──────────┐
│ derinlik │ id │    ad    │
├──────────┼────┼──────────┤
│ 0        │ 4  │ Kadikoy  │
│ 1        │ 7  │ Moda     │
│ 1        │ 8  │ Bostanci │
│ 2        │ 2  │ Anadolu  │
│ 3        │ 4  │ Kadikoy  │
│ 3        │ 5  │ Uskudar  │
│ 4        │ 7  │ Moda     │
│ 4        │ 8  │ Bostanci │
│ 5        │ 2  │ Anadolu  │
│ 6        │ 4  │ Kadikoy  │
│ 6        │ 5  │ Uskudar  │
│ 7        │ 7  │ Moda     │
│ 7        │ 8  │ Bostanci │
└──────────┴────┴──────────┘
```

Kadıköy üçüncü ve altıncı derinlikte, Moda birinci, dördüncü ve yedinci derinlikte yeniden
görünüyor. Örüntü üç adımda bir tekrar ediyor: `4 → 7 → 2 → 4`. Sınır kaldırılsaydı sorgu
durmayacak, bellek ya da geçici disk alanı tükenene kadar satır üretecekti.

Doğru çözüm, gezilen düğümleri takip edip yeniden ziyareti engellemektir. İzlenen yol bir
dizgide biriktirilir ve özyineleme adımı, yolda zaten geçmiş bir düğümü ikinci kez
almayı reddeder:

```bash
sqlite3 -box -header <<'SQL'
CREATE TABLE sube(id INTEGER PRIMARY KEY, ad TEXT NOT NULL, ust_id INT REFERENCES sube(id));
INSERT INTO sube VALUES (1,'Merkez',NULL),(2,'Anadolu',1),(3,'Avrupa',1),
  (4,'Kadikoy',2),(5,'Uskudar',2),(6,'Besiktas',3),(7,'Moda',4),(8,'Bostanci',4);
UPDATE sube SET ust_id = 7 WHERE id = 2;

WITH RECURSIVE agac(id, ad, derinlik, yol) AS (
  SELECT id, ad, 0, '/' || id || '/' FROM sube WHERE id = 4
  UNION ALL
  SELECT s.id, s.ad, a.derinlik + 1, a.yol || s.id || '/'
  FROM sube s JOIN agac a ON s.ust_id = a.id
  WHERE a.yol NOT LIKE '%/' || s.id || '/%'
)
SELECT derinlik, id, ad, yol FROM agac ORDER BY derinlik, id;
SQL
```

```text
┌──────────┬────┬──────────┬───────────┐
│ derinlik │ id │    ad    │    yol    │
├──────────┼────┼──────────┼───────────┤
│ 0        │ 4  │ Kadikoy  │ /4/       │
│ 1        │ 7  │ Moda     │ /4/7/     │
│ 1        │ 8  │ Bostanci │ /4/8/     │
│ 2        │ 2  │ Anadolu  │ /4/7/2/   │
│ 3        │ 5  │ Uskudar  │ /4/7/2/5/ │
└──────────┴────┴──────────┴───────────┘
```

Sorgu beş satırda durdu. Kadıköy'e ikinci kez ulaşılabilecek olan `/4/7/2/4/` adımı,
yolda `/4/` zaten geçtiği için elendi. Kimlikler eğik çizgi arasına alınarak yazılıyor;
böylece `/4/` örüntüsü `/14/` ya da `/41/` ile karışmaz.

Bu koruma her düğümü **yol başına** bir kez ziyaret eder; genel bir çizgede aynı düğüme
farklı yollardan ulaşılabildiği için satır sayısı yine büyüyebilir. Her düğümü tümüyle bir
kez ziyaret etmek gerekiyorsa `UNION ALL` yerine yinelenenleri eleyen `UNION` kullanılır;
bunun bedeli, her turda tekillik denetimidir.

#### Hiyerarşi Dışında

Özyinelemeli ifadenin taban adımı bir tablodan gelmek zorunda değildir. Sabit bir satırdan
başlayıp bir kural uygulayarak dizi üretilebilir. Bunun tipik kullanımı, verinin olmadığı
günleri raporda sıfırla göstermektir:

```bash
sqlite3 -box -header <<'SQL'
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT);
INSERT INTO odunc VALUES
  (1,1,1,'2024-03-01','2024-03-15'),(2,3,1,'2024-03-01','2024-03-20'),
  (3,5,1,'2024-03-04','2024-03-18'),(6,2,2,'2024-03-01','2024-03-12'),
  (10,1,3,'2024-03-04','2024-03-10'),(14,5,4,'2024-03-04','2024-03-09');

WITH RECURSIVE gun(tarih) AS (
  SELECT '2024-03-01'
  UNION ALL
  SELECT date(tarih, '+1 day') FROM gun WHERE tarih < '2024-03-06'
)
SELECT g.tarih, COUNT(o.id) AS islem
FROM gun g LEFT JOIN odunc o ON o.alis = g.tarih
GROUP BY g.tarih ORDER BY g.tarih;
SQL
```

```text
┌────────────┬───────┐
│   tarih    │ islem │
├────────────┼───────┤
│ 2024-03-01 │ 3     │
│ 2024-03-02 │ 0     │
│ 2024-03-03 │ 0     │
│ 2024-03-04 │ 3     │
│ 2024-03-05 │ 0     │
│ 2024-03-06 │ 0     │
└────────────┴───────┘
```

Yalnız `odunc` tablosu gruplansaydı 2 ve 3 Mart satırları hiç görünmezdi; boş günler
raporda kaybolurdu. Üretilen takvim, sol dış birleştirmenin sol tarafını oluşturarak
eksik günleri sıfırla doldurur. Tarih işlevlerinin adı motora göre değişir; değişmeyen,
taban adımı ile durma koşulundan oluşan yapıdır.

Durma koşulunun **özyineleme adımının içinde** olması zorunludur. Koşul dışarıdaki
`WHERE` yan tümcesine yazılırsa üretim durmaz; dış süzgeç yalnızca üretilmiş satırları
eler. Bu, özyinelemeli sorgu yazarken en sık yapılan hatadır.

#### Özet

- Özyinelemeli ortak tablo ifadesi, taban adımı ile kendine başvuran özyineleme adımının
  `UNION ALL` ile birleşmesinden oluşur; yeni satır üretilmeyen turda durur.
- Derinlik, yol ve sıra numarası gibi değerler tabloda bulunmaz, gezinme sırasında birikir.
- Birleştirme koşulunun yönü değiştirilerek aynı yapı hem alt ağacı hem üst zinciri gezer.
- Veride çevrim varsa özyineleme durmaz; ziyaret edilen düğümleri yolda biriktirip
  yinelenen adımı elemek, sorguyu sonlandıran korumadır.
- Taban adımı sabit bir satır olabilir; bu, raporlarda eksik günleri dolduran takvim
  üretiminin standart yoludur.

#### Sonraki Adım

Bu dersteki hesapların ortak yanı, sonucun **satır kümesini** değiştirmesiydi: gezinme
yeni satırlar üretti, gruplama satırları teke indirdi. Bazı sorularda ise satırlar
korunmalı, her satırın yanına komşularına bakan bir değer eklenmelidir — bir üyenin
işleminin yanında o üyenin o güne kadarki toplamı gibi. Gruplama bunu yapamaz, çünkü
gruplama satırları yutar. Sonraki ders, satırları koruyarak komşu satırlar üzerinde hesap
yapan pencere işlevlerini tanıtacak ve çerçeve tanımının aynı sorguda sonucu nasıl
değiştirdiğini gösterecek.

### Pencere İşlevleri

Source: https://academia.sh/tr/kurslar/ileri-sql/pencere-islevleri

OVER yan tümcesiyle satırları koruyarak toplama, bölümleme, çerçeve tanımının RANGE ve ROWS ayrımı, kümülatif toplam ile kayan pencere ve komşu satıra erişim.

Önceki dersteki hesapların ortak yanı, satır kümesini değiştirmeleriydi: gezinme yeni
satırlar üretti, gruplama satırları teke indirdi. Bazı sorularda ise satırlar korunmalı,
her satırın yanına komşularına bakan bir değer eklenmelidir.

"Her ödünç işleminin yanında, o üyenin toplam işlem sayısı" böyle bir sorudur. Gruplama
bunu doğrudan veremez: `GROUP BY` uygulanınca ayrıntı satırları yok olur ve geriye grup
başına bir satır kalır. Ayrıntıyı korumak isteyen, gruplanmış sonucu ayrıntıyla yeniden
birleştirmek zorunda kalır. **Pencere işlevleri (window functions)**, bu birleştirmeyi
gereksiz kılar: toplama işlevini satırları yutmadan, her satırın kendi görüş alanı
üzerinde çalıştırır.

#### Aynı İşlev, İki Kullanım

Bir toplama işlevinin ardına `OVER` yan tümcesi eklendiğinde işlev **pencere işlevine**
dönüşür. `OVER` boş bırakılırsa pencere tüm sonuç kümesidir; `PARTITION BY` verilirse
sonuç kümesi bölümlere ayrılır ve her satır kendi bölümünü görür.

```bash
sqlite3 -box -header <<'SQL'
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT);
INSERT INTO odunc VALUES
  (1,1,1,'2024-03-01','2024-03-15'),(2,3,1,'2024-03-01','2024-03-20'),
  (6,2,2,'2024-03-01','2024-03-12'),(3,5,1,'2024-03-04','2024-03-18'),
  (7,4,2,'2024-03-06','2024-03-25'),(4,7,1,'2024-03-11',NULL),
  (8,6,2,'2024-03-11','2024-03-19'),(23,2,7,'2024-03-11','2024-03-16'),
  (5,9,1,'2024-03-18','2024-03-29'),(24,5,7,'2024-03-18','2024-03-24'),
  (9,8,2,'2024-03-21',NULL);

SELECT uye_id, COUNT(*) AS adet FROM odunc GROUP BY uye_id ORDER BY uye_id;
SELECT id, uye_id, alis, COUNT(*) OVER (PARTITION BY uye_id) AS uye_adedi
FROM odunc ORDER BY uye_id, alis LIMIT 6;
SQL
```

```text
┌────────┬──────┐
│ uye_id │ adet │
├────────┼──────┤
│ 1      │ 5    │
│ 2      │ 4    │
│ 7      │ 2    │
└────────┴──────┘
┌────┬────────┬────────────┬───────────┐
│ id │ uye_id │    alis    │ uye_adedi │
├────┼────────┼────────────┼───────────┤
│ 1  │ 1      │ 2024-03-01 │ 5         │
│ 2  │ 1      │ 2024-03-01 │ 5         │
│ 3  │ 1      │ 2024-03-04 │ 5         │
│ 4  │ 1      │ 2024-03-11 │ 5         │
│ 5  │ 1      │ 2024-03-18 │ 5         │
│ 6  │ 2      │ 2024-03-01 │ 4         │
└────┴────────┴────────────┴───────────┘
```

İlk sorgu on bir satırı üçe indirdi. İkincisi on bir satırı korudu ve her satıra kendi
üyesinin toplamını yazdı — birinci sorgunun ürettiği 5, 4, 2 sayıları burada ilgili
satırların yanında görünüyor. `PARTITION BY` ile `GROUP BY` aynı bölümlemeyi yapar; fark,
bölümlemenin sonucunda satırların yutulup yutulmamasıdır.

Değerlendirme sırası bu ayrımı açıklar. Pencere işlevleri, `WHERE`, `GROUP BY` ve `HAVING`
uygulandıktan **sonra** çalışır. İki sonuç doğar: pencere işlevinin girdisi süzülmüş
kümedir ve pencere işlevinin sonucu aynı sorgunun `WHERE` yan tümcesinde kullanılamaz.
Pencere sonucuna göre süzmek gerekiyorsa sorgu bir ortak tablo ifadesine sarılır ve süzme
dışarıda yapılır.

#### Çerçeve Tanımı

`OVER` içine `ORDER BY` eklendiğinde pencere sıralanır ve **çerçeve (frame)** kavramı
devreye girer: satırın gördüğü alan artık tüm bölüm değil, bölümün sıralı bir dilimidir.
Çerçeve iki biçimde tanımlanır:

- `ROWS`: sınırlar **satır sayısıyla** ölçülür. Geçerli satırdan geriye iki satır demek,
  fiziksel olarak iki satır demektir.
- `RANGE`: sınırlar **değer** üzerinden ölçülür. Sıralama anahtarında aynı değere sahip
  satırlar — eşler — çerçeveye birlikte girer ya da birlikte girmez.

`ORDER BY` verilip çerçeve yazılmazsa varsayılan çerçeve
`RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW` olur. Fark, sıralama anahtarında
yinelenen değerler olduğunda ortaya çıkar. Aşağıdaki sorgu üç çerçeveyi aynı veride yan
yana koyar:

```bash
sqlite3 -box -header <<'SQL'
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT);
INSERT INTO odunc VALUES
  (1,1,1,'2024-03-01','2024-03-15'),(2,3,1,'2024-03-01','2024-03-20'),
  (6,2,2,'2024-03-01','2024-03-12'),(3,5,1,'2024-03-04','2024-03-18'),
  (7,4,2,'2024-03-06','2024-03-25'),(4,7,1,'2024-03-11',NULL),
  (8,6,2,'2024-03-11','2024-03-19'),(23,2,7,'2024-03-11','2024-03-16'),
  (5,9,1,'2024-03-18','2024-03-29'),(24,5,7,'2024-03-18','2024-03-24'),
  (9,8,2,'2024-03-21',NULL);

SELECT alis,
       COUNT(*) OVER (ORDER BY alis) AS varsayilan,
       COUNT(*) OVER (ORDER BY alis
                      ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS satir_bazli,
       COUNT(*) OVER (ORDER BY alis
                      ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS kayan_uc
FROM odunc ORDER BY alis, id;
SQL
```

```text
┌────────────┬────────────┬─────────────┬──────────┐
│    alis    │ varsayilan │ satir_bazli │ kayan_uc │
├────────────┼────────────┼─────────────┼──────────┤
│ 2024-03-01 │ 3          │ 1           │ 1        │
│ 2024-03-01 │ 3          │ 2           │ 2        │
│ 2024-03-01 │ 3          │ 3           │ 3        │
│ 2024-03-04 │ 4          │ 4           │ 3        │
│ 2024-03-06 │ 5          │ 5           │ 3        │
│ 2024-03-11 │ 8          │ 6           │ 3        │
│ 2024-03-11 │ 8          │ 7           │ 3        │
│ 2024-03-11 │ 8          │ 8           │ 3        │
│ 2024-03-18 │ 10         │ 9           │ 3        │
│ 2024-03-18 │ 10         │ 10          │ 3        │
│ 2024-03-21 │ 11         │ 11          │ 3        │
└────────────┴────────────┴─────────────┴──────────┘
```

Üç sütun aynı veriden üç farklı yanıt üretti.

İlk sütunda 1 Mart'ın üç satırı da 3 değerini aldı: değer tabanlı çerçeve, aynı tarihe
sahip satırları tek bir eş kümesi sayar ve hepsine kümenin sonundaki toplamı verir. Bu,
"1 Mart sonu itibarıyla toplam" sorusunun yanıtıdır ve satırların tablodaki sırasından
etkilenmez.

İkinci sütun satır tabanlıdır: her satır bir öncekinin üstüne bir ekler ve 1, 2, 3 diye
ilerler. Aynı tarihli satırların farklı değerler alması, sonucun **sıralamaya** bağlı
olduğunu gösterir. Sıralama anahtarı satırları tam olarak ayırmıyorsa bu sütunun değeri
belirlenimci değildir: aynı sorgu, motorun satırları farklı sırada okuduğu bir çalışmada
üçlü grubun içinde farklı dağılım verebilir. Ayrım gerekiyorsa `ORDER BY` anahtarına
tekilliği sağlayan bir sütun eklenir.

Üçüncü sütun kayan penceredir: geçerli satır ve ondan önceki iki satır. İlk iki satırda
çerçeve henüz dolmadığı için 1 ve 2 değerleri çıktı; sonrasında sabit 3 kaldı. Kayan
pencere, kümülatif toplamın tersine **yereldir**: uzak geçmiş çerçeveden düşer.

Bu üç davranış farkı, çerçevenin bir ayrıntı olmadığını gösterir. Aynı işlev, aynı
sıralama ve aynı veriyle yazılmış üç ifade üç ayrı sütun üretti; hangisinin doğru olduğu
sorulan soruya bağlıdır.

#### Kümülatif Toplam ve Kayan Ortalama

Çerçeveler her sütunda tekrar yazılırsa sorgu okunmaz hâle gelir. `WINDOW` yan tümcesi
pencere tanımlarına ad verir; `OVER` sonrasında yalnız ad yazılır:

```bash
sqlite3 -box -header <<'SQL'
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT);
INSERT INTO odunc VALUES
  (1,1,1,'2024-03-01','2024-03-15'),(2,3,1,'2024-03-01','2024-03-20'),
  (6,2,2,'2024-03-01','2024-03-12'),(3,5,1,'2024-03-04','2024-03-18'),
  (7,4,2,'2024-03-06','2024-03-25'),(4,7,1,'2024-03-11',NULL),
  (8,6,2,'2024-03-11','2024-03-19'),(23,2,7,'2024-03-11','2024-03-16'),
  (5,9,1,'2024-03-18','2024-03-29'),(24,5,7,'2024-03-18','2024-03-24'),
  (9,8,2,'2024-03-21',NULL);

SELECT alis,
       CAST(SUM(julianday(COALESCE(iade,'2024-03-31')) - julianday(alis))
            OVER kumulatif AS INT) AS gun_toplami,
       ROUND(AVG(julianday(COALESCE(iade,'2024-03-31')) - julianday(alis))
             OVER kayan, 1) AS kayan_ortalama
FROM odunc
WINDOW kumulatif AS (ORDER BY alis ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW),
       kayan AS (ORDER BY alis ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)
ORDER BY alis, id;
SQL
```

```text
┌────────────┬─────────────┬────────────────┐
│    alis    │ gun_toplami │ kayan_ortalama │
├────────────┼─────────────┼────────────────┤
│ 2024-03-01 │ 14          │ 14.0           │
│ 2024-03-01 │ 33          │ 16.5           │
│ 2024-03-01 │ 44          │ 14.7           │
│ 2024-03-04 │ 58          │ 14.7           │
│ 2024-03-06 │ 77          │ 14.7           │
│ 2024-03-11 │ 97          │ 17.7           │
│ 2024-03-11 │ 105         │ 15.7           │
│ 2024-03-11 │ 110         │ 11.0           │
│ 2024-03-18 │ 121         │ 8.0            │
│ 2024-03-18 │ 127         │ 7.3            │
│ 2024-03-21 │ 137         │ 9.0            │
└────────────┴─────────────┴────────────────┘
```

Ölçülen değer, her işlemin kaç gün sürdüğüdür; iade edilmemiş kayıtlar için sayım günü
varsayılmıştır. Birinci sütun bu sürelerin kümülatif toplamıdır ve hep artar. İkinci
sütun son üç işlemin ortalama süresidir ve iner çıkar: mart ortasından sonra kısa süreli
işlemler geldikçe ortalama 17,7'den 7,3'e düşüyor. Kümülatif toplam eğilimi gizler, kayan
ortalama gösterir; ikisi aynı veriden okunan iki ayrı bilgidir.

`WINDOW` yan tümcesi standarttadır ve sorgunun sonuna, `ORDER BY` öncesine yazılır.
Desteklenmediği bir motorda tanımlar `OVER` içine açık açık kopyalanır; anlam değişmez.

#### Komşu Satıra Erişim

Bazı işlevler yalnız pencere işlevi olarak vardır; toplama karşılıkları yoktur.
`LAG` ve `LEAD`, aynı bölümde belirli sayıda önceki ya da sonraki satırın değerini getirir:

```bash
sqlite3 -box -header <<'SQL'
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT);
INSERT INTO odunc VALUES
  (1,1,1,'2024-03-01','2024-03-15'),(2,3,1,'2024-03-01','2024-03-20'),
  (6,2,2,'2024-03-01','2024-03-12'),(3,5,1,'2024-03-04','2024-03-18'),
  (7,4,2,'2024-03-06','2024-03-25'),(4,7,1,'2024-03-11',NULL),
  (8,6,2,'2024-03-11','2024-03-19'),(23,2,7,'2024-03-11','2024-03-16'),
  (5,9,1,'2024-03-18','2024-03-29'),(24,5,7,'2024-03-18','2024-03-24'),
  (9,8,2,'2024-03-21',NULL);

SELECT uye_id, alis,
       LAG(alis) OVER (PARTITION BY uye_id ORDER BY alis) AS onceki,
       CAST(julianday(alis) - julianday(LAG(alis) OVER (PARTITION BY uye_id ORDER BY alis))
            AS INT) AS aradaki_gun
FROM odunc ORDER BY uye_id, alis;
SQL
```

```text
┌────────┬────────────┬────────────┬─────────────┐
│ uye_id │    alis    │   onceki   │ aradaki_gun │
├────────┼────────────┼────────────┼─────────────┤
│ 1      │ 2024-03-01 │            │             │
│ 1      │ 2024-03-01 │ 2024-03-01 │ 0           │
│ 1      │ 2024-03-04 │ 2024-03-01 │ 3           │
│ 1      │ 2024-03-11 │ 2024-03-04 │ 7           │
│ 1      │ 2024-03-18 │ 2024-03-11 │ 7           │
│ 2      │ 2024-03-01 │            │             │
│ 2      │ 2024-03-06 │ 2024-03-01 │ 5           │
│ 2      │ 2024-03-11 │ 2024-03-06 │ 5           │
│ 2      │ 2024-03-21 │ 2024-03-11 │ 10          │
│ 7      │ 2024-03-11 │            │             │
│ 7      │ 2024-03-18 │ 2024-03-11 │ 7           │
└────────┴────────────┴────────────┴─────────────┘
```

Her bölümün ilk satırında önceki satır yoktur; `LAG` orada boş değer verir ve boş değerle
yapılan çıkarma da boş değer üretir. Bölüm sınırının aşılmaması pencerenin tanımı gereğidir:
`uye_id` değiştiğinde geçmiş sıfırlanır, bir üyenin son işlemi başka üyenin ilk işlemine
komşu sayılmaz.

Aynı sonucu ilişkili alt sorguyla üretmek mümkündür — "bu satırdan küçük tarihlerin en
büyüğü" — ancak o yazım satır başına bir arama yapar. Pencere işlevi bölümü bir kez
sıralar ve tek geçişte ilerler.

#### Özet

- `OVER` yan tümcesi, toplama işlevini satırları koruyan bir pencere işlevine çevirir;
  `PARTITION BY`, `GROUP BY` ile aynı bölümlemeyi satır yutmadan yapar.
- Pencere işlevleri `WHERE`, `GROUP BY` ve `HAVING` sonrasında çalışır; sonuçlarına göre
  süzmek için sorgunun bir ortak tablo ifadesine sarılması gerekir.
- Çerçeve `ROWS` ile satır sayısına, `RANGE` ile değere göre ölçülür; `ORDER BY` verilip
  çerçeve yazılmazsa varsayılan `RANGE … CURRENT ROW` olur ve eş satırlar aynı sonucu alır.
- Aynı veride varsayılan çerçeve, satır tabanlı kümülatif toplam ve üç satırlık kayan
  pencere üç ayrı sütun üretti; hangisinin doğru olduğu sorulan soruya bağlıdır.
- `LAG` ve `LEAD` bölüm içinde komşu satıra erişir ve bölüm sınırını aşmaz.

#### Sonraki Adım

Bu derste pencere, satırların üzerinde bir hesap yürüttü ama satırlara **sıra** vermedi.
Oysa "en çok ödünç alan üç üye" gibi sorular sıralama numarası ister ve hemen ardından bir
karar gerektirir: eşit sayıya sahip iki üye aynı sırayı mı almalı, farklı mı? Sonraki ders
satır numarası, sıra ve yoğun sıra işlevlerini aynı veri üzerinde yan yana koyarak
beraberlik davranışlarındaki farkı gösterecek.

### Sıralama İşlevleri

Source: https://academia.sh/tr/kurslar/ileri-sql/siralama-islevleri

Satır numarası, sıra ve yoğun sıra işlevlerinin beraberlik davranışları, dilim ve yüzdelik dağılım işlevleri, bölüm içinde ilk N seçimi ve satır numarasıyla tekilleştirme.

Önceki ders pencerenin üzerinde hesap yürüttü ama satırlara sıra vermedi. "En çok ödünç
alan üç üye" gibi sorular ise önce bir sıra numarası ister; hemen ardından da bir karar
gerektirir: eşit sayıya sahip iki üye aynı sırayı mı almalıdır, farklı mı? Bu soruya üç
ayrı yanıt verilebilir ve SQL üçünü de ayrı işlev olarak tanımlar.

Bu ders, sıralama işlevlerini aynı veri üzerinde yan yana koyup farklarını gösterir ve
sıralamanın belirlenimci olması için gereken koşulu kurar.

#### Üç İşlev, Üç Beraberlik Kararı

Üç işlev de pencereyi `ORDER BY` ile sıralar ve her satıra bir tam sayı verir. Ayrıldıkları
nokta, sıralama anahtarında **eşit değerli** satırlarla ne yapıldığıdır:

- `ROW_NUMBER`: eşlik tanımaz. Her satıra farklı bir numara verir; 1, 2, 3 diye sürer.
- `RANK`: eşlere aynı sırayı verir, sonra **atlar**. İki satır ikinci olursa sonraki
  dördüncüdür.
- `DENSE_RANK`: eşlere aynı sırayı verir, **atlamaz**. İki satır ikinci olursa sonraki
  üçüncüdür.

Üçü de aynı sorguda yazılabilir; ortak pencere `WINDOW` yan tümcesinde bir kez tanımlanır:

```bash
sqlite3 -box -header <<'SQL'
CREATE TABLE uye(id INTEGER PRIMARY KEY, ad TEXT, sube TEXT);
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT);
INSERT INTO uye VALUES (1,'Ayse','Kadikoy'),(2,'Burak','Kadikoy'),(3,'Ceren','Uskudar'),
  (4,'Deniz','Uskudar'),(5,'Emre','Besiktas'),(6,'Fatma','Besiktas'),
  (7,'Gokhan','Kadikoy'),(8,'Hale','Uskudar');
INSERT INTO odunc VALUES
  (1,1,1,'2024-03-01','2024-03-15'),(2,3,1,'2024-03-01','2024-03-20'),
  (3,5,1,'2024-03-04','2024-03-18'),(4,7,1,'2024-03-11',NULL),
  (5,9,1,'2024-03-18','2024-03-29'),(6,2,2,'2024-03-01','2024-03-12'),
  (7,4,2,'2024-03-06','2024-03-25'),(8,6,2,'2024-03-11','2024-03-19'),
  (9,8,2,'2024-03-21',NULL),(10,1,3,'2024-03-04','2024-03-10'),
  (11,3,3,'2024-03-06','2024-03-27'),(12,10,3,'2024-03-13','2024-03-22'),
  (13,2,3,'2024-03-25',NULL),(14,5,4,'2024-03-04','2024-03-09'),
  (15,7,4,'2024-03-13','2024-03-26'),(16,4,4,'2024-03-20',NULL),
  (17,6,5,'2024-03-06','2024-03-14'),(18,9,5,'2024-03-11','2024-03-23'),
  (19,1,5,'2024-03-25','2024-03-28'),(20,8,6,'2024-03-04','2024-03-17'),
  (21,10,6,'2024-03-13','2024-03-21'),(22,3,6,'2024-03-20',NULL),
  (23,2,7,'2024-03-11','2024-03-16'),(24,5,7,'2024-03-18','2024-03-24'),
  (25,4,8,'2024-03-13','2024-03-24');

WITH sayim AS (
  SELECT u.ad, u.sube, COUNT(o.id) AS adet
  FROM uye u LEFT JOIN odunc o ON o.uye_id = u.id
  GROUP BY u.id, u.ad, u.sube
)
SELECT ad, adet,
       ROW_NUMBER() OVER s AS satir_no,
       RANK()       OVER s AS sira,
       DENSE_RANK() OVER s AS yogun_sira
FROM sayim
WINDOW s AS (ORDER BY adet DESC)
ORDER BY adet DESC, ad;
SQL
```

```text
┌────────┬──────┬──────────┬──────┬────────────┐
│   ad   │ adet │ satir_no │ sira │ yogun_sira │
├────────┼──────┼──────────┼──────┼────────────┤
│ Ayse   │ 5    │ 1        │ 1    │ 1          │
│ Burak  │ 4    │ 2        │ 2    │ 2          │
│ Ceren  │ 4    │ 3        │ 2    │ 2          │
│ Deniz  │ 3    │ 4        │ 4    │ 3          │
│ Emre   │ 3    │ 5        │ 4    │ 3          │
│ Fatma  │ 3    │ 6        │ 4    │ 3          │
│ Gokhan │ 2    │ 7        │ 7    │ 4          │
│ Hale   │ 1    │ 8        │ 8    │ 5          │
└────────┴──────┴──────────┴──────┴────────────┘
```

Dört ödünç alan iki üye var. `RANK` ikisine de 2 verdi ve sonraki üyeye 3 değil 4 verdi:
üçüncülük tüketilmiş sayıldı. `DENSE_RANK` ise ikisine 2 verip sonrakine 3 verdi; sıra
numaraları boşluksuz ilerledi. `ROW_NUMBER` beraberliği hiç görmedi ve 2 ile 3'ü keyfî
biçimde dağıttı.

Bu üç sütun üç ayrı soruya karşılık gelir. `RANK`, "kaç üye benden fazla aldı" sorusunun
yanıtına bir eklenmiş hâlidir — yarışma sıralamalarının alışkanlığı budur. `DENSE_RANK`,
"kaç farklı değer benden büyüktür" sorusunu yanıtlar ve seviye saymak için uygundur.
`ROW_NUMBER` sıra değil, **kimlik** üretir: satırları birbirinden ayırmak için kullanılır.

Son sütunda `Hale` için `RANK` 8, `DENSE_RANK` 5 veriyor. İki sayı arasındaki fark
beraberliklerin toplam etkisidir: sekiz üye ama beş farklı değer var.

#### Beraberliğin Belirsizliği

`ROW_NUMBER` sütununda `Burak` 2, `Ceren` 3 aldı. Bu dağılım sorgunun yazımından çıkmaz:
ikisinin de `adet` değeri 4 olduğuna göre `ORDER BY adet DESC` ikisini ayırmaz. Numaraların
hangisine gideceğine motor karar verdi.

Bu, kaçınılması gereken bir durumdur. Aynı sorgu farklı bir plan altında, farklı bir sürümde
ya da tablo farklı sırada okunduğunda numaraları takas edebilir. `ROW_NUMBER` bir sonuç
kümesini sayfalamak ya da tekilleştirmek için kullanılıyorsa, bu takas sessiz bir hataya
dönüşür: sayfalar arasında bir kayıt iki kez görünür, bir başkası hiç görünmez.

Kural açıktır: `ROW_NUMBER` kullanılan pencerede `ORDER BY` anahtarı satırları **tek
biçimde** belirlemelidir. Anahtar bunu sağlamıyorsa sonuna birincil anahtar gibi tekil bir
sütun eklenir — `ORDER BY adet DESC, ad` yazımı yukarıdaki belirsizliği ortadan kaldırır.
`RANK` ve `DENSE_RANK` bu sorundan etkilenmez, çünkü eş satırlara zaten aynı değeri
verirler.

#### Dilim ve Yüzdelik Dağılım

Sıralama ailesinin diğer üyeleri, satırın sıralamadaki **konumunu** oransal olarak verir.
`NTILE(n)` bölümü olabildiğince eşit `n` parçaya böler; `PERCENT_RANK` sıranın sıfır ile
bir arasındaki karşılığını, `CUME_DIST` ise "bu satır dâhil, buraya kadar olan satırların
oranını" verir:

```bash
sqlite3 -box -header <<'SQL'
CREATE TABLE uye(id INTEGER PRIMARY KEY, ad TEXT, sube TEXT);
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT);
INSERT INTO uye VALUES (1,'Ayse','Kadikoy'),(2,'Burak','Kadikoy'),(3,'Ceren','Uskudar'),
  (4,'Deniz','Uskudar'),(5,'Emre','Besiktas'),(6,'Fatma','Besiktas'),
  (7,'Gokhan','Kadikoy'),(8,'Hale','Uskudar');
INSERT INTO odunc VALUES
  (1,1,1,'2024-03-01','2024-03-15'),(2,3,1,'2024-03-01','2024-03-20'),
  (3,5,1,'2024-03-04','2024-03-18'),(4,7,1,'2024-03-11',NULL),
  (5,9,1,'2024-03-18','2024-03-29'),(6,2,2,'2024-03-01','2024-03-12'),
  (7,4,2,'2024-03-06','2024-03-25'),(8,6,2,'2024-03-11','2024-03-19'),
  (9,8,2,'2024-03-21',NULL),(10,1,3,'2024-03-04','2024-03-10'),
  (11,3,3,'2024-03-06','2024-03-27'),(12,10,3,'2024-03-13','2024-03-22'),
  (13,2,3,'2024-03-25',NULL),(14,5,4,'2024-03-04','2024-03-09'),
  (15,7,4,'2024-03-13','2024-03-26'),(16,4,4,'2024-03-20',NULL),
  (17,6,5,'2024-03-06','2024-03-14'),(18,9,5,'2024-03-11','2024-03-23'),
  (19,1,5,'2024-03-25','2024-03-28'),(20,8,6,'2024-03-04','2024-03-17'),
  (21,10,6,'2024-03-13','2024-03-21'),(22,3,6,'2024-03-20',NULL),
  (23,2,7,'2024-03-11','2024-03-16'),(24,5,7,'2024-03-18','2024-03-24'),
  (25,4,8,'2024-03-13','2024-03-24');

WITH sayim AS (
  SELECT u.ad, COUNT(o.id) AS adet
  FROM uye u LEFT JOIN odunc o ON o.uye_id = u.id
  GROUP BY u.id, u.ad
)
SELECT ad, adet,
       NTILE(4) OVER s AS dilim,
       ROUND(PERCENT_RANK() OVER s, 2) AS yuzdelik_sira,
       ROUND(CUME_DIST()    OVER s, 2) AS birikimli_oran
FROM sayim
WINDOW s AS (ORDER BY adet DESC)
ORDER BY adet DESC, ad;
SQL
```

```text
┌────────┬──────┬───────┬───────────────┬────────────────┐
│   ad   │ adet │ dilim │ yuzdelik_sira │ birikimli_oran │
├────────┼──────┼───────┼───────────────┼────────────────┤
│ Ayse   │ 5    │ 1     │ 0.0           │ 0.13           │
│ Burak  │ 4    │ 1     │ 0.14          │ 0.38           │
│ Ceren  │ 4    │ 2     │ 0.14          │ 0.38           │
│ Deniz  │ 3    │ 2     │ 0.43          │ 0.75           │
│ Emre   │ 3    │ 3     │ 0.43          │ 0.75           │
│ Fatma  │ 3    │ 3     │ 0.43          │ 0.75           │
│ Gokhan │ 2    │ 4     │ 0.86          │ 0.88           │
│ Hale   │ 1    │ 4     │ 1.0           │ 1.0            │
└────────┴──────┴───────┴───────────────┴────────────────┘
```

Dikkat çeken satır `Burak` ile `Ceren`: aynı `adet` değerine sahipler, yüzdelik sıraları da
birikimli oranları da aynı, ama farklı dilimlere düştüler. `NTILE` eşleri gözetmez;
satırları sayıya göre böler ve bölüm sınırı bir eş kümesinin ortasına düşerse onları
ayırır. Bu yüzden dilim numarası, eşit değerli satırlar için `ROW_NUMBER` kadar
belirsizdir.

`PERCENT_RANK` ilk satıra her zaman 0, `CUME_DIST` son satıra her zaman 1 verir. İkisi
arasındaki fark tanımlarındadır: yüzdelik sıra "önümde kaç satır var" oranını, birikimli
oran "benim ve benden büyük olmayanların" oranını ölçer.

#### Bölüm İçinde İlk N

Sıralama işlevlerinin en yaygın kullanımı, her bölümün en iyilerini seçmektir. Önceki
derste belirtildiği gibi pencere işlevinin sonucu aynı sorgunun `WHERE` yan tümcesinde
kullanılamaz; sıralama bir ortak tablo ifadesinde yapılır, süzme dışarıda:

```bash
sqlite3 -box -header <<'SQL'
CREATE TABLE uye(id INTEGER PRIMARY KEY, ad TEXT, sube TEXT);
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT);
INSERT INTO uye VALUES (1,'Ayse','Kadikoy'),(2,'Burak','Kadikoy'),(3,'Ceren','Uskudar'),
  (4,'Deniz','Uskudar'),(5,'Emre','Besiktas'),(6,'Fatma','Besiktas'),
  (7,'Gokhan','Kadikoy'),(8,'Hale','Uskudar');
INSERT INTO odunc VALUES
  (1,1,1,'2024-03-01','2024-03-15'),(2,3,1,'2024-03-01','2024-03-20'),
  (3,5,1,'2024-03-04','2024-03-18'),(4,7,1,'2024-03-11',NULL),
  (5,9,1,'2024-03-18','2024-03-29'),(6,2,2,'2024-03-01','2024-03-12'),
  (7,4,2,'2024-03-06','2024-03-25'),(8,6,2,'2024-03-11','2024-03-19'),
  (9,8,2,'2024-03-21',NULL),(10,1,3,'2024-03-04','2024-03-10'),
  (11,3,3,'2024-03-06','2024-03-27'),(12,10,3,'2024-03-13','2024-03-22'),
  (13,2,3,'2024-03-25',NULL),(14,5,4,'2024-03-04','2024-03-09'),
  (15,7,4,'2024-03-13','2024-03-26'),(16,4,4,'2024-03-20',NULL),
  (17,6,5,'2024-03-06','2024-03-14'),(18,9,5,'2024-03-11','2024-03-23'),
  (19,1,5,'2024-03-25','2024-03-28'),(20,8,6,'2024-03-04','2024-03-17'),
  (21,10,6,'2024-03-13','2024-03-21'),(22,3,6,'2024-03-20',NULL),
  (23,2,7,'2024-03-11','2024-03-16'),(24,5,7,'2024-03-18','2024-03-24'),
  (25,4,8,'2024-03-13','2024-03-24');

WITH sayim AS (
  SELECT u.ad, u.sube, COUNT(o.id) AS adet
  FROM uye u LEFT JOIN odunc o ON o.uye_id = u.id
  GROUP BY u.id, u.ad, u.sube
),
siralanmis AS (
  SELECT sube, ad, adet,
         RANK()       OVER b AS sira,
         ROW_NUMBER() OVER b AS satir_no
  FROM sayim
  WINDOW b AS (PARTITION BY sube ORDER BY adet DESC)
)
SELECT sube, ad, adet, sira, satir_no FROM siralanmis
WHERE sira <= 1 ORDER BY sube, ad;
SQL
```

```text
┌──────────┬───────┬──────┬──────┬──────────┐
│   sube   │  ad   │ adet │ sira │ satir_no │
├──────────┼───────┼──────┼──────┼──────────┤
│ Besiktas │ Emre  │ 3    │ 1    │ 1        │
│ Besiktas │ Fatma │ 3    │ 1    │ 2        │
│ Kadikoy  │ Ayse  │ 5    │ 1    │ 1        │
│ Uskudar  │ Ceren │ 4    │ 1    │ 1        │
└──────────┴───────┴──────┴──────┴──────────┘
```

Beşiktaş'tan iki satır geldi, diğerlerinden birer satır. Bunun nedeni işlev seçimidir:
`RANK` ile süzülünce beraberlikteki tüm satırlar gelir; `WHERE satir_no <= 1` yazılsaydı
`Fatma` düşerdi ve hangisinin düştüğü belirsiz olurdu.

Seçim, sorunun ne istediğine bağlıdır. "Her şubenin en çok ödünç alan üyesi" sorusu
beraberlikte iki ad istiyorsa `RANK`, tam olarak bir satır istiyorsa `ROW_NUMBER` uygundur —
ikinci durumda beraberliği bozan ek bir anahtar da belirtilmelidir. `WHERE sira <= 3`
biçimindeki eşiği değiştirmek, aynı sorguyu ilk üçe genişletir.

#### Satır Numarasıyla Tekilleştirme

`ROW_NUMBER` bir sıralama değil ayıklama aracı olarak da kullanılır. Bölüm içinde
numaralanan satırlardan yalnız birincisini almak, "her grubun bir temsilcisi" desenidir:

```bash
sqlite3 -box -header <<'SQL'
CREATE TABLE uye(id INTEGER PRIMARY KEY, ad TEXT, sube TEXT);
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT);
INSERT INTO uye VALUES (1,'Ayse','Kadikoy'),(2,'Burak','Kadikoy'),(3,'Ceren','Uskudar'),
  (4,'Deniz','Uskudar'),(5,'Emre','Besiktas'),(6,'Fatma','Besiktas'),
  (7,'Gokhan','Kadikoy'),(8,'Hale','Uskudar');
INSERT INTO odunc VALUES
  (1,1,1,'2024-03-01','2024-03-15'),(2,3,1,'2024-03-01','2024-03-20'),
  (3,5,1,'2024-03-04','2024-03-18'),(4,7,1,'2024-03-11',NULL),
  (5,9,1,'2024-03-18','2024-03-29'),(6,2,2,'2024-03-01','2024-03-12'),
  (7,4,2,'2024-03-06','2024-03-25'),(8,6,2,'2024-03-11','2024-03-19'),
  (9,8,2,'2024-03-21',NULL),(10,1,3,'2024-03-04','2024-03-10'),
  (11,3,3,'2024-03-06','2024-03-27'),(12,10,3,'2024-03-13','2024-03-22'),
  (13,2,3,'2024-03-25',NULL),(14,5,4,'2024-03-04','2024-03-09'),
  (15,7,4,'2024-03-13','2024-03-26'),(16,4,4,'2024-03-20',NULL),
  (17,6,5,'2024-03-06','2024-03-14'),(18,9,5,'2024-03-11','2024-03-23'),
  (19,1,5,'2024-03-25','2024-03-28'),(20,8,6,'2024-03-04','2024-03-17'),
  (21,10,6,'2024-03-13','2024-03-21'),(22,3,6,'2024-03-20',NULL),
  (23,2,7,'2024-03-11','2024-03-16'),(24,5,7,'2024-03-18','2024-03-24'),
  (25,4,8,'2024-03-13','2024-03-24');

WITH numarali AS (
  SELECT id, uye_id, alis,
         ROW_NUMBER() OVER (PARTITION BY uye_id ORDER BY alis DESC, id DESC) AS no
  FROM odunc
)
SELECT n.uye_id, u.ad, n.id AS odunc_id, n.alis
FROM numarali n JOIN uye u ON u.id = n.uye_id
WHERE n.no = 1 ORDER BY n.uye_id;
SQL
```

```text
┌────────┬────────┬──────────┬────────────┐
│ uye_id │   ad   │ odunc_id │    alis    │
├────────┼────────┼──────────┼────────────┤
│ 1      │ Ayse   │ 5        │ 2024-03-18 │
│ 2      │ Burak  │ 9        │ 2024-03-21 │
│ 3      │ Ceren  │ 13       │ 2024-03-25 │
│ 4      │ Deniz  │ 16       │ 2024-03-20 │
│ 5      │ Emre   │ 19       │ 2024-03-25 │
│ 6      │ Fatma  │ 22       │ 2024-03-20 │
│ 7      │ Gokhan │ 24       │ 2024-03-18 │
│ 8      │ Hale   │ 25       │ 2024-03-13 │
└────────┴────────┴──────────┴────────────┘
```

Sıralama anahtarı `alis DESC, id DESC` biçiminde iki sütunludur. `alis` tek başına aynı
üye içinde yinelenebilir; `id` birincil anahtar olduğu için eklenmesi sonucu belirlenimci
kılar. Bu, önceki bölümdeki kuralın uygulanmış hâlidir.

Aynı sonuç, İlişkili Alt Sorgular dersindeki `MAX` alt sorgusuyla da üretilebilirdi. İki
yazımın farkı, beraberlik durumunda ortaya çıkar: alt sorgulu yazım aynı tarihe sahip iki
kaydın ikisini de döndürür, satır numarasıyla yazım tam olarak bir tanesini seçer.

#### Özet

- `ROW_NUMBER` eşlik tanımaz, `RANK` eşlere aynı sırayı verip atlar, `DENSE_RANK` eşlere
  aynı sırayı verip atlamaz.
- `RANK` "kaç satır benden önde", `DENSE_RANK` "kaç farklı değer benden önde" sorusunu
  yanıtlar; `ROW_NUMBER` sıra değil kimlik üretir.
- `ROW_NUMBER` ve `NTILE` sonuçları, sıralama anahtarı satırları tek biçimde belirlemiyorsa
  belirlenimci değildir; anahtara tekil bir sütun eklenmelidir.
- Pencere sonucuna göre süzmek için sorgu bir ortak tablo ifadesine sarılır; bölüm içinde
  ilk N seçiminde `RANK` beraberlikteki tüm satırları, `ROW_NUMBER` tam olarak N satırı
  getirir.
- `PERCENT_RANK` ilk satıra 0, `CUME_DIST` son satıra 1 verir; `NTILE` eş satırları farklı
  dilimlere ayırabilir.

#### Sonraki Adım

Buraya kadarki her sorgunun sonucu, sütunları sorgu yazılırken belli olan bir tabloydu.
Raporlarda ise sık sık tersi istenir: satırlardaki değerler **sütun başlığına** çıksın,
her tür ya da her ay ayrı bir sütun olsun. Bu dönüşüm, satır sayısını azaltıp sütun sayısını
artırır ve SQL'in sabit sütun listesi kuralıyla doğrudan çelişir. Sonraki ders, döndürme
işlemlerini koşullu toplamayla nasıl yazıldığını ve ters yönde satıra dönüştürmenin nasıl
yapıldığını gösterecek.

### Döndürme İşlemleri

Source: https://academia.sh/tr/kurslar/ileri-sql/dondurme-islemleri

Uzun biçimden geniş biçime koşullu toplamayla döndürme, FILTER yan tümcesi, ters döndürmenin iki yazımı ve sütun listesinin sorgu yazılırken bilinme zorunluluğu.

Buraya kadarki her sorgunun sonucu, sütunları sorgu yazılırken belli olan bir tabloydu.
Raporlarda ise sık sık tersi istenir: satırlardaki değerler sütun başlığına çıksın, her
kitap türü ayrı bir sütun olsun, şubeler satırlarda kalsın.

Bu dönüşüme **döndürme (pivot)** denir. Kaynak biçim **uzun biçimdir**: her ölçüm bir
satır, ayrım sütunları değer taşır. Hedef biçim **geniş biçimdir**: ayrım sütunundaki her
değer bir sütuna dönüşür. Dönüşüm, SQL'in temel bir kuralıyla gerilim içindedir — sonucun
sütun listesi, sorgu derlenirken bilinmek zorundadır.

#### Uzun Biçim

Başlangıç noktası, gruplamayla üretilen olağan özettir: şube ve tür ikilisi başına ödünç
sayısı.

```bash
sqlite3 -box -header <<'SQL'
CREATE TABLE uye(id INTEGER PRIMARY KEY, ad TEXT, sube TEXT);
CREATE TABLE kitap(id INTEGER PRIMARY KEY, baslik TEXT, tur TEXT);
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT);
INSERT INTO uye VALUES (1,'Ayse','Kadikoy'),(2,'Burak','Kadikoy'),(3,'Ceren','Uskudar'),
  (4,'Deniz','Uskudar'),(5,'Emre','Besiktas'),(6,'Fatma','Besiktas'),
  (7,'Gokhan','Kadikoy'),(8,'Hale','Uskudar');
INSERT INTO kitap VALUES (1,'Kayip Zaman','roman'),(2,'Sessiz Ev','roman'),
  (3,'Sayilar Kurami','bilim'),(4,'Evrenin Yapisi','bilim'),(5,'Kisa Tarih','tarih'),
  (6,'Anadolu Notlari','tarih'),(7,'Siirler','siir'),(8,'Denemeler','deneme'),
  (9,'Yol Haritasi','roman'),(10,'Bilgi Kurami','felsefe');
INSERT INTO odunc VALUES
  (1,1,1,'2024-03-01','2024-03-15'),(2,3,1,'2024-03-01','2024-03-20'),
  (3,5,1,'2024-03-04','2024-03-18'),(4,7,1,'2024-03-11',NULL),
  (5,9,1,'2024-03-18','2024-03-29'),(6,2,2,'2024-03-01','2024-03-12'),
  (7,4,2,'2024-03-06','2024-03-25'),(8,6,2,'2024-03-11','2024-03-19'),
  (9,8,2,'2024-03-21',NULL),(10,1,3,'2024-03-04','2024-03-10'),
  (11,3,3,'2024-03-06','2024-03-27'),(12,10,3,'2024-03-13','2024-03-22'),
  (13,2,3,'2024-03-25',NULL),(14,5,4,'2024-03-04','2024-03-09'),
  (15,7,4,'2024-03-13','2024-03-26'),(16,4,4,'2024-03-20',NULL),
  (17,6,5,'2024-03-06','2024-03-14'),(18,9,5,'2024-03-11','2024-03-23'),
  (19,1,5,'2024-03-25','2024-03-28'),(20,8,6,'2024-03-04','2024-03-17'),
  (21,10,6,'2024-03-13','2024-03-21'),(22,3,6,'2024-03-20',NULL),
  (23,2,7,'2024-03-11','2024-03-16'),(24,5,7,'2024-03-18','2024-03-24'),
  (25,4,8,'2024-03-13','2024-03-24');

SELECT u.sube, k.tur, COUNT(*) AS adet
FROM odunc o JOIN uye u ON u.id = o.uye_id JOIN kitap k ON k.id = o.kitap_id
GROUP BY u.sube, k.tur
ORDER BY u.sube, k.tur;
SQL
```

```text
┌──────────┬─────────┬──────┐
│   sube   │   tur   │ adet │
├──────────┼─────────┼──────┤
│ Besiktas │ bilim   │ 1    │
│ Besiktas │ deneme  │ 1    │
│ Besiktas │ felsefe │ 1    │
│ Besiktas │ roman   │ 2    │
│ Besiktas │ tarih   │ 1    │
│ Kadikoy  │ bilim   │ 2    │
│ Kadikoy  │ deneme  │ 1    │
│ Kadikoy  │ roman   │ 4    │
│ Kadikoy  │ siir    │ 1    │
│ Kadikoy  │ tarih   │ 3    │
│ Uskudar  │ bilim   │ 3    │
│ Uskudar  │ felsefe │ 1    │
│ Uskudar  │ roman   │ 2    │
│ Uskudar  │ siir    │ 1    │
│ Uskudar  │ tarih   │ 1    │
└──────────┴─────────┴──────┘
```

Bu biçim veri işleme için elverişlidir: yeni bir tür eklendiğinde sorgu değişmez, yalnız
satır sayısı artar. Okumak içinse elverişsizdir. Kadıköy ile Üsküdar'ın roman sayısını
karşılaştırmak için gözün on beş satır arasında yukarı aşağı gezinmesi gerekir; ayrıca
sıfır olan hücreler — Beşiktaş'ta şiir yok — hiç görünmez.

#### Koşullu Toplamayla Döndürme

Döndürmenin taşınabilir yazımı, her hedef sütun için bir koşullu toplama ifadesi
yazmaktır. `CASE` ifadesi, satırın o sütuna ait olup olmadığına göre 1 ya da 0 üretir;
`SUM` bunları toplar.

```bash
sqlite3 -box -header <<'SQL'
CREATE TABLE uye(id INTEGER PRIMARY KEY, ad TEXT, sube TEXT);
CREATE TABLE kitap(id INTEGER PRIMARY KEY, baslik TEXT, tur TEXT);
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT);
INSERT INTO uye VALUES (1,'Ayse','Kadikoy'),(2,'Burak','Kadikoy'),(3,'Ceren','Uskudar'),
  (4,'Deniz','Uskudar'),(5,'Emre','Besiktas'),(6,'Fatma','Besiktas'),
  (7,'Gokhan','Kadikoy'),(8,'Hale','Uskudar');
INSERT INTO kitap VALUES (1,'Kayip Zaman','roman'),(2,'Sessiz Ev','roman'),
  (3,'Sayilar Kurami','bilim'),(4,'Evrenin Yapisi','bilim'),(5,'Kisa Tarih','tarih'),
  (6,'Anadolu Notlari','tarih'),(7,'Siirler','siir'),(8,'Denemeler','deneme'),
  (9,'Yol Haritasi','roman'),(10,'Bilgi Kurami','felsefe');
INSERT INTO odunc VALUES
  (1,1,1,'2024-03-01','2024-03-15'),(2,3,1,'2024-03-01','2024-03-20'),
  (3,5,1,'2024-03-04','2024-03-18'),(4,7,1,'2024-03-11',NULL),
  (5,9,1,'2024-03-18','2024-03-29'),(6,2,2,'2024-03-01','2024-03-12'),
  (7,4,2,'2024-03-06','2024-03-25'),(8,6,2,'2024-03-11','2024-03-19'),
  (9,8,2,'2024-03-21',NULL),(10,1,3,'2024-03-04','2024-03-10'),
  (11,3,3,'2024-03-06','2024-03-27'),(12,10,3,'2024-03-13','2024-03-22'),
  (13,2,3,'2024-03-25',NULL),(14,5,4,'2024-03-04','2024-03-09'),
  (15,7,4,'2024-03-13','2024-03-26'),(16,4,4,'2024-03-20',NULL),
  (17,6,5,'2024-03-06','2024-03-14'),(18,9,5,'2024-03-11','2024-03-23'),
  (19,1,5,'2024-03-25','2024-03-28'),(20,8,6,'2024-03-04','2024-03-17'),
  (21,10,6,'2024-03-13','2024-03-21'),(22,3,6,'2024-03-20',NULL),
  (23,2,7,'2024-03-11','2024-03-16'),(24,5,7,'2024-03-18','2024-03-24'),
  (25,4,8,'2024-03-13','2024-03-24');

SELECT u.sube,
       SUM(CASE WHEN k.tur = 'roman'   THEN 1 ELSE 0 END) AS roman,
       SUM(CASE WHEN k.tur = 'bilim'   THEN 1 ELSE 0 END) AS bilim,
       SUM(CASE WHEN k.tur = 'tarih'   THEN 1 ELSE 0 END) AS tarih,
       SUM(CASE WHEN k.tur = 'siir'    THEN 1 ELSE 0 END) AS siir,
       SUM(CASE WHEN k.tur = 'deneme'  THEN 1 ELSE 0 END) AS deneme,
       SUM(CASE WHEN k.tur = 'felsefe' THEN 1 ELSE 0 END) AS felsefe,
       COUNT(*) AS toplam
FROM odunc o JOIN uye u ON u.id = o.uye_id JOIN kitap k ON k.id = o.kitap_id
GROUP BY u.sube ORDER BY u.sube;
SQL
```

```text
┌──────────┬───────┬───────┬───────┬──────┬────────┬─────────┬────────┐
│   sube   │ roman │ bilim │ tarih │ siir │ deneme │ felsefe │ toplam │
├──────────┼───────┼───────┼───────┼──────┼────────┼─────────┼────────┤
│ Besiktas │ 2     │ 1     │ 1     │ 0    │ 1      │ 1       │ 6      │
│ Kadikoy  │ 4     │ 2     │ 3     │ 1    │ 1      │ 0       │ 11     │
│ Uskudar  │ 2     │ 3     │ 1     │ 1    │ 0      │ 1       │ 8      │
└──────────┴───────┴───────┴───────┴──────┴────────┴─────────┴────────┘
```

On beş satır üçe indi ve boş hücreler sıfır olarak göründü. Bu, uzun biçimin veremediği
bilgidir: Beşiktaş'ta şiir ödüncü **yok** ile şiir ödüncü **bilinmiyor** arasındaki fark
artık okunur.

`ELSE 0` bölümü ihmal edilirse eşleşmeyen satırlar boş değer üretir. `SUM` boş değerleri
atladığı için sonuç çoğu durumda değişmez; ancak bir grubun **tüm** satırları eşleşmezse
sütun sıfır yerine boş kalır. Sıfırın anlamlı olduğu raporlarda `ELSE 0` yazılması ya da
sonucun `COALESCE` ile sarılması gerekir.

Bu yazımın maliyeti tek bir taramadır: her satır bir kez okunur, altı ifade aynı satır
üzerinde değerlendirilir. Aynı sonucu altı ayrı sorguyu birleştirerek üretmek, tabloyu
altı kez taramak demektir.

#### FILTER Yan Tümcesi

Standart SQL, koşullu toplama için ayrı bir yazım tanımlar: toplama işlevinin ardına
gelen `FILTER (WHERE …)` yan tümcesi, o işlevin yalnız koşulu sağlayan satırları görmesini
sağlar. Amaç `CASE` ile aynıdır, okunurluk daha iyidir — koşul, sayılan şeyin yanında durur:

```bash
sqlite3 -box -header <<'SQL'
CREATE TABLE uye(id INTEGER PRIMARY KEY, ad TEXT, sube TEXT);
CREATE TABLE kitap(id INTEGER PRIMARY KEY, baslik TEXT, tur TEXT);
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT);
INSERT INTO uye VALUES (1,'Ayse','Kadikoy'),(2,'Burak','Kadikoy'),(3,'Ceren','Uskudar'),
  (4,'Deniz','Uskudar'),(5,'Emre','Besiktas'),(6,'Fatma','Besiktas'),
  (7,'Gokhan','Kadikoy'),(8,'Hale','Uskudar');
INSERT INTO kitap VALUES (1,'Kayip Zaman','roman'),(2,'Sessiz Ev','roman'),
  (3,'Sayilar Kurami','bilim'),(4,'Evrenin Yapisi','bilim'),(5,'Kisa Tarih','tarih'),
  (6,'Anadolu Notlari','tarih'),(7,'Siirler','siir'),(8,'Denemeler','deneme'),
  (9,'Yol Haritasi','roman'),(10,'Bilgi Kurami','felsefe');
INSERT INTO odunc VALUES
  (1,1,1,'2024-03-01','2024-03-15'),(2,3,1,'2024-03-01','2024-03-20'),
  (3,5,1,'2024-03-04','2024-03-18'),(4,7,1,'2024-03-11',NULL),
  (5,9,1,'2024-03-18','2024-03-29'),(6,2,2,'2024-03-01','2024-03-12'),
  (7,4,2,'2024-03-06','2024-03-25'),(8,6,2,'2024-03-11','2024-03-19'),
  (9,8,2,'2024-03-21',NULL),(10,1,3,'2024-03-04','2024-03-10'),
  (11,3,3,'2024-03-06','2024-03-27'),(12,10,3,'2024-03-13','2024-03-22'),
  (13,2,3,'2024-03-25',NULL),(14,5,4,'2024-03-04','2024-03-09'),
  (15,7,4,'2024-03-13','2024-03-26'),(16,4,4,'2024-03-20',NULL),
  (17,6,5,'2024-03-06','2024-03-14'),(18,9,5,'2024-03-11','2024-03-23'),
  (19,1,5,'2024-03-25','2024-03-28'),(20,8,6,'2024-03-04','2024-03-17'),
  (21,10,6,'2024-03-13','2024-03-21'),(22,3,6,'2024-03-20',NULL),
  (23,2,7,'2024-03-11','2024-03-16'),(24,5,7,'2024-03-18','2024-03-24'),
  (25,4,8,'2024-03-13','2024-03-24');

SELECT u.sube,
       COUNT(*) FILTER (WHERE k.tur = 'roman') AS roman,
       COUNT(*) FILTER (WHERE k.tur = 'bilim') AS bilim,
       COUNT(*) FILTER (WHERE k.tur = 'tarih') AS tarih,
       COUNT(*) FILTER (WHERE o.iade IS NULL)  AS iade_edilmemis,
       COUNT(*) AS toplam
FROM odunc o JOIN uye u ON u.id = o.uye_id JOIN kitap k ON k.id = o.kitap_id
GROUP BY u.sube ORDER BY u.sube;
SQL
```

```text
┌──────────┬───────┬───────┬───────┬────────────────┬────────┐
│   sube   │ roman │ bilim │ tarih │ iade_edilmemis │ toplam │
├──────────┼───────┼───────┼───────┼────────────────┼────────┤
│ Besiktas │ 2     │ 1     │ 1     │ 1              │ 6      │
│ Kadikoy  │ 4     │ 2     │ 3     │ 2              │ 11     │
│ Uskudar  │ 2     │ 3     │ 1     │ 2              │ 8      │
└──────────┴───────┴───────┴───────┴────────────────┴────────┘
```

Roman, bilim ve tarih sütunları önceki sonucun aynısıdır. Dördüncü sütun, döndürmenin
yalnız bir ayrım sütunuyla sınırlı olmadığını gösterir: `iade_edilmemis`, tür ile ilgisi
olmayan bağımsız bir koşuldur ve aynı taramada hesaplanmıştır. Farklı ölçütlerin aynı
satırda yan yana getirilmesi, koşullu toplamanın döndürmeden bağımsız asıl yararıdır.

`FILTER` yan tümcesi standarttadır ancak her motorda bulunmaz; desteklenmediği yerde
`CASE` yazımı her zaman çalışır.

#### Ters Yön: Sütundan Satıra

Ters işlem, geniş biçimdeki bir tabloyu uzun biçime çevirir. İhtiyaç, dış bir kaynaktan
gelen geniş özetin normalleştirilmiş bir tabloya aktarılmasında ya da tür sayısı
değiştikçe bozulmayan bir sorgu yazılmasında doğar.

Taşınabilir yazım, sütun adlarını bir değer listesine dönüştürüp kaynak tabloyla çapraz
birleştirmektir:

```bash
sqlite3 -box -header <<'SQL'
CREATE TABLE ozet(sube TEXT PRIMARY KEY, roman INT, bilim INT, tarih INT);
INSERT INTO ozet VALUES ('Besiktas',2,1,1),('Kadikoy',4,2,3),('Uskudar',2,3,1);

WITH turler(tur) AS (VALUES ('roman'),('bilim'),('tarih'))
SELECT o.sube, t.tur,
       CASE t.tur WHEN 'roman' THEN o.roman
                  WHEN 'bilim' THEN o.bilim
                  WHEN 'tarih' THEN o.tarih END AS adet
FROM ozet o CROSS JOIN turler t
ORDER BY o.sube, t.tur;
SQL
```

```text
┌──────────┬───────┬──────┐
│   sube   │  tur  │ adet │
├──────────┼───────┼──────┤
│ Besiktas │ bilim │ 1    │
│ Besiktas │ roman │ 2    │
│ Besiktas │ tarih │ 1    │
│ Kadikoy  │ bilim │ 2    │
│ Kadikoy  │ roman │ 4    │
│ Kadikoy  │ tarih │ 3    │
│ Uskudar  │ bilim │ 3    │
│ Uskudar  │ roman │ 2    │
│ Uskudar  │ tarih │ 1    │
└──────────┴───────┴──────┘
```

Çapraz birleştirme üç şubeyi üç türle eşleştirerek dokuz satır üretti; `CASE` her satırda
doğru sütunu seçti. Kaynak tablo **bir kez** okunur.

Aynı sonuç, her sütun için bir sorgu yazıp `UNION ALL` ile birleştirerek de üretilir:

```bash
sqlite3 -box -header <<'SQL'
CREATE TABLE ozet(sube TEXT PRIMARY KEY, roman INT, bilim INT, tarih INT);
INSERT INTO ozet VALUES ('Besiktas',2,1,1),('Kadikoy',4,2,3),('Uskudar',2,3,1);

SELECT sube, 'roman' AS tur, roman AS adet FROM ozet
UNION ALL SELECT sube, 'bilim', bilim FROM ozet
UNION ALL SELECT sube, 'tarih', tarih FROM ozet
ORDER BY sube, tur;
SQL
```

```text
┌──────────┬───────┬──────┐
│   sube   │  tur  │ adet │
├──────────┼───────┼──────┤
│ Besiktas │ bilim │ 1    │
│ Besiktas │ roman │ 2    │
│ Besiktas │ tarih │ 1    │
│ Kadikoy  │ bilim │ 2    │
│ Kadikoy  │ roman │ 4    │
│ Kadikoy  │ tarih │ 3    │
│ Uskudar  │ bilim │ 3    │
│ Uskudar  │ roman │ 2    │
│ Uskudar  │ tarih │ 1    │
└──────────┴───────┴──────┘
```

Sonuçlar aynı, okunma ve maliyet farklı. Birleşimli yazım her sütun için tabloyu bir kez
tarar: üç sütun, üç tarama. Küçük özet tablolarında bu önemsizdir; kaynak büyükse çapraz
birleştirmeli yazım tercih edilir.

#### Sütun Listesi Neden Sabittir

Her iki yönde de tür adları sorgunun metnine elle yazıldı. Bunun nedeni bir eksiklik
değil, ilişkisel modelin bir sonucudur: bir sorgunun sonucu bir bağıntıdır ve bağıntının
başlığı — sütun adları ve tipleri — sorgu çalıştırılmadan bellidir. Veriye bakarak sütun
üretmek bu tanımı bozar.

Bunun pratik karşılığı şudur: yeni bir kitap türü eklendiğinde geniş biçimli sorgu
kendiliğinden bir sütun kazanmaz; sorgunun güncellenmesi gerekir. Sütun listesini veriden
üretmek isteyen, sorgu metnini uygulama tarafında birleştirip motora göndermek zorundadır.
Bu yol açıktır ama iki bedeli vardır: metin birleştirmeyle üretilen sorgular enjeksiyon
yüzeyi açar ve her farklı sütun listesi ayrı bir sorgu metni ürettiği için plan
önbelleğinden yararlanmaz. Bu iki konu, kursun Sorgu Başarımı konusundaki Dinamik SQL
Riskleri dersinde ele alınacaktır.

Bazı motorlar `PIVOT` ya da benzeri bir anahtar sözcük sunar. Bu yazımlar standart SQL'in
parçası değildir ve sütun listesini yine sorgu yazılırken ister; yaptıkları, koşullu
toplamayı kısaltmaktır.

#### Özet

- Uzun biçim veri işlemeye, geniş biçim okumaya elverişlidir; döndürme bu iki biçim
  arasındaki dönüşümdür.
- Taşınabilir döndürme yazımı, hedef sütun başına bir koşullu toplama ifadesidir ve tek
  taramada tamamlanır; `ELSE 0` yazılmazsa hiç eşleşmeyen gruplarda sütun boş kalır.
- `FILTER (WHERE …)` yan tümcesi aynı işi daha okunur yazar ve birbirinden bağımsız
  ölçütlerin aynı satırda toplanmasına izin verir.
- Ters döndürme, sütun adlarını değer listesine çevirip çapraz birleştirerek tek taramada
  ya da `UNION ALL` ile sütun başına bir tarama yaparak yazılır.
- Sonuç sütunlarının sorgu yazılırken bilinmesi zorunludur; sütun listesini veriden üretmek
  sorgu metninin uygulama tarafında birleştirilmesini gerektirir.

#### Sonraki Adım

Bu konu boyunca yazılan her sorgu tek bir deyimdi ve tek bir soruya yanıt verdi. Veri
değiştiren işler ise çoğu zaman tek deyime sığmaz: bir kitabın ödünç verilmesi, ödünç
kaydının açılmasını ve kitabın durumunun güncellenmesini birlikte gerektirir. İkisinden
biri yapılıp diğeri yapılmazsa veri tutarsız kalır. Sonraki konu, birden çok deyimi
bölünmez tek bir birim hâline getiren işlemleri ele alacak; ilk ders işlemin başlatılması,
kesinleştirilmesi ve geri alınmasıyla başlayacak.

## İşlemler

İşlem denetimi, kaydetme noktalarıyla kısmi geri alma, yalıtım düzeyleri ve okuma anomalileri, satır ve tablo kilitleri ile kilitlenme, saklı yordamlar ve tetikleyicilerin örtük yan etkileri.

### İşlem Denetimi

Source: https://academia.sh/tr/kurslar/ileri-sql/islem-denetimi

Otomatik kesinleştirme kipi, BEGIN ile açık işlem başlatma, COMMIT ve ROLLBACK, deyim hatasının işlemi kendiliğinden bitirmemesi ve bağlantı koptuğunda örtük geri alma.

Önceki konu boyunca yazılan her sorgu tek bir deyimdi ve tek bir soruya yanıt verdi. Veri
değiştiren işler ise çoğu zaman tek deyime sığmaz. Bir kitabın ödünç verilmesi iki değişiklik
gerektirir: ödünç kaydının açılması ve kitabın raf durumunun güncellenmesi. İkisinden biri
yapılıp diğeri yapılmazsa veritabanı, gerçekte var olmayan bir durumu anlatır — kayıt açık
görünürken kitap rafta ya da tersi.

İlişkisel Kuram kursunda ACID özellikleri tanıtılırken atomiklik "ya hep ya hiç" olarak
tanımlanmıştı. **İşlem (transaction)**, bu tanımın uygulamadaki karşılığıdır: birden çok
deyimi, motorun bölünmez saydığı tek bir birim hâline getirir.

#### Otomatik Kesinleştirme

Açık bir işlem başlatılmadan çalıştırılan her deyim, kendi başına bir işlemdir. Bu davranışa
**otomatik kesinleştirme (autocommit)** denir: deyim başarıyla biterse etkisi kalıcıdır,
hata verirse hiç olmamış gibidir.

```bash
rm -f k1.db
sqlite3 -box -header k1.db <<'SQL'
CREATE TABLE kitap(id INTEGER PRIMARY KEY, baslik TEXT, rafta INT NOT NULL DEFAULT 1);
INSERT INTO kitap VALUES (1,'Kayip Zaman',1),(2,'Sessiz Ev',1),(3,'Sayilar Kurami',1);
UPDATE kitap SET rafta = 0 WHERE id = 1;
SQL
sqlite3 -box -header k1.db 'SELECT id, baslik, rafta FROM kitap ORDER BY id;'
rm -f k1.db
```

```text
┌────┬────────────────┬───────┐
│ id │     baslik     │ rafta │
├────┼────────────────┼───────┤
│ 1  │ Kayip Zaman    │ 0     │
│ 2  │ Sessiz Ev      │ 1     │
│ 3  │ Sayilar Kurami │ 1     │
└────┴────────────────┴───────┘
```

Birinci komut veritabanını kurup güncelledi ve sona erdi; ikinci komut ayrı bir süreç
olarak açtı ve değişikliği gördü. Hiçbir yerde kesinleştirme yazılmadı, çünkü her deyim
kendiliğinden kesinleşti.

Bu kip tekil değişiklikler için elverişlidir, birlikte olması gereken değişiklikler için
tehlikelidir: iki deyim arasında bir hata olursa birincisi kalıcıdır. Otomatik
kesinleştirmenin açık ya da kapalı olması ve nasıl kapatıldığı motora göre değişir; bazı
motorlarda `BEGIN` yazmak, bazılarında bir ayar değiştirmek gerekir. Değişmeyen, açık
işlemin `COMMIT` ya da `ROLLBACK` ile bitirilmesidir.

#### Başlatma, Kesinleştirme, Geri Alma

Açık işlem `BEGIN` ile başlar. O andan sonraki değişiklikler yalnız o işlem içinden
görünür; `COMMIT` çalıştırılana kadar kalıcı değildir. `ROLLBACK`, işlem başladıktan sonra
yapılmış tüm değişiklikleri siler:

```bash
rm -f k2.db
sqlite3 -box -header k2.db <<'SQL'
CREATE TABLE kitap(id INTEGER PRIMARY KEY, baslik TEXT, rafta INT NOT NULL DEFAULT 1);
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT);
INSERT INTO kitap VALUES (1,'Kayip Zaman',1),(2,'Sessiz Ev',1),(3,'Sayilar Kurami',1);

BEGIN;
INSERT INTO odunc(kitap_id, uye_id, alis) VALUES (1, 4, '2024-03-11');
UPDATE kitap SET rafta = 0 WHERE id = 1;
SELECT 'islem icinde' AS an, (SELECT COUNT(*) FROM odunc) AS odunc_sayisi,
       (SELECT rafta FROM kitap WHERE id = 1) AS kitap1_rafta;
ROLLBACK;

SELECT 'geri alindiktan sonra' AS an, (SELECT COUNT(*) FROM odunc) AS odunc_sayisi,
       (SELECT rafta FROM kitap WHERE id = 1) AS kitap1_rafta;
SQL
rm -f k2.db
```

```text
┌──────────────┬──────────────┬──────────────┐
│      an      │ odunc_sayisi │ kitap1_rafta │
├──────────────┼──────────────┼──────────────┤
│ islem icinde │ 1            │ 0            │
└──────────────┴──────────────┴──────────────┘
┌───────────────────────┬──────────────┬──────────────┐
│          an           │ odunc_sayisi │ kitap1_rafta │
├───────────────────────┼──────────────┼──────────────┤
│ geri alindiktan sonra │ 0            │ 1            │
└───────────────────────┴──────────────┴──────────────┘
```

İşlem içinde ödünç kaydı vardı ve kitap raftan düşmüştü. Geri alma sonrasında ikisi de eski
hâline döndü — tek tek değil, **birlikte**. Atomiklik budur: geri alma kısmi bir sonuç
bırakmaz.

İşlem içindeki `SELECT` deyimi, henüz kesinleşmemiş kendi değişikliklerini görüyor. Bu,
işlemin kendi içinde tutarlı olmasının gereğidir; aynı anda başka bir bağlantının bu
satırları görüp göremeyeceği ayrı bir sorudur ve yalıtım düzeyleri dersinde ele alınacaktır.

`COMMIT` yazılsaydı iki değişiklik de kalıcı olurdu. Kesinleştirmenin kalıcılık anlamı da
tanımlıdır: `COMMIT` başarıyla döndüğünde değişiklikler, hemen ardından gelen bir güç
kesintisinde bile kaybolmaz. Motorun bunu nasıl sağladığı — yazma öncesi günlük — İlişkisel
Veritabanı Yönetimi kursunun konusudur.

#### Deyim Hatası İşlemi Bitirmez

Sık yapılan bir varsayım, işlem içindeki bir deyimin hata vermesi hâlinde işlemin
kendiliğinden geri alınacağıdır. Standart davranış bu değildir: hata veren **deyim**
etkisiz kalır, işlem açık kalmaya devam eder ve kararı uygulama verir.

Aşağıdaki şemada kısmi bir tekillik dizini var: bir kitabın aynı anda birden çok açık ödünç
kaydı olamaz. İkinci kez ödünç verme denemesi bu kısıta takılır:

```bash
rm -f k3.db
sqlite3 -box -header k3.db <<'SQL'
CREATE TABLE kitap(id INTEGER PRIMARY KEY, baslik TEXT, rafta INT NOT NULL DEFAULT 1);
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT);
CREATE UNIQUE INDEX odunc_acik ON odunc(kitap_id) WHERE iade IS NULL;
INSERT INTO kitap VALUES (1,'Kayip Zaman',0),(2,'Sessiz Ev',1);
INSERT INTO odunc(kitap_id, uye_id, alis) VALUES (1, 4, '2024-03-11');

BEGIN;
INSERT INTO odunc(kitap_id, uye_id, alis) VALUES (1, 7, '2024-03-12');
UPDATE kitap SET rafta = 0 WHERE id = 1;
SELECT 'hata sonrasi islem icinde' AS an, COUNT(*) AS odunc_sayisi FROM odunc;
ROLLBACK;
SELECT 'geri alindiktan sonra' AS an, COUNT(*) AS odunc_sayisi FROM odunc;
SQL
rm -f k3.db
```

```text
Runtime error near line 8: UNIQUE constraint failed: odunc.kitap_id (19)
┌───────────────────────────┬──────────────┐
│            an             │ odunc_sayisi │
├───────────────────────────┼──────────────┤
│ hata sonrasi islem icinde │ 1            │
└───────────────────────────┴──────────────┘
┌───────────────────────┬──────────────┐
│          an           │ odunc_sayisi │
├───────────────────────┼──────────────┤
│ geri alindiktan sonra │ 1            │
└───────────────────────┴──────────────┘
```

Üç şey birden görülüyor. Birincisi, hatalı `INSERT` etkisiz kaldı; ödünç sayısı 1'de
kaldı. İkincisi, işlem **kapanmadı**: sonraki deyimler çalışmayı sürdürdü ve `UPDATE`
uygulandı. Üçüncüsü, geri alma bu `UPDATE` etkisini de sildi.

İkinci nokta, gerçek bir hata kaynağıdır. Hata iletisi göz ardı edilip `COMMIT` yazılsaydı,
ödünç kaydı açılmadığı hâlde kitap raftan düşmüş olarak kesinleşecekti — tam olarak
kaçınılmak istenen tutarsızlık. Bu yüzden uygulama kodunda her deyimin sonucu denetlenir
ve hata durumunda açıkça `ROLLBACK` çalıştırılır.

Hata iletisinin biçimi motora göre değişir; buradaki metin, kısıtın adını ve türünü veren
tipik bir örnektir. Değişmeyen, ihlalin **deyim düzeyinde** raporlanması ve işlemin
kaderinin uygulamaya bırakılmasıdır. Bazı motorlarda işlem, hatadan sonra yalnız geri alma
kabul eden bir kilitli duruma geçer; bu ayrımın hedef motorda bilinmesi gerekir.

#### Bağlantı Koptuğunda

Kesinleştirilmemiş bir işlem, bağlantı sona erdiğinde geri alınır. Bu, uygulamanın açık bir
şey yapmasını gerektirmez; motorun garantisidir. Aşağıdaki betik bir işlem başlatıp satır
ekler ve `COMMIT` yazmadan süreci sonlandırır:

```bash
cat > kesintili.mjs <<'JS'
import { DatabaseSync } from 'node:sqlite';

const db = new DatabaseSync('kesinti.db');
db.exec(`
  CREATE TABLE IF NOT EXISTS odunc(id INTEGER PRIMARY KEY, kitap_id INT,
                                   uye_id INT, alis TEXT);
`);
db.exec("BEGIN");
db.exec("INSERT INTO odunc(kitap_id, uye_id, alis) VALUES (1, 4, '2024-03-11')");
console.log('işlem içinde görülen satır:',
            db.prepare('SELECT COUNT(*) AS n FROM odunc').get().n);
process.exit(0);
JS
rm -f kesinti.db
node kesintili.mjs
sqlite3 -box -header kesinti.db 'SELECT COUNT(*) AS yeniden_acildiginda FROM odunc;'
rm -f kesinti.db
```

```text
işlem içinde görülen satır: 1
┌─────────────────────┐
│ yeniden_acildiginda │
├─────────────────────┤
│ 0                   │
└─────────────────────┘
```

Süreç kendi işlemi içinde satırı gördü; veritabanı dosyası yeniden açıldığında satır yoktu.
Tablo tanımı kalıcı oldu, çünkü `CREATE TABLE` işlem başlamadan önce otomatik kesinleştirme
kipinde çalıştı.

Bu davranış, kesinleştirmenin bir eşik olduğunu gösterir: `COMMIT` dönmeden önce hiçbir şey
söz verilmemiştir. Uygulama tarafında bunun karşılığı, işlemi açan kodun her çıkış yolunda
— hata, erken dönüş, kesinti — işlemi kapatmasını güvenceye almaktır.

#### İşlemin Sınırını Belirlemek

İşlemin ne kadar geniş tutulacağı bir tasarım kararıdır ve iki yönlü baskı altındadır.

Dar tutulursa birlikte olması gereken değişiklikler ayrı işlemlere düşer ve arada kalan bir
hata tutarsızlık bırakır. İşlemin sınırı, **iş kuralının** sınırıdır: "ödünç verme"
işleminde kayıt açma ile raf durumu güncelleme aynı işlemde olmalıdır.

Geniş tutulursa işlem uzun süre açık kalır; tuttuğu kaynaklar başka bağlantıları bekletir
ve geri alma maliyeti büyür. Kullanıcı girdisi beklerken açık işlem tutmak bu hatanın en
bilinen biçimidir: veritabanı, kullanıcının yanıtını bekleyerek dakikalarca kilit
tutabilir.

Bu iki baskının nasıl dengeleneceği kilitleme davranışı dersinde somutlaşacaktır. Şimdilik
kural olarak: işlem, iş kuralının gerektirdiği en küçük deyim kümesini kapsar ve
başladıktan sonra dış dünyayı beklemez.

#### Özet

- Açık işlem başlatılmadan çalıştırılan her deyim kendi başına bir işlemdir; bu kipe
  otomatik kesinleştirme denir.
- `BEGIN` ile açılan işlemde değişiklikler `COMMIT` çalıştırılana kadar kalıcı değildir;
  `ROLLBACK` işlem boyunca yapılmış tüm değişiklikleri birlikte siler.
- Deyim hatası yalnız o deyimi etkisiz kılar; işlem açık kalır ve geri alma kararı
  uygulamaya aittir, hatadan sonra kesinleştirmek kısmi sonucu kalıcı yapar.
- Kesinleştirilmemiş bir işlem, bağlantı sona erdiğinde geri alınır.
- İşlemin sınırı iş kuralının sınırıdır; gereğinden geniş tutulan işlem kaynak tutar ve
  başka bağlantıları bekletir.

#### Sonraki Adım

Geri alma bu derste bir "tümünü sil" düğmesiydi: işlemin başına dönmekten başka seçenek
yoktu. Toplu işlerde bu fazla kaba kalır — yüz kitabın envanter kaydı işlenirken üçüncüsü
hatalıysa, ilk ikisini de atmak gereksizdir. Sonraki ders işlem içinde ara duraklar
tanımlamayı, yalnız o duraktan sonrasını geri almayı ve iç içe geri alma noktalarının nasıl
davrandığını gösterecek.

### Kaydetme Noktaları

Source: https://academia.sh/tr/kurslar/ileri-sql/kaydetme-noktalari

SAVEPOINT ile işlem içinde ara durak tanımlama, ROLLBACK TO ile kısmi geri alma, RELEASE'in kesinleştirme olmadığı, iç içe noktaların davranışı ve aynı noktaya birden çok kez dönme.

Önceki derste geri alma tek düğmeliydi: `ROLLBACK` işlemin başına döner, arada yapılmış her
şeyi siler. Toplu işlerde bu fazla kaba kalır. Yüz kitaplık bir envanter aktarımında
üçüncü kayıt hatalıysa, ilk ikisini de atmak gereksiz iş demektir; aktarımın baştan
başlaması gerekir.

Standart SQL bunun için işlem içinde ara duraklar tanımlar. **Kaydetme noktası
(savepoint)**, işlemin belirli bir anına verilen addır; geri alma o ada kadar yapılabilir
ve öncesi korunur.

#### Ara Durak Tanımlamak

Üç deyim vardır. `SAVEPOINT ad` bulunulan noktaya ad verir. `ROLLBACK TO ad` o noktadan
sonra yapılmış değişiklikleri siler, ama işlemi bitirmez. `RELEASE ad` noktayı listeden
düşürür ve o noktaya artık dönülemez.

```bash
sqlite3 -box -header <<'SQL'
CREATE TABLE kitap(id INTEGER PRIMARY KEY, baslik TEXT NOT NULL, tur TEXT NOT NULL);
INSERT INTO kitap VALUES (1,'Kayip Zaman','roman');

BEGIN;
INSERT INTO kitap VALUES (11,'Ada','roman');

SAVEPOINT parti2;
INSERT INTO kitap VALUES (12,'Kuyu','roman');
INSERT INTO kitap VALUES (13,'Yaz Gunlugu','deneme');
ROLLBACK TO parti2;

INSERT INTO kitap VALUES (14,'Kum Saati','siir');
COMMIT;

SELECT id, baslik FROM kitap ORDER BY id;
SQL
```

```text
┌────┬─────────────┐
│ id │   baslik    │
├────┼─────────────┤
│ 1  │ Kayip Zaman │
│ 11 │ Ada         │
│ 14 │ Kum Saati   │
└────┴─────────────┘
```

Dört satır eklenmeye çalışıldı, ikisi kesinleşti. `parti2` noktasından sonra eklenen 12 ve 13
numaralı kitaplar silindi; noktadan önce eklenen 11 numaralı kitap korundu. Geri almadan
sonra işlem sürdü ve 14 numaralı kitap eklendi — `ROLLBACK TO`, işlemi kapatmaz.

Bu, toplu aktarımın standart deseninin temelidir: her parti öncesinde bir kaydetme noktası
konur, parti hatasız biterse nokta serbest bırakılır, hata verirse o noktaya dönülüp parti
atlanır. Önceki partilerin işi korunur.

#### RELEASE Kesinleştirme Değildir

`RELEASE` sözcüğü yanıltıcıdır; hiçbir şeyi kalıcı yapmaz. Yaptığı tek şey, o adı geçerli
noktalar listesinden çıkarmaktır. Nokta serbest bırakıldıktan sonra yapılan değişiklikler
hâlâ işlemin parçasıdır ve dış `ROLLBACK` onları da siler:

```bash
sqlite3 -box -header <<'SQL'
CREATE TABLE kitap(id INTEGER PRIMARY KEY, baslik TEXT NOT NULL, tur TEXT NOT NULL);

BEGIN;
SAVEPOINT parti;
INSERT INTO kitap VALUES (11,'Ada','roman');
RELEASE parti;
SELECT 'release sonrasi' AS an, COUNT(*) AS satir FROM kitap;
ROLLBACK;
SELECT 'rollback sonrasi' AS an, COUNT(*) AS satir FROM kitap;
SQL
```

```text
┌─────────────────┬───────┐
│       an        │ satir │
├─────────────────┼───────┤
│ release sonrasi │ 1     │
└─────────────────┴───────┘
┌──────────────────┬───────┐
│        an        │ satir │
├──────────────────┼───────┤
│ rollback sonrasi │ 0     │
└──────────────────┴───────┘
```

Serbest bırakma sonrasında satır duruyordu; işlemin geri alınmasıyla kayboldu. Kalıcılığı
sağlayan tek deyim `COMMIT`'tir. Kaydetme noktaları, işlemin **içindeki** yapıyı düzenler;
işlemin dışarıya karşı sözünü değiştirmez.

Bu ayrım, kaydetme noktalarının "iç içe işlem" olmadığını da söyler. Gerçek bir iç içe
işlem, iç işlemi dış işlemden bağımsız kesinleştirebilirdi. Kaydetme noktalarında böyle bir
şey yoktur: dış işlem geri alınırsa içerideki her şey gider.

#### İç İçe Noktalar

Kaydetme noktaları yığıt gibi davranır: her yeni nokta öncekilerin üstüne konur. Bir
noktaya dönüldüğünde, o noktadan sonra tanımlanmış tüm noktalar da geçersizleşir.

```bash
sqlite3 -box -header <<'SQL'
CREATE TABLE kitap(id INTEGER PRIMARY KEY, baslik TEXT NOT NULL, tur TEXT NOT NULL);

BEGIN;
INSERT INTO kitap VALUES (11,'Ada','roman');
SAVEPOINT dis;
  INSERT INTO kitap VALUES (12,'Kuyu','roman');
  SAVEPOINT ic;
    INSERT INTO kitap VALUES (13,'Yaz Gunlugu','deneme');
  ROLLBACK TO ic;
  INSERT INTO kitap VALUES (14,'Kum Saati','siir');
ROLLBACK TO dis;
INSERT INTO kitap VALUES (15,'Uzak Kiyi','roman');
COMMIT;

SELECT id, baslik FROM kitap ORDER BY id;
SQL
```

```text
┌────┬───────────┐
│ id │  baslik   │
├────┼───────────┤
│ 11 │ Ada       │
│ 15 │ Uzak Kiyi │
└────┴───────────┘
```

Beş ekleme yapıldı, ikisi kaldı. `ROLLBACK TO ic` yalnız 13 numaralı satırı sildi; 12 ve 14
ayakta kaldı. Sonra `ROLLBACK TO dis` çalıştı ve `dis` noktasından sonraki her şeyi —
12 ve 14 numaralı satırları, ayrıca `ic` noktasının kendisini — sildi. Dış noktadan önce
eklenen 11 numaralı satır korundu, geri almadan sonra eklenen 15 numaralı satır kesinleşti.

Girintiler yalnız okumaya yardım eder; SQL'de anlamları yoktur. Yığıt davranışını okunur
kılan tek şey, adların kapsamı yansıtacak biçimde seçilmesidir.

#### Aynı Noktaya Birden Çok Kez Dönmek

`ROLLBACK TO`, geri döndüğü noktayı **tüketmez**. Nokta yerinde kalır ve tekrar
kullanılabilir. Bu, bir işlemin içinde birkaç seçeneği sırayla denemeyi mümkün kılar:

```bash
sqlite3 -box -header <<'SQL'
CREATE TABLE kitap(id INTEGER PRIMARY KEY, baslik TEXT NOT NULL, tur TEXT NOT NULL);
INSERT INTO kitap VALUES (11,'Ada','roman');

BEGIN;
SAVEPOINT deneme;
INSERT INTO kitap VALUES (11,'Cakisan','roman');
ROLLBACK TO deneme;
INSERT INTO kitap VALUES (12,'Kuyu','roman');
ROLLBACK TO deneme;
INSERT INTO kitap VALUES (13,'Yaz Gunlugu','deneme');
RELEASE deneme;
COMMIT;

SELECT id, baslik FROM kitap ORDER BY id;
SQL
```

```text
Runtime error near line 6: UNIQUE constraint failed: kitap.id (19)
┌────┬─────────────┐
│ id │   baslik    │
├────┼─────────────┤
│ 11 │ Ada         │
│ 13 │ Yaz Gunlugu │
└────┴─────────────┘
```

`deneme` noktasına iki kez dönüldü ve ikisinde de çalıştı. İlk deneme kısıt ihlaline
takıldı; ikincisi başarılıydı ama yine geri alındı; üçüncüsü kesinleşti. Bir öncekiler
işlemde iz bırakmadı.

Aynı ada ikinci kez `SAVEPOINT` yazılırsa yeni bir nokta oluşur ve eskisi gölgelenir; geri
alma en son tanımlanana döner. Karışıklığı önlemek için adların benzersiz seçilmesi ya da
noktanın kullanımdan sonra serbest bırakılması gerekir.

#### Parti Deseni

Desenin bütünü, hatayı yakalayıp karar veren bir uygulama koduyla tamamlanır. Aşağıdaki
betik üç partilik bir aktarım yapar; ikinci partide çakışan bir kimlik vardır:

```bash
cat > aktarim.mjs <<'JS'
import { DatabaseSync } from 'node:sqlite';

const db = new DatabaseSync(':memory:');
db.exec(`CREATE TABLE kitap(id INTEGER PRIMARY KEY, baslik TEXT NOT NULL,
                            tur TEXT NOT NULL);
         INSERT INTO kitap VALUES (11,'Ada','roman');`);

const partiler = [
  [[21, 'Kuyu', 'roman'], [22, 'Kum Saati', 'siir']],
  [[23, 'Yaz Gunlugu', 'deneme'], [11, 'Cakisan', 'roman']],
  [[24, 'Uzak Kiyi', 'roman']],
];

const ekle = db.prepare('INSERT INTO kitap VALUES (?, ?, ?)');
db.exec('BEGIN');
partiler.forEach((parti, sira) => {
  db.exec(`SAVEPOINT parti_${sira}`);
  try {
    for (const satir of parti) ekle.run(...satir);
    db.exec(`RELEASE parti_${sira}`);
    console.log(`parti ${sira}: kabul (${parti.length} satır)`);
  } catch (hata) {
    db.exec(`ROLLBACK TO parti_${sira}`);
    console.log(`parti ${sira}: atlandı — ${hata.message}`);
  }
});
db.exec('COMMIT');

for (const s of db.prepare('SELECT id, baslik FROM kitap ORDER BY id').all()) {
  console.log(s.id, s.baslik);
}
JS
node aktarim.mjs
```

```text
parti 0: kabul (2 satır)
parti 1: atlandı — UNIQUE constraint failed: kitap.id
parti 2: kabul (1 satır)
11 Ada
21 Kuyu
22 Kum Saati
24 Uzak Kiyi
```

İkinci partinin ilk satırı — 23 numaralı kitap — geçerliydi ve eklenmişti; parti geri
alınınca o da gitti. Bu, desenin bilinçli bir tercihidir: parti bir bütün olarak kabul
edilir ya da tümüyle reddedilir. Geçerli satırların korunması isteniyorsa parti boyutu bire
indirilir, o zaman da her satır için ayrı bir nokta maliyeti doğar.

Betikte `RELEASE` yalnız başarılı partilerde çalışıyor. Başarısız parti için nokta yerinde
kalır, ancak `COMMIT` tüm noktaları kapattığı için bu bir sızıntı oluşturmaz.

#### Nerede Kullanılır

Kaydetme noktalarının üç tipik kullanım alanı vardır.

**Toplu aktarım.** Her partiden önce nokta konur; parti hatalıysa o noktaya dönülüp kayıt
bir hata kütüğüne yazılır ve aktarım sürer. Alternatif, her partiyi ayrı işlem yapmaktır;
o zaman da partiler arası bütünlük garantisi kaybolur.

**Denenip vazgeçilebilen adımlar.** Bir hesabın maliyetli bir yolu önce denenir, sonuç
uygun değilse geri alınıp başka yol seçilir. İşlemin bütünü açık kaldığı için ara sonuçlar
dışarıya sızmaz.

**Kütüphane sınırlarında yeniden kullanılabilir kod.** Kendisi işlem açıp açmadığını
bilmeyen bir işlev, koşulsuz `BEGIN` yazamaz — zaten açık bir işlemin içindeyse hata alır.
Bunun yerine bir kaydetme noktası açar; hata olursa yalnız kendi yaptığını geri alır ve
çağıranın işlemine dokunmaz. Çoğu veri erişim kitaplığındaki "iç içe işlem" desteği
gerçekte budur.

Maliyet tarafında dikkat edilecek nokta, kaydetme noktalarının bedava olmamasıdır: motor,
her noktadan sonraki değişiklikleri geri alabilmek için ek kayıt tutar. Satır başına bir
nokta açan bir aktarım, işin kendisinden çok muhasebesini büyütür. Nokta sayısı parti
düzeyinde tutulur, satır düzeyinde değil.

#### Özet

- `SAVEPOINT` işlem içinde bir ara durağa ad verir; `ROLLBACK TO` o noktadan sonrasını
  siler ve işlemi kapatmaz.
- `RELEASE` kesinleştirme değildir; yalnız noktayı listeden düşürür, kalıcılığı sağlayan
  tek deyim `COMMIT`'tir.
- Noktalar yığıt gibi davranır: bir noktaya dönülünce ondan sonra tanımlanmış noktalar da
  geçersizleşir.
- `ROLLBACK TO` noktayı tüketmez; aynı noktaya birden çok kez dönülerek farklı seçenekler
  denenebilir.
- Nokta başına ek kayıt tutulduğu için kaydetme noktaları parti düzeyinde kullanılır, satır
  düzeyinde değil.

#### Sonraki Adım

Bu iki ders boyunca veritabanına yalnız bir bağlantı erişiyordu; işlemin gördüğü tek şey
kendi değişiklikleriydi. Gerçek sistemlerde ise aynı anda birçok bağlantı okur ve yazar.
"Kesinleşmemiş değişikliği başkası görebilir mi", "aynı sorgu işlem içinde iki kez
çalıştırılınca aynı sonucu verir mi" soruları o zaman anlam kazanır. Sonraki ders, bu
soruların yanıtlarını düzeye bağlayan yalıtım kavramını tanımlayacak ve standartta
tanımlanan okuma anomalilerini tek tek ele alacak.

### Yalıtım Düzeyleri

Source: https://academia.sh/tr/kurslar/ileri-sql/yalitim-duzeyleri

Kirli okuma, tekrarlanamayan okuma ve hayalet satır anomalileri, standartta tanımlı dört yalıtım düzeyi, iki bağlantıyla gerçek bir gözlem ve gösterilemeyen düzeyin model üzerinden anlatımı.

Önceki iki ders boyunca veritabanına yalnız bir bağlantı erişiyordu. İşlem kendi
değişikliklerini görüyor, dışarıda bir şey olmuyordu. Gerçek sistemlerde ise aynı anda
birçok bağlantı okur ve yazar; o zaman iki soru anlam kazanır: bir işlem, başka bir işlemin
henüz kesinleştirmediği değişikliği görebilir mi? Aynı sorgu bir işlem içinde iki kez
çalıştırılırsa aynı sonucu verir mi?

**Yalıtım (isolation)**, bu soruların yanıtını belirleyen özelliktir. İlişkisel Kuram
kursundaki ACID sıralamasının üçüncü harfi budur. Tam yalıtım, eşzamanlı çalışan işlemlerin
sonucunun, onları sırayla çalıştırmakla aynı olmasını ister. Bu güçlü garanti pahalıdır; bu
yüzden standart, gevşetilmiş düzeyler tanımlar ve her düzeyi **hangi anomaliye izin
verdiğiyle** tarif eder.

#### Üç Anomali

Standart üç okuma anomalisi tanımlar. Üçü de "iki işlem araya girdiğinde okunan verinin
bozulması" ile ilgilidir, ancak bozulmanın biçimi farklıdır.

**Kirli okuma (dirty read).** Bir işlem, başka bir işlemin henüz kesinleştirmediği
değişikliği okur. Diğer işlem geri alınırsa, okunan değer hiç var olmamış bir değerdir.

**Tekrarlanamayan okuma (non-repeatable read).** Bir işlem aynı satırı iki kez okur ve
farklı değerler görür; arada başka bir işlem o satırı güncelleyip kesinleştirmiştir.

**Hayalet satır (phantom row).** Bir işlem aynı koşulu iki kez sorgular ve ikincisinde
koşula uyan **yeni satırlar** görür; arada başka bir işlem satır eklemiştir. Tekrarlanamayan
okumadan farkı, değişenin bir satırın değeri değil, kümenin üyeliği olmasıdır.

Ayrım pratikte önemlidir, çünkü satır kilitleriyle önlenebilen ile önlenemeyen buradan
ayrılır: var olan bir satır kilitlenebilir, henüz var olmayan bir satır kilitlenemez.

#### Dört Düzey

Standart dört düzey tanımlar. Her düzey, kendinden gevşek olanların önlediklerini de önler:

| Düzey | Kirli okuma | Tekrarlanamayan okuma | Hayalet satır |
|---|---|---|---|
| `READ UNCOMMITTED` | olabilir | olabilir | olabilir |
| `READ COMMITTED` | olamaz | olabilir | olabilir |
| `REPEATABLE READ` | olamaz | olamaz | olabilir |
| `SERIALIZABLE` | olamaz | olamaz | olamaz |

Tabloyu okurken iki nokta gözden kaçmamalıdır.

Birincisi, tablo **izin verilenleri** sıralar, olacakları değil. `READ COMMITTED` düzeyinde
tekrarlanamayan okuma "olabilir"; her zaman olur demek değildir. Bir motor, daha güçlü bir
garanti sunup yine de o düzeyin adını kullanabilir.

İkincisi, düzeyler bir gerçekleştirim değil, bir **sözleşmedir**. Aynı adı taşıyan iki
motor bunu farklı biçimlerde sağlayabilir: biri kilitleme, diğeri çok sürümlü eşzamanlılık
denetimi kullanabilir. Sonuç, yasaklanan anomalilerin ortaya çıkmamasıdır; yol serbesttir.

Düzey, standartta `SET TRANSACTION ISOLATION LEVEL` deyimiyle seçilir. Hangi düzeylerin
gerçekten sunulduğu motora göre değişir: bazı motorlar dördünü de kabul eder ama bir kısmını
daha güçlü bir düzeye eşler, bazıları hiç seçim sunmaz. Aşağıdaki gözlemin yapıldığı motor
ikinci gruptadır — düzey seçimi yoktur, okumalar tutarlı bir görüntü üzerinden yapılır.

#### İki Bağlantıyla Gözlem

Aşağıdaki betik aynı veritabanı dosyasına **iki ayrı bağlantı** açar ve araya girmeyi
denetimli biçimde üretir. Önce okuyucu bir işlem açmadan iki kez okur; sonra aynı okumayı
açık bir işlem içinde yapar. Yazar her iki durumda da aynı işi yapar: bir satırı günceller
ve yeni bir açık ödünç kaydı ekler.

```bash
rm -f yalitim.db yalitim.db-wal yalitim.db-shm
cat > yalitim.mjs <<'JS'
import { DatabaseSync } from 'node:sqlite';

const okuyucu = new DatabaseSync('yalitim.db');
const yazar = new DatabaseSync('yalitim.db');

okuyucu.exec('PRAGMA journal_mode = WAL');
yazar.exec(`
  CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT,
                     iade TEXT);
  INSERT INTO odunc VALUES (1,1,4,'2024-03-01',NULL),(2,3,4,'2024-03-04',NULL),
                           (3,5,7,'2024-03-06',NULL);
`);

const oku = (b) => b.prepare(
  `SELECT (SELECT alis FROM odunc WHERE id = 1) AS kayit1_alis,
          (SELECT COUNT(*) FROM odunc WHERE iade IS NULL) AS acik_kayit`).get();

console.log('— işlem açmadan okuma —');
console.log('1. okuma:', JSON.stringify(oku(okuyucu)));
yazar.exec("UPDATE odunc SET alis = '2024-03-02' WHERE id = 1");
yazar.exec("INSERT INTO odunc VALUES (4,7,4,'2024-03-11',NULL)");
console.log('yazar bir satırı güncelledi, bir satır ekledi, kesinleştirdi');
console.log('2. okuma:', JSON.stringify(oku(okuyucu)));

console.log('— işlem içinde okuma —');
okuyucu.exec('BEGIN');
console.log('1. okuma:', JSON.stringify(oku(okuyucu)));
yazar.exec("UPDATE odunc SET alis = '2024-03-03' WHERE id = 1");
yazar.exec("INSERT INTO odunc VALUES (5,9,7,'2024-03-13',NULL)");
console.log('yazar aynı işi bir kez daha yaptı ve kesinleştirdi');
console.log('2. okuma:', JSON.stringify(oku(okuyucu)));
okuyucu.exec('COMMIT');
console.log('işlem bitti, 3. okuma:', JSON.stringify(oku(okuyucu)));
JS
node yalitim.mjs
rm -f yalitim.db yalitim.db-wal yalitim.db-shm
```

```text
— işlem açmadan okuma —
1. okuma: {"kayit1_alis":"2024-03-01","acik_kayit":3}
yazar bir satırı güncelledi, bir satır ekledi, kesinleştirdi
2. okuma: {"kayit1_alis":"2024-03-02","acik_kayit":4}
— işlem içinde okuma —
1. okuma: {"kayit1_alis":"2024-03-02","acik_kayit":4}
yazar aynı işi bir kez daha yaptı ve kesinleştirdi
2. okuma: {"kayit1_alis":"2024-03-02","acik_kayit":4}
işlem bitti, 3. okuma: {"kayit1_alis":"2024-03-03","acik_kayit":5}
```

Birinci bölümde okuyucu işlem açmadı; her `SELECT` kendi başına bir işlemdi. İki okuma
arasında hem satırın değeri değişti — tekrarlanamayan okumanın tanımı — hem de koşula uyan
satır sayısı arttı — hayalet satırın tanımı. Bu bir hata değildir: iki ayrı işlem arasında
dünyanın değişmesi beklenen şeydir.

İkinci bölümde okuyucu `BEGIN` ile bir işlem açtı. Yazar aynı iki değişikliği yaptı ve
kesinleştirdi, ancak okuyucunun ikinci okuması **hiç değişmedi**: ne değer ne sayım. İşlem
kapandıktan sonra yapılan üçüncü okuma yeni durumu gösterdi. Aradaki fark, işlemin ilk
okumada aldığı tutarlı görüntüyü sonuna kadar koruması, yani anlık görüntü üzerinden okumaya
devam etmesidir.

Gözlem, tablonun ilk üç satırının farkını somutlaştırır. İşlem açmadan okuyan taraf,
`READ COMMITTED` düzeyinin izin verdiği davranışı gösterdi: yalnız kesinleşmiş veriyi gördü,
ama tekrarlanan okumaları farklıydı. İşlem içinde okuyan taraf, `SERIALIZABLE` düzeyinin
okuma tarafındaki davranışını gösterdi: hem tekrarlanamayan okuma hem hayalet satır ortadan
kalktı.

#### Gösterilemeyen Düzey

Tablodaki dört düzeyden biri bu motorda gözlenemez. `READ UNCOMMITTED`, kesinleşmemiş
değişikliğin okunmasına izin verir; kullanılan motor bunu hiçbir ayarla sunmaz, çünkü
okumalar zaten kesinleşmiş bir anlık görüntüden yapılır. Kirli okumanın sonucunun ne
olduğunu göstermek için gerçek bir motor çıktısı üretilemez.

Bunun yerine davranış **modellenebilir**. Aşağıdaki betik bir veritabanı değildir; tek bir
kaydın kesinleşmiş ve kesinleşmemiş değerlerini tutan küçük bir zamanlama modelidir. Kirli
okumaya izin veren ve vermeyen iki durumda okuyucunun ne göreceğini karşılaştırır:

```bash
cat > kirli-model.mjs <<'JS'
// Kirli okuma modeli: kesinleşmemiş değeri gören bir motorun davranışını taklit
// eder. Gerçek bir motor çıktısı değildir; amaç sonucun ne olduğunu göstermektir.
const kayit = { rafta: 1, kesinlesmemis: null };

const oku = (kirliOkumaVar) =>
  kirliOkumaVar && kayit.kesinlesmemis !== null ? kayit.kesinlesmemis : kayit.rafta;

const dene = (kirliOkumaVar) => {
  kayit.kesinlesmemis = 0;              // yazar: kitabı ödünç verdi, henüz kesinleşmedi
  const gorulen = oku(kirliOkumaVar);   // okuyucu araya girdi
  kayit.kesinlesmemis = null;           // yazar: işlemi geri aldı
  return { okuyucununGordugu: gorulen, geriAlmaSonrasiGercek: kayit.rafta };
};

console.log('kirli okumaya izin veren düzey    :', JSON.stringify(dene(true)));
console.log('kirli okumaya izin vermeyen düzey :', JSON.stringify(dene(false)));
JS
node kirli-model.mjs
```

```text
kirli okumaya izin veren düzey    : {"okuyucununGordugu":0,"geriAlmaSonrasiGercek":1}
kirli okumaya izin vermeyen düzey : {"okuyucununGordugu":1,"geriAlmaSonrasiGercek":1}
```

Birinci satırda okuyucu kitabı ödünç verilmiş gördü; yazar işlemi geri aldığı için o değer
hiçbir zaman gerçek olmadı. Kirli okumanın kötülüğü budur: eski bir değer değil, **hiç var
olmamış** bir değer okunur. Buna dayanarak verilen bir karar — kitabı rezerve etmemek gibi —
hiçbir gerçek duruma karşılık gelmez.

Bu blok bir model olduğu için, çıktısı bir motorun davranışı hakkında kanıt değildir; yalnız
tanımın sonucunu görünür kılar. Bir motorda `READ UNCOMMITTED` gerçekten sunuluyorsa, aynı
sonucun o motorda ölçülmesi gerekir.

#### Düzey Seçmek

Düşük düzeyler daha az koordinasyon, dolayısıyla daha yüksek eşzamanlılık sağlar. Bedeli,
uygulamanın anomalilerle kendi başına baş etmesidir.

Karar, işin okuduğu veriye ne yaptığına bakılarak verilir. Okunan değer yalnız
görüntüleniyorsa gevşek bir düzey yeterlidir; okunan değere dayanarak **yazma** yapılıyorsa
— "rafta mı diye bak, raftaysa ödünç ver" — gevşek düzey sessiz bir hataya kapı açar. İki
işlem aynı anda rafta görüp aynı kitabı iki üyeye verebilir.

Bu duruma **kayıp güncelleme** denir ve yalnız yalıtım düzeyi yükseltilerek değil, okumayı
kilitleyerek de önlenebilir. İki yolun da bedeli, aynı satıra dokunan işlemlerin
birbirini beklemesidir. Sonraki dersin konusu bu bekleme mekanizmasıdır.

#### Özet

- Kirli okuma kesinleşmemiş veriyi, tekrarlanamayan okuma değişmiş bir satır değerini,
  hayalet satır koşula uyan yeni satırları görür.
- Standart dört düzeyi hangi anomaliye izin verdikleriyle tanımlar; tablo izin verilenleri
  sıralar, olacakları değil.
- Düzey bir sözleşmedir, gerçekleştirim değil; aynı adı taşıyan iki motor bunu kilitleme ya
  da çok sürümlü denetimle sağlayabilir.
- İki bağlantılı gözlemde işlem açmadan okuyan taraf hem değer hem satır sayısı değişikliğini
  gördü; açık işlem içinde okuyan taraf ilk okumadaki görüntüyü sonuna kadar korudu.
- Okunan değere dayanarak yazma yapılıyorsa gevşek düzey kayıp güncellemeye kapı açar.

#### Sonraki Adım

Yalıtım düzeyleri neyin görünüp neyin görünmeyeceğini söyler, ama bunun bedelini
söylemez. İki işlem aynı satıra yazmak istediğinde biri beklemek zorundadır; beklemenin ne
kadar süreceği, neyin kilitlendiğine bağlıdır. İki işlem birbirinin tuttuğu kaynağı
beklerse hiçbiri ilerleyemez. Sonraki ders kilit türlerini, gerçek bir çakışmada motorun
verdiği hatayı ve karşılıklı beklemenin nasıl çözüldüğünü ele alacak.

### Kilitleme Davranışı

Source: https://academia.sh/tr/kurslar/ileri-sql/kilitleme-davranisi

Paylaşımlı ve dışlayıcı kilitler, kilit ayrıntı düzeyi, iki bağlantıyla üretilen gerçek bir çakışma ve hata iletisi, yükseltme çakışması, kilitlenme döngüsü ve bekleme süresi sınırı.

Yalıtım düzeyleri neyin görüneceğini söyler, bedelini söylemez. Bedel, aynı veriye dokunan
işlemlerin birbirini beklemesidir. Beklemeyi yöneten mekanizma **kilittir (lock)**: bir
işlem bir kaynağa dokunmadan önce üzerinde hak alır, işi bitene kadar tutar, sonra bırakır.

Bu ders kilit türlerini tanımlar, iki bağlantıyla gerçek bir çakışma üretir ve motorun
verdiği hatayı okur.

#### İki Kilit Türü

Temel model iki kilit türüne dayanır.

**Paylaşımlı kilit (shared lock)** okuma içindir. Aynı kaynak üzerinde birden çok
paylaşımlı kilit birlikte bulunabilir; okuyucular birbirini engellemez.

**Dışlayıcı kilit (exclusive lock)** yazma içindir. Bir kaynakta dışlayıcı kilit varsa
üzerine ne paylaşımlı ne başka bir dışlayıcı kilit konabilir.

Uyuşma kuralı tek cümleyle özetlenir: okuma okumayı engellemez, yazma her şeyi engeller.
Çok sürümlü eşzamanlılık denetimi kullanan motorlar bu kuralı okuma tarafında gevşetir —
okuyucular eski sürümü gördüğü için yazarı beklemez. Bu, İlişkisel Veritabanı Yönetimi
kursunda ele alınan bir gerçekleştirim seçimidir.

Kilidin **ne** üzerine konduğu ayrı bir eksendir: satır, sayfa, tablo ya da veritabanının
tamamı. İnce ayrıntı düzeyi eşzamanlılığı artırır ama kilit sayısını ve muhasebesini
büyütür. Aşağıdaki gözlemin yapıldığı motor, yazma için veritabanının tamamını kilitler;
satır kilidi kullanan bir motorda aynı senaryoda farklı satırlara yazan iki işlem birbirini
engellemez. Bu, motora göre değişen bir davranıştır ve taşınabilir kod yazarken bilinmesi
gerekir.

#### Gerçek Bir Çakışma

Aşağıdaki betik aynı dosyaya iki bağlantı açar. Birinci bağlantı bir yazma işlemi açar ve
açık tutar; ikinci bağlantı yazmayı dener:

```bash
rm -f kilit.db kilit.db-wal kilit.db-shm
cat > kilit.mjs <<'JS'
import { DatabaseSync } from 'node:sqlite';

const a = new DatabaseSync('kilit.db');
const b = new DatabaseSync('kilit.db');
a.exec('PRAGMA journal_mode = WAL');
a.exec(`CREATE TABLE kitap(id INTEGER PRIMARY KEY, baslik TEXT, rafta INT);
        INSERT INTO kitap VALUES (1,'Kayip Zaman',1),(2,'Sessiz Ev',1);`);

a.exec('BEGIN IMMEDIATE');
a.exec('UPDATE kitap SET rafta = 0 WHERE id = 1');
console.log('A: yazma işlemi açık, 1 numaralı kitap güncellendi');

try {
  b.exec('BEGIN IMMEDIATE');
  b.exec('UPDATE kitap SET rafta = 0 WHERE id = 2');
  console.log('B: güncelledi');
} catch (hata) {
  console.log('B hata aldı:', hata.code, '-', hata.message);
}

console.log('B okuyabiliyor mu:',
            b.prepare('SELECT rafta FROM kitap WHERE id = 1').get().rafta);
a.exec('COMMIT');
console.log('A kesinleştirdi; B artık yazabilir');
b.exec('BEGIN IMMEDIATE');
b.exec('UPDATE kitap SET rafta = 0 WHERE id = 2');
b.exec('COMMIT');
console.log('son durum:', JSON.stringify(a.prepare(
  'SELECT id, rafta FROM kitap ORDER BY id').all()));
JS
node kilit.mjs
rm -f kilit.db kilit.db-wal kilit.db-shm
```

```text
A: yazma işlemi açık, 1 numaralı kitap güncellendi
B hata aldı: ERR_SQLITE_ERROR - database is locked
B okuyabiliyor mu: 1
A kesinleştirdi; B artık yazabilir
son durum: [{"id":1,"rafta":0},{"id":2,"rafta":0}]
```

Üç gözlem var.

Birincisi, B ikinci **kitaba** yazmak istiyordu, A ise birinciye yazmıştı. Yine de
engellendi: bu motorda yazma kilidi veritabanı düzeyindedir. Satır kilidi kullanan bir
motorda bu deneme başarılı olurdu.

İkincisi, hata iletisi bekleme değil **reddetme** bildiriyor. Bekleme süresi sınırı sıfır
olduğu için motor kuyruğa girmek yerine hemen döndü. İleti biçimi motora özgüdür; ortak
olan, çakışmanın uygulamaya bir hata olarak ulaşmasıdır.

Üçüncüsü, B yazamazken **okuyabildi** ve A'nın kesinleştirmediği değeri değil, eski değeri
gördü. Okuyucunun yazardan bağımsız ilerlemesi, önceki dersteki anlık görüntü davranışının
kilit tarafındaki karşılığıdır.

A kesinleştirdikten sonra B'nin aynı yazması sorunsuz geçti. Çakışma kalıcı bir engel
değil, zamanlamaya bağlı bir yarıştır.

#### Yükseltme Çakışması

Daha ince bir durum, iki işlemin de önce okuyup sonra yazmasıdır. İkisi de okuma hakkıyla
başlar, sonra yazma hakkına **yükselmek** ister. Biri yükselirse diğerinin isteği
karşılanamaz:

```bash
rm -f kilit2.db kilit2.db-wal kilit2.db-shm
cat > kilitlenme.mjs <<'JS'
import { DatabaseSync } from 'node:sqlite';

const a = new DatabaseSync('kilit2.db');
const b = new DatabaseSync('kilit2.db');
a.exec('PRAGMA journal_mode = WAL');
a.exec(`CREATE TABLE kitap(id INTEGER PRIMARY KEY, baslik TEXT, rafta INT);
        INSERT INTO kitap VALUES (1,'Kayip Zaman',1),(2,'Sessiz Ev',1);`);

a.exec('BEGIN');
b.exec('BEGIN');
console.log('A okudu:', a.prepare('SELECT rafta FROM kitap WHERE id = 1').get().rafta);
console.log('B okudu:', b.prepare('SELECT rafta FROM kitap WHERE id = 1').get().rafta);

a.exec('UPDATE kitap SET rafta = 0 WHERE id = 1');
console.log('A yazdı');
try {
  b.exec('UPDATE kitap SET rafta = 0 WHERE id = 1');
  console.log('B yazdı');
} catch (hata) {
  console.log('B hata aldı:', hata.code, '-', hata.message);
}
b.exec('ROLLBACK');
a.exec('COMMIT');
console.log('son durum:', JSON.stringify(a.prepare(
  'SELECT id, rafta FROM kitap ORDER BY id').all()));
JS
node kilitlenme.mjs
rm -f kilit2.db kilit2.db-wal kilit2.db-shm
```

```text
A okudu: 1
B okudu: 1
A yazdı
B hata aldı: ERR_SQLITE_ERROR - database is locked
son durum: [{"id":1,"rafta":0},{"id":2,"rafta":1}]
```

İki işlem de kitabı rafta gördü; ikisi de ödünç vermek istedi. Yalıtım olmasaydı ikisi de
başarılı olur, kitap iki üyeye birden verilmiş görünürdü — önceki dersteki kayıp güncelleme.
Kilit bunu engelledi: biri kazandı, diğeri hata aldı.

Hata alan taraf için doğru davranış, işlemi geri alıp **yeniden denemektir**. Yeniden
denemede okuma güncel değeri getirir ve kitabın artık rafta olmadığı görülür. Çakışma
çözümü, uygulamanın bu yeniden deneme döngüsünü yazmasıyla tamamlanır; motorun tek başına
yaptığı, çakışmayı sessiz bırakmamaktır.

Bu deseni hatasız kurmanın standart yolu, okumayı baştan yazma niyetiyle yapmaktır.
Standart SQL bunun için okunan satırları kilitleyen bir yazım tanımlar:

```sql
SELECT rafta FROM kitap WHERE id = 1 FOR UPDATE;
```

Bu yazım, satırı okurken üzerine dışlayıcı hak alır; ikinci işlem okuma aşamasında beklemeye
girer ve yükseltme çakışması hiç oluşmaz. Yukarıdaki gözlemin yapıldığı motor bu yan
tümceyi tanımaz; onun karşılığı, işlemi `BEGIN IMMEDIATE` ile yazma niyetiyle açmaktır.
Deyimin kendisi burada çalıştırılmamıştır, biçimi gösterilmiştir.

#### Kilitlenme

İki işlem birbirinin tuttuğu kaynağı beklerse hiçbiri ilerleyemez. Bu duruma **kilitlenme
(deadlock)** denir ve en yalın hâli iki kaynakla oluşur:

| Adım | İşlem A | İşlem B |
|---|---|---|
| 1 | `kitap` satırını kilitler | — |
| 2 | — | `uye` satırını kilitler |
| 3 | `uye` satırını ister, bekler | — |
| 4 | — | `kitap` satırını ister, bekler |

Dördüncü adımdan sonra bekleme çizgesinde bir çevrim vardır ve hiçbir bekleme kendiliğinden
sona ermez. Motorlar bunu iki yoldan çözer. Kilitlenme algılayan motorlar bekleme çizgesini
tarar, çevrim bulunca bir işlemi **kurban** seçip geri alır; kurbana bir kilitlenme hatası
döner. Algılama yapmayan motorlar bir bekleme süresi sınırı koyar; süre dolunca bekleyen
işlem hata alır. İkinci yolda gerçek bir kilitlenme ile yalnızca yavaş bir işlem birbirinden
ayırt edilemez.

Yukarıdaki gözlemde çevrim oluşmadı, çünkü o motor aynı anda tek yazara izin verir; ikinci
yazar beklemeye bile geçmeden reddedilir. Kilitlenme, ince ayrıntı düzeyinde kilit tutan
motorların sorunudur — daha çok eşzamanlılığın bedeli.

Önleme, çevrimin oluşmasını engellemekle olur:

- Kaynaklara **her zaman aynı sırayla** dokunmak. Her iki işlem de önce `kitap` sonra `uye`
  kilitlerse üçüncü ve dördüncü adım aynı yönde olur, çevrim kapanmaz.
- İşlemi kısa tutmak. Kilit tutma süresi kısaldıkça çakışma olasılığı düşer.
- Gerekiyorsa tüm kilitleri baştan almak; ilerledikçe hak yükseltmemek.

Bu üçü, çakışmayı yok etmez, olasılığını düşürür. Hata alma ihtimali kaldığı için,
çakışabilecek her işlemin yeniden denenebilir yazılması gerekir: aynı işlemi ikinci kez
çalıştırmanın ek bir yan etki üretmemesi, yani yinelemeye dayanıklı olması.

#### Bekleme Süresi Sınırı

Reddetme ile bekleme arasındaki seçim bir ayardır. Bekleme süresi sınırı verilirse, çakışan
işlem hemen hata almak yerine belirtilen süre boyunca kilidin serbest kalmasını bekler:

```bash
rm -f kilit3.db kilit3.db-wal kilit3.db-shm
cat > bekleme.mjs <<'JS'
import { DatabaseSync } from 'node:sqlite';

const a = new DatabaseSync('kilit3.db');
const b = new DatabaseSync('kilit3.db', { timeout: 250 });
a.exec('PRAGMA journal_mode = WAL');
a.exec(`CREATE TABLE kitap(id INTEGER PRIMARY KEY, baslik TEXT, rafta INT);
        INSERT INTO kitap VALUES (1,'Kayip Zaman',1);`);

a.exec('BEGIN IMMEDIATE');
a.exec('UPDATE kitap SET rafta = 0 WHERE id = 1');

const basla = process.hrtime.bigint();
try {
  b.exec('BEGIN IMMEDIATE');
} catch (hata) {
  const gecen = Number(process.hrtime.bigint() - basla) / 1e6;
  console.log('B hata aldı:', hata.code);
  console.log('beklediği süre 250 ms sınırının üstünde mi:', gecen >= 250);
}
a.exec('COMMIT');
JS
node bekleme.mjs
rm -f kilit3.db kilit3.db-wal kilit3.db-shm
```

```text
B hata aldı: ERR_SQLITE_ERROR
beklediği süre 250 ms sınırının üstünde mi: true
```

Ölçülen sürenin kendisi makineye ve yüke bağlı olduğu için yazdırılmadı; yazdırılan, sınırın
aşılıp aşılmadığıdır. Sonuç, ilk gözlemdekiyle aynı hatadır — fark, hatanın hemen değil,
belirtilen süre boyunca beklendikten sonra gelmesidir.

Sınırın seçimi bir ödünleşimdir. Kısa sınır, çakışmayı hızlı bildirir ve uygulamanın
yeniden denemesine izin verir; uzun sınır, geçici çakışmaları uygulamaya hiç göstermez ama
gerçek bir kilitlenmede kaynak tutmayı uzatır. İkisi de işlemlerin kısa tutulması varsayımı
üzerine kuruludur.

#### Özet

- Paylaşımlı kilit okuma, dışlayıcı kilit yazma içindir; okuma okumayı engellemez, yazma
  her şeyi engeller.
- Kilit ayrıntı düzeyi satırdan veritabanına kadar değişir; ince düzey eşzamanlılığı
  artırır, muhasebeyi büyütür.
- Gözlemde ikinci yazar `database is locked` hatası aldı ama okumayı sürdürebildi; çakışma
  kalıcı engel değil, zamanlamaya bağlı bir yarıştır.
- Okuyup sonra yazan iki işlem yükseltme çakışması üretir; standart çözüm okumayı yazma
  niyetiyle kilitlemek, uygulama çözümü işlemi geri alıp yeniden denemektir.
- Kilitlenme, bekleme çizgesinde çevrim oluşmasıdır; kaynaklara aynı sırayla dokunmak,
  işlemi kısa tutmak ve yeniden denenebilir yazmak önleyici alışkanlıklardır.

#### Sonraki Adım

Bu derse kadar işlemin sınırını, hata durumunda ne yapılacağını ve yeniden denemeyi hep
uygulama kodu belirledi; veritabanı yalnız deyimleri aldı. Oysa bu mantığın bir kısmı
veritabanının içinde de tanımlanabilir: ödünç verme kuralı bir yordama konabilir, uygulama
o yordamı tek çağrıyla tetikleyebilir. Sonraki ders, sunucu tarafında çalışan yordam ve
işlevlerin standart biçimini, sağladıklarını ve getirdiği ödünleşimleri ele alacak.

### Saklı Yordamlar ve İşlevler

Source: https://academia.sh/tr/kurslar/ileri-sql/sakli-yordamlar-ve-islevler

Sunucu tarafında çalışan yordam ve işlevlerin standart biçimi, yordam ile işlev ayrımı, belirlenimciliğin sonuçları, gidiş-dönüş sayısının ölçülmesi ve görünümlerle karşılaştırma.

Buraya kadar işlemin sınırını, hata durumunda ne yapılacağını ve yeniden denemeyi hep
uygulama kodu belirledi; veritabanı yalnız gönderilen deyimleri aldı. Bu mantığın bir kısmı
veritabanının içinde de tanımlanabilir. "Ödünç verme" kuralı bir yordama konursa uygulama
iki deyim değil, tek bir çağrı gönderir; kural veriyle aynı yerde durur.

**Saklı yordam (stored procedure)** ve **saklı işlev (stored function)**, sunucu tarafında
saklanan ve adıyla çağrılan kod birimleridir. Bu ders ikisinin standart biçimini, ayrımını
ve getirdiği ödünleşimleri ele alır.

#### Yordam ve İşlev

İkisini ayıran, nasıl çağrıldıkları ve ne döndürdükleridir.

**Yordam**, `CALL` deyimiyle çağrılır. Bir değer döndürmek zorunda değildir; girdi, çıktı
ve giriş-çıkış parametreleri alabilir ve gövdesinde veri değiştiren deyimler bulunabilir.

**İşlev**, bir değer döndürür ve bir ifadenin geçebildiği her yerde — `SELECT` listesinde,
`WHERE` koşulunda — kullanılabilir. Bu yüzden yan etkisiz olması beklenir.

Standart SQL'in yordamsal alt kümesi, ödünç verme kuralını şöyle yazar:

```sql
CREATE PROCEDURE odunc_ver(IN p_kitap_id INTEGER, IN p_uye_id INTEGER)
LANGUAGE SQL
BEGIN ATOMIC
  INSERT INTO odunc(kitap_id, uye_id, alis)
    VALUES (p_kitap_id, p_uye_id, CURRENT_DATE);
  UPDATE kitap SET rafta = 0 WHERE id = p_kitap_id;
END;
```

Çağrısı tek satırdır:

```sql
CALL odunc_ver(1, 4);
```

Gecikme hesabı ise bir işlevdir; değer döndürür ve sorguda kullanılır:

```sql
CREATE FUNCTION gecikme_gunu(p_alis DATE, p_iade DATE)
RETURNS INTEGER
LANGUAGE SQL
DETERMINISTIC
RETURN CASE
         WHEN (COALESCE(p_iade, CURRENT_DATE) - p_alis) - 14 > 0
         THEN (COALESCE(p_iade, CURRENT_DATE) - p_alis) - 14
         ELSE 0
       END;
```

Bu üç blok **çalıştırılmamıştır**; biçimi göstermek için yazılmıştır. Nedeni, yordamsal alt
kümenin desteklenmesinin motora göre değişmesidir: bazı motorlar standarda yakın bir dil
sunar, bazıları kendi yordam dilini kullanır, bazıları — bu kursun gözlem ortamı dâhil —
saklı yordamı hiç desteklemez. Değişmeyen, `CREATE PROCEDURE` ile `CREATE FUNCTION`
ayrımı ve `CALL` ile çağırma biçimidir.

`BEGIN ATOMIC` bloğu, gövdedeki deyimlerin bölünmez bir birim olduğunu söyler: biri
başarısız olursa hiçbiri uygulanmaz. İşlem denetimi dersindeki atomiklik burada yordam
tanımının parçası hâline gelir.

#### Uygulamada Tanımlanan İşlev

Saklı yordam desteklemeyen bir motorda işlevin karşılığı, bağlantıya kayıtlı bir uygulama
işlevidir. Kod uygulamada durur, ama sorgunun içinde çalışır — motor her satır için onu
çağırır:

```bash
cat > islev.mjs <<'JS'
import { DatabaseSync } from 'node:sqlite';

const db = new DatabaseSync(':memory:');
db.exec(`
  CREATE TABLE odunc(id INTEGER PRIMARY KEY, uye_id INT, alis TEXT, iade TEXT);
  INSERT INTO odunc VALUES (1,1,'2024-03-01','2024-03-15'),(2,1,'2024-03-04','2024-03-18'),
    (3,2,'2024-03-06','2024-03-25'),(4,2,'2024-03-11',NULL),(5,7,'2024-03-18','2024-03-24');
`);

const GUN = 86400000;
db.function('gecikme_gunu', { deterministic: true }, (alis, iade) => {
  const bitis = iade === null ? Date.parse('2024-03-31') : Date.parse(iade);
  const gun = Math.round((bitis - Date.parse(alis)) / GUN);
  return Math.max(0, gun - 14);
});

for (const s of db.prepare(
  `SELECT id, uye_id, alis, COALESCE(iade, '(acik)') AS iade,
          gecikme_gunu(alis, iade) AS gecikme
   FROM odunc WHERE gecikme_gunu(alis, iade) > 0 ORDER BY id`).all()) {
  console.log(JSON.stringify(s));
}
JS
node islev.mjs
```

```text
{"id":3,"uye_id":2,"alis":"2024-03-06","iade":"2024-03-25","gecikme":5}
{"id":4,"uye_id":2,"alis":"2024-03-11","iade":"(acik)","gecikme":6}
```

Beş kayıttan ikisi on dört günlük süreyi aştı. Kural tek yerde yazıldı ve hem `SELECT`
listesinde hem `WHERE` koşulunda kullanıldı; aynı hesabın iki yerde kopyalanmasına gerek
kalmadı. Saklı işlevin sağladığı da tam olarak budur — fark, kodun nerede saklandığıdır.

#### Belirlenimcilik

İşlev tanımındaki `DETERMINISTIC` sözcüğü bir söz verir: aynı argümanlarla çağrıldığında
her zaman aynı sonucu döndürür. Bu söz, planlayıcıya iki izin verir — sonucu önbelleğe alıp
tekrar hesaplamamak ve işlevin sonucunu bir dizinde saklamak.

İkinci izin ölçülebilir. Aşağıdaki betik iki işlev tanımlar; ikisi de aynı gövdeye sahiptir,
yalnız biri belirlenimci olduğunu bildirir. Her ikisi üzerinde dizin oluşturulmaya çalışılır:

```bash
cat > belirlenimci.mjs <<'JS'
import { DatabaseSync } from 'node:sqlite';

const db = new DatabaseSync(':memory:');
db.exec(`CREATE TABLE odunc(id INTEGER PRIMARY KEY, alis TEXT, iade TEXT);`);

db.function('sabit_sure', { deterministic: true },
            (alis, iade) => (iade === null ? -1 : 1));
db.function('degisken_sure', (alis, iade) => (iade === null ? -1 : 1));

for (const [ad, islev] of [['sabit_sure', 'sabit_sure'],
                           ['degisken_sure', 'degisken_sure']]) {
  try {
    db.exec(`CREATE INDEX dizin_${ad} ON odunc(${islev}(alis, iade))`);
    console.log(`${ad}: dizin oluşturuldu`);
  } catch (hata) {
    console.log(`${ad}: ${hata.message}`);
  }
}
JS
node belirlenimci.mjs
```

```text
sabit_sure: dizin oluşturuldu
degisken_sure: non-deterministic functions prohibited in index expressions
```

Gerekçe basittir: dizin, işlevin sonucunu saklar. Sonuç zamanla değişebiliyorsa dizindeki
değer veriyle uyumsuz hâle gelir ve sorgular yanlış yanıt verir. Motor bu tehlikeyi baştan
keser.

Aynı gerekçe, belirlenimci olmayan bir işlevin sorgu içinde **her satır için yeniden**
çağrılmasını da açıklar. Geçerli zamanı, rastgele sayıyı ya da oturum bilgisini okuyan bir
işlev belirlenimci ilan edilirse, motorun yaptığı eniyilemeler sessiz hatalara dönüşür.
Bildirim bir söz olduğu için sorumluluk yazarındadır.

#### Gidiş Dönüş Maliyeti

Sunucu tarafı mantığın en somut yararı, uygulama ile motor arasındaki tur sayısını
azaltmasıdır. Bu, iş miktarından bağımsız bir maliyettir: her deyim ayrı bir istek, ayrı bir
yanıt demektir.

Aşağıdaki betik iki yüz kitabı ödünç verir. Önce her kitap için iki deyim gönderir, sonra
aynı işi iki küme deyimiyle yapar:

```bash
cat > gidis.mjs <<'JS'
import { DatabaseSync } from 'node:sqlite';

const db = new DatabaseSync(':memory:');
db.exec(`
  CREATE TABLE kitap(id INTEGER PRIMARY KEY, rafta INT NOT NULL);
  CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT);
`);
for (let i = 1; i <= 200; i += 1) db.exec(`INSERT INTO kitap VALUES (${i}, 1)`);

let deyim = 0;
const calistir = (sql, ...arg) => { deyim += 1; db.prepare(sql).run(...arg); };

deyim = 0;
db.exec('BEGIN');
for (let i = 1; i <= 200; i += 1) {
  calistir('INSERT INTO odunc(kitap_id, uye_id, alis) VALUES (?, 4, ?)',
           i, '2024-03-11');
  calistir('UPDATE kitap SET rafta = 0 WHERE id = ?', i);
}
db.exec('COMMIT');
console.log('satır satır  : gönderilen deyim =', deyim,
            '| ödünç kaydı =', db.prepare('SELECT COUNT(*) AS n FROM odunc').get().n);

db.exec('DELETE FROM odunc; UPDATE kitap SET rafta = 1;');
deyim = 0;
db.exec('BEGIN');
calistir(`INSERT INTO odunc(kitap_id, uye_id, alis)
          SELECT id, 4, '2024-03-11' FROM kitap WHERE rafta = 1`);
calistir('UPDATE kitap SET rafta = 0 WHERE rafta = 1');
db.exec('COMMIT');
console.log('küme tabanlı : gönderilen deyim =', deyim,
            '| ödünç kaydı =', db.prepare('SELECT COUNT(*) AS n FROM odunc').get().n);
JS
node gidis.mjs
```

```text
satır satır  : gönderilen deyim = 400 | ödünç kaydı = 200
küme tabanlı : gönderilen deyim = 2 | ödünç kaydı = 200
```

Aynı iki yüz kayıt, 400 deyim yerine 2 deyimle üretildi. Burada motor ile uygulama aynı
süreçte olduğu için farkın etkisi küçüktür; ağ üzerinden konuşan bir uygulamada 400 deyim
400 tur demektir ve her turun gecikmesi işin kendisinden büyük olabilir.

Saklı yordamın kazandırdığı budur: döngü sunucu tarafında kalır, uygulama tek çağrı yapar.
Ancak ölçüm ikinci bir şey daha söyler — turu azaltmanın yordam gerektirmediği. Küme
tabanlı yazım, saklı yordam olmadan aynı kazancı sağladı. Yordam, ancak küme deyimiyle
ifade edilemeyen bir denetim akışı gerektiğinde bu gerekçeyle savunulabilir.

#### Görünümle Karşılaştırma

Sunucu tarafına yalnız yordam konmaz. **Görünüm (view)**, bir sorguya ad verip şemada
saklar; parametre almaz ama tekrar eden birleştirme ve süzme mantığını tek yerde toplar:

```bash
sqlite3 -box -header <<'SQL'
CREATE TABLE uye(id INTEGER PRIMARY KEY, ad TEXT, sube TEXT);
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT);
INSERT INTO uye VALUES (1,'Ayse','Kadikoy'),(2,'Burak','Kadikoy'),(7,'Gokhan','Kadikoy');
INSERT INTO odunc VALUES (1,1,1,'2024-03-01','2024-03-15'),(4,7,1,'2024-03-11',NULL),
  (9,8,2,'2024-03-21',NULL),(23,2,7,'2024-03-11','2024-03-16');

CREATE VIEW acik_odunc AS
  SELECT o.id, u.ad, o.alis
  FROM odunc o JOIN uye u ON u.id = o.uye_id
  WHERE o.iade IS NULL;

SELECT * FROM acik_odunc ORDER BY id;
SELECT COUNT(*) AS acik_sayisi FROM acik_odunc;
SQL
```

```text
┌────┬───────┬────────────┐
│ id │  ad   │    alis    │
├────┼───────┼────────────┤
│ 4  │ Ayse  │ 2024-03-11 │
│ 9  │ Burak │ 2024-03-21 │
└────┴───────┴────────────┘
┌─────────────┐
│ acik_sayisi │
├─────────────┤
│ 2           │
└─────────────┘
```

"Açık ödünç" tanımı tek yerde durdu ve iki sorgu da onu kullandı. Tanım değişirse — örneğin
iptal edilmiş kayıtlar da dışlanacaksa — tek bir yer güncellenir. Görünüm, ortak tablo
ifadesinden farklı olarak kalıcıdır ve şemanın parçasıdır.

Sunucu tarafına kod koymanın bedeli de vardır. Yordam gövdeleri sürüm denetimine ve sınama
düzenine uygulama kodu kadar kolay girmez; her motorun yordam dili farklı olduğundan
taşınabilirlik kaybolur; hata ayıklama araçları uygulama diline göre sınırlıdır. Ölçü,
konan mantığın niteliğine bakılarak verilir: **veri bütünlüğünü** koruyan kurallar veriye
yakın durmaktan kazanır, iş akışı kararları uygulamada kalmaktan.

#### Özet

- Yordam `CALL` ile çağrılır ve veri değiştirebilir; işlev değer döndürür ve ifade
  içinde kullanılır.
- Standart yordamsal alt küme `CREATE PROCEDURE` ve `CREATE FUNCTION` ile tanımlanır;
  desteklenme derecesi motora göre değişir.
- `DETERMINISTIC` bildirimi planlayıcıya önbellekleme ve dizinleme izni verir;
  belirlenimci olmayan bir işlev dizin ifadesinde kullanılamaz.
- Sunucu tarafı mantık gidiş-dönüş sayısını azaltır; ölçümde 400 deyimlik iş 2 deyime indi,
  ancak aynı kazanç küme tabanlı yazımla yordamsız da sağlandı.
- Görünüm, parametresiz bir sunucu tarafı soyutlamadır ve tekrar eden sorgu mantığını
  şemada tek yerde tutar.

#### Sonraki Adım

Bu derste sunucu tarafındaki kod hep **çağrıldı**: yordam `CALL` ile, işlev sorgu içinde
adıyla. Çağrıldığı yerden okunduğu için ne yaptığı görünürdü. Veritabanı bir de
çağrılmadan çalışan kod tanımlar: bir tabloya yazıldığında kendiliğinden devreye giren
tetikleyiciler. Sonraki ders bunların nasıl tanımlandığını, ne işe yaradığını ve
çağrılmadan çalışan kodun neden özel bir dikkat gerektirdiğini gösterecek.

### Tetikleyiciler

Source: https://academia.sh/tr/kurslar/ileri-sql/tetikleyiciler

Tetikleyici tanımı, BEFORE ve AFTER ile satır başına çalışma, denetim kaydı ve türetilmiş durum güncelleme, RAISE ile kural dayatma, görünüm üzerinde INSTEAD OF ve özyineleme riski.

Önceki derste sunucu tarafındaki kod hep çağrıldı: yordam `CALL` ile, işlev sorgu içinde
adıyla. Çağrıldığı yerden okunduğu için ne yaptığı görünürdü.

**Tetikleyici (trigger)** çağrılmaz. Bir tabloya ekleme, güncelleme ya da silme yapıldığında
motor onu kendiliğinden çalıştırır. Bu, güçlü bir araç ve aynı ölçüde bir tuzaktır: sıradan
görünen bir `UPDATE` deyimi, metninde hiç geçmeyen tablolara yazabilir.

#### Tanım ve Çalışma Anı

Tetikleyici üç şeyle tanımlanır: hangi tabloyu izlediği, hangi olayda çalıştığı ve olayın
öncesinde mi sonrasında mı çalıştığı.

- `BEFORE`: deyim satırı işlemeden önce çalışır. Değer düzeltmek ya da işlemi reddetmek
  için kullanılır.
- `AFTER`: satır işlendikten sonra çalışır. Başka tablolara yazmak için kullanılır.
- `INSTEAD OF`: satır işlenmez, onun **yerine** tetikleyici çalışır. Görünümler içindir.

`FOR EACH ROW` yazımı, tetikleyicinin etkilenen her satır için ayrı çalıştığını söyler.
Gövde içinde iki özel ad kullanılabilir: `NEW`, satırın yeni hâlini; `OLD`, eski hâlini
verir. Ekleme olayında `OLD`, silme olayında `NEW` tanımsızdır.

İlk kullanım, iki işi birden yapan bir çift tetikleyicidir: türetilmiş durumu güncellemek ve
denetim kaydı yazmak.

```bash
sqlite3 -box -header <<'SQL'
CREATE TABLE kitap(id INTEGER PRIMARY KEY, baslik TEXT, rafta INT NOT NULL DEFAULT 1);
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT);
CREATE TABLE denetim(id INTEGER PRIMARY KEY, olay TEXT, odunc_id INT, ayrinti TEXT);
INSERT INTO kitap VALUES (1,'Kayip Zaman',1),(2,'Sessiz Ev',1);

CREATE TRIGGER odunc_acildi AFTER INSERT ON odunc
FOR EACH ROW
BEGIN
  UPDATE kitap SET rafta = 0 WHERE id = NEW.kitap_id;
  INSERT INTO denetim(olay, odunc_id, ayrinti)
    VALUES ('acildi', NEW.id, 'kitap ' || NEW.kitap_id || ' uye ' || NEW.uye_id);
END;

CREATE TRIGGER odunc_kapandi AFTER UPDATE OF iade ON odunc
FOR EACH ROW WHEN OLD.iade IS NULL AND NEW.iade IS NOT NULL
BEGIN
  UPDATE kitap SET rafta = 1 WHERE id = NEW.kitap_id;
  INSERT INTO denetim(olay, odunc_id, ayrinti)
    VALUES ('kapandi', NEW.id, 'iade ' || NEW.iade);
END;

INSERT INTO odunc(id, kitap_id, uye_id, alis) VALUES (1, 1, 4, '2024-03-11');
UPDATE odunc SET iade = '2024-03-20' WHERE id = 1;

SELECT id, baslik, rafta FROM kitap ORDER BY id;
SELECT id, olay, odunc_id, ayrinti FROM denetim ORDER BY id;
SQL
```

```text
┌────┬─────────────┬───────┐
│ id │   baslik    │ rafta │
├────┼─────────────┼───────┤
│ 1  │ Kayip Zaman │ 1     │
│ 2  │ Sessiz Ev   │ 1     │
└────┴─────────────┴───────┘
┌────┬─────────┬──────────┬─────────────────┐
│ id │  olay   │ odunc_id │     ayrinti     │
├────┼─────────┼──────────┼─────────────────┤
│ 1  │ acildi  │ 1        │ kitap 1 uye 4   │
│ 2  │ kapandi │ 1        │ iade 2024-03-20 │
└────┴─────────┴──────────┴─────────────────┘
```

İki deyim yazıldı: bir `INSERT` ve bir `UPDATE`. Olan altı şey şu: ödünç kaydı açıldı,
kitap raftan düştü, denetim satırı yazıldı; sonra kayıt kapandı, kitap rafa döndü, ikinci
denetim satırı yazıldı. Sonuçtaki `rafta` değerinin 1 olması, ikinci tetikleyicinin
birincinin etkisini geri aldığını gösterir.

`WHEN` yan tümcesi, tetikleyicinin yalnız belirli satırlarda çalışmasını sağlar. Buradaki
koşul, kaydın gerçekten **kapandığı** güncellemeleri ayırıyor; iade tarihinin düzeltildiği
bir güncelleme kitabı ikinci kez rafa koymayacak.

`AFTER UPDATE OF iade` yazımı tetikleyiciyi tek sütuna bağlar. Sütun listesi verilmezse
tetikleyici, tablodaki herhangi bir sütunun güncellenmesinde çalışır — genellikle
istenmeyen bir genişliktir.

#### Kural Dayatmak

`BEFORE` tetikleyicileri işlemi reddedebilir. Standart SQL bunun için sinyal deyimi
tanımlar; motorlarda karşılığı bir hata yükseltme işlevidir. Aşağıdaki tetikleyici, rafta
olmayan bir kitabın ödünç verilmesini engeller:

```bash
sqlite3 -box -header <<'SQL'
CREATE TABLE kitap(id INTEGER PRIMARY KEY, baslik TEXT, rafta INT NOT NULL DEFAULT 1);
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT);
INSERT INTO kitap VALUES (1,'Kayip Zaman',0),(2,'Sessiz Ev',1);

CREATE TRIGGER odunc_kontrol BEFORE INSERT ON odunc
FOR EACH ROW
WHEN (SELECT rafta FROM kitap WHERE id = NEW.kitap_id) = 0
BEGIN
  SELECT RAISE(ABORT, 'kitap rafta degil');
END;

INSERT INTO odunc(id, kitap_id, uye_id, alis) VALUES (1, 2, 4, '2024-03-11');
INSERT INTO odunc(id, kitap_id, uye_id, alis) VALUES (2, 1, 7, '2024-03-11');
SELECT id, kitap_id, uye_id FROM odunc ORDER BY id;
SQL
```

```text
Runtime error near line 13: kitap rafta degil (19)
┌────┬──────────┬────────┐
│ id │ kitap_id │ uye_id │
├────┼──────────┼────────┤
│ 1  │ 2        │ 4      │
└────┴──────────┴────────┘
```

Rafta olan kitabın ödüncü geçti, olmayanınki tetikleyicinin verdiği iletiyle reddedildi.
İleti metni tetikleyicide yazılmıştır; uygulamaya ulaşan hata, kısıt ihlali hatalarıyla aynı
yoldan gelir.

Bu araç kısıtların yerini tutmaz. Bir kural bir sütunun tek başına değeriyle ifade
edilebiliyorsa `CHECK` kısıtı, başka bir tablodaki satırın varlığıyla ifade edilebiliyorsa
yabancı anahtar kullanılmalıdır; ikisi de bildirimseldir, planlayıcı tarafından bilinir ve
tetikleyiciden ucuzdur. Tetikleyici, ancak birden çok tabloyu birlikte inceleyen kurallar
için gerekir.

#### Görünüm Üzerinde

Karmaşık bir görünüm doğrudan güncellenemez: motor, görünüm satırındaki bir değişikliğin
temel tablolara nasıl yansıyacağını türetemez. `INSTEAD OF` tetikleyicisi bu eşlemeyi elle
tanımlar:

```bash
sqlite3 -box -header <<'SQL'
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT);
INSERT INTO odunc VALUES (1,1,4,'2024-03-01',NULL),(2,3,7,'2024-03-04',NULL);

CREATE VIEW acik_odunc AS SELECT id, kitap_id, uye_id, alis FROM odunc WHERE iade IS NULL;

CREATE TRIGGER acik_odunc_kapat INSTEAD OF DELETE ON acik_odunc
FOR EACH ROW
BEGIN
  UPDATE odunc SET iade = '2024-03-31' WHERE id = OLD.id;
END;

DELETE FROM acik_odunc WHERE id = 1;
SELECT id, alis, COALESCE(iade,'(acik)') AS iade FROM odunc ORDER BY id;
SQL
```

```text
┌────┬────────────┬────────────┐
│ id │    alis    │    iade    │
├────┼────────────┼────────────┤
│ 1  │ 2024-03-01 │ 2024-03-31 │
│ 2  │ 2024-03-04 │ (acik)     │
└────┴────────────┴────────────┘
```

Yazılan deyim bir silmeydi; olan bir güncellemedir. Satır tabloda duruyor, yalnız iade
tarihi doldu. Görünümün açısından bakınca doğru — satır artık "açık ödünç" değil — ama
deyimin metni bunu söylemiyor. Örtük yan etkinin en açık örneği budur.

#### Özyineleme

Bir tetikleyici, kendi izlediği tabloyu güncellerse yeniden çalışabilir. Motorlar bunu ya
baştan engeller ya da bir derinlik sınırıyla durdurur:

```bash
sqlite3 -box -header <<'SQL'
PRAGMA recursive_triggers = ON;
CREATE TABLE kitap(id INTEGER PRIMARY KEY, baslik TEXT, sayac INT NOT NULL DEFAULT 0);
INSERT INTO kitap VALUES (1,'Kayip Zaman',0);

CREATE TRIGGER sayac_artir AFTER UPDATE OF sayac ON kitap
FOR EACH ROW
BEGIN
  UPDATE kitap SET sayac = NEW.sayac + 1 WHERE id = NEW.id;
END;

UPDATE kitap SET sayac = 1 WHERE id = 1;
SELECT id, sayac FROM kitap;
SQL
```

```text
Runtime error near line 11: too many levels of trigger recursion
┌────┬───────┐
│ id │ sayac │
├────┼───────┤
│ 1  │ 0     │
└────┴───────┘
```

Tek bir `UPDATE` yazıldı; tetikleyici kendini çağırdı ve zincir derinlik sınırında kesildi.
Deyim tümüyle geri alındığı için sayaç sıfırda kaldı — atomiklik burada da geçerlidir.

Özyinelemenin açık ya da kapalı olması motora göre değişir; kapalıyken aynı tanım sessizce
tek tur çalışır ve sorun görünmez. İki motor arasında taşınan bir şemanın davranışının
değişebileceği yerlerden biri budur.

#### Örtük Yan Etkinin Bedeli

Tetikleyicilerin sağladığı yarar açıktır: kural, onu tetikleyen koddan bağımsız olarak her
zaman uygulanır. Uygulamanın hangi katmanından gelirse gelsin, ödünç kaydı açıldığında
denetim satırı yazılır. Bu, kuralın atlanamamasını güvenceye alır.

Bedeli, deyimin metni ile etkisinin ayrışmasıdır. Bunun somut sonuçları şunlardır:

- **Hata ayıklama.** Beklenmedik bir satır değişikliğinin kaynağı sorgu kütüklerinde
  görünmez; şemadaki tetikleyicilerin okunması gerekir.
- **Sıra belirsizliği.** Aynı olaya bağlı birden çok tetikleyicinin çalışma sırası standartta
  belirlenmemiştir; birbirine bağımlı iki tetikleyici yazmak kırılgandır.
- **Toplu işlemler.** Satır başına çalışan tetikleyici, yüz binlik bir güncellemede yüz bin
  kez çalışır; toplu yükleme yollarının bazılarında ise hiç çalışmayabilir.
- **Görünmez maliyet.** Basit görünen bir deyimin planı, tetikleyici gövdesindeki sorguları
  da içerir.

Ölçü şudur: tetikleyici, **veri bütünlüğünü** koruyan ve atlanmaması gereken kurallar için
uygundur — denetim kaydı, türetilmiş sütunun tutarlılığı, çok tablolu kısıtlar. İş akışı
kararları — bildirim göndermek, ücret hesaplamak, dış sistem çağırmak — uygulama katmanında
kalmalıdır. Yazıldıklarında da kısa tutulmalı ve şemayla birlikte belgelenmelidir.

#### Özet

- Tetikleyici çağrılmaz; izlediği tabloda olay gerçekleştiğinde motor tarafından çalıştırılır
  ve `NEW` ile `OLD` üzerinden satırın iki hâline erişir.
- `BEFORE` düzeltme ve reddetme, `AFTER` başka tablolara yazma, `INSTEAD OF` görünüm
  güncellemesi içindir; `WHEN` yan tümcesi çalışmayı daraltır.
- Tek sütunla ifade edilebilen kurallar `CHECK` ya da yabancı anahtarla bildirilmelidir;
  tetikleyici çok tablolu kurallar için gerekir.
- Kendi tablosunu güncelleyen tetikleyici özyineleme üretir; motor bunu derinlik sınırıyla
  keser ve deyimin tamamı geri alınır.
- Örtük yan etki, deyimin metni ile etkisini ayırır; hata ayıklamayı, sıra varsayımlarını ve
  toplu işlem maliyetini etkiler.

#### Sonraki Adım

Bu konu boyunca sorgular doğru sonucu vermeye odaklandı: alt sorgu doğru kümeyi seçti,
pencere doğru çerçeveyi gördü, işlem doğru anda kesinleşti. Doğruluk tek ölçüt değildir.
İlişkili alt sorgular dersinde iki yazımın aynı sonucu 200'e karşı 25 satır ziyaretiyle
ürettiği ölçülmüş, farkı yaratanın erişim yolu olduğu görülmüştü; o derste bir dizin
eklendiğinde plan `SCAN` yerine `SEARCH` demişti. Sonraki konu bu noktadan başlar. İlk ders,
dizinin ne olduğunu ve aramanın maliyetini nasıl düşürdüğünü kurar; oradan sorgu planını
okumaya ve aynı sonucu daha az işle veren yeniden yazımlara geçilecektir.

## Sorgu Başarımı

Dizinin arama maliyetini nasıl düşürdüğü, sorgu planının okunması, dizini devre dışı bırakan yazımlar, birleştirme sırası ve algoritma seçimi, alt sorguların eşdeğer yeniden yazımı, planlayıcının istatistik kaynağı ve dinamik SQL'in doğruluk, güvenlik ve plan önbelleği üzerindeki etkileri.

### Dizin Kavramı

Source: https://academia.sh/tr/kurslar/ileri-sql/dizin-kavrami

Tam tablo taramasının maliyeti, dizinin bu maliyeti neye çevirdiği, aynı sorgunun dizinsiz ve dizinli ölçümü, dizinin yer ve yazma tarafındaki bedeli ve seçiciliğin kazancı belirlemesi.

İşlemler konusu, veritabanının doğru cevabı vermesi için gereken düzeni tamamladı:
kesinleştirme sınırları, yalıtım düzeyleri, kilitler ve tetikleyicilerin örtük yan
etkileri. O konunun sonunda elde kalan şey, doğru sonucu veren bir sorgudur.

Bu ders aynı sorguyu ikinci kez sormaz. Sorduğu şey şudur: veritabanı o doğru sonucu
verirken kaç satıra baktı? İki milyon satırlık bir tabloda tek bir üyenin kayıtlarını
isteyen sorgu, doğru cevabı iki milyon satırı okuyarak da verebilir, altmış kadar adımda
da verebilir. İkisi arasındaki fark, sorgunun yazımında değil, verinin nasıl
düzenlendiğindedir.

#### Ölçülebilir Bir Veri Kümesi

Başarım iddiaları küçük tablolarda görünmez: yüz satırlık bir tabloda her yol hızlıdır.
Ölçümün gürültünün üstüne çıkması için tablo yeterince büyük olmalıdır. Aşağıdaki betik,
kütüphane ödünç kayıtlarından oluşan bir veri kümesi kurar. Satırlar özyinelemeli bir
ortak tablo ifadesiyle üretilir; dışarıdan hiçbir dosya gerekmez.

```sql
-- kurulum.sql — kutuphane veri kumesi
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;
```

Betik `kurulum.sql` adıyla kaydedilip çalıştırılır. Bu konudaki bütün ölçümler bu veri
kümesi üzerinde yapılır.

```sh
rm -f kutuphane.db
sqlite3 kutuphane.db < kurulum.sql
sqlite3 kutuphane.db "SELECT 'uye', count(*) FROM uye
  UNION ALL SELECT 'kitap', count(*) FROM kitap
  UNION ALL SELECT 'odunc', count(*) FROM odunc;"
```

```
uye|120000
kitap|200000
odunc|2000000
```

#### Aramanın Varsayılan Maliyeti

Tek bir üyenin ödünç kayıtları soruluyor. Veritabanı bu sorguyu nasıl karşılıyor?
**Sorgu planı (query plan)**, sorgunun hangi adımlarla yürütüleceğini gösteren ağaçtır;
komut satırından `EXPLAIN QUERY PLAN` ön ekiyle istenir. Ölçüm için iki araç daha
kullanılır: `.timer on` geçen süreyi, `.stats vmstep` sorgunun harcadığı sanal makine
adımı sayısını yazar.

```sh
sqlite3 kutuphane.db <<'SQL'
EXPLAIN QUERY PLAN SELECT odunc_id, alis_tarihi FROM odunc WHERE uye_id = 4242;
.timer on
.stats vmstep
SELECT count(*) FROM odunc WHERE uye_id = 4242;
SELECT count(*) FROM odunc WHERE uye_id = 4242;
SELECT count(*) FROM odunc WHERE uye_id = 4242;
SQL
```

{/* mask: (?<=Run Time: real )[0-9.]+ */}
{/* mask: (?<=user )[0-9.]+(?= sys) */}
{/* mask: (?<=sys )[0-9.]+ */}
```
QUERY PLAN
`--SCAN odunc
17
VM-steps: 6000028
Run Time: real 0.036 user 0.028072 sys 0.007521
17
VM-steps: 6000028
Run Time: real 0.036 user 0.028525 sys 0.007273
17
VM-steps: 6000028
Run Time: real 0.036 user 0.028689 sys 0.007418
```

Planın tek düğümü var: `SCAN odunc`. Bu, **tam tablo taraması (full table scan)**
demektir — tablodaki her satır okunur, koşul her satır için ayrı ayrı sınanır. On yedi
satır döndürmek için iki milyon satır ziyaret edilmiştir.

İki ölçü birlikte okunmalıdır. Süre ortama bağlıdır: aynı sorgu daha hızlı bir diskte
ya da daha yüklü bir makinede farklı çıkar; bu yüzden süreye mutlak bir değer olarak
değil, aynı ortamdaki başka bir süreyle **oranı** üzerinden bakılır. Adım sayısı ise
ortamdan bağımsızdır: aynı veri ve aynı plan her makinede aynı adım sayısını verir.
Altı milyon adımın kaynağı da bellidir — iki milyon satırın her biri için ortalama üç
sanal makine komutu.

Bu maliyet tablo büyüklüğüyle doğru orantılıdır. Ödünç sayısı ikiye katlanırsa tarama
da ikiye katlanır: $O(n)$. Algoritmalar kursundaki Doğrusal Arama dersinin sonucudur bu;
tek fark, dizinin diskte olmasıdır.

#### Dizin Nedir

**Dizin (index)**, bir sütunun değerlerini sıralı tutan ve her değerin hangi satıra ait
olduğunu kaydeden ayrı bir yapıdır. Tablo verisi yerinde kalır; dizin, o veriye ikinci
bir erişim yolu açar.

Kütüphane benzetmesi doğrudan çalışır: raflardaki kitaplar bir düzene göre dizilidir,
ancak kataloğu olmayan bir kütüphanede belli bir yazarın kitaplarını bulmanın yolu bütün
rafları gezmektir. Katalog fişleri yazar adına göre sıralanmış ayrı bir kutuda durur ve
her fiş bir raf numarası taşır. Fiş kutusunda arama, kutunun ortasını açıp yönü
belirleyerek ilerler.

İlişkisel veritabanlarında bu kutunun karşılığı çoğunlukla bir **B-ağacıdır (B-tree)**.
Veri Yapıları kursunda B-Ağaçları dersinde kurulan yapı budur: her düğüm bir disk
sayfasına sığacak kadar geniştir, ağaç dengelidir ve yüksekliği kayıt sayısının
logaritmasıyla artar. Bir anahtarı bulmak için kökten yaprağa inmek gerekir; iki milyon
kayıtlı bir dizide bu, birkaç sayfanın okunması demektir. Tablo on kat büyüdüğünde
tarama on kat pahalılaşır, ağaçta inilen düzey sayısı ise çoğu zaman hiç değişmez: bir
düzey eklenmesi için tablonun, bir sayfaya sığan anahtar sayısı kadar büyümesi gerekir —
onlarca değil, yüzlerce kat.

#### Dizinin Ölçülen Etkisi

Aynı sorgu, `uye_id` üzerinde dizin varken tekrarlanır.

```sh
sqlite3 kutuphane.db <<'SQL'
.timer on
CREATE INDEX odunc_uye ON odunc(uye_id);
.timer off
EXPLAIN QUERY PLAN SELECT odunc_id, alis_tarihi FROM odunc WHERE uye_id = 4242;
.timer on
.stats vmstep
SELECT count(*) FROM odunc WHERE uye_id = 4242;
SELECT count(*) FROM odunc WHERE uye_id = 4242;
SELECT count(*) FROM odunc WHERE uye_id = 4242;
SQL
```

{/* mask: (?<=Run Time: real )[0-9.]+ */}
{/* mask: (?<=user )[0-9.]+(?= sys) */}
{/* mask: (?<=sys )[0-9.]+ */}
```
Run Time: real 0.263 user 0.219940 sys 0.036873
QUERY PLAN
`--SEARCH odunc USING INDEX odunc_uye (uye_id=?)
17
VM-steps: 62
Run Time: real 0.000 user 0.000016 sys 0.000012
17
VM-steps: 62
Run Time: real 0.000 user 0.000007 sys 0.000004
17
VM-steps: 62
Run Time: real 0.000 user 0.000005 sys 0.000004
```

Plan düğümü `SCAN` iken `SEARCH ... USING INDEX` oldu. Okunacak olan budur: veritabanı
artık tabloyu baştan sona okumuyor, dizinden `uye_id = 4242` anahtarına iniyor ve yalnız
oradaki satırlara bakıyor.

Adım sayısı 6.000.028'den 62'ye indi — yaklaşık yüz bin kat. Süre ise ölçülemez oldu:
`0.000` değeri "sıfır süre" demek değil, zamanlayıcının çözünürlüğünün altında kalmak
demektir. Bu, ölçüm disiplininin ilk kuralını hatırlatır: bir yol yeterince ucuzladığında
süre bilgi vermeyi bırakır, ölçülmesi gereken şey yapılan iştir. Dizinin oluşturulması
da bedava değildir; iki milyon satırın sıralanması bu ortamda çeyrek saniye sürmüştür.

#### Dizinin Bedeli

Dizin, okumayı ucuzlatırken iki yerde pahalılaştırır: yer ve yazma. Aşağıdaki ölçüm aynı
veri kümesinin iki kopyasına aynı iki yüz bin satırı ekler; kopyaların biri dizinsiz,
diğeri `odunc` üzerinde üç dizinlidir.

```sh
rm -f dizinsiz.db dizinli.db
sqlite3 dizinsiz.db < kurulum.sql
cp dizinsiz.db dizinli.db
sqlite3 dizinli.db 'CREATE INDEX o_uye ON odunc(uye_id);
CREATE INDEX o_kitap ON odunc(kitap_id);
CREATE INDEX o_alis ON odunc(alis_tarihi);'

for db in dizinsiz.db dizinli.db; do
  printf '%s ' "$db"
  sqlite3 "$db" <<'SQL'
.timer on
INSERT INTO odunc (kitap_id, uye_id, alis_tarihi, iade_tarihi)
WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n < 200000)
SELECT 1 + ((n*7) % 200000), 1 + ((n*13) % 120000),
       date('2024-01-01','+'||(n%300)||' days'), NULL
FROM s;
SQL
done
ls -l dizinsiz.db dizinli.db | awk '{print $9, $5}'
```

{/* mask: (?<=Run Time: real )[0-9.]+ */}
{/* mask: (?<=user )[0-9.]+(?= sys) */}
{/* mask: (?<=sys )[0-9.]+ */}
```
dizinsiz.db Run Time: real 0.060 user 0.055046 sys 0.003094
dizinli.db Run Time: real 0.397 user 0.289750 sys 0.095673
dizinli.db 191307776
dizinsiz.db 90783744
```

Aynı ekleme üç dizin varken bu ortamda altı buçuk kat uzun sürdü ve dosya iki katından
fazla büyüdü. Nedeni doğrudan: eklenen her satır için üç ayrı ağaca da bir anahtar
yazılır ve o ağaçların dengesi korunur. Silme ve anahtar sütunu güncelleyen `UPDATE` için
de aynı bedel geçerlidir. Dizin bu yüzden "eklenince iyi olur" türü bir seçenek değil,
okuma ile yazma arasında bilinçli bir ödünleşimdir.

#### Seçicilik

Her sütuna dizin açmak, bedeli çoğaltmanın yanı sıra çoğu zaman kazandırmaz da. Belirleyen
ölçü **seçiciliktir (selectivity)**: bir koşulun tablonun ne kadar küçük bir bölümünü
seçtiği. `kitap` tablosunda `sube_id` sekiz farklı değer alır, `yazar` ise dört bin.
Aşağıdaki ölçüm aynı sorgu biçimini iki sütun için, dizinsiz ve dizinli olarak
çalıştırır.

```sh
rm -f secicilik.db
sqlite3 secicilik.db < kurulum.sql

echo "dizinsiz:"
sqlite3 secicilik.db <<'SQL'
.stats vmstep
SELECT sum(yil) FROM kitap WHERE sube_id = 3;
SELECT sum(yil) FROM kitap WHERE yazar = 'Yazar 7';
SQL

sqlite3 secicilik.db 'CREATE INDEX kitap_sube ON kitap(sube_id);
CREATE INDEX kitap_yazar ON kitap(yazar);'

echo "dizinli:"
sqlite3 secicilik.db <<'SQL'
.stats vmstep
SELECT sum(yil) FROM kitap WHERE sube_id = 3;
SELECT sum(yil) FROM kitap WHERE yazar = 'Yazar 7';
SQL
```

```
dizinsiz:
49674950
VM-steps: 650011
99075
VM-steps: 600111
dizinli:
49674950
VM-steps: 125012
99075
VM-steps: 262
```

Yazar koşulu iki bin üç yüz kat ucuzladı; şube koşulu yalnızca beş kat. Ayrım
seçicilikten geliyor: `yazar = 'Yazar 7'` iki yüz bin kitabın ellisini, `sube_id = 3`
ise yirmi beş binini seçiyor. İkinci durumda veritabanı yirmi beş bin dizin girdisini
okuduktan sonra yirmi beş bin satırı ayrıca tablodan almak zorunda; bu iş, tabloyu
sırayla okumaktan çok da ucuz değil. Seçiciliği düşük bir sütuna açılan dizin, yazma
bedelini öder ama okuma tarafında karşılığını vermez.

#### Özet

- Dizinsiz bir sütun üzerindeki koşul tam tablo taramasıyla karşılanır; maliyet satır
  sayısıyla doğru orantılıdır.
- Dizin, sütun değerlerini sıralı tutan ayrı bir yapıdır; B-ağacı biçimindeki bir dizinde
  arama maliyeti satır sayısının logaritmasıyla artar.
- Aynı sorgu bu veri kümesinde 6.000.028 adımdan 62 adıma indi; süre zamanlayıcının
  çözünürlüğünün altına düştüğü için ölçü olarak adım sayısı kullanıldı.
- Dizin yer kaplar ve her ekleme, silme ve anahtar güncellemesini pahalılaştırır; üç
  dizinli tabloya ekleme bu ortamda altı buçuk kat uzun sürdü.
- Kazanç seçicilikle belirlenir: az sayıda satır seçen koşullarda dizin büyük kazandırır,
  tablonun büyük bölümünü seçen koşullarda kazanç kaybolur.

#### Sonraki Adım

Bu derste plan çıktısına iki kez bakıldı ve iki kelime ayırt edildi: `SCAN` ve `SEARCH`.
Gerçek sorgular tek düğümlü değildir — birleştirmeler, sıralamalar, gruplamalar ve alt
sorgular planı çok satırlı bir ağaca dönüştürür. Sonraki ders bu ağacı okumayı ele alır:
düğümlerin hangi sırada çalıştığı, hangi düğümün hangi tabloyu sürdüğü ve sıralamanın
plana nasıl yansıdığı. Plan çıktısının biçimi motordan motora değişir; okunacak olan
biçim değil, plandaki karardır.

### Sorgu Planı Okuma

Source: https://academia.sh/tr/kurslar/ileri-sql/sorgu-plani-okuma

Plan ağacının yapısı ve okunma yönü, tarama ile arama düğümlerinin ayrımı, kapsayan dizinin plandaki izi, birleştirme düğümlerinin sırası, sıralama düğümünün ölçülen maliyeti ve bileşik dizinde sütun sırasının plana etkisi.

Önceki ders plan çıktısına iki kez baktı ve iki kelime ayırt etti: `SCAN` ve `SEARCH`.
Tek tablolu, tek koşullu bir sorguda plan tek satırdır ve okunması kolaydır.

Gerçek sorgular böyle değildir. Üç tablo birleştiren, süzen, gruplayan ve sıralayan bir
sorgunun planı çok düğümlü bir ağaçtır; bu ağaçtaki her düğüm bir karar taşır. Bu ders o
ağacı okumayı ele alır: düğümlerin ne anlattığını, hangi tablonun diğerlerini sürdüğünü
ve sıralamanın plana nasıl yansıdığını.

#### Veri Kümesi

Ölçümler önceki dersteki kütüphane veri kümesi üzerinde yapılır. Aşağıdaki blok bu kümeyi
sıfırdan kurar; başka hiçbir dosya gerektirmez.

```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
```

#### Plan Bir Ağaçtır

Belli bir yazarın kitaplarının kimler tarafından ödünç alındığını soran sorgu üç tabloya
dokunur. Planı da üç satırdır.

```sh
sqlite3 kutuphane.db <<'SQL'
CREATE INDEX odunc_uye   ON odunc(uye_id);
CREATE INDEX odunc_kitap ON odunc(kitap_id);
CREATE INDEX kitap_yazar ON kitap(yazar);
EXPLAIN QUERY PLAN
SELECT u.ad, k.baslik, o.alis_tarihi
FROM odunc o
JOIN uye u   ON u.uye_id   = o.uye_id
JOIN kitap k ON k.kitap_id = o.kitap_id
WHERE k.yazar = 'Yazar 7';
SQL
```

```
QUERY PLAN
|--SEARCH k USING INDEX kitap_yazar (yazar=?)
|--SEARCH o USING INDEX odunc_kitap (kitap_id=?)
`--SEARCH u USING INTEGER PRIMARY KEY (rowid=?)
```

Üç satır, üç tablo erişimidir ve sıraları anlamlıdır. En üstteki düğüm **sürücü tablodur
(driving table)**: veritabanı işe oradan başlar. Alttaki düğümler, sürücüden gelen her
satır için bir kez çalışır. Burada iş `kitap` tablosundan başlıyor, çünkü tek süzme
koşulu orada; `yazar = 'Yazar 7'` elli satır bırakıyor. Bu elli kitabın her biri için
`odunc` içinde `kitap_id` araması yapılıyor, bulunan her ödünç kaydı için de `uye`
tablosundan birincil anahtarla tek satır çekiliyor.

Sorgu metninde `odunc` önce yazılmıştı. Plan bunu dikkate almadı: `FROM` yan tümcesindeki
sıra bir yürütme sırası değil, bir ad listesidir. Erişim sırasına planlayıcı karar verir.

#### Tarama ve Arama Düğümleri

Plan düğümlerinin ilk ayrımı, tablonun baştan sona mı okunduğu yoksa bir anahtardan mı
girildiğidir.

`SCAN`, tablodaki ya da dizindeki her kaydın sırayla okunduğunu söyler. `SEARCH`, bir
anahtar üzerinden girildiğini ve yalnız eşleşen bölümün okunduğunu söyler; parantez
içindeki `(uye_id=?)` hangi koşulun anahtar olarak kullanıldığını gösterir. Üçüncü bir
ayrım, aranan yapının kendisidir: `USING INTEGER PRIMARY KEY (rowid=?)` ayrı bir dizin
değil, tablonun kendi birincil anahtarıdır.

Dördüncü kelime, tabloya hiç dokunulmadığını söyler.

```sh
sqlite3 kutuphane.db <<'SQL'
EXPLAIN QUERY PLAN SELECT count(*) FROM odunc WHERE uye_id = 4242;
EXPLAIN QUERY PLAN SELECT count(iade_tarihi) FROM odunc WHERE uye_id = 4242;
.stats vmstep
SELECT count(*) FROM odunc WHERE uye_id = 4242;
SELECT count(iade_tarihi) FROM odunc WHERE uye_id = 4242;
SQL
```

```
QUERY PLAN
`--SEARCH odunc USING COVERING INDEX odunc_uye (uye_id=?)
QUERY PLAN
`--SEARCH odunc USING INDEX odunc_uye (uye_id=?)
17
VM-steps: 62
17
VM-steps: 97
```

İki sorgu aynı dizini kullanıyor, ancak birincisinin planında `COVERING` sözcüğü var.
**Kapsayan dizin (covering index)**, sorgunun istediği bütün sütunları kendi içinde
barındıran dizindir: `uye_id` dizinde zaten var, sayım için başka sütun gerekmiyor, bu
yüzden tabloya hiç gidilmiyor. İkinci sorgu `iade_tarihi` sütununu istiyor; o sütun
dizinde olmadığı için her eşleşen dizin girdisinden tabloya bir dönüş yapılıyor. On yedi
satır için fark otuz beş adım; bu fark eşleşen satır sayısıyla doğru orantılı büyür.

#### Sıralama Düğümü

Bazı düğümler tabloya değil, veri üzerinde yapılan bir işe karşılık gelir. Sıralama
bunların en pahalısıdır.

```sh
sqlite3 kutuphane.db <<'SQL'
EXPLAIN QUERY PLAN SELECT odunc_id, alis_tarihi FROM odunc ORDER BY alis_tarihi LIMIT 5;
.timer on
.stats vmstep
SELECT odunc_id, alis_tarihi FROM odunc ORDER BY alis_tarihi LIMIT 5;
.timer off
.stats off
CREATE INDEX odunc_alis ON odunc(alis_tarihi);
EXPLAIN QUERY PLAN SELECT odunc_id, alis_tarihi FROM odunc ORDER BY alis_tarihi LIMIT 5;
.timer on
.stats vmstep
SELECT odunc_id, alis_tarihi FROM odunc ORDER BY alis_tarihi LIMIT 5;
SQL
```

{/* mask: (?<=Run Time: real )[0-9.]+ */}
{/* mask: (?<=user )[0-9.]+(?= sys) */}
{/* mask: (?<=sys )[0-9.]+ */}
```
QUERY PLAN
|--SCAN odunc
`--USE TEMP B-TREE FOR ORDER BY
2437|2018-01-01
4874|2018-01-01
7311|2018-01-01
9748|2018-01-01
12185|2018-01-01
VM-steps: 12000130
Run Time: real 0.066 user 0.057176 sys 0.008530
QUERY PLAN
`--SCAN odunc USING COVERING INDEX odunc_alis
2437|2018-01-01
4874|2018-01-01
7311|2018-01-01
9748|2018-01-01
12185|2018-01-01
VM-steps: 32
Run Time: real 0.000 user 0.000014 sys 0.000014
```

`USE TEMP B-TREE FOR ORDER BY` düğümü, sıralamanın geçici bir yapı kurularak yapıldığını
söyler. Beş satır isteniyor olması bunu ucuzlatmıyor: en küçük beş tarihi bulmak için iki
milyon satırın hepsinin görülmesi gerekiyor, çünkü sıra bilinmiyor.

Dizin oluşturulduğunda sıralama düğümü plandan kayboluyor. Dizin zaten tarihe göre
sıralıdır; veritabanı baştan beş girdi okuyup duruyor. On iki milyon adım otuz iki adıma
iniyor — dizinin ikinci işlevi budur: aramayı ucuzlatmanın yanı sıra **hazır bir sıra**
sunar. Süre ise yine ölçüm sınırının altına düşüyor; ortama bağlı olan süre değil,
oranların yönü kararlıdır.

#### Bileşik Dizinde Sütun Sırası

**Bileşik dizin (composite index)**, birden çok sütun üzerinde tanımlanan dizindir ve
girdileri önce birinci sütuna, eşitlik durumunda ikinciye göre sıralar. Sıra bir ayrıntı
değil, dizinin ne işe yarayacağını belirleyen karardır. Aşağıdaki ölçüm aynı sorguyu iki
farklı sütun sırasıyla çalıştırır.

```sh
for sira in "alis_tarihi, uye_id" "uye_id, alis_tarihi"; do
  rm -f sira.db
  sqlite3 sira.db < kurulum.sql
  sqlite3 sira.db "CREATE INDEX odunc_bilesik ON odunc($sira);"
  printf 'dizin: (%s)\n' "$sira"
  sqlite3 sira.db <<'SQL'
EXPLAIN QUERY PLAN
SELECT odunc_id FROM odunc WHERE uye_id = 4242 ORDER BY alis_tarihi;
.stats vmstep
SELECT count(*) FROM
  (SELECT odunc_id FROM odunc WHERE uye_id = 4242 ORDER BY alis_tarihi);
SQL
done
```

```
dizin: (alis_tarihi, uye_id)
QUERY PLAN
`--SCAN odunc USING COVERING INDEX odunc_bilesik
17
VM-steps: 6000101
dizin: (uye_id, alis_tarihi)
QUERY PLAN
`--SEARCH odunc USING COVERING INDEX odunc_bilesik (uye_id=?)
17
VM-steps: 135
```

İki dizin aynı iki sütunu içeriyor, aynı yeri kaplıyor ve aynı sorgu çalışıyor. Fark kırk
dört bin kat. Nedeni sıralama düzenidir: telefon rehberi soyada göre sıralıysa ada göre
arama yapılamaz. `(alis_tarihi, uye_id)` dizininde `uye_id` değerleri tarih tarih
dağılmıştır; bir üyeyi bulmak için dizinin tamamının okunması gerekir — plan bunu `SCAN`
diyerek söyler. `(uye_id, alis_tarihi)` dizininde ise aynı üyenin bütün kayıtları yan
yanadır ve tarih sırasındadır; plan `SEARCH` diyor ve sıralama düğümü hiç görünmüyor.

Kural şudur: eşitlikle süzülen sütunlar dizinin başında, aralık koşulu ve sıralama için
kullanılan sütunlar sonrasında yer alır.

#### Okunacak Olan Biçim Değil Karar

Buradaki çıktılar tek bir motorun biçimidir. Başka motorlarda plan bir ağaç yerine
girintili bir liste, bir tablo ya da düğüm başına tahmini satır sayısı ve maliyet taşıyan
satırlar olarak gelir; düğüm adları da değişir — sıralı tarama, dizin taraması, karma
birleştirme gibi adlar aynı kavramların başka isimleridir.

Değişmeyen şey, plandan okunan kararlardır: tabloya baştan sona mı bakıldı yoksa bir
anahtardan mı girildi, hangi tablo diğerlerini sürdü, tabloya dönmek gerekti mi, ayrıca
bir sıralama yapıldı mı. Bir plan çıktısını okumak, bu dört sorunun cevabını çıkarmaktır.

#### Özet

- Plan bir ağaçtır; üstteki düğüm sürücü tablodur ve alttaki düğümler sürücüden gelen her
  satır için çalışır. `FROM` yan tümcesindeki sıra yürütme sırasını belirlemez.
- `SCAN` baştan sona okumayı, `SEARCH` bir anahtardan girmeyi gösterir; parantez içindeki
  koşul hangi sütunun anahtar olarak kullanıldığını söyler.
- `COVERING` sözcüğü sorgunun yalnız dizinden yanıtlandığını, tabloya hiç dönülmediğini
  bildirir.
- Sıralama düğümü ayrı bir maliyettir; uygun bir dizin sıralamayı plandan tamamen kaldırdı
  ve aynı sorgu 12.000.130 adımdan 32 adıma indi.
- Bileşik dizinde sütun sırası, dizinin hangi sorguya yarayacağını belirler; aynı iki
  sütunun ters sırası aynı sorguyu 135 adımdan 6.000.101 adıma çıkardı.

#### Sonraki Adım

Bu dersteki bütün ölçümlerde dizin vardı ve kullanıldı. Uygulamada sık karşılaşılan durum
başkadır: dizin vardır, sorgu o sütunu süzer, plan yine de `SCAN` der. Sonraki ders bu
durumu ele alır — sütunun üzerine uygulanan bir işlev, sol ucu açık bir kalıp eşleme ya da
karşılaştırma kuralı uyuşmazlığı dizini nasıl devre dışı bırakır ve aynı sonucu veren
hangi yeniden yazım dizini geri getirir.

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

Source: https://academia.sh/tr/kurslar/ileri-sql/dizin-kullanimini-engelleyen-yazimlar

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
```

{/* mask: (?<=Run Time: real )[0-9.]+ */}
{/* mask: (?<=user )[0-9.]+(?= sys) */}
{/* mask: (?<=sys )[0-9.]+ */}
```
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
```

{/* mask: (?<=Run Time: real )[0-9.]+ */}
{/* mask: (?<=user )[0-9.]+(?= sys) */}
{/* mask: (?<=sys )[0-9.]+ */}
```
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
```

{/* mask: (?<=Run Time: real )[0-9.]+ */}
{/* mask: (?<=user )[0-9.]+(?= sys) */}
{/* mask: (?<=sys )[0-9.]+ */}
```
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: 346
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
```

{/* mask: (?<=Run Time: real )[0-9.]+ */}
{/* mask: (?<=user )[0-9.]+(?= sys) */}
{/* mask: (?<=sys )[0-9.]+ */}
```
QUERY PLAN
`--SEARCH kitap USING 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.

### Birleştirme Eniyilemesi

Source: https://academia.sh/tr/kurslar/ileri-sql/birlestirme-eniyilemesi

İç içe döngü birleştirmesinin maliyet çarpanı, iç taraftaki dizinin belirleyiciliği, birleştirme sırasının ölçülen etkisi, döngünün dış satır sayısıyla doğrusal ölçeklenmesi ve karma ile sıralı birleştirmenin hangi durumda kazandığı.

Önceki derslerdeki 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. Veritabanı üç şeye ayrı ayrı karar
verir: tabloların hangi sırayla okunacağı, her tabloya hangi yolla erişileceği ve
satırların hangi yöntemle eşleştirileceği.

Bu üç karar birbirine bağlıdır ve yanlış birleşimi, doğru birleşiminden yüzlerce kat
pahalıya mal olur. Bu ders kararların plandaki karşılığını ve ölçülen etkisini 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
```

#### İç İçe Döngü ve İçerideki Dizin

Birleştirmenin en temel yöntemi **iç içe döngü birleştirmesidir (nested loop join)**: dış
tablodan okunan her satır için iç tabloda eşleşen satırlar aranır. Maliyeti tek bir çarpım
verir — dış satır sayısı çarpı bir iç aramanın maliyeti. İkinci çarpanı belirleyen şey, iç
taraftaki birleştirme sütununda dizin olup olmadığıdır.

```sh
sqlite3 kutuphane.db <<'SQL'
CREATE INDEX kitap_yazar ON kitap(yazar);
EXPLAIN QUERY PLAN
SELECT count(*) FROM kitap k JOIN odunc o ON o.kitap_id = k.kitap_id
WHERE k.yazar = 'Yazar 7';
.timer on
.stats vmstep
SELECT count(*) FROM kitap k JOIN odunc o ON o.kitap_id = k.kitap_id
WHERE k.yazar = 'Yazar 7';
.timer off
.stats off
CREATE INDEX odunc_kitap ON odunc(kitap_id);
EXPLAIN QUERY PLAN
SELECT count(*) FROM kitap k JOIN odunc o ON o.kitap_id = k.kitap_id
WHERE k.yazar = 'Yazar 7';
.timer on
.stats vmstep
SELECT count(*) FROM kitap k JOIN odunc o ON o.kitap_id = k.kitap_id
WHERE k.yazar = 'Yazar 7';
SQL
```

{/* mask: (?<=Run Time: real )[0-9.]+ */}
{/* mask: (?<=user )[0-9.]+(?= sys) */}
{/* mask: (?<=sys )[0-9.]+ */}
```
QUERY PLAN
|--SCAN o
`--SEARCH k USING INTEGER PRIMARY KEY (rowid=?)
500
VM-steps: 10000512
Run Time: real 0.378 user 0.338558 sys 0.039777
QUERY PLAN
|--SEARCH k USING COVERING INDEX kitap_yazar (yazar=?)
`--SEARCH o USING COVERING INDEX odunc_kitap (kitap_id=?)
500
VM-steps: 1712
Run Time: real 0.000 user 0.000062 sys 0.000049
```

Birinci planda `odunc` sürücü tablo: iki milyon ödünç kaydı okunuyor, her biri için
`kitap` tablosuna birincil anahtarla gidiliyor ve sonunda yazar koşulu süzüyor. Sorgu
beş yüz satır döndürmek için on milyon adım harcıyor.

İkinci planda sıra tersine döndü. Fark, tek bir dizinin varlığıdır: `odunc.kitap_id`
üzerinde dizin olduğu anda `kitap` tablosundan başlamak anlamlı hale geldi. Yazar koşulu
elli kitap bırakıyor, her kitap için ödünç kayıtları dizinden aranıyor. On milyon adım
1.712 adıma indi.

Buradan çıkan kural, birleştirme başarımının en kısa özetidir: **birleştirme sütunlarında
dizin yoksa sıra seçilemez.** Planlayıcının önündeki seçenek sayısı, dizinlerin açtığı
erişim yollarıyla belirlenir.

#### Sıranın Ölçülen Etkisi

Sıranın etkisini yalıtmak için aynı sorgu, dizinler yerinde dururken ters sırada
çalıştırılır. Bu motorda `CROSS JOIN` yazımı birleştirme sırasını sabitler: soldaki tablo
dış döngü olur. Sıra sabitlemenin yazımı motora göre değişir; burada bir ölçüm aracı
olarak kullanılıyor.

```sh
sqlite3 kutuphane.db <<'SQL'
EXPLAIN QUERY PLAN
SELECT count(*) FROM odunc o CROSS JOIN kitap k ON k.kitap_id = o.kitap_id
WHERE k.yazar = 'Yazar 7';
.timer on
.stats vmstep
SELECT count(*) FROM odunc o CROSS JOIN kitap k ON k.kitap_id = o.kitap_id
WHERE k.yazar = 'Yazar 7';
SQL
```

{/* mask: (?<=Run Time: real )[0-9.]+ */}
{/* mask: (?<=user )[0-9.]+(?= sys) */}
{/* mask: (?<=sys )[0-9.]+ */}
```
QUERY PLAN
|--SCAN o USING COVERING INDEX odunc_kitap
`--SEARCH k USING INTEGER PRIMARY KEY (rowid=?)
500
VM-steps: 10000512
Run Time: real 0.067 user 0.063345 sys 0.004211
```

Aynı veri, aynı dizinler, aynı sonuç: 500. Yalnız sıra değişti ve adım sayısı 1.712'den
10.000.512'ye çıktı — beş bin sekiz yüz kat. Dış döngüde iki milyon satır olduğunda içeride
ne kadar ucuz bir arama yapıldığının pek önemi kalmıyor.

Bu çıktı ikinci bir şey daha gösteriyor. Adım sayısı, dizinsiz durumdaki ölçümle birebir
aynı: 10.000.512. Süre ise beş kat kısa. İki ölçünün farklı şeyleri saydığı buradan
görülür: adım sayısı sanal makinenin çalıştırdığı komutları sayar, süre ise okunan baytları
da hesaba katar. Kapsayan dizin üzerinden tarama, tablo üzerinden taramayla aynı sayıda
komut çalıştırır ama çok daha az veri okur.

#### Döngü Nasıl Ölçekleniyor

İç içe döngünün maliyeti dış satır sayısıyla doğru orantılıdır. Aşağıdaki ölçüm iki farklı
süzme koşuluyla dış tarafı büyütür.

```sh
sqlite3 kutuphane.db <<'SQL'
CREATE INDEX kitap_yil ON kitap(yil);
.stats vmstep
SELECT count(*) FROM kitap WHERE yazar = 'Yazar 7';
SELECT count(*) FROM kitap k JOIN odunc o ON o.kitap_id = k.kitap_id
WHERE k.yazar = 'Yazar 7';
SELECT count(*) FROM kitap WHERE yil = 1990;
SELECT count(*) FROM kitap k JOIN odunc o ON o.kitap_id = k.kitap_id
WHERE k.yil = 1990;
SQL
```

```
50
VM-steps: 161
500
VM-steps: 1712
2667
VM-steps: 8012
26670
VM-steps: 90690
```

Dış taraf elli satırdan 2.667 satıra çıktığında (53,3 kat), birleştirmenin adım sayısı
1.712'den 90.690'a çıkıyor (53,0 kat). Oran neredeyse birebir: döngü, dış satır sayısında
$O(n)$ davranıyor ve iç aramanın maliyeti sabit kalıyor.

Bu doğrusallık, iç içe döngünün hem gücü hem sınırıdır. Dış taraf küçükse yöntem
yenilmezdir: hiçbir hazırlık yapılmaz, yalnız gereken satırlara dokunulur. Dış taraf
büyüdükçe her satır için ayrı bir ağaç inişi yapılır ve bu inişlerin toplamı, iki tabloyu
bir kez baştan sona okumaktan pahalı hale gelir.

#### İki Küme Yöntemi

Dış taraf büyüdüğünde satır satır arama yerine küme temelli iki yöntem kullanılır. İkisi
de standart ilişkisel eniyilemenin parçasıdır; hangi motorun hangisini gerçekleştirdiği
değişir.

**Karma birleştirme (hash join)** küçük tarafı bir kez okuyup birleştirme sütunundan bir
karma tablosu kurar, sonra büyük tarafı bir kez okuyup her satırın eşleşmesini bu tablodan
arar. Maliyeti iki girdinin toplamıdır, çarpımı değil. Koşulu vardır: eşitlik
karşılaştırması gerekir ve karma tablosunun belleğe sığması beklenir; sığmazsa tablo
parçalara bölünüp diske taşınır. Veri Yapıları kursundaki Karma Tabloları dersinde kurulan
yapı budur.

**Sıralı birleştirme (merge join)** iki girdiyi de birleştirme sütununa göre sıralı okur ve
iki imleci birlikte ilerleterek tek geçişte eşleştirir. Girdiler zaten sıralıysa — örneğin
her iki tarafta da o sütunda dizin varsa — hazırlık maliyeti yoktur ve bellek kullanımı
düşüktür. Sıralı değilse önce sıralama gerekir; o zaman maliyeti sıralama belirler.

Seçim üç ölçüye bakar: girdilerin büyüklüğü, birleştirme koşulunun eşitlik olup olmaması ve
girdilerin hazır sıralı gelip gelmediği. Küçük dış taraf ve dizinli iç taraf iç içe
döngüyü, iki büyük tablo karma birleştirmeyi, iki sıralı büyük girdi sıralı birleştirmeyi
işaret eder.

Bu ders boyunca ölçülen planların hepsi iç içe döngüdür; kullanılan motorun birleştirme
yöntemi budur. Plan çıktısında hangi ailenin göründüğü motora bağlıdır, ancak okunacak
karar aynıdır: hangi taraf sürüyor, iç tarafa nasıl erişiliyor ve eşleştirme satır satır
mı yoksa küme olarak mı yapılıyor.

#### Özet

- İç içe döngü birleştirmesinin maliyeti, dış satır sayısı ile bir iç aramanın maliyetinin
  çarpımıdır; iç taraftaki dizin ikinci çarpanı belirler.
- Birleştirme sütununda dizin olmadığında planlayıcının sıra seçeneği kalmaz; dizin
  eklendiğinde aynı sorgu 10.000.512 adımdan 1.712 adıma indi.
- Sıra tek başına belirleyicidir: dizinler yerinde dururken sıranın tersine çevrilmesi aynı
  sorgunun adım sayısını beş bin sekiz yüz kat artırdı.
- Aynı adım sayısı aynı süre demek değildir; kapsayan dizin üzerinden tarama, tablo
  taramasıyla aynı komut sayısında beş kat kısa sürdü.
- Dış taraf büyüdükçe iç içe döngü doğrusal büyür; bu noktadan sonra karma birleştirme ya
  da sıralı birleştirme gibi küme temelli yöntemler tercih edilir.

#### Sonraki Adım

Birleştirme, tabloları yan yana getirmenin tek yolu değildir. Aynı soru çoğu zaman alt
sorguyla da yazılabilir ve iki yazım aynı sonucu verirken çok farklı planlar üretebilir —
özellikle alt sorgu dış satıra bağlıysa. Sonraki ders bu yazımları karşılaştırır: hangi alt
sorgu bir kez, hangisi satır başına değerlendirilir ve hangi eşdeğer yeniden yazım aradaki
farkı kapatır.

### Alt Sorgu Azaltma

Source: https://academia.sh/tr/kurslar/ileri-sql/alt-sorgu-azaltma

İlişkili alt sorgunun satır başına değerlendirilmesi ve pencere işleviyle tek geçişe indirgenmesi, aynı sorunun IN, EXISTS ve birleştirme yazımlarının ölçülen planları, NOT IN ile NULL etkileşimi ve yeniden yazımın eşdeğerlik koşulları.

Birleştirme, tabloları yan yana getirmenin tek yolu değildir. Aynı soru çoğu zaman alt
sorguyla da yazılabilir; iki yazım aynı sonucu verir ama aynı planı üretmez.

Ayrımın kaynağı tek bir sorudadır: alt sorgu bir kez mi, yoksa dış sorgunun her satırı
için bir kez mi değerlendirilir? Dış sorguya bağlı olmayan bir alt sorgu bir kez çalışır ve
sonucu kullanılır. Dış satırdan bir değer alan **ilişkili alt sorgu (correlated subquery)**
ise her satır için yeniden çalışır — yani gizli bir döngüdür ve maliyeti önceki dersteki iç
içe döngü formülüne uyar.

#### 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
```

#### İlişkili Alt Sorgu Bir Döngüdür

Soru şu: her üyenin en son aldığı kitap hangisidir? Doğrudan yazımı, her ödünç kaydı için o
üyenin en büyük tarihini hesaplayıp karşılaştırmaktır.

```sh
sqlite3 kutuphane.db <<'SQL'
CREATE INDEX odunc_uye   ON odunc(uye_id);
CREATE INDEX odunc_kitap ON odunc(kitap_id);
CREATE INDEX odunc_alis  ON odunc(alis_tarihi);
EXPLAIN QUERY PLAN
SELECT count(*) FROM odunc o
WHERE o.alis_tarihi = (SELECT max(o2.alis_tarihi) FROM odunc o2 WHERE o2.uye_id = o.uye_id);
.timer on
.stats vmstep
SELECT count(*) FROM odunc o
WHERE o.alis_tarihi = (SELECT max(o2.alis_tarihi) FROM odunc o2 WHERE o2.uye_id = o.uye_id);
.timer off
.stats off
EXPLAIN QUERY PLAN
SELECT count(*) FROM
  (SELECT alis_tarihi, max(alis_tarihi) OVER (PARTITION BY uye_id) AS son FROM odunc)
WHERE alis_tarihi = son;
.timer on
.stats vmstep
SELECT count(*) FROM
  (SELECT alis_tarihi, max(alis_tarihi) OVER (PARTITION BY uye_id) AS son FROM odunc)
WHERE alis_tarihi = son;
SQL
```

{/* mask: (?<=Run Time: real )[0-9.]+ */}
{/* mask: (?<=user )[0-9.]+(?= sys) */}
{/* mask: (?<=sys )[0-9.]+ */}
```
QUERY PLAN
|--SCAN o
`--CORRELATED SCALAR SUBQUERY 1
   `--SEARCH o2 USING INDEX odunc_uye (uye_id=?)
120000
VM-steps: 232279993
Run Time: real 7.855 user 6.967326 sys 0.866287
QUERY PLAN
|--CO-ROUTINE (subquery-1)
|  |--CO-ROUTINE (subquery-3)
|  |  `--SCAN odunc USING INDEX odunc_uye
|  `--SCAN (subquery-3)
`--SCAN (subquery-1)
120000
VM-steps: 73200032
Run Time: real 1.050 user 1.027045 sys 0.022464
```

Birinci planda `CORRELATED SCALAR SUBQUERY` düğümü, alt sorgunun dış satıra bağlı olduğunu
söyler. Dış tarafta iki milyon satır var; her biri için o üyenin bütün kayıtlarına bakılıp
en büyük tarih bulunuyor. Dizinin varlığı iç maliyeti düşürüyor ama döngüyü ortadan
kaldırmıyor: iki yüz otuz iki milyon adım.

İkinci yazım aynı hesabı **pencere işleviyle (window function)** yapıyor. Bileşik Sorgular
konusunda tanıtılan bu yapı, her satıra kendi bölümünün toplu değerini iliştirir. Plandaki
düğümler tek bir geçişi anlatıyor: dizin `uye_id` sırasında okunuyor, aynı üyenin satırları
zaten yan yana olduğu için bölümün en büyüğü tek geçişte belirleniyor. Adım sayısı üçte
birine, süre bu ortamda yedi buçukta birine indi.

Sonuç iki yazımda da aynı: 120.000. Kazancın kaynağı daha iyi bir dizin değil, aynı işin
tekrarlanmamasıdır.

#### Aynı Sorunun Üç Yazımı

"2024 yılında en az bir kez ödünç alınmış kaç kitap var?" sorusu üç ayrı biçimde
yazılabilir. Üçü de aynı sayıyı verir.

```sh
sqlite3 kutuphane.db <<'SQL'
EXPLAIN QUERY PLAN
SELECT count(*) FROM kitap k WHERE k.kitap_id IN
  (SELECT o.kitap_id FROM odunc o WHERE o.alis_tarihi >= '2024-01-01');
EXPLAIN QUERY PLAN
SELECT count(*) FROM kitap k WHERE EXISTS
  (SELECT 1 FROM odunc o WHERE o.kitap_id = k.kitap_id AND o.alis_tarihi >= '2024-01-01');
EXPLAIN QUERY PLAN
SELECT count(DISTINCT k.kitap_id) FROM kitap k JOIN odunc o ON o.kitap_id = k.kitap_id
WHERE o.alis_tarihi >= '2024-01-01';
.timer on
.stats vmstep
SELECT count(*) FROM kitap k WHERE k.kitap_id IN
  (SELECT o.kitap_id FROM odunc o WHERE o.alis_tarihi >= '2024-01-01');
SELECT count(*) FROM kitap k WHERE EXISTS
  (SELECT 1 FROM odunc o WHERE o.kitap_id = k.kitap_id AND o.alis_tarihi >= '2024-01-01');
SELECT count(DISTINCT k.kitap_id) FROM kitap k JOIN odunc o ON o.kitap_id = k.kitap_id
WHERE o.alis_tarihi >= '2024-01-01';
SQL
```

{/* mask: (?<=Run Time: real )[0-9.]+ */}
{/* mask: (?<=user )[0-9.]+(?= sys) */}
{/* mask: (?<=sys )[0-9.]+ */}
```
QUERY PLAN
|--SEARCH k USING INTEGER PRIMARY KEY (rowid=?)
`--LIST SUBQUERY 1
   `--SEARCH o USING INDEX odunc_alis (alis_tarihi>?)
QUERY PLAN
|--SCAN k
`--CORRELATED SCALAR SUBQUERY 1
   `--SEARCH o USING INDEX odunc_kitap (kitap_id=?)
QUERY PLAN
|--USE TEMP B-TREE FOR count(DISTINCT)
|--SEARCH o USING INDEX odunc_alis (alis_tarihi>?)
`--SEARCH k USING INTEGER PRIMARY KEY (rowid=?)
105374
VM-steps: 1536319
Run Time: real 0.262 user 0.146818 sys 0.114816
105374
VM-steps: 9251955
Run Time: real 0.382 user 0.364876 sys 0.017004
105374
VM-steps: 1527452
Run Time: real 0.410 user 0.221352 sys 0.188635
```

Üç yazım, üç ayrı plan. `IN` yazımı alt sorguyu bir liste olarak üretiyor ve dış tarafı
bu listeye karşı süzüyor. `EXISTS` yazımı ilişkili: iki yüz bin kitabın her biri için `odunc` içinde arama
yapıyor ve adım sayısı altı kat yüksek. Birleştirme yazımı en az adımı harcıyor, ancak
`DISTINCT` için geçici bir yapı kurduğundan bu ortamda en uzun süreyi alıyor.

Çıkarılacak sonuç, bir yazımın diğerinden üstün olduğu değildir. Üç ölçüm üç ayrı sıralama
veriyor ve adım sayısıyla süre aynı kazananı göstermiyor. `EXISTS` yazımı, aranan koşul çok
seçici olduğunda ve ilk eşleşmede durabildiğinde öne geçer; `IN` yazımı alt sorgunun sonucu
küçük olduğunda; birleştirme yazımı iki tarafın da büyük olduğu ve yinelemenin sorun
olmadığı durumlarda. Karar ölçümle verilir.

Birleştirme yazımının bir yan etkisi de vardır: bir kitabın 2024 içinde birden çok ödüncü
varsa birleştirme o kitabı birden çok satır olarak üretir. `count(DISTINCT ...)` yazımı bu
yüzden zorunludur — eşdeğerlik, sonucun sayısında değil, satır çokluğunda bozulur.

#### NOT IN ve NULL

Yeniden yazımın eşdeğer olup olmadığı her zaman göründüğü kadar açık değildir. `NOT IN` ile
`NOT EXISTS` arasındaki fark bunun en sert örneğidir.

```sh
sqlite3 kutuphane.db <<'SQL'
SELECT count(*) FROM kitap WHERE kitap_id NOT IN (SELECT 1 UNION ALL SELECT 2);
SELECT count(*) FROM kitap WHERE kitap_id NOT IN (SELECT 1 UNION ALL SELECT NULL);
SELECT count(*) FROM kitap k WHERE NOT EXISTS
  (SELECT 1 FROM (SELECT 1 AS v UNION ALL SELECT NULL) t WHERE t.v = k.kitap_id);
SELECT count(*) FROM odunc WHERE iade_tarihi IS NULL;
SQL
```

```
199998
0
199999
222222
```

İkinci sorgu sıfır döndürüyor. Nedeni üç değerli mantıktır: `kitap_id NOT IN (1, NULL)`
ifadesi, `kitap_id <> 1 AND kitap_id <> NULL` demektir; ikinci karşılaştırma hiçbir zaman
doğru olmaz, bilinmeyen kalır. Bilinmeyen bir koşulu olan satır süzgeçten geçemez, bu
yüzden sonuç kümesi boşalır.

`NOT EXISTS` aynı tuzağa düşmez: eşleşme arar, bulamazsa satırı geçirir. Üçüncü sorgu
199.999 döndürüyor — beklenen sayı.

Bu, akademik bir ayrıntı değildir. Dördüncü sorgu, `odunc` tablosunda iki yüz yirmi iki
binden fazla kaydın `iade_tarihi` alanının boş olduğunu gösteriyor. Bu sütun üzerinden
kurulmuş bir `NOT IN` alt sorgusu sessizce boş sonuç üretir; sorgu hata vermez, yalnız
yanlış cevap verir. `NOT EXISTS` yazımı hem bu davranıştan bağımsızdır hem de çoğu
planlayıcı tarafından daha iyi eniyilenir.

#### Yeniden Yazımın Kuralı

Bu derste dört yazım çifti karşılaştırıldı. Ortak yöntem üç adımdır.

Birinci adım, alt sorgunun dış satıra bağlı olup olmadığını görmektir. Bağlıysa plan
düğümünde bunu söyleyen bir işaret bulunur ve maliyet dış satır sayısıyla çarpılır.
İkinci adım, satır başına yapılan işi tek geçişe indirecek bir yapı aramaktır: pencere
işlevi, gruplama, ortak tablo ifadesi ya da birleştirme. Üçüncü adım, yeniden yazımın
gerçekten eşdeğer olduğunu denetlemektir — NULL davranışı, satır çokluğu ve boş sonuç
durumları bu denetimin üç ayrı kalemidir.

Eşdeğerlik denetiminin en güvenilir biçimi, iki yazımı da çalıştırıp sonuçları
karşılaştırmaktır. Bu derste her karşılaştırmada iki yazımın da aynı sayıyı döndürdüğü
gösterildi; bu bir teyit değil, yeniden yazımın ön koşuludur.

#### Özet

- Dış satıra bağlı bir alt sorgu her satır için yeniden değerlendirilir; maliyeti iç içe
  döngü formülüne uyar ve planda ilişkili olduğunu söyleyen bir düğümle görünür.
- Satır başına yapılan bir toplama, pencere işleviyle tek geçişe indirildiğinde bu veri
  kümesinde 232.279.993 adımdan 73.200.032 adıma düştü.
- Aynı soru `IN`, `EXISTS` ve birleştirme ile yazıldığında üç ayrı plan çıktı; adım sayısı
  ve süre aynı yazımı işaret etmedi, karar ölçümle verilir.
- Birleştirmeye çevrilen bir alt sorgu satır çokluğunu değiştirebilir; eşdeğerlik için
  `DISTINCT` ya da gruplama gerekir.
- Alt sorgunun sonucunda tek bir NULL varsa `NOT IN` boş küme döndürür; `NOT EXISTS` bu
  davranıştan etkilenmez.

#### Sonraki Adım

Buraya kadar planlayıcının verdiği kararlar hep yerinde çıktı: doğru tabloyu sürdü, doğru
dizini seçti. Bu kararları neye dayanarak verdiği ise henüz sorulmadı. Planlayıcı veriyi
sorgu anında okumaz; tablolar ve dizinler hakkında önceden toplanmış istatistiklere bakar
ve her adımda kaç satır döneceğini **tahmin eder**. Sonraki ders bu tahminin kaynağını,
ne kadar isabetli olduğunun nasıl ölçüleceğini ve tahmin bozulduğunda planın nasıl
değiştiğini ele alır.

### İstatistikler ve Kardinalite

Source: https://academia.sh/tr/kurslar/ileri-sql/istatistikler-ve-kardinalite

Planlayıcının satır sayısı tahmini, istatistik toplanmadan kullanılan varsayılan değerler, ANALYZE sonrası tahminin düzelmesi, tahminin plana ölçülen etkisi, eskiyen istatistiğin sonuçları ve ortalamanın çarpık dağılımı anlatamaması.

Önceki derslerde planlayıcının kararları hep yerinde çıktı: doğru tabloyu sürdü, doğru
dizini seçti, gereksiz sıralamadan kaçındı. Bu kararları neye dayanarak verdiği ise
sorulmadı.

Planlayıcı, sorgu çalışmadan önce karar verir. Elinde veri yoktur; olsaydı zaten sorgunun
kendisini çalıştırmış olurdu. Onun yerine, her adımdan kaç satır döneceğini **tahmin
eder** ve seçenekleri bu tahminler üzerinden karşılaştırır. Tahminin kaynağı, tablolar ve
dizinler hakkında önceden toplanmış istatistiklerdir. Bu ders o tahmini görünür kılar,
ölçer ve bozulduğunda ne olduğunu gösterir.

#### 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
```

#### Tahmin Görünür Kılınabilir

`.scanstats est` komutu, plan düğümlerinin yanına dört sayı ekler: düğümün kaç kez
çalıştığı (`loops`), toplam kaç satır ürettiği (`rows`), çalışma başına kaç satır
(`rpl`) ve planlayıcının bunun kaç olacağını **tahmin ettiği** (`est`).

`.scanstats` her `sqlite3` derlemesinde bulunmaz; komutu desteklemeyen bir derlemede bu alanlar
çıktıda görünmez.

{/* run: skip */}
```sh
sqlite3 kutuphane.db <<'SQL'
CREATE INDEX odunc_uye   ON odunc(uye_id);
CREATE INDEX kitap_yazar ON kitap(yazar);
.scanstats est
SELECT count(*) FROM odunc WHERE uye_id = 4242;
SELECT count(*) FROM kitap WHERE yazar = 'Yazar 7';
SQL
```

```
17
QUERY PLAN
`--SEARCH odunc USING COVERING INDEX odunc_uye (uye_id=?)     (loops=1 rows=17 rpl=17.0 est=10.0)
50
QUERY PLAN
`--SEARCH kitap USING COVERING INDEX kitap_yazar (yazar=?)     (loops=1 rows=50 rpl=50.0 est=10.0)
```

Her iki düğümde de tahmin aynı: on. İki dizin farklı yoğunlukta, tablolar farklı
büyüklükte, gerçekleşen sayılar on yedi ve elli. Tahminin ikisinde de on olması, bunun bir
hesap değil bir **varsayılan değer** olduğunu gösterir: istatistik toplanmamış bir dizin
için planlayıcı, eşitlik koşulunun sabit sayıda satır döndüreceğini varsayar.

Bu varsayım, seçenekler birbirine yakınken zararsızdır. Seçeneklerden biri diğerinden yüz
kat pahalıysa ve fark tam da bu sayıdan geliyorsa yanlış plan seçilir.

#### İstatistik Toplamak

`ANALYZE`, tabloları ve dizinleri tarayıp özet bilgileri bir sistem tablosuna yazar.

{/* run: skip */}
```sh
sqlite3 kutuphane.db <<'SQL'
ANALYZE;
SELECT tbl, idx, stat FROM sqlite_stat1 ORDER BY tbl, idx;
.scanstats est
SELECT count(*) FROM odunc WHERE uye_id = 4242;
SELECT count(*) FROM kitap WHERE yazar = 'Yazar 7';
SQL
```

```
kitap|kitap_yazar|200000 50
odunc|odunc_uye|2000000 17
sube||8
uye||120000
17
QUERY PLAN
`--SEARCH odunc USING COVERING INDEX odunc_uye (uye_id=?)     (loops=1 rows=17 rpl=17.0 est=16.0)
50
QUERY PLAN
`--SEARCH kitap USING COVERING INDEX kitap_yazar (yazar=?)     (loops=1 rows=50 rpl=50.0 est=48.0)
```

İstatistik tablosundaki her satır iki sayı taşır: tablonun satır sayısı ve dizin anahtarı
başına düşen ortalama satır sayısı. `odunc_uye` için `2000000 17` yazması, iki milyon
ödünç kaydının bulunduğunu ve bir üyeye ortalama on yedi kayıt düştüğünü söyler. Bu sayıya
**kardinalite tahmini (cardinality estimate)** denir.

Tahminler on yerine on altı ve kırk sekize çıktı; gerçekleşen değerler on yedi ve elli.
Planlayıcı artık iki dizini birbirinden ayırt edebiliyor.

#### Tahminin Plana Etkisi

Tahminin doğrulanması yalnız bir sayının düzelmesi değildir; plan da değişir.

```sh
rm -f plan.db
sqlite3 plan.db < kurulum.sql
sqlite3 plan.db <<'SQL'
CREATE INDEX kitap_sube ON kitap(sube_id);
EXPLAIN QUERY PLAN
SELECT count(*) FROM odunc o JOIN kitap k ON k.kitap_id = o.kitap_id WHERE k.sube_id = 3;
.timer on
.stats vmstep
SELECT count(*) FROM odunc o JOIN kitap k ON k.kitap_id = o.kitap_id WHERE k.sube_id = 3;
.timer off
.stats off
ANALYZE;
EXPLAIN QUERY PLAN
SELECT count(*) FROM odunc o JOIN kitap k ON k.kitap_id = o.kitap_id WHERE k.sube_id = 3;
.timer on
.stats vmstep
SELECT count(*) FROM odunc o JOIN kitap k ON k.kitap_id = o.kitap_id WHERE k.sube_id = 3;
SQL
```

{/* mask: (?<=Run Time: real )[0-9.]+ */}
{/* mask: (?<=user )[0-9.]+(?= sys) */}
{/* mask: (?<=sys )[0-9.]+ */}
```
QUERY PLAN
|--SCAN o
`--SEARCH k USING COVERING INDEX kitap_sube (sube_id=? AND rowid=?)
250000
VM-steps: 10250011
Run Time: real 0.367 user 0.356097 sys 0.011025
QUERY PLAN
|--SCAN o
|--BLOOM FILTER ON k (sube_id=? AND rowid=?)
`--SEARCH k USING INDEX kitap_sube (sube_id=? AND rowid=?)
250000
VM-steps: 13425016
Run Time: real 0.102 user 0.093311 sys 0.009379
```

Sorgu, dizinler ve veri aynı; değişen tek şey istatistiğin varlığı. Plana yeni bir düğüm
eklendi: iki milyon ödünç kaydının çoğunun aradığı şubeye ait olmadığı önceden ucuz bir
süzgeçle eleniyor, ağaç inişi yalnız kalanlar için yapılıyor. Süre bu ortamda üç buçuk kat
düştü.

Adım sayısı ise arttı: 10.250.011'den 13.425.016'ya. İki ölçünün ayrıldığı yer burasıdır.
Süzgecin kendisi de komut çalıştırır; kazandığı şey komut değil, ağaç inişi ve sayfa
okumasıdır. Bu, adım sayısını tek ölçü saymanın sınırıdır — adım sayısı planın yaptığı işi
sayar, o işin birim maliyetini değil.

#### İstatistik Eskirse

İstatistik toplandığı andaki veriyi anlatır. Veri değiştikçe anlattığı şey gerçeklikten uzaklaşır.

{/* run: skip */}
```sh
rm -f eskiyen.db
sqlite3 eskiyen.db < kurulum.sql
sqlite3 eskiyen.db <<'SQL'
CREATE INDEX odunc_uye ON odunc(uye_id);
ANALYZE;
INSERT INTO odunc (kitap_id, uye_id, alis_tarihi, iade_tarihi)
WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n < 500000)
SELECT 1 + ((n * 7) % 200000), 4242,
       date('2025-01-01', '+' || (n % 200) || ' days'), NULL
FROM s;
SELECT tbl, idx, stat FROM sqlite_stat1 WHERE idx = 'odunc_uye';
.scanstats est
SELECT count(*) FROM odunc WHERE uye_id = 4242;
.scanstats off
ANALYZE;
SELECT tbl, idx, stat FROM sqlite_stat1 WHERE idx = 'odunc_uye';
.scanstats est
SELECT count(*) FROM odunc WHERE uye_id = 4242;
SELECT count(*) FROM odunc WHERE uye_id = 5000;
SQL
```

```
odunc|odunc_uye|2000000 17
500017
QUERY PLAN
`--SEARCH odunc USING COVERING INDEX odunc_uye (uye_id=?)     (loops=1 rows=500017 rpl=500017.0 est=16.0)
odunc|odunc_uye|2500000 21
500017
QUERY PLAN
`--SEARCH odunc USING COVERING INDEX odunc_uye (uye_id=?)     (loops=1 rows=500017 rpl=500017.0 est=20.0)
16
QUERY PLAN
`--SEARCH odunc USING COVERING INDEX odunc_uye (uye_id=?)     (loops=1 rows=16 rpl=16.0 est=20.0)
```

Kurmaca bir durum kurulmuş durumda: tek bir üyeye beş yüz bin kayıt eklendi. İstatistik
eklemeden önce toplandığı için hâlâ `2000000 17` diyor ve tahmin on altıda kalıyor;
gerçekleşen sayı 500.017. Tahmin otuz bin kattan fazla sapmış durumda.

`ANALYZE` yeniden çalıştırıldığında istatistik `2500000 21` oluyor. Tahmin on altıdan
yirmiye çıkıyor — hâlâ 500.017'nin yanına bile yaklaşmıyor. Bu bir hata değil, ölçünün
tanımıdır: saklanan sayı bir **ortalamadır** ve iki buçuk milyon kayıt yüz yirmi bin üyeye
bölündüğünde ortalama gerçekten yirmi bir çıkar.

#### Ortalamanın Anlatamadığı

Son çıktının son satırı ayrımı tamamlıyor: 5000 numaralı üye için gerçekleşen değer on
altı, tahmin yirmi. Ortalama, sıradan üyeler için gayet iyi çalışıyor; yalnız aykırı üye
için tamamen yanlış. Çarpık bir dağılımda tek bir ortalama iki gruba birden hizmet edemez.

Bunun karşılığı, değer başına istatistik tutmaktır: en sık geçen değerlerin listesi ve
değer aralıklarının dağılımı — yani bir **histogram (histogram)**. Bazı motorlar bu
bilgiyi toplar ve `uye_id = 4242` ile `uye_id = 5000` için ayrı tahminler üretir.
Toplamayanlarda tek çare, çarpıklığı sorgu yazımıyla ya da kısmi dizinlerle yönetmektir.

İkinci sınır, sütunlar arası bağımlılıktır. Planlayıcı `sehir = 'Bursa'` ve
`kayit_tarihi >= '2020-01-01'` koşullarını çoğunlukla bağımsız kabul eder ve seçicilikleri
çarpar. Koşullar gerçekte bağımlıysa sonuç ciddi biçimde şişer ya da eksilir.

Uygulama tarafında çıkarılacak ders kısadır: istatistik toplama, veri hacmi büyük ölçüde
değiştikten sonra tekrarlanmalıdır — toplu yükleme, arşivleme ve şema değişikliği sonrası.
Bir sorgu açıklanamaz biçimde yavaşladığında bakılacak ilk yerlerden biri, tahmin ile
gerçekleşen arasındaki farktır.

#### Özet

- Planlayıcı kararlarını, her adımdan kaç satır döneceğine dair tahminlere dayandırır;
  bu tahminler plan çıktısında gerçekleşen sayılarla yan yana görülebilir.
- İstatistik toplanmamışken tahmin sabit bir varsayılan değerdir; bu veri kümesinde iki
  ayrı dizin için de on çıktı.
- `ANALYZE`, tablo satır sayısını ve anahtar başına ortalama satır sayısını bir sistem
  tablosuna yazar; tahminler bunun ardından gerçekleşen değerlere yaklaştı.
- İstatistiğin varlığı planı değiştirdi ve süreyi bu ortamda üç buçuk kat düşürdü; aynı
  sorguda adım sayısı arttığı için iki ölçü aynı yönü göstermedi.
- Saklanan sayı bir ortalamadır; çarpık dağılımlarda aykırı değerler için tahmin binlerce
  kat sapabilir, bu yüzden bazı motorlar değer başına histogram tutar.

#### Sonraki Adım

Bu konudaki bütün sorgular sabit metinlerdi. Uygulamalar sorguları çoğu zaman çalışma
zamanında kurar: arama ekranındaki alanlara göre koşullar eklenir, sıralama sütunu
kullanıcıdan gelir. Sorgu metnini dizgi birleştirmeyle kurmak hem doğruluk hem güvenlik
hem de başarım tarafında sonuçlar doğurur. Son ders bu üç sonucu ve bağlı değişkenin
hepsine birden verdiği yanıtı ele alır.

### Dinamik SQL Riskleri

Source: https://academia.sh/tr/kurslar/ileri-sql/dinamik-sql-riskleri

Sorgu metnini dizgi birleştirmeyle kurmanın doğruluk ve güvenlik sonuçları, bağlı değişkenin aynı girdiyi nasıl doğru işlediği, ayrıştırma ve planlama maliyetinin ölçülmesi, plan önbelleğinin çalışma koşulu ve değişken yapıdaki sorguların güvenli kurulumu.

Bu konudaki bütün sorgular sabit metinlerdi. Uygulamalar ise sorguları çoğu zaman çalışma
zamanında kurar: arama ekranında doldurulan alanlara göre koşullar eklenir, sıralama sütunu
kullanıcıdan gelir, sayfa numarası her istekte değişir.

Sorgu metnini kullanıcı girdisiyle birleştirerek kurmak, ilk bakışta yalnız bir yazım
kolaylığı gibi görünür. Üç ayrı sonucu vardır: sorgu sıradan girdilerde kırılır, girdinin
sorgunun anlamını değiştirmesine kapı açar ve planlayıcının yaptığı işi her çağrıda
tekrarlatır. Bu ders üçünü de 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
```

#### Metnin İçine Karışan Girdi

Kütüphane kataloğunda başlığa göre arama yapan bir ekran düşünelim. Girdi, sorgu metnine
tırnaklar arasına yerleştiriliyor. Kataloğa, başlığında kesme işareti bulunan bir kitap
eklenmiş durumda.

```sh
sqlite3 kutuphane.db "INSERT INTO kitap VALUES (200001, 'Ay''in Kizi', 'Yazar 7', 1998, 3);"

aranan="Kitap 4242"
sqlite3 kutuphane.db "SELECT kitap_id, baslik FROM kitap WHERE baslik = '$aranan';"

aranan="Ay'in Kizi"
echo "--- ayni sorgu, tirnak iceren baslikla ---"
sqlite3 kutuphane.db "SELECT kitap_id, baslik FROM kitap WHERE baslik = '$aranan';"
echo "cikis kodu: $?"
```

```
4242|Kitap 4242
--- ayni sorgu, tirnak iceren baslikla ---
Error: in prepare, unrecognized token: "';"
  p_id, baslik FROM kitap WHERE baslik = 'Ay'in Kizi';
                                      error here ---^
cikis kodu: 1
```

Birinci arama çalışıyor. İkincisi, tamamen zararsız bir girdiyle — bir kitap başlığında
bulunan kesme işaretiyle — sözdizimi hatası veriyor. Hata iletisi nedeni açıkça
gösteriyor: girdideki kesme işareti, dizgiyi kapatan tırnak olarak okundu ve sorgunun geri
kalanı bozuldu.

Kırılmanın kaynağı, bir tırnak karakterinin özel olması değildir. Kaynak şudur: **girdi,
veri olarak değil, sorgu metninin bir parçası olarak veritabanına ulaştı.** Veritabanı
kendisine gelen metni bir program olarak ayrıştırır ve o metinde girdinin nerede bitip
programın nerede başladığını ayırt edecek bir bilgisi yoktur.

Aynı mekanizma, güvenlik açığının da tanımıdır. Girdi sorgu metninin parçasıysa, uygun
biçimde hazırlanmış bir girdi sorgunun anlamını değiştirebilir: koşul her zaman doğru hale
gelebilir, sorgunun kapsamı genişleyebilir, hiç istenmeyen bir tabloya erişilebilir. Buna
**SQL enjeksiyonu (SQL injection)** denir ve kaynağı, girdinin özel karakter içermesi değil,
metnin girdi ile birleştirilerek kurulmasıdır. Bu yüzden çözüm de karakter temizlemek
değildir: kaçış listeleri eksik kalır, kodlama farkları listeyi atlatır, her yeni bağlam
yeni bir liste ister. Çözüm, girdinin metne hiç karışmamasıdır.

#### Bağlı Değişken

**Bağlı değişken (bound parameter)**, sorgu metninde değerin durması gereken yere bir yer
tutucu koyar; değer sorgudan ayrı bir kanalla gönderilir. Veritabanı önce metni ayrıştırıp
planlar, sonra değeri o plana yerleştirir.

```sh
aranan="Ay'in Kizi"
sqlite3 kutuphane.db <<SQL
.parameter init
.parameter set :baslik "$aranan"
SELECT kitap_id, baslik FROM kitap WHERE baslik = :baslik;
SQL
```

```
200001|Ay'in Kizi
```

Aynı girdi, aynı veri, farklı sonuç: kayıt bulundu. Sorgu metni `:baslik` yer tutucusunu
içeriyor ve bu metin girdiden bağımsız olarak sabit. Girdinin içinde ne olursa olsun —
kesme işareti, noktalı virgül, `--` dizisi — hepsi tek bir dizgi değeri olarak kalır,
çünkü ayrıştırma girdi gelmeden bitmiştir.

Bu, veri temizlemenin daha iyi bir biçimi değildir; farklı bir yaklaşımdır. Bağlı değişken
kullanıldığında temizlenecek bir şey kalmaz, çünkü girdi hiçbir zaman program metni
olmamıştır. Uygulama tarafında bu, sorgu metninin kod içinde sabit durması ve değerlerin
sürücünün parametre arayüzüyle geçirilmesi demektir.

#### Ayrıştırma ve Planlama Maliyeti

Bağlı değişkenin ikinci kazancı başarım tarafındadır ve ölçülebilir. Bir sorgunun
çalıştırılması iki aşamadır: metnin ayrıştırılıp planlanması ve planın yürütülmesi. Birinci
aşamanın maliyeti, hiç satır döndürmeyen bir sorguda yalıtılabilir.

```sh
sqlite3 kutuphane.db 'CREATE INDEX kitap_yazar ON kitap(yazar);
CREATE INDEX odunc_kitap ON odunc(kitap_id);'

i=1
: > basit.sql
: > karmasik.sql
while [ "$i" -le 20000 ]; do
  printf 'SELECT 1;\n' >> basit.sql
  printf "SELECT s.ad, count(*) FROM odunc o JOIN kitap k ON k.kitap_id = o.kitap_id JOIN uye u ON u.uye_id = o.uye_id JOIN sube s ON s.sube_id = k.sube_id WHERE k.yazar = 'Yok %s' AND u.sehir = 'Bursa' GROUP BY s.ad ORDER BY 2 DESC;\n" "$i" >> karmasik.sql
  i=$((i + 1))
done

printf 'basit    : '; { time sqlite3 kutuphane.db < basit.sql > /dev/null ; } 2>&1 | tr '\n' ' '; echo
printf 'karmasik : '; { time sqlite3 kutuphane.db < karmasik.sql > /dev/null ; } 2>&1 | tr '\n' ' '; echo
```

{/* mask: [0-9]+m[0-9.]+s */}
```
basit    :  real	0m0.022s user	0m0.013s sys	0m0.007s 
karmasik :  real	0m0.220s user	0m0.159s sys	0m0.060s 
```

İki dosya da yirmi bin deyim içeriyor. Karmaşık dosyadaki sorgular hiçbir satır
döndürmüyor: yazar koşulu hiçbir kayıtla eşleşmiyor ve dizin sayesinde yürütme neredeyse
sıfır. Aradaki fark — bu ortamda yaklaşık 0,19 saniye, deyim başına on mikrosaniye
civarı — dört tablolu birleştirmenin ayrıştırılması ve planlanmasıdır. Süreler ortama
bağlıdır; anlamlı olan, planlamanın ölçülebilir bir maliyeti olduğudur.

Veritabanları bu maliyeti **plan önbelleği (plan cache)** ile öderler: derlenmiş plan
saklanır ve aynı sorgu yeniden geldiğinde yeniden planlanmaz. Önbelleğin anahtarı sorgu
metnidir. Bağlı değişken kullanıldığında metin her çağrıda birebir aynıdır ve önbellek
isabet eder. Dizgi birleştirmeyle kurulan sorguda ise her farklı değer farklı bir metin
üretir: önbellek hiç isabet etmez ve tek kullanımlık girdilerle dolarak işe yarar planları
dışarı atar.

Bağlı değişkenin bir ödünleşimi de vardır. Plan tek bir değere göre değil, bütün değerler
için ortaklaşa seçilir; çarpık dağılımlı bir sütunda aynı plan sık geçen değer için de
seyrek geçen değer için de kullanılır. Önceki dersteki ortalama tartışmasının aynısıdır bu.
Bazı motorlar ilk çağrıların değerlerine bakıp bu durumu ele alan yöntemler uygular.

#### Değişken Yapıdaki Sorgular

Değerler bağlı değişkenle geçirilebilir; sütun ve tablo adları geçirilemez, çünkü onlar
sorgunun yapısına aittir. Sıralama sütunu kullanıcıdan geliyorsa yapılacak şey, gelen adı
sabit bir listeye karşı doğrulamak ve yalnız listedeki adı metne koymaktır.

```sh
sutun_girdi="yil"
aranan="Ay'in Kizi"

case "$sutun_girdi" in
  baslik|yazar|yil) sutun="$sutun_girdi" ;;
  *) echo "gecersiz siralama sutunu"; exit 1 ;;
esac

sqlite3 kutuphane.db <<SQL
.parameter init
.parameter set :aranan "$aranan"
SELECT kitap_id, baslik, yil FROM kitap
WHERE baslik = :aranan OR yazar = 'Yazar 7'
ORDER BY $sutun LIMIT 3;
SQL
```

```
7|Kitap 7|1957
12007|Kitap 12007|1957
24007|Kitap 24007|1957
```

Ayrım nettir: `:aranan` bir değerdir ve bağlanır; `$sutun` bir tanımlayıcıdır ve metne
girer, bu yüzden değeri kullanıcının verdiği dizgi değil, listede yer alan sabitlerden
biridir. Gelen ad listede yoksa sorgu hiç kurulmaz. Aynı yöntem koşul sayısı değişen
aramalar için de geçerlidir: koşul parçaları kodun içinde sabit metinler olarak durur,
hangilerinin ekleneceğine kod karar verir, değerlerin hepsi bağlanır.

Bu düzenin başarım tarafındaki yan etkisi, farklı koşul birleşimlerinin farklı sorgu
metinleri üretmesidir. Sayı sınırlı kaldığı sürece — birkaç düzine olası birleşim — plan
önbelleği bunların hepsini tutabilir. Sınırın kaybolduğu yer, metne değer karıştığı yerdir.

#### Özet

- Sorgu metnini girdiyle birleştirerek kurmak, girdinin program metnine karışması demektir;
  kesme işareti içeren sıradan bir başlık bile sorguyu sözdizimi hatasıyla kırar.
- Aynı mekanizma SQL enjeksiyonunun kaynağıdır; çözüm karakter temizlemek değil, girdiyi
  metnin dışında tutmaktır.
- Bağlı değişken, metni girdiden bağımsız kılar: ayrıştırma girdi gelmeden biter, değer
  plana sonradan yerleşir ve aynı girdi doğru çalışır.
- Ayrıştırma ve planlama ölçülebilir bir maliyettir; bu ortamda dört tablolu bir sorgu için
  deyim başına on mikrosaniye civarında ölçüldü.
- Plan önbelleği sorgu metnine göre çalışır; bağlı değişken metni sabitlediği için isabet
  eder, dizgi birleştirme her çağrıda yeni metin üreterek önbelleği işlevsiz kılar.
- Tanımlayıcılar bağlanamaz; sütun ve tablo adları sabit bir listeye karşı doğrulanıp öyle
  kullanılır.

#### Kurs Kapanışı

İleri SQL kursu üç konuda ilerledi. Bileşik Sorgular, tek bir deyimle ifade edilebilecek
soruların sınırını genişletti: alt sorgular, ortak tablo ifadeleri, özyinelemeli sorgular ve
pencere işlevleri. İşlemler, birden çok deyimin bir arada doğru davranmasını sağlayan düzeni
kurdu: kesinleştirme sınırları, yalıtım düzeyleri, kilitlenme ve tetikleyicilerin örtük yan
etkileri. Sorgu Başarımı konusu ise doğru sonucu veren sorgunun ne kadar işle verildiğini
sordu.

Bu son konunun yöntemi, cevaplarından daha kalıcıdır. Her iddia iki ölçüyle sınandı: plan
çıktısı kararı gösterdi, adım sayısı ve süre bedeli gösterdi; süre ortama bağlı olduğu için
yorumlar oranlar üzerinden yapıldı. Plan çıktısının biçimi ve düğüm adları motora göre
değişir — okunacak olan biçim değil, plandaki dört karardır: tarama mı arama mı, hangi
tablo sürüyor, tabloya dönmek gerekiyor mu, ayrıca sıralama yapılıyor mu.

Buradaki bütün eniyilemeler sorgu düzeyindeydi: dizin tanımı, koşul yazımı, birleştirme
sırası, alt sorgu biçimi, istatistik tazeliği ve parametre kullanımı. Sıra motorun
kendisindedir. **İlişkisel Veritabanı Yönetimi** kursu, buraya kadar kara kutu olarak
kullanılan motoru açar: süreç ve bellek mimarisi, verinin sayfalar halinde diske yazılma
biçimi, yazma öncesi günlük ve denetim noktaları, dizinlerin bakımı ve şişmesi, bölümleme,
yedekleme ve zaman noktasına kurtarma, çoğaltma ve devralma. Sorgunun ne kadar işle
çalıştığını ölçmeyi bilen biri, o işin motorun içinde nereye düştüğünü de öğrenmeye hazır
demektir.

