---
title: 'SQL Temelleri'
source: 'https://academia.sh/tr/kurslar/sql-temelleri'
language: tr
updated: '2026-08-17T04:40:29+00:00'
license: 'CC BY-SA 4.0'
lessons: 18
outcomes:
    - 'Veri sorgulama, ekleme, güncelleme ve silme deyimlerini yazar'
    - 'Birleştirme (join) türlerini doğru seçer'
    - 'Gruplama ve toplama işlevleriyle özet üretir'
---

# SQL Temelleri

İlişkisel modeli sorgulayan dil: seçme, süzme, sıralama, boş değer davranışı, birleştirme türleri, toplama ve gruplama ile veriyi ekleyip değiştiren deyimler.

## Sorgulama

SQL'in dil aileleri, SELECT deyiminin yapısı, koşul yazımı, sıralama ve sınırlama, boş değerlerin sorgudaki davranışı ve yerleşik işlevler.

### SQL Dil Aileleri

Source: https://academia.sh/tr/kurslar/sql-temelleri/sql-dil-aileleri

Tanımlama, işleme, sorgulama, denetim ve işlem deyimleri: hangi ailenin neyi değiştirdiği, bildirimsel yazımın anlamı ve kurs boyunca kullanılacak kütüphane şemasının kurulması.

Veri Modelleme ve İlişkisel Kuram kursu bir şema bırakarak bitti: bağıntılar, birincil ve
yabancı anahtarlar, varlık ve başvuru bütünlüğü kısıtları, normalleştirilmiş tablolar. O
kursun ürettiği şey bir tasarımdı — kâğıt üzerinde tutarlı, ama henüz sorulmamış. Bir
şemanın değeri, ona sorulabilen sorularla ölçülür.

Bu kurs o soruları soran dili kurar. Dilin adı **yapılandırılmış sorgu dili**
(Structured Query Language, SQL) ve ilişkisel modelin kuramsal işlemlerini — seçme,
izdüşüm, birleştirme — yazıya döken standart arayüzdür. İlk ders dilin haritasını
çıkarır: hangi deyim ailesi neyi değiştirir, bir deyim nasıl okunur ve kurs boyunca
üzerinde çalışılacak veri nasıl kurulur.

#### Bildirimsel Bir Dil

Programlama Temelleri kursunda yazılan her döngü **nasıl** sorusunu yanıtlıyordu: diziyi
baştan sona gez, koşulu sağlayanı birikece ekle, sonucu döndür. SQL bu soruyu sormaz. SQL
**ne** istendiğini yazar; sonuca hangi sırayla, hangi veri yapısıyla, hangi dizinle
ulaşılacağına motor karar verir. Bu yüzden SQL bir **bildirimsel dildir** (declarative
language).

Ayrımın pratik sonucu şudur: aynı soruyu farklı yazımlarla ifade eden iki sorgu, motorun
seçtiği **sorgu planı** (query plan) aynı olduğunda aynı hızda çalışır. Yazımın kendisi
başarımı belirlemez; belirleyen, motorun yazımdan çıkardığı plandır. Bu, kursun ilerleyen
bölümlerinde tekrar edilecek bir ayrımdır — şimdilik yalnız şu kadarı yeter: SQL'de bir
sorguyu iyileştirmek, çoğunlukla döngüyü hızlandırmak değil, soruyu daha kesin sormaktır.

#### Deyim Aileleri

SQL tek bir dil olarak öğretilir ama içinde birbirinden farklı beş iş yapan deyim kümesi
barındırır. Bu kümeler geleneksel olarak şöyle adlandırılır:

| Aile | Açılım | Ne yapar | Örnek deyimler |
|---|---|---|---|
| Tanımlama | veri tanımlama dili (data definition language) | Şemanın kendisini kurar ve değiştirir | `CREATE`, `ALTER`, `DROP` |
| İşleme | veri işleme dili (data manipulation language) | Tablolardaki satırları ekler, değiştirir, siler | `INSERT`, `UPDATE`, `DELETE` |
| Sorgulama | veri sorgulama dili (data query language) | Var olan veriden yeni bir sonuç kümesi üretir | `SELECT` |
| Denetim | veri denetim dili (data control language) | Nesneler üzerinde yetki verir ve alır | `GRANT`, `REVOKE` |
| İşlem | işlem denetim dili (transaction control language) | Bir grup değişikliği bütün olarak kesinleştirir ya da geri alır | `COMMIT`, `ROLLBACK` |

Ayrım keyfi değil: her ailenin geri alınabilirliği ve etkilediği şey farklıdır. Sorgulama
hiçbir şeyi değiştirmez. İşleme satırları değiştirir ve bir işlem içindeyse geri
alınabilir. Tanımlama şemayı değiştirir ve geri alınabilirliği **motora göre değişir** —
bazı motorlar şema değişikliğini işlemin parçası sayar, bazıları tanımlama deyimini
gördüğü anda açık işlemi kendiliğinden kesinleştirir.

Bu kursun ilk iki konusu yalnız sorgulamayla ilgilenir; üçüncü konu işleme ve tanımlama
deyimlerine geçer. Denetim deyimleri de o konuda ele alınır.

#### Şemayı Kurmak

Kurs boyunca tek bir örnek sürecek: bir kütüphanenin ödünç kayıtları. Dört tablo var —
şubeler, kitaplar, üyeler ve ödünç işlemleri. Bu, İlişkisel Kuram kursunda normalleştirmesi
yapılan şemanın aynısıdır.

Aşağıdaki blok bir veritabanı dosyası oluşturur ve dört tabloyu tanımlar. Komut satırında
çağrılan motor `sqlite3`; dosya adı `kutuphane.db`. Deyimlerin kendisi standart tanımlama
deyimleridir.

```sh
rm -f kutuphane.db
sqlite3 kutuphane.db <<'SQL'
CREATE TABLE sube (
  sube_id INTEGER PRIMARY KEY,
  ad      TEXT NOT NULL,
  sehir   TEXT NOT NULL
);
CREATE TABLE kitap (
  kitap_id   INTEGER PRIMARY KEY,
  baslik     TEXT NOT NULL,
  yazar      TEXT NOT NULL,
  basim_yili INTEGER,
  sube_id    INTEGER REFERENCES sube(sube_id)
);
CREATE TABLE uye (
  uye_id       INTEGER PRIMARY KEY,
  ad           TEXT NOT NULL,
  soyad        TEXT NOT NULL,
  eposta       TEXT,
  kayit_tarihi TEXT NOT NULL
);
CREATE TABLE odunc (
  odunc_id    INTEGER PRIMARY KEY,
  kitap_id    INTEGER NOT NULL REFERENCES kitap(kitap_id),
  uye_id      INTEGER NOT NULL REFERENCES uye(uye_id),
  alis_tarihi TEXT NOT NULL,
  iade_tarihi TEXT
);
SQL
sqlite3 kutuphane.db '.tables'
```

```
kitap  odunc  sube   uye  
```

Tanımlarda İlişkisel Kuram kursundaki üç kavram doğrudan görünüyor. `PRIMARY KEY` birincil
anahtarı, `REFERENCES` yabancı anahtarı, `NOT NULL` ise bir sütunun boş değer kabul
etmediğini bildirir. `basim_yili`, `eposta` ve `iade_tarihi` sütunlarında `NOT NULL`
yazmaması bilinçlidir: basım yılı bilinmeyen kitap, elektronik posta adresi vermemiş üye
ve henüz iade edilmemiş ödünç kaydı olabilir. Bu üç sütun kursun ilerleyen derslerinde boş
değer davranışını göstermek için kullanılacak.

#### Veriyi Yerleştirmek

Şema boş bir iskelettir. Aşağıdaki blok, bir önceki blokta oluşturulan `kutuphane.db`
dosyasına örnek satırları ekler ve tablo başına satır sayısını yazdırır. `.headers on` ve
`.mode column` satırları komut satırı aracının çıktı biçimini belirler; sorgunun kendisiyle
ilgileri yoktur.

```sh
sqlite3 kutuphane.db <<'SQL'
INSERT INTO sube VALUES (1,'Merkez','Ankara'),(2,'Bahçelievler','Ankara'),
  (3,'Kadıköy','İstanbul'),(4,'Konak','İzmir');
INSERT INTO kitap VALUES (1,'Körlük','José Saramago',1995,1),(2,'Tutunamayanlar','Oğuz Atay',1972,1),
  (3,'Kum Kitabı','Jorge Luis Borges',1975,2),(4,'Yaban','Yakup Kadri',1932,2),
  (5,'Sessiz Ev','Orhan Pamuk',1983,3),(6,'Anayurt Oteli','Yusuf Atılgan',NULL,3),
  (7,'Tehlikeli Oyunlar','Oğuz Atay',1973,NULL);
INSERT INTO uye VALUES (1,'Ayşe','Demir','ayse@ornek.test','2023-02-14'),
  (2,'Mehmet','Kaya','mehmet@ornek.test','2023-05-30'),(3,'Zeynep','Arslan',NULL,'2024-01-09'),
  (4,'Emre','Yıldız','emre@ornek.test','2024-03-22'),(5,'Selin','Aydın',NULL,'2024-11-05'),
  (6,'Burak','Şahin','burak@ornek.test','2025-01-18');
INSERT INTO odunc VALUES (1,1,1,'2025-01-10','2025-01-24'),(2,2,1,'2025-02-02','2025-02-20'),
  (3,1,2,'2025-02-11',NULL),(4,3,3,'2025-03-01','2025-03-15'),(5,4,3,'2025-03-18','2025-04-02'),
  (6,1,4,'2025-04-05','2025-04-19'),(7,5,4,'2025-04-21',NULL),(8,2,5,'2025-05-02','2025-05-30'),
  (9,7,1,'2025-05-14','2025-05-28'),(10,3,5,'2025-06-03',NULL),(11,6,2,'2025-06-11','2025-06-25'),
  (12,4,4,'2025-06-20','2025-07-04');
.headers on
.mode column
SELECT 'kitap' AS tablo, COUNT(*) AS satir FROM kitap
UNION ALL SELECT 'uye', COUNT(*) FROM uye
UNION ALL SELECT 'odunc', COUNT(*) FROM odunc;
SQL
```

```
tablo  satir
-----  -----
kitap  7    
uye    6    
odunc  12   
```

Bu iki blok kurs boyunca tekrarlanacak. Her ders kendi başına çalışabilsin diye, izleyen
derslerin başında aynı şema ve veri sıkıştırılmış tek bir blok olarak yeniden verilecek.

#### Bir Sorgunun İskeleti

Üçüncü aile — sorgulama — tek bir deyimden oluşur: `SELECT`. Kurulan veriye yöneltilen en
kısa soru şöyle görünür.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT baslik, basim_yili FROM kitap WHERE yazar = 'Oğuz Atay';
SQL
```

```
baslik             basim_yili
-----------------  ----------
Tutunamayanlar     1972      
Tehlikeli Oyunlar  1973      
```

Deyim üç **yan tümceden** (clause) oluşuyor: `SELECT` hangi sütunların döneceğini,
`FROM` hangi tablodan okunacağını, `WHERE` hangi satırların kalacağını söylüyor. Yazım
sırası bu, ama anlam sırası farklıdır — motor önce `FROM`, sonra `WHERE`, en son `SELECT`
yan tümcesini değerlendirir. Bir sonraki ders bu sırayı ayrıntılandırır.

Yazımla ilgili üç kural şimdiden geçerlidir. Anahtar sözcükler büyük/küçük harfe duyarlı
değildir; büyük harfle yazılmaları yalnızca okunabilirlik geleneğidir. Metin değerleri tek
tırnak içine alınır — çift tırnak standartta **tanıtıcı** (identifier), yani tablo ve sütun
adı, alıntılamak içindir. Her deyim noktalı virgülle biter; bu, birden çok deyimin arka
arkaya yazılabilmesini sağlar.

#### İşlemin Geri Alınabilirliği

İşleme deyimleri şemayı değil satırları değiştirir ve bu değişiklik bir **işlem**
(transaction) içinde geçici tutulabilir. İşlem, ya bütünüyle kesinleşen ya da bütünüyle
geri alınan bir deyim öbeğidir.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
BEGIN;
DELETE FROM odunc WHERE iade_tarihi IS NULL;
SELECT COUNT(*) AS islem_icinde FROM odunc;
ROLLBACK;
SELECT COUNT(*) AS geri_alindiktan_sonra FROM odunc;
SQL
```

```
islem_icinde
------------
9           
geri_alindiktan_sonra
---------------------
12                   
```

İşlemin içinde tablo dokuz satırlı görünüyor, geri alma sonrasında yeniden on iki satır.
Silinen üç satır hiçbir zaman kalıcı olmadı. `COMMIT` yazılsaydı sonuç dokuz satırda
sabitlenecekti. İşlemlerin yalıtım düzeyleri ve eşzamanlılık davranışı İleri SQL kursunun
konusudur; burada yalnız şu kadarı gerekli: bir değişikliği denemenin güvenli yolu, onu
`BEGIN` ile başlayan bir öbeğe koymaktır.

#### Standart ve Lehçe

SQL bir standarda dayanır, ama hiçbir motor standardı eksiksiz gerçekleştirmez ve her
motor standardın üstüne kendi eklerini koyar. Bu eklere **lehçe** (dialect) denir.
Kursta öğretilen kurallar standarda ait olanlardır; bir davranış motora göre değişiyorsa
metinde açıkça işaretlenir.

Ayrım pratikte üç yerde karşımıza çıkar. Birincisi, sonuç kümesini sınırlama yazımıdır —
standart yazım ile yaygın kısayol farklıdır. İkincisi, örüntü eşlemenin büyük/küçük harf
davranışıdır. Üçüncüsü, dış birleştirmenin bazı yönlerinin her motorda bulunmamasıdır. Bu
üçü de ilgili derslerinde ayrıca ele alınacak.

Lehçe farkına karşı tutum, taşınabilirliği bir amaç değil bir ölçüt saymaktır: standart
yazım varken lehçeye özgü yazımı seçmek, sorguyu belirli bir ürüne bağlar. Bu bazen
bilinçli bir karardır; bilinçsiz olduğunda göç maliyeti olarak geri döner.

#### Özet

- SQL bildirimsel bir dildir: sonucun ne olacağı yazılır, nasıl hesaplanacağına motor
  karar verir.
- Deyimler beş aileye ayrılır — tanımlama, işleme, sorgulama, denetim ve işlem — ve her
  ailenin etkilediği şey ile geri alınabilirliği farklıdır.
- Kurs boyunca kullanılacak şema dört tablodan oluşur: şube, kitap, üye ve ödünç işlemi;
  boş değer kabul eden üç sütun bilinçli olarak bırakılmıştır.
- Bir sorgu yan tümcelerden kurulur ve yazım sırası ile değerlendirme sırası aynı değildir.
- İşleme deyimleri bir işlem içine alınarak denenebilir; `ROLLBACK` değişikliği hiç
  olmamış hale getirir.
- Standart SQL ile motor lehçesi ayrı şeylerdir; motora bağlı davranışlar bu kursta ayrıca
  işaretlenir.

#### Sonraki Adım

Bu derste `SELECT` yalnızca bir örnek olarak geçti. Oysa sorgulama ailesinin tamamı tek bu
deyimden ibarettir ve gücü, sütun listesinin sabit adlardan çok daha fazlasını kabul
etmesinden gelir: hesaplanmış ifadeler, yeniden adlandırılmış sütunlar, yinelenen satırların
elenmesi. Sonraki ders `SELECT` yan tümcesinin yapısını ve tablo sütunları ile sorgu
sütunları arasındaki farkı kurar.

### SELECT Yapısı

Source: https://academia.sh/tr/kurslar/sql-temelleri/select-yapisi

Sütun listesinin ne olduğu, takma adlar, hesaplanmış ifadeler, yinelenen satırların elenmesi ve yan tümcelerin yazım sırası ile değerlendirme sırası arasındaki fark.

Önceki ders SQL'in beş deyim ailesini ayırdı ve sorgulama ailesinin tek bir deyimden —
`SELECT` — oluştuğunu söyledi. Bu deyim örnek olarak bir kez kullanıldı: iki sütun, bir
tablo, bir koşul. Oysa `SELECT` yan tümcesine yazılabilecek şey tablo sütunlarının
adlarıyla sınırlı değildir.

Bu ders sütun listesinin ne kabul ettiğini kurar. Bir sorgunun ürettiği sonuç yeni bir
bağıntıdır; sütunları tablodaki sütunlarla aynı olmak zorunda değildir. Adları değişebilir,
sayıları değişebilir, hiç var olmayan bir sütun hesaplanarak eklenebilir.

Aşağıdaki blok kurs boyunca kullanılan şemayı ve örnek veriyi kurar. Bu dersin bütün
sorguları bu blokta oluşturulan `kutuphane.db` dosyası üzerinde çalışır.

```sh
rm -f kutuphane.db
sqlite3 kutuphane.db <<'SQL'
CREATE TABLE sube (sube_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, sehir TEXT NOT NULL);
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL, yazar TEXT NOT NULL,
                    basim_yili INTEGER, sube_id INTEGER REFERENCES sube(sube_id));
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, soyad TEXT NOT NULL,
                  eposta TEXT, kayit_tarihi TEXT NOT NULL);
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL REFERENCES kitap(kitap_id),
                    uye_id INTEGER NOT NULL REFERENCES uye(uye_id), alis_tarihi TEXT NOT NULL,
                    iade_tarihi TEXT);
INSERT INTO sube VALUES (1,'Merkez','Ankara'),(2,'Bahçelievler','Ankara'),
  (3,'Kadıköy','İstanbul'),(4,'Konak','İzmir');
INSERT INTO kitap VALUES (1,'Körlük','José Saramago',1995,1),(2,'Tutunamayanlar','Oğuz Atay',1972,1),
  (3,'Kum Kitabı','Jorge Luis Borges',1975,2),(4,'Yaban','Yakup Kadri',1932,2),
  (5,'Sessiz Ev','Orhan Pamuk',1983,3),(6,'Anayurt Oteli','Yusuf Atılgan',NULL,3),
  (7,'Tehlikeli Oyunlar','Oğuz Atay',1973,NULL);
INSERT INTO uye VALUES (1,'Ayşe','Demir','ayse@ornek.test','2023-02-14'),
  (2,'Mehmet','Kaya','mehmet@ornek.test','2023-05-30'),(3,'Zeynep','Arslan',NULL,'2024-01-09'),
  (4,'Emre','Yıldız','emre@ornek.test','2024-03-22'),(5,'Selin','Aydın',NULL,'2024-11-05'),
  (6,'Burak','Şahin','burak@ornek.test','2025-01-18');
INSERT INTO odunc VALUES (1,1,1,'2025-01-10','2025-01-24'),(2,2,1,'2025-02-02','2025-02-20'),
  (3,1,2,'2025-02-11',NULL),(4,3,3,'2025-03-01','2025-03-15'),(5,4,3,'2025-03-18','2025-04-02'),
  (6,1,4,'2025-04-05','2025-04-19'),(7,5,4,'2025-04-21',NULL),(8,2,5,'2025-05-02','2025-05-30'),
  (9,7,1,'2025-05-14','2025-05-28'),(10,3,5,'2025-06-03',NULL),(11,6,2,'2025-06-11','2025-06-25'),
  (12,4,4,'2025-06-20','2025-07-04');
SQL
```

#### Yıldız ve Açık Liste

En kısa sorgu, tablonun bütün sütunlarını ister.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT * FROM sube;
SQL
```

```
sube_id  ad            sehir   
-------  ------------  --------
1        Merkez        Ankara  
2        Bahçelievler  Ankara  
3        Kadıköy       İstanbul
4        Konak         İzmir   
```

Yıldız, "tablonun o anki bütün sütunları" anlamına gelir. Keşif sırasında yararlıdır ve
kalıcı kodda üç ayrı soruna yol açar. Birincisi, şemaya sonradan sütun eklendiğinde sonuç
kümesinin sütun sayısı sessizce değişir; sütunları sırayla okuyan çağıran taraf bozulur.
İkincisi, gerekmeyen sütunlar da okunur — geniş bir metin sütunu için bu, gereksiz disk ve
ağ trafiği demektir. Üçüncüsü, sorguyu okuyan kişi hangi sütunlara gerçekten ihtiyaç
duyulduğunu göremez.

Açık liste bu üç sorunu birden ortadan kaldırır.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT baslik, yazar FROM kitap;
SQL
```

```
baslik             yazar            
-----------------  -----------------
Körlük             José Saramago    
Tutunamayanlar     Oğuz Atay        
Kum Kitabı         Jorge Luis Borges
Yaban              Yakup Kadri      
Sessiz Ev          Orhan Pamuk      
Anayurt Oteli      Yusuf Atılgan    
Tehlikeli Oyunlar  Oğuz Atay        
```

Sütun listesindeki sıra sonuçtaki sıradır. Tablodaki fiziksel sıra bağlayıcı değildir.

#### Takma Adlar

Sonuç kümesindeki bir sütunun adı `AS` ile değiştirilebilir. Buna **takma ad** (alias)
denir.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT baslik AS kitap_adi, yazar AS yazan FROM kitap WHERE sube_id = 1;
SQL
```

```
kitap_adi       yazan        
--------------  -------------
Körlük          José Saramago
Tutunamayanlar  Oğuz Atay    
```

Takma ad iki işe yarar. Sütuna anlamlı bir ad verir — özellikle hesaplanmış ifadelerde,
çünkü hesaplanmış bir sütunun öntanımlı adı motora göre değişen bir dizedir. İkincisi,
birleştirmelerde aynı adı taşıyan iki sütunu ayırt etmeyi sağlar; bu, ikinci konunun
konusudur.

`AS` sözcüğü çoğu motorda isteğe bağlıdır — `baslik kitap_adi` yazımı da çalışır. Yine de
`AS` yazmak, sütun listesinde unutulmuş bir virgülün sessizce takma ada dönüşmesini
önler. `SELECT baslik yazar FROM kitap` deyimi hata vermez; iki sütun yerine, adı `yazar`
olan tek bir sütun döndürür.

#### Hesaplanmış İfadeler

Sütun listesine sütun adı değil, **ifade** yazılabilir. İfade satır başına
değerlendirilir ve sonuç kümesinde yeni bir sütun olarak görünür.

Metin birleştirme standartta `||` işleciyle yazılır.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT ad || ' ' || soyad AS tam_ad, kayit_tarihi FROM uye;
SQL
```

```
tam_ad         kayit_tarihi
-------------  ------------
Ayşe Demir     2023-02-14  
Mehmet Kaya    2023-05-30  
Zeynep Arslan  2024-01-09  
Emre Yıldız    2024-03-22  
Selin Aydın    2024-11-05  
Burak Şahin    2025-01-18  
```

Birleştirme işlecinin yazımı **motora göre değişir**: bazı motorlar `||` yerine ya da
onunla birlikte bir birleştirme işlevi sunar, bazılarında `||` bambaşka bir anlam taşır.
Taşınabilir olması gereken sorgularda bu, ilk kontrol edilecek noktalardandır.

Aritmetik ifadeler de aynı biçimde yazılır. Aşağıdaki sorgu basım yılından on yıllık
dilimi hesaplar.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT baslik, basim_yili, (basim_yili / 10) * 10 AS on_yil FROM kitap;
SQL
```

```
baslik             basim_yili  on_yil
-----------------  ----------  ------
Körlük             1995        1990  
Tutunamayanlar     1972        1970  
Kum Kitabı         1975        1970  
Yaban              1932        1930  
Sessiz Ev          1983        1980  
Anayurt Oteli                        
Tehlikeli Oyunlar  1973        1970  
```

İki gözlem var. Birincisi, iki tam sayının bölümünün tam sayı olarak mı yoksa ondalık
olarak mı değerlendirileceği **motora göre değişir**; burada tam sayı bölmesi
uygulandığı için `1995 / 10` işlemi `199` verdi. Taşınabilir yazım, bölmeden önce tipi
açıkça belirlemektir.

İkincisi, `Anayurt Oteli` satırında `on_yil` sütunu boş. Basım yılı bilinmediği için
ifadenin sonucu da bilinmiyor: boş değeri içeren aritmetik ifade boş değer üretir. Bu
davranış beşinci dersin tamamını dolduracak kadar sonuç doğurur.

#### Yinelenen Satırların Elenmesi

`SELECT` sonucu bir çoklu kümedir: aynı satır birden çok kez görünebilir. `DISTINCT`
anahtar sözcüğü yinelenen satırları eler.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT DISTINCT yazar FROM kitap;
SQL
```

```
yazar            
-----------------
José Saramago    
Oğuz Atay        
Jorge Luis Borges
Yakup Kadri      
Orhan Pamuk      
Yusuf Atılgan    
```

Yedi kitap var ama altı yazar döndü: aynı yazarın iki kitabı tek satıra indi.

`DISTINCT` sütuna değil, sonuç satırının tamamına uygulanır. `SELECT DISTINCT yazar, baslik`
yazımı yazarları değil, yazar–başlık çiftlerini tekilleştirir; kitap başlıkları farklı
olduğu için hiçbir satır elenmez. Bu, `DISTINCT` ile en sık yapılan yanlıştır: bir sütunu
tekilleştirmek isterken sonuç kümesine ikinci bir sütun eklendiğinde eleme etkisiz kalır.

Çıktıdaki sıraya güvenilmemelidir. Sıralama belirtilmeyen bir sorgunun satır sırası
tanımsızdır; motor tekilleştirmeyi hangi yöntemle yaparsa sıra ona göre çıkar. Sıra
gerektiğinde dördüncü dersteki sıralama yan tümcesi yazılır.

#### Değerlendirme Sırası

Yazım sırası `SELECT`, `FROM`, `WHERE` biçimindedir; anlam sırası ise `FROM`, `WHERE`,
`SELECT` biçimindedir. Motor önce hangi tablodan okuyacağını belirler, sonra satırları
süzer, en son sütun listesini hesaplar.

Bunun ölçülebilir bir sonucu var: `SELECT` yan tümcesinde tanımlanan bir takma ad,
kendisinden önce değerlendirilen `WHERE` yan tümcesinde standarda göre görünür değildir.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT ad || ' ' || soyad AS tam_ad FROM uye WHERE tam_ad = 'Ayşe Demir';
SQL
```

```
tam_ad    
----------
Ayşe Demir
```

Sorgu burada çalıştı. Ancak bu, standart bir davranış değil, kullanılan motorun
sağladığı bir gevşemedir; aynı sorgu başka bir motorda "böyle bir sütun yok" hatası
verebilir. Davranış **motora göre değişir**. Taşınabilir yazım, ifadenin `WHERE` içinde
tekrar edilmesidir:

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT ad || ' ' || soyad AS tam_ad FROM uye WHERE ad || ' ' || soyad = 'Ayşe Demir';
SQL
```

```
tam_ad    
----------
Ayşe Demir
```

İfadenin iki kez yazılması motorun onu iki kez hesaplayacağı anlamına gelmez; bildirimsel
dilde tekrar, plana değil yazıma aittir.

#### Özet

- Sonuç kümesi yeni bir bağıntıdır; sütunları tablonun sütunlarıyla aynı olmak zorunda
  değildir.
- Yıldız yazımı keşif için uygundur, kalıcı sorgularda şema değişikliğine kırılgan ve
  gereksiz veri okur.
- Takma ad sütuna ad verir; `AS` yazmak, unutulmuş virgülün sessizce takma ada dönüşmesini
  önler.
- Sütun listesine ifade yazılabilir; boş değer içeren ifade boş değer üretir.
- `DISTINCT` tek bir sütuna değil, sonuç satırının tamamına uygulanır.
- Yazım sırası ile değerlendirme sırası ayrıdır; `SELECT` içindeki takma adın `WHERE`
  içinde görünürlüğü motora göre değişir.

#### Sonraki Adım

Bu dersteki `WHERE` yan tümceleri tek bir eşitlikten ibaretti. Oysa süzme, sorgunun
sonucunu en çok belirleyen yan tümcedir: karşılaştırma işleçleri, mantıksal bağlaçlar,
aralık ve liste denetimleri, metin örüntüsü eşleme. Sonraki ders koşul yazımını bütün
biçimleriyle kurar ve örüntü eşlemenin büyük/küçük harf davranışının neden motora bağlı
olduğunu gösterir.

### Koşullar

Source: https://academia.sh/tr/kurslar/sql-temelleri/kosullar

Karşılaştırma işleçleri, mantıksal bağlaçların önceliği, aralık ve liste denetimleri, örüntü eşleme ile kaçış karakteri ve büyük/küçük harf davranışının motora bağlı olması.

Önceki ders sonuç kümesinin sütunlarını belirledi: hangi sütunlar, hangi adlarla, hangi
ifadelerle. Satırlar konusunda tek bir araç kullanıldı — tek bir eşitlik koşulu. Bir
sorgunun döndürdüğü satır sayısını belirleyen asıl yan tümce ise `WHERE`.

`WHERE` yan tümcesi, `FROM` yan tümcesinin ürettiği her satır için bir mantıksal ifade
değerlendirir ve yalnızca **doğru** sonuç veren satırları geçirir. Bu ders o ifadenin
yazılabileceği bütün biçimleri kurar: karşılaştırma, bağlaç, aralık, liste ve metin
örüntüsü.

Aşağıdaki blok şemayı ve örnek veriyi kurar; dersin bütün sorguları bu blokta oluşturulan
`kutuphane.db` dosyası üzerinde çalışır.

```sh
rm -f kutuphane.db
sqlite3 kutuphane.db <<'SQL'
CREATE TABLE sube (sube_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, sehir TEXT NOT NULL);
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL, yazar TEXT NOT NULL,
                    basim_yili INTEGER, sube_id INTEGER REFERENCES sube(sube_id));
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, soyad TEXT NOT NULL,
                  eposta TEXT, kayit_tarihi TEXT NOT NULL);
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL REFERENCES kitap(kitap_id),
                    uye_id INTEGER NOT NULL REFERENCES uye(uye_id), alis_tarihi TEXT NOT NULL,
                    iade_tarihi TEXT);
INSERT INTO sube VALUES (1,'Merkez','Ankara'),(2,'Bahçelievler','Ankara'),
  (3,'Kadıköy','İstanbul'),(4,'Konak','İzmir');
INSERT INTO kitap VALUES (1,'Körlük','José Saramago',1995,1),(2,'Tutunamayanlar','Oğuz Atay',1972,1),
  (3,'Kum Kitabı','Jorge Luis Borges',1975,2),(4,'Yaban','Yakup Kadri',1932,2),
  (5,'Sessiz Ev','Orhan Pamuk',1983,3),(6,'Anayurt Oteli','Yusuf Atılgan',NULL,3),
  (7,'Tehlikeli Oyunlar','Oğuz Atay',1973,NULL);
INSERT INTO uye VALUES (1,'Ayşe','Demir','ayse@ornek.test','2023-02-14'),
  (2,'Mehmet','Kaya','mehmet@ornek.test','2023-05-30'),(3,'Zeynep','Arslan',NULL,'2024-01-09'),
  (4,'Emre','Yıldız','emre@ornek.test','2024-03-22'),(5,'Selin','Aydın',NULL,'2024-11-05'),
  (6,'Burak','Şahin','burak@ornek.test','2025-01-18');
INSERT INTO odunc VALUES (1,1,1,'2025-01-10','2025-01-24'),(2,2,1,'2025-02-02','2025-02-20'),
  (3,1,2,'2025-02-11',NULL),(4,3,3,'2025-03-01','2025-03-15'),(5,4,3,'2025-03-18','2025-04-02'),
  (6,1,4,'2025-04-05','2025-04-19'),(7,5,4,'2025-04-21',NULL),(8,2,5,'2025-05-02','2025-05-30'),
  (9,7,1,'2025-05-14','2025-05-28'),(10,3,5,'2025-06-03',NULL),(11,6,2,'2025-06-11','2025-06-25'),
  (12,4,4,'2025-06-20','2025-07-04');
SQL
```

#### Karşılaştırma İşleçleri

Altı karşılaştırma işleci vardır: `=`, `<>`, `<`, `>`, `<=`, `>=`. Eşitsizlik işlecinin
standart yazımı `<>` biçimindedir; çoğu motor `!=` yazımını da kabul eder, ancak taşınabilir
olan `<>` yazımıdır.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT baslik, basim_yili FROM kitap WHERE basim_yili > 1975;
SQL
```

```
baslik     basim_yili
---------  ----------
Körlük     1995      
Sessiz Ev  1983      
```

Yedi kitaptan ikisi döndü. Geri kalan beş kitabın dördü koşulu sağlamıyor; beşincisinin
— `Anayurt Oteli` — basım yılı bilinmiyor. Bilinmeyen bir değerin `1975`'ten büyük olup
olmadığı da bilinmez, dolayısıyla satır geçmez. Boş değerin koşullardaki davranışı beşinci
dersin konusu; şimdilik akılda tutulacak kural şudur: boş değer içeren bir karşılaştırma
ne doğru ne yanlıştır ve `WHERE` yalnız doğruyu geçirir.

Metin karşılaştırması da aynı işleçlerle yapılır.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT baslik, yazar FROM kitap WHERE yazar <> 'Oğuz Atay';
SQL
```

```
baslik         yazar            
-------------  -----------------
Körlük         José Saramago    
Kum Kitabı     Jorge Luis Borges
Yaban          Yakup Kadri      
Sessiz Ev      Orhan Pamuk      
Anayurt Oteli  Yusuf Atılgan    
```

Metin karşılaştırmasında iki dizenin eşit sayılıp sayılmayacağını **harmanlama**
(collation) belirler. Harmanlama, karakter sırasını ve büyük/küçük harf ile aksan
duyarlılığını tanımlar; öntanımlı harmanlama **motora göre değişir**. Aynı sorgu bir
motorda `'oğuz atay'` değerini eşit sayabilir, diğerinde saymayabilir.

#### Mantıksal Bağlaçlar ve Öncelik

Koşullar `AND`, `OR` ve `NOT` ile birleştirilir. Üçünün önceliği farklıdır: `NOT` en
yüksek, sonra `AND`, en düşük `OR`. Bu, Programlama Temelleri kursundaki işleç önceliği
kuralının aynısıdır ve aynı hataya yol açar.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT baslik, yazar, basim_yili FROM kitap
WHERE yazar = 'Oğuz Atay' OR yazar = 'Orhan Pamuk' AND basim_yili > 1980;
SQL
```

```
baslik             yazar        basim_yili
-----------------  -----------  ----------
Tutunamayanlar     Oğuz Atay    1972      
Sessiz Ev          Orhan Pamuk  1983      
Tehlikeli Oyunlar  Oğuz Atay    1973      
```

Sonuçta 1980 öncesi iki kitap var. Niyet "bu iki yazardan birinin 1980 sonrası kitapları"
olsaydı sonuç yanlış olurdu. `AND` daha yüksek öncelikli olduğu için koşul, "yazar Oğuz
Atay **ya da** (yazar Orhan Pamuk **ve** yıl 1980'den büyük)" biçiminde ayrıştırıldı.
Ayraç niyeti düzeltir.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT baslik, yazar, basim_yili FROM kitap
WHERE (yazar = 'Oğuz Atay' OR yazar = 'Orhan Pamuk') AND basim_yili > 1980;
SQL
```

```
baslik     yazar        basim_yili
---------  -----------  ----------
Sessiz Ev  Orhan Pamuk  1983      
```

Kural: `AND` ile `OR` aynı koşulda geçtiğinde ayraç yazılır. Öncelik kuralına güvenmek
doğru sonuç verse bile, sorguyu okuyanın niyeti çıkarmasını zorlaştırır.

#### Aralık ve Liste

İki karşılaştırmayla yazılan aralık koşulunun kısa bir yazımı vardır.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT baslik, basim_yili FROM kitap WHERE basim_yili >= 1970 AND basim_yili <= 1980;
SQL
```

```
baslik             basim_yili
-----------------  ----------
Tutunamayanlar     1972      
Kum Kitabı         1975      
Tehlikeli Oyunlar  1973      
```

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT baslik, basim_yili FROM kitap WHERE basim_yili BETWEEN 1970 AND 1980;
SQL
```

```
baslik             basim_yili
-----------------  ----------
Tutunamayanlar     1972      
Kum Kitabı         1975      
Tehlikeli Oyunlar  1973      
```

İki sorgu aynı satırları döndürüyor. `BETWEEN` **her iki uca da dahildir**; `1970` ve
`1980` değerleri aralığın içindedir. Bu nokta sık atlanır ve özellikle tarihlerde hataya
yol açar: bir ayın tamamını `BETWEEN` ile seçmek isterken ayın son gününü kapsayan sınırın
gün mü yoksa gün ile saat mi olduğu karışır. Zaman bileşeni taşıyan sütunlarda alt sınırı
dahil, üst sınırı hariç yazan biçim (`>=` ve `<`) daha güvenlidir.

Sonlu bir değer kümesi için `IN` yazılır.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT baslik, yazar FROM kitap WHERE yazar IN ('Oğuz Atay','Orhan Pamuk');
SQL
```

```
baslik             yazar      
-----------------  -----------
Tutunamayanlar     Oğuz Atay  
Sessiz Ev          Orhan Pamuk
Tehlikeli Oyunlar  Oğuz Atay  
```

`IN`, `OR` ile bağlanmış eşitliklerin kısaltmasıdır. Uzun listelerde okunabilirliği
artırır ve motora tek bir küme denetimi olarak planlanabilecek bir yapı sunar. Değillemesi
`NOT IN` biçiminde yazılır ve listede boş değer bulunduğunda beklenmedik davranır; bu tuzak
beşinci derste ölçülecek.

#### Değillemenin Sınırı

`NOT` bir koşulu tersine çevirir, ama "tersi" sözcüğü boş değer varken yanıltıcıdır.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT baslik, sube_id FROM kitap WHERE sube_id = 1;
SELECT baslik, sube_id FROM kitap WHERE NOT (sube_id = 1);
SQL
```

```
baslik          sube_id
--------------  -------
Körlük          1      
Tutunamayanlar  1      
baslik         sube_id
-------------  -------
Kum Kitabı     2      
Yaban          2      
Sessiz Ev      3      
Anayurt Oteli  3      
```

İlk sorgu iki satır, ikincisi dört satır döndürdü. Toplam altı; oysa tabloda yedi kitap
var. Kayıp satır `Tehlikeli Oyunlar`: şube kimliği boş olduğu için ne birinci koşulu ne de
değillemesini sağlıyor. Bir koşul ile değillemesinin birlikte tablonun tamamını vermesi,
yalnızca ilgili sütunda boş değer yoksa geçerlidir.

#### Örüntü Eşleme

Metin içinde arama `LIKE` işleciyle yapılır. İki joker karakter vardır: `%` sıfır ya da
daha çok karakteri, `_` tam olarak bir karakteri karşılar.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT baslik FROM kitap WHERE baslik LIKE '_um%';
SQL
```

```
baslik    
----------
Kum Kitabı
```

Örüntü, "herhangi bir karakter, ardından `um`, ardından herhangi bir şey" diyor ve yalnız
bir başlığa uyuyor.

Asıl soru şudur: `LIKE` büyük/küçük harfe duyarlı mıdır? Yanıt **motora göre değişir** ve
aynı motor içinde bile karakterin ASCII olup olmamasına göre değişebilir.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT baslik FROM kitap WHERE baslik LIKE 'k%';
SQL
```

```
baslik    
----------
Körlük    
Kum Kitabı
```

Küçük harfli `k` ile yazılan örüntü, büyük `K` ile başlayan iki başlığı yakaladı: bu
motorda ASCII harfler için `LIKE` harf duyarsızdır. Aynı motor ASCII dışı harflerde ise
duyarlıdır.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT baslik FROM kitap WHERE baslik LIKE '%ö%';
SELECT baslik FROM kitap WHERE baslik LIKE '%Ö%';
SQL
```

```
baslik
------
Körlük
```

İkinci sorgu hiç satır döndürmedi. `Körlük` başlığında `ö` var, ama `Ö` ile yazılan örüntü
eşleşmedi — harf katlaması yalnız ASCII aralığında uygulandı. Türkçe metinlerde bu, sessiz
sonuç kaybının doğrudan kaynağıdır.

Duyarlılığı sorgunun kendisiyle sabitlemek de tam çözüm değildir, çünkü harf dönüştürme
işlevleri de aynı sınıra takılabilir.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT UPPER('Körlük') AS buyuk, LOWER('KÖRLÜK') AS kucuk;
SQL
```

```
buyuk   kucuk 
------  ------
KöRLüK  kÖrlÜk
```

Dönüştürme ASCII harfleri değiştirdi, `ö` ve `Ö` olduğu gibi kaldı. Doğru çözüm, sorguyu
yamamak değil, sütuna harf ve aksan duyarsız bir harmanlama tanımlamak ya da motorun
Unicode desteğini etkinleştirmektir; ikisi de **motora göre değişir**.

Örüntünün kendisinde `%` ya da `_` karakteri aranıyorsa kaçış karakteri bildirilir.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT 'Indirim %20' AS metin,
       'Indirim %20' LIKE '%!%%' ESCAPE '!' AS kacisli,
       'Indirim %20' LIKE '%%%'             AS kacissiz
UNION ALL
SELECT 'Indirim 20',
       'Indirim 20'  LIKE '%!%%' ESCAPE '!',
       'Indirim 20'  LIKE '%%%';
SQL
```

```
metin        kacisli  kacissiz
-----------  -------  --------
Indirim %20  1        1       
Indirim 20   0        1       
```

`ESCAPE '!'` bildirimi, `!%` yazımını "gerçek yüzde işareti" olarak okutur. Kaçışsız
örüntü ise her iki metne de uyuyor, çünkü `%%%` yazımı "herhangi bir şey" demektir. Ölçüt
sütunundaki `1` ve `0` değerleri mantıksal doğru ve yanlışın gösterimidir; boole değerinin
nasıl yazıldığı da **motora göre değişir**.

#### Özet

- `WHERE` yalnızca doğru sonuç veren satırları geçirir; ne doğru ne yanlış olan satır
  elenir.
- `NOT` en yüksek, `AND` orta, `OR` en düşük önceliklidir; ikisi bir arada geçtiğinde
  ayraç yazılır.
- `BETWEEN` her iki uca dahildir; zaman bileşeni olan sütunlarda alt sınır dahil, üst
  sınır hariç yazım daha güvenlidir.
- `IN`, `OR` ile bağlanmış eşitliklerin kısaltmasıdır.
- Bir koşul ile değillemesi tablonun tamamını yalnızca ilgili sütunda boş değer yoksa
  verir.
- `LIKE` işlecinde `%` ve `_` joker karakterlerdir, `ESCAPE` bunları gerçek karakter
  yapar; harf duyarlılığı motora ve karakterin ASCII olup olmamasına göre değişir.

#### Sonraki Adım

Buraya kadar hangi satırların döneceği belirlendi, ama hangi sırayla döneceği hiç
belirlenmedi. Çıktılardaki sıra motorun okuma biçiminden geliyor ve garanti değil.
Sonraki ders sonuç kümesini sıralayan yan tümceyi, çok anahtarlı sıralamayı, boş değerlerin
sıralamadaki yerini ve sonucu ilk N satırla sınırlamanın standart ile yaygın yazımları
arasındaki farkı ele alır.

### Sıralama ve Sınırlama

Source: https://academia.sh/tr/kurslar/sql-temelleri/siralama-ve-sinirlama

Sonuç kümesinin sıralanması, çok anahtarlı sıralama, harmanlamanın sıra üzerindeki etkisi, boş değerlerin yeri ve sonucu ilk satırlarla sınırlamanın standart ile yaygın yazımları.

Önceki üç ders hangi sütunların ve hangi satırların döneceğini belirledi. Hiçbiri satırların
hangi sırayla döneceğini söylemedi. Çıktılarda görülen sıra motorun tabloyu okuma
biçiminden geliyordu; aynı sorgu farklı bir plan seçildiğinde farklı sırada dönebilir.

İlişkisel modelde bir bağıntı sırasız bir küme olduğu için bu şaşırtıcı değil: sıra,
verinin değil sorgunun bir özelliğidir. Sıra isteniyorsa açıkça yazılmalıdır. Bu ders
sıralama yan tümcesini ve onunla birlikte anlam kazanan sınırlama yazımını kurar.

Aşağıdaki blok şemayı ve örnek veriyi kurar; dersin bütün sorguları bu blokta oluşturulan
`kutuphane.db` dosyası üzerinde çalışır.

```sh
rm -f kutuphane.db
sqlite3 kutuphane.db <<'SQL'
CREATE TABLE sube (sube_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, sehir TEXT NOT NULL);
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL, yazar TEXT NOT NULL,
                    basim_yili INTEGER, sube_id INTEGER REFERENCES sube(sube_id));
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, soyad TEXT NOT NULL,
                  eposta TEXT, kayit_tarihi TEXT NOT NULL);
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL REFERENCES kitap(kitap_id),
                    uye_id INTEGER NOT NULL REFERENCES uye(uye_id), alis_tarihi TEXT NOT NULL,
                    iade_tarihi TEXT);
INSERT INTO sube VALUES (1,'Merkez','Ankara'),(2,'Bahçelievler','Ankara'),
  (3,'Kadıköy','İstanbul'),(4,'Konak','İzmir');
INSERT INTO kitap VALUES (1,'Körlük','José Saramago',1995,1),(2,'Tutunamayanlar','Oğuz Atay',1972,1),
  (3,'Kum Kitabı','Jorge Luis Borges',1975,2),(4,'Yaban','Yakup Kadri',1932,2),
  (5,'Sessiz Ev','Orhan Pamuk',1983,3),(6,'Anayurt Oteli','Yusuf Atılgan',NULL,3),
  (7,'Tehlikeli Oyunlar','Oğuz Atay',1973,NULL);
INSERT INTO uye VALUES (1,'Ayşe','Demir','ayse@ornek.test','2023-02-14'),
  (2,'Mehmet','Kaya','mehmet@ornek.test','2023-05-30'),(3,'Zeynep','Arslan',NULL,'2024-01-09'),
  (4,'Emre','Yıldız','emre@ornek.test','2024-03-22'),(5,'Selin','Aydın',NULL,'2024-11-05'),
  (6,'Burak','Şahin','burak@ornek.test','2025-01-18');
INSERT INTO odunc VALUES (1,1,1,'2025-01-10','2025-01-24'),(2,2,1,'2025-02-02','2025-02-20'),
  (3,1,2,'2025-02-11',NULL),(4,3,3,'2025-03-01','2025-03-15'),(5,4,3,'2025-03-18','2025-04-02'),
  (6,1,4,'2025-04-05','2025-04-19'),(7,5,4,'2025-04-21',NULL),(8,2,5,'2025-05-02','2025-05-30'),
  (9,7,1,'2025-05-14','2025-05-28'),(10,3,5,'2025-06-03',NULL),(11,6,2,'2025-06-11','2025-06-25'),
  (12,4,4,'2025-06-20','2025-07-04');
SQL
```

#### Tek Anahtarla Sıralama

`ORDER BY` yan tümcesi sıralama anahtarını alır. Yön belirtilmezse artan sıra
varsayılır; `ASC` ve `DESC` yönü açıkça yazar.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT baslik, basim_yili FROM kitap ORDER BY basim_yili;
SQL
```

```
baslik             basim_yili
-----------------  ----------
Anayurt Oteli                
Yaban              1932      
Tutunamayanlar     1972      
Tehlikeli Oyunlar  1973      
Kum Kitabı         1975      
Sessiz Ev          1983      
Körlük             1995      
```

Basım yılı bilinmeyen kitap en başa geldi. Boş değerin sıralamadaki yeri kuramsal olarak
belirsizdir — bilinmeyen bir sayının 1932'den büyük mü küçük mü olduğu söylenemez — bu
yüzden motorlar bir varsayım yapar ve **bu varsayım motora göre değişir**. Bazıları boş
değerleri artan sıralamada başa, bazıları sona koyar.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT baslik, basim_yili FROM kitap ORDER BY basim_yili DESC;
SQL
```

```
baslik             basim_yili
-----------------  ----------
Körlük             1995      
Sessiz Ev          1983      
Kum Kitabı         1975      
Tehlikeli Oyunlar  1973      
Tutunamayanlar     1972      
Yaban              1932      
Anayurt Oteli                
```

Yön tersine döndüğünde boş değer de tarafını değiştirdi. Yeri sorguda sabitlemek için
standart bir belirteç vardır.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT baslik, basim_yili FROM kitap ORDER BY basim_yili NULLS LAST;
SQL
```

```
baslik             basim_yili
-----------------  ----------
Yaban              1932      
Tutunamayanlar     1972      
Tehlikeli Oyunlar  1973      
Kum Kitabı         1975      
Sessiz Ev          1983      
Körlük             1995      
Anayurt Oteli                
```

`NULLS FIRST` ve `NULLS LAST` belirteçleri artan sıralamada bile boş değerleri sona
alabilir. Desteği **motora göre değişir**; desteklenmeyen motorlarda aynı sonuç, boş olup
olmadığını sınayan bir ifadeyi birinci sıralama anahtarı yaparak elde edilir.

#### Harmanlama ve Metin Sırası

Metin sıralamasının sonucu harmanlamaya bağlıdır. Öntanımlı harmanlama karakterleri
alfabetik değil, kodlarına göre karşılaştırıyorsa sonuç Türkçe alfabe sırasından ayrılır.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT baslik FROM kitap ORDER BY baslik;
SQL
```

```
baslik           
-----------------
Anayurt Oteli    
Kum Kitabı       
Körlük           
Sessiz Ev        
Tehlikeli Oyunlar
Tutunamayanlar   
Yaban            
```

`Kum Kitabı`, `Körlük` başlığından önce geldi. Türkçe alfabede `ö` harfi `u` harfinden
önce gelir; burada uygulanan karşılaştırma ise karakterlerin ikili gösterimini
karşılaştırdığı için `u` önce çıktı. Aynı etki yazar sütununda da görülür: `Orhan Pamuk`
ile `Oğuz Atay` arasındaki sıra da alfabetik değildir.

Bu bir hata değil, bir yapılandırma sonucudur. Dile duyarlı sıralama isteniyorsa sütuna ya
da sorguya dile özgü bir harmanlama tanımlanır; hangi harmanlamaların bulunduğu ve nasıl
adlandırıldığı **motora göre değişir**. Kullanıcıya gösterilen listelerde bu, göz ardı
edildiğinde fark edilmesi güç bir kusurdur.

#### Çok Anahtarlı Sıralama

Birden çok anahtar virgülle ayrılır ve sırayla uygulanır: ilk anahtarda eşit olan satırlar
ikinci anahtara göre sıralanır. Her anahtarın yönü ayrı yazılır.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT baslik, yazar, basim_yili FROM kitap ORDER BY yazar, basim_yili DESC;
SQL
```

```
baslik             yazar              basim_yili
-----------------  -----------------  ----------
Kum Kitabı         Jorge Luis Borges  1975      
Körlük             José Saramago      1995      
Sessiz Ev          Orhan Pamuk        1983      
Tehlikeli Oyunlar  Oğuz Atay          1973      
Tutunamayanlar     Oğuz Atay          1972      
Yaban              Yakup Kadri        1932      
Anayurt Oteli      Yusuf Atılgan                
```

Aynı yazarın iki kitabı yeni basımdan eskiye sıralandı. `DESC` yalnız kendi anahtarına
uygulanır; yazar sütunu artan kaldı.

Sıralama anahtarı ifade de olabilir ve sonuç sütununun sıra numarası da yazılabilir.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT baslik, basim_yili FROM kitap ORDER BY 2 DESC;
SQL
```

```
baslik             basim_yili
-----------------  ----------
Körlük             1995      
Sessiz Ev          1983      
Kum Kitabı         1975      
Tehlikeli Oyunlar  1973      
Tutunamayanlar     1972      
Yaban              1932      
Anayurt Oteli                
```

Sıra numarasıyla yazım kısa ama kırılgandır: sütun listesine bir sütun eklendiğinde
sıralama sessizce başka bir sütuna kayar. Sütun adı ya da takma ad yazmak her zaman daha
güvenlidir.

#### Belirlenimci Sıralama

`ORDER BY` yalnız yazıldığı anahtarlar üzerinde bir sıra kurar. Anahtarlarda eşit olan
satırların kendi aralarındaki sıra tanımsızdır ve motor planı değiştirdiğinde değişebilir.
Yukarıdaki sorguda yazar eşit olduğunda basım yılı ayırt ediciydi; ikisi de eşit olsaydı
sonuç çalıştırmadan çalıştırmaya farklılaşabilirdi.

Sonucun her çalıştırmada aynı olması gerekiyorsa — kütüğe yazılan raporlar, sayfalanmış
listeler, karşılaştırılan çıktılar — sıralama anahtarlarının sonuna benzersiz bir sütun
eklenir. Birincil anahtar bu iş için doğrudan uygundur. `ORDER BY yazar, kitap_id` yazımı,
yazarı aynı olan satırlar arasında bile tek bir sıra tanımlar.

#### Sınırlama

Sıralanmış bir sonucun yalnız ilk birkaç satırı isteniyorsa sınırlama yazılır. Standart
yazım ile yaygın kısayol farklıdır ve bu, kursta ikinci kez karşılaşılan lehçe ayrımıdır.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT baslik FROM kitap ORDER BY baslik FETCH FIRST 3 ROWS ONLY;
SQL
```

```
Parse error near line 3: near "FETCH": syntax error
  SELECT baslik FROM kitap ORDER BY baslik FETCH FIRST 3 ROWS ONLY;
                             error here ---^
```

Standart yazım `OFFSET … ROWS FETCH FIRST … ROWS ONLY` biçimindedir ve burada kullanılan
motor bunu tanımıyor. Motorun kabul ettiği yazım ise geniş bir kesimde ortak olan
kısayoldur.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT baslik, basim_yili FROM kitap ORDER BY basim_yili DESC LIMIT 3;
SQL
```

```
baslik      basim_yili
----------  ----------
Körlük      1995      
Sessiz Ev   1983      
Kum Kitabı  1975      
```

Atlanacak satır sayısı da belirtilebilir.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT baslik, basim_yili FROM kitap ORDER BY basim_yili DESC LIMIT 3 OFFSET 3;
SQL
```

```
baslik             basim_yili
-----------------  ----------
Tehlikeli Oyunlar  1973      
Tutunamayanlar     1972      
Yaban              1932      
```

İki sorgu birlikte, üçer satırlık iki sayfa üretti. Sayfalama yazımının iki tuzağı vardır.
Birincisi, sıralama yazılmadan sınırlama yazmak anlamsızdır: hangi satırların "ilk üç"
olduğu tanımsız kalır ve iki ardışık sayfa aynı satırı iki kez gösterebilir. İkincisi,
atlama sayısıyla sayfalama, sayfalar arasında veri değiştiğinde kayar — silinen bir satır
sonraki sayfanın ilk satırını atlatır. Bu iki tuzağın ikincisi, İleri SQL kursunda anahtara
dayalı sayfalama ile çözülür.

Atlama sayısının başarım maliyeti de sessizdir: motor atlanan satırları da üretip atmak
zorundadır, dolayısıyla büyük atlama değerleri, sayfa numarası büyüdükçe yavaşlayan
listeler üretir.

#### Özet

- Sıralama verinin değil sorgunun özelliğidir; `ORDER BY` yazılmadan satır sırası
  tanımsızdır.
- Boş değerlerin sıralamadaki yeri motora göre değişir; `NULLS FIRST` ve `NULLS LAST`
  belirteçleri yeri sabitler, ancak desteği de motora bağlıdır.
- Metin sırası harmanlamaya bağlıdır; öntanımlı harmanlama Türkçe alfabe sırasını vermeyebilir.
- Çok anahtarlı sıralamada her anahtarın yönü ayrı yazılır ve anahtarlar sırayla uygulanır.
- Eşit anahtarlı satırların sırası tanımsızdır; belirlenimci sonuç için sıralamaya
  benzersiz bir sütun eklenir.
- Sınırlamanın standart yazımı ile yaygın kısayolu farklıdır; sıralamasız sınırlama
  anlamsız, atlamaya dayalı sayfalama ise veri değişiminde kaygandır.

#### Sonraki Adım

Son üç derste boş değer üç ayrı yerde karşımıza çıktı: hesaplanmış ifadeyi boşalttı,
koşulun değillemesini eksik bıraktı, sıralamada motora bağlı bir yer aldı. Bunlar tek bir
kuralın sonuçlarıdır. Sonraki ders o kuralı — bilinmeyen değerin üç değerli mantığını —
kurar, boş değerle çalışan işlevleri tanıtır ve liste koşulunun değillemesinde ortaya çıkan
tuzağı ölçer.

### Boş Değerlerle Çalışma

Source: https://academia.sh/tr/kurslar/sql-temelleri/bos-degerlerle-calisma

Üç değerli mantığın sorgudaki sonuçları, boş değer sınamaları, boş değer yerine geçen işlevler, koşullu ifade ile boşluk doldurma ve liste koşulunun değillemesindeki tuzak.

Önceki üç ders boş değerle üç kez karşılaştı: hesaplanmış bir ifadeyi boşalttı, bir koşulun
değillemesini eksik bıraktı, sıralamada motora bağlı bir yer aldı. Her seferinde açıklama
ertelendi. Bu ders ertelenen açıklamayı yapar.

Boş değer (null) bir değer değil, **değerin yokluğunu** bildiren bir işarettir. Sıfır değildir,
boş dize değildir; "bu satır için bu bilgi yok" demektir. İlişkisel Kuram kursunda boş
değerin şemadaki anlamı tanımlanmıştı. Burada sorgudaki davranışı kurulur ve o davranış tek
bir kaynaktan çıkar: bilinmeyen bir değerle yapılan karşılaştırmanın sonucu da bilinmezdir.

Aşağıdaki blok şemayı ve örnek veriyi kurar; dersin bütün sorguları bu blokta oluşturulan
`kutuphane.db` dosyası üzerinde çalışır.

```sh
rm -f kutuphane.db
sqlite3 kutuphane.db <<'SQL'
CREATE TABLE sube (sube_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, sehir TEXT NOT NULL);
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL, yazar TEXT NOT NULL,
                    basim_yili INTEGER, sube_id INTEGER REFERENCES sube(sube_id));
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, soyad TEXT NOT NULL,
                  eposta TEXT, kayit_tarihi TEXT NOT NULL);
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL REFERENCES kitap(kitap_id),
                    uye_id INTEGER NOT NULL REFERENCES uye(uye_id), alis_tarihi TEXT NOT NULL,
                    iade_tarihi TEXT);
INSERT INTO sube VALUES (1,'Merkez','Ankara'),(2,'Bahçelievler','Ankara'),
  (3,'Kadıköy','İstanbul'),(4,'Konak','İzmir');
INSERT INTO kitap VALUES (1,'Körlük','José Saramago',1995,1),(2,'Tutunamayanlar','Oğuz Atay',1972,1),
  (3,'Kum Kitabı','Jorge Luis Borges',1975,2),(4,'Yaban','Yakup Kadri',1932,2),
  (5,'Sessiz Ev','Orhan Pamuk',1983,3),(6,'Anayurt Oteli','Yusuf Atılgan',NULL,3),
  (7,'Tehlikeli Oyunlar','Oğuz Atay',1973,NULL);
INSERT INTO uye VALUES (1,'Ayşe','Demir','ayse@ornek.test','2023-02-14'),
  (2,'Mehmet','Kaya','mehmet@ornek.test','2023-05-30'),(3,'Zeynep','Arslan',NULL,'2024-01-09'),
  (4,'Emre','Yıldız','emre@ornek.test','2024-03-22'),(5,'Selin','Aydın',NULL,'2024-11-05'),
  (6,'Burak','Şahin','burak@ornek.test','2025-01-18');
INSERT INTO odunc VALUES (1,1,1,'2025-01-10','2025-01-24'),(2,2,1,'2025-02-02','2025-02-20'),
  (3,1,2,'2025-02-11',NULL),(4,3,3,'2025-03-01','2025-03-15'),(5,4,3,'2025-03-18','2025-04-02'),
  (6,1,4,'2025-04-05','2025-04-19'),(7,5,4,'2025-04-21',NULL),(8,2,5,'2025-05-02','2025-05-30'),
  (9,7,1,'2025-05-14','2025-05-28'),(10,3,5,'2025-06-03',NULL),(11,6,2,'2025-06-11','2025-06-25'),
  (12,4,4,'2025-06-20','2025-07-04');
SQL
```

#### Karşılaştırmanın Sonucu

Boş değerin eşitlik sınamasındaki davranışı doğrudan ölçülebilir.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT NULL = NULL AS esit, NULL <> NULL AS farkli, NULL IS NULL AS bos_mu;
SQL
```

```
esit  farkli  bos_mu
----  ------  ------
              1     
```

İlk iki sütun boş; üçüncüsü `1` yani doğru. Bir boş değer başka bir boş değere ne eşittir
ne de eşit değildir. İki bilinmeyen sayının birbirine eşit olup olmadığı bilinemez, çünkü
her ikisi de bilinmiyor. Eşitlik işleci bu durumda ne doğru ne yanlış üretir; üçüncü bir
mantıksal değer üretir: **bilinmeyen** (unknown).

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT 1 = 1 AS dogru, 1 = 2 AS yanlis, 1 = NULL AS bilinmiyor;
SQL
```

```
dogru  yanlis  bilinmiyor
-----  ------  ----------
1      0                 
```

Üç sütun mantığın üç değerini gösteriyor: doğru `1`, yanlış `0`, bilinmeyen ise boş.
`WHERE` yan tümcesinin yalnız doğruyu geçirdiği kuralı burada bir sonuca bağlanır: boş
değer içeren bir karşılaştırma hiçbir satırı geçirmez — ne koşulda, ne değillemesinde.

Boş değeri sınamanın tek doğru yolu `IS NULL` ve `IS NOT NULL` yazımlarıdır. Bunlar
karşılaştırma işleci değil, doğrudan boşluk sınayan yüklemlerdir ve her zaman doğru ya da
yanlış üretirler.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT baslik, basim_yili FROM kitap WHERE basim_yili IS NULL;
SQL
```

```
baslik         basim_yili
-------------  ----------
Anayurt Oteli            
```

#### Üç Değerli Mantık

Mantıksal bağlaçlar da üç değerle çalışır. Kural, bilinmeyeni "belki doğru belki yanlış"
diye okumaktır: sonuç her iki olasılıkta da aynıysa belirlenir, farklıysa bilinmeyen kalır.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT 1 AND NULL AS d_ve_b, 0 AND NULL AS y_ve_b, 1 OR NULL AS d_veya_b,
       0 OR NULL AS y_veya_b, NOT NULL AS degil_b;
SQL
```

```
d_ve_b  y_ve_b  d_veya_b  y_veya_b  degil_b
------  ------  --------  --------  -------
        0       1                          
```

İki sütun belirlendi. Yanlış ile bilinmeyenin `AND` bağlacı yanlıştır: bilinmeyen ne olursa
olsun sonuç yanlış. Doğru ile bilinmeyenin `OR` bağlacı doğrudur: bir taraf zaten doğru.
Kalan üçü bilinmeyen kaldı. Değilleme bilinmeyeni değiştirmez — bilinmeyenin tersi de
bilinmeyendir.

Bu tablo, Programlama Temelleri kursundaki kısa devre değerlendirme kuralının üç değerli
karşılığıdır: bir bağlacın sonucu bir taraftan belirlenebiliyorsa diğer taraf sonucu
etkilemez.

#### Boş Değer Yerine Değer Koymak

Sonuç kümesinde boş yerine anlamlı bir değer göstermek için standart işlev `COALESCE`
kullanılır. Argümanlarını soldan sağa değerlendirir ve boş olmayan ilkini döndürür.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT ad, soyad, COALESCE(eposta,'(adres yok)') AS iletisim FROM uye;
SQL
```

```
ad      soyad   iletisim         
------  ------  -----------------
Ayşe    Demir   ayse@ornek.test  
Mehmet  Kaya    mehmet@ornek.test
Zeynep  Arslan  (adres yok)      
Emre    Yıldız  emre@ornek.test  
Selin   Aydın   (adres yok)      
Burak   Şahin   burak@ornek.test 
```

`COALESCE` ikiden çok argüman alabilir; öncelikli kaynaklardan ilk dolu olanı seçmek için
kullanılır. Birçok motor iki argümanlı bir kısaltma da sunar, ancak o kısaltmanın adı
**motora göre değişir**; taşınabilir yazım `COALESCE` yazımıdır.

Ters yönde çalışan işlev `NULLIF` iki argümanı karşılaştırır ve eşitlerse boş değer,
değilse birinci argümanı döndürür.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT ad, NULLIF(sehir,'Ankara') AS ankara_disi FROM sube ORDER BY sube_id;
SQL
```

```
ad            ankara_disi
------------  -----------
Merkez                   
Bahçelievler             
Kadıköy       İstanbul   
Konak         İzmir      
```

`NULLIF` en çok, anlamsız bir yer tutucuyu — sıfır, boş dize, `-1` — gerçek boş değere
çevirmek için işe yarar. Sıfıra bölmeyi engellemek de yaygın kullanımıdır: bölenin sıfır
olduğu durumda boş değer üretmek, hata almaktan daha yönetilebilirdir.

Boş değerin dönüştürülmesi koşullu ifadeyle de yazılabilir. `CASE` yapısı, bir sütunu
okunabilir bir duruma çevirmenin standart yoludur.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT odunc_id, alis_tarihi,
       CASE WHEN iade_tarihi IS NULL THEN 'dışarıda' ELSE 'iade edildi' END AS durum
FROM odunc WHERE odunc_id <= 4 ORDER BY odunc_id;
SQL
```

```
odunc_id  alis_tarihi  durum      
--------  -----------  -----------
1         2025-01-10   iade edildi
2         2025-02-02   iade edildi
3         2025-02-11   dışarıda   
4         2025-03-01   iade edildi
```

`CASE WHEN iade_tarihi = NULL` yazılsaydı hiçbir satır `dışarıda` olarak
etiketlenmezdi — koşul hiçbir zaman doğru olmazdı ve her satır `ELSE` dalına düşerdi. Bu,
boş değer hatalarının en sinsi biçimidir: sorgu hata vermez, yanlış cevap verir.

#### Liste Koşulunun Değillemesi

Üç değerli mantığın en pahalı sonucu, liste koşulunun değillemesinde ortaya çıkar.
Kitapların bulunduğu şube kimlikleri `1`, `2` ve `3`; yedinci kitabın şubesi ise henüz
belirlenmemiş, yani boş. Hangi şubede hiç kitap olmadığını soran koşul önce boş değer
olmadan yazılsın.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT ad FROM sube WHERE sube_id NOT IN (1,2,3);
SQL
```

```
ad   
-----
Konak
```

Doğru yanıt: `Konak` şubesinde hiç kitap yok. Aynı liste, kitap tablosundaki değerlerden
üretilseydi içinde bir de boş değer bulunurdu. Bu durumda ne olduğu ölçülebilir.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT ad FROM sube WHERE sube_id NOT IN (1,2,3,NULL);
SELECT 'dönen satır sayısı: ' || COUNT(*) AS olcum
FROM sube WHERE sube_id NOT IN (1,2,3,NULL);
SQL
```

```
olcum                
---------------------
dönen satır sayısı: 0
```

Birinci sorgu hiçbir satır yazdırmadı; ikincisi bunu sayarak doğruladı. Tek bir boş değer,
sorgunun **hiçbir satır döndürmemesine** yol açtı ve hata
verilmedi. Nedeni doğrudan üç değerli mantıktan gelir: `NOT IN` koşulu, "listedeki her
öğeye eşit değil" biçiminde açılır. `4 <> 1 AND 4 <> 2 AND 4 <> 3 AND 4 <> NULL` ifadesinin
son çarpanı bilinmeyendir; doğru ile bilinmeyenin `AND` bağlacı da bilinmeyendir. Koşul
hiçbir satır için doğru olmaz.

Aynı liste olumlu yönde kullanıldığında sorun görünmez.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT ad FROM sube WHERE sube_id IN (1,2,3,NULL);
SQL
```

```
ad          
------------
Merkez      
Bahçelievler
Kadıköy     
```

`IN` koşulu `OR` ile açıldığı için, bir taraf doğru olduğunda bilinmeyen sonucu etkilemez.
Tuzak yalnızca değillemededir ve bu asimetri, hatanın gözden kaçmasının başlıca nedenidir:
olumlu sorgu doğru çalıştığı için değillemesinin de doğru çalıştığı varsayılır.

Korunma yolu, listeyi üreten kaynakta boş değerleri elemek ya da koşulu boş değere
duyarsız bir yüklemle yazmaktır. Standartta bunun için `IS DISTINCT FROM` yüklemi vardır;
boş değerleri de karşılaştırılabilir sayar.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT 1 IS DISTINCT FROM NULL AS bir_ve_bos,
       NULL IS DISTINCT FROM NULL AS bos_ve_bos,
       NULL IS NOT DISTINCT FROM NULL AS bos_bosa_esit;
SQL
```

```
bir_ve_bos  bos_ve_bos  bos_bosa_esit
----------  ----------  -------------
1           0           1            
```

Üç sütun da doğru ya da yanlış üretti; hiçbiri bilinmeyen değil. `IS DISTINCT FROM`
yükleminin desteği **motora göre değişir**; bulunmayan motorlarda aynı etki, boşluk
sınamasını koşula açıkça eklemekle sağlanır.

Boş değerlerin toplama işlevlerindeki davranışı da ayrı bir kural izler ve ikinci konuda
ölçülecek: toplama işlevleri boş değerleri sayıma katmaz.

#### Özet

- Boş değer, değerin yokluğunu bildiren bir işarettir; sıfır ya da boş dize değildir.
- Boş değerle yapılan karşılaştırma ne doğru ne yanlış, bilinmeyen üretir; `WHERE` yalnız
  doğruyu geçirdiği için hem koşul hem değillemesi satırı eler.
- Boşluk yalnız `IS NULL` ve `IS NOT NULL` ile sınanır.
- `COALESCE` boş olmayan ilk argümanı, `NULLIF` eşitlik durumunda boş değer döndürür;
  `CASE` içinde boşluk sınaması eşitlikle değil `IS NULL` ile yazılır.
- `NOT IN` listesinde tek bir boş değer, sorgunun hata vermeden sıfır satır döndürmesine
  yol açar; aynı liste `IN` ile kullanıldığında sorun görünmez.
- `IS DISTINCT FROM` boş değere duyarsız karşılaştırma sağlar, desteği motora bağlıdır.

#### Sonraki Adım

Bu derste `COALESCE`, `NULLIF` ve `CASE` yapıları boş değer bağlamında kullanıldı; oysa
üçü de daha geniş bir ailenin üyesidir. Sorgu, sütun değerlerini olduğu gibi göstermek
zorunda değil — metni kesebilir, sayıyı yuvarlayabilir, tarihten yıl çıkarabilir. Sonraki
ders dizgi, sayı ve tarih işlevlerini ele alır ve tarih işlevlerinin adlandırmasının neden
standardın en zayıf uygulandığı alan olduğunu gösterir.

### Yerleşik İşlevler

Source: https://academia.sh/tr/kurslar/sql-temelleri/yerlesik-islevler

Satır başına çalışan dizgi, sayı ve tarih işlevleri; karakter ile bayt uzunluğu ayrımı, tam sayı bölmesi ve tarih işlevlerinin standarda en uzak alan olması.

Önceki ders boş değeri dönüştüren üç yapıyı kullandı: `COALESCE`, `NULLIF` ve `CASE`.
Üçü de tek bir satırın değerlerini alıp tek bir değer üreten **skaler işlevlerdir**
(scalar function) — sonuç kümesinin satır sayısını değiştirmezler, yalnız sütun değerlerini
dönüştürürler.

Bu ders aynı ailenin geri kalanını kurar: metin, sayı ve tarih üzerinde çalışan yerleşik
işlevler. Ailenin öğretici tarafı yalnız ne yaptıkları değil, adlandırmalarının ne kadar
değişken olduğudur. Yerleşik işlevler, standart SQL ile motor lehçesinin en görünür
biçimde ayrıldığı alandır.

Aşağıdaki blok şemayı ve örnek veriyi kurar; dersin bütün sorguları bu blokta oluşturulan
`kutuphane.db` dosyası üzerinde çalışır.

```sh
rm -f kutuphane.db
sqlite3 kutuphane.db <<'SQL'
CREATE TABLE sube (sube_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, sehir TEXT NOT NULL);
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL, yazar TEXT NOT NULL,
                    basim_yili INTEGER, sube_id INTEGER REFERENCES sube(sube_id));
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, soyad TEXT NOT NULL,
                  eposta TEXT, kayit_tarihi TEXT NOT NULL);
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL REFERENCES kitap(kitap_id),
                    uye_id INTEGER NOT NULL REFERENCES uye(uye_id), alis_tarihi TEXT NOT NULL,
                    iade_tarihi TEXT);
INSERT INTO sube VALUES (1,'Merkez','Ankara'),(2,'Bahçelievler','Ankara'),
  (3,'Kadıköy','İstanbul'),(4,'Konak','İzmir');
INSERT INTO kitap VALUES (1,'Körlük','José Saramago',1995,1),(2,'Tutunamayanlar','Oğuz Atay',1972,1),
  (3,'Kum Kitabı','Jorge Luis Borges',1975,2),(4,'Yaban','Yakup Kadri',1932,2),
  (5,'Sessiz Ev','Orhan Pamuk',1983,3),(6,'Anayurt Oteli','Yusuf Atılgan',NULL,3),
  (7,'Tehlikeli Oyunlar','Oğuz Atay',1973,NULL);
INSERT INTO uye VALUES (1,'Ayşe','Demir','ayse@ornek.test','2023-02-14'),
  (2,'Mehmet','Kaya','mehmet@ornek.test','2023-05-30'),(3,'Zeynep','Arslan',NULL,'2024-01-09'),
  (4,'Emre','Yıldız','emre@ornek.test','2024-03-22'),(5,'Selin','Aydın',NULL,'2024-11-05'),
  (6,'Burak','Şahin','burak@ornek.test','2025-01-18');
INSERT INTO odunc VALUES (1,1,1,'2025-01-10','2025-01-24'),(2,2,1,'2025-02-02','2025-02-20'),
  (3,1,2,'2025-02-11',NULL),(4,3,3,'2025-03-01','2025-03-15'),(5,4,3,'2025-03-18','2025-04-02'),
  (6,1,4,'2025-04-05','2025-04-19'),(7,5,4,'2025-04-21',NULL),(8,2,5,'2025-05-02','2025-05-30'),
  (9,7,1,'2025-05-14','2025-05-28'),(10,3,5,'2025-06-03',NULL),(11,6,2,'2025-06-11','2025-06-25'),
  (12,4,4,'2025-06-20','2025-07-04');
SQL
```

#### Uzunluk: Karakter mi, Bayt mı

Metnin uzunluğunu veren işlev, ölçtüğü şey konusunda net olmalıdır. Bilgisayarlar Nasıl
Çalışır kursunda kurulan ayrım burada doğrudan görünür: bir karakter birden çok bayt
kaplayabilir.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT LENGTH('Körlük') AS karakter, LENGTH(CAST('Körlük' AS BLOB)) AS bayt;
SQL
```

```
karakter  bayt
--------  ----
6         8   
```

Altı karakterlik sözcük sekiz bayt tutuyor: `ö` ve `ü` harflerinin her biri iki bayt.
Uzunluk işlevinin karakter mi bayt mı saydığı **motora göre değişir**; bazı motorlarda ayrı
adlarla iki işlev bulunur, bazılarında tipin metin mi ikili veri mi olduğuna bakılır.
Alan sınırı denetiminde bu ayrım doğrudan hataya dönüşür — karakterle ölçüp baytla saklayan
bir denetim, Türkçe metinde beklenenden erken taşar.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT baslik, LENGTH(baslik) AS uzunluk FROM kitap ORDER BY uzunluk DESC, baslik;
SQL
```

```
baslik             uzunluk
-----------------  -------
Tehlikeli Oyunlar  17     
Tutunamayanlar     14     
Anayurt Oteli      13     
Kum Kitabı         10     
Sessiz Ev          9      
Körlük             6      
Yaban              5      
```

#### Kesme, Arama, Değiştirme

Metnin bir parçasını almak için parça alma işlevi kullanılır. Standart yazım ile yaygın
yazım burada ayrılır.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT SUBSTRING('2024-01-09' FROM 1 FOR 4) AS yil;
SQL
```

```
Parse error near line 3: near "FROM": syntax error
  SELECT SUBSTRING('2024-01-09' FROM 1 FOR 4) AS yil;
                  error here ---^
```

Standarttaki `SUBSTRING(… FROM … FOR …)` yazımı bu motorda tanınmıyor. Motorun kabul
ettiği yazım, argümanları virgülle ayıran biçimdir.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT ad, SUBSTR(kayit_tarihi,1,4) AS kayit_yili FROM uye ORDER BY uye_id;
SQL
```

```
ad      kayit_yili
------  ----------
Ayşe    2023      
Mehmet  2023      
Zeynep  2024      
Emre    2024      
Selin   2024      
Burak   2025      
```

Bir alt dizginin konumunu bulan işlevin adı da **motora göre değişir**. Standarttaki
`POSITION(… IN …)` yazımı yerine bu motorda ayrı adlı bir işlev bulunuyor.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT eposta, INSTR(eposta,'@') AS konum FROM uye WHERE eposta IS NOT NULL ORDER BY uye_id;
SQL
```

```
eposta             konum
-----------------  -----
ayse@ornek.test    5    
mehmet@ornek.test  7    
emre@ornek.test    5    
burak@ornek.test   6    
```

Konum `1`'den başlıyor. SQL'de dizgi konumları bir tabanlıdır; Programlama Temelleri
kursundaki sıfır tabanlı dizin alışkanlığı burada geçerli değildir ve bir kaydırma hatasının
sık kaynağıdır.

Değiştirme ve kırpma işlevleri daha az değişkendir.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT baslik, REPLACE(baslik,'Kitabı','Kitap') AS degistirilmis FROM kitap
WHERE baslik LIKE '%Kitab%';
SELECT '[' || TRIM('   Körlük   ') || ']' AS kirpilmis;
SQL
```

```
baslik      degistirilmis
----------  -------------
Kum Kitabı  Kum Kitap    
kirpilmis
---------
[Körlük] 
```

Kırpma işlevi öntanımlı olarak iki uçtaki boşlukları alır; hangi karakterlerin
kırpılacağını belirten ve yalnız bir uçtan kırpan biçimleri de vardır.

#### Sayı İşlevleri ve Bölme

Sayısal işlevlerin çoğu tek biçimlidir, ama bölme davranışı ayrık durur.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT ABS(-7) AS mutlak, ROUND(2.555, 2) AS yuvarlak,
       17 % 5 AS kalan, 17 / 5 AS bolum, 17.0 / 5 AS ondalik;
SQL
```

```
mutlak  yuvarlak  kalan  bolum  ondalik
------  --------  -----  -----  -------
7       2.56      2      3      3.4    
```

`17 / 5` işlemi `3` verdi, `17.0 / 5` işlemi `3.4`. Bölmenin tam sayı mı ondalık mı
olduğunu belirleyen şey, işlenenlerin tipidir. Bu davranış **motora göre değişir**: bazı
motorlar iki tam sayının bölümünü de ondalık üretir. Taşınabilir yazım, bölmeden önce en az
bir işleneni ondalık tipe çevirmektir.

Kalan işleci de iki yazımla bulunabilir ve yazımlar aynı tipi üretmeyebilir.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT 17 % 5 AS islecle, MOD(17,5) AS islevle;
SQL
```

```
islecle  islevle
-------  -------
2        2.0    
```

Aynı sayı, biri tam sayı diğeri ondalık olarak döndü. Sonuç bir karşılaştırmaya ya da
gruplamaya girecekse bu fark önemlidir.

Yuvarlamada da dikkat edilecek bir nokta var: ondalık sayılar ikilik gösterimde tam
karşılanamaz — Bilgisayarlar Nasıl Çalışır kursundaki kayan noktalı sayı dersinin sonucu
burada da geçerlidir. Parasal hesaplarda yuvarlama işlevine değil, ondalık tipe güvenilir.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT baslik, basim_yili, basim_yili % 10 AS son_hane FROM kitap
WHERE basim_yili IS NOT NULL ORDER BY kitap_id;
SQL
```

```
baslik             basim_yili  son_hane
-----------------  ----------  --------
Körlük             1995        5       
Tutunamayanlar     1972        2       
Kum Kitabı         1975        5       
Yaban              1932        2       
Sessiz Ev          1983        3       
Tehlikeli Oyunlar  1973        3       
```

#### Tarih İşlevleri

Tarih, standardın en zayıf uygulandığı alandır. Standartta tarihten parça çıkarmak için bir
yükleme benzeri yazım, tarihe süre eklemek için ise bir aralık tipi tanımlıdır. İkisi de
her motorda bulunmaz.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT EXTRACT(YEAR FROM alis_tarihi) AS yil FROM odunc;
SQL
```

```
Parse error near line 3: near "FROM": syntax error
  SELECT EXTRACT(YEAR FROM alis_tarihi) AS yil FROM odunc;
                      ^--- error here
```

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT alis_tarihi + INTERVAL '14' DAY AS son_gun FROM odunc;
SQL
```

```
Parse error near line 3: near "DAY": syntax error
  SELECT alis_tarihi + INTERVAL '14' DAY AS son_gun FROM odunc;
                       error here ---^
```

İki standart yazım da bu motorda çalışmadı. Motorun sunduğu karşılıklar başka adlar taşıyor.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT odunc_id, alis_tarihi,
       strftime('%Y', alis_tarihi) AS yil,
       date(alis_tarihi,'+14 days')  AS son_gun
FROM odunc WHERE odunc_id <= 4 ORDER BY odunc_id;
SQL
```

```
odunc_id  alis_tarihi  yil   son_gun   
--------  -----------  ----  ----------
1         2025-01-10   2025  2025-01-24
2         2025-02-02   2025  2025-02-16
3         2025-02-11   2025  2025-02-25
4         2025-03-01   2025  2025-03-15
```

İki tarih arasındaki farkı gün cinsinden almak da motora özgü bir yazım gerektirir.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT odunc_id, alis_tarihi, iade_tarihi,
       CAST(julianday(iade_tarihi) - julianday(alis_tarihi) AS INTEGER) AS gun
FROM odunc WHERE iade_tarihi IS NOT NULL AND odunc_id <= 5 ORDER BY odunc_id;
SQL
```

```
odunc_id  alis_tarihi  iade_tarihi  gun
--------  -----------  -----------  ---
1         2025-01-10   2025-01-24   14 
2         2025-02-02   2025-02-20   18 
4         2025-03-01   2025-03-15   14 
5         2025-03-18   2025-04-02   15 
```

Üçüncü ödünç kaydı listede yok: iade tarihi boş olduğu için fark hesaplanamaz ve koşul
onu eledi. Boş değer kuralı burada da işliyor.

Tarih işlevlerinin motora bağlılığı pratik bir sonuç doğurur: tarih hesabı sorgunun içine
gömüldüğünde sorgu taşınamaz hale gelir. Taşınabilirlik gerekiyorsa tarih aritmetiği ya
uygulama katmanına alınır ya da motora özgü kısmı tek bir yerde toplanır. Ayrıca tarih
sütununun saat dilimi taşıyıp taşımadığı da **motora ve tipe göre değişir**; saat dilimsiz
bir sütunda yapılan gün hesabı, saat dilimi sınırında bir gün kayabilir.

#### Özet

- Skaler işlevler satır başına çalışır ve sonuç kümesinin satır sayısını değiştirmez.
- Uzunluk işlevinin karakter mi bayt mı saydığı motora bağlıdır; ASCII dışı metinde ikisi
  ayrışır.
- Dizgi konumları bir tabanlıdır; parça alma ve konum bulma işlevlerinin standart yazımı
  her motorda bulunmaz.
- Bölmenin tam sayı mı ondalık mı olacağını işlenenlerin tipi belirler ve bu davranış
  motora göre değişir.
- Tarih işlevleri standarttan en çok ayrılan alandır; parça çıkarma ve süre ekleme
  yazımları motordan motora değişir.
- Tarih hesabında boş değer kuralı sürer: bir ucu boş olan fark hesaplanamaz.

#### Sonraki Adım

Buraya kadar bütün sorgular tek bir tablodan okudu. Oysa normalleştirilmiş bir şemada
anlamlı sorular tek tabloya sığmaz: "hangi üye hangi kitabı ödünç aldı" sorusunun yanıtı üç
tabloya dağılmıştır, çünkü ödünç tablosunda yalnız kimlikler var. Sonraki konu tabloları
birleştiren yan tümceyi kurar ve ilk dersi eşleşen satırların kesişimini — iç birleştirmeyi
— ele alır.

## Birleştirmeler ve Toplama

İç ve dış birleştirme, çapraz ve kendi kendine birleştirme, toplama işlevleri, gruplama ile grup düzeyi süzme ve küme işlemleri.

### İç Birleştirme

Source: https://academia.sh/tr/kurslar/sql-temelleri/ic-birlestirme

Normalleştirilmiş şemada dağılmış bilginin tek sonuçta toplanması, birleştirme koşulu, tablo takma adları, eşleşmeyen satırların düşmesi ve satır sayısının çoğalması.

Önceki konu tek tablodan okuyan sorguları kurdu: sütun seçimi, koşul, sıralama, boş değer
davranışı, işlevler. Bu araçlarla yanıtlanamayan bir soru kaldı. Ödünç tablosuna
bakıldığında hangi üyenin hangi kitabı aldığı görünmez.

Aşağıdaki blok şemayı ve örnek veriyi kurar; dersin bütün sorguları bu blokta oluşturulan
`kutuphane.db` dosyası üzerinde çalışır.

```sh
rm -f kutuphane.db
sqlite3 kutuphane.db <<'SQL'
CREATE TABLE sube (sube_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, sehir TEXT NOT NULL);
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL, yazar TEXT NOT NULL,
                    basim_yili INTEGER, sube_id INTEGER REFERENCES sube(sube_id));
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, soyad TEXT NOT NULL,
                  eposta TEXT, kayit_tarihi TEXT NOT NULL);
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL REFERENCES kitap(kitap_id),
                    uye_id INTEGER NOT NULL REFERENCES uye(uye_id), alis_tarihi TEXT NOT NULL,
                    iade_tarihi TEXT);
INSERT INTO sube VALUES (1,'Merkez','Ankara'),(2,'Bahçelievler','Ankara'),
  (3,'Kadıköy','İstanbul'),(4,'Konak','İzmir');
INSERT INTO kitap VALUES (1,'Körlük','José Saramago',1995,1),(2,'Tutunamayanlar','Oğuz Atay',1972,1),
  (3,'Kum Kitabı','Jorge Luis Borges',1975,2),(4,'Yaban','Yakup Kadri',1932,2),
  (5,'Sessiz Ev','Orhan Pamuk',1983,3),(6,'Anayurt Oteli','Yusuf Atılgan',NULL,3),
  (7,'Tehlikeli Oyunlar','Oğuz Atay',1973,NULL);
INSERT INTO uye VALUES (1,'Ayşe','Demir','ayse@ornek.test','2023-02-14'),
  (2,'Mehmet','Kaya','mehmet@ornek.test','2023-05-30'),(3,'Zeynep','Arslan',NULL,'2024-01-09'),
  (4,'Emre','Yıldız','emre@ornek.test','2024-03-22'),(5,'Selin','Aydın',NULL,'2024-11-05'),
  (6,'Burak','Şahin','burak@ornek.test','2025-01-18');
INSERT INTO odunc VALUES (1,1,1,'2025-01-10','2025-01-24'),(2,2,1,'2025-02-02','2025-02-20'),
  (3,1,2,'2025-02-11',NULL),(4,3,3,'2025-03-01','2025-03-15'),(5,4,3,'2025-03-18','2025-04-02'),
  (6,1,4,'2025-04-05','2025-04-19'),(7,5,4,'2025-04-21',NULL),(8,2,5,'2025-05-02','2025-05-30'),
  (9,7,1,'2025-05-14','2025-05-28'),(10,3,5,'2025-06-03',NULL),(11,6,2,'2025-06-11','2025-06-25'),
  (12,4,4,'2025-06-20','2025-07-04');
SQL
```

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT odunc_id, kitap_id, uye_id, alis_tarihi FROM odunc WHERE odunc_id <= 4 ORDER BY odunc_id;
SQL
```

```
odunc_id  kitap_id  uye_id  alis_tarihi
--------  --------  ------  -----------
1         1         1       2025-01-10 
2         2         1       2025-02-02 
3         1         2       2025-02-11 
4         3         3       2025-03-01 
```

Görünen tek şey kimlikler. Bu bir eksiklik değil, normalleştirmenin doğrudan sonucudur:
üyenin adı üye tablosunda bir kez yazılır, ödünç kaydında yalnız ona başvuran yabancı
anahtar bulunur. Bilgi bilerek dağıtılmıştır; sorgunun işi onu geçici olarak toplamaktır.
Toplama işlemine **birleştirme** (join) denir.

#### Birleştirme Koşulu

Birleştirme, iki tablonun satırlarını bir koşula göre eşleştirir. Yazımı `JOIN` ve `ON`
yan tümceleriyle kurulur.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT o.odunc_id, u.ad, u.soyad, o.alis_tarihi
FROM odunc AS o
JOIN uye AS u ON o.uye_id = u.uye_id
WHERE o.odunc_id <= 4
ORDER BY o.odunc_id;
SQL
```

```
odunc_id  ad      soyad   alis_tarihi
--------  ------  ------  -----------
1         Ayşe    Demir   2025-01-10 
2         Ayşe    Demir   2025-02-02 
3         Mehmet  Kaya    2025-02-11 
4         Zeynep  Arslan  2025-03-01 
```

Kimlikler adlara dönüştü. `ON` yan tümcesindeki koşul, ödünç kaydındaki yabancı anahtarı
üye tablosundaki birincil anahtarla eşliyor. Bu, birleştirmenin en yaygın biçimidir:
eşitlik koşuluyla yazılan ve **iç birleştirme** (inner join) adını taşıyan biçim. `JOIN`
sözcüğü tek başına yazıldığında iç birleştirme anlaşılır; `INNER JOIN` yazımı da aynı
şeydir.

`AS o` ve `AS u` yazımları tablolara **takma ad** verir. İki tabloda aynı adlı sütunlar
bulunduğunda — burada iki tabloda da `uye_id` var — hangi tablonun sütunundan söz edildiğini
takma ad belirler. Tek harflik takma adlar yaygındır ama tablo sayısı arttıkça
okunabilirliği düşürür; anlamlı kısaltmalar daha iyidir.

#### Birden Çok Tablo

Birleştirme zincirlenebilir. Ödünç kaydı hem üyeye hem kitaba başvurduğu için, iki
birleştirme sorunun tamamını yanıtlar.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT o.odunc_id, u.ad || ' ' || u.soyad AS uye, k.baslik, o.alis_tarihi
FROM odunc AS o
JOIN uye AS u   ON o.uye_id = u.uye_id
JOIN kitap AS k ON o.kitap_id = k.kitap_id
ORDER BY o.odunc_id;
SQL
```

```
odunc_id  uye            baslik             alis_tarihi
--------  -------------  -----------------  -----------
1         Ayşe Demir     Körlük             2025-01-10 
2         Ayşe Demir     Tutunamayanlar     2025-02-02 
3         Mehmet Kaya    Körlük             2025-02-11 
4         Zeynep Arslan  Kum Kitabı         2025-03-01 
5         Zeynep Arslan  Yaban              2025-03-18 
6         Emre Yıldız    Körlük             2025-04-05 
7         Emre Yıldız    Sessiz Ev          2025-04-21 
8         Selin Aydın    Tutunamayanlar     2025-05-02 
9         Ayşe Demir     Tehlikeli Oyunlar  2025-05-14 
10        Selin Aydın    Kum Kitabı         2025-06-03 
11        Mehmet Kaya    Anayurt Oteli      2025-06-11 
12        Emre Yıldız    Yaban              2025-06-20 
```

Ödünç tablosundaki on iki kaydın hepsi döndü. Her birleştirme sonucu yeni bir bağıntıdır ve
bir sonraki birleştirmenin girdisi olur; sıra, yazıldığı sıradır ama motorun uyguladığı
sıra farklı olabilir — bildirimsel dilin kuralı burada da geçerlidir.

#### Eşleşmeyen Satırlar Düşer

İç birleştirmenin tanımlayıcı özelliği, koşulu sağlamayan satırların sonuçta hiç
görünmemesidir. Kitapları şubeleriyle birleştiren sorgu bunu gösteriyor.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT k.baslik, s.ad AS sube
FROM kitap AS k
JOIN sube AS s ON k.sube_id = s.sube_id
ORDER BY k.kitap_id;
SQL
```

```
baslik          sube        
--------------  ------------
Körlük          Merkez      
Tutunamayanlar  Merkez      
Kum Kitabı      Bahçelievler
Yaban           Bahçelievler
Sessiz Ev       Kadıköy     
Anayurt Oteli   Kadıköy     
```

Altı satır döndü, oysa kitap tablosunda yedi kitap var. Eksik olan `Tehlikeli Oyunlar`:
şube kimliği boş olduğu için `k.sube_id = s.sube_id` koşulu hiçbir şube satırı için doğru
olmuyor. Beşinci dersteki kural burada da işliyor — boş değerle yapılan eşitlik ne doğru ne
yanlış üretir ve birleştirme de yalnız doğruyu geçirir.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT COUNT(*) AS kitap FROM kitap;
SELECT COUNT(*) AS birlestirme_sonucu FROM kitap AS k JOIN sube AS s ON k.sube_id = s.sube_id;
SQL
```

```
kitap
-----
7    
birlestirme_sonucu
------------------
6                 
```

Kaybın sessizliği önemlidir: sorgu hata vermez, bir eksik döndürür. Rapor sayıları
tutmadığında ilk bakılacak yerlerden biri, iç birleştirmenin elediği satırlardır.
Eşleşmeyen satırların da korunması gerekiyorsa dış birleştirme yazılır; sonraki dersin
konusu budur.

#### Satır Sayısı Artabilir de

Birleştirme yalnız satır düşürmez, çoğaltabilir de. Bir tablodaki bir satır, diğer tabloda
birden çok satırla eşleşiyorsa sonuçta o kadar kez görünür.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT k.baslik, o.odunc_id, o.alis_tarihi
FROM kitap AS k
JOIN odunc AS o ON k.kitap_id = o.kitap_id
WHERE k.baslik = 'Körlük'
ORDER BY o.odunc_id;
SQL
```

```
baslik  odunc_id  alis_tarihi
------  --------  -----------
Körlük  1         2025-01-10 
Körlük  3         2025-02-11 
Körlük  6         2025-04-05 
```

Kitap tablosunda tek olan satır, ödünç kayıtlarıyla birleştiğinde üç kez göründü. Bu
doğrudur — üç ayrı ödünç işlemi var — ama sayım yapılırken tuzağa dönüşür: bu sonuç
üzerinden kitap sayılırsa `Körlük` üç kez sayılır. Toplama işlevleri konusunda bu ayrım
tekrar ele alınacak.

Genel kural: bire-çok bir ilişkide birleştirme, "bir" tarafındaki satırları "çok"
tarafındaki eşleşme sayısı kadar tekrarlar.

#### Kısaltılmış Yazımlar

Birleştirmenin daha kısa iki yazımı vardır ve ikisi de dikkat ister.

Birincisi, birleştirme koşulunu iki tabloda aynı adı taşıyan sütuna dayandıran `USING`
yan tümcesidir.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT baslik, ad AS sube FROM kitap JOIN sube USING (sube_id) ORDER BY kitap_id;
SQL
```

```
baslik          sube        
--------------  ------------
Körlük          Merkez      
Tutunamayanlar  Merkez      
Kum Kitabı      Bahçelievler
Yaban           Bahçelievler
Sessiz Ev       Kadıköy     
Anayurt Oteli   Kadıköy     
```

`USING (sube_id)` yazımı `ON k.sube_id = s.sube_id` ile aynı sonucu verir ve ortak sütunu
sonuçta bir kez gösterir. Adlandırma tutarlıysa yararlıdır; sütun adları farklıysa
kullanılamaz.

İkincisi, birleştirme koşulunu hiç yazmayan **doğal birleştirmedir** (natural join): iki
tabloda aynı adı taşıyan bütün sütunlar birleştirme koşulu sayılır. Kısalığı, sessiz
yanlışlık pahasına gelir.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT 'uye NATURAL JOIN sube satır sayısı: ' || COUNT(*) AS olcum FROM uye NATURAL JOIN sube;
SQL
```

```
olcum                                
-------------------------------------
uye NATURAL JOIN sube satır sayısı: 0
```

Üye ile şube tablosu arasında hiçbir anlamlı ilişki yok, ama ikisinde de `ad` adlı bir sütun
var. Doğal birleştirme bunu bir eşleştirme ölçütü sanıp üye adı ile şube adını karşılaştırdı
ve sıfır satır döndürdü. Hata iletisi yok; yalnız boş bir sonuç.

Asıl tehlike, şemaya sonradan eklenen bir sütunun adının bir başka tabloyla çakışmasıdır: o
gün doğru çalışan doğal birleştirme, ertesi gün sessizce başka bir şey hesaplamaya başlar.
Birleştirme koşulunu açıkça yazmak, bu sınıf hatayı tümüyle ortadan kaldırır.

Eski bir yazım daha vardır: tabloları virgülle ayırıp koşulu `WHERE` içine koymak.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT k.baslik, s.ad AS sube FROM kitap AS k, sube AS s
WHERE k.sube_id = s.sube_id ORDER BY k.kitap_id;
SQL
```

```
baslik          sube        
--------------  ------------
Körlük          Merkez      
Tutunamayanlar  Merkez      
Kum Kitabı      Bahçelievler
Yaban           Bahçelievler
Sessiz Ev       Kadıköy     
Anayurt Oteli   Kadıköy     
```

Sonuç aynı, ama iki kusuru var. Birleştirme koşulu ile süzme koşulu aynı yan tümcede
karışır; ve koşul unutulduğunda sorgu hata vermeden bütün eşleşme çiftlerini üretir. `ON`
yazımı koşulu birleştirmenin yanında tutarak her ikisini de önler.

#### Özet

- Normalleştirilmiş şemada bilgi bilerek dağıtılır; birleştirme onu sorgu süresince
  geçici olarak toplar.
- İç birleştirme yalnız koşulu doğru olan satır çiftlerini döndürür; eşleşmeyen satır
  hata vermeden düşer.
- Boş değer taşıyan yabancı anahtar hiçbir satırla eşleşmez ve iç birleştirmede kaybolur.
- Bire-çok ilişkide birleştirme, "bir" tarafındaki satırı eşleşme sayısı kadar tekrarlar.
- Tablo takma adları aynı adlı sütunları ayırt eder ve çok tablolu sorguları okunabilir
  kılar.
- `USING` ortak adlı sütunda kısalık sağlar; doğal birleştirme koşulu ad benzerliğine
  bıraktığı için sessiz yanlışlığa açıktır.

#### Sonraki Adım

İç birleştirmenin eşleşmeyen satırı düşürmesi, kimi sorularda tam olarak istenmeyen şeydir:
"hiç kitabı olmayan şube" ya da "hiç ödünç almamış üye" gibi sorular, tam da eşleşmesi
olmayan satırları arar. Sonraki ders bu satırları koruyan dış birleştirmeleri ele alır,
korunan satırların boş değerle nasıl dolduğunu gösterir ve sağ ile tam dış birleştirmenin
motor desteğine bağlı olduğunu ölçer.

### Dış Birleştirmeler

Source: https://academia.sh/tr/kurslar/sql-temelleri/dis-birlestirmeler

Eşleşmesi olmayan satırların korunması, korunan satırların boş değerle dolması, eşleşmeyeni bulan kalıp, sağ ve tam dış birleştirmenin motor desteği ve koşulun yerinin sonucu değiştirmesi.

Önceki ders iç birleştirmenin bir sınırını gösterdi: koşulu sağlamayan satır sonuçta hiç
görünmez. Kitaplar şubelerle birleştirildiğinde şubesi belirlenmemiş kitap düştü, sonuç
yedi yerine altı satır oldu.

Kimi sorular tam da o düşen satırları arar. "Hangi şubede hiç kitap yok", "hangi üye hiç
ödünç almamış", "hangi kitap hiç kimse tarafından alınmamış" — üçü de eşleşmesi olmayan
satırları soruyor. İç birleştirme bunları yanıtlayamaz, çünkü aradığı şeyi zaten eliyor.
Bu ders eşleşmesi olmayan satırları koruyan birleştirme türünü kurar.

Aşağıdaki blok şemayı ve örnek veriyi kurar; dersin bütün sorguları bu blokta oluşturulan
`kutuphane.db` dosyası üzerinde çalışır.

```sh
rm -f kutuphane.db
sqlite3 kutuphane.db <<'SQL'
CREATE TABLE sube (sube_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, sehir TEXT NOT NULL);
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL, yazar TEXT NOT NULL,
                    basim_yili INTEGER, sube_id INTEGER REFERENCES sube(sube_id));
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, soyad TEXT NOT NULL,
                  eposta TEXT, kayit_tarihi TEXT NOT NULL);
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL REFERENCES kitap(kitap_id),
                    uye_id INTEGER NOT NULL REFERENCES uye(uye_id), alis_tarihi TEXT NOT NULL,
                    iade_tarihi TEXT);
INSERT INTO sube VALUES (1,'Merkez','Ankara'),(2,'Bahçelievler','Ankara'),
  (3,'Kadıköy','İstanbul'),(4,'Konak','İzmir');
INSERT INTO kitap VALUES (1,'Körlük','José Saramago',1995,1),(2,'Tutunamayanlar','Oğuz Atay',1972,1),
  (3,'Kum Kitabı','Jorge Luis Borges',1975,2),(4,'Yaban','Yakup Kadri',1932,2),
  (5,'Sessiz Ev','Orhan Pamuk',1983,3),(6,'Anayurt Oteli','Yusuf Atılgan',NULL,3),
  (7,'Tehlikeli Oyunlar','Oğuz Atay',1973,NULL);
INSERT INTO uye VALUES (1,'Ayşe','Demir','ayse@ornek.test','2023-02-14'),
  (2,'Mehmet','Kaya','mehmet@ornek.test','2023-05-30'),(3,'Zeynep','Arslan',NULL,'2024-01-09'),
  (4,'Emre','Yıldız','emre@ornek.test','2024-03-22'),(5,'Selin','Aydın',NULL,'2024-11-05'),
  (6,'Burak','Şahin','burak@ornek.test','2025-01-18');
INSERT INTO odunc VALUES (1,1,1,'2025-01-10','2025-01-24'),(2,2,1,'2025-02-02','2025-02-20'),
  (3,1,2,'2025-02-11',NULL),(4,3,3,'2025-03-01','2025-03-15'),(5,4,3,'2025-03-18','2025-04-02'),
  (6,1,4,'2025-04-05','2025-04-19'),(7,5,4,'2025-04-21',NULL),(8,2,5,'2025-05-02','2025-05-30'),
  (9,7,1,'2025-05-14','2025-05-28'),(10,3,5,'2025-06-03',NULL),(11,6,2,'2025-06-11','2025-06-25'),
  (12,4,4,'2025-06-20','2025-07-04');
SQL
```

#### Sol Dış Birleştirme

**Sol dış birleştirme** (left outer join), soldaki tablonun bütün satırlarını korur.
Sağdaki tabloda eşleşme bulunmayan satırlar için sağ tablonun sütunları boş değerle
doldurulur.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT k.baslik, s.ad AS sube
FROM kitap AS k
LEFT JOIN sube AS s ON k.sube_id = s.sube_id
ORDER BY k.kitap_id;
SQL
```

```
baslik             sube        
-----------------  ------------
Körlük             Merkez      
Tutunamayanlar     Merkez      
Kum Kitabı         Bahçelievler
Yaban              Bahçelievler
Sessiz Ev          Kadıköy     
Anayurt Oteli      Kadıköy     
Tehlikeli Oyunlar              
```

Yedi kitabın hepsi döndü. `Tehlikeli Oyunlar` satırında şube adı boş: kitap korundu, ama
eşleşme olmadığı için sağ tablodan gelen sütun dolduramadı. `OUTER` sözcüğü isteğe
bağlıdır; `LEFT JOIN` ile `LEFT OUTER JOIN` aynı şeydir.

Boş değerin kaynağı burada önemlidir. Şube adı sütunu `NOT NULL` tanımlı olduğu için şube
tablosunda hiç boş ad yok. Sonuçtaki boşluk veriden gelmiyor, **birleştirmenin kendisi
üretti**. Dış birleştirme sonucundaki boş değer iki farklı şeyi gösterebilir: ya kaynak
satırda gerçekten boş bir değer vardı, ya da hiç eşleşme yoktu. Ayrım gerektiğinde
eşleşmenin varlığı, sağ tablonun birincil anahtarı sınanarak anlaşılır.

Yön değiştirildiğinde korunan taraf da değişir.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT s.ad AS sube, k.baslik
FROM sube AS s
LEFT JOIN kitap AS k ON k.sube_id = s.sube_id
ORDER BY s.sube_id, k.kitap_id;
SQL
```

```
sube          baslik        
------------  --------------
Merkez        Körlük        
Merkez        Tutunamayanlar
Bahçelievler  Kum Kitabı    
Bahçelievler  Yaban         
Kadıköy       Sessiz Ev     
Kadıköy       Anayurt Oteli 
Konak                       
```

Şimdi bütün şubeler görünüyor; hiç kitabı olmayan `Konak` da listede, kitap sütunu boş.
Aynı iki tablo, aynı koşul, farklı korunan taraf.

#### Eşleşmeyeni Bulmak

Dış birleştirmenin en çok kullanılan kalıbı, korunan ama eşleşmeyen satırları ayıklamaktır.
Eşleşme yoksa sağ tablonun bütün sütunları boş olacağına göre, sağ tablonun birincil
anahtarını boşluk için sınamak bu satırları seçer.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT s.ad AS kitapsiz_sube
FROM sube AS s
LEFT JOIN kitap AS k ON k.sube_id = s.sube_id
WHERE k.kitap_id IS NULL;
SQL
```

```
kitapsiz_sube
-------------
Konak        
```

Aynı kalıp üyelere uygulanır.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT u.ad, u.soyad
FROM uye AS u
LEFT JOIN odunc AS o ON o.uye_id = u.uye_id
WHERE o.odunc_id IS NULL;
SQL
```

```
ad     soyad
-----  -----
Burak  Şahin
```

Sınamanın **birincil anahtar** üzerinde yapılması gerektiğine dikkat edilmelidir. Boş değer
kabul eden bir sütun sınansaydı sonuç yanlış olurdu: o sütunun gerçekten boş olduğu eşleşen
satırlar da listeye karışırdı. Birincil anahtar hiçbir zaman boş olamayacağı için, boş
görünmesi tek bir anlama gelir — eşleşme yok.

#### Sağ ve Tam Dış Birleştirme

Sol dış birleştirmenin aynadaki karşılığı **sağ dış birleştirmedir** (right outer join):
sağdaki tablonun satırları korunur.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT s.ad AS sube, k.baslik
FROM kitap AS k
RIGHT JOIN sube AS s ON k.sube_id = s.sube_id
ORDER BY s.sube_id, k.kitap_id;
SQL
```

```
sube          baslik        
------------  --------------
Merkez        Körlük        
Merkez        Tutunamayanlar
Bahçelievler  Kum Kitabı    
Bahçelievler  Yaban         
Kadıköy       Sessiz Ev     
Kadıköy       Anayurt Oteli 
Konak                       
```

Sonuç, bir önceki bölümdeki `sube LEFT JOIN kitap` sorgusuyla aynı. Bu bir rastlantı
değil: **her sağ dış birleştirme, tablo sırası ters çevrilerek sol dış birleştirmeye
dönüştürülebilir**. İki yazım arasında anlam farkı yoktur.

Bu eşdeğerlik pratik bir değer taşır, çünkü sağ dış birleştirme desteği **motora göre
değişir** — bazı motorlar yalnız sol dış birleştirmeyi gerçekleştirir. Sorgu sol biçimde
yazıldığında taşınabilirlik sorunu ortadan kalkar. Okunabilirlik açısından da sol biçim
tercih edilir: korunan tablo `FROM` yan tümcesinde, yani okuma sırasında ilk görülen yerde
durur.

**Tam dış birleştirme** (full outer join) iki tarafın da eşleşmeyen satırlarını korur.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT s.ad AS sube, k.baslik
FROM kitap AS k
FULL OUTER JOIN sube AS s ON k.sube_id = s.sube_id
ORDER BY s.sube_id, k.kitap_id;
SQL
```

```
sube          baslik           
------------  -----------------
              Tehlikeli Oyunlar
Merkez        Körlük           
Merkez        Tutunamayanlar   
Bahçelievler  Kum Kitabı       
Bahçelievler  Yaban            
Kadıköy       Sessiz Ev        
Kadıköy       Anayurt Oteli    
Konak                          
```

Sekiz satır: altı eşleşme, şubesi olmayan bir kitap ve kitabı olmayan bir şube. İki
yönün eksikleri tek sonuçta toplandı.

Tam dış birleştirmenin desteği de **motora göre değişir** ve sağ dış birleştirmeden daha
seyrek bulunur. Bulunmadığı yerde eşdeğer yazım, sol dış birleştirme ile ters yönlü sol dış
birleştirmenin birleşimidir; küme işlemleri bu konunun son dersinde ele alınacak.

#### Koşulun Yeri Sonucu Değiştirir

Dış birleştirmede en sık yapılan yanlış, sağ tabloya ait bir koşulu `ON` yerine `WHERE`
içine yazmaktır. İç birleştirmede iki yazım aynı sonucu verir; dış birleştirmede vermez.

Önce koşul birleştirmenin içinde:

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT u.ad, u.soyad, o.odunc_id, o.alis_tarihi
FROM uye AS u
LEFT JOIN odunc AS o ON o.uye_id = u.uye_id AND o.alis_tarihi >= '2025-05-01'
ORDER BY u.uye_id, o.odunc_id;
SQL
```

```
ad      soyad   odunc_id  alis_tarihi
------  ------  --------  -----------
Ayşe    Demir   9         2025-05-14 
Mehmet  Kaya    11        2025-06-11 
Zeynep  Arslan                       
Emre    Yıldız  12        2025-06-20 
Selin   Aydın   8         2025-05-02 
Selin   Aydın   10        2025-06-03 
Burak   Şahin                        
```

Altı üyenin hepsi listede. Koşulu sağlayan ödünç kaydı olmayan iki üye korundu ve ödünç
sütunları boş kaldı.

Şimdi aynı koşul süzme yan tümcesinde:

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT u.ad, u.soyad, o.odunc_id, o.alis_tarihi
FROM uye AS u
LEFT JOIN odunc AS o ON o.uye_id = u.uye_id
WHERE o.alis_tarihi >= '2025-05-01'
ORDER BY u.uye_id, o.odunc_id;
SQL
```

```
ad      soyad   odunc_id  alis_tarihi
------  ------  --------  -----------
Ayşe    Demir   9         2025-05-14 
Mehmet  Kaya    11        2025-06-11 
Emre    Yıldız  12        2025-06-20 
Selin   Aydın   8         2025-05-02 
Selin   Aydın   10        2025-06-03 
```

İki üye kayboldu. Nedeni beşinci dersteki kuraldır: birleştirme korunan satırların ödünç
sütunlarını boş değerle doldurdu, `WHERE` yan tümcesi ise bu satırlarda
`NULL >= '2025-05-01'` karşılaştırmasını değerlendirdi ve sonuç bilinmeyen çıktı. `WHERE`
yalnız doğruyu geçirir, dolayısıyla korunan satırlar elendi.

Kural şöyle özetlenir: `ON` yan tümcesi **hangi satırların eşleştiğini**, `WHERE` yan
tümcesi **birleştirme bittikten sonra hangi satırların kalacağını** belirler. Sağ tabloya
ait bir koşul `WHERE` içine yazıldığında dış birleştirme sessizce iç birleştirmeye döner.
Bu dönüşümün tek istisnası, kasten yazılan boşluk sınamasıdır — eşleşmeyeni bulan kalıp tam
da bu etkiyi kullanır.

#### Özet

- Sol dış birleştirme soldaki tablonun bütün satırlarını korur ve eşleşme yoksa sağ
  tablonun sütunlarını boş değerle doldurur.
- Sonuçtaki boş değer ya kaynaktan gelir ya da birleştirmenin kendisi üretmiştir; ayrım
  sağ tablonun birincil anahtarı sınanarak yapılır.
- Eşleşmeyen satırları bulan kalıp, dış birleştirmeden sonra sağ tablonun birincil
  anahtarını boşluk için sınamaktır.
- Her sağ dış birleştirme, tablo sırası ters çevrilerek sol dış birleştirmeye dönüşür;
  sağ ve tam dış birleştirmenin desteği motora göre değişir.
- Tam dış birleştirme iki tarafın da eşleşmeyen satırlarını korur.
- Sağ tabloya ait koşul `WHERE` içine yazıldığında dış birleştirme sessizce iç
  birleştirmeye döner; koşul `ON` içinde yazılmalıdır.

#### Sonraki Adım

Buraya kadarki birleştirmelerin hepsi bir eşitlik koşuluna dayandı. Koşulun kendisi
kaldırılırsa ne olur, ya da bir tablo kendisiyle birleştirilirse? Sonraki ders bu iki özel
durumu ele alır: koşulsuz birleştirmenin ürettiği çarpım ve bir tablonun iki farklı takma
adla kendisine bağlanmasıyla kurulan karşılaştırmalar.

### Çapraz ve Kendi Kendine Birleştirme

Source: https://academia.sh/tr/kurslar/sql-temelleri/capraz-ve-kendi-kendine-birlestirme

Koşulsuz birleştirmenin ürettiği kartezyen çarpım, kazara çarpımın belirtileri, bir tablonun iki takma adla kendisine bağlanması ve yinelenen çiftlerin elenmesi.

Önceki iki ders birleştirmeyi bir eşitlik koşulu üzerinden kurdu: soldaki satırın yabancı
anahtarı, sağdaki satırın birincil anahtarına eşit. Koşul her zaman böyle olmak zorunda
değil — hatta koşulun hiç olmadığı bir birleştirme de tanımlıdır.

Bu ders iki özel duruma bakar. Birincisi koşulsuz birleştirme: her satırın her satırla
eşleştiği çarpım. İkincisi bir tablonun kendisiyle birleştirilmesi: aynı tablodan gelen iki
satırı karşılaştıran sorgular. İkisi de kuralın istisnası değil, aynı kuralın uç
durumlarıdır.

Aşağıdaki blok şemayı ve örnek veriyi kurar; dersin bütün sorguları bu blokta oluşturulan
`kutuphane.db` dosyası üzerinde çalışır.

```sh
rm -f kutuphane.db
sqlite3 kutuphane.db <<'SQL'
CREATE TABLE sube (sube_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, sehir TEXT NOT NULL);
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL, yazar TEXT NOT NULL,
                    basim_yili INTEGER, sube_id INTEGER REFERENCES sube(sube_id));
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, soyad TEXT NOT NULL,
                  eposta TEXT, kayit_tarihi TEXT NOT NULL);
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL REFERENCES kitap(kitap_id),
                    uye_id INTEGER NOT NULL REFERENCES uye(uye_id), alis_tarihi TEXT NOT NULL,
                    iade_tarihi TEXT);
INSERT INTO sube VALUES (1,'Merkez','Ankara'),(2,'Bahçelievler','Ankara'),
  (3,'Kadıköy','İstanbul'),(4,'Konak','İzmir');
INSERT INTO kitap VALUES (1,'Körlük','José Saramago',1995,1),(2,'Tutunamayanlar','Oğuz Atay',1972,1),
  (3,'Kum Kitabı','Jorge Luis Borges',1975,2),(4,'Yaban','Yakup Kadri',1932,2),
  (5,'Sessiz Ev','Orhan Pamuk',1983,3),(6,'Anayurt Oteli','Yusuf Atılgan',NULL,3),
  (7,'Tehlikeli Oyunlar','Oğuz Atay',1973,NULL);
INSERT INTO uye VALUES (1,'Ayşe','Demir','ayse@ornek.test','2023-02-14'),
  (2,'Mehmet','Kaya','mehmet@ornek.test','2023-05-30'),(3,'Zeynep','Arslan',NULL,'2024-01-09'),
  (4,'Emre','Yıldız','emre@ornek.test','2024-03-22'),(5,'Selin','Aydın',NULL,'2024-11-05'),
  (6,'Burak','Şahin','burak@ornek.test','2025-01-18');
INSERT INTO odunc VALUES (1,1,1,'2025-01-10','2025-01-24'),(2,2,1,'2025-02-02','2025-02-20'),
  (3,1,2,'2025-02-11',NULL),(4,3,3,'2025-03-01','2025-03-15'),(5,4,3,'2025-03-18','2025-04-02'),
  (6,1,4,'2025-04-05','2025-04-19'),(7,5,4,'2025-04-21',NULL),(8,2,5,'2025-05-02','2025-05-30'),
  (9,7,1,'2025-05-14','2025-05-28'),(10,3,5,'2025-06-03',NULL),(11,6,2,'2025-06-11','2025-06-25'),
  (12,4,4,'2025-06-20','2025-07-04');
SQL
```

#### Kartezyen Çarpım

**Çapraz birleştirme** (cross join) hiçbir koşul almaz: soldaki her satır, sağdaki her
satırla eşleştirilir. Sonucun satır sayısı iki tablonun satır sayılarının çarpımıdır.
Kümeler kuramındaki karşılığı **kartezyen çarpımdır** (Cartesian product).

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT COUNT(*) AS kitap_satiri FROM kitap;
SELECT COUNT(*) AS sube_satiri FROM sube;
SELECT COUNT(*) AS carpim FROM kitap CROSS JOIN sube;
SQL
```

```
kitap_satiri
------------
7           
sube_satiri
-----------
4          
carpim
------
28    
```

Yedi çarpı dört, yirmi sekiz. Çarpımın kendisi küçük bir örnekte zararsız görünür; on bin
satırlı iki tabloda yüz milyon satır demektir. Bu, sorgunun yanlış yazılmasıyla ortaya
çıkabilecek en pahalı sonuçtur ve İleri SQL kursunda sorgu planı okunurken ilk aranan
belirtilerden biridir.

Sonucu görünür bir boyutta tutmak için sağ taraf süzülebilir.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT s.ad AS sube, k.baslik
FROM sube AS s
CROSS JOIN kitap AS k
WHERE k.yazar = 'Oğuz Atay'
ORDER BY s.sube_id, k.kitap_id;
SQL
```

```
sube          baslik           
------------  -----------------
Merkez        Tutunamayanlar   
Merkez        Tehlikeli Oyunlar
Bahçelievler  Tutunamayanlar   
Bahçelievler  Tehlikeli Oyunlar
Kadıköy       Tutunamayanlar   
Kadıköy       Tehlikeli Oyunlar
Konak         Tutunamayanlar   
Konak         Tehlikeli Oyunlar
```

Dört şube, iki kitap, sekiz satır. Hiçbir satır "bu kitap bu şubede" demiyor — çapraz
birleştirme bir olgu değil, bir **olasılık listesi** üretir. Kullanışlı olduğu yer de
budur: her şube ile her kitabın olası her bileşimini kurup, gerçekleşenleri dış
birleştirmeyle üzerine bindirmek, boş hücreleri de görünür kılan tam bir rapor ızgarası
verir.

#### Kazara Çarpım

Çapraz birleştirme çoğunlukla kasten yazılmaz; birleştirme koşulu unutulduğunda ortaya
çıkar. Virgülle ayrılmış eski yazımda koşul `WHERE` yan tümcesine bırakıldığı için bu
unutma özellikle kolaydır.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT COUNT(*) AS kosulsuz FROM kitap, sube;
SQL
```

```
kosulsuz
--------
28      
```

Sonuç, açıkça yazılmış çapraz birleştirmeyle aynı: yirmi sekiz. Sorgu hata vermedi. Bir
önceki derste aynı iki tablonun koşullu birleştirmesi altı satır döndürüyordu.

Belirti tanınabilir: beklenenden çok daha fazla satır, tekrar eden değerler ve toplamların
katlanması. Önlem de yazımsaldır — `JOIN … ON` biçimi kullanıldığında koşul birleştirmenin
yanında durur ve unutulduğunda göze çarpar. `CROSS JOIN` yazımı da kasıtlı olduğunu
belgeler; sorguyu okuyan kişi çarpımın niyet olduğunu görür.

#### Kendi Kendine Birleştirme

Bir tablo kendisiyle de birleştirilebilir. Bunun için tabloya iki farklı takma ad verilir;
motor açısından bu, iki ayrı tablo gibidir. Yazımın adı **kendi kendine birleştirmedir**
(self join) ve ayrı bir birleştirme türü değildir — iç ya da dış birleştirmenin, iki
tarafında aynı tablonun bulunduğu bir örneğidir.

Aynı yazarın kitap çiftlerini bulan sorgu, önce koşulsuz haliyle yazılsın.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT a.baslik AS a_kitap, b.baslik AS b_kitap
FROM kitap AS a
JOIN kitap AS b ON a.yazar = b.yazar
ORDER BY a.kitap_id, b.kitap_id;
SQL
```

```
a_kitap            b_kitap          
-----------------  -----------------
Körlük             Körlük           
Tutunamayanlar     Tutunamayanlar   
Tutunamayanlar     Tehlikeli Oyunlar
Kum Kitabı         Kum Kitabı       
Yaban              Yaban            
Sessiz Ev          Sessiz Ev        
Anayurt Oteli      Anayurt Oteli    
Tehlikeli Oyunlar  Tutunamayanlar   
Tehlikeli Oyunlar  Tehlikeli Oyunlar
```

Dokuz satırın yalnız ikisi anlamlı, o ikisi de aynı çiftin iki yönü. Kendi kendine
birleştirmenin iki tipik kusuru burada görünüyor. Birincisi, her satır kendisiyle eşleşiyor
— yazarı kendine eşit olduğu için. İkincisi, gerçek çiftler iki kez, yönleri ters çevrilmiş
olarak çıkıyor.

İkisi de tek bir koşulla giderilir: eşleştirmeyi birincil anahtarın sırasına bağlamak.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT a.yazar, a.baslik AS ilk, b.baslik AS ikinci
FROM kitap AS a
JOIN kitap AS b ON a.yazar = b.yazar AND a.kitap_id < b.kitap_id
ORDER BY a.kitap_id;
SQL
```

```
yazar      ilk             ikinci           
---------  --------------  -----------------
Oğuz Atay  Tutunamayanlar  Tehlikeli Oyunlar
```

`a.kitap_id < b.kitap_id` koşulu iki işi birden yapıyor. Eşitlik dışlandığı için satır
kendisiyle eşleşemiyor; sıralama tek yönlü olduğu için her çift bir kez çıkıyor. Kesin
eşitsizlik yerine `<>` yazılsaydı yalnız birinci sorun çözülür, aynalı yinelemeler kalırdı.

Aynı kalıp başka bir soruya uygulanabilir: aynı şehirdeki şube çiftleri.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT a.sehir, a.ad AS sube_1, b.ad AS sube_2
FROM sube AS a
JOIN sube AS b ON a.sehir = b.sehir AND a.sube_id < b.sube_id;
SQL
```

```
sehir   sube_1  sube_2      
------  ------  ------------
Ankara  Merkez  Bahçelievler
```

#### Kendi Kendine Birleştirmeyi Zincirlemek

Kendi kendine birleştirme başka birleştirmelerle birlikte kullanılabilir. Aynı kitabı ödünç
almış üye çiftlerini bulmak için ödünç tablosu kendisiyle, sonuç ise üye ve kitap
tablolarıyla birleştirilir.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT k.baslik, u1.ad AS uye_1, u2.ad AS uye_2
FROM odunc AS o1
JOIN odunc AS o2 ON o1.kitap_id = o2.kitap_id AND o1.uye_id < o2.uye_id
JOIN uye AS u1   ON u1.uye_id = o1.uye_id
JOIN uye AS u2   ON u2.uye_id = o2.uye_id
JOIN kitap AS k  ON k.kitap_id = o1.kitap_id
ORDER BY k.kitap_id, u1.uye_id, u2.uye_id;
SQL
```

```
baslik          uye_1   uye_2 
--------------  ------  ------
Körlük          Ayşe    Mehmet
Körlük          Ayşe    Emre  
Körlük          Mehmet  Emre  
Tutunamayanlar  Ayşe    Selin 
Kum Kitabı      Zeynep  Selin 
Yaban           Zeynep  Emre  
```

Beş tablo başvurusu var ama tablo sayısı üç: ödünç tablosu iki kez, üye tablosu iki kez
geçiyor. Takma adlar burada isteğe bağlı değil, zorunludur — hangi `uye_id` sütunundan söz
edildiğini başka hiçbir şey belirleyemez.

`Körlük` üç satır üretti çünkü üç ayrı üye almış: üç öğeden seçilen ikili sayısı üçtür.
Kendi kendine birleştirmenin satır sayısı, gruptaki eleman sayısının karesiyle büyür;
büyük gruplarda bu, çapraz birleştirme kadar pahalı olabilir.

#### Özet

- Çapraz birleştirme koşulsuzdur; satır sayısı iki tablonun satır sayılarının çarpımıdır.
- Çarpım bir olgu listesi değil olasılık listesi üretir; rapor ızgarası kurmakta işe yarar.
- Birleştirme koşulu unutulduğunda kazara çarpım hata vermeden oluşur; belirtisi
  beklenenden çok satır ve katlanan toplamlardır.
- Kendi kendine birleştirme ayrı bir tür değildir; aynı tabloya iki takma adla başvurmaktır
  ve takma ad zorunludur.
- Koşulsuz kendi kendine birleştirme her satırı kendisiyle ve her çifti iki yönde
  eşleştirir; birincil anahtar üzerinde kesin eşitsizlik ikisini birden giderir.
- Çift üreten birleştirmelerin maliyeti grup büyüklüğünün karesiyle artar.

#### Sonraki Adım

Buraya kadarki bütün sorgular satır düzeyinde kaldı: her sonuç satırı, kaynak satırların
bir bileşimine karşılık geliyordu. Oysa "kaç kitap", "ortalama ödünç süresi", "en eski
basım" gibi sorular satırları değil, satır kümelerini özetler. Sonraki ders satır kümesini
tek değere indirgeyen toplama işlevlerini kurar ve bu işlevlerin boş değerleri neden
saymadığını ölçer.

### Toplama İşlevleri

Source: https://academia.sh/tr/kurslar/sql-temelleri/toplama-islevleri

Satır kümesini tek değere indirgeyen sayım, toplam, ortalama ve uç değer işlevleri; boş değerlerin atlanması, yıldızlı sayım ile sütunlu sayımın farkı ve boş kümenin sonucu.

Önceki üç ders satır düzeyinde kaldı: her sonuç satırı, kaynak satırların bir bileşimine
karşılık geliyordu. Sorgu ne kadar tablo birleştirirse birleştirsin, ürettiği şey yine
satırlardı.

"Kaç kitap var", "ortalama ödünç süresi ne", "en eski basım hangi yıl" gibi sorular başka
bir şey ister: bir satır kümesinin tek bir değerle özetlenmesini. Bunu yapan işlevlere
**toplama işlevi** (aggregate function) denir. Skaler işlevlerden farkları, girdilerinin
tek satır değil bir satır kümesi olmasıdır.

Aşağıdaki blok şemayı ve örnek veriyi kurar; dersin bütün sorguları bu blokta oluşturulan
`kutuphane.db` dosyası üzerinde çalışır.

```sh
rm -f kutuphane.db
sqlite3 kutuphane.db <<'SQL'
CREATE TABLE sube (sube_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, sehir TEXT NOT NULL);
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL, yazar TEXT NOT NULL,
                    basim_yili INTEGER, sube_id INTEGER REFERENCES sube(sube_id));
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, soyad TEXT NOT NULL,
                  eposta TEXT, kayit_tarihi TEXT NOT NULL);
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL REFERENCES kitap(kitap_id),
                    uye_id INTEGER NOT NULL REFERENCES uye(uye_id), alis_tarihi TEXT NOT NULL,
                    iade_tarihi TEXT);
INSERT INTO sube VALUES (1,'Merkez','Ankara'),(2,'Bahçelievler','Ankara'),
  (3,'Kadıköy','İstanbul'),(4,'Konak','İzmir');
INSERT INTO kitap VALUES (1,'Körlük','José Saramago',1995,1),(2,'Tutunamayanlar','Oğuz Atay',1972,1),
  (3,'Kum Kitabı','Jorge Luis Borges',1975,2),(4,'Yaban','Yakup Kadri',1932,2),
  (5,'Sessiz Ev','Orhan Pamuk',1983,3),(6,'Anayurt Oteli','Yusuf Atılgan',NULL,3),
  (7,'Tehlikeli Oyunlar','Oğuz Atay',1973,NULL);
INSERT INTO uye VALUES (1,'Ayşe','Demir','ayse@ornek.test','2023-02-14'),
  (2,'Mehmet','Kaya','mehmet@ornek.test','2023-05-30'),(3,'Zeynep','Arslan',NULL,'2024-01-09'),
  (4,'Emre','Yıldız','emre@ornek.test','2024-03-22'),(5,'Selin','Aydın',NULL,'2024-11-05'),
  (6,'Burak','Şahin','burak@ornek.test','2025-01-18');
INSERT INTO odunc VALUES (1,1,1,'2025-01-10','2025-01-24'),(2,2,1,'2025-02-02','2025-02-20'),
  (3,1,2,'2025-02-11',NULL),(4,3,3,'2025-03-01','2025-03-15'),(5,4,3,'2025-03-18','2025-04-02'),
  (6,1,4,'2025-04-05','2025-04-19'),(7,5,4,'2025-04-21',NULL),(8,2,5,'2025-05-02','2025-05-30'),
  (9,7,1,'2025-05-14','2025-05-28'),(10,3,5,'2025-06-03',NULL),(11,6,2,'2025-06-11','2025-06-25'),
  (12,4,4,'2025-06-20','2025-07-04');
SQL
```

#### Sayımın Üç Biçimi

Sayım işlevinin üç yazımı vardır ve üçü farklı şeyler sayar.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT COUNT(*) AS satir, COUNT(basim_yili) AS yili_bilinen,
       COUNT(DISTINCT yazar) AS ayri_yazar
FROM kitap;
SQL
```

```
satir  yili_bilinen  ayri_yazar
-----  ------------  ----------
7      6             6         
```

`COUNT(*)` **satırları** sayar; sütun değerlerine hiç bakmaz, yedi satır yedidir.
`COUNT(basim_yili)` o sütundaki **boş olmayan değerleri** sayar; basım yılı bilinmeyen
kitap sayıma girmedi, sonuç altı oldu. `COUNT(DISTINCT yazar)` ise boş olmayan **ayrı**
değerleri sayar; yedi kitabın altı yazarı var.

Bu ayrım toplama işlevlerinin genel kuralının ilk örneğidir: **toplama işlevleri boş
değerleri atlar**. Tek istisna `COUNT(*)`, çünkü bir sütuna değil satırın kendisine bakar.

Aynı ayrım ödünç tablosunda doğrudan bir soruya karşılık gelir.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT COUNT(*) AS kayit, COUNT(iade_tarihi) AS iade_edilen FROM odunc;
SQL
```

```
kayit  iade_edilen
-----  -----------
12     9          
```

On iki ödünç kaydının dokuzu iade edilmiş. İkinci sütun, iade tarihi boş olan üç kaydı
saymadı — hiçbir koşul yazılmadan, yalnız boş değer kuralının sonucu olarak.

#### Toplam, Ortalama ve Uç Değerler

Dört işlev daha aynı aileyi tamamlar.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT MIN(basim_yili) AS en_eski, MAX(basim_yili) AS en_yeni,
       SUM(basim_yili) AS toplam, AVG(basim_yili) AS ortalama
FROM kitap;
SQL
```

```
en_eski  en_yeni  toplam  ortalama        
-------  -------  ------  ----------------
1932     1995     11830   1971.66666666667
```

Dördü de boş değeri atladı. Toplam altı sayının toplamı, ortalama da altıya bölünmüş
hali. Ortalamanın kaça bölündüğü, boş değerlerin en sık gözden kaçan sonucudur.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT AVG(basim_yili) AS ortalama,
       SUM(basim_yili) * 1.0 / COUNT(*)          AS satira_bolunen,
       SUM(basim_yili) * 1.0 / COUNT(basim_yili) AS dolu_degere_bolunen
FROM kitap;
SQL
```

```
ortalama          satira_bolunen  dolu_degere_bolunen
----------------  --------------  -------------------
1971.66666666667  1690.0          1971.66666666667   
```

Ortada duran sayı yanlış. Toplam altı değerden geliyor, `COUNT(*)` ise yedi satır sayıyor;
bölme, olmayan bir kitabı sıfır basım yılıyla hesaba katmış gibi davranıyor. Ortalama
işlevi ile üçüncü sütun aynı sonucu veriyor, çünkü ikisi de yalnız dolu değerleri sayıyor.

Karar, boş değerin ne anlama geldiğine bağlıdır. "Bilinmiyor" ise ortalamanın dışında
tutulması doğrudur. "Sıfır" anlamına geliyorsa şema yanlıştır: sıfır bir değerdir ve
sütuna sıfır yazılmalıdır. İki durumu birbirine karıştırmak, veriden çıkan sayıları sessizce
bozar.

Uç değer işlevleri metin sütunlarında da çalışır ve sonuç harmanlamaya bağlıdır.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT MIN(baslik) AS ilk, MAX(baslik) AS son FROM kitap;
SQL
```

```
ilk            son  
-------------  -----
Anayurt Oteli  Yaban
```

Dördüncü derste görülen kural burada da geçerli: hangi metnin "en küçük" sayılacağını
karşılaştırma düzeni belirler ve öntanımlı düzen **motora göre değişir**.

#### Boş Kümenin Sonucu

Toplama işlevleri hiçbir satır bulamadığında ne döndürür? Sayım ile toplam bu soruya farklı
yanıt verir.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT COUNT(*) AS satir, SUM(basim_yili) AS toplam,
       COALESCE(SUM(basim_yili), 0) AS guvenli
FROM kitap WHERE yazar = 'Bilinmeyen Yazar';
SQL
```

```
satir  toplam  guvenli
-----  ------  -------
0              0      
```

Sorgu hiçbir satır bulmadı. Sayım `0` döndürdü — sıfır satır saymak tanımlı bir işlem.
Toplam ise boş döndürdü, `0` değil: hiçbir sayı yoksa toplam da tanımsızdır. Bu ayrım
kuramsal olarak tutarlıdır ama pratikte hataya yol açar, çünkü toplamı bir hesaba sokan
kod genellikle sayı bekler.

Ayrıca dikkat edilecek nokta şudur: toplama işlevi içeren ve gruplaması olmayan bir sorgu,
kaynak satır bulunmasa bile **her zaman bir satır döndürür**. Yukarıdaki çıktı boş değil;
içi boş bir satır. Gruplama eklendiğinde bu davranış değişir ve sonraki dersin konusu olur.

Korunma yolu, toplamı kullanan yerde boş değeri bir yer tutucuya çevirmektir — üçüncü
sütundaki yazım bunu yapıyor.

#### Toplama ile Sütunu Karıştırmak

Toplama işlevi bütün satır kümesini tek satıra indirger. Aynı sorguda gruplanmamış bir
sütun da istenirse mantıksal bir çelişki doğar: hangi satırın değeri gösterilecektir?

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT baslik, MAX(basim_yili) AS en_yeni FROM kitap;
SQL
```

```
baslik  en_yeni
------  -------
Körlük  1995   
```

Sorgu çalıştı ve tutarlı bir yanıt verdi: en yeni basımın başlığı gerçekten `Körlük`.
Ancak bu, **motora göre değişen** bir davranıştır. Standart, gruplanmamış bir sütunun
toplama işleviyle birlikte seçilmesine izin vermez; bazı motorlar hata verir, bazıları
rastgele bir satırın değerini döndürür, bu motor ise uç değer işlevleriyle birlikte
kullanıldığında eşleşen satırı seçer.

Kural olarak bu yazımdan kaçınılmalıdır. Uç değeri veren satırın tamamı isteniyorsa doğru
araç sıralama ile sınırlamadır ya da İleri SQL kursunda ele alınan pencere işlevleridir.

#### Birleştirme ile Birlikte Toplama

Toplama işlevleri birleştirilmiş bir sonuç üzerinde de çalışır. Ödünç süresi, iki tarih
sütununun farkı olduğu için hesaplanmış bir ifade üzerinden toplanır.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT COUNT(*) AS kayit,
       COUNT(iade_tarihi) AS iade_edilen,
       ROUND(AVG(julianday(iade_tarihi) - julianday(alis_tarihi)), 2) AS ortalama_gun
FROM odunc;
SQL
```

```
kayit  iade_edilen  ortalama_gun
-----  -----------  ------------
12     9            16.11       
```

Üç sütun aynı tabloyu farklı büyüklüklerle özetliyor: on iki kayıt var, dokuzu iade
edilmiş, iade edilenlerin ortalama süresi on altı günün biraz üzerinde. Ortalamanın paydası
yine dokuz; iade edilmemiş kayıtlar için fark hesaplanamadığı için boş değer üretildi ve
atlandı.

Birleştirmenin satır çoğaltma etkisi burada bir tuzağa dönüşebilir. Bir kitabı ödünç
kayıtlarıyla birleştirip kitap sayarsa, birden çok kez ödünç alınmış kitaplar birden çok
kez sayılır. Böyle bir sorguda `COUNT(DISTINCT kitap_id)` yazımı doğru sonucu verir.

#### Özet

- Toplama işlevleri bir satır kümesini tek değere indirger ve gruplamasız yazıldıklarında
  sonuç her zaman tek satırdır.
- `COUNT(*)` satırları sayar, `COUNT(sütun)` o sütundaki boş olmayan değerleri sayar,
  ayrı değer sayımı ise yinelenenleri bir kez sayar.
- Toplama işlevleri boş değerleri atlar; ortalamanın paydası satır sayısı değil dolu değer
  sayısıdır.
- Boş küme üzerinde sayım sıfır, toplam ise boş değer döndürür.
- Gruplanmamış bir sütunu toplama işleviyle birlikte seçmek standart değildir ve davranışı
  motora göre değişir.
- Birleştirme satırları çoğalttığında sayımlar şişer; ayrı değer sayımı bunu düzeltir.

#### Sonraki Adım

Buraya kadarki toplamalar bütün tabloyu tek bir sonuca indirgedi. Oysa asıl işe yarayan
soru genellikle "toplamda kaç" değil, "her şube için kaç", "her üye için kaç" biçimindedir.
Sonraki ders satırları kümelere ayırıp her küme için ayrı bir özet üreten gruplamayı kurar
ve grupları süzen yan tümcenin `WHERE` yan tümcesinden neden ayrı olduğunu aynı sorguda
ölçer.

### Gruplama ve Süzme

Source: https://academia.sh/tr/kurslar/sql-temelleri/gruplama-ve-suzme

Satırların kümelere ayrılması, grup başına özet üretimi, boş değerin kendi grubunu kurması, dış birleştirmede sayım tuzağı ve satır süzmesi ile grup süzmesinin aynı sorguda ölçülmesi.

Önceki ders toplama işlevlerini kurdu, ama hepsi bütün tabloyu tek bir sonuca indirgedi.
Pratikte istenen genellikle bu değildir: "toplam kaç kitap" sorusunun yanıtı bir kez
öğrenilir, "her yazarın kaç kitabı var" sorusu ise sürekli sorulur.

Bu ders satırları kümelere ayıran ve her küme için ayrı bir özet üreten yan tümceyi kurar.
Ardından, grupların kendisini süzen yan tümceyi tanıtır ve bunun neden satırları süzen yan
tümceden ayrı olduğunu aynı sorgu üzerinde ölçer.

Aşağıdaki blok şemayı ve örnek veriyi kurar; dersin bütün sorguları bu blokta oluşturulan
`kutuphane.db` dosyası üzerinde çalışır.

```sh
rm -f kutuphane.db
sqlite3 kutuphane.db <<'SQL'
CREATE TABLE sube (sube_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, sehir TEXT NOT NULL);
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL, yazar TEXT NOT NULL,
                    basim_yili INTEGER, sube_id INTEGER REFERENCES sube(sube_id));
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, soyad TEXT NOT NULL,
                  eposta TEXT, kayit_tarihi TEXT NOT NULL);
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL REFERENCES kitap(kitap_id),
                    uye_id INTEGER NOT NULL REFERENCES uye(uye_id), alis_tarihi TEXT NOT NULL,
                    iade_tarihi TEXT);
INSERT INTO sube VALUES (1,'Merkez','Ankara'),(2,'Bahçelievler','Ankara'),
  (3,'Kadıköy','İstanbul'),(4,'Konak','İzmir');
INSERT INTO kitap VALUES (1,'Körlük','José Saramago',1995,1),(2,'Tutunamayanlar','Oğuz Atay',1972,1),
  (3,'Kum Kitabı','Jorge Luis Borges',1975,2),(4,'Yaban','Yakup Kadri',1932,2),
  (5,'Sessiz Ev','Orhan Pamuk',1983,3),(6,'Anayurt Oteli','Yusuf Atılgan',NULL,3),
  (7,'Tehlikeli Oyunlar','Oğuz Atay',1973,NULL);
INSERT INTO uye VALUES (1,'Ayşe','Demir','ayse@ornek.test','2023-02-14'),
  (2,'Mehmet','Kaya','mehmet@ornek.test','2023-05-30'),(3,'Zeynep','Arslan',NULL,'2024-01-09'),
  (4,'Emre','Yıldız','emre@ornek.test','2024-03-22'),(5,'Selin','Aydın',NULL,'2024-11-05'),
  (6,'Burak','Şahin','burak@ornek.test','2025-01-18');
INSERT INTO odunc VALUES (1,1,1,'2025-01-10','2025-01-24'),(2,2,1,'2025-02-02','2025-02-20'),
  (3,1,2,'2025-02-11',NULL),(4,3,3,'2025-03-01','2025-03-15'),(5,4,3,'2025-03-18','2025-04-02'),
  (6,1,4,'2025-04-05','2025-04-19'),(7,5,4,'2025-04-21',NULL),(8,2,5,'2025-05-02','2025-05-30'),
  (9,7,1,'2025-05-14','2025-05-28'),(10,3,5,'2025-06-03',NULL),(11,6,2,'2025-06-11','2025-06-25'),
  (12,4,4,'2025-06-20','2025-07-04');
SQL
```

#### Satırları Gruplara Ayırmak

`GROUP BY` yan tümcesi, belirtilen sütunlarda aynı değeri taşıyan satırları bir grup
sayar ve her grup için tek bir sonuç satırı üretir.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT yazar, COUNT(*) AS kitap_sayisi
FROM kitap
GROUP BY yazar
ORDER BY kitap_sayisi DESC, yazar;
SQL
```

```
yazar              kitap_sayisi
-----------------  ------------
Oğuz Atay          2           
Jorge Luis Borges  1           
José Saramago      1           
Orhan Pamuk        1           
Yakup Kadri        1           
Yusuf Atılgan      1           
```

Yedi satır altı gruba ayrıldı ve her grup tek satır olarak döndü. Sıralama, toplama
işlevinin ürettiği sütuna göre yapılabiliyor — sıralama gruplamadan sonra değerlendirildiği
için bu geçerlidir.

Gruplamalı bir sorguda sütun listesine yazılabilecek şeyler kısıtlıdır: ya gruplama
sütunlarından biri, ya bir toplama işlevi. Başka bir sütun yazmak anlamsızdır, çünkü grup
içinde o sütunun tek bir değeri yoktur. Bu kuralın uygulanması **motora göre değişir** —
kimi motor hata verir, kimi rastgele bir satırın değerini döndürür.

#### Boş Değer Kendi Grubunu Kurar

Boş değerlerin gruplamadaki davranışı, koşullardaki davranışından ayrılır.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT sube_id, COUNT(*) AS kitap_sayisi FROM kitap GROUP BY sube_id ORDER BY sube_id;
SQL
```

```
sube_id  kitap_sayisi
-------  ------------
         1           
1        2           
2        2           
3        2           
```

Şube kimliği boş olan kitap yok sayılmadı; kendi başına bir grup oldu. Gruplama boş
değerleri **birbirine eşit sayar**, oysa eşitlik işleci saymaz. Bu tutarsızlık gibi
görünür ama gerekli bir seçimdir: aksi halde her boş değer ayrı bir grup olurdu ve gruplama
işe yaramazdı.

Gruplama anahtarı ifade de olabilir; on yıllık dilimlere göre dağılım bunu gösteriyor.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT (basim_yili / 10) * 10 AS on_yil, COUNT(*) AS adet
FROM kitap WHERE basim_yili IS NOT NULL
GROUP BY on_yil
ORDER BY on_yil;
SQL
```

```
on_yil  adet
------  ----
1930    1   
1970    3   
1980    1   
1990    1   
```

#### Dış Birleştirmede Sayım Tuzağı

Gruplama en çok dış birleştirmeyle birlikte kullanılır: her şube için kitap sayısı, kitabı
olmayan şubeler de dahil. Bu bileşimde sayım yazımı sonucu doğrudan belirler.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT s.ad AS sube, COUNT(k.kitap_id) AS kitap_sayisi, COUNT(*) AS satir_sayisi
FROM sube AS s
LEFT JOIN kitap AS k ON k.sube_id = s.sube_id
GROUP BY s.sube_id, s.ad
ORDER BY s.sube_id;
SQL
```

```
sube          kitap_sayisi  satir_sayisi
------------  ------------  ------------
Merkez        2             2           
Bahçelievler  2             2           
Kadıköy       2             2           
Konak         0             1           
```

Son satırda iki sütun ayrışıyor. `Konak` şubesinin hiç kitabı yok, ama dış birleştirme onu
korumak için kitap sütunları boş olan bir satır üretti. `COUNT(*)` o satırı saydı ve `1`
verdi — yanlış. `COUNT(k.kitap_id)` ise boş değeri atladığı için `0` verdi — doğru.

Kural: dış birleştirmeden sonra sayım yapılırken **korunan tarafın değil, eşleşen tarafın**
bir sütunu sayılır ve o sütun boş değer kabul etmeyen bir sütun, tercihen birincil anahtar
olmalıdır. Bu, önceki dersteki "toplama işlevleri boş değeri atlar" kuralının doğrudan
uygulanmasıdır.

Gruplama anahtarı birden çok sütundan oluşabilir; grup, sütunların bileşimine göre kurulur.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT s.sehir, s.ad AS sube, COUNT(k.kitap_id) AS kitap_sayisi
FROM sube AS s
LEFT JOIN kitap AS k ON k.sube_id = s.sube_id
GROUP BY s.sehir, s.ad
ORDER BY s.sehir, s.ad;
SQL
```

```
sehir     sube          kitap_sayisi
--------  ------------  ------------
Ankara    Bahçelievler  2           
Ankara    Merkez        2           
İstanbul  Kadıköy       2           
İzmir     Konak         0           
```

Şehir ile şube adının her bileşimi ayrı bir grup oldu. Ankara'nın iki şubesi tek satırda
toplanmadı, çünkü ikinci anahtar onları ayırıyor.

#### Satır Süzmesi ile Grup Süzmesi

`WHERE` yan tümcesi satırları, `HAVING` yan tümcesi grupları süzer. Aralarındaki fark
değerlendirme sırasından gelir: `WHERE` gruplama **öncesinde**, `HAVING` gruplama
**sonrasında** çalışır. Sıra şöyledir — `FROM`, `WHERE`, `GROUP BY`, `HAVING`, `SELECT`,
`ORDER BY`.

Sonucu ölçmek için aynı soruyu iki kez sormak yeter. Önce süzmesiz hali: en az iki kez
ödünç almış üyeler.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT u.ad, u.soyad, COUNT(*) AS odunc_sayisi
FROM odunc AS o
JOIN uye AS u ON u.uye_id = o.uye_id
GROUP BY u.uye_id, u.ad, u.soyad
HAVING COUNT(*) >= 2
ORDER BY odunc_sayisi DESC, u.uye_id;
SQL
```

```
ad      soyad   odunc_sayisi
------  ------  ------------
Ayşe    Demir   3           
Emre    Yıldız  3           
Mehmet  Kaya    2           
Zeynep  Arslan  2           
Selin   Aydın   2           
```

`HAVING COUNT(*) >= 2` koşulu gruplar üzerinde çalıştı; iki kayıttan az olan gruplar
elendi. Şimdi aynı sorguya bir satır süzmesi eklensin: yalnız iade edilmiş kayıtlar
sayılsın.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT u.ad, u.soyad, COUNT(*) AS odunc_sayisi
FROM odunc AS o
JOIN uye AS u ON u.uye_id = o.uye_id
WHERE o.iade_tarihi IS NOT NULL
GROUP BY u.uye_id, u.ad, u.soyad
HAVING COUNT(*) >= 2
ORDER BY odunc_sayisi DESC, u.uye_id;
SQL
```

```
ad      soyad   odunc_sayisi
------  ------  ------------
Ayşe    Demir   3           
Zeynep  Arslan  2           
Emre    Yıldız  2           
```

Beş satır üçe indi ve sayılar değişti. `WHERE` yan tümcesi iade edilmemiş üç kaydı gruplama
başlamadan eledi; gruplar bu eksik veriyle kuruldu ve `HAVING` koşulu yeni sayılar üzerinde
uygulandı. `Emre` üçten ikiye düştü, `Mehmet` ile `Selin` eşiğin altına inip listeden çıktı.

Ayrım şöyle özetlenir: `WHERE` "hangi satırlar sayılacak", `HAVING` "hangi gruplar
gösterilecek" sorusunu yanıtlar. İkisi yer değiştiremez. Toplama işlevi içeren bir koşul
`WHERE` içine yazılamaz, çünkü o aşamada henüz grup yoktur.

Tersi de geçerli olmalıdır: gruplanmamış bir sütuna ait koşul `HAVING` içine
yazılmamalıdır. Standart buna izin vermez, ancak uygulanışı **motora göre değişir**.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT u.ad, COUNT(*) AS adet
FROM odunc AS o
JOIN uye AS u ON u.uye_id = o.uye_id
GROUP BY u.uye_id, u.ad
HAVING o.iade_tarihi IS NOT NULL;
SQL
```

```
ad      adet
------  ----
Ayşe    3   
Zeynep  2   
Emre    3   
Selin   2   
```

Sorgu hata vermedi, ama sonuç hiçbir soruya karşılık gelmiyor: sayılar süzmesiz sorgudaki
gibi, satır kümesi ise ikisinin arasında — süzmesiz sorgudan yalnız `Mehmet` düşmüş,
süzmeli sorgunun elediği `Selin` ise kalmış. Grup içindeki tek bir satırın iade tarihi
değerlendirilmiş ve grubun tamamının kaderi ona bağlanmış. Bu tür sonuçlar hata iletisi
üretmeden yanlıştır; grup düzeyi olmayan her koşul `WHERE` yan tümcesine yazılmalıdır.

`HAVING` yan tümcesinin `SELECT` listesinde bulunmayan bir toplama işlevine başvurması ise
geçerlidir — grup süzmesi, gösterilen sütunlarla sınırlı değildir.

#### Özet

- `GROUP BY` satırları anahtar değerine göre kümelere ayırır ve her küme için tek satır
  üretir.
- Gruplamalı bir sorgunun sütun listesinde yalnız gruplama sütunları ve toplama işlevleri
  bulunmalıdır.
- Gruplama boş değerleri birbirine eşit sayar; boş değer kendi grubunu kurar.
- Dış birleştirmeden sonra sayım, eşleşen tarafın boş değer kabul etmeyen bir sütunu
  üzerinden yapılmalıdır; `COUNT(*)` korunan satırı da sayar.
- `WHERE` gruplamadan önce satırları, `HAVING` gruplamadan sonra grupları süzer; sıra
  `FROM`, `WHERE`, `GROUP BY`, `HAVING`, `SELECT`, `ORDER BY` biçimindedir.
- Grup düzeyi olmayan koşulu `HAVING` içine yazmak, kimi motorda hata vermeden yanlış
  sonuç üretir.

#### Sonraki Adım

Gruplama, tek bir sorgunun ürettiği satırları düzenledi. Kimi soru ise iki ayrı sorgunun
sonuçlarını karşılaştırmayı ister: iki listede ortak olanlar, birinde olup diğerinde
olmayanlar, ikisinin toplamı. Sonraki ders sonuç kümelerini küme olarak birleştiren
işlemleri kurar ve yinelenen satırların korunup korunmamasının neden açık bir seçim
olduğunu gösterir.

### Küme İşlemleri

Source: https://academia.sh/tr/kurslar/sql-temelleri/kume-islemleri

İki sonuç kümesini birleşim, kesişim ve fark olarak birleştirmek; yinelenen satırların korunup korunmaması, boş değerin küme işlemlerindeki eşitliği ve işleç önceliğinin motora bağlılığı.

Önceki ders gruplamayı kurdu: tek bir sorgunun ürettiği satırlar kümelere ayrıldı ve her
küme özetlendi. Birleştirme ile gruplama, hep tek bir sorgu içinde çalıştı.

Kimi soru ise iki ayrı sorgunun sonuçlarını karşılaştırmayı ister. "Bu iki listede ortak
olanlar", "birincide olup ikincide olmayanlar", "iki listenin toplamı" — üçü de küme
kuramının işlemleridir ve SQL'de doğrudan karşılıkları vardır. Bu ders o işlemleri kurar.

Ayrımı baştan koymak gerekir: birleştirme satırları **yan yana** ekler, sütun sayısını
artırır. Küme işlemleri satırları **alt alta** ekler, sütun sayısını değiştirmez.

Aşağıdaki blok şemayı ve örnek veriyi kurar; dersin bütün sorguları bu blokta oluşturulan
`kutuphane.db` dosyası üzerinde çalışır.

```sh
rm -f kutuphane.db
sqlite3 kutuphane.db <<'SQL'
CREATE TABLE sube (sube_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, sehir TEXT NOT NULL);
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL, yazar TEXT NOT NULL,
                    basim_yili INTEGER, sube_id INTEGER REFERENCES sube(sube_id));
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, soyad TEXT NOT NULL,
                  eposta TEXT, kayit_tarihi TEXT NOT NULL);
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL REFERENCES kitap(kitap_id),
                    uye_id INTEGER NOT NULL REFERENCES uye(uye_id), alis_tarihi TEXT NOT NULL,
                    iade_tarihi TEXT);
INSERT INTO sube VALUES (1,'Merkez','Ankara'),(2,'Bahçelievler','Ankara'),
  (3,'Kadıköy','İstanbul'),(4,'Konak','İzmir');
INSERT INTO kitap VALUES (1,'Körlük','José Saramago',1995,1),(2,'Tutunamayanlar','Oğuz Atay',1972,1),
  (3,'Kum Kitabı','Jorge Luis Borges',1975,2),(4,'Yaban','Yakup Kadri',1932,2),
  (5,'Sessiz Ev','Orhan Pamuk',1983,3),(6,'Anayurt Oteli','Yusuf Atılgan',NULL,3),
  (7,'Tehlikeli Oyunlar','Oğuz Atay',1973,NULL);
INSERT INTO uye VALUES (1,'Ayşe','Demir','ayse@ornek.test','2023-02-14'),
  (2,'Mehmet','Kaya','mehmet@ornek.test','2023-05-30'),(3,'Zeynep','Arslan',NULL,'2024-01-09'),
  (4,'Emre','Yıldız','emre@ornek.test','2024-03-22'),(5,'Selin','Aydın',NULL,'2024-11-05'),
  (6,'Burak','Şahin','burak@ornek.test','2025-01-18');
INSERT INTO odunc VALUES (1,1,1,'2025-01-10','2025-01-24'),(2,2,1,'2025-02-02','2025-02-20'),
  (3,1,2,'2025-02-11',NULL),(4,3,3,'2025-03-01','2025-03-15'),(5,4,3,'2025-03-18','2025-04-02'),
  (6,1,4,'2025-04-05','2025-04-19'),(7,5,4,'2025-04-21',NULL),(8,2,5,'2025-05-02','2025-05-30'),
  (9,7,1,'2025-05-14','2025-05-28'),(10,3,5,'2025-06-03',NULL),(11,6,2,'2025-06-11','2025-06-25'),
  (12,4,4,'2025-06-20','2025-07-04');
SQL
```

#### İki Sonuç Kümesi

Örnekler boyunca iki sorgu kullanılacak. Birincisi 1980 öncesi basılmış kitapların
yazarlarını, ikincisi merkez şubedeki kitapların yazarlarını veriyor.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT yazar FROM kitap WHERE basim_yili < 1980 ORDER BY yazar;
SELECT yazar FROM kitap WHERE sube_id = 1 ORDER BY yazar;
SQL
```

```
yazar            
-----------------
Jorge Luis Borges
Oğuz Atay        
Oğuz Atay        
Yakup Kadri      
yazar        
-------------
José Saramago
Oğuz Atay    
```

Birinci küme dört satır ve içinde bir yineleme var — aynı yazarın iki kitabı da 1980
öncesi. İkinci küme iki satır. Ortak değer `Oğuz Atay`.

İki sorgunun küme işlemiyle birleştirilebilmesi için **birleşim uyumlu** (union compatible)
olmaları gerekir: sütun sayıları eşit ve karşılıklı sütunların tipleri karşılaştırılabilir
olmalı. Sayı eşit değilse sorgu çalışmaz.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT baslik FROM kitap
UNION
SELECT ad, sehir FROM sube;
SQL
```

```
Parse error near line 3: SELECTs to the left and right of UNION do not have the same number of result columns
```

Sonuçtaki sütun adları **ilk sorgudan** alınır; sonraki sorguların takma adları göz ardı
edilir.

#### Birleşim ve Yinelenen Satırlar

Birleşim iki kümenin satırlarını alt alta koyar. İki yazımı vardır ve aralarındaki tek
fark yinelenen satırlara ne olduğudur.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT yazar FROM kitap WHERE basim_yili < 1980
UNION
SELECT yazar FROM kitap WHERE sube_id = 1
ORDER BY yazar;
SQL
```

```
yazar            
-----------------
Jorge Luis Borges
José Saramago    
Oğuz Atay        
Yakup Kadri      
```

Dört artı iki, altı satır bekleniyordu; dört satır döndü. `UNION` yinelenen satırları
**eler** — hem iki küme arasındaki ortak satırı, hem de tek bir kümenin içindeki
yinelemeyi. `Oğuz Atay` üç kez geçtiği halde bir kez göründü.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT yazar FROM kitap WHERE basim_yili < 1980
UNION ALL
SELECT yazar FROM kitap WHERE sube_id = 1
ORDER BY yazar;
SQL
```

```
yazar            
-----------------
Jorge Luis Borges
José Saramago    
Oğuz Atay        
Oğuz Atay        
Oğuz Atay        
Yakup Kadri      
```

`UNION ALL` hiçbir şey elemedi: altı satır. Bu ayrım yalnız bir sonuç farkı değil, bir
maliyet farkıdır. Yineleme elemek için motorun bütün satırları karşılaştırması gerekir —
sıralama ya da karma tablosu kurar. `UNION ALL` böyle bir iş yapmaz.

Seçim açık olmalıdır: yinelemenin gerçekten elenmesi gerekiyorsa `UNION`, gerekmiyorsa
`UNION ALL`. Ayrık oldukları bilinen iki kümeyi `UNION` ile birleştirmek, hiçbir işe
yaramayan bir eleme maliyeti öder. Ters yönde, yinelemenin elenmesi gerekirken `UNION ALL`
yazmak sessizce şişmiş sayımlar üretir.

#### Kesişim ve Fark

Kesişim, iki kümede de bulunan satırları verir.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT yazar FROM kitap WHERE basim_yili < 1980
INTERSECT
SELECT yazar FROM kitap WHERE sube_id = 1;
SQL
```

```
yazar    
---------
Oğuz Atay
```

Fark, birinci kümede olup ikincide olmayan satırları verir.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT yazar FROM kitap WHERE basim_yili < 1980
EXCEPT
SELECT yazar FROM kitap WHERE sube_id = 1;
SQL
```

```
yazar            
-----------------
Jorge Luis Borges
Yakup Kadri      
```

Fark işlemi **yönlüdür**: sıra değiştirildiğinde sonuç değişir. Kesişim ve birleşim
yönsüzdür.

İki işlem de öntanımlı olarak yinelenen satırları eler. Yinelemeyi koruyan biçimleri
standartta tanımlıdır ama desteği **motora göre değişir**.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT yazar FROM kitap WHERE basim_yili < 1980
INTERSECT ALL
SELECT yazar FROM kitap WHERE sube_id = 1;
SQL
```

```
Parse error near line 3: near "ALL": syntax error
  azar FROM kitap WHERE basim_yili < 1980 INTERSECT ALL SELECT yazar FROM kitap 
                                      error here ---^
```

Fark işleminin **adı da motora göre değişir**: bazı motorlarda `EXCEPT` yerine başka bir
anahtar sözcük kullanılır. Taşınabilirlik gerekiyorsa küme işlemleri, adları ve
seçenekleriyle birlikte hedef motorda sınanmalıdır.

#### Boş Değer Küme İşlemlerinde Eşittir

Beşinci derste ölçülen tuzak hatırlanmalı: `NOT IN` listesindeki tek bir boş değer sorguyu
sıfır satıra düşürüyordu. Küme işlemleri aynı tuzağı taşımaz, çünkü boş değerleri
gruplamanın yaptığı gibi **birbirine eşit** sayarlar.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode box
SELECT NULL AS birlesim UNION SELECT NULL;
SELECT NULL AS birlesim_tumu UNION ALL SELECT NULL;
SQL
```

```
┌──────────┐
│ birlesim │
├──────────┤
│          │
└──────────┘
┌───────────────┐
│ birlesim_tumu │
├───────────────┤
│               │
│               │
└───────────────┘
```

Çerçeveli çıktı biçimi burada satır sayısını görünür kılıyor: iki boş değer birleşimde tek
satıra indi, `UNION ALL` yazımında iki satır kaldı. Boş değer, karşılaştırma işleci
açısından kendisine eşit değildir ama küme işlemleri açısından eşittir.

Bunun doğrudan bir sonucu var: "hangi şubede hiç kitap yok" sorusu fark işlemiyle
yazıldığında, kitap tablosundaki boş şube kimliği sonucu bozmaz.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT sube_id FROM sube
EXCEPT
SELECT sube_id FROM kitap;
SQL
```

```
sube_id
-------
4      
```

Doğru yanıt tek satır. Aynı soru `NOT IN` ile yazılsaydı, kitap tablosundaki boş kimlik
yüzünden hiçbir satır dönmeyecekti. Fark işlemi, boş değerli sütunlarda `NOT IN` yazımına
göre daha güvenli bir seçenektir.

#### Öncelik ve Sıralama

Birden çok küme işlecinin aynı deyimde kullanılması durumunda değerlendirme sırası önem
kazanır. Standart, kesişime birleşim ve farktan daha yüksek öncelik verir. Uygulaması
**motora göre değişir**.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT yazar FROM kitap WHERE sube_id = 1
UNION
SELECT yazar FROM kitap WHERE basim_yili < 1980
INTERSECT
SELECT yazar FROM kitap WHERE basim_yili > 1980;
SQL
```

```
yazar        
-------------
José Saramago
```

Bu motor işleçleri soldan sağa uyguladı: önce birleşim, sonra kesişim. Kesişim önce
uygulansaydı ikinci ve üçüncü küme ortak satır taşımadığı için boş kalır, birleşim ilk
kümeyi olduğu gibi döndürür ve sonuç iki satır olurdu. Ayraçla gruplamak da her yerde
kabul edilmez.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT yazar FROM kitap WHERE sube_id = 1
UNION
(SELECT yazar FROM kitap WHERE basim_yili < 1980
 INTERSECT
 SELECT yazar FROM kitap WHERE basim_yili > 1980);
SQL
```

```
Parse error near line 3: near "(": syntax error
  SELECT yazar FROM kitap WHERE sube_id = 1 UNION (SELECT yazar FROM kitap WHERE
                                    error here ---^
```

Sonuç olarak, farklı küme işleçlerini tek deyimde karıştırmak taşınabilir bir yazım
değildir. Karışık kullanım gerektiğinde ara sonuçları adlandıran yapılar — İleri SQL
kursundaki ortak tablo ifadeleri — hem sırayı belirginleştirir hem de sorguyu okunur kılar.

Sıralamada da bir kural var: `ORDER BY` yan tümcesi tek tek sorgulara değil, **birleşik
sonucun tamamına** uygulanır ve deyimin sonunda tek kez yazılır. Bu yüzden sıralama
anahtarı, ilk sorgunun ürettiği sütun adlarıyla anılır.

#### Özet

- Birleştirme satırları yan yana, küme işlemleri alt alta ekler; küme işlemleri sütun
  sayısını değiştirmez.
- Küme işlemine giren sorgular birleşim uyumlu olmalıdır; sütun adları ilk sorgudan alınır.
- `UNION` yinelenen satırları eler ve bunun için karşılaştırma maliyeti öder; `UNION ALL`
  hiçbir şey elemez.
- Kesişim ve fark öntanımlı olarak yinelemeleri eler; yinelemeyi koruyan biçimlerin ve
  fark işlecinin adının desteği motora göre değişir.
- Küme işlemleri boş değerleri birbirine eşit sayar; bu, fark işlemini `NOT IN` yazımına
  göre daha güvenli kılar.
- Farklı küme işleçlerinin tek deyimdeki önceliği motora göre değişir; `ORDER BY` birleşik
  sonucun tamamına uygulanır.

#### Sonraki Adım

Bu konu boyunca yapılan her şey okumaydı: var olan satırlar seçildi, süzüldü, birleştirildi,
özetlendi, kümelendi. Veri hep oradaydı — ilk derste bir kez yazıldı ve bir daha
değiştirilmedi. Sıra veriyi ve onu tutan şemanın kendisini yazmakta. Sonraki konunun ilk
dersi tablo oluşturmayı ele alır: sütun tanımları, tip seçimi ve kısıtların şemada nasıl
ifade edildiği. İlişkisel Kuram kursunda kâğıt üzerinde tasarlanan bütünlük kuralları,
orada çalışan bir tanıma dönüşür.

## Veri Değiştirme ve Tanımlama

Tablo tanımı ve kısıtlar, şema değiştirmenin sınırları ve taşınabilir yeniden kurma yolu, tekli ve toplu satır ekleme, koşulsuz güncelleme ve silmenin sonuçları, görünümlerin güncellenebilirliği ve nesne düzeyi yetkilendirme.

### Tablo Oluşturma

Source: https://academia.sh/tr/kurslar/sql-temelleri/tablo-olusturma

Sütun tanımının parçaları, alan ve tablo düzeyi kısıtlar, anahtarların şemada ifadesi ve kısıt ihlallerinin motorda ürettiği hata iletileri.

Kursun buraya kadarki bölümü var olan veriyi okumakla geçti: sütun seçtik, koşul yazdık,
tabloları birleştirdik, grupladık ve küme işlemleriyle sonuç kümelerini birbirine ekledik.
Her sorgu, önceden kurulmuş bir şemayı veri olarak aldı. Bu ders o varsayımı kaldırır ve
sıradaki soruyu sorar: şemanın kendisi nasıl yazılır.

SQL Dil Aileleri dersinde ayrılan ailelerden ilki — veri tanımlama dili — bu konunun
alanıdır. Kütüphane şeması o derste kurulmuş ve üç kısıt türü tanıtılmıştı: `PRIMARY KEY`,
`NOT NULL` ve `REFERENCES`. Orada bu bildirimlerin **ne söylediği** anlatıldı; bu ders
neyi **engellediklerini** gösterir ve listeye üçün dışındakileri ekler — varsayılan
değerler, değer aralığı denetimleri, benzersizlik ve adlandırılmış kısıtlar.

Tablo tanımı, o tabloya girebilecek satırların **kuralını** belirler. Kural şemada
durursa motor onu her yazma denemesinde uygular; kural yalnızca uygulama kodunda durursa,
o koda uğramayan her yol kuralı deler. Bu dersin konusu bu farktır.

#### Tablo Tanımının Parçaları

`CREATE TABLE` deyiminin gövdesi virgülle ayrılmış öge listesidir. Her öge ya bir sütun
tanımıdır ya da bir tablo düzeyi kısıttır. Sütun tanımı üç parçadan oluşur: ad, tip ve
sıfır veya daha çok **kısıt (constraint)**.

Şemanın en küçük tablosu şubelerdir.

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode column
CREATE TABLE sube (
  sube_id INTEGER PRIMARY KEY,
  ad      TEXT NOT NULL,
  sehir   TEXT NOT NULL
);

INSERT INTO sube VALUES (1,'Merkez','Ankara'),(2,'Bahçelievler','Ankara');
INSERT INTO sube (sube_id, ad, sehir) VALUES (3,'Kadıköy','İstanbul');

SELECT * FROM sube;
SQL
```

```
sube_id  ad            sehir   
-------  ------------  --------
1        Merkez        Ankara  
2        Bahçelievler  Ankara  
3        Kadıköy       İstanbul
```

Komut satırındaki `:memory:` argümanı, veritabanını diske değil belleğe kurar: örnek
biter bitmez ortadan kalkar. Bu dersin bütün blokları böyle çalışır ve geride dosya
bırakmaz. Sorgu derslerindeki `kutuphane.db` dosyasına dokunulmaz.

#### Boşluğu Yasaklamak ve Varsayılan Değer

Veri Modelleme ve İlişkisel Kuram kursunda boş değerin "bilinmiyor" anlamına geldiği ve üç
değerli mantığı tetiklediği gösterilmişti. Bir sütun için boşluk anlamsızsa, bu şemada
söylenir: `NOT NULL`.

`DEFAULT` ise ekleme sırasında değer verilmeyen sütunun neyle doldurulacağını belirtir.
`UNIQUE` de sütundaki değerlerin birbirini tekrarlamamasını ister. Üyeler tablosu üçünü
birden kullanır. Sorgu derslerindeki tanıma göre iki değişiklik vardır: e-posta artık
tekil olmak zorundadır ve üyelik durumunu tutan bir sütun eklenmiştir.

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode column
CREATE TABLE uye (
  uye_id       INTEGER PRIMARY KEY,
  ad           TEXT NOT NULL,
  soyad        TEXT NOT NULL,
  eposta       TEXT UNIQUE,
  kayit_tarihi TEXT NOT NULL,
  durum        TEXT NOT NULL DEFAULT 'etkin'
);

INSERT INTO uye (uye_id, ad, soyad, eposta, kayit_tarihi)
VALUES (1,'Ayşe','Demir','ayse@ornek.test','2023-02-14');
INSERT INTO uye (uye_id, ad, soyad, eposta, kayit_tarihi)
VALUES (2,'Mehmet',NULL,'mehmet@ornek.test','2023-05-30');
INSERT INTO uye (uye_id, ad, soyad, eposta, kayit_tarihi)
VALUES (3,'Zeynep','Arslan','ayse@ornek.test','2024-01-09');

SELECT * FROM uye;
SQL
```

```
Runtime error near line 14: NOT NULL constraint failed: uye.soyad (19)
Runtime error near line 16: UNIQUE constraint failed: uye.eposta (19)
uye_id  ad    soyad  eposta           kayit_tarihi  durum
------  ----  -----  ---------------  ------------  -----
1       Ayşe  Demir  ayse@ornek.test  2023-02-14    etkin
```

Üç ekleme denendi, biri geçti. Birinci satır `durum` sütununu hiç yazmadığı hâlde `etkin`
değerini aldı: varsayılan değer devreye girdi. İkincisi soyadı boş bıraktı ve `NOT NULL`
kısıtına takıldı. Üçüncüsü zaten kullanılan bir e-posta adresini tekrarladı.

Hata iletisinin yapısı önemlidir: hangi kısıt türü, hangi tablo, hangi sütun. Kısıt
ihlalinde motor **satırı yazmaz**; deyim başarısız olur ve etkisi tümüyle geri alınır.
Kısmen yazılmış satır diye bir şey yoktur.

#### Tekillik ve Boş Değer

`PRIMARY KEY`, İlişkisel Kuram kursunda tanımlanan birincil anahtarın şemadaki
karşılığıdır ve iki kısıtın bileşimidir: benzersizlik artı boş olmama. `UNIQUE` yalnız
birincisini ister.

Aradaki farkı boş değer belirginleştirir. Standart SQL'de benzersiz bir sütun birden çok
boş değer taşıyabilir, çünkü iki bilinmeyen değerin eşit olduğu söylenemez. Şemadaki
`eposta` sütunu bunun örneğidir: kimi üyelerin e-postası kayıtlı değildir.

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode column
CREATE TABLE uye (
  uye_id       INTEGER PRIMARY KEY,
  ad           TEXT NOT NULL,
  soyad        TEXT NOT NULL,
  eposta       TEXT UNIQUE,
  kayit_tarihi TEXT NOT NULL
);

INSERT INTO uye VALUES (3,'Zeynep','Arslan',NULL,'2024-01-09');
INSERT INTO uye VALUES (5,'Selin','Aydın',NULL,'2024-11-05');
INSERT INTO uye VALUES (6,'Burak','Şahin','burak@ornek.test','2025-01-18');

SELECT count(*) AS satir, count(eposta) AS epostasi_olan FROM uye;
SQL
```

```
satir  epostasi_olan
-----  -------------
3      1            
```

İki satır aynı anda boş e-postayla durabildi. Bu davranış motora göre değişebilen bir
noktadır: kimi motorlar birden çok boş değeri kabul eder, kimileri bu kararı tanımlayıcıya
bırakan ek yazımlar sunar. Bir sütunun hem tekil hem zorunlu olması isteniyorsa
`NOT NULL UNIQUE` ikilisi yazılır — ki bu, birincil anahtarın tanımıdır.

Anahtar birden çok sütundan da oluşabilir. Bir üyenin aynı kitabı iki kez sıraya
yazdırmasını engelleyen kural, tablo düzeyinde bileşik bir birincil anahtardır.

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode column
CREATE TABLE rezervasyon (
  uye_id   INTEGER NOT NULL,
  kitap_id INTEGER NOT NULL,
  tarih    TEXT    NOT NULL,
  PRIMARY KEY (uye_id, kitap_id)
);

INSERT INTO rezervasyon VALUES (1, 5, '2025-07-01');
INSERT INTO rezervasyon VALUES (2, 5, '2025-07-02');
INSERT INTO rezervasyon VALUES (1, 6, '2025-07-03');
INSERT INTO rezervasyon VALUES (1, 5, '2025-07-04');

SELECT * FROM rezervasyon;
SQL
```

```
Runtime error near line 13: UNIQUE constraint failed: rezervasyon.uye_id, rezervasyon.kitap_id (19)
uye_id  kitap_id  tarih     
------  --------  ----------
1       5         2025-07-01
2       5         2025-07-02
1       6         2025-07-03
```

Aynı kitabı farklı üyeler ve aynı üye farklı kitaplar için sıraya girebildi; tekrarlanan
tek şey **çiftin kendisiydi**. Bileşik anahtarda tekillik sütunların ayrı ayrı değil,
birlikte oluşturduğu değer üzerinde tanımlıdır.

#### Değer Aralığını Daraltmak

`CHECK`, satır yazılırken doğrulanan bir koşul yazar. Koşul yanlış sonuç verirse satır
kabul edilmez. Üyelik durumunun üç değerden biri olması gibi bir kural, uygulama koduna
değil şemaya böyle taşınır.

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode column
CREATE TABLE uye (
  uye_id       INTEGER PRIMARY KEY,
  ad           TEXT NOT NULL,
  soyad        TEXT NOT NULL,
  kayit_tarihi TEXT NOT NULL,
  durum        TEXT NOT NULL DEFAULT 'etkin'
                 CHECK (durum IN ('etkin', 'askida', 'kapali'))
);

INSERT INTO uye (uye_id, ad, soyad, kayit_tarihi, durum)
VALUES (1,'Ayşe','Demir','2023-02-14','askida');
INSERT INTO uye (uye_id, ad, soyad, kayit_tarihi, durum)
VALUES (2,'Mehmet','Kaya','2023-05-30','pasif');

SELECT * FROM uye;
SQL
```

```
Runtime error near line 14: CHECK constraint failed: durum IN ('etkin', 'askida', 'kapali') (19)
uye_id  ad    soyad  kayit_tarihi  durum 
------  ----  -----  ------------  ------
1       Ayşe  Demir  2023-02-14    askida
```

Sütun tanımının içine yazılan bir denetim koşulu yalnız o sütuna bakabilir. İki sütunu
birlikte sınayan bir kural gerektiğinde kısıt, sütun listesinin sonuna **tablo düzeyi
kısıt** olarak yazılır. `CONSTRAINT ad` ön eki kısıta ad verir; ad, hata iletisinde
görünür ve hatanın hangi kurala ait olduğunu okunur kılar.

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode column
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,
  CONSTRAINT iade_alistan_sonra
    CHECK (iade_tarihi IS NULL OR iade_tarihi >= alis_tarihi)
);

INSERT INTO odunc VALUES (1, 1, 1, '2025-01-10', '2025-01-24');
INSERT INTO odunc VALUES (3, 1, 2, '2025-02-11', NULL);
INSERT INTO odunc VALUES (4, 3, 3, '2025-03-01', '2025-02-15');

SELECT * FROM odunc;
SQL
```

```
Runtime error near line 15: CHECK constraint failed: iade_alistan_sonra (19)
odunc_id  kitap_id  uye_id  alis_tarihi  iade_tarihi
--------  --------  ------  -----------  -----------
1         1         1       2025-01-10   2025-01-24 
3         1         2       2025-02-11              
```

Koşuldaki `iade_tarihi IS NULL OR` parçası niyeti açık yazar ama davranışı değiştirmez.
Boş değerle yapılan karşılaştırma doğru değil **bilinmeyen** sonuç verir ve kısıt yalnızca
**kesin yanlış** sonuçta satırı reddeder; bilinmeyeni ihlal saymaz. Kısıt yalnızca ikinci
koşuldan oluşsaydı da iade edilmemiş ödünç kabul edilirdi — reddedilen satır her durumda
gerçekten `iade_tarihi < alis_tarihi` olandır. Boş değerin üç değerli mantığı `WHERE`
süzgecinde eleyici, kısıtta bağışlayıcıdır; kısıt yazımında en sık tökezlenen yer budur.

#### Tablolar Arası Bağ

`REFERENCES` yazımı, bir sütundaki değerin başka bir tablonun anahtarında bulunmasını
şart koşar. Bu, başvuru bütünlüğünün şemadaki ifadesidir: var olmayan bir şubeye kitap
yazılamaz.

Yabancı anahtar denetiminin **açık olup olmadığı motora göre değişir**. Kullanılan komut
satırı aracında denetim öntanımlı olarak kapalıdır ve bağlantı başına açılır.

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode column
CREATE TABLE sube (sube_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, sehir TEXT NOT NULL);
CREATE TABLE kitap (
  kitap_id   INTEGER PRIMARY KEY,
  baslik     TEXT NOT NULL,
  yazar      TEXT NOT NULL,
  basim_yili INTEGER CHECK (basim_yili BETWEEN 1450 AND 2100),
  sube_id    INTEGER REFERENCES sube(sube_id)
);

INSERT INTO sube VALUES (1,'Merkez','Ankara');
INSERT INTO kitap VALUES (1,'Körlük','José Saramago',1995,9);

SELECT * FROM kitap;
SQL
```

```
kitap_id  baslik  yazar          basim_yili  sube_id
--------  ------  -------------  ----------  -------
1         Körlük  José Saramago  1995        9      
```

Şemada yazan kısıt uygulanmadı; olmayan şube numarasına sahip satır tabloya girdi. Aynı
blok `PRAGMA foreign_keys = ON;` satırıyla açıldığında sonuç değişir.

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode column
PRAGMA foreign_keys = ON;

CREATE TABLE sube (sube_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, sehir TEXT NOT NULL);
CREATE TABLE kitap (
  kitap_id   INTEGER PRIMARY KEY,
  baslik     TEXT NOT NULL,
  yazar      TEXT NOT NULL,
  basim_yili INTEGER CHECK (basim_yili BETWEEN 1450 AND 2100),
  sube_id    INTEGER REFERENCES sube(sube_id)
);

INSERT INTO sube VALUES (1,'Merkez','Ankara');
INSERT INTO kitap VALUES (1,'Körlük','José Saramago',1995,9);
INSERT INTO kitap VALUES (2,'Tutunamayanlar','Oğuz Atay',1972,1);
INSERT INTO kitap VALUES (7,'Tehlikeli Oyunlar','Oğuz Atay',1973,NULL);

SELECT * FROM kitap;
SQL
```

```
Runtime error near line 15: FOREIGN KEY constraint failed (19)
kitap_id  baslik             yazar      basim_yili  sube_id
--------  -----------------  ---------  ----------  -------
2         Tutunamayanlar     Oğuz Atay  1972        1      
7         Tehlikeli Oyunlar  Oğuz Atay  1973               
```

Olmayan şubeye bağlanan kitap reddedildi, geçerli şubeye bağlanan kabul edildi. Üçüncü
satırın şube numarası boştur ve o da kabul edildi: yabancı anahtar, sütun boş olduğunda
denetim yapmaz — "hangi şubede olduğu bilinmiyor" ifadesi, "olmayan bir şubede" ifadesinden
farklıdır.

`PRAGMA` yazımı standart SQL'in parçası değildir, kullanılan motora aittir. Aktarılabilir
olan ders şudur: **bir kısıtın şemada yazılı olması, denetlendiği anlamına gelmez.** Yeni
bir veritabanıyla çalışmaya başlarken yabancı anahtar denetiminin durumu sınanır; kapalı
bir denetim, aylar sonra fark edilen öksüz satırlar demektir.

#### Kütüphane Şeması

Dersteki parçalar bir araya geldiğinde konunun geri kalanında kullanılacak şema çıkar.

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode column
PRAGMA foreign_keys = ON;

CREATE TABLE sube (
  sube_id INTEGER PRIMARY KEY,
  ad      TEXT NOT NULL,
  sehir   TEXT NOT NULL
);

CREATE TABLE kitap (
  kitap_id   INTEGER PRIMARY KEY,
  baslik     TEXT    NOT NULL,
  yazar      TEXT    NOT NULL,
  basim_yili INTEGER CHECK (basim_yili BETWEEN 1450 AND 2100),
  sube_id    INTEGER REFERENCES sube(sube_id)
);

CREATE TABLE uye (
  uye_id       INTEGER PRIMARY KEY,
  ad           TEXT NOT NULL,
  soyad        TEXT NOT NULL,
  eposta       TEXT UNIQUE,
  kayit_tarihi TEXT NOT NULL,
  durum        TEXT NOT NULL DEFAULT 'etkin'
                 CHECK (durum IN ('etkin', 'askida', 'kapali'))
);

CREATE TABLE odunc (
  odunc_id    INTEGER PRIMARY KEY,
  kitap_id    INTEGER NOT NULL REFERENCES kitap(kitap_id),
  uye_id      INTEGER NOT NULL REFERENCES uye(uye_id),
  alis_tarihi TEXT    NOT NULL,
  iade_tarihi TEXT,
  CONSTRAINT iade_alistan_sonra
    CHECK (iade_tarihi IS NULL OR iade_tarihi >= alis_tarihi)
);

INSERT INTO sube VALUES (1,'Merkez','Ankara'),(3,'Kadıköy','İstanbul');
INSERT INTO kitap VALUES (1,'Körlük','José Saramago',1995,1),(5,'Sessiz Ev','Orhan Pamuk',1983,3);
INSERT INTO uye (uye_id, ad, soyad, eposta, kayit_tarihi)
VALUES (1,'Ayşe','Demir','ayse@ornek.test','2023-02-14'),(3,'Zeynep','Arslan',NULL,'2024-01-09');
INSERT INTO odunc VALUES (1,1,1,'2025-01-10','2025-01-24'),(4,5,3,'2025-03-01',NULL);

SELECT u.ad, u.soyad, k.baslik, s.ad AS sube, o.alis_tarihi
FROM odunc AS o
JOIN uye   AS u ON u.uye_id   = o.uye_id
JOIN kitap AS k ON k.kitap_id = o.kitap_id
JOIN sube  AS s ON s.sube_id  = k.sube_id
ORDER BY o.odunc_id;
SQL
```

```
ad      soyad   baslik     sube     alis_tarihi
------  ------  ---------  -------  -----------
Ayşe    Demir   Körlük     Merkez   2025-01-10 
Zeynep  Arslan  Sessiz Ev  Kadıköy  2025-03-01 
```

Dört tablonun da birincil anahtarı, veriden gelmeyen bir tam sayıdır. Kitaplar için doğal
bir aday vardı — ISBN — ama o, bir **baskıyı** adlandırır; aynı baskının iki nüshası aynı
numarayı taşır ve anahtar nüshaları ayırt edemez. Doğal bir alanın anahtar gibi görünüp
anahtar olamaması, şema tasarımında sık rastlanan bir durumdur ve genellikle bu dersteki
gibi üretilmiş bir anahtarla çözülür.

#### Özet

- Sütun tanımı ad, tip ve kısıtlardan oluşur; kısıt şemada durursa motor onu her yazma
  yolunda uygular, uygulama kodunda durursa yalnızca o koddan geçen yolda uygulanır.
- `NOT NULL` boşluğu yasaklar, `DEFAULT` yazılmayan sütunu doldurur, `CHECK` değer
  aralığını daraltır; boş değer alabilen sütunlarda kısıt koşulu boşluğu ayrıca
  karşılamalıdır.
- `UNIQUE` tekilliği, `PRIMARY KEY` tekillik ile zorunluluğu birlikte ister; anahtar
  birden çok sütundan oluşabilir ve tekillik o sütunların birlikte ürettiği değer
  üzerindedir.
- `REFERENCES` başvuru bütünlüğünü şemaya yazar, boş değerli sütunda denetim yapmaz ve
  denetimin açık olup olmadığı motora göre değişir.
- Kısıt ihlalinde deyim tümüyle başarısız olur; hata iletisi kısıt türünü, tabloyu ve
  kısıt adını verdiği için tanı doğrudan okunabilir.

#### Sonraki Adım

Şema ilk yazıldığı hâlde kalmaz: yeni bir sütun gerekir, bir sütunun adı yanlış
seçilmiştir, bir kısıt sonradan eklenmek istenir. Sonraki ders şema değiştirmeyi ele alır
ve asıl güçlüğü gösterir: `ALTER TABLE` ile tek deyimde yapılabilen değişiklikler
sınırlıdır ve bu sınır motorlar arasında farklıdır. Sınırın ötesine geçildiğinde tablo
yeniden kurulur; bu işlemin taşınabilir ve veriyi kaybetmeyen biçimi de o derste
kurulacak.

### Şema Değiştirme

Source: https://academia.sh/tr/kurslar/sql-temelleri/sema-degistirme

ALTER TABLE ile sütun ekleme, adlandırma ve kaldırma; tek deyimle yapılamayan değişikliklerin motora göre değişen sınırı ve tabloyu taşınabilir biçimde yeniden kurma yordamı.

Önceki ders şemayı kısıtlarıyla birlikte yazdı. Gerçek bir şema ise ilk yazıldığı hâlde
kalmaz: üyelere telefon alanı gerekir, bir sütunun adı yanlış seçilmiştir, e-posta alanına
sonradan bir kısıt eklenmek istenir. Üstelik bunlar tablo boşken değil, içinde veri
varken yapılır.

`ALTER TABLE` deyimi bu değişiklikleri yazar. Deyimin kendisi standarttır, ancak
**hangi değişikliği tek deyimde yapabildiği motora göre değişir** — SQL'de taşınabilirliğin
en zayıf olduğu alanlardan biridir. Bu ders hem deyimi hem de sınırı gösterir, sonra
sınırın ötesine geçmenin her motorda çalışan yolunu kurar.

#### Sütun Eklemek ve Yeniden Adlandırmak

En çok gereken iki değişiklik en ucuz olanlardır: sonuna sütun eklemek ve bir sütunun
adını değiştirmek. Her ikisi de var olan satırların içeriğine dokunmadan yapılabilir.

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode column
CREATE TABLE uye (
  uye_id       INTEGER PRIMARY KEY,
  ad           TEXT NOT NULL,
  soyad        TEXT NOT NULL,
  eposta       TEXT UNIQUE,
  kayit_tarihi TEXT NOT NULL
);
INSERT INTO uye VALUES (1,'Ayşe','Demir','ayse@ornek.test','2023-02-14'),
                       (3,'Zeynep','Arslan',NULL,'2024-01-09');

ALTER TABLE uye ADD COLUMN telefon TEXT;
ALTER TABLE uye ADD COLUMN bildirim TEXT NOT NULL DEFAULT 'eposta';
ALTER TABLE uye RENAME COLUMN kayit_tarihi TO uyelik_tarihi;

SELECT * FROM uye;
SQL
```

```
uye_id  ad      soyad   eposta           uyelik_tarihi  telefon  bildirim
------  ------  ------  ---------------  -------------  -------  --------
1       Ayşe    Demir   ayse@ornek.test  2023-02-14              eposta  
3       Zeynep  Arslan                   2024-01-09              eposta  
```

Var olan iki satır yeni sütunları da aldı. `telefon` sütununda değer yok, çünkü kısıt da
varsayılan da yazılmadı: eski satırlar boş değer taşır. `bildirim` sütununda ise `NOT NULL`
kısıtı var, dolayısıyla boşluk kabul edilemez — motor eski satırları varsayılan değerle
doldurdu. Sütun eklerken bu iki durumdan biri seçilmelidir: ya sütun boş kalabilir ya da
eski satırların ne alacağı yazılır.

`RENAME COLUMN` yalnızca adı değiştirir; veriye ve kısıtlara dokunmaz. Bu, görünüşte
zararsız ama etkisi geniş bir işlemdir: sütunun eski adını kullanan her sorgu, her
görünüm ve her uygulama kodu kırılır. Adlandırma hatası fark edildiğinde düzeltmek
ucuzdur, aylar sonra düzeltmek değildir.

Bu dersteki blokların her biri kendi başına çalışır ve üye tablosunun yalnızca örneğe
gereken sütunlarını kurar; kursun asıl şeması önceki derste yazılan tanımdır. Yukarıdaki
yeniden adlandırma da o tanıma işlenmez, etkisini göstermek için yapılmıştır — sonraki
derslerde sütunun adı `kayit_tarihi` olarak sürer.

#### Eklemenin Sınırları

Sütun eklemek her zaman ucuz değildir. Ucuz olmasının nedeni, motorun var olan satırlara
dokunmak zorunda kalmamasıdır. Bu koşulun bozulduğu üç durum vardır ve motor bunları
reddeder.

```sh
sqlite3 :memory: <<'SQL'
CREATE TABLE uye (
  uye_id       INTEGER PRIMARY KEY,
  ad           TEXT NOT NULL,
  soyad        TEXT NOT NULL,
  eposta       TEXT UNIQUE,
  kayit_tarihi TEXT NOT NULL
);
INSERT INTO uye VALUES (1,'Ayşe','Demir','ayse@ornek.test','2023-02-14');

ALTER TABLE uye ADD COLUMN kimlik_no TEXT NOT NULL;
ALTER TABLE uye ADD COLUMN kart_no TEXT UNIQUE;
ALTER TABLE uye ADD COLUMN son_giris TEXT DEFAULT (date('now'));
SQL
```

```
Runtime error near line 10: Cannot add a NOT NULL column with default value NULL
Parse error near line 11: Cannot add a UNIQUE column
Runtime error near line 12: Cannot add a column with non-constant default
```

Üç ret, üç ayrı gerekçeyle gelir. Birincisi, varsayılanı olmayan zorunlu sütundur: var
olan satırın o sütunda ne yazacağı belirsizdir, boş bırakılamaz da. İkincisi benzersizlik
kısıtıdır: motor, var olan satırları yeniden dizinlemeden benzersizliği kuramadığı için
`ADD COLUMN` ile gelen sütuna `UNIQUE` konulmasını satır sayısına bakmadan reddeder — tablo
boş olsa bile. Üçüncüsü sabit olmayan varsayılandır: değeri
her satır için ayrı hesaplanan bir varsayılan, eski satırlara ne yazılacağını belirsiz
bırakır.

Bu üç iletinin sözcükleri motora özgüdür. Ortak olan kural şudur: **var olan satırların
yeniden yazılmasını gerektiren bir ekleme tek deyimle yapılamaz.** Sütun önce boş
bırakılabilir hâlde eklenir, satırlar doldurulur, kısıt sonra kurulur.

#### Sütun Kaldırmak

Sütun kaldırmak da benzer bir sınıra takılır. İşlemin kendisi tanımlıdır, ancak
kaldırılacak sütun bir kısıtın ya da dizinin parçasıysa reddedilir.

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode column
CREATE TABLE uye (
  uye_id       INTEGER PRIMARY KEY,
  ad           TEXT NOT NULL,
  soyad        TEXT NOT NULL,
  eposta       TEXT UNIQUE,
  kayit_tarihi TEXT NOT NULL,
  telefon      TEXT
);
INSERT INTO uye VALUES (1,'Ayşe','Demir','ayse@ornek.test','2023-02-14','0312 000 00 00');

ALTER TABLE uye DROP COLUMN telefon;
SELECT * FROM uye;
ALTER TABLE uye DROP COLUMN eposta;
SQL
```

```
uye_id  ad    soyad  eposta           kayit_tarihi
------  ----  -----  ---------------  ------------
1       Ayşe  Demir  ayse@ornek.test  2023-02-14  
Parse error near line 15: cannot drop UNIQUE column: "eposta"
```

Kısıtsız `telefon` sütunu kalktı; benzersizlik kısıtı taşıyan `eposta` kalkmadı. Sütun
kaldırmanın geri alınamaz olduğunu da not etmek gerekir: sütunla birlikte içindeki bütün
değerler gider. Üretimdeki bir tabloda sütun kaldırmadan önce yapılan iş, sütunu
kullanmayı bırakıp bir süre beklemektir; kaldırma en son adımdır.

#### Tabloyu Yeniden Adlandırmak

Tablo adı değiştiğinde, o tabloya başvuran yabancı anahtarların ne olacağı ayrı bir
sorudur. Kullanılan motor başvuruları izler ve günceller.

```sh
sqlite3 :memory: <<'SQL'
PRAGMA foreign_keys = ON;

CREATE TABLE sube (sube_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, sehir TEXT NOT NULL);
CREATE TABLE kitap (
  kitap_id INTEGER PRIMARY KEY,
  baslik   TEXT NOT NULL,
  yazar    TEXT NOT NULL,
  sube_id  INTEGER REFERENCES sube(sube_id)
);

ALTER TABLE sube RENAME TO subeler;
.schema kitap
SQL
```

```
CREATE TABLE kitap (
  kitap_id INTEGER PRIMARY KEY,
  baslik   TEXT NOT NULL,
  yazar    TEXT NOT NULL,
  sube_id  INTEGER REFERENCES "subeler"(sube_id)
);
```

`kitap` tablosunun tanımı, ona hiç dokunulmadığı hâlde değişti. Bu davranış motora göre
değişir: kimi motorlar başvuruyu izler, kimileri eski adı tutmayı sürdürür ve şema
tutarsız kalır. Tablo adı değiştirmenin öncesinde, o tabloya kimin başvurduğu
çıkarılmalıdır.

#### Tabloyu Yeniden Kurmak

Bir sütunun tipini değiştirmek, var olan bir sütuna kısıt eklemek, bir kısıtı kaldırmak
ya da sütun sırasını düzeltmek — bunların hiçbiri kullanılan motorda tek deyimle
yapılamaz. Kimi motorlarda bir kısmı yapılabilir, ancak yapılabildiği yerde bile tablo
büyükse işlem satırları yeniden yazar.

Her motorda çalışan yol, tabloyu yeniden kurmaktır. Yordam dört adımdır: istenen tanımla
yeni bir tablo yaratılır, veri kopyalanır, eski tablo düşürülür, yeni tablo eski adı alır.
Dördü tek bir işlemin içinde yapılır ki yarıda kalan bir adım şemayı bozuk bırakmasın.

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode column
PRAGMA foreign_keys = OFF;

CREATE TABLE uye (
  uye_id       INTEGER PRIMARY KEY,
  ad           TEXT NOT NULL,
  soyad        TEXT NOT NULL,
  eposta       TEXT UNIQUE,
  kayit_tarihi TEXT NOT NULL,
  durum        TEXT NOT NULL DEFAULT 'etkin'
                 CHECK (durum IN ('etkin', 'askida', 'kapali')),
  bildirim     TEXT NOT NULL DEFAULT 'eposta'
);
INSERT INTO uye (uye_id, ad, soyad, eposta, kayit_tarihi) VALUES
  (1,'Ayşe','Demir','ayse@ornek.test','2023-02-14'),
  (3,'Zeynep','Arslan',NULL,'2024-01-09');

BEGIN;

CREATE TABLE uye_yeni (
  uye_id       INTEGER PRIMARY KEY,
  ad           TEXT NOT NULL,
  soyad        TEXT NOT NULL,
  eposta       TEXT UNIQUE CHECK (eposta IS NULL OR eposta LIKE '%@%'),
  kayit_tarihi TEXT NOT NULL,
  durum        TEXT NOT NULL DEFAULT 'etkin'
                 CHECK (durum IN ('etkin', 'askida', 'kapali')),
  bildirim     TEXT NOT NULL DEFAULT 'eposta'
                 CHECK (bildirim IN ('eposta', 'sms', 'yok'))
);

INSERT INTO uye_yeni (uye_id, ad, soyad, eposta, kayit_tarihi, durum, bildirim)
SELECT uye_id, ad, soyad, eposta, kayit_tarihi, durum, bildirim FROM uye;

DROP TABLE uye;
ALTER TABLE uye_yeni RENAME TO uye;

COMMIT;

PRAGMA foreign_key_check;
SELECT * FROM uye;
SQL
```

```
uye_id  ad      soyad   eposta           kayit_tarihi  durum  bildirim
------  ------  ------  ---------------  ------------  -----  --------
1       Ayşe    Demir   ayse@ornek.test  2023-02-14    etkin  eposta  
3       Zeynep  Arslan                   2024-01-09    etkin  eposta  
```

Yeni tanımdaki iki denetim koşulu da `ALTER TABLE` ile eklenemezdi, çünkü ikisi de **var
olan** sütunlara bağlanıyor: biri `eposta`, diğeri dersin başında eklenen `bildirim`
sütunu. O sütun eklenirken varsayılan verilebilmişti ama kısıt verilememişti; kısıt
ancak burada, tablo yeniden kurulurken tanıma girdi. Veri korundu, iki satır da yeni
tanıma uydu — boş e-postalı satır dahil, çünkü koşul boşluğu ayrıca karşılıyor.

Kopyalama adımının bir yan yararı vardır: `SELECT` listesi yazıldığı için sütunlar
dönüştürülebilir. Bir tarih sütunu metinden sayıya çevriliyorsa dönüşüm burada yapılır;
eski sütun düşürülüyorsa listeye alınmaz.

Kopyalama sırasında yeni tanımın kısıtları uygulanır. Eski veri yeni kısıtı ihlal
ediyorsa deyim başarısız olur ve işlem geri alınır — istenen budur. Şema değişikliği,
veriyi kısıta uydurma işiyle birlikte planlanır.

`PRAGMA foreign_key_check` çağrısı hiçbir şey yazdırmadı: kırık başvuru yok. Bu çağrı
yordamın son adımıdır ve atlanmamalıdır.

#### Yeniden Kurmanın Riski

Yabancı anahtar denetimi yordam boyunca kapatılır, çünkü `DROP TABLE` adımı ile
`RENAME TO` adımı arasında hedef tablo geçici olarak yoktur. Denetimin kapalı olması,
kopyalamada yapılan bir hatanın sessizce geçmesi demektir.

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode column
PRAGMA foreign_keys = OFF;

CREATE TABLE sube (sube_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, sehir TEXT NOT NULL);
CREATE TABLE kitap (
  kitap_id INTEGER PRIMARY KEY,
  baslik   TEXT NOT NULL,
  sube_id  INTEGER NOT NULL REFERENCES sube(sube_id)
);
INSERT INTO sube VALUES (1,'Merkez','Ankara'),(3,'Kadıköy','İstanbul');
INSERT INTO kitap VALUES (1,'Körlük',1),(5,'Sessiz Ev',3);

BEGIN;
CREATE TABLE sube_yeni (
  sube_id INTEGER PRIMARY KEY,
  ad      TEXT NOT NULL,
  sehir   TEXT NOT NULL,
  acik    INTEGER NOT NULL DEFAULT 1
);
INSERT INTO sube_yeni (sube_id, ad, sehir)
SELECT sube_id, ad, sehir FROM sube WHERE sehir = 'Ankara';
DROP TABLE sube;
ALTER TABLE sube_yeni RENAME TO sube;
COMMIT;

PRAGMA foreign_key_check;
SQL
```

```
table  rowid  parent  fkid
-----  -----  ------  ----
kitap  5      sube    0   
```

Kopyalama deyimindeki koşul yüzünden bir şube taşınmadı ve ona bağlı kitap öksüz kaldı.
İşlem başarıyla kesinleşti, hiçbir hata verilmedi; kırığı yalnızca denetim çağrısı
gösterdi. Yordamın bu son adımı, sessiz veri kaybının tek uyarısıdır.

Uygulamada üç alışkanlık bu riski küçültür. Yeniden kurma yordamı, üretim verisinin bir
kopyası üzerinde önce denenir. Kopyalama deyiminden sonra satır sayıları karşılaştırılır.
Ve yordam bir işlemin içinde çalıştırılır, böylece adımlardan biri başarısız olduğunda
şema eski hâline döner.

#### Özet

- `ALTER TABLE` sütun ekleme, yeniden adlandırma ve kaldırmayı tek deyimde yapar; hangi
  değişikliğin desteklendiği motora göre değişir.
- Var olan satırların yeniden yazılmasını gerektiren eklemeler reddedilir: varsayılanı
  olmayan zorunlu sütun, benzersiz sütun ve sabit olmayan varsayılan.
- Sütun kaldırmak geri alınamaz ve kısıtın parçası olan sütunlarda reddedilir; güvenli
  sıra, önce kullanımı bırakmak sonra kaldırmaktır.
- Tek deyimle yapılamayan değişiklikler için taşınabilir yol tabloyu yeniden kurmaktır:
  yeni tablo, kopyalama, düşürme, yeniden adlandırma — hepsi tek işlem içinde.
- Yeniden kurma sırasında yabancı anahtar denetimi kapalı olduğu için, yordamın son adımı
  başvuru bütünlüğünü sınamaktır.

#### Sonraki Adım

Şema kurulmuş ve değiştirilebilir durumda. Sıradaki soru, içine veriyi nasıl koyduğumuz.
Sonraki ders satır eklemeyi ele alır: tek satırlık ekleme, tek deyimde çok satır, sorgu
sonucunu doğrudan tabloya yazma ve bunların birbirinden ayrıldığı yer olan başarım.
Yirmi bin satırın tek tek mi yoksa tek işlem içinde mi eklendiği, ölçülebilir bir
farktır.

### Satır Ekleme

Source: https://academia.sh/tr/kurslar/sql-temelleri/satir-ekleme

INSERT deyiminin sütun listeli ve konumsal biçimleri, tek deyimde çok satır, sorgu sonucunu tabloya yazma, çatışma davranışı ve eklemenin işlem içinde yapılmasının ölçülen etkisi.

Şema kuruldu ve değiştirilebilir durumda. Sıradaki soru içine veriyi nasıl koyduğumuz.
`INSERT` deyimi görünüşte kursun en yalın deyimidir — bir tablo adı, bir değer listesi —
ama iki noktada ayrışır: değerlerin sütunlara nasıl bağlandığı ve eklemenin kaç işlem
içinde yapıldığı. İkincisi, aynı veriyi yazan iki betik arasında yüzlerce kat fark
üretebilir.

#### Tek Satır Eklemek

Deyimin iki yazımı vardır. Sütun listeli yazımda hangi değerin hangi sütuna gittiği
deyimde yazılıdır. Konumsal yazımda sütun listesi atlanır ve değerler tablodaki sütun
sırasına göre eşlenir.

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode column
CREATE TABLE uye (
  uye_id       INTEGER PRIMARY KEY,
  ad           TEXT NOT NULL,
  soyad        TEXT NOT NULL,
  eposta       TEXT UNIQUE,
  kayit_tarihi TEXT NOT NULL,
  durum        TEXT NOT NULL DEFAULT 'etkin'
);

INSERT INTO uye (uye_id, ad, soyad, eposta, kayit_tarihi)
VALUES (1,'Ayşe','Demir','ayse@ornek.test','2023-02-14');
INSERT INTO uye VALUES (2,'Mehmet','Kaya','mehmet@ornek.test','2023-05-30','askida');
INSERT INTO uye VALUES (3,'Zeynep','Arslan',NULL,'2024-01-09');

SELECT * FROM uye;
SQL
```

```
Parse error near line 15: table uye has 6 columns but 5 values were supplied
uye_id  ad      soyad  eposta             kayit_tarihi  durum 
------  ------  -----  -----------------  ------------  ------
1       Ayşe    Demir  ayse@ornek.test    2023-02-14    etkin 
2       Mehmet  Kaya   mehmet@ornek.test  2023-05-30    askida
```

İlk deyim `durum` sütununu yazmadı ve varsayılan değeri aldı. İkincisi bütün sütunları
sırayla verdi. Üçüncüsü konumsal yazımı kullandı ama altı sütunluk tabloya beş değer
gönderdi ve reddedildi.

Sütun listesi bu örnekte yalnızca bir yazım tercihi gibi görünür; sürdürülebilirlik
açısından ise ikisi eşit değildir. Bir sütun eklendiğinde ya da sütun sırası
değiştiğinde konumsal yazım ya hata verir ya da — daha kötüsü — tipler uyduğu için
değerleri yanlış sütunlara yazar. Önceki dersteki `ALTER TABLE ADD COLUMN` deyimi tam da
bunu yapan bir değişikliktir. Sütun listesi yazılırsa şema değişikliği deyimi etkilemez.
Kalıcı kodda ekleme deyimleri sütun listesiyle yazılır; konumsal yazım komut satırında
hızlı denemeler için kalır.

#### Tek Deyimde Çok Satır

`VALUES` sözcüğünden sonra virgülle ayrılmış birden çok satır yazılabilir. Motor bunu
tek bir deyim olarak işler.

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode column
CREATE TABLE sube (sube_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, sehir TEXT NOT NULL);

INSERT INTO sube (sube_id, ad, sehir) VALUES
  (1, 'Merkez',       'Ankara'),
  (2, 'Bahçelievler', 'Ankara'),
  (3, 'Kadıköy',      'İstanbul');

SELECT count(*) AS eklenen FROM sube;
SQL
```

```
eklenen
-------
3      
```

Tek deyim olması yalnızca yazımı kısaltmaz, davranışı da belirler: deyim bölünmez.
Satırlardan biri bir kısıtı ihlal ederse hiçbiri yazılmaz.

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode column
CREATE TABLE sube (sube_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, sehir TEXT NOT NULL);
INSERT INTO sube VALUES (1,'Merkez','Ankara');

INSERT INTO sube (sube_id, ad, sehir) VALUES
  (3, 'Kadıköy',      'İstanbul'),
  (1, 'Merkez',       'Ankara'),
  (4, 'Konak',        'İzmir');

SELECT * FROM sube ORDER BY sube_id;
SQL
```

```
Runtime error near line 6: UNIQUE constraint failed: sube.sube_id (19)
sube_id  ad      sehir 
-------  ------  ------
1        Merkez  Ankara
```

Çakışan satır ikinci sıradaydı; buna rağmen ondan önceki Kadıköy de sonraki Konak da
tabloya girmedi. Bir deyimin ya tamamı uygulanır ya da hiçbiri — bu, İlişkisel Kuram
kursunda atomiklik olarak adlandırılan özelliğin deyim düzeyindeki görünümüdür.

#### Sorgu Sonucunu Eklemek

`VALUES` yerine bir `SELECT` yazılabilir. Bu biçim, veriyi uygulamaya taşımadan bir
tablodan diğerine aktarır; okuma ve yazma tek deyimde olur.

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode column
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 odunc VALUES
  (1,1,1,'2025-01-10','2025-01-24'),
  (2,2,1,'2025-02-02','2025-02-20'),
  (3,1,2,'2025-02-11',NULL),
  (4,3,3,'2025-03-01','2025-03-15');

CREATE TABLE odunc_arsiv (
  odunc_id   INTEGER PRIMARY KEY,
  kitap_id   INTEGER NOT NULL,
  uye_id     INTEGER NOT NULL,
  gun_sayisi INTEGER NOT NULL
);

INSERT INTO odunc_arsiv (odunc_id, kitap_id, uye_id, gun_sayisi)
SELECT odunc_id, kitap_id, uye_id,
       CAST(julianday(iade_tarihi) - julianday(alis_tarihi) AS INTEGER)
FROM odunc
WHERE iade_tarihi IS NOT NULL;

SELECT changes() AS aktarilan;
SELECT * FROM odunc_arsiv;
SQL
```

```
aktarilan
---------
3        
odunc_id  kitap_id  uye_id  gun_sayisi
--------  --------  ------  ----------
1         1         1       14        
2         2         1       18        
4         3         3       14        
```

`SELECT` listesindeki ifadeler hedef sütunlarla **sırayla** eşlenir; adların örtüşmesi
gerekmez, sütun sayısı ve tipleri uymalıdır. Sorgunun `WHERE` koşulu hangi satırların
aktarılacağını belirler: iade edilmemiş ödünç dışarıda kaldı.

`changes()` çağrısı son deyimin etkilediği satır sayısını verir. Adı motora göre değişir,
ancak karşılığı her motorda bulunur ve toplu işlemlerde beklenen sayının doğrulanması için
kullanılır. `julianday` gün farkını hesaplayan bir tarih işlevidir, `CAST` ise sonucu tam
sayıya çevirir.

#### Eklenen Satırı Geri Okumak

Birincil anahtarı motor üretiyorsa, eklenen satırın anahtarı ekleme sonrasında bilinmez.
Pek çok motor bunun için ekleme deyimine bir çıktı listesi eklemeye izin verir.

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode column
CREATE TABLE uye (
  uye_id       INTEGER PRIMARY KEY,
  ad           TEXT NOT NULL,
  soyad        TEXT NOT NULL,
  kayit_tarihi TEXT NOT NULL,
  durum        TEXT NOT NULL DEFAULT 'etkin'
);

INSERT INTO uye (ad, soyad, kayit_tarihi)
VALUES ('Emre','Yıldız','2024-03-22') RETURNING uye_id, ad, durum;
INSERT INTO uye (ad, soyad, kayit_tarihi)
VALUES ('Selin','Aydın','2024-11-05') RETURNING uye_id, ad, durum;
SQL
```

```
uye_id  ad    durum
------  ----  -----
1       Emre  etkin
uye_id  ad     durum
------  -----  -----
2       Selin  etkin
```

Anahtar sütunu hiç yazılmadı; motor onu üretti ve varsayılan durumla birlikte deyimin
çıktısında verdi. `RETURNING` yazımı **motora göre değişir**: kimi motorlar destekler,
kimileri son üretilen anahtarı veren ayrı bir işlev sunar. Ekleme sonrası anahtara
ihtiyaç duyulduğunda ilk sorulacak soru, kullanılan motorun bunu hangi yolla verdiğidir.

#### Çatışma Durumunda Ne Olacağı

Var olan bir anahtarla ekleme yapıldığında öntanımlı davranış hatadır. Kimi işlerde
istenen ise "varsa güncelle, yoksa ekle" davranışıdır.

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode column
CREATE TABLE sube (sube_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, sehir TEXT NOT NULL);
INSERT INTO sube VALUES (1,'Merkez','Ankara');

INSERT INTO sube (sube_id, ad, sehir) VALUES (1, 'Merkez Şube', 'Ankara')
  ON CONFLICT (sube_id) DO UPDATE SET ad = excluded.ad;

INSERT INTO sube (sube_id, ad, sehir) VALUES (1, 'Yok Sayılacak', 'Ankara')
  ON CONFLICT (sube_id) DO NOTHING;

SELECT * FROM sube;
SQL
```

```
sube_id  ad           sehir 
-------  -----------  ------
1        Merkez Şube  Ankara
```

İlk deyim çatışmayı yakalayıp adı güncelledi, ikincisi çatışmayı sessizce yok saydı.
`excluded`, eklenmeye çalışılan ama çatışma yüzünden yazılamayan satırı adlandırır.

Bu yazımın adı ve ayrıntısı motora göre değişir; kimi motorlar aynı işi başka bir
anahtar sözcükle yapar. Değişmeyen nokta, çatışma davranışının **deyimde** yazılmasının,
uygulama kodunda "önce sorgula sonra ekle" biçiminde yazılmasından güvenli olmasıdır:
sorgu ile ekleme arasında geçen sürede başka bir oturum aynı satırı ekleyebilir.

#### Eklemenin İşlem İçinde Toplanması

Ekleme deyimlerinin kaç işlem içinde çalıştığı, aynı veriyi yazan iki betiği ayırır. Açık
bir işlem başlatılmazsa motor her deyimi kendi işlemi sayar ve her deyimin sonunda
kalıcılığı güvenceye almak için diske eşitleme yapar. Yirmi bin deyim, yirmi bin eşitleme
demektir.

```sh
cd "$(mktemp -d)"

satirlar() {
  awk 'BEGIN { for (i = 1; i <= 20000; i++) printf "INSERT INTO odunc_yuk VALUES (%d, %d, %d, \0472025-03-01\047);\n", i, 1 + i % 7, 1 + i % 6 }'
}
sema="CREATE TABLE odunc_yuk (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL, uye_id INTEGER NOT NULL, alis_tarihi TEXT NOT NULL);"

{ echo "$sema"; satirlar; }                               > tek_tek.sql
{ echo "$sema"; echo "BEGIN;"; satirlar; echo "COMMIT;"; } > tek_islem.sql

echo "--- her deyim ayri islem ---"
time sqlite3 a.db ".read tek_tek.sql"
echo "--- hepsi tek islemde ---"
time sqlite3 b.db ".read tek_islem.sql"
sqlite3 a.db "SELECT count(*) FROM odunc_yuk;"
sqlite3 b.db "SELECT count(*) FROM odunc_yuk;"
```

```
--- her deyim ayri islem ---

real	0m4.323s
user	0m0.153s
sys	0m3.148s
--- hepsi tek islemde ---

real	0m0.026s
user	0m0.022s
sys	0m0.003s
20000
20000
```

Aynı yirmi bin satır, aynı deyimler, aynı sonuç. Aradaki fark yüzlerce kattır. Ölçüm
diskin ve makinenin özelliklerine bağlıdır — sayılar her çalıştırmada ve her makinede
değişir — ama oranın büyüklüğü, işlem sınırının başarımda ne kadar belirleyici olduğunu
gösterir. `awk` içindeki `\047` yazımı tek tırnak karakterini üretir; kabuk tırnaklamasıyla
çakışmadan SQL dizgisi yazmayı sağlar.

Aynı etkiyi tek deyimde çok satır yazarak da elde etmek olanaklıdır: bin satırlık bir
`VALUES` listesi zaten tek işlemdir. İki yöntem birlikte kullanılır — toplu deyimler,
açık bir işlemin içinde. İşlem sınırının çok geniş tutulmasının da bir bedeli vardır ve o
konu İleri SQL kursunun kilitleme dersine aittir.

#### Özet

- Ekleme deyiminin sütun listeli biçimi şema değişikliklerine dayanıklıdır; konumsal
  biçim sütun sırası değiştiğinde sessizce yanlış sütuna yazabilir.
- Tek deyimde yazılan çok satır bölünmez: satırlardan biri kısıta takılırsa hiçbiri
  yazılmaz.
- `INSERT ... SELECT` biçimi veriyi uygulamaya taşımadan tablolar arasında aktarır;
  eşleme sütun adlarına göre değil sıraya göre yapılır.
- Eklenen satırın motor tarafından üretilen değerlerini geri okuma ve çatışma davranışını
  deyimde belirtme olanağı vardır, ancak yazımı motora göre değişir.
- Eklemeyi açık bir işlemin içinde toplamak, her deyimin ayrı işlem sayılmasına göre
  ölçülebilir biçimde daha hızlıdır; fark diske eşitleme sayısından gelir.

#### Sonraki Adım

Veri yerinde. Sonraki ders onu değiştirmeyi ve silmeyi ele alır. `UPDATE` ve `DELETE`
deyimlerinin sözdizimi kısadır, tehlikesi ise koşul cümlesinin isteğe bağlı olmasından
gelir: koşulu unutulmuş bir güncelleme sözdizimsel olarak kusursuzdur ve tablodaki her
satırı değiştirir. O dersin çekirdeği, bu deyimleri yazarken çalışan alışkanlıktır —
önce işlem açmak, etkilenen satır sayısını görmek ve gerekirse geri almak.

### Güncelleme ve Silme

Source: https://academia.sh/tr/kurslar/sql-temelleri/guncelleme-ve-silme

UPDATE ve DELETE deyimlerinin yapısı, koşul cümlesi unutulduğunda ne olduğu, etkilenen satır sayısının okunması, işlem açıp geri alma alışkanlığı ve silmenin bağlı satırlara etkisi.

Veri tabloya girdi. Sıradaki iki deyim onu değiştirir ve siler. Sözdizimleri kursun en
kısa deyimleri arasındadır; tehlikeleri de tam olarak buradan gelir. `WHERE` cümlesi
isteğe bağlıdır — yani koşulu unutulmuş bir güncelleme sözdizimsel olarak kusursuzdur,
motor tarafından uyarısız kabul edilir ve tablodaki **her** satırı değiştirir.

Bu dersin bütün örnekleri `:memory:` veritabanında çalışır: bellekte kurulan, komut
bitince yok olan geçici bir veritabanı. Yıkıcı bir deyimin ne yaptığını görmenin yolu onu
çalıştırmaktır, ancak çalıştırılacağı yer gerçek veri değildir.

#### Koşullu Güncelleme

`UPDATE` deyimi üç parçadan oluşur: hangi tablo, hangi sütunlara hangi değerler, hangi
satırlar. Üçüncü parça yazılmazsa "hangi satırlar" sorusunun yanıtı "hepsi" olur.

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode column
CREATE TABLE uye (
  uye_id INTEGER PRIMARY KEY,
  ad     TEXT NOT NULL,
  soyad  TEXT NOT NULL,
  durum  TEXT NOT NULL DEFAULT 'etkin'
           CHECK (durum IN ('etkin', 'askida', 'kapali'))
);
INSERT INTO uye VALUES (1,'Ayşe','Demir','etkin'),(2,'Mehmet','Kaya','etkin'),
                       (3,'Zeynep','Arslan','askida'),(4,'Emre','Yıldız','etkin');

SELECT uye_id, ad, soyad FROM uye WHERE uye_id = 2;

UPDATE uye SET durum = 'askida' WHERE uye_id = 2;
SELECT changes() AS etkilenen;

SELECT * FROM uye;
SQL
```

```
uye_id  ad      soyad
------  ------  -----
2       Mehmet  Kaya 
etkilenen
---------
1        
uye_id  ad      soyad   durum 
------  ------  ------  ------
1       Ayşe    Demir   etkin 
2       Mehmet  Kaya    askida
3       Zeynep  Arslan  askida
4       Emre    Yıldız  etkin 
```

Blokta iki alışkanlık gömülü. Birincisi, güncellemeden önce **aynı koşulla** bir `SELECT`
çalıştırmaktır: hangi satırların etkileneceği önce görülür. İkincisi, güncellemeden sonra
etkilenen satır sayısını okumaktır. Beklenen sayı biliniyorsa — burada bir — sapma anında
fark edilir.

Güncelleme kısıtları atlamaz. `durum` sütununa listede olmayan bir değer yazan bir
güncelleme, ekleme sırasındakiyle aynı `CHECK` hatasını üretir. Şemadaki kısıtlar yazma
yollarının tamamında geçerlidir.

#### Koşulun Unutulması

Aşağıdaki blok, koşulu olmayan bir güncellemenin ne yaptığını gösterir. Deyimin kendisi
bir öncekiyle aynıdır, yalnızca `WHERE` cümlesi yoktur.

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode column
CREATE TABLE uye (
  uye_id INTEGER PRIMARY KEY,
  ad     TEXT NOT NULL,
  soyad  TEXT NOT NULL,
  durum  TEXT NOT NULL DEFAULT 'etkin'
           CHECK (durum IN ('etkin', 'askida', 'kapali'))
);
INSERT INTO uye VALUES (1,'Ayşe','Demir','etkin'),(2,'Mehmet','Kaya','etkin'),
                       (3,'Zeynep','Arslan','askida'),(4,'Emre','Yıldız','etkin');

UPDATE uye SET durum = 'askida';
SELECT changes() AS etkilenen;

SELECT * FROM uye;
SQL
```

```
etkilenen
---------
4        
uye_id  ad      soyad   durum 
------  ------  ------  ------
1       Ayşe    Demir   askida
2       Mehmet  Kaya    askida
3       Zeynep  Arslan  askida
4       Emre    Yıldız  askida
```

Dört satır. Hata yok, uyarı yok, onay sorusu yok. Kütüphanenin bütün üyeleri askıya
alındı ve bunu söyleyen tek şey `changes()` çıktısındaki sayıdır.

Zararın ölçeği tablonun büyüklüğüyle orantılıdır. Dört satırlık bir örnekte sonuç bir
saniyede görülür; dört yüz bin satırlık bir üye tablosunda, eski değerlerin nerede
tutulduğu sorusuna dönüşür. Eski değerler hiçbir yerde tutulmaz — güncelleme yerinde
yapılır. Geri dönüş yolu yedekten kurtarmadır ve o da yedeğin alındığı ana kadardır.

Aynısı silme için de geçerlidir ve orada daha keskindir: koşulsuz bir `DELETE`, tablonun
bütün satırlarını kaldırır.

#### Geri Alınabilir Alışkanlık

Bu riskin karşılığı dikkat değil, yordamdır. Yazma deyimi açık bir işlemin içinde
çalıştırılır; etkilenen satır sayısı okunur; sayı beklendiği gibiyse işlem
kesinleştirilir, değilse geri alınır.

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode column
CREATE TABLE uye (
  uye_id INTEGER PRIMARY KEY,
  ad     TEXT NOT NULL,
  soyad  TEXT NOT NULL,
  durum  TEXT NOT NULL DEFAULT 'etkin'
);
INSERT INTO uye VALUES (1,'Ayşe','Demir','etkin'),(2,'Mehmet','Kaya','etkin'),
                       (3,'Zeynep','Arslan','askida'),(4,'Emre','Yıldız','etkin');

BEGIN;
UPDATE uye SET durum = 'askida';
SELECT changes() AS etkilenen;
ROLLBACK;

SELECT * FROM uye;
SQL
```

```
etkilenen
---------
4        
uye_id  ad      soyad   durum 
------  ------  ------  ------
1       Ayşe    Demir   etkin 
2       Mehmet  Kaya    etkin 
3       Zeynep  Arslan  askida
4       Emre    Yıldız  etkin 
```

Aynı koşulsuz güncelleme çalıştı, aynı dört satırı etkiledi, sonra `ROLLBACK` deyimi
hepsini geri aldı. Tablo işlem öncesindeki hâlindedir: üçüncü üye askıda, diğerleri etkin.

`BEGIN` ile başlayan bir işlem, `COMMIT` görene kadar kalıcı değildir. Bu, elle yazılan
her yazma deyiminin varsayılan çerçevesi olmalıdır. Deyim doğruysa `COMMIT`, sayı
şaşırtıcıysa `ROLLBACK` yazılır — ve şaşırtıcı sayıyı görme fırsatı ancak işlem açıkken
vardır.

İşlemlerin ayrıntısı — yalıtım düzeyleri, kilitleme, kısmi geri alma — İleri SQL kursunun
konusudur. Burada gereken tek şey çerçevedir.

#### Koşulu Sorgudan Almak

Güncellenecek satırlar bir sorgunun sonucuyla belirlenebilir. Bu, "iade edilmemiş ve
tarihi geçmiş ödüncü olan üyeleri askıya al" gibi kuralları tek deyimde yazmayı sağlar.

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode column
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, soyad TEXT NOT NULL,
                  durum TEXT NOT NULL DEFAULT 'etkin');
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 uye VALUES (1,'Ayşe','Demir','etkin'),(2,'Mehmet','Kaya','etkin'),
                       (3,'Zeynep','Arslan','etkin');
INSERT INTO odunc VALUES
  (1, 1, 1, '2024-11-01', NULL),
  (2, 2, 2, '2025-06-01', NULL),
  (3, 3, 3, '2024-12-15', '2025-01-02');

BEGIN;
UPDATE uye
SET durum = 'askida'
WHERE uye_id IN (SELECT uye_id FROM odunc
                 WHERE iade_tarihi IS NULL AND alis_tarihi < '2025-01-01');
SELECT changes() AS etkilenen;
COMMIT;

SELECT * FROM uye;
SQL
```

```
etkilenen
---------
1        
uye_id  ad      soyad   durum 
------  ------  ------  ------
1       Ayşe    Demir   askida
2       Mehmet  Kaya    etkin 
3       Zeynep  Arslan  etkin 
```

Yalnızca birinci üye askıya alındı: ikincinin ödüncü açık ama tarihi yeni, üçüncününki
eski ama iade edilmiş. Alt sorgu her iki koşulu birlikte uyguladı.

Bu yazımın sınanabilir olması bir yan yarardır: alt sorgu, güncellemeden önce tek başına
`SELECT` olarak çalıştırılabilir ve hangi kimliklerin döndüğü görülür. Alt sorguların
ayrıntılı ele alınışı İleri SQL kursuna aittir; burada kullanımı koşul yazmakla sınırlıdır.

#### Silme ve Bağlı Satırlar

`DELETE` deyiminin yapısı daha da kısadır: hangi tablo ve hangi satırlar.

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode column
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 odunc VALUES
  (1,1,1,'2023-01-10','2023-01-24'),
  (2,2,1,'2023-02-02','2023-02-20'),
  (3,1,2,'2025-02-11',NULL),
  (4,3,3,'2025-03-01','2025-03-15');

DELETE FROM odunc WHERE iade_tarihi IS NOT NULL AND iade_tarihi < '2024-01-01';
SELECT changes() AS silinen;

DELETE FROM odunc;
SELECT changes() AS silinen;
SELECT count(*) AS kalan FROM odunc;
SQL
```

```
silinen
-------
2      
silinen
-------
2      
kalan
-----
0    
```

İkinci deyimde koşul yoktu ve tablo boşaldı. Bu yüzden silme deyimleri, güncellemeden de
sıkı bir yordamla yazılır: önce koşulun `SELECT count(*)` ile sayılması, sonra işlem
içinde silme.

Bir satırın silinmesi ona bağlı satırları da ilgilendirir. Yabancı anahtar tanımına
eklenen davranış, motorun ne yapacağını belirler. Kısıtlayıcı davranışta silme reddedilir.

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode column
PRAGMA foreign_keys = ON;
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, soyad TEXT NOT NULL);
CREATE TABLE odunc (
  odunc_id    INTEGER PRIMARY KEY,
  kitap_id    INTEGER NOT NULL,
  uye_id      INTEGER NOT NULL REFERENCES uye(uye_id) ON DELETE RESTRICT,
  alis_tarihi TEXT NOT NULL
);
INSERT INTO uye VALUES (1,'Ayşe','Demir'),(2,'Mehmet','Kaya');
INSERT INTO odunc VALUES (1,1,1,'2025-02-11');

DELETE FROM uye WHERE uye_id = 1;
DELETE FROM uye WHERE uye_id = 2;
SELECT changes() AS silinen;
SELECT * FROM uye;
SQL
```

```
Runtime error near line 14: FOREIGN KEY constraint failed (19)
silinen
-------
1      
uye_id  ad    soyad
------  ----  -----
1       Ayşe  Demir
```

Ödüncü olan üye silinemedi, olmayan silindi. Yayılan davranışta ise bağlı satırlar da
kaldırılır.

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode column
PRAGMA foreign_keys = ON;
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, soyad TEXT NOT NULL);
CREATE TABLE odunc (
  odunc_id    INTEGER PRIMARY KEY,
  kitap_id    INTEGER NOT NULL,
  uye_id      INTEGER NOT NULL REFERENCES uye(uye_id) ON DELETE CASCADE,
  alis_tarihi TEXT NOT NULL
);
INSERT INTO uye VALUES (1,'Ayşe','Demir'),(2,'Mehmet','Kaya');
INSERT INTO odunc VALUES (1,1,1,'2025-01-10'),(2,2,1,'2025-02-02'),(3,1,2,'2025-02-11');

DELETE FROM uye WHERE uye_id = 1;
SELECT changes() AS silinen_uye;
SELECT * FROM odunc;
SQL
```

```
silinen_uye
-----------
1          
odunc_id  kitap_id  uye_id  alis_tarihi
--------  --------  ------  -----------
3         1         2       2025-02-11 
```

Tek bir üye silindi, onunla birlikte iki ödünç kaydı da gitti. Etkilenen satır sayısı
yine `1` yazıyor: sayaç, deyimin doğrudan hedeflediği tabloya bakar, yayılan silmeleri
saymaz. Yayılan davranışın seçilmesi, ödünç geçmişinin üyeyle birlikte yok olmasını göze
almak demektir. Kütüphane kayıtları gibi geçmişin değerli olduğu alanlarda satır silmek
yerine kaydı kapalı işaretlemek — üyeye `kapali` durumu vermek — daha uygun bir tasarımdır.

#### Özet

- `UPDATE` ve `DELETE` deyimlerinde koşul cümlesi isteğe bağlıdır; yazılmadığında deyim
  tablodaki bütün satırları etkiler ve motor uyarı vermez.
- Etkilenen satır sayısı her yazma deyiminden sonra okunur; beklenen sayıdan sapma,
  yanlış koşulun tek erken göstergesidir.
- Elle yazılan yazma deyimleri işlem içinde çalıştırılır: sayı doğruysa kesinleştirilir,
  değilse geri alınır.
- Güncellenecek satırlar bir alt sorguyla belirlenebilir; alt sorgu, deyimden önce tek
  başına çalıştırılarak sınanır.
- Silmenin bağlı satırlara etkisi yabancı anahtar tanımında yazılır; yayılan silme
  geçmişi de götürdüğü için, kayıtların değerli olduğu alanlarda kapatma tercih edilir.

#### Sonraki Adım

Buraya kadarki deyimler tabloların üzerinde doğrudan çalıştı. Sonraki ders araya bir
katman koyar: sorgunun kendisine ad vererek tablo gibi kullanılan görünümler. Görünüm
veriyi çoğaltmaz, sorguyu saklar — ve bu da ilginç bir soruyu doğurur: bir görünüme satır
eklenebilir mi, güncellenebilir mi. Yanıtı görünümün nasıl tanımlandığına ve motora
bağlıdır; o dersin bir bölümü bunu deneyerek gösterir.

### Görünümler

Source: https://academia.sh/tr/kurslar/sql-temelleri/gorunumler

Adlandırılmış sorgu olarak görünüm tanımı, karmaşıklığı gizleme ve erişimi daraltma işlevleri, güncellenebilirlik koşulları ve görünüm üzerinden yazmanın motora göre değişen davranışı.

Buraya kadarki deyimlerin hepsi tabloların üzerinde doğrudan çalıştı. Bu dersin konusu
araya giren bir katman: sorgunun kendisine ad vermek ve o adı tablo gibi kullanmak.

Bir kütüphane veritabanında aynı sorgu tekrar tekrar yazılır — açık ödünçler, askıdaki
üyeler, şube başına kitap sayısı. Sorgu her yazıldığında yeniden düşünülür, her
kopyasında ayrı hata yapılabilir ve şema değiştiğinde her kopya ayrı ayrı güncellenmek
zorunda kalır. **Görünüm (view)**, bu tekrarı ortadan kaldıran yapıdır.

#### Görünüm Adlandırılmış Bir Sorgudur

`CREATE VIEW` deyimi bir sorguya ad verir. Ad kullanıldığında motor, saklanan sorguyu
çalıştırır. Görünüm veri tutmaz; bu yüzden **sanal tablo** olarak da anılır.

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode column
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, soyad TEXT NOT NULL,
                  durum TEXT NOT NULL DEFAULT 'etkin');
INSERT INTO uye VALUES (1,'Ayşe','Demir','etkin'),(2,'Mehmet','Kaya','askida'),
                       (3,'Zeynep','Arslan','etkin');

CREATE VIEW etkin_uye AS
  SELECT uye_id, ad, soyad FROM uye WHERE durum = 'etkin';

SELECT * FROM etkin_uye;

UPDATE uye SET durum = 'etkin' WHERE uye_id = 2;
SELECT * FROM etkin_uye;
SQL
```

```
uye_id  ad      soyad 
------  ------  ------
1       Ayşe    Demir 
3       Zeynep  Arslan
uye_id  ad      soyad 
------  ------  ------
1       Ayşe    Demir 
2       Mehmet  Kaya  
3       Zeynep  Arslan
```

Görünümün tanımı arada hiç değişmedi; yalnızca temel tablodaki bir satırın durumu
değişti ve görünüm o değişikliği hemen yansıttı. Bu, görünüm ile kopya tablo arasındaki
temel farktır: kopya bir andaki durumu dondurur, görünüm sorgunun kendisini saklar.

Görünüm sorgulanırken tablo gibi davranır. `WHERE`, `ORDER BY`, birleştirme, hatta başka
bir görünüm içinde kullanım — hepsi geçerlidir. Sorgu yazan için görünüm ile tablo
arasında bir fark yoktur.

#### Görünümün İki İşi

Görünümler iki ayrı gereksinimi karşılar. Birincisi karmaşıklığı gizlemektir: birden çok
tabloyu birleştiren, gruplayan ve toplama işlevleri kullanan bir sorgu bir kez yazılır,
sonra tek bir adla anılır.

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode column
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, soyad TEXT 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 uye VALUES (1,'Ayşe','Demir'),(2,'Mehmet','Kaya'),(3,'Zeynep','Arslan');
INSERT INTO odunc VALUES
  (1,1,1,'2025-01-10','2025-01-24'),
  (2,2,1,'2025-02-02','2025-02-20'),
  (3,1,2,'2025-02-11',NULL),
  (9,7,1,'2025-05-14',NULL);

CREATE VIEW uye_ozeti AS
  SELECT u.uye_id, u.ad, u.soyad,
         count(o.odunc_id) AS toplam_odunc,
         sum(CASE WHEN o.odunc_id IS NOT NULL AND o.iade_tarihi IS NULL
                  THEN 1 ELSE 0 END) AS acik_odunc
  FROM uye AS u
  LEFT JOIN odunc AS o ON o.uye_id = u.uye_id
  GROUP BY u.uye_id, u.ad, u.soyad;

SELECT * FROM uye_ozeti ORDER BY uye_id;
SELECT ad, soyad FROM uye_ozeti WHERE acik_odunc > 0 ORDER BY uye_id;
SQL
```

```
uye_id  ad      soyad   toplam_odunc  acik_odunc
------  ------  ------  ------------  ----------
1       Ayşe    Demir   3             1         
2       Mehmet  Kaya    1             1         
3       Zeynep  Arslan  0             0         
ad      soyad
------  -----
Ayşe    Demir
Mehmet  Kaya 
```

İkinci sorgu, bir dış birleştirme ve gruplama yazmak yerine tek bir koşul yazdı.
Görünümün üzerine yazılan `WHERE` koşulu, saklanan sorgunun sonucuna uygulanır.

Toplama koşulundaki `o.odunc_id IS NOT NULL` parçası dış birleştirmenin bir sonucudur:
hiç ödüncü olmayan üye için birleştirme boş değerlerle bir satır üretir ve o satır, koşul
yalnızca iade tarihinin boşluğuna baksaydı açık ödünç sayılırdı. Görünüm, bu inceliği bir
kez doğru yazıp saklamanın da yoludur.

Görünümün ikinci işi erişimi daraltmaktır. `etkin_uye` görünümünü sorgulayan biri, üye
tablosundaki `durum` sütununu hiç görmez. Sütun ve satır düzeyinde ne gösterileceği
görünümün tanımında belirlenir; okuyucuya temel tablo yerine görünüm verilir. Bu, sonraki
dersin konusu olan yetkilendirmenin en çok kullanılan aracıdır.

#### Görünüme Yazmak

Görünüm tablo gibi sorgulanabiliyorsa, tablo gibi yazılabilir mi. Bu sorunun yanıtı hem
görünümün tanımına hem de motora bağlıdır.

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode column
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, soyad TEXT NOT NULL,
                  durum TEXT NOT NULL DEFAULT 'etkin');
INSERT INTO uye VALUES (1,'Ayşe','Demir','etkin'),(2,'Mehmet','Kaya','askida');

CREATE VIEW etkin_uye AS SELECT uye_id, ad, soyad FROM uye WHERE durum = 'etkin';

INSERT INTO etkin_uye (uye_id, ad, soyad) VALUES (3,'Zeynep','Arslan');
UPDATE etkin_uye SET soyad = 'Demir Kaya' WHERE uye_id = 1;
DELETE FROM etkin_uye WHERE uye_id = 1;

SELECT * FROM etkin_uye;
SQL
```

```
Parse error near line 9: cannot modify etkin_uye because it is a view
Parse error near line 10: cannot modify etkin_uye because it is a view
Parse error near line 11: cannot modify etkin_uye because it is a view
uye_id  ad    soyad
------  ----  -----
1       Ayşe  Demir
```

Kullanılan motorda görünümler salt okunurdur; üç yazma denemesi de aynı gerekçeyle
reddedildi. Bu, motora göre değişen bir karardır — pek çok motor belirli koşulları
sağlayan görünümlere doğrudan yazmaya izin verir.

Standart SQL bu koşulları **güncellenebilir görünüm** başlığı altında tanımlar. Ortak
çekirdeği şudur: görünümün her satırı, temel tablodaki tek bir satıra geri
izlenebilmelidir. Bunu bozan yazımlar, görünümü salt okunur yapar:

- Birden çok tablonun birleştirilmesi — bir satır iki tablodan geldiğinde, güncelleme
  hangisine yazılacağı belirsizdir.
- Gruplama ve toplama işlevleri — sonuçtaki bir satır birden çok temel satırı özetler.
- Tekrarların atılması ve küme işlemleri — satırlar arasındaki karşılık kaybolur.
- Sonuçta ifadeyle üretilen sütunlar — hesaplanmış bir sütuna yazılan değerin girdilere
  nasıl dağıtılacağı tanımsızdır.

Kalan durum — tek tablodan seçilen, gruplanmamış ve tekrarları atılmamış bir görünüm —
kural olarak güncellenebilirdir. Yukarıdaki `etkin_uye` bu tanıma uyar ve buna karşın
reddedildi: koşulun sağlanması, motorun desteklediği anlamına gelmez.

#### Yazılabilir Görünüm Kurmak

Doğrudan yazmayı desteklemeyen motorlarda, yazmanın ne anlama geldiği açıkça tanımlanır.
Görünüm üzerine kurulan bir tetikleyici, deyimin yerine geçer ve temel tabloya yazar.

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode column
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, soyad TEXT NOT NULL,
                  durum TEXT NOT NULL DEFAULT 'etkin');
INSERT INTO uye VALUES (1,'Ayşe','Demir','etkin'),(2,'Mehmet','Kaya','askida');

CREATE VIEW etkin_uye AS SELECT uye_id, ad, soyad FROM uye WHERE durum = 'etkin';

CREATE TRIGGER etkin_uye_ekle INSTEAD OF INSERT ON etkin_uye
BEGIN
  INSERT INTO uye (uye_id, ad, soyad, durum)
  VALUES (NEW.uye_id, NEW.ad, NEW.soyad, 'etkin');
END;

INSERT INTO etkin_uye (uye_id, ad, soyad) VALUES (3,'Zeynep','Arslan');
SELECT * FROM uye;
SQL
```

```
uye_id  ad      soyad   durum 
------  ------  ------  ------
1       Ayşe    Demir   etkin 
2       Mehmet  Kaya    askida
3       Zeynep  Arslan  etkin 
```

`INSTEAD OF` yazımı, adının söylediğini yapar: ekleme deyimi görünüme uygulanmaz, onun
yerine gövdedeki deyimler çalışır. `NEW`, eklenmeye çalışılan satırı adlandırır. Görünümde
olmayan `durum` sütununu tetikleyici doldurdu — görünüme yazmanın anlamı burada
tanımlanmış oldu.

Tetikleyicilerin ayrıntısı, örtük yan etkilerinin yarattığı sorunlar dahil, İleri SQL
kursuna aittir. Bu derste gösterilen, görünümü yazılabilir kılmanın standart olmayan
yolunun bu olduğudur.

#### Görünümden Kaybolan Satır

Yazılabilir bir görünümün kendine özgü bir tuzağı vardır. Görünüm satırları bir koşulla
süzüyorsa, o koşulu sağlamayan bir satır görünüm üzerinden eklenebilir — ve eklendikten
sonra görünümde görünmez.

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode column
CREATE TABLE kitap (
  kitap_id INTEGER PRIMARY KEY,
  baslik   TEXT NOT NULL,
  yazar    TEXT NOT NULL,
  sube_id  INTEGER NOT NULL
);
INSERT INTO kitap VALUES (1,'Körlük','José Saramago',1);

CREATE VIEW merkez_kitap AS
  SELECT kitap_id, baslik, yazar, sube_id FROM kitap WHERE sube_id = 1;

CREATE TRIGGER merkez_kitap_ekle INSTEAD OF INSERT ON merkez_kitap
BEGIN
  INSERT INTO kitap (kitap_id, baslik, yazar, sube_id)
  VALUES (NEW.kitap_id, NEW.baslik, NEW.yazar, NEW.sube_id);
END;

INSERT INTO merkez_kitap VALUES (5,'Sessiz Ev','Orhan Pamuk',3);

SELECT * FROM merkez_kitap;
SELECT * FROM kitap;
SQL
```

```
kitap_id  baslik  yazar          sube_id
--------  ------  -------------  -------
1         Körlük  José Saramago  1      
kitap_id  baslik     yazar          sube_id
--------  ---------  -------------  -------
1         Körlük     José Saramago  1      
5         Sessiz Ev  Orhan Pamuk    3      
```

Ekleme başarılı oldu, satır temel tabloya yazıldı — ama merkez şubenin kitaplarını
gösteren görünümde yok. Görünümü kullanan biri, az önce eklediği satırı bir daha göremez.

Standart SQL bu durum için görünüm tanımına eklenen bir seçenek tanımlar: görünüm
üzerinden yazılan satır, görünümün koşulunu sağlamak zorunda tutulur. Bu seçenek de
evrensel değildir.

```sh
sqlite3 :memory: <<'SQL'
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, durum TEXT NOT NULL);
CREATE VIEW etkin_uye AS SELECT uye_id FROM uye WHERE durum = 'etkin' WITH CHECK OPTION;
SQL
```

```
Parse error near line 2: near "WITH": syntax error
  e AS SELECT uye_id FROM uye WHERE durum = 'etkin' WITH CHECK OPTION;
                                      error here ---^
```

Standartta yazan bir cümle, kullanılan motorda ayrıştırılamadı bile. Bu davranışın motora
göre değiştiğini bilmek, taşınabilir şema yazmanın gereğidir: görünüm koşulunun
korunması gerekiyorsa ve motor bu seçeneği tanımıyorsa, denetim tetikleyicinin gövdesinde
elle yazılır.

#### Özet

- Görünüm veri tutmaz, sorguyu saklar; temel tablodaki her değişikliği anında yansıtır ve
  sorgulanırken tablo gibi davranır.
- Görünümün iki işi vardır: birleştirme ve gruplama karmaşıklığını tek adın arkasına
  almak ve okuyucuya temel tablonun yalnızca bir bölümünü göstermek.
- Bir görünümün güncellenebilir olması için her satırının tek bir temel satıra geri
  izlenebilmesi gerekir; birleştirme, gruplama, tekrar atma ve hesaplanmış sütun bunu
  bozar.
- Koşulları sağlayan bir görünümün yazılabilir olması motora bağlıdır; desteklenmediği
  yerde yazmanın anlamı `INSTEAD OF` tetikleyicisiyle açıkça tanımlanır.
- Süzen bir görünüme koşulu sağlamayan satır yazılabilir ve o satır görünümde görünmez;
  standardın bunu engelleyen seçeneği her motorda bulunmaz.

#### Sonraki Adım

Görünümün ikinci işi — okuyucuya temel tablo yerine daraltılmış bir pencere vermek — yarım
kalmış bir düşüncedir: görünüm sunulsa bile, temel tablonun kendisi hâlâ ortadadır. Onu
kimin görebileceğini söyleyen ayrı bir mekanizma gerekir. Kursun son dersi nesne düzeyi
izinleri ele alır: yetkinin kime, hangi nesne üzerinde, hangi işlem için verildiği; bu
yetkinin geri alınması; ve verilen izinlerin işin gerektirdiğinden fazla olup olmadığının
sayılması.

### Yetki Verme ve Alma

Source: https://academia.sh/tr/kurslar/sql-temelleri/yetki-verme-ve-alma

Nesne düzeyi izinlerin standart SQL yazımı, yetkili-nesne-izin üçlüsü, aktarım seçeneği ve rol kavramı; en az yetki ilkesinin bir yetki matrisi üzerinde ölçülmesi.

Önceki ders görünümün ikinci işini yarım bıraktı. Görünüm okuyucuya temel tablonun
yalnızca bir bölümünü gösterebilir — ama gösterebilmesi, temel tablonun gizlendiği
anlamına gelmez. Görünümü sorgulayan biri, isterse aynı bağlantıda temel tabloyu da
sorgular. Eksik olan, kimin neyi görebileceğini söyleyen ayrı bir mekanizmadır.

SQL Dil Aileleri dersinde ayrılan beş aileden dördüncüsü — veri denetim dili — tam olarak
bunu yazar. Deyimleri kısadır, kavramları ise şema tasarımı kadar sonuç doğurur: bir
sistemdeki en pahalı hataların bir bölümü, gereğinden geniş verilmiş izinlerden çıkar.

#### Yetki Modelinin Parçaları

Nesne düzeyi yetkilendirme üç şeyi birbirine bağlar.

**Yetkili**, izni alan taraftır: bir kullanıcı hesabı, bir rol ya da "herkes" anlamına
gelen genel bir ad. **Nesne**, iznin üzerinde tanımlandığı şeydir: tablo, görünüm, sütun,
şema ya da veritabanının kendisi. **İzin**, o nesnede yapılabilecek işlemi adlandırır:
okuma, ekleme, güncelleme, silme, başvurma, tanım değiştirme.

Bu üçlünün her bir bileşimi ayrı bir yetkidir. Bir tabloda okuma izni olan bir yetkilinin
aynı tabloda yazma izni yoktur; bunun için ayrıca verilmesi gerekir.

Yetkiyi kim verir sorusunun standart yanıtı nesnenin sahibidir: bir tabloyu yaratan
taraf, o tablo üzerinde bütün izinlere ve bu izinleri başkalarına verme hakkına baştan
sahiptir.

#### Yetki Vermek ve Geri Almak

Standart SQL'de izinler `GRANT` ile verilir, `REVOKE` ile geri alınır.

```sql
GRANT SELECT, INSERT, UPDATE ON odunc TO gorevli;
GRANT SELECT ON uye_ozeti TO rapor;
GRANT SELECT (uye_id, ad, soyad) ON uye TO rapor;
GRANT SELECT ON kitap TO katalog WITH GRANT OPTION;

REVOKE DELETE ON odunc FROM gorevli;
REVOKE ALL PRIVILEGES ON uye FROM rapor CASCADE;
```

Deyimler okunduğu gibi çalışır: hangi izinler, hangi nesne üzerinde, kime. İkinci satırdaki
nesne bir tablo değil, önceki derste tanımlanan görünümdür — izin, tablolarda olduğu gibi
görünümler üzerinde de verilir. Üçüncü satır izni sütun düzeyinde daraltır: yetkili
tabloyu sorgulayabilir ama yalnızca sayılan sütunları görür, e-posta adresine erişemez.
Dördüncü satırdaki **aktarım seçeneği**, yetkiliye aldığı izni başkalarına verme hakkını
da tanır.

Geri almanın iki inceliği vardır. `ALL PRIVILEGES`, o nesnede yetkiliye verilmiş bütün
izinleri kapsar. `CASCADE` ise aktarım seçeneğiyle verilmiş bir iznin dallanmasını
izler: yetkili o izni başkalarına dağıttıysa, geri alma o dağıtımları da kaldırır. Bu
seçenek yazılmazsa ve dallanma varsa, standart geri almayı reddeder — çünkü sessizce
bırakılan bir dal, geri alındığı sanılan bir izin demektir.

Aktarım seçeneği bu yüzden ölçülü kullanılır. Bir izni kimin dağıtabileceği,
dağıtılabilecek izinlerin kendisinden daha önemli bir karardır: aktarım hakkı verilen her
yetkili, yetki matrisini tek başına genişletebilecek bir noktadır.

Rol kavramı, aynı sorunu daha yönetilebilir kılar. İzinler tek tek kullanıcılara değil
role verilir, kullanıcılar role bağlanır. Bir görev tanımı değiştiğinde tek bir rolün
izinleri düzenlenir; işe yeni başlayan biri role bağlanarak bütün izinleri devralır. Rol
olmadan, izinler kullanıcı sayısıyla çarpılır ve kimsenin tam olarak neye erişebildiği
izlenemez olur.

Bu deyimlerin **her motorda bulunmadığı** vurgulanmalıdır. Kullanıcı hesabı kavramı
olmayan motorlar vardır; bu dersin komut satırı aracı da böyledir ve `GRANT` deyimini
ayrıştıramaz. Erişim denetimi orada dosya sistemi izinlerine ve uygulama katmanına
bırakılır. Yetkilendirmenin nerede yapıldığı, motor seçilirken sorulacak sorulardandır.

#### En Az Yetki İlkesini Ölçmek

Linux'a Giriş kursunda tanıtılan en az yetki ilkesi burada da geçerlidir: her yetkiliye,
işini yapmak için gereken izinlerden fazlası verilmez. İlkenin uygulandığı iddia edilmez,
**sayılır**. Verilen izinler ve işin gerektirdiği izinler iki küme olarak yazılırsa, fark
doğrudan hesaplanabilir.

Aşağıdaki betik kütüphane veritabanı için bu iki kümeyi kurar, matrisi yazdırır ve
farkları sayar. Sonra fazla verilmiş izinleri geri alıp matrisi yeniden çıkarır.

```sh
cat > yetki.mjs <<'EOF'
// Nesne duzeyi yetki matrisini kurar ve en az yetki ilkesinin ihlallerini sayar.
const izinler  = ["SELECT", "INSERT", "UPDATE", "DELETE"];
const nesneler = ["uye", "kitap", "odunc", "uye_ozeti"];

// GRANT ile verilmis yetkiler: "yetkili nesne izin"
const verilen = new Set([
  "gorevli uye SELECT",   "gorevli uye UPDATE",   "gorevli kitap SELECT",
  "gorevli odunc SELECT", "gorevli odunc INSERT",
  "gorevli odunc UPDATE", "gorevli odunc DELETE", "gorevli uye_ozeti SELECT",
  "rapor uye SELECT",     "rapor kitap SELECT",   "rapor odunc SELECT",
  "rapor odunc DELETE",   "rapor uye_ozeti SELECT",
  "katalog kitap SELECT", "katalog kitap INSERT", "katalog kitap UPDATE",
]);

// Isin gerektirdigi yetkiler.
const gereken = new Set([
  "gorevli uye SELECT",   "gorevli uye UPDATE",   "gorevli kitap SELECT",
  "gorevli odunc SELECT", "gorevli odunc INSERT",
  "gorevli odunc UPDATE", "gorevli uye_ozeti SELECT",
  "rapor uye_ozeti SELECT",
  "katalog kitap SELECT", "katalog kitap INSERT", "katalog kitap UPDATE",
]);

const yetkililer = [...new Set([...verilen].map((k) => k.split(" ")[0]))];

function raporla(baslik, kume) {
  console.log(baslik);
  console.log(("  yetkili  nesne      " + izinler.map((i) => i.padEnd(7)).join("")).trimEnd());
  for (const y of yetkililer) {
    for (const n of nesneler) {
      const hucre = izinler.map((i) => {
        const v = kume.has(`${y} ${n} ${i}`);
        const g = gereken.has(`${y} ${n} ${i}`);
        return (v ? (g ? "var" : "FAZLA") : g ? "EKSIK" : "-").padEnd(7);
      });
      console.log(("  " + y.padEnd(9) + n.padEnd(11) + hucre.join("")).trimEnd());
    }
  }
  const fazla = [...kume].filter((k) => !gereken.has(k));
  const eksik = [...gereken].filter((k) => !kume.has(k));
  console.log(`  fazla verilmis yetki: ${fazla.length}`);
  for (const k of fazla) console.log("    " + k);
  console.log(`  eksik yetki: ${eksik.length}`);
  return fazla;
}

const fazla = raporla("== REVOKE oncesi ==", verilen);
const sonra = new Set([...verilen].filter((k) => !fazla.includes(k)));
raporla("== REVOKE sonrasi ==", sonra);
EOF
node yetki.mjs
```

```
== REVOKE oncesi ==
  yetkili  nesne      SELECT INSERT UPDATE DELETE
  gorevli  uye        var    -      var    -
  gorevli  kitap      var    -      -      -
  gorevli  odunc      var    var    var    FAZLA
  gorevli  uye_ozeti  var    -      -      -
  rapor    uye        FAZLA  -      -      -
  rapor    kitap      FAZLA  -      -      -
  rapor    odunc      FAZLA  -      -      FAZLA
  rapor    uye_ozeti  var    -      -      -
  katalog  uye        -      -      -      -
  katalog  kitap      var    var    var    -
  katalog  odunc      -      -      -      -
  katalog  uye_ozeti  -      -      -      -
  fazla verilmis yetki: 5
    gorevli odunc DELETE
    rapor uye SELECT
    rapor kitap SELECT
    rapor odunc SELECT
    rapor odunc DELETE
  eksik yetki: 0
== REVOKE sonrasi ==
  yetkili  nesne      SELECT INSERT UPDATE DELETE
  gorevli  uye        var    -      var    -
  gorevli  kitap      var    -      -      -
  gorevli  odunc      var    var    var    -
  gorevli  uye_ozeti  var    -      -      -
  rapor    uye        -      -      -      -
  rapor    kitap      -      -      -      -
  rapor    odunc      -      -      -      -
  rapor    uye_ozeti  var    -      -      -
  katalog  uye        -      -      -      -
  katalog  kitap      var    var    var    -
  katalog  odunc      -      -      -      -
  katalog  uye_ozeti  -      -      -      -
  fazla verilmis yetki: 0
  eksik yetki: 0
```

Beş ihlal çıktı ve her biri ayrı bir gerekçeyle açıklanabilir.

`gorevli odunc DELETE` izni, bir önceki dersin sonucudur: ödünç kaydı silmek geçmişi yok
eder, iade işlemi ise kaydı güncelleyerek yapılır. Görevlinin silme iznine gereksinimi
yoktur.

`rapor` yetkilisinin üç temel tabloda okuma izni ise daha ilginçtir. Raporlama işinin
gerçekten okumaya gereksinimi vardır — ama temel tablolara değil, `uye_ozeti`
görünümüne. İzin görünümde verildiğinde raporlama yine çalışır, üye tablosundaki
e-posta adresleri ise erişilebilir olmaktan çıkar. Önceki dersin yarım kalan düşüncesi
burada tamamlanır: **görünüm neyin gösterileceğini tanımlar, yetki onu bağlayıcı kılar.**
İkisi ayrı ayrı eksiktir.

`rapor odunc DELETE` izni ise hiçbir gerekçeyle açıklanamaz; büyük bir olasılıkla bir
kopyalama artığıdır. Yetki matrisleri elle büyüdükçe bu tür artıklar birikir ve
kimsenin farkına varmadığı bir yazma yolu bırakır.

Geri alma sonrasındaki ikinci matriste hem fazla hem eksik yetki sıfırdır: her yetkilinin
izinleri işinin gerektirdiğine eşittir. Bu sayının düzenli olarak hesaplanması, ilkeye
uyulduğunu iddia etmekten farklı bir şeydir.

#### Özet

- Nesne düzeyi yetkilendirme yetkili, nesne ve izin üçlüsünü bağlar; her bileşim ayrı bir
  yetkidir ve ayrıca verilmesi gerekir.
- `GRANT` izin verir, `REVOKE` geri alır; izin sütun düzeyinde daraltılabilir, aktarım
  seçeneğiyle genişletilebilir ve geri alma dallanmayı izleyecek biçimde yazılabilir.
- Roller izinleri kullanıcılardan ayırır; görev tanımı değiştiğinde tek bir rolün izinleri
  düzenlenir ve erişim izlenebilir kalır.
- Görünüm neyin gösterileceğini tanımlar, yetki onu bağlayıcı kılar; erişim daraltmak
  için ikisi birlikte kullanılır.
- En az yetki ilkesi iddia edilmez, sayılır: verilen izinler ile işin gerektirdiği
  izinlerin farkı hesaplanabilir bir büyüklüktür.

#### Kurs Kapanışı

Bu kurs SQL'in temel deyimlerini kurdu. Sorgulama konusunda `SELECT` yapısı, koşullar,
sıralama, boş değerlerle çalışma ve yerleşik işlevler ele alındı. Birleştirmeler konusu
tabloları iç ve dış birleştirmelerle bir araya getirdi, toplama işlevleri ve gruplamayla
özet üretti, küme işlemleriyle sonuç kümelerini birleştirdi. Bu konu ise okumadan yazmaya
geçti: şemanın tanımlanması ve kısıtlarla korunması, şemanın değiştirilmesi, satır
ekleme, güncelleme ve silmenin riskleri, görünümler ve nesne düzeyi izinler.

Kurs boyunca tek bir örnek sürdü — şubeleri, üyeleri, kitapları ve ödünç işlemleri olan
bir kütüphane veritabanı. Aynı şema üzerinde hem sorgular hem yazma deyimleri yazıldı;
kısıtların neyi engellediği, koşulsuz bir güncellemenin ne yaptığı ve bir görünümün
neyi gizleyip neyi gizlemediği aynı tablolarda görüldü.

Sıradaki kurs İleri SQL, buradaki deyimleri üç yönde derinleştirir. Bileşik sorgular
konusu alt sorguları, ortak tablo ifadelerini, özyinelemeli sorguları ve pencere
işlevlerini ele alır — bu kursta alt sorgu yalnızca koşul yazmak için kullanıldı, orada
kendi başına bir yapı olur. İşlemler konusu, bu kursta bir alışkanlık olarak kullanılan
`BEGIN` ve `ROLLBACK` çiftinin altındaki mekanizmayı açar: kaydetme noktaları, yalıtım
düzeyleri, kilitleme ve kilitlenme. Sorgu başarımı konusu ise dizinleri, sorgu planını
okumayı ve planı bozan yazımları ele alır — bu kursta ölçülen ekleme farkının yanına,
okuma tarafının maliyet modeli eklenir.

