---
title: 'Veri Modelleme ve İlişkisel Kuram'
source: 'https://academia.sh/tr/kurslar/iliskisel-kuram'
language: tr
updated: '2026-08-17T04:40:26+00:00'
license: 'CC BY-SA 4.0'
lessons: 14
outcomes:
    - 'İlişkisel modelin temel kavramlarını tanımlar'
    - 'Bir alanı normalleştirilmiş şemaya çevirir'
    - 'Bütünlük kısıtlarını şemada ifade eder'
---

# Veri Modelleme ve İlişkisel Kuram

Dosyada tutulan veriden ilişkisel şemaya geçişi, anahtar ve bütünlük kısıtlarını, boş değerlerin mantığını, normalizasyonu ve veri işleme biçimlerinin seçimini kuran başlangıç kursu.

## İlişkisel Model

Dosyadan veritabanına geçişin gerekçesi, bağıntı-satır-sütun-alan kavramları, anahtar türleri, bütünlük kısıtları, boş değerlerin üç değerli mantığı ve veri tipi seçimi.

### Veritabanı Nedir

Source: https://academia.sh/tr/kurslar/iliskisel-kuram/veritabani-nedir

Verinin dosyada tutulmasının yarattığı arama, eşzamanlılık ve bütünlük sorunları; veritabanı yönetim sisteminin bunlardan hangilerini hangi güvencelerle karşıladığı.

Veri Yapıları kursu, verinin bellekte nasıl düzenlendiğini ve bu düzenin işlem maliyetini
nasıl belirlediğini kurdu: dizi, karma tablosu, ikili arama ağacı. Linux'a Giriş ve Kabuk
Programlama kursları, aynı veri diskte bir metin dosyasında dururken onu süzmeyi, kesmeyi
ve saymayı öğretti. Node.js kursunda bir dosya okundu, ayrıştırıldı ve program içinde
kullanıldı. Üç kursun ortak varsayımı şuydu: veri ya bellektedir ya da tek bir dosyadadır
ve ona erişen tek bir program vardır.

Bu müfredat o varsayımı kaldırır. Sorusu şudur: veri bir dosyada tutulduğunda hangi
sorunlar çıkar ve veritabanı bunların hangisini çözer? Kurs boyunca tek bir alan
kullanılacak — bir kütüphanenin ödünç kayıtları: kitaplar, üyeler, şubeler ve ödünç
işlemleri. Bir kütüphane kataloğunun arayüzü ne kadar iyi tasarlanırsa tasarlansın,
arkasındaki veri yanlış modellenmişse arayüz de yanlış yanıt verir.

#### Dosyadaki Kayıt

Kütüphane, ödünç işlemlerini virgülle ayrılmış bir metin dosyasında tutuyor olsun. Kabuk
araçları bu dosyayla iş görmeye yeter:

```sh
cat > odunc.csv <<'CSV'
odunc_no,uye_ad,uye_eposta,kitap_baslik,kitap_yazar,sube,alis_tarihi,iade_tarihi
1001,Ayse Demir,ayse.demir@ornek.org,Kayip Zaman,Elif Toprak,Merkez,2025-03-02,2025-03-16
1002,Mert Kaya,mert.kaya@ornek.org,Kayip Zaman,Elif Toprak,Merkez,2025-03-04,
1003,Ayse Demir,ayse.demir@ornek.org,Deniz Fenerleri,Kemal Aras,Bahcelievler,2025-03-05,2025-03-19
1004,Ayse Demir,ayse.demir@ornek.org,Kayip Zaman,Elif Toprak,merkez,2025-03-20,
1005,Selin Ozturk,selin.ozturk@ornek.org,Deniz Fenerleri,Kemal Aras,Merkez,2025-03-21,
1006,Mert Kaya,mert.kaya@ornek.org,Sessiz Bahce,Elif Toprak,Bahcelievler,2025-03-22,2025-04-01
CSV

awk -F, 'NR > 1 && $8 == "" { print $2, $4 }' odunc.csv
```

```
Mert Kaya Kayip Zaman
Ayse Demir Kayip Zaman
Selin Ozturk Deniz Fenerleri
```

İade edilmemiş kitapların listesi çıktı. Dosya küçükken bu yeterlidir. Sorunlar dosya
büyüdüğünde değil, **soru değiştiğinde** ve **dosyaya erişen ikinci bir program
belirdiğinde** başlar.

#### Sorunun Yazılması

"Hangi kitap kaç kez ödünç verildi ve kaç farklı üye aldı?" sorusunu düşünelim. Kabuk
araçlarıyla bu, birden çok geçiş, bir sıralama ve elle tutulan bir sayaç demektir. Aynı
veri bir veritabanına konduğunda soru, çözümün nasıl hesaplanacağını söylemeden yazılır:

```sh
cat > odunc.csv <<'CSV'
odunc_no,uye_ad,uye_eposta,kitap_baslik,kitap_yazar,sube,alis_tarihi,iade_tarihi
1001,Ayse Demir,ayse.demir@ornek.org,Kayip Zaman,Elif Toprak,Merkez,2025-03-02,2025-03-16
1002,Mert Kaya,mert.kaya@ornek.org,Kayip Zaman,Elif Toprak,Merkez,2025-03-04,
1003,Ayse Demir,ayse.demir@ornek.org,Deniz Fenerleri,Kemal Aras,Bahcelievler,2025-03-05,2025-03-19
1004,Ayse Demir,ayse.demir@ornek.org,Kayip Zaman,Elif Toprak,merkez,2025-03-20,
1005,Selin Ozturk,selin.ozturk@ornek.org,Deniz Fenerleri,Kemal Aras,Merkez,2025-03-21,
1006,Mert Kaya,mert.kaya@ornek.org,Sessiz Bahce,Elif Toprak,Bahcelievler,2025-03-22,2025-04-01
CSV

sqlite3 :memory: <<'SQL'
.mode csv
.import odunc.csv odunc_ham
.headers on
.mode box
SELECT kitap_baslik,
       COUNT(*)                    AS odunc_sayisi,
       COUNT(DISTINCT uye_eposta)  AS uye_sayisi
FROM odunc_ham
GROUP BY kitap_baslik
ORDER BY odunc_sayisi DESC;
SQL
```

```
┌─────────────────┬──────────────┬────────────┐
│  kitap_baslik   │ odunc_sayisi │ uye_sayisi │
├─────────────────┼──────────────┼────────────┤
│ Kayip Zaman     │ 3            │ 2          │
│ Deniz Fenerleri │ 2            │ 2          │
│ Sessiz Bahce    │ 1            │ 1          │
└─────────────────┴──────────────┴────────────┘
```

Aradaki fark söz diziminden ibaret değildir. Kabuk çözümünde **nasıl** hesaplanacağı
yazılır; ikincisinde
**ne** istendiği yazılır. Buna **bildirimsel sorgulama** (declarative querying) denir:
istenen sonuç tanımlanır, sonuca hangi sırayla ve hangi yöntemle varılacağına sistem karar
verir. Aynı soru bin kayıtta da on milyon kayıtta da aynı biçimde yazılır; değişen, sistemin
seçtiği yürütme planıdır.

#### Aynı Anda İki Yazan

Asıl kırılma, dosyaya iki program aynı anda yazdığında görülür. Ödünç verme işlemi üç
adımdır: durumu oku, nüshanın boşta olduğunu doğrula, kaydı ekle. İki gişe aynı nüshayı
aynı anda verirse ne olur?

```sh
cat > odunc_ver.mjs <<'JS'
import { readFile, writeFile } from "node:fs/promises";
import { setTimeout as bekle } from "node:timers/promises";

const dosya = "durum.json";
const uye = process.argv[2];

const metin = await readFile(dosya, "utf8");
const durum = JSON.parse(metin);

if (durum.oduncte.includes(7)) {
  console.log(`${uye}: 7 numarali nusha zaten oduncte`);
} else {
  await bekle(50);                       // disk ve ag gecikmesinin yerine
  durum.oduncte.push(7);
  durum.kayit.push({ nusha_no: 7, uye });
  await writeFile(dosya, JSON.stringify(durum));
  console.log(`${uye}: 7 numarali nusha verildi`);
}
JS

echo '{"oduncte": [], "kayit": []}' > durum.json
node odunc_ver.mjs ayse & node odunc_ver.mjs mert & wait
cat durum.json
```

```
mert: 7 numarali nusha verildi
ayse: 7 numarali nusha verildi
{"oduncte":[7],"kayit":[{"nusha_no":7,"uye":"mert"}]}
```

İki satırın sırası ve dosyada kalan kaydın hangisi olduğu çalıştırmadan çalıştırmaya
değişir; sabit olan iki şey vardır. Birincisi: her iki süreç de işlemi başarıyla
tamamladığını bildirir, yani tek nüsha iki üyeye verilmiştir. İkincisi: dosyada tek bir
kayıt kalmıştır — sonra yazan, öncekinin eklediği kaydı da silmiştir. Buna **kayıp
güncelleme** (lost update) denir.

Okuma ile yazma arasındaki bekleme, gecikmeyi belirlenimci kılmak için konmuştur. Gerçek
bir sistemde o gecikmeyi disk erişimi, ağ turu veya işletim sisteminin süreç değiştirmesi
üretir; kaldırıldığında sorun ortadan kalkmaz, yalnızca daha seyrek görünür hâle gelir.

#### Kısıtın Yazılmamış Olması

Dosyanın söylemediği başka şeyler de vardır. `1004` numaralı kayıtta şube adı `merkez`,
diğerlerinde `Merkez` yazılmıştır; kabuk araçları bunları iki ayrı şube sayar. Aynı üye
adı üç satırda tekrar eder — üyenin e-posta adresi değişirse üç satırın da düzeltilmesi
gerekir, biri unutulursa dosya kendi içinde çelişir. Hiçbir satır, "bir nüsha aynı anda
yalnız bir üyede olabilir" kuralını taşımaz; bu kural yalnızca ona yazan programların
kafasındadır.

Veritabanı yönetim sisteminde kural, veriyle birlikte saklanır ve ihlali motor
reddeder:

```sh
sqlite3 :memory: <<'SQL'
CREATE TABLE odunc (
  odunc_no    INTEGER PRIMARY KEY,
  nusha_no    INTEGER NOT NULL,
  uye_no      INTEGER NOT NULL,
  alis_tarihi TEXT NOT NULL,
  iade_tarihi TEXT
);
CREATE UNIQUE INDEX odunc_acik ON odunc (nusha_no) WHERE iade_tarihi IS NULL;
INSERT INTO odunc VALUES (1001, 7, 41, '2025-03-02', NULL);
INSERT INTO odunc VALUES (1002, 7, 52, '2025-03-04', NULL);
SQL
```

```
Runtime error near line 10: UNIQUE constraint failed: odunc.nusha_no (19)
```

Hata iletisinin biçimi motora göre değişir; değişmeyen, ikinci eklemenin
gerçekleşmemesidir. Kuralı uygulayan artık uygulama kodu değil, veriyi saklayan sistemin
kendisidir. Bu ayrım kursun ekseni olacak: bir kuralı **şemada** ifade etmek ile onu her
yazan programda tekrar etmek arasındaki fark.

#### Veritabanı Yönetim Sisteminin Verdikleri

**Veritabanı yönetim sistemi** (database management system), veriyi saklayan ve ona
erişimi düzenleyen yazılımdır. Verdiği güvenceler dört başlıkta toplanır:

- **Bildirimsel erişim.** Sorgu ne istendiğini söyler; erişim yolunu, dizin kullanımını ve
  birleştirme sırasını sistem seçer.
- **Bütünlük.** Şemaya yazılan kısıtlar her yazma girişiminde denetlenir; hangi programın
  yazdığı fark etmez.
- **Eşzamanlılık denetimi.** Aynı anda çalışan işlemler birbirinin ortasını görmez;
  kayıp güncelleme motorun sorumluluğundadır.
- **Kalıcılık.** Tamamlandığı bildirilen bir iş, sistem o anda çökse bile veride kalır;
  yarım kalan iş hiç yapılmamış gibi geri alınır.

Son iki madde birlikte, kursun son konusunda tanımlanacak olan işlem kavramını verir.
Bütünlük ise hemen sıradaki derslerin konusudur.

#### Dosyanın Doğru Seçim Olduğu Yerler

Veritabanı her sorunun karşılığı değildir. Yapılandırma dosyaları, tek yazanı olan kütükler,
bir kereye mahsus çözümleme için indirilen ham veri, sürüm denetimine girmesi gereken metin
— bunların dosyada durması yerindedir. Ayırt edici ölçüt boyut değil, şu üç sorudur: veriye
aynı anda birden çok yazan var mı, veri üzerinde uygulanması gereken kurallar var mı ve
verinin yarım yazılması kabul edilebilir mi? Üçüne de "hayır" yanıtı veriliyorsa dosya
yeterlidir.

#### Özet

- Dosya tabanlı saklama, veri büyüdüğünde değil, soru çeşitlendiğinde ve ikinci bir yazan
  belirdiğinde yetersiz kalır.
- Bildirimsel sorgulama, istenen sonucu hesaplama yönteminden ayırır; yürütme kararını
  sistem verir.
- Aynı dosyaya eşzamanlı okuma–değiştirme–yazma yapan iki süreç kayıp güncelleme üretir ve
  ikisi de başarı bildirir.
- Şemaya yazılan bir kısıt, veriye erişen bütün programlar için geçerlidir; uygulama kodunda
  tekrarlanan bir denetim yalnızca onu yazan program için geçerlidir.
- Veritabanı yönetim sisteminin dört güvencesi bildirimsel erişim, bütünlük, eşzamanlılık
  denetimi ve kalıcılıktır.

#### Sonraki Adım

Bu ders, kuralların veriyle birlikte saklanması gerektiğini gösterdi ama kuralların neyin
üzerine yazıldığını tanımlamadı. `odunc.csv` dosyasındaki satırlar ile veritabanındaki
satırlar arasında, virgül kullanımından daha derin bir fark vardır: birinin altında
matematiksel bir yapı durur, diğerinin altında yalnızca bir biçimlendirme sözleşmesi. Sıradaki
ders o yapıyı — bağıntı, satır, sütun ve alan kavramlarını — tanımlar ve tablonun neden
sıralı bir liste olmadığını gösterir.

### İlişkisel Model

Source: https://academia.sh/tr/kurslar/iliskisel-kuram/iliskisel-model

Bağıntı, satır, sütun ve alan kavramlarının tanımı; satır sırasının ve satır yinelemesinin modeldeki yeri ile SQL tablosunun bağıntıdan ayrıldığı noktalar.

Önceki ders, bir metin dosyasındaki satırlar ile veritabanındaki satırlar arasında
biçimlendirmeden daha derin bir fark olduğunu söyleyerek bitti. Bu dersin sorusu o farkı
adlandırır: bir tablonun altında hangi matematiksel yapı durur ve bu yapı tabloya bakarken
gördüğümüz hangi ayrıntıların **veriye ait olmadığını** söyler?

Yanıt tek bir kavramda toplanır. İlişkisel modelde veri, tablo gibi görünen ama tablo
olmayan bir nesnede tutulur: **bağıntı** (relation). Bu dersteki her kural, bağıntının
küme kuramından gelen tanımının doğrudan sonucudur.

#### Alan

Bir sütunun alabileceği değerlerin kümesine o sütunun **alanı** (domain) denir. Şubenin
bulunduğu il bir metin alanından, kitabın yayın yılı bir tam sayı alanından, kitabın
ödünç verilebilirliği iki elemanlı bir doğruluk alanından değer alır.

Alan, tipten daha dardır. Yayın yılının tipi tam sayıdır; alanı ise "1450 ile 2100
arasındaki tam sayılar" olabilir. Tip, değerin nasıl saklandığını; alan, hangi değerlerin
anlamlı olduğunu söyler. Şemada alan, bir **kısıt** (constraint) olarak yazılır:

```sh
sqlite3 :memory: <<'SQL'
CREATE TABLE kitap (
  isbn       TEXT NOT NULL,
  baslik     TEXT NOT NULL,
  yayin_yili INTEGER NOT NULL CHECK (yayin_yili BETWEEN 1450 AND 2100)
);
INSERT INTO kitap VALUES ('978-975-0000-01-1', 'Kayıp Zaman', 2019);
INSERT INTO kitap VALUES ('978-975-0000-02-8', 'Deniz Fenerleri', 20199);
SQL
```

```
Runtime error near line 7: CHECK constraint failed: yayin_yili BETWEEN 1450 AND 2100 (19)
```

Parmağın kaydığı bir eklemede yıl `20199` olarak yazılmıştır ve motor onu geri çevirir.
Alanı şemada tanımlamamak, bu denetimi veriye yazan her programın ayrı ayrı yapmasını
beklemek demektir.

#### Bağıntı

$n$ tane alan $D_1, D_2, \dots, D_n$ verildiğinde, bunların **kartezyen çarpımı** her
bileşeni ilgili alandan seçilmiş bütün $n$'li sıralı listelerin kümesidir. Bir bağıntı,
bu çarpımın bir alt kümesidir:

$$
R \subseteq D_1 \times D_2 \times \dots \times D_n
$$

Kartezyen çarpım, "yazılabilecek her şey"i verir; bağıntı, bunlardan "gerçekten doğru
olanlar"ı seçer. Şube bağıntısında `('MRK', 'Merkez', 'Ankara')` üçlüsü bulunuyorsa, bu
kütüphanede MRK kodlu, Merkez adlı, Ankara'daki bir şubenin var olduğu bildirilmiştir.
Bağıntıda bulunmayan üçlüler ise yanlış sayılır. Bir tablo, bir olgu listesidir.

Bağıntının iki parçası vardır. **Başlık** (heading), öznitelik adı ile alan
eşleşmelerinin kümesidir ve zamanla değişmez. **Gövde** (body), başlığa uyan satırların
kümesidir ve her eklemede, silmede, güncellemede değişir. Başlıktaki öznitelik sayısına
bağıntının **derecesi** (degree), gövdedeki satır sayısına **kardinalitesi** (cardinality)
denir.

#### Satır ve Sütun

Gövdedeki her eleman bir **satır**, kuramsal adıyla bir **demettir** (tuple). Satır,
öznitelik adlarından değerlere giden bir eşlemedir: `sube_kodu` özniteliğine `'MRK'`,
`ad` özniteliğine `'Merkez'` karşılık gelir.

Başlıktaki her öğe bir **sütundur** ve bir **özniteliğe** (attribute) karşılık gelir.
Sütun bir ada ve bir alana sahiptir; sıra numarasına değil. `sube` bağıntısında ikinci
sütun diye bir kavram yoktur, `ad` sütunu vardır.

Buradan iki sonuç doğar ve ikisi de kullanımda sık unutulur.

#### Satırların Sırası Veriye Ait Değildir

Gövde bir kümedir; kümenin elemanları sıralı değildir. Bir sorgu satırları hangi sırayla
verirse versin, o sıra veriden değil, motorun seçtiği erişim yolundan gelir. Aynı tabloya
bir dizin eklemek, hiçbir satırı değiştirmeden sonucun sırasını değiştirmeye yeter:

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode box
CREATE TABLE sube (sube_kodu TEXT, ad TEXT, il TEXT);
INSERT INTO sube VALUES ('MRK', 'Merkez', 'Ankara'),
                        ('BHC', 'Bahçelievler', 'Ankara'),
                        ('KDK', 'Kadıköy', 'İstanbul');
SELECT sube_kodu, ad FROM sube;
CREATE INDEX sube_ad ON sube (ad, sube_kodu);
SELECT sube_kodu, ad FROM sube;
SQL
```

```
┌───────────┬──────────────┐
│ sube_kodu │      ad      │
├───────────┼──────────────┤
│ MRK       │ Merkez       │
│ BHC       │ Bahçelievler │
│ KDK       │ Kadıköy      │
└───────────┴──────────────┘
┌───────────┬──────────────┐
│ sube_kodu │      ad      │
├───────────┼──────────────┤
│ BHC       │ Bahçelievler │
│ KDK       │ Kadıköy      │
│ MRK       │ Merkez       │
└───────────┴──────────────┘
```

İki sorgu aynıdır, veri aynıdır, sıra farklıdır. İkinci sorgu artık tabloyu değil, ada
göre düzenli tutulan dizini okumaktadır. Hangi erişim yolunun seçileceği motora göre
değişir; kural olarak sıralı sonuç isteniyorsa sıralama açıkça istenmelidir. Sırayı açıkça
istemeyen bir program, doğruluğunu tesadüfe bağlamıştır.

#### Yinelenen Satırlar ve Çok Küme

Küme kuramında bir eleman ya kümededir ya değildir; iki kez bulunamaz. Bağıntı tanımı
gereği yinelenen satır barındırmaz. SQL tablosu ise bu noktada bağıntıdan ayrılır: anahtar
tanımlanmamışsa aynı satır iki kez yazılabilir.

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode box
CREATE TABLE sube (sube_kodu TEXT, ad TEXT, il TEXT);
INSERT INTO sube VALUES ('MRK', 'Merkez', 'Ankara');
INSERT INTO sube VALUES ('MRK', 'Merkez', 'Ankara');
SELECT COUNT(*) AS satir, COUNT(DISTINCT sube_kodu) AS farkli_kod FROM sube;
SQL
```

```
┌───────┬────────────┐
│ satir │ farkli_kod │
├───────┼────────────┤
│ 2     │ 1          │
└───────┴────────────┘
```

SQL tablosu bir küme değil, bir **çok kümedir** (multiset): aynı değer birden çok kez
bulunabilir. Bu, modelin bir eksiği değil, kuramdan bilinçli bir sapmadır — yinelenenleri
elemek sıralama ya da karma gerektirir, yani ücretlidir. Sonuç şudur: tablonun bağıntı
gibi davranması kendiliğinden olmaz, şemada istenir. Bunu isteme yolu anahtar
tanımlamaktır ve sıradaki dersin konusu budur.

#### Bağıntı Değişkeni ve Şema

Yukarıdaki `sube` tablosu, zaman içinde değişen bir değeri tutar: bugün üç satır, yarın
dört. Kuramda bu ayrım adlandırılmıştır. **Bağıntı değişkeni** (relation variable) bir
addır ve o adın herhangi bir andaki değeri bir bağıntıdır. `CREATE TABLE` bir bağıntı
değişkeni oluşturur; `INSERT` onun değerini değiştirir.

Buradan, veritabanı çalışmasının iki ayrı düzlemi çıkar. **Şema** (schema), bağıntı
değişkenlerinin başlıklarını ve üzerlerindeki kısıtları belirtir; nadiren değişir ve
değiştiğinde göç gerektirir. **Örnek** (instance), belirli bir andaki değerler
bütünüdür; sürekli değişir. Bir tasarım hatası şemadadır, bir veri hatası örnektedir; ilkini
düzeltmek ikincisinden pahalıdır.

#### Değerin Bölünmezliği

Modelin son kuralı, satır ile sütunun kesiştiği yerde **tek bir değer** bulunmasıdır.
Şubenin telefon numaraları için `'0312-000-0001, 0312-000-0002'` biçiminde tek bir metin
yazmak, oraya bir liste sıkıştırmak demektir. Sistem o metni bölünmez sayar: içindeki
ikinci numarayı arayan bir sorgu yazılamaz, bir numaranın kaldırılması metin düzenlemeye
dönüşür, numara üzerinde kısıt tanımlanamaz.

Bunun karşılığı, çok değerli olguyu ayrı bir bağıntıya taşımaktır — her telefon numarası
kendi satırında, şube koduyla birlikte. Bu kural Normalizasyon konusunda birinci normal
form adıyla biçimselleştirilecek; şimdilik modelin bir gereği olarak durur.

#### Özet

- Bağıntı, öznitelik alanlarının kartezyen çarpımının bir alt kümesidir; başlığı sabit,
  gövdesi değişkendir.
- Alan, bir sütunun anlamlı değerler kümesidir ve tipten dardır; şemada kısıt olarak
  yazılır.
- Gövde bir küme olduğundan satır sırası veriye ait değildir; sıralı sonuç isteniyorsa
  açıkça istenmelidir.
- SQL tablosu çok küme olduğu için yinelenen satır kabul eder; bağıntı gibi davranması
  şemada anahtar tanımlanmasına bağlıdır.
- Şema bağıntı değişkenlerini ve kısıtlarını, örnek ise belirli bir andaki değerleri
  belirtir.

#### Sonraki Adım

Bu ders iki kez aynı yere çıktı: satırların birbirinden ayırt edilebilmesi gerekiyor ve
bu ayırt etme şemada söylenmedikçe gerçekleşmiyor. Sıradaki ders, bir satırı tek başına
belirleyen sütun kümesini tanımlar; birden çok adayın bulunduğu durumda hangisinin
seçileceğini tartışır ve bir bağıntının satırlarını başka bir bağıntının satırlarına
bağlayan anahtarı kurar.

### Anahtarlar

Source: https://academia.sh/tr/kurslar/iliskisel-kuram/anahtarlar

Süperanahtar, aday anahtar, birincil anahtar, alternatif anahtar ve yabancı anahtar tanımları; doğal ile vekil anahtar arasındaki seçim ve anahtar seçim ölçütleri.

Önceki ders iki kez aynı noktaya çıktı: satırların birbirinden ayırt edilebilmesi gerekiyor
ve bu ayırt etme şemada söylenmedikçe gerçekleşmiyor. SQL tablosu, hiçbir şey söylenmezse
aynı satırı iki kez kabul eden bir çok kümedir. Bu dersin sorusu şudur: bir satırı tek
başına belirleyen sütun kümesi nasıl tanımlanır, birden çok aday varsa hangisi seçilir ve
bir bağıntının satırı başka bir bağıntının satırına nasıl bağlanır?

#### Süperanahtar ve Aday Anahtar

Bir bağıntının başlığındaki öznitelikler kümesine $H$ diyelim. $K \subseteq H$ alt kümesi,
bağıntının hiçbir geçerli değerinde iki farklı satır $K$ üzerinde aynı değerleri taşımıyorsa
bir **süperanahtardır** (superkey). Yani $K$ üzerindeki değerler satırı belirler.

Bu tanım fazla cömerttir: bir süperanahtara gereksiz sütun eklemek onu yine süperanahtar
bırakır. Üye bağıntısında `uye_no` satırı belirliyorsa, `(uye_no, ad)` de belirler.
Gereksizliği elemek için indirgeme yapılır: hiçbir öz alt kümesi süperanahtar olmayan bir
süperanahtara **aday anahtar** (candidate key) denir. Aday anahtar indirgenemez bir
belirleyicidir; bir bağıntının aday anahtarları farklı boyutlarda olabilir.

Bir bağıntının birden çok aday anahtarı olabilir. Üye bağıntısında üye numarası da
e-posta adresi de satırı tek başına belirler:

```sh
sqlite3 :memory: <<'SQL'
CREATE TABLE uye (
  uye_no       INTEGER PRIMARY KEY,
  eposta       TEXT NOT NULL UNIQUE,
  ad           TEXT NOT NULL,
  kayit_tarihi TEXT NOT NULL
);
INSERT INTO uye VALUES (41, 'ayse.demir@ornek.org', 'Ayşe Demir', '2024-09-12');
INSERT INTO uye VALUES (52, 'ayse.demir@ornek.org', 'Ayşe D. Demir', '2025-01-08');
SQL
```

```
Runtime error near line 8: UNIQUE constraint failed: uye.eposta (19)
```

İkinci ekleme farklı bir üye numarası taşımasına rağmen reddedilir, çünkü e-posta adresi de
bir aday anahtardır ve o adres zaten kullanılmıştır. Aday anahtarlık, bugünkü veride
yinelenme bulunmamasından değil, **hiçbir zaman bulunamayacağı kuralından** gelir. Bu ayrım
önemlidir: veride şu an tekrar etmeyen bir sütun anahtar sanılabilir. Doğru soru "şu anda
tekrar ediyor mu" değil, "tekrar etmesi mümkün mü" sorusudur. Kütüphane bağlamında üye adı
buna iyi bir örnektir — bugün tekrar etmiyor olması, iki Ayşe Demir'in üye olamayacağı
anlamına gelmez.

#### Birincil ve Alternatif Anahtar

Aday anahtarlardan biri, bağıntının resmî kimliği olarak seçilir; buna **birincil anahtar**
(primary key) denir. Seçilmeyen aday anahtarlara **alternatif anahtar** (alternate key)
denir ve onlar da şemada tekillik kısıtı olarak yazılır — yukarıdaki `UNIQUE` bunu yapar.

Birincil anahtar seçimi bir tercih meselesidir ve motorun davranışını da etkiler: satırlara
başka bağıntılardan yapılan başvurular birincil anahtar üzerinden kurulur, dizin genellikle
onun üzerine düşer.

```sh
sqlite3 :memory: <<'SQL'
CREATE TABLE uye (
  uye_no INTEGER PRIMARY KEY,
  eposta TEXT NOT NULL UNIQUE,
  ad     TEXT NOT NULL
);
INSERT INTO uye VALUES (41, 'ayse.demir@ornek.org', 'Ayşe Demir');
INSERT INTO uye VALUES (41, 'mert.kaya@ornek.org', 'Mert Kaya');
SQL
```

```
Runtime error near line 7: UNIQUE constraint failed: uye.uye_no (19)
```

#### Bileşik Anahtar

Anahtar tek sütundan oluşmak zorunda değildir. Birden çok sütunun birlikte oluşturduğu
anahtara **bileşik anahtar** (composite key) denir. Kütüphanede bir kitabın fiziksel
kopyası — bundan sonra **nüsha** — üç şeyle belirlenir: hangi kitap, hangi şube, o şubedeki
kaçıncı kopya.

```sh
sqlite3 :memory: <<'SQL'
CREATE TABLE nusha (
  isbn      TEXT    NOT NULL,
  sube_kodu TEXT    NOT NULL,
  kopya_no  INTEGER NOT NULL,
  PRIMARY KEY (isbn, sube_kodu, kopya_no)
);
INSERT INTO nusha VALUES ('978-975-0000-01-1', 'MRK', 1),
                         ('978-975-0000-01-1', 'MRK', 2),
                         ('978-975-0000-01-1', 'BHC', 1);
INSERT INTO nusha VALUES ('978-975-0000-01-1', 'BHC', 1);
SQL
```

```
Runtime error near line 10: UNIQUE constraint failed: nusha.isbn, nusha.sube_kodu, nusha.kopya_no (19)
```

Aynı kitabın Merkez şubesindeki birinci ve ikinci kopyası ile Bahçelievler şubesindeki
birinci kopyası ayrı satırlardır; dördüncü ekleme üçüncüyle çakıştığı için reddedilir.
Bileşik anahtarın sütun sırası anahtarlık açısından önemsizdir — küme kümedir — ama dizin
davranışı açısından önemlidir; bu ayrım motorun iç yapısını ele alan kurslara aittir.

#### Doğal ve Vekil Anahtar

Anahtar, alanın kendi verisinden gelebilir ya da kimliklendirme amacıyla üretilmiş olabilir.
Alanın kendi verisinden gelene **doğal anahtar** (natural key) denir: kitabın ISBN'i, şube
kodu, üyenin e-posta adresi. Yalnızca satırı ayırt etmek için üretilene **vekil anahtar**
(surrogate key) denir: ödünç işleminin sıra numarası gibi.

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode box
CREATE TABLE odunc (
  odunc_no    INTEGER PRIMARY KEY,
  uye_no      INTEGER NOT NULL,
  alis_tarihi TEXT    NOT NULL
);
INSERT INTO odunc (uye_no, alis_tarihi) VALUES (41, '2025-03-02'), (52, '2025-03-04');
SELECT * FROM odunc;
SQL
```

```
┌──────────┬────────┬─────────────┐
│ odunc_no │ uye_no │ alis_tarihi │
├──────────┼────────┼─────────────┤
│ 1        │ 41     │ 2025-03-02  │
│ 2        │ 52     │ 2025-03-04  │
└──────────┴────────┴─────────────┘
```

Ekleme deyimi `odunc_no` sütununa değer vermedi; değerler sistem tarafından üretildi.
**Kimlik sütununun nasıl üretildiği motora göre değişir**: kimi motorda ayrı bir sıra
üreteci nesnesi tanımlanır, kimi motorda sütun bildiriminde belirtilir, kiminde tablonun
gizli satır numarası kullanılır. Üretilen değerlerin boşluksuz artacağı hiçbir motorda
güvence değildir; geri alınan bir işlemin tükettiği numara geri gelmez. Vekil anahtarı
"kaçıncı kayıt" diye okumak bu yüzden hatalıdır.

Doğal anahtar veriyi anlamlı kılar ve fazladan sütun gerektirmez; buna karşılık alanın
kuralları değiştiğinde değişebilir. Vekil anahtar değişmez ve dardır; buna karşılık
anlamsızdır, tekilliği kendi başına güvenceye almaz — vekil anahtar konan bir bağıntıda
doğal aday anahtar yine de `UNIQUE` ile yazılmalıdır, yoksa aynı kitap iki farklı numarayla
iki kez girilebilir.

#### Yabancı Anahtar

Bağıntılar birbirine **yabancı anahtarla** (foreign key) bağlanır: bir bağıntıdaki sütun
kümesinin değerleri, başka bir bağıntının aday anahtar değerleri arasında bulunmak
zorundadır.

```sh
sqlite3 :memory: <<'SQL'
PRAGMA foreign_keys = ON;
CREATE TABLE uye (
  uye_no INTEGER PRIMARY KEY,
  ad     TEXT NOT NULL
);
CREATE TABLE odunc (
  odunc_no    INTEGER PRIMARY KEY,
  uye_no      INTEGER NOT NULL REFERENCES uye (uye_no),
  alis_tarihi TEXT    NOT NULL
);
INSERT INTO uye VALUES (41, 'Ayşe Demir');
INSERT INTO odunc VALUES (1001, 41, '2025-03-02');
INSERT INTO odunc VALUES (1002, 77, '2025-03-04');
SQL
```

```
Runtime error near line 13: FOREIGN KEY constraint failed (19)
```

`41` numaralı üye tabloda bulunduğu için ilk ödünç kaydı geçer; `77` numaralı üye
bulunmadığı için ikincisi reddedilir. Yabancı anahtar, ilişkisel modelde bağıntıları
birbirine bağlayan tek düzenektir — satırlar birbirine işaretçiyle değil, **değerle**
bağlanır. Bu, Veri Yapıları kursundaki bağlı listelerden temel bir ayrılıştır: bağlantı
bellek adresine değil, anahtar değerine dayandığı için veritabanı kapatılıp açıldığında,
başka bir makineye taşındığında ya da yedekten geri yüklendiğinde geçerliliğini korur.

#### Anahtar Seçimi Ölçütleri

Aday anahtarlar arasından birincil anahtarı seçerken dört ölçüt işe yarar.

- **Kararlılık.** Anahtar değerinin değişmemesi gerekir; ona başvuran her satır o değeri
  taşır. E-posta adresi bu yüzden kötü bir birincil anahtardır, iyi bir alternatif
  anahtardır.
- **Darlık.** Anahtar, ona başvuran her bağıntıda tekrar eder ve dizinlerde yer kaplar. Üç
  metin sütunlu bileşik anahtar yerine tek tam sayı, başvuru sayısı arttıkça belirgin fark
  yaratır.
- **Bölünmezlik.** İçinde ayrıştırılabilir bilgi taşıyan anahtar — şube kodu ile yılı
  birleştiren bir dizgi gibi — kural değiştiğinde bozulur.
- **Gizlilik.** Anahtar, dışarıya verilen adreslerde ve bağlantılarda görünür. Kimlik
  numarası gibi kişisel bir değeri birincil anahtar yapmak, onu görünür kılmayı da
  beraberinde getirir.

#### Özet

- Süperanahtar satırı belirleyen herhangi bir sütun kümesidir; aday anahtar, hiçbir öz alt
  kümesi süperanahtar olmayan süperanahtardır.
- Aday anahtarlardan biri birincil anahtar seçilir; kalanlar alternatif anahtar olarak
  tekillik kısıtıyla yazılır.
- Anahtarlık bugünkü veriden değil, veriye konan kuraldan gelir; "şu an tekrar etmiyor"
  anahtarlık kanıtı değildir.
- Doğal anahtar veriden gelir ve değişebilir; vekil anahtar üretilir ve değişmez, ancak
  doğal aday anahtarın tekilliğini şemadan kaldırmaz.
- Yabancı anahtar, satırları adresle değil değerle bağlar; bu yüzden taşıma ve geri yükleme
  altında geçerliliğini korur.

#### Sonraki Adım

Bu derste yabancı anahtar tanımlandı ve bir ihlali reddedildi, ama arkasındaki kural
adlandırılmadı: neden var olmayan bir üyeye ödünç kaydı yazılamıyor ve o üye kaydı sonradan
silinmek istenirse ne olmalı? Sıradaki ders, anahtarların dayandığı iki bütünlük kuralını —
varlık bütünlüğü ile başvuru bütünlüğünü — tanımlar ve silme ile güncelleme durumunda
sistemin seçebileceği davranışları gösterir.

### Bütünlük Kısıtları

Source: https://academia.sh/tr/kurslar/iliskisel-kuram/butunluk-kisitlari

Alan, varlık ve başvuru bütünlüğünün tanımı; yabancı anahtarın silme ve güncelleme eylemleri; kısıt denetiminin zamanlaması ve kısıtın şemaya yazılmasının gerekçesi.

Önceki ders bir yabancı anahtar ihlalini reddetti ama arkasındaki kuralı adlandırmadı.
Bu dersin sorusu şudur: veritabanının her an sağlaması gereken kurallar nelerdir, bu
kurallar şemada nasıl yazılır ve kuralı bozacak bir silme geldiğinde sistemin
seçebileceği davranışlar hangileridir?

Kısıtlar üç katmanda toplanır. **Alan bütünlüğü** (domain integrity) bir sütuna hangi
değerlerin yazılabileceğini söyler — tip, `NOT NULL` ve `CHECK` bunun araçlarıdır ve
önceki derste görüldü. **Varlık bütünlüğü** (entity integrity) her satırın kimliğinin
belirli olmasını ister. **Başvuru bütünlüğü** (referential integrity) bir bağıntıdan
diğerine yapılan başvurunun boşa düşmemesini ister. Dördüncü bir katman, alana özgü iş
kurallarıdır; bunlar da mümkün olduğunca şemaya yazılır.

#### Varlık Bütünlüğü

Kural tek cümledir: **birincil anahtarı oluşturan hiçbir sütun boş değer alamaz.**
Gerekçesi doğrudan anahtarın tanımından gelir. Anahtarın işi satırı ayırt etmektir; boş
değer ise "bilinmiyor" demektir. Kimliği bilinmeyen bir satır, kendisine başvurulamayan
bir satırdır.

Bu kuralın standartta yazılı olması, her motorun onu aynı sıkılıkta uyguladığı anlamına
gelmez:

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode box
CREATE TABLE sube (sube_kodu TEXT PRIMARY KEY, ad TEXT NOT NULL);
INSERT INTO sube VALUES (NULL, 'Adsız Şube');
SELECT COUNT(*) AS satir FROM sube;
SQL
```

```
┌───────┐
│ satir │
├───────┤
│ 1     │
└───────┘
```

Birincil anahtarı boş olan bir satır tabloya girdi. **Bu davranış motora göre değişir**;
kimi motor aynı deyimi reddeder. Taşınabilir ve niyeti açık şema, kuralı kendi yazar:

```sh
sqlite3 :memory: <<'SQL'
CREATE TABLE sube (sube_kodu TEXT PRIMARY KEY NOT NULL, ad TEXT NOT NULL);
INSERT INTO sube VALUES (NULL, 'Adsız Şube');
SQL
```

```
Runtime error near line 2: NOT NULL constraint failed: sube.sube_kodu (19)
```

Buradan çıkan genel ilke, kursun tekrar eden temalarından biridir: bir kuralın motorda
zaten var olduğunu varsaymak yerine şemada açıkça yazmak, hem taşınabilirliği hem
okunabilirliği artırır.

#### Başvuru Bütünlüğü

Kural şudur: bir yabancı anahtarın değeri ya boştur, ya da başvurduğu bağıntıda o değere
sahip bir satır **vardır**. Üçüncü bir olasılık yoktur; hedefi bulunmayan satıra **yetim
kayıt** (orphan record) denir.

Bu kuralın uygulanıp uygulanmadığı da motora göre değişir. Burada kullanılan komut satırı
aracında denetim varsayılan olarak kapalıdır ve oturum başına açılması gerekir:

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode box
PRAGMA foreign_keys;
CREATE TABLE uye (uye_no INTEGER PRIMARY KEY, ad TEXT NOT NULL);
CREATE TABLE odunc (
  odunc_no INTEGER PRIMARY KEY,
  uye_no   INTEGER NOT NULL REFERENCES uye (uye_no)
);
INSERT INTO odunc VALUES (1001, 77);
SELECT COUNT(*) AS yetim_kayit FROM odunc;
SQL
```

```
┌──────────────┐
│ foreign_keys │
├──────────────┤
│ 0            │
└──────────────┘
┌─────────────┐
│ yetim_kayit │
├─────────────┤
│ 1           │
└─────────────┘
```

Şemada `REFERENCES` yazılıdır, üye tablosu boştur, buna rağmen ödünç kaydı girmiştir.
Denetim kapalı olduğu için bildirim yalnızca belgeleme değeri taşımıştır. Bu dersin geri
kalanındaki bloklar denetimi açan satırla başlar.

#### Silme ve Güncelleme Eylemleri

Başvuru bütünlüğü bozulabilecek iki an vardır: başvurulan satır silindiğinde ve başvurulan
anahtar değeri güncellendiğinde. Şema, bu iki an için bir davranış seçer.

- **`NO ACTION` / `RESTRICT`** — işlem reddedilir. Standarttaki varsayılan davranıştır;
  ikisi arasındaki fark denetimin zamanlamasındadır.
- **`CASCADE`** — silme ya da güncelleme, başvuran satırlara yayılır.
- **`SET NULL`** — başvuran satırın yabancı anahtarı boşaltılır; sütunun boş değer
  kabul etmesi gerekir.
- **`SET DEFAULT`** — başvuran satırın yabancı anahtarı sütunun öntanımlı değerine
  çekilir; o değerin hedefte bulunması gerekir.

Seçim keyfî değildir, alanın anlamını izler. Üye silindiğinde onun ceza kayıtlarının
kalması anlamsızdır — bunlar üyeye bağımlı, kendi başına var olamayan kayıtlardır, yani
`CASCADE`. Şube kapandığında üyenin kaydı silinmemelidir; üye şubesiz kalır, yani
`SET NULL`. Üyeye ait ödünç geçmişi ise silinmemelidir — geçmiş kayıt bir olgudur — bu
yüzden silme reddedilir.

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode box
.nullvalue (bos)
PRAGMA foreign_keys = ON;
CREATE TABLE sube (sube_kodu TEXT PRIMARY KEY NOT NULL, ad TEXT NOT NULL);
CREATE TABLE uye (
  uye_no    INTEGER PRIMARY KEY,
  ad        TEXT NOT NULL,
  sube_kodu TEXT REFERENCES sube (sube_kodu) ON DELETE SET NULL
);
CREATE TABLE ceza (
  ceza_no INTEGER PRIMARY KEY,
  uye_no  INTEGER NOT NULL REFERENCES uye (uye_no) ON DELETE CASCADE,
  tutar   INTEGER NOT NULL
);
INSERT INTO sube VALUES ('MRK', 'Merkez'), ('BHC', 'Bahçelievler');
INSERT INTO uye VALUES (41, 'Ayşe Demir', 'MRK'), (52, 'Mert Kaya', 'BHC');
INSERT INTO ceza VALUES (9001, 52, 30), (9002, 52, 15);
DELETE FROM uye  WHERE uye_no = 52;
DELETE FROM sube WHERE sube_kodu = 'MRK';
SELECT (SELECT COUNT(*) FROM uye)  AS uye_satiri,
       (SELECT COUNT(*) FROM ceza) AS ceza_satiri,
       (SELECT sube_kodu FROM uye WHERE uye_no = 41) AS uye41_sube;
SQL
```

```
┌────────────┬─────────────┬────────────┐
│ uye_satiri │ ceza_satiri │ uye41_sube │
├────────────┼─────────────┼────────────┤
│ 1          │ 0           │ (bos)      │
└────────────┴─────────────┴────────────┘
```

Tek bir üye silindi ve ona bağlı iki ceza kaydı da gitti; şube silindi ve ona bağlı üye
şubesiz kaldı. `.nullvalue` ayarı, boş değeri çıktıda görünür kılmak içindir — ayar
verilmediğinde boş değer ile boş dizgi ekranda ayırt edilemez.

Öntanımlı davranış ise reddetmektir:

```sh
sqlite3 :memory: <<'SQL'
PRAGMA foreign_keys = ON;
CREATE TABLE uye (uye_no INTEGER PRIMARY KEY, ad TEXT NOT NULL);
CREATE TABLE odunc (
  odunc_no INTEGER PRIMARY KEY,
  uye_no   INTEGER NOT NULL REFERENCES uye (uye_no)
);
INSERT INTO uye VALUES (41, 'Ayşe Demir');
INSERT INTO odunc VALUES (1001, 41);
DELETE FROM uye WHERE uye_no = 41;
SQL
```

```
Runtime error near line 9: FOREIGN KEY constraint failed (19)
```

`CASCADE` seçiminde dikkatli olmak gerekir: yayılma zincirlenir. Üyeyi silen bir deyim,
ona bağlı ceza kayıtlarını, onlara bağlı ödeme kayıtlarını ve bu zincirin devamını da
siler. Silinen satır sayısı, deyimi yazanın gördüğünden çok daha büyük olabilir.

#### Kısıt Ne Zaman Denetlenir

Kısıtlar öntanımlı olarak her deyimin sonunda denetlenir. Bu, bazı doğru durumların
geçici olarak kurulamamasına yol açar. İki bağıntı birbirine karşılıklı başvuruyorsa —
her şubenin bir yöneticisi vardır ve her yönetici bir şubede çalışır — hangisi önce
eklenirse eklensin, ekleme anında karşı taraf henüz yoktur.

Çözüm, denetimi işlemin sonuna ertelemektir; buna **ertelenmiş kısıt** (deferred
constraint) denir. Ertelenmiş denetimde ara durumlar kuralı bozabilir, yeter ki işlem
tamamlandığında kural sağlansın. **Bu yeteneğin bulunup bulunmadığı ve nasıl bildirildiği
motora göre değişir**; bulunmayan motorlarda karşılıklı başvurunun bir ucunun boş değer
kabul etmesi ve iki adımda doldurulması gerekir.

#### Kısıtı Nereye Yazmalı

Her kural şemaya sığmaz. "Bir üye aynı anda en çok beş nüsha ödünç alabilir" kuralı, tek
satıra bakarak denetlenemez; sayım gerektirir. Böyle kurallar ya bir tetikleyiciyle ya da
uygulama katmanında yazılır.

Ölçüt şudur: kural veriye erişen **bütün** yollar için geçerliyse ve tek bir satır ya da
satır kümesi üzerinden ifade edilebiliyorsa şemaya yazılır. Şemadaki kısıt, hangi
programın yazdığından, hangi dilde yazıldığından ve veriye elle mi yoksa toplu göçle mi
dokunulduğundan bağımsız olarak uygulanır. Uygulama katmanındaki denetim yalnızca o
uygulamadan geçen yazmaları kapsar; ikinci bir servis, bir bakım betiği ya da elle
çalıştırılan bir deyim onu atlar.

Kısıtın ikinci işlevi belgelemedir. Şemayı okuyan biri, `ON DELETE CASCADE` yazan bir
satırdan ceza kaydının üyeye bağımlı olduğunu okur. Aynı bilgi uygulama kodunda dağınık
biçimde durduğunda okunmaz.

#### Özet

- Varlık bütünlüğü birincil anahtarın boş değer alamamasıdır; kimliği belirsiz satıra
  başvurulamaz.
- Başvuru bütünlüğü, yabancı anahtarın ya boş olmasını ya da hedefte karşılığı bulunmasını
  ister; ihlali yetim kayıt üretir.
- Motorun bir kuralı öntanımlı olarak uyguladığını varsaymak yerine kısıtı şemada açıkça
  yazmak taşınabilirliği ve okunabilirliği artırır.
- Silme ve güncelleme eylemleri — reddetme, yayma, boşaltma, öntanımlıya çekme — alanın
  anlamına göre seçilir; yayma zincirlenerek beklenenden çok satır siler.
- Bütün yazma yollarını bağlayan ve satır düzeyinde ifade edilebilen kurallar şemaya,
  sayım ya da geçmiş gerektirenler uygulama katmanına yazılır.

#### Sonraki Adım

Bu derste iki kez "ya boştur" ifadesi geçti: yabancı anahtar boş olabilir, `SET NULL`
sütunu boşaltır. Boş değerin ne anlama geldiği ise tanımlanmadı. Sıradaki ders, boş
değerin bir değer olmadığını, karşılaştırmaların onunla ne yaptığını ve doğru–yanlış
ikilisine üçüncü bir sonucun eklenmesinin sorgu yazımında hangi tuzakları açtığını gösterir.

### Boş Değerler

Source: https://academia.sh/tr/kurslar/iliskisel-kuram/bos-degerler

Boş değerin bir değer olmaması, üç değerli mantığın doğruluk tablosu, süzgeçlerin ve NOT IN yazımının davranışı, tekillik kısıtının boş değere bakışı ve boş değerden kaçınma yolları.

Önceki derste iki kez "ya boştur" ifadesi geçti: yabancı anahtar boş olabilir, `SET NULL`
eylemi sütunu boşaltır. Boş değerin ne anlama geldiği ise tanımlanmadı. Bu dersin sorusu
şudur: boş değer nedir, onunla yapılan karşılaştırmalar ne döndürür ve doğru–yanlış
ikilisine üçüncü bir sonuç eklenmesi sorgu yazımında hangi tuzakları açar?

#### Boş Değer Bir Değer Değildir

**Boş değer** (null), bir sütunda değer bulunmadığını gösteren işarettir. Sıfır değildir,
boş dizgi değildir, "yok" adlı özel bir değer de değildir. Anlamı "bu satır için o bilgi
mevcut değildir" biçiminde okunur ve iki ayrı durumu aynı işaretle temsil eder:
bilinmeyen bilgi (üyenin telefon numarası vardır ama kayıtlı değildir) ve uygulanamayan
bilgi (ödünç işleminin iade tarihi henüz oluşmamıştır). Bu iki durumun aynı işareti
paylaşması, ilişkisel modele yöneltilen en eski eleştirilerden biridir.

Bir değer olmadığı için, boş değer bir değerle karşılaştırılamaz. Karşılaştırmanın sonucu
doğru da yanlış da değildir:

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode box
.nullvalue (bos)
SELECT NULL = NULL           AS esit_mi,
       NULL <> NULL          AS farkli_mi,
       NULL IS NULL          AS bos_mu,
       (NULL = NULL) IS NULL AS sonuc_bos_mu;
SQL
```

```
┌─────────┬───────────┬────────┬──────────────┐
│ esit_mi │ farkli_mi │ bos_mu │ sonuc_bos_mu │
├─────────┼───────────┼────────┼──────────────┤
│ (bos)   │ (bos)     │ 1      │ 1            │
└─────────┴───────────┴────────┴──────────────┘
```

`NULL = NULL` doğru değildir; `NULL <> NULL` de doğru değildir. İkisi de üçüncü bir
sonuç verir. Boş değeri sınamanın tek doğru yolu `IS NULL` ve `IS NOT NULL`
işleçleridir — bunlar karşılaştırma değil, durum sorgusudur ve her zaman doğru ya da
yanlış döndürür.

#### Üçüncü Doğruluk Değeri

Karşılaştırmanın verdiği üçüncü sonucun adı **bilinmiyor** (unknown), sisteminin adı ise
**üç değerli mantıktır** (three-valued logic). `AND` ve `OR` işleçleri bu üçüncü değeri de
kapsayacak biçimde genişletilmiştir. Tabloyu ezberlemek gerekmez; hesaplatmak yeter:

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode box
.nullvalue (bos)
WITH d(ad, v) AS (VALUES ('dogru', 1), ('yanlis', 0), ('bilinmiyor', NULL))
SELECT a.ad AS sol, b.ad AS sag,
       a.v AND b.v AS ve, a.v OR b.v AS veya
FROM d a, d b;
SQL
```

```
┌────────────┬────────────┬───────┬───────┐
│    sol     │    sag     │  ve   │ veya  │
├────────────┼────────────┼───────┼───────┤
│ dogru      │ dogru      │ 1     │ 1     │
│ dogru      │ yanlis     │ 0     │ 1     │
│ dogru      │ bilinmiyor │ (bos) │ 1     │
│ yanlis     │ dogru      │ 0     │ 1     │
│ yanlis     │ yanlis     │ 0     │ 0     │
│ yanlis     │ bilinmiyor │ 0     │ (bos) │
│ bilinmiyor │ dogru      │ (bos) │ 1     │
│ bilinmiyor │ yanlis     │ 0     │ (bos) │
│ bilinmiyor │ bilinmiyor │ (bos) │ (bos) │
└────────────┴────────────┴───────┴───────┘
```

Tabloda okunacak kural şudur: sonucu belirleyen bir taraf varsa bilinmezlik yayılmaz.
`yanlis AND bilinmiyor` yanlıştır, çünkü bir taraf yanlışsa sonuç ne olursa olsun
yanlıştır. `dogru OR bilinmiyor` doğrudur, aynı gerekçeyle. Diğer bütün durumlarda
bilinmezlik sonuca taşınır. Bu, Programlama Temelleri kursundaki kısa devre
değerlendirmenin mantık düzeyindeki karşılığıdır.

#### Süzgecin Böldüğü Küme

`WHERE` yalnızca **doğru** sonucu veren satırları geçirir; yanlış ile bilinmiyor birlikte
elenir. Bunun görünür sonucu, birbirinin değili sanılan iki koşulun tüm satırları
kapsamamasıdır:

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode box
CREATE TABLE odunc (odunc_no INTEGER PRIMARY KEY, uye_no INTEGER, iade_tarihi TEXT);
INSERT INTO odunc VALUES (1001, 41, '2025-03-16'), (1002, 52, NULL),
                         (1003, 41, '2025-03-19'), (1004, 41, NULL);
SELECT (SELECT COUNT(*) FROM odunc) AS toplam,
       (SELECT COUNT(*) FROM odunc WHERE iade_tarihi =  '2025-03-16') AS esit,
       (SELECT COUNT(*) FROM odunc WHERE iade_tarihi <> '2025-03-16') AS farkli,
       (SELECT COUNT(*) FROM odunc WHERE iade_tarihi IS NULL) AS bos;
SQL
```

```
┌────────┬──────┬────────┬─────┐
│ toplam │ esit │ farkli │ bos │
├────────┼──────┼────────┼─────┤
│ 4      │ 1    │ 1      │ 2   │
└────────┴──────┴────────┴─────┘
```

Dört satır vardır; "eşittir" bir satır, "eşit değildir" bir satır döndürür. Toplamları
dörde ulaşmaz, çünkü iki satır iki süzgeçten de geçmez. İki değerli mantıkta alışılmış
olan "koşul ya da değili tüm kümeyi kapsar" kuralı, boş değer bulunan bir sütunda
geçerli değildir. Kapsama isteniyorsa koşul açıkça yazılır.

#### `NOT IN` Tuzağı

Aynı kuralın en pahalı sonucu alt sorgularda görülür. `x NOT IN (a, b, c)` yazımı,
`x <> a AND x <> b AND x <> c` ifadesine denktir. Listede tek bir boş değer bulunuyorsa o
karşılaştırma bilinmiyor döner, `AND` zinciri bilinmezliğe düşer ve **hiçbir satır**
geçmez:

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode box
CREATE TABLE uye  (uye_no INTEGER PRIMARY KEY, ad TEXT NOT NULL);
CREATE TABLE ceza (ceza_no INTEGER PRIMARY KEY, uye_no INTEGER);
INSERT INTO uye  VALUES (41, 'Ayşe Demir'), (52, 'Mert Kaya'), (63, 'Selin Öztürk');
INSERT INTO ceza VALUES (9001, 52), (9002, NULL);

SELECT 'NOT IN, alt sorgu ham' AS yazim, COUNT(*) AS donen_satir
  FROM uye WHERE uye_no NOT IN (SELECT uye_no FROM ceza)
UNION ALL
SELECT 'NOT IN, bos degerler elenmis', COUNT(*)
  FROM uye WHERE uye_no NOT IN (SELECT uye_no FROM ceza WHERE uye_no IS NOT NULL)
UNION ALL
SELECT 'NOT EXISTS', COUNT(*)
  FROM uye u WHERE NOT EXISTS (SELECT 1 FROM ceza c WHERE c.uye_no = u.uye_no);
SQL
```

```
┌──────────────────────────────┬─────────────┐
│            yazim             │ donen_satir │
├──────────────────────────────┼─────────────┤
│ NOT IN, alt sorgu ham        │ 0           │
│ NOT IN, bos degerler elenmis │ 2           │
│ NOT EXISTS                   │ 2           │
└──────────────────────────────┴─────────────┘
```

Doğru yanıt ikidir: cezası bulunmayan iki üye vardır. İlk yazım sıfır döndürür ve bunu
hata vermeden yapar — sorgu geçerlidir, sonuç sessizce yanlıştır. Bu, boş değerin en sık
karşılaşılan tuzağıdır ve iki savunması vardır: ya alt sorguda boş değerler elenir, ya da
`NOT EXISTS` yazımı seçilir. İkincisi tercih edilir, çünkü doğruluğu alt sorgunun içeriğine
bağlı değildir.

#### Tekillik ve Boş Değer

Tekillik kısıtı da aynı mantığı izler. İki boş değer birbirine eşit sayılmadığı için,
tekil bir sütun birden çok boş değer taşıyabilir:

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode box
CREATE TABLE uye (
  uye_no INTEGER PRIMARY KEY,
  ad     TEXT NOT NULL,
  tc_no  TEXT UNIQUE
);
INSERT INTO uye VALUES (41, 'Ayşe Demir', NULL);
INSERT INTO uye VALUES (52, 'Mert Kaya', NULL);
INSERT INTO uye VALUES (63, 'Selin Öztürk', NULL);
SELECT COUNT(*) AS satir FROM uye;
SQL
```

```
┌───────┐
│ satir │
├───────┤
│ 3     │
└───────┘
```

Üç satır da geçmiştir. Bu davranış standarda uygundur, ancak **motora göre değişebilir**;
kimi sistemde tekil sütunda yalnız bir boş değere izin verilir. Bu belirsizliğin pratik
sonucu şudur: bir sütunun hem tekil hem zorunlu olmasını istiyorsanız `UNIQUE` yeterli
değildir, yanına `NOT NULL` yazılmalıdır. Tersi de doğrudur: tekillik kısıtı, boş
değerlerin sayısını sınırlamak için kullanılamaz.

#### Toplama İşlevleri

Toplama işlevleri boş değerleri sayıma katmaz. `COUNT(*)` satır sayar, `COUNT(sutun)` o
sütunda değer bulunan satırları sayar; `SUM` ve `AVG` boş değerleri atlar:

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode box
.nullvalue (bos)
CREATE TABLE ceza (ceza_no INTEGER PRIMARY KEY, tutar INTEGER);
INSERT INTO ceza VALUES (9001, 30), (9002, NULL), (9003, 10);
SELECT COUNT(*) AS satir, COUNT(tutar) AS dolu_tutar,
       SUM(tutar) AS toplam, AVG(tutar) AS ortalama FROM ceza;
SQL
```

```
┌───────┬────────────┬────────┬──────────┐
│ satir │ dolu_tutar │ toplam │ ortalama │
├───────┼────────────┼────────┼──────────┤
│ 3     │ 2          │ 40     │ 20.0     │
└───────┴────────────┴────────┴──────────┘
```

Ortalama, üç satırın değil iki değerin ortalamasıdır. Boş değeri sıfır saymak isteniyorsa
bu açıkça yazılmalıdır; işlevin kendiliğinden yaptığı şey atlamaktır. Sıralamada boş
değerlerin başa mı sona mı geleceği ise standartta belirtilmemiştir ve **motora göre
değişir**; belirli bir yer isteniyorsa sıralama ifadesinde açıkça bildirilmelidir.

#### Boş Değerden Kaçınmak

Boş değer bir araçtır, hedef değildir. Üç yaygın durumda gereksiz yere ortaya çıkar ve
üçünün de modelleme karşılığı vardır.

- **Çok değerli olgu için ayrılmış sütunlar.** `telefon_1`, `telefon_2`, `telefon_3`
  sütunlarının ikisi çoğu satırda boştur. Karşılığı, telefonları ayrı bir bağıntıya
  taşımaktır.
- **Yalnız bazı satırlar için anlamlı sütunlar.** Süreli üyeliğe özgü bir bitiş tarihi,
  süresiz üyelerde boş durur. Karşılığı, alt türü ayrı bir bağıntıda tutmaktır.
- **Henüz gerçekleşmemiş olay.** İade tarihinin boş olması, ödünç işleminin sürdüğü
  anlamına gelir. Bu, boş değerin yerinde kullanımıdır — olayın olmadığını bildiren
  başka bir işaret yoktur.

Genel ölçüt şudur: boş değer bir **durum** bildiriyorsa yerindedir; eksik modellemeyi
kapatıyorsa şema düzeltilmelidir.

#### Özet

- Boş değer bir değer değildir; sıfırdan ve boş dizgiden ayrıdır ve yalnız `IS NULL` ile
  sınanır.
- Karşılaştırmalar üçüncü bir sonuç üretir; `WHERE` yalnız doğru olanı geçirir, bu yüzden
  bir koşul ile değili tüm satırları kapsamaz.
- `NOT IN` alt sorgusunda tek bir boş değer sonucu sessizce boşaltır; `NOT EXISTS` bu
  duyarlılığı taşımaz.
- Tekillik kısıtı boş değerleri birbirinden farklı sayar; zorunluluk isteniyorsa
  `NOT NULL` ayrıca yazılır.
- Toplama işlevleri boş değerleri atlar; ortalama, satır sayısına değil değer sayısına
  bölünür.

#### Sonraki Adım

Bu ders bir sütunun boş olabilmesinin sonuçlarını gösterdi; sütunun **ne tuttuğu** ise
hâlâ belirsiz. Kurs boyunca tarihler metin olarak, tutarlar tam sayı olarak yazıldı ve bu
seçimler gerekçelendirilmedi. Sıradaki ders sayı, metin, tarih ve ikili tipleri ele alır;
para tutarının neden kayan noktalı sayıda tutulmadığını, metin karşılaştırmasının hangi
ayara bağlı olduğunu ve büyük ikili verinin veritabanında durup durmayacağını tartışır.

### Veri Tipleri

Source: https://academia.sh/tr/kurslar/iliskisel-kuram/veri-tipleri

Tam sayı, ondalık ve kayan noktalı sayı seçimi; metin uzunluğu ve harmanlama; tarih ile saat diliminin saklanması; ikili veri, doğruluk değeri ve numaralandırma kalıpları.

Önceki beş ders şemayı kurdu: bağıntılar, anahtarlar, kısıtlar ve boş değerin mantığı.
Sütunların **ne tuttuğu** ise gerekçelendirilmeden geçildi — tarihler metin, tutarlar tam
sayı olarak yazıldı. Bu dersin sorusu şudur: bir sütunun tipi hangi ölçütlere göre
seçilir ve yanlış seçimin bedeli nerede ortaya çıkar?

Tip seçimi üç şeyi aynı anda belirler: hangi değerlerin saklanabileceği, karşılaştırma ve
sıralamanın nasıl yapılacağı ve satır başına kaç bayt harcanacağı. İlk ikisi doğruluğu,
üçüncüsü maliyeti etkiler; yanlış seçim genellikle ilk ikisinden fark edilir.

#### Sayılar ve Para

Bilgisayarlar Nasıl Çalışır kursu, kayan noktalı sayının ikilik kesirlerle çalıştığını ve
ondalık kesirlerin çoğunu tam gösteremediğini kurmuştu. O gerçek veritabanında da geçerlidir
ve ekranda görünmez:

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode box
SELECT 0.1 + 0.2       AS toplam,
       0.1 + 0.2 = 0.3 AS uce_onda_esit_mi,
       10 + 20 = 30    AS kurus_esit_mi;
SQL
```

```
┌────────┬──────────────────┬───────────────┐
│ toplam │ uce_onda_esit_mi │ kurus_esit_mi │
├────────┼──────────────────┼───────────────┤
│ 0.3    │ 0                │ 1             │
└────────┴──────────────────┴───────────────┘
```

Toplam ekranda `0.3` yazar ama `0.3` değildir. Gösterim yuvarlar, karşılaştırma yuvarlamaz.
Bir gecikme cezası bu tiple tutulursa, ödenen tutarla borç tutarının eşitliğini sınayan
sorgu bazen yanlış yanıt verir ve bu yanlışlık kuruşun altındadır — yani kütüğe bakan
kimse göremez.

Üç seçenek vardır. **Tam sayı** tipleri kesindir; para için en küçük birim seçilerek
kullanılır — tutar kuruş cinsinden tam sayı olarak saklanır, gösterimde yüze bölünür.
**Ondalık** tipleri (`NUMERIC`, `DECIMAL`) belirli bir basamak sayısıyla kesin ondalık
aritmetik sunar; parasal alan için standart karşılıktır. **Kayan noktalı** tipler ise
ölçüm, oran ve bilimsel büyüklük gibi zaten yaklaşık olan değerler içindir.

Kural şudur: sayılan şey için tam sayı ya da ondalık, ölçülen şey için kayan noktalı tip
kullanılır. Para sayılır.

Tam sayıda ikinci ölçüt genişliktir. Dar tip yer kazandırır ama taşma sınırını yaklaştırır;
üye numarası için iki baytlık bir tip seçmek, altmış beş binden fazla üye kaydedilemeyeceği
anlamına gelir. Taşmanın nasıl davrandığı — sarma mı hata mı — motora göre değişir; sınırı
en baştan geniş seçmek, sonradan tip değiştirmekten ucuzdur.

#### Tipin Uygulanması

Tipin bildirilmiş olması, ona uyulacağı anlamına gelmeyebilir:

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode box
CREATE TABLE kitap (isbn TEXT PRIMARY KEY, yayin_yili INTEGER NOT NULL);
INSERT INTO kitap VALUES ('978-975-0000-01-1', 'iki bin on dokuz');
SELECT yayin_yili, typeof(yayin_yili) AS saklanan_tip FROM kitap;
SQL
```

```
┌──────────────────┬──────────────┐
│    yayin_yili    │ saklanan_tip │
├──────────────────┼──────────────┤
│ iki bin on dokuz │ text         │
└──────────────────┴──────────────┘
```

Sütun tam sayı olarak bildirilmiştir, saklanan değer metindir. **Tip bildiriminin ne kadar
bağlayıcı olduğu motora göre değişir**: kimi motor dönüşümü dener ve başarısız olursa
reddeder, kimi motor değeri olduğu gibi kabul eder. Taşınabilir savunma yine kısıttır —
alan tanımı, tipten bağımsız olarak değeri sınırlar:

```sh
sqlite3 :memory: <<'SQL'
CREATE TABLE kitap (
  isbn       TEXT PRIMARY KEY NOT NULL,
  yayin_yili INTEGER NOT NULL CHECK (yayin_yili BETWEEN 1450 AND 2100)
);
INSERT INTO kitap VALUES ('978-975-0000-01-1', 'iki bin on dokuz');
SQL
```

```
Runtime error near line 5: CHECK constraint failed: yayin_yili BETWEEN 1450 AND 2100 (19)
```

#### Metin ve Harmanlama

Metin tipleri iki eksende ayrılır. **Uzunluk**: sabit uzunluklu tip her satırda aynı yeri
kullanır ve kısa değerleri boşlukla doldurur; değişken uzunluklu tip yalnız yazılanı
saklar. Sabit uzunluk yalnız gerçekten sabit veriler için — iki harfli ülke kodu, üç
karakterli şube kodu — yerindedir. **Sınır**: uzunluk sınırı bir alan kısıtıdır; şube
kodunun üç karakter olması kuralı, sınırsız bir metin sütunuyla ifade edilmez.

Asıl incelik karşılaştırmadadır. İki metnin eşit sayılıp sayılmayacağı ve hangisinin önce
geleceği, **harmanlamaya** (collation) bağlıdır:

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode box
SELECT 'ayse' = 'AYSE'                 AS ikili_karsilastirma,
       'ayse' = 'AYSE' COLLATE NOCASE  AS harmanli_karsilastirma,
       'ayşe' = 'AYŞE' COLLATE NOCASE  AS turkce_harfli;
SQL
```

```
┌─────────────────────┬────────────────────────┬───────────────┐
│ ikili_karsilastirma │ harmanli_karsilastirma │ turkce_harfli │
├─────────────────────┼────────────────────────┼───────────────┤
│ 0                   │ 1                      │ 0             │
└─────────────────────┴────────────────────────┴───────────────┘
```

Öntanımlı karşılaştırma bayt bayttır: `ayse` ile `AYSE` farklıdır. Büyük–küçük harf
duyarsız harmanlama seçildiğinde bu ikisi eşitlenir, ama `ayşe` ile `AYŞE` yine
eşitlenmez — bu harmanlama yalnız temel Latin harflerini kapsar. **Harmanlama adları ve
kapsamları motora göre değişir**; Türkçe metinle çalışan bir şemada dil duyarlı bir
harmanlamanın açıkça seçilmesi gerekir. Karakter Kodlamaları dersindeki normalleştirme
sorunu burada yeniden karşımıza çıkar: görünüşte aynı iki dizgi, kod noktası dizisi farklı
olduğunda eşit sayılmaz.

#### Tarih ve Zaman

Tarih ve zaman için üç ayrı kavram vardır: yalnız tarih, yalnız saat ve ikisini birleştiren
zaman damgası. Zaman damgasında ek bir ayrım daha bulunur — değer bir saat dilimine bağlı
mıdır, yoksa yerel duvar saati midir? Kütüphane açılış saati "09:00" yerel bir kuraldır;
ödünç işleminin gerçekleştiği an ise evrensel bir noktadır ve saat dilimiyle birlikte
saklanmalıdır.

Ayrı bir tarih tipi bulunmayan ortamlarda ISO 8601 biçiminde metin kullanılır. Bu biçimin
değerli yanı, sözlük sırasının kronolojik sıraya eşit olmasıdır:

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode box
.nullvalue (bos)
CREATE TABLE odunc (odunc_no INTEGER PRIMARY KEY, alis_tarihi TEXT NOT NULL,
                    iade_tarihi TEXT);
INSERT INTO odunc VALUES (1001, '2025-03-02', '2025-03-16'),
                         (1002, '2025-03-04', '2025-04-01'),
                         (1003, '2025-12-30', NULL);
SELECT odunc_no, alis_tarihi,
       julianday(iade_tarihi) - julianday(alis_tarihi) AS gun,
       alis_tarihi > '2025-03-03' AS mart_ucunden_sonra
FROM odunc ORDER BY alis_tarihi;
SQL
```

```
┌──────────┬─────────────┬───────┬────────────────────┐
│ odunc_no │ alis_tarihi │  gun  │ mart_ucunden_sonra │
├──────────┼─────────────┼───────┼────────────────────┤
│ 1001     │ 2025-03-02  │ 14.0  │ 0                  │
│ 1002     │ 2025-03-04  │ 28.0  │ 1                  │
│ 1003     │ 2025-12-30  │ (bos) │ 1                  │
└──────────┴─────────────┴───────┴────────────────────┘
```

Sıralama ve aralık karşılaştırması metin üzerinde doğru çalışır. Aritmetik ise tarih
işlevleri gerektirir ve **bu işlevlerin adları motora göre değişir**; burada gün farkı
almak için bir gün sayısına çeviren işlev kullanılmıştır. Metin gösteriminin bedeli budur:
tip denetimi yoktur, `'2025-13-45'` yazılabilir ve her işlem ayrıştırma gerektirir. Ayrı
bir tarih tipi bulunan motorlarda o tip seçilir.

Sıfırlanmış bir tarih ya da `'0000-00-00'` gibi bir yer tutucu kullanmak, önceki dersteki
boş değer tartışmasını atlatmak için yapılan bir kaçamaktır ve her sorguda o yer tutucunun
elenmesini gerektirir. Bilinmeyen tarih için boş değer kullanılır.

#### Doğruluk Değeri, İkili Veri ve Numaralandırma

Üç yaygın durum daha vardır.

- **Doğruluk değeri.** Ayrı bir tip bulunmayan motorlarda tek basamaklı bir tam sayı
  sütunu, yanına `CHECK (sutun IN (0, 1))` kısıtı yazılarak kullanılır. Kısıt olmadan
  sütun üçüncü bir değer alabilir ve o değeri hangi kodun yazdığı bulunamaz.
- **İkili veri.** Kapak görseli, taranmış belge gibi veriler ikili tiplerde saklanabilir.
  Ölçüt boyuttur: küçük ve satırla birlikte anlamlı olan ikili veri sütunda durabilir;
  büyük nesneler için dosya sisteminde ya da nesne deposunda tutup veritabanında yalnız
  adresini saklamak, yedek boyutunu ve satır okuma maliyetini düşürür. Bunun bedeli,
  iki saklama yeri arasındaki tutarlılığın artık uygulamanın sorumluluğunda olmasıdır.
- **Numaralandırma.** Ödünç işleminin durumu — açık, gecikmiş, kapanmış — sınırlı bir
  değer kümesidir. İki karşılık vardır: metin sütunu üzerinde `CHECK ... IN (...)` kısıtı,
  ya da değerleri satır olarak tutan ayrı bir bağıntıya yabancı anahtar. İkincisi yeni
  değer eklemeyi şema değişikliği olmaktan çıkarır ve değere ek bilgi — görünen ad, sıra
  numarası — iliştirmeye izin verir.

#### Özet

- Sayılan büyüklükler tam sayı ya da ondalık tiple, ölçülen büyüklükler kayan noktalı
  tiple saklanır; para sayılır.
- Kayan noktalı toplamın ekrandaki gösterimi doğru görünse de eşitlik karşılaştırması
  yanlış sonuç verebilir.
- Tip bildiriminin ne kadar bağlayıcı olduğu motora göre değişir; alanın gerçek sınırı
  kısıtla yazıldığında her motorda geçerli olur.
- Metin eşitliği ve sıralaması harmanlamaya bağlıdır; büyük–küçük harf duyarsızlığı
  Türkçe harflerde kendiliğinden gelmez.
- Tarih için ayrı tip yoksa ISO 8601 metni kullanılır; sıralama doğru çalışır, aritmetik
  motora özgü işlev gerektirir.

#### Sonraki Adım

İlişkisel Model konusu burada tamamlanıyor: bağıntı tanımlandı, anahtarlar seçildi,
kısıtlar yazıldı, boş değerin mantığı kuruldu ve sütunlara tip verildi. Geriye tek soru
kalıyor — sütunlar hangi bağıntıya konmalı? Kursun ilk dersindeki `odunc.csv` dosyasında
üye adı üç satırda tekrar ediyordu ve e-posta değişikliğinin üç yerde yapılması
gerekiyordu. Sıradaki konu, bu tekrarın nereden geldiğini gösteren kavramla başlar:
işlevsel bağımlılık.

## Normalizasyon

İşlevsel bağımlılık kavramı, birinci-ikinci-üçüncü normal formlar, Boyce–Codd ve ötesi, denormalizasyonun ölçülen ödünü ve yaygın şema tasarım kalıpları.

### İşlevsel Bağımlılık

Source: https://academia.sh/tr/kurslar/iliskisel-kuram/islevsel-bagimlilik

İşlevsel bağımlılığın tanımı ve gösterimi, bağımlılığın veriden değil anlamdan gelmesi, bağımlılık türleri, Armstrong aksiyomları ve öznitelik kapanışıyla anahtar bulma.

İlişkisel Model konusu şemayı kurdu ama tek bir soruyu yanıtsız bıraktı: hangi sütun hangi
bağıntıya konmalı? Kursun ilk dersindeki `odunc.csv` dosyasında üye adı üç satırda tekrar
ediyor, e-posta değişikliği üç yerde düzeltme gerektiriyordu. Bu dersin sorusu şudur: o
tekrarın kaynağı nedir ve nasıl biçimsel olarak adlandırılır?

Yanıt tek bir kavramdadır. Bir sütun kümesinin değeri, başka bir sütun kümesinin değerini
belirliyorsa aralarında bir bağımlılık vardır; tekrar, bu bağımlılığın yanlış yere
yerleştirilmesinden doğar.

#### Tanım ve Gösterim

$R$ bağıntısının öznitelik kümesi $H$ ve $X, Y \subseteq H$ olsun. $R$'nin her geçerli
değerinde, $X$ üzerinde aynı değerleri taşıyan iki satır $Y$ üzerinde de aynı değerleri
taşıyorsa, $Y$ özniteliklerinin $X$'e **işlevsel bağımlı** (functionally dependent) olduğu
söylenir ve şöyle yazılır:

$$
X \to Y
$$

Okunuşu "$X$, $Y$'yi belirler"dir. Kütüphane alanında birkaç örnek:

- $\text{üye no} \to \text{üye adı}, \text{üye e-postası}$ — üye numarası bilinince üyenin
  adı ve adresi bellidir.
- $\text{ISBN} \to \text{kitap başlığı}, \text{kitap yazarı}$
- $\text{şube kodu} \to \text{şube adı}$
- $\text{ödünç no} \to$ diğer bütün öznitelikler.

Son satır, bir önceki konudaki süperanahtar tanımının bağımlılık diliyle yazılışıdır:
$K$ bir süperanahtardır ancak ve ancak $K \to H$ ise. Aday anahtar ise bu özelliği taşıyan
en küçük kümedir. Anahtar kavramı, bağımlılık kavramının özel bir durumudur.

#### Bağımlılık Anlamdan Gelir

Kritik nokta şudur: işlevsel bağımlılık, bugünkü veriden okunmaz. Bugünkü veri bir
bağımlılığı yalnızca **çürütebilir**; doğrulayamaz. Bağımlılık, alanın kuralından gelir.

Aradaki farkı bir örnek üzerinde saymak, bağımlılık adaylarını elemenin en hızlı yoludur:

```js
const basliklar = "odunc_no uye_no uye_ad isbn kitap_baslik sube_kodu sube_ad".split(" ");
const veri = [
  "1001 41 Ayse_Demir 975-01 Kayip_Zaman MRK Merkez",
  "1002 52 Mert_Kaya 975-01 Kayip_Zaman MRK Merkez",
  "1003 41 Ayse_Demir 975-02 Deniz_Fenerleri BHC Bahcelievler",
  "1004 41 Ayse_Demir 975-01 Kayip_Zaman MRK Merkez",
  "1005 63 Ayse_Demir 975-02 Deniz_Fenerleri MRK Merkez",
  "1006 52 Mert_Kaya 975-03 Sessiz_Bahce BHC Bahcelievler",
].map((s) => Object.fromEntries(s.split(" ").map((v, i) => [basliklar[i], v])));

function ihlalSay(sol, sag) {
  const grup = new Map();
  for (const satir of veri) {
    const anahtar = sol.map((a) => satir[a]).join("|");
    const deger = sag.map((a) => satir[a]).join("|");
    if (grup.has(anahtar)) grup.get(anahtar).add(deger);
    else grup.set(anahtar, new Set([deger]));
  }
  return [...grup.values()].filter((d) => d.size > 1).length;
}

const adaylar = [
  [["odunc_no"], ["uye_no"]],
  [["uye_no"], ["uye_ad"]],
  [["uye_ad"], ["uye_no"]],
  [["isbn"], ["kitap_baslik"]],
  [["kitap_baslik"], ["isbn"]],
  [["sube_kodu"], ["sube_ad"]],
  [["uye_no"], ["isbn"]],
];

for (const [sol, sag] of adaylar) {
  const ihlal = ihlalSay(sol, sag);
  const durum = ihlal === 0 ? "veriyle uyumlu" : `${ihlal} grupta ihlal`;
  console.log(`${sol.join(",")} -> ${sag.join(",")}`.padEnd(30), durum);
}
```

```
odunc_no -> uye_no             veriyle uyumlu
uye_no -> uye_ad               veriyle uyumlu
uye_ad -> uye_no               1 grupta ihlal
isbn -> kitap_baslik           veriyle uyumlu
kitap_baslik -> isbn           veriyle uyumlu
sube_kodu -> sube_ad           veriyle uyumlu
uye_no -> isbn                 2 grupta ihlal
```

Çözümleyici, sol taraf değerlerine göre gruplar ve bir grupta birden çok sağ taraf değeri
görürse ihlal sayar. `uye_ad -> uye_no` bir grupta ihlal verir: iki farklı üye aynı adı
taşımaktadır. `uye_no -> isbn` iki grupta ihlal verir: aynı üye birden çok kitap almıştır.

Buradaki asıl ders, "veriyle uyumlu" çıkan satırlardadır. `kitap_baslik -> isbn`
bağımlılığı bu altı satırda çürütülmemiştir, ama geçerli değildir — iki farklı kitabın aynı
başlığı taşıması mümkündür ve o kayıt girildiği gün şema bozulur. Çözümleyici bir eleme
aracıdır: ihlal bulursa bağımlılık **kesin** yoktur, bulmazsa yalnızca **henüz**
çürütülmemiştir. Kararı alanın kuralı verir.

#### Bağımlılık Türleri

Dört ayrım normal formların tanımında kullanılacaktır.

**Önemsiz bağımlılık** (trivial dependency): $Y \subseteq X$ olduğunda $X \to Y$ her
zaman doğrudur. $\text{üye no}, \text{üye adı} \to \text{üye adı}$ bilgi taşımaz.

**Tam bağımlılık** (full dependency): $X \to Y$ geçerli ve $X$'in hiçbir öz alt kümesi
$Y$'yi belirlemiyorsa, bağımlılık tamdır.

**Kısmi bağımlılık** (partial dependency): $X$ bileşik bir anahtarken $Y$, $X$'in bir öz
alt kümesine bağımlıysa bağımlılık kısmidir. Ödünç kaydının anahtarı
$(\text{üye no}, \text{ISBN})$ olsaydı, $\text{üye no} \to \text{üye adı}$ kısmi bir
bağımlılık olurdu: üye adı anahtarın tamamına değil yarısına bağlıdır.

**Geçişli bağımlılık** (transitive dependency): $X \to Y$ ve $Y \to Z$ geçerliyken $Z$,
$X$'e $Y$ üzerinden bağlıdır. Ödünç kaydında
$\text{ödünç no} \to \text{şube kodu} \to \text{şube adı}$ zinciri budur; şube adı ödünç
işlemine doğrudan değil, şube kodu üzerinden bağlıdır.

Kısmi ve geçişli bağımlılıklar, aynı olgunun birden çok satırda tekrar etmesine yol açar —
şube adı, o şubeden yapılan her ödünç işleminde yeniden yazılır. Sıradaki dersin konusu bu
iki bağımlılığın giderilmesidir.

#### Armstrong Aksiyomları

Bağımlılıklar birbirinden türetilebilir. Üç aksiyom, bilinen bir bağımlılık kümesinden
çıkarılabilecek her bağımlılığı üretmeye yeter.

- **Yansıma** (reflexivity): $Y \subseteq X$ ise $X \to Y$.
- **Genişletme** (augmentation): $X \to Y$ ise, her $Z$ için $X \cup Z \to Y \cup Z$.
- **Geçişlilik** (transitivity): $X \to Y$ ve $Y \to Z$ ise $X \to Z$.

Bunlardan üç kullanışlı kural türer: **birleşim** ($X \to Y$ ve $X \to Z$ ise
$X \to Y \cup Z$), **ayrışma** ($X \to Y \cup Z$ ise $X \to Y$ ve $X \to Z$) ve **sözde
geçişlilik**. Ayrışma kuralı, sağ tarafında birden çok öznitelik bulunan bağımlılıkları tek
öznitelikli parçalara bölmeyi meşru kılar; çözümleme genellikle bu biçimde yapılır.

Aksiyom takımının iki özelliği vardır: **sağlamdır** (türetilen her bağımlılık gerçekten
geçerlidir) ve **tamdır** (geçerli her bağımlılık türetilebilir). Bu yüzden normal form
denetimleri elle sezgiyle değil, mekanik olarak yapılabilir.

#### Öznitelik Kapanışı

Türetmeyi elle yapmak yerine bir küme hesaplanır. $X$ kümesinin **öznitelik kapanışı**
(attribute closure) $X^+$, verilen bağımlılıklardan yararlanarak $X$'ten belirlenebilen
bütün özniteliklerin kümesidir. Hesap yinelemelidir: sol tarafı $X^+$ içinde kalan her
bağımlılığın sağ tarafı kümeye eklenir, küme büyümeyi durdurana dek sürer.

Kapanış iki soruyu tek adımda yanıtlar. $X \to Y$ geçerli midir? $Y \subseteq X^+$ ise
evet. $X$ süperanahtar mıdır? $X^+$ bütün öznitelikleri kapsıyorsa evet.

```js
const oznitelikler = "odunc_no uye_no uye_ad isbn kitap_baslik sube_kodu sube_ad".split(" ");
const bagimliliklar = [
  [["odunc_no"], ["uye_no", "isbn", "sube_kodu"]],
  [["uye_no"], ["uye_ad"]],
  [["isbn"], ["kitap_baslik"]],
  [["sube_kodu"], ["sube_ad"]],
];

function kapanis(baslangic) {
  const sonuc = new Set(baslangic);
  let buyudu = true;
  while (buyudu) {
    buyudu = false;
    for (const [sol, sag] of bagimliliklar) {
      if (sol.every((a) => sonuc.has(a))) {
        for (const a of sag) if (sonuc.has(a) === false) { sonuc.add(a); buyudu = true; }
      }
    }
  }
  return [...sonuc];
}

for (const kume of [["odunc_no"], ["uye_no"], ["uye_no", "isbn"]]) {
  const k = kapanis(kume);
  console.log(`{${kume.join(",")}}+ = {${k.join(",")}}`);
  console.log(`  -> ${k.length === oznitelikler.length ? "superanahtar" : "superanahtar degil"}`);
}
```

```
{odunc_no}+ = {odunc_no,uye_no,isbn,sube_kodu,uye_ad,kitap_baslik,sube_ad}
  -> superanahtar
{uye_no}+ = {uye_no,uye_ad}
  -> superanahtar degil
{uye_no,isbn}+ = {uye_no,isbn,uye_ad,kitap_baslik}
  -> superanahtar degil
```

Ödünç numarası tek başına bütün özniteliklere ulaşır, yani süperanahtardır; hiçbir öz alt
kümesi bulunmadığından aynı zamanda aday anahtardır. Üye numarası ile ISBN birlikte bile
şube bilgisine ulaşamaz — çünkü şube, ödünç işlemine bağlıdır, üye ile kitabın çiftine
değil.

Kapanış hesabı, Veri Yapıları kursundaki geçişli kapanış fikrinin bağımlılıklar üzerine
uygulanmış hâlidir: bir düğümden ulaşılabilen düğümler kümesi yerine, bir öznitelik
kümesinden belirlenebilen öznitelikler kümesi bulunur.

#### Bağımlılığın Şemadaki Karşılığı

Bir bağımlılık şemada iki biçimde görünür. Sol tarafı bir aday anahtar ise bağımlılık
zaten anahtar kısıtıyla uygulanır — anahtar tekil olduğu için her değer bir kez geçer ve
sağ taraf o satırda bir kez yazılır. Sol tarafı anahtar değilse bağımlılık şemaya
yazılamaz; motorun onu bilmesinin bir yolu yoktur ve ihlal edilmesini engelleyecek hiçbir
kısıt kurulamaz.

Buradan normalizasyonun tek cümlelik gerekçesi çıkar: **her işlevsel bağımlılığın sol
tarafı bir anahtar olacak biçimde bağıntıları bölmek**, bağımlılıkların şema tarafından
uygulanmasını sağlar. Sıradaki ders bu bölme işlemini adım adım tanımlar.

#### Özet

- $X \to Y$, $X$ üzerinde eşleşen iki satırın $Y$ üzerinde de eşleşmesi kuralıdır;
  süperanahtar, sağ tarafı bütün öznitelikler olan bağımlılığın sol tarafıdır.
- Bağımlılık alanın kuralından gelir; veri örneği onu yalnızca çürütebilir, doğrulayamaz.
- Kısmi bağımlılık bileşik anahtarın parçasına, geçişli bağımlılık bir ara öznitelik
  üzerinden kurulur; ikisi de satır tekrarına yol açar.
- Armstrong aksiyomları sağlam ve tamdır, bu yüzden bağımlılık çıkarımı mekanik olarak
  yapılabilir.
- Öznitelik kapanışı, bir bağımlılığın geçerliliğini ve bir kümenin süperanahtar olup
  olmadığını tek hesapla yanıtlar.

#### Sonraki Adım

Bu ders, tekrarın kaynağını adlandırdı: sol tarafı anahtar olmayan bağımlılıklar. Sıradaki
ders bunları gidermenin basamaklarını tanımlar. Birinci normal form değerlerin
bölünmezliğini, ikinci normal form kısmi bağımlılığın kaldırılmasını, üçüncü normal form
geçişli bağımlılığın kaldırılmasını ister. Her basamakta aynı soru sorulacak: bu bölme,
hangi güncelleme anomalisini ortadan kaldırıyor?

### Normal Formlar

Source: https://academia.sh/tr/kurslar/iliskisel-kuram/normal-formlar

Güncelleme anomalilerinin üç türü; birinci, ikinci ve üçüncü normal formun tanımı; her basamağın hangi anomaliyi kaldırdığının sorgu çıktısıyla gösterimi.

Önceki ders tekrarın kaynağını adlandırdı: sol tarafı anahtar olmayan işlevsel
bağımlılıklar. Bu dersin sorusu şudur: bu bağımlılıklar hangi sırayla giderilir ve her
adım hangi somut sorunu ortadan kaldırır?

Normalizasyonun ölçütü estetik değildir. Ölçüt, **güncelleme anomalileridir** (update
anomalies): bir bağıntının yapısı yüzünden ortaya çıkan, veriyi tutarsız bırakan
durumlar. Üç türü vardır.

- **Ekleme anomalisi** (insertion anomaly): bir olguyu kaydetmek için ilgisiz başka bir
  olgunun da bilinmesi gerekir. Henüz hiç ödünç verilmemiş bir şube, ödünç kayıtlarının
  tutulduğu tabloya yazılamaz.
- **Silme anomalisi** (deletion anomaly): bir olgu silinirken ikinci bir olgu da
  kaybolur. Bir kitabın tek ödünç kaydı silindiğinde kitabın kendisi de kayıtlardan çıkar.
- **Güncelleme anomalisi** (modification anomaly): aynı olgu birden çok satırda yazılı
  olduğu için değişiklik hepsine uygulanmalıdır; biri atlanırsa veri kendi içinde çelişir.

Normal formlar, bu üç anomaliyi doğuran yapıları basamak basamak eleyen tanımlardır.

#### Birinci Normal Form

**Birinci normal form** (first normal form), her satır–sütun kesişiminde tek ve bölünmez
bir değer bulunmasını ister. Liste, iç içe yapı ya da tekrarlanan sütun grubu bulunmaz.

Kuralın bedeli somuttur. Telefon numaraları tek bir metin sütununda virgülle ayrılmış
tutulduğunda, bir numarayı aramak dizgi eşlemesine döner ve dizgi eşlemesi yanlış eşleşir:

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode box
CREATE TABLE uye_duz (uye_no INTEGER PRIMARY KEY, ad TEXT, telefonlar TEXT);
INSERT INTO uye_duz VALUES (41, 'Ayse Demir',   '0312-555-0101,0312-555-0102'),
                           (52, 'Mert Kaya',    '0312-555-0110'),
                           (63, 'Selin Ozturk', '0312-555-011');

CREATE TABLE uye_telefon (uye_no INTEGER, telefon TEXT, PRIMARY KEY (uye_no, telefon));
INSERT INTO uye_telefon VALUES (41, '0312-555-0101'), (41, '0312-555-0102'),
                               (52, '0312-555-0110'), (63, '0312-555-011');

SELECT 'tek sutunda liste' AS yazim, COUNT(*) AS bulunan
  FROM uye_duz WHERE telefonlar LIKE '%0312-555-011%'
UNION ALL
SELECT 'ayri bagintida', COUNT(*)
  FROM uye_telefon WHERE telefon = '0312-555-011';
SQL
```

```
┌───────────────────┬─────────┐
│       yazim       │ bulunan │
├───────────────────┼─────────┤
│ tek sutunda liste │ 2       │
│ ayri bagintida    │ 1       │
└───────────────────┴─────────┘
```

Aranan numara `0312-555-011` idi ve tek sahibi vardı. Dizgi araması, o numarayı önek
olarak içeren `0312-555-0110` kaydını da getirdi. Numaralar ayrı bir bağıntıda tutulduğunda
karşılaştırma eşitlik olur ve yanlış eşleşme kalkar; ayrıca numaraya kısıt yazılabilir,
tekilliği denetlenebilir ve tek bir numara silinebilir.

Aynı kural "tekrarlanan sütun grubu" biçimi için de geçerlidir. `telefon_1`, `telefon_2`,
`telefon_3` sütunları listeyi yatay yazmaktan başka bir şey değildir; dördüncü numara
geldiğinde şema değişikliği gerektirir.

#### İkinci Normal Form

**İkinci normal form** (second normal form) birinci normal formu sağlayan ve **anahtar
dışı hiçbir özniteliğin aday anahtarın öz alt kümesine bağımlı olmadığı** bağıntıyı
tanımlar. Kısacası kısmi bağımlılık bulunmaz. Tanım yalnızca bileşik anahtarlı
bağıntılarda anlam taşır; tek sütunluk anahtarda öz alt küme yoktur.

Her şubedeki kitap adedini tutan bağıntının anahtarı `(isbn, sube_kodu)` çiftidir. Kitabın
başlığı yalnız ISBN'e, şubenin adı yalnız şube koduna bağlıdır — ikisi de kısmi
bağımlılıktır:

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode box
CREATE TABLE stok_duz (
  isbn         TEXT    NOT NULL,
  sube_kodu    TEXT    NOT NULL,
  adet         INTEGER NOT NULL,
  kitap_baslik TEXT    NOT NULL,
  sube_ad      TEXT    NOT NULL,
  PRIMARY KEY (isbn, sube_kodu)
);
INSERT INTO stok_duz VALUES
  ('975-01', 'MRK', 3, 'Kayip Zaman',     'Merkez'),
  ('975-01', 'BHC', 1, 'Kayip Zaman',     'Bahcelievler'),
  ('975-02', 'MRK', 2, 'Deniz Fenerleri', 'Merkez'),
  ('975-02', 'BHC', 4, 'Deniz Fenerleri', 'Bahcelievler'),
  ('975-03', 'BHC', 1, 'Sessiz Bahce',    'Bahcelievler');

UPDATE stok_duz SET sube_ad = 'Bahcelievler Subesi'
 WHERE sube_kodu = 'BHC' AND isbn = '975-01';

SELECT sube_kodu, COUNT(*) AS satir, COUNT(DISTINCT sube_ad) AS farkli_ad
FROM stok_duz GROUP BY sube_kodu;
SQL
```

```
┌───────────┬───────┬───────────┐
│ sube_kodu │ satir │ farkli_ad │
├───────────┼───────┼───────────┤
│ BHC       │ 3     │ 2         │
│ MRK       │ 2     │ 1         │
└───────────┴───────┴───────────┘
```

Şube adını değiştiren güncelleme bir satıra uygulandı ve şube artık iki farklı adla
kayıtlı. Motor bunu engelleyemez, çünkü "aynı şube kodunun aynı adı taşıması" kuralı
şemada yazılı değildir — yazılamaz da, bağımlılığın sol tarafı anahtar değildir.

Ayrıştırma, kısmi bağımlılığın sol tarafını kendi bağıntısının anahtarı yapar: kitap
bilgisi `kitap` bağıntısına, şube bilgisi `sube` bağıntısına taşınır, `stok` yalnız
anahtar ile ona tam bağımlı `adet` sütununu tutar.

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode box
PRAGMA foreign_keys = ON;
CREATE TABLE kitap (isbn TEXT PRIMARY KEY NOT NULL, baslik TEXT NOT NULL);
CREATE TABLE sube  (sube_kodu TEXT PRIMARY KEY NOT NULL, ad TEXT NOT NULL);
CREATE TABLE stok  (
  isbn      TEXT    NOT NULL REFERENCES kitap (isbn),
  sube_kodu TEXT    NOT NULL REFERENCES sube (sube_kodu),
  adet      INTEGER NOT NULL,
  PRIMARY KEY (isbn, sube_kodu)
);
INSERT INTO kitap VALUES ('975-01', 'Kayip Zaman'), ('975-02', 'Deniz Fenerleri'),
                         ('975-03', 'Sessiz Bahce');
INSERT INTO sube  VALUES ('MRK', 'Merkez'), ('BHC', 'Bahcelievler'), ('KDK', 'Kadikoy');
INSERT INTO stok  VALUES ('975-01', 'MRK', 3), ('975-01', 'BHC', 1), ('975-02', 'MRK', 2),
                         ('975-02', 'BHC', 4), ('975-03', 'BHC', 1);

UPDATE sube SET ad = 'Bahcelievler Subesi' WHERE sube_kodu = 'BHC';

SELECT changes() AS guncellenen_satir,
       (SELECT COUNT(*) FROM sube WHERE sube_kodu = 'KDK') AS stoksuz_sube;
SQL
```

```
┌───────────────────┬──────────────┐
│ guncellenen_satir │ stoksuz_sube │
├───────────────────┼──────────────┤
│ 1                 │ 1            │
└───────────────────┴──────────────┘
```

Şube adı tek bir satırda tutulduğu için tek güncelleme yeter ve tutarsızlık kurulamaz.
Aynı ayrıştırma ekleme anomalisini de kaldırır: hiç kitabı bulunmayan Kadıköy şubesi
kayıtlıdır — düz tabloda bu satır yazılamazdı, çünkü anahtarın ISBN parçası boş kalırdı.
Değişen satır sayısını veren işlevin adı motora göre değişir.

#### Üçüncü Normal Form

**Üçüncü normal form** (third normal form) ikinci normal formu sağlayan ve **anahtar dışı
hiçbir özniteliğin başka bir anahtar dışı özniteliğe bağımlı olmadığı** bağıntıyı
tanımlar. Kısacası geçişli bağımlılık bulunmaz.

Ödünç kaydında anahtar tek sütundur, dolayısıyla kısmi bağımlılık yoktur; buna karşılık
`odunc_no → uye_no → uye_ad, uye_eposta` zinciri vardır. Üye bilgisi ödünç işlemine
doğrudan değil, üye numarası üzerinden bağlıdır:

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode box
CREATE TABLE odunc_duz (
  odunc_no     INTEGER PRIMARY KEY,
  uye_no       INTEGER NOT NULL,
  uye_ad       TEXT    NOT NULL,
  uye_eposta   TEXT    NOT NULL,
  isbn         TEXT    NOT NULL,
  kitap_baslik TEXT    NOT NULL,
  alis_tarihi  TEXT    NOT NULL
);
INSERT INTO odunc_duz VALUES
  (1001, 41, 'Ayse Demir', 'ayse@ornek.org', '975-01', 'Kayip Zaman',     '2025-03-02'),
  (1002, 52, 'Mert Kaya',  'mert@ornek.org', '975-01', 'Kayip Zaman',     '2025-03-04'),
  (1003, 41, 'Ayse Demir', 'ayse@ornek.org', '975-02', 'Deniz Fenerleri', '2025-03-05'),
  (1004, 41, 'Ayse Demir', 'ayse@ornek.org', '975-01', 'Kayip Zaman',     '2025-03-20'),
  (1005, 52, 'Mert Kaya',  'mert@ornek.org', '975-03', 'Sessiz Bahce',    '2025-03-22');

UPDATE odunc_duz SET uye_eposta = 'ayse.demir@ornek.org' WHERE odunc_no = 1001;
DELETE FROM odunc_duz WHERE odunc_no = 1005;

SELECT (SELECT COUNT(DISTINCT uye_eposta) FROM odunc_duz WHERE uye_no = 41) AS uye41_eposta,
       (SELECT COUNT(*) FROM odunc_duz WHERE isbn = '975-03')               AS kitap975_03;
SQL
```

```
┌──────────────┬─────────────┐
│ uye41_eposta │ kitap975_03 │
├──────────────┼─────────────┤
│ 2            │ 0           │
└──────────────┴─────────────┘
```

İki anomali aynı anda görünür. Bir satırda düzeltilen e-posta adresi, aynı üyenin iki
farklı adrese sahip görünmesine yol açtı. Tek ödünç kaydı silinen kitap ise veritabanından
tamamen kayboldu — başlığı yalnız o satırda yazılıydı.

Ayrıştırma, geçişli zincirin ortasını kendi bağıntısının anahtarı yapar:

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode box
PRAGMA foreign_keys = ON;
CREATE TABLE uye   (uye_no INTEGER PRIMARY KEY, ad TEXT NOT NULL, eposta TEXT NOT NULL);
CREATE TABLE kitap (isbn TEXT PRIMARY KEY NOT NULL, baslik TEXT NOT NULL);
CREATE TABLE odunc (
  odunc_no    INTEGER PRIMARY KEY,
  uye_no      INTEGER NOT NULL REFERENCES uye (uye_no),
  isbn        TEXT    NOT NULL REFERENCES kitap (isbn),
  alis_tarihi TEXT    NOT NULL
);
INSERT INTO uye   VALUES (41, 'Ayse Demir', 'ayse@ornek.org'),
                         (52, 'Mert Kaya',  'mert@ornek.org');
INSERT INTO kitap VALUES ('975-01', 'Kayip Zaman'), ('975-02', 'Deniz Fenerleri'),
                         ('975-03', 'Sessiz Bahce');
INSERT INTO odunc VALUES (1001, 41, '975-01', '2025-03-02'), (1002, 52, '975-01', '2025-03-04'),
                         (1003, 41, '975-02', '2025-03-05'), (1004, 41, '975-01', '2025-03-20'),
                         (1005, 52, '975-03', '2025-03-22');

UPDATE uye SET eposta = 'ayse.demir@ornek.org' WHERE uye_no = 41;
DELETE FROM odunc WHERE odunc_no = 1005;

SELECT (SELECT COUNT(DISTINCT eposta) FROM uye WHERE uye_no = 41) AS uye41_eposta,
       (SELECT COUNT(*) FROM kitap WHERE isbn = '975-03')         AS kitap975_03;
SQL
```

```
┌──────────────┬─────────────┐
│ uye41_eposta │ kitap975_03 │
├──────────────┼─────────────┤
│ 1            │ 1           │
└──────────────┴─────────────┘
```

Aynı iki deyim çalıştırıldı; sonuç değişti. E-posta tek yerde tutulduğu için tek adres
kaldı, kitap kaydı ödünç kaydından bağımsız olduğu için silmeden etkilenmedi. İkisi de
sorgu yazımıyla değil, şema yapısıyla sağlandı.

#### Ayrıştırmanın İki Koşulu

Bağıntı bölmek her zaman güvenli değildir. İki koşul aranır.

**Kayıpsız birleştirme** (lossless join): ayrılan bağıntılar yeniden birleştirildiğinde
başlangıçtaki satırlar aynen elde edilmeli, fazladan satır doğmamalıdır. Koşul şudur:
ortak öznitelik kümesi, parçalardan en az birinin süperanahtarı olmalıdır. Yukarıdaki
ayrıştırmaların hepsi bunu sağlar, çünkü bölme her zaman bir bağımlılığın sol tarafından
yapıldı.

**Bağımlılık koruma** (dependency preservation): başlangıçtaki bağımlılıklar, parçalar
üzerinde ayrı ayrı denetlenebilmelidir. Korunmayan bir bağımlılık, ancak parçaları
birleştirerek sınanabilir — yani kısıt olarak yazılamaz.

Üçüncü normal forma kadar olan ayrıştırmalarda ikisi de birlikte sağlanabilir. Sıradaki
dersin konusu, bu iki koşulun ayrıştığı durumdur.

#### Özet

- Normalizasyonun ölçütü ekleme, silme ve güncelleme anomalilerinin ortadan kalkmasıdır.
- Birinci normal form her hücrede tek bölünmez değer ister; liste tutan sütun, aramayı
  dizgi eşlemesine indirger ve yanlış eşleşme üretir.
- İkinci normal form kısmi bağımlılığı, üçüncü normal form geçişli bağımlılığı kaldırır;
  ikisi de bölmeyi bağımlılığın sol tarafından yapar.
- Normalleştirilmiş şemada bir olgu tek yerde yazılıdır; güncelleme tek satıra uygulanır
  ve tutarsızlık kurulamaz.
- Ayrıştırma kayıpsız birleştirme ve bağımlılık koruma koşullarını sağlamalıdır; ortak
  öznitelik kümesi parçalardan birinin süperanahtarı olduğunda birleştirme kayıpsızdır.

#### Sonraki Adım

Üçüncü normal form, anahtar dışı özniteliklere odaklanır. Peki bağımlılığın sağ tarafı
anahtarın bir parçasıysa? Tanım gereği o öznitelik anahtar dışı değildir ve üçüncü normal
form ihlal edilmiş sayılmaz — ama tekrar sürer. Sıradaki ders bu boşluğu kapatan
Boyce–Codd normal formunu tanımlar, kayıpsız birleştirme ile bağımlılık korumanın birlikte
sağlanamadığı durumu gösterir ve çok değerli bağımlılıkla dördüncü normal forma geçer.

### İleri Normal Formlar

Source: https://academia.sh/tr/kurslar/iliskisel-kuram/ileri-normal-formlar

Boyce–Codd normal formunun tanımı ve üçüncü normal formdan farkı, bağımlılık korumanın yitirildiği ayrıştırma, çok değerli bağımlılık ile dördüncü normal form ve normalizasyonun durma noktası.

Üçüncü normal form, anahtar dışı özniteliklere odaklanır: bunların ne anahtarın parçasına
ne de başka bir anahtar dışı özniteliğe bağlı olmasını ister. Peki bir bağımlılığın sağ
tarafı anahtarın parçasıysa? Tanım gereği o öznitelik anahtar dışı değildir, dolayısıyla
üçüncü normal form ihlal edilmiş sayılmaz — buna karşılık aynı olgu birden çok satırda
tekrar etmeye devam eder. Bu dersin sorusu şudur: bu boşluk nasıl kapatılır ve kapatmanın
bedeli nedir?

#### Boyce–Codd Normal Formu

**Boyce–Codd normal formu** (Boyce–Codd normal form) tek ve daha sıkı bir koşul koyar:
**önemsiz olmayan her işlevsel bağımlılığın sol tarafı bir süperanahtar olmalıdır.**
Üçüncü normal formdaki "anahtar dışı öznitelik" ayrıcalığı kalkar; hangi öznitelik olursa
olsun, onu belirleyen kümenin anahtar olması gerekir.

İki tanımın ayrıştığı yer, aday anahtarların örtüştüğü bağıntılardır. Kütüphanede bir
kitabın hangi rafta durduğunu tutan bağıntıyı düşünelim. Kural şudur: bir kitap her şubede
tek bir rafta durur ve her raf tek bir şubeye aittir. Buradan iki aday anahtar çıkar —
`(isbn, sube_kodu)` ve `(isbn, raf)` — ve bir de bağımlılık: `raf → sube_kodu`.

Bu bağıntı üçüncü normal formdadır: `sube_kodu` bir aday anahtarın parçası olduğu için
anahtar dışı sayılmaz. Ama `raf` bir süperanahtar değildir, dolayısıyla Boyce–Codd
koşulunu bozar. Sonucu şemanın uygulayamadığı bir tutarsızlıktır:

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode box
CREATE TABLE yerlesim (
  isbn      TEXT NOT NULL,
  sube_kodu TEXT NOT NULL,
  raf       TEXT NOT NULL,
  PRIMARY KEY (isbn, sube_kodu),
  UNIQUE (isbn, raf)
);
INSERT INTO yerlesim VALUES ('975-01', 'MRK', 'R12'),
                            ('975-02', 'MRK', 'R12'),
                            ('975-03', 'BHC', 'R07');
INSERT INTO yerlesim VALUES ('975-04', 'BHC', 'R12');
SELECT raf, COUNT(DISTINCT sube_kodu) AS farkli_sube FROM yerlesim GROUP BY raf;
SQL
```

```
┌─────┬─────────────┐
│ raf │ farkli_sube │
├─────┼─────────────┤
│ R07 │ 1           │
│ R12 │ 2           │
└─────┴─────────────┘
```

Her iki aday anahtar da şemada yazılıdır ve hiçbiri ihlal edilmemiştir; buna rağmen R12
rafı iki ayrı şubede görünmektedir. `raf → sube_kodu` bağımlılığının sol tarafı anahtar
olmadığı için o kural şemaya yazılamaz, motorun onu bilmesinin bir yolu yoktur. Ayrıca
rafın şubesi, o raftaki her kitap için yeniden yazılmaktadır — tekrar sürer.

#### Ayrıştırma ve Bağımlılık Korumanın Yitirilmesi

Boyce–Codd ayrıştırması, bozan bağımlılığın sol tarafını yeni bir bağıntının anahtarı
yapar: `raf(raf, sube_kodu)` ve `kitap_rafi(isbn, raf)`.

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode box
PRAGMA foreign_keys = ON;
CREATE TABLE raf (raf TEXT PRIMARY KEY NOT NULL, sube_kodu TEXT NOT NULL);
CREATE TABLE kitap_rafi (
  isbn TEXT NOT NULL,
  raf  TEXT NOT NULL REFERENCES raf (raf),
  PRIMARY KEY (isbn, raf)
);
INSERT INTO raf VALUES ('R12', 'MRK'), ('R07', 'BHC'), ('R21', 'MRK');
INSERT INTO kitap_rafi VALUES ('975-01', 'R12'), ('975-02', 'R12'), ('975-03', 'R07');
INSERT INTO kitap_rafi VALUES ('975-01', 'R21');
SELECT (SELECT COUNT(DISTINCT sube_kodu) FROM raf WHERE raf = 'R12') AS r12_sube,
       (SELECT COUNT(*) FROM kitap_rafi k JOIN raf r ON r.raf = k.raf
         WHERE k.isbn = '975-01' AND r.sube_kodu = 'MRK')            AS kitap975_01_mrk;
SQL
```

```
┌──────────┬─────────────────┐
│ r12_sube │ kitap975_01_mrk │
├──────────┼─────────────────┤
│ 1        │ 2               │
└──────────┴─────────────────┘
```

İlk sütun kazancı gösterir: raf artık tek bir şubeye aittir ve bu kural anahtarla
uygulanmaktadır. İkinci sütun bedeli gösterir: `975-01` numaralı kitap Merkez şubesinde
iki ayrı rafta durmaktadır. Ayrıştırmadan önce bu satır yazılamazdı, çünkü
`(isbn, sube_kodu)` anahtardı.

Yitirilen şey `(isbn, sube_kodu) → raf` bağımlılığıdır. Bu bağımlılık artık iki bağıntının
hiçbirinde ifade edilemez; sınamak için ikisini birleştirmek gerekir. Buna **bağımlılık
korumanın yitirilmesi** denir ve Boyce–Codd normal formunun bilinen sınırıdır: her bağıntı
kayıpsız biçimde Boyce–Codd formuna ayrıştırılabilir, ancak bunu yaparken bağımlılık
korumak her zaman mümkün değildir.

Karar bu noktada tasarımcıya kalır. Hangi kuralın şemada uygulanması daha önemliyse ona
göre seçilir: rafın tek şubeye ait olması mı, yoksa bir kitabın şube başına tek rafta
durması mı? Üçüncü normal formda kalıp ikinci kuralı korumak da geçerli bir karardır —
normal formlar bir merdiven değil, ödünleşim listesidir.

#### Çok Değerli Bağımlılık

Boyce–Codd normal formu işlevsel bağımlılıkları tüketir, ama tekrarın tek kaynağı bunlar
değildir. Bir üyenin birden çok ilgi alanı ve birden çok telefon numarası varsa ve bu iki
küme birbirinden bağımsızsa, üçünü tek bağıntıda tutmak kartezyen çarpım üretir. Buna
**çok değerli bağımlılık** (multivalued dependency) denir.

Bağıntı Boyce–Codd formundadır — anahtar bütün özniteliklerden oluşur, dolayısıyla
önemsiz olmayan hiçbir işlevsel bağımlılık yoktur. Yine de her yeni telefon numarası, üye
kaç ilgi alanı taşıyorsa o kadar satır eklemeyi gerektirir:

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode box
CREATE TABLE uye_birlesik (uye_no INTEGER, ilgi TEXT, telefon TEXT,
                           PRIMARY KEY (uye_no, ilgi, telefon));
INSERT INTO uye_birlesik VALUES
  (41, 'tarih', '0312-555-0101'), (41, 'tarih', '0312-555-0102'),
  (41, 'tarih', '0312-555-0103'), (41, 'siir',  '0312-555-0101'),
  (41, 'siir',  '0312-555-0102'), (41, 'siir',  '0312-555-0103');

CREATE TABLE uye_ilgi    (uye_no INTEGER, ilgi TEXT,    PRIMARY KEY (uye_no, ilgi));
CREATE TABLE uye_telefon (uye_no INTEGER, telefon TEXT, PRIMARY KEY (uye_no, telefon));
INSERT INTO uye_ilgi    VALUES (41, 'tarih'), (41, 'siir');
INSERT INTO uye_telefon VALUES (41, '0312-555-0101'), (41, '0312-555-0102'),
                               (41, '0312-555-0103');

INSERT INTO uye_ilgi     VALUES (41, 'gezi');
INSERT INTO uye_birlesik VALUES (41, 'gezi', '0312-555-0101');

SELECT (SELECT COUNT(*) FROM uye_birlesik) AS birlesik_satir,
       (SELECT COUNT(*) FROM uye_ilgi) + (SELECT COUNT(*) FROM uye_telefon) AS ayrik_satir,
       (SELECT COUNT(*) FROM uye_ilgi i JOIN uye_telefon t USING (uye_no)) AS birlestirilmis;
SQL
```

```
┌────────────────┬─────────────┬────────────────┐
│ birlesik_satir │ ayrik_satir │ birlestirilmis │
├────────────────┼─────────────┼────────────────┤
│ 7              │ 6           │ 9              │
└────────────────┴─────────────┴────────────────┘
```

Üçüncü ilgi alanı eklenirken tek satır yazıldı ve birleşik bağıntı yedi satırda kaldı;
tutarlı olması için dokuz satır olmalıydı. Sessiz bir tutarsızlık doğdu: `gezi` ilgi
alanına sahip üyenin yalnız bir telefonu varmış gibi görünüyor. Ayrık bağıntılarda toplam
altı satır tutuldu ve birleştirme doğru dokuz satırı üretti. Fark, satır sayısının
$m \times n$ yerine $m + n$ ile büyümesidir.

**Dördüncü normal form** (fourth normal form), önemsiz olmayan her çok değerli bağımlılığın
sol tarafının süperanahtar olmasını ister. Uygulamadaki karşılığı yalın bir kuraldır:
birbirinden bağımsız iki çok değerli olgu aynı bağıntıda tutulmaz.

#### Daha İleri Formlar

**Beşinci normal form** (fifth normal form), bağıntının ikiye değil ancak üç ya da daha
çok parçaya bölündüğünde kayıpsız ayrıştırılabildiği durumları ele alır; bu durumu
doğuran kısıta **birleştirme bağımlılığı** (join dependency) denir. Gerçek şemalarda
seyrek görülür ve genellikle üçlü bir ilişkinin gerçekte üç ikili ilişkiden oluştuğu
durumlarda ortaya çıkar.

**Kilit ve anahtar normal formu** (domain-key normal form) ise ölçütü en genel biçimde
koyar: bağıntı üzerindeki her kısıt, yalnız alan tanımlarından ve anahtarlardan
türetilebilmelidir. Bu koşulu sağlayan bir bağıntıda hiçbir güncelleme anomalisi
bulunamaz. Kuramsal bir hedef olarak değerlidir; her bağıntı için ulaşılabilir değildir ve
ulaşıldığının sınanması genel olarak yapılamaz.

#### Nerede Durmalı

Uygulamada tasarımların çoğu üçüncü normal formda ya da Boyce–Codd formunda durur. Karar
ölçütü, formun numarası değil şu iki sorudur: hangi anomaliler gerçekten ortaya çıkabilir
ve bunları önleyen kısıt şemada yazılabiliyor mu?

İzlenecek sıra da bellidir. Önce alanın kuralları — yani işlevsel ve çok değerli
bağımlılıklar — yazılır. Sonra aday anahtarlar kapanış hesabıyla bulunur. Sonra sol tarafı
anahtar olmayan bağımlılıklar aranır; her biri ya bir ayrıştırmaya ya da bilinçli bir
istisnaya dönüşür. İstisna kararı yazılı kalmalıdır, çünkü bir sonraki tasarımcı için o
yapı hata gibi görünecektir.

#### Özet

- Boyce–Codd normal formu, önemsiz olmayan her bağımlılığın sol tarafının süperanahtar
  olmasını ister; üçüncü normal formdaki anahtar dışı ayrıcalığını kaldırır.
- İki form yalnız aday anahtarların örtüştüğü bağıntılarda ayrışır; üçüncü normal formda
  kalan bir şema, uygulanamayan bir bağımlılık barındırabilir.
- Her bağıntı kayıpsız biçimde Boyce–Codd formuna ayrıştırılabilir, ancak bağımlılık
  koruma her zaman sağlanamaz; hangi kuralın şemada uygulanacağı bir tasarım kararıdır.
- Bağımsız iki çok değerli olgu aynı bağıntıda tutulduğunda satır sayısı çarpımla büyür ve
  eksik satır sessiz tutarsızlık üretir; dördüncü normal form bunu ayırır.
- Normal formlar bir merdiven değil ödünleşim listesidir; durma noktası, gerçekten ortaya
  çıkabilecek anomalilere göre seçilir.

#### Sonraki Adım

Buraya kadarki bütün adımlar aynı yönde ilerledi: tekrarı kaldır, bağıntıyı böl, kuralı
şemaya yazdır. Bunun ödenmemiş bir bedeli vardır — bölünen her bağıntı, okuma sırasında
yeniden birleştirilmek zorundadır. Sıradaki ders bu bedeli ölçer ve ters yöndeki kararı
ele alır: tekrarı bilerek geri koymak hangi koşulda savunulabilir ve karşılığında hangi
yazma maliyeti üstlenilir?

### Denormalizasyon

Source: https://academia.sh/tr/kurslar/iliskisel-kuram/denormalizasyon

Normalleştirilmiş şemanın okuma bedelinin ölçülmesi, denormalizasyonun yazma maliyeti, türetilmiş sütunların her yazma yolunu kapsama zorunluluğu ve denormalizasyon kararının ölçütleri.

Normalizasyon basamaklarının hepsi aynı yönde ilerledi: tekrarı kaldır, bağıntıyı böl,
kuralı şemaya yazdır. Önceki dersin sonunda söylenen bedel burada ölçülecek — bölünen her
bağıntı, okuma sırasında yeniden birleştirilir. Bu dersin sorusu şudur: bu bedel nasıl
sayılır ve tekrarı bilerek geri koymak hangi koşulda savunulabilir?

**Denormalizasyon** (denormalization), normalleştirilmiş bir şemaya okuma başarımı için
bilinçli olarak tekrar eklemektir. Tanımdaki iki sözcük belirleyicidir: **bilinçli**
(hatayla değil, ölçüme dayanarak) ve **normalleştirilmiş bir şemaya** (normalleştirmeyi
hiç yapmamak denormalizasyon değil, eksik tasarımdır).

#### Okumanın Bedeli

Ödünç listesini ekranda göstermek için üye adı, kitap başlığı ve şube adı gerekir. Bu
bilgiler normalleştirilmiş şemada dört ayrı bağıntıda durur. Motorun sorguyu yürütürken
kaç bağıntıya eriştiği, plan çıktısından sayılabilir:

```sh
sqlite3 :memory: <<'SQL'
CREATE TABLE sube  (sube_kodu TEXT PRIMARY KEY NOT NULL, ad TEXT NOT NULL);
CREATE TABLE uye   (uye_no INTEGER PRIMARY KEY, ad TEXT NOT NULL);
CREATE TABLE kitap (isbn TEXT PRIMARY KEY NOT NULL, baslik TEXT NOT NULL);
CREATE TABLE odunc (
  odunc_no    INTEGER PRIMARY KEY,
  uye_no      INTEGER NOT NULL REFERENCES uye (uye_no),
  isbn        TEXT    NOT NULL REFERENCES kitap (isbn),
  sube_kodu   TEXT    NOT NULL REFERENCES sube (sube_kodu),
  alis_tarihi TEXT    NOT NULL
);
CREATE TABLE odunc_ozet (
  odunc_no     INTEGER PRIMARY KEY,
  uye_ad       TEXT NOT NULL,
  kitap_baslik TEXT NOT NULL,
  sube_ad      TEXT NOT NULL,
  alis_tarihi  TEXT NOT NULL
);
EXPLAIN QUERY PLAN
SELECT u.ad, k.baslik, s.ad, o.alis_tarihi
FROM odunc o
JOIN uye   u ON u.uye_no    = o.uye_no
JOIN kitap k ON k.isbn      = o.isbn
JOIN sube  s ON s.sube_kodu = o.sube_kodu
WHERE o.alis_tarihi >= '2025-03-01';
EXPLAIN QUERY PLAN
SELECT uye_ad, kitap_baslik, sube_ad, alis_tarihi
FROM odunc_ozet WHERE alis_tarihi >= '2025-03-01';
SQL
```

```
QUERY PLAN
|--SCAN o
|--SEARCH u USING INTEGER PRIMARY KEY (rowid=?)
|--SEARCH k USING INDEX sqlite_autoindex_kitap_1 (isbn=?)
`--SEARCH s USING INDEX sqlite_autoindex_sube_1 (sube_kodu=?)
QUERY PLAN
`--SCAN odunc_ozet
```

Birinci planda dört erişim adımı, ikincisinde bir tane vardır. **Plan çıktısının biçimi ve
plan isteme yolu motora göre değişir**; sayılan şey biçimden bağımsızdır — sorgunun kaç
bağıntıya dokunduğu ve her birine hangi yoldan eriştiği. Ödünç tablosu taranıyor, kalan
üçüne anahtar üzerinden tek satırlık erişim yapılıyor. Yani tarama başına üç ek arama;
milyon satırlık bir taramada üç milyon arama.

Birleştirmelerin bedeli her zaman bu kadar belirgin değildir. Anahtar üzerinden yapılan
erişimler ucuzdur ve küçük bağıntılar bellekte kalır. Ölçmeden denormalizasyona geçmek,
bu dersin savunmadığı karardır.

#### Yazmanın Bedeli

Tekrar geri konduğunda maliyet yer değiştirir. Şubenin adı değiştiğinde
normalleştirilmiş şemada bir satır, özet tabloda o şubeye ait her satır güncellenir:

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode box
CREATE TABLE sube (sube_kodu TEXT PRIMARY KEY NOT NULL, ad TEXT NOT NULL);
CREATE TABLE odunc_ozet (odunc_no INTEGER PRIMARY KEY, sube_kodu TEXT NOT NULL,
                         sube_ad TEXT NOT NULL);
INSERT INTO sube VALUES ('MRK', 'Merkez'), ('BHC', 'Bahcelievler');
INSERT INTO odunc_ozet VALUES (1001, 'MRK', 'Merkez'), (1002, 'MRK', 'Merkez'),
                              (1003, 'BHC', 'Bahcelievler'), (1004, 'MRK', 'Merkez'),
                              (1005, 'MRK', 'Merkez');

UPDATE sube SET ad = 'Merkez Sube' WHERE sube_kodu = 'MRK';
SELECT changes() AS kaynak_satiri;

UPDATE odunc_ozet SET sube_ad = 'Merkez Sube' WHERE sube_kodu = 'MRK';
SELECT changes() AS ozet_satiri;

INSERT INTO odunc_ozet VALUES (1006, 'MRK', 'Merkez');
SELECT sube_kodu, COUNT(DISTINCT sube_ad) AS farkli_ad FROM odunc_ozet GROUP BY sube_kodu;
SQL
```

```
┌───────────────┐
│ kaynak_satiri │
├───────────────┤
│ 1             │
└───────────────┘
┌─────────────┐
│ ozet_satiri │
├─────────────┤
│ 4           │
└─────────────┘
┌───────────┬───────────┐
│ sube_kodu │ farkli_ad │
├───────────┼───────────┤
│ BHC       │ 1         │
│ MRK       │ 2         │
└───────────┴───────────┘
```

Bir satır yerine dört satır güncellendi — bu, ölçülebilir ve kabul edilebilir bir
maliyettir. Asıl tehlike son deyimdedir: özet tabloya eski adla yeni bir satır eklendi ve
şube yine iki adla göründü. Denormalizasyonun bedeli, güncelleme maliyeti değil, **her
yazma yolunun kopyayı doğru doldurma zorunluluğudur**.

#### Türetilmiş Sütun ve Her Yazma Yolu

Aynı sorun türetilmiş değerlerde daha keskin görünür. Üyenin açık ödünç sayısını her
sorguda saymak yerine bir sütunda tutmak, sayımın maliyetini yazmaya taşır. Tutarlılık
tetikleyicilerle sağlanabilir — yeter ki bütün yazma yolları kapsansın:

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode box
CREATE TABLE uye   (uye_no INTEGER PRIMARY KEY, ad TEXT NOT NULL,
                    acik_odunc INTEGER NOT NULL DEFAULT 0);
CREATE TABLE odunc (odunc_no INTEGER PRIMARY KEY, uye_no INTEGER NOT NULL,
                    iade_tarihi TEXT);
INSERT INTO uye VALUES (41, 'Ayse Demir', 0), (52, 'Mert Kaya', 0);

CREATE TRIGGER odunc_eklendi AFTER INSERT ON odunc
BEGIN
  UPDATE uye SET acik_odunc = acik_odunc + 1 WHERE uye_no = NEW.uye_no;
END;
CREATE TRIGGER odunc_iade AFTER UPDATE OF iade_tarihi ON odunc
WHEN OLD.iade_tarihi IS NULL AND NEW.iade_tarihi IS NOT NULL
BEGIN
  UPDATE uye SET acik_odunc = acik_odunc - 1 WHERE uye_no = NEW.uye_no;
END;

INSERT INTO odunc VALUES (1001, 41, NULL), (1002, 41, NULL), (1003, 52, NULL);
UPDATE odunc SET iade_tarihi = '2025-03-16' WHERE odunc_no = 1002;
SELECT u.uye_no, u.acik_odunc AS sayac,
       (SELECT COUNT(*) FROM odunc o
         WHERE o.uye_no = u.uye_no AND o.iade_tarihi IS NULL) AS gercek
FROM uye u;

DELETE FROM odunc WHERE odunc_no = 1001;
SELECT u.uye_no, u.acik_odunc AS sayac,
       (SELECT COUNT(*) FROM odunc o
         WHERE o.uye_no = u.uye_no AND o.iade_tarihi IS NULL) AS gercek
FROM uye u;
SQL
```

```
┌────────┬───────┬────────┐
│ uye_no │ sayac │ gercek │
├────────┼───────┼────────┤
│ 41     │ 1     │ 1      │
│ 52     │ 1     │ 1      │
└────────┴───────┴────────┘
┌────────┬───────┬────────┐
│ uye_no │ sayac │ gercek │
├────────┼───────┼────────┤
│ 41     │ 1     │ 0      │
│ 52     │ 1     │ 1      │
└────────┴───────┴────────┘
```

Ekleme ve iade yolları kapsandığı için ilk sonuç doğrudur. Silme yolu için tetikleyici
yazılmamıştır; bir ödünç kaydı silindiğinde sayaç olduğu yerde kalır ve gerçekten
sayılan değerle ayrışır. Türetilmiş sütunun kuralı budur: **kaynağı değiştiren her yol
sayılmalıdır** — ekleme, silme, güncelleme, toplu göç ve elle çalıştırılan bakım
deyimleri dâhil. Bir yol atlanırsa sütun sessizce yanlışa döner ve yanlışlığın ne zaman
başladığı kayıtlarda görünmez.

Bu yüzden türetilmiş değerler için ikinci bir gereklilik daha vardır: kaynaktan yeniden
hesaplayan ve farkı bildiren bir denetim işi. Yukarıdaki ikinci sorgu tam olarak budur.

#### Denormalizasyon Biçimleri

Uygulamada beş kalıp görülür ve maliyetleri farklıdır.

- **Kopyalanan sütun.** Sık okunan bir öznitelik, başvuran bağıntıya kopyalanır. Kaynak
  nadiren değişiyorsa ucuzdur.
- **Türetilmiş toplam.** Sayım, tutar toplamı ya da en son tarih bir sütunda tutulur.
  Yukarıdaki örnektir; her yazma yolunu kapsamak gerekir.
- **Özet bağıntısı.** Raporlama için önceden hesaplanmış bir bağıntı tutulur ve belirli
  aralıklarla yenilenir. Tazelik ile maliyet arasında açık bir ödünleşim kurar.
- **Önceden birleştirilmiş bağıntı.** Sık kullanılan bir birleştirmenin sonucu saklanır.
- **Görünüm.** Birleştirme bir görünümün arkasına alınır; sonuç saklanmadığı için tekrar
  eklenmez, yalnız sorgu yazımı yalınlaşır. Sonucun saklandığı biçimi bulunup
  bulunmadığı ve nasıl tazelendiği **motora göre değişir**.

Son madde önemlidir: görünüm tek başına denormalizasyon değildir. Okuma maliyeti aynı
kalır, yalnız tekrarlanan sorgu metni ortadan kalkar.

#### Karar Ölçütleri

Denormalizasyon son sıradaki araçtır. Ondan önce denenecekler bellidir: sorgunun kendisini
düzeltmek, gerekli dizinleri eklemek, gereksiz sütun okumasını kesmek, sayfalama kullanmak.
Bunlar veriyi tek yerde bırakır; denormalizasyon bırakmaz.

Karar için dört soru sorulur. Okuma ile yazma oranı nedir — okuma baskın değilse kazanç
küçük, maliyet büyüktür. Kopyalanan veri ne sıklıkta değişiyor — sık değişen veri kopyayı
sürekli geçersiz kılar. Bayat veri kabul edilebilir mi — özet bağıntısı için verilecek en
kritik yanıttır. Ve kopyayı dolduran bütün yollar biliniyor mu — bilinmiyorsa karar
ertelenmelidir.

Kararın kendisi de yazılı kalmalıdır. Şemayı sonradan okuyan biri, tekrarı bir tasarım
hatası sanıp "düzeltmeye" kalkabilir; hangi ölçümün bu tekrarı gerekli kıldığı
kayıtlıysa bu olmaz.

#### Özet

- Denormalizasyon, normalleştirilmiş bir şemaya ölçüme dayanarak tekrar eklemektir;
  normalleştirmeyi hiç yapmamak denormalizasyon değildir.
- Okuma bedeli, sorgunun kaç bağıntıya ve hangi yolla eriştiğinin plan çıktısından
  sayılmasıyla ölçülür.
- Tekrar eklendiğinde maliyet yazmaya geçer: güncellenen satır sayısı artar ve her yazma
  yolu kopyayı doğru doldurmak zorunda kalır.
- Türetilmiş sütunun tutarlılığı, kaynağı değiştiren yollardan biri atlandığında sessizce
  bozulur; kaynaktan yeniden hesaplayan bir denetim işi zorunludur.
- Denormalizasyondan önce sorgu düzeltme, dizin ekleme ve sayfalama denenir; karar
  gerekçesiyle birlikte yazılı bırakılır.

#### Sonraki Adım

Normalizasyon konusunun son dersi, tek tek kuralların ötesine geçip tekrar eden tasarım
durumlarına bakar: bire çok ve çoka çok ilişkiler nasıl kurulur, ağaç biçimindeki veri
ilişkisel şemada nasıl tutulur, geçmişi saklamak gerektiğinde satır nasıl sürümlenir
ve hangi yaygın kalıplar aslında karşı kalıptır — her satırın anahtar-değer çifti olarak
tutulduğu tablolar bunların başında gelir.

### Şema Tasarım Kalıpları

Source: https://academia.sh/tr/kurslar/iliskisel-kuram/sema-tasarim-kaliplari

İlişki kardinalitelerinin şemaya çevrilmesi, hiyerarşik verinin saklanması, alt tür ve zaman boyutu kalıpları ile anahtar-değer tablosu başta olmak üzere yaygın karşı kalıplar.

Önceki dersler tek tek kuralları verdi: bağımlılıkları bul, bağıntıyı böl, tekrarı ölçerek
geri koy. Uygulamada aynı modelleme durumları tekrar tekrar karşımıza çıkar ve her birinin
yerleşik bir karşılığı vardır. Bu dersin sorusu şudur: hangi durum hangi şema yapısına
çevrilir ve hangi yaygın yapılar aslında sorunun kendisidir?

#### İlişki Kardinaliteleri

İki bağıntı arasındaki ilişkinin şemaya çevrilmesi kardinaliteye bağlıdır.

**Bire çok** ilişki, yabancı anahtarın "çok" tarafına konmasıyla kurulur. Bir üyenin
birden çok ödünç kaydı vardır; `uye_no` sütunu ödünç bağıntısında durur. Ters yön —
üye bağıntısında ödünç numarası tutmak — tek hücreye liste sıkıştırmak demektir ve
birinci normal formu bozar.

**Bire bir** ilişki nadirdir ve genellikle iki gerekçeden birine dayanır: satırın seyrek
doldurulan bölümünü ayırmak, ya da erişimi farklı yetkiye bağlı sütunları ayrı tutmak.
Yabancı anahtar iki taraftan birine konur ve tekillik kısıtıyla desteklenir.

**Çoka çok** ilişki doğrudan ifade edilemez; araya bir **bağlantı bağıntısı** (junction
table) konur. Anahtarı, iki tarafın anahtarlarından oluşan bileşik anahtardır:

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode box
PRAGMA foreign_keys = ON;
CREATE TABLE kitap (isbn TEXT PRIMARY KEY NOT NULL, baslik TEXT NOT NULL);
CREATE TABLE yazar (yazar_no INTEGER PRIMARY KEY, ad TEXT NOT NULL);
CREATE TABLE kitap_yazar (
  isbn     TEXT    NOT NULL REFERENCES kitap (isbn),
  yazar_no INTEGER NOT NULL REFERENCES yazar (yazar_no),
  sira     INTEGER NOT NULL,
  PRIMARY KEY (isbn, yazar_no)
);
INSERT INTO kitap VALUES ('975-01', 'Kayip Zaman'), ('975-02', 'Deniz Fenerleri');
INSERT INTO yazar VALUES (7, 'Elif Toprak'), (8, 'Kemal Aras');
INSERT INTO kitap_yazar VALUES ('975-01', 7, 1), ('975-01', 8, 2), ('975-02', 8, 1);
SELECT k.baslik, COUNT(*) AS yazar_sayisi FROM kitap k
JOIN kitap_yazar ky ON ky.isbn = k.isbn GROUP BY k.isbn ORDER BY k.isbn;
SQL
```

```
┌─────────────────┬──────────────┐
│     baslik      │ yazar_sayisi │
├─────────────────┼──────────────┤
│ Kayip Zaman     │ 2            │
│ Deniz Fenerleri │ 1            │
└─────────────────┴──────────────┘
```

Bağlantı bağıntısı çoğu zaman yalın bir eşleştirmeden fazlasını taşır. Buradaki `sira`
sütunu, yazarların kapaktaki sırasını tutar — ilişkinin kendisine ait bir özniteliktir ve
başka hiçbir bağıntıya sığmaz. Ödünç işlemi de aslında böyledir: üye ile nüsha arasındaki
ilişkiye ait alış ve iade tarihlerini taşır.

#### Hiyerarşik Veri

Kitap konuları ağaç oluşturur: edebiyat altında roman, roman altında tarihsel roman. En
yalın karşılık, bağıntının kendi anahtarına başvurmasıdır — buna **komşuluk listesi**
(adjacency list) denir. Ağaç üzerinde dolaşmak özyinelemeli sorgu gerektirir:

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode box
PRAGMA foreign_keys = ON;
CREATE TABLE konu (
  konu_no  INTEGER PRIMARY KEY,
  ad       TEXT    NOT NULL,
  ust_konu INTEGER REFERENCES konu (konu_no)
);
INSERT INTO konu VALUES (1, 'Edebiyat', NULL), (2, 'Roman', 1), (3, 'Siir', 1),
                        (4, 'Tarihsel Roman', 2), (5, 'Bilim', NULL);

WITH RECURSIVE dal(konu_no, ad, derinlik, yol) AS (
  SELECT konu_no, ad, 0, ad FROM konu WHERE ust_konu IS NULL
  UNION ALL
  SELECT k.konu_no, k.ad, d.derinlik + 1, d.yol || ' / ' || k.ad
  FROM konu k JOIN dal d ON k.ust_konu = d.konu_no
)
SELECT derinlik, yol FROM dal ORDER BY yol;
SQL
```

```
┌──────────┬───────────────────────────────────┐
│ derinlik │                yol                │
├──────────┼───────────────────────────────────┤
│ 0        │ Bilim                             │
│ 0        │ Edebiyat                          │
│ 1        │ Edebiyat / Roman                  │
│ 2        │ Edebiyat / Roman / Tarihsel Roman │
│ 1        │ Edebiyat / Siir                   │
└──────────┴───────────────────────────────────┘
```

Sorgu, kökten başlayıp her adımda bir alt seviyeyi ekleyerek ağacı açar. Veri Yapıları
kursundaki enine arama ile aynı fikirdir; fark, kuyruğun sorgu motorunda tutulmasıdır.

Komşuluk listesinin yazması ucuz, okuması pahalıdır: bir düğümün bütün alt ağacını almak
derinlik kadar adım gerektirir. Üç yaygın karşılık vardır. **Yol numaralandırma** (path
enumeration) her satırda kökten gelen yolu bir dizgide tutar; alt ağaç sorgusu önek
eşlemesine döner, ama taşıma işlemleri bütün alt ağacı günceller. **İç içe küme** (nested
set) her düğüme sol ve sağ sayı verir; okuma çok hızlıdır, ekleme neredeyse bütün tabloyu
kaydırır. **Kapanış tablosu** (closure table) her ata–torun çiftini ayrı satırda tutar;
okuma ve yazma dengelidir, karşılığında satır sayısı artar. Seçim, okuma ile yazma
oranına göre yapılır.

#### Alt Tür ve Zaman

**Alt tür** (subtype) durumu, ortak bir üst kavramın farklı öznitelikler taşıyan
türlerinden doğar: kütüphanedeki materyaller kitap, süreli yayın ve ses kaydı olabilir;
ortak alanları vardır, ayrık alanları da. Üç karşılık kullanılır. Tek bağıntıda tutmak —
bütün türlerin sütunlarını yan yana koymak — çok sayıda boş değer üretir. Tür başına ayrı
bağıntı, ortak sorguları zorlaştırır. Ortak öznitelikleri bir üst bağıntıda, ayrık
öznitelikleri tür başına bir alt bağıntıda tutmak dengeli karşılıktır; alt bağıntının
birincil anahtarı, aynı zamanda üst bağıntıya yabancı anahtardır.

**Zaman boyutu** iki ayrı gereksinimi gizler. Birincisi, bir olgunun geçerli olduğu
aralıktır: üyelik ücretinin hangi tarihler arasında hangi tutarda olduğu. Karşılığı,
satıra başlangıç ve bitiş tarihi eklemek ve aralıkların örtüşmemesini kısıtla
denetlemektir. İkincisi, bir satırın geçmiş hâllerini saklamaktır: her değişiklikte yeni
bir sürüm satırı yazılır, güncel satır bir bayrakla ya da boş bitiş tarihiyle işaretlenir.
İkisini karıştırmak, ne geçmişi doğru veren ne de güncel durumu ucuza veren bir şema
üretir.

#### Karşı Kalıplar

Bazı yapılar sorunu çözer görünürken kısıtları ortadan kaldırır. En yaygını, sütunları
satır olarak tutan anahtar-değer tablosudur:

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode box
.nullvalue (bos)
CREATE TABLE kitap_ozellik (
  isbn    TEXT NOT NULL,
  ozellik TEXT NOT NULL,
  deger   TEXT NOT NULL,
  PRIMARY KEY (isbn, ozellik)
);
INSERT INTO kitap_ozellik VALUES
  ('975-01', 'baslik', 'Kayip Zaman'), ('975-01', 'yayin_yili', '2019'),
  ('975-02', 'baslik', 'Deniz Fenerleri'), ('975-02', 'yayin_yili', 'iki bin yirmi'),
  ('975-03', 'baslik', 'Sessiz Bahce');

SELECT b.deger AS baslik, y.deger AS yayin_yili
FROM kitap_ozellik b
LEFT JOIN kitap_ozellik y ON y.isbn = b.isbn AND y.ozellik = 'yayin_yili'
WHERE b.ozellik = 'baslik'
ORDER BY b.isbn;
SQL
```

```
┌─────────────────┬───────────────┐
│     baslik      │  yayin_yili   │
├─────────────────┼───────────────┤
│ Kayip Zaman     │ 2019          │
│ Deniz Fenerleri │ iki bin yirmi │
│ Sessiz Bahce    │ (bos)         │
└─────────────────┴───────────────┘
```

Üç kayıp aynı çıktıda görünür. Yayın yılına tip ve alan kısıtı yazılamaz, çünkü `deger`
sütunu bütün öznitelikler için ortaktır — `iki bin yirmi` girmiştir. Zorunluluk
uygulanamaz; üçüncü kitabın yılı hiç yoktur ve bunu engelleyecek bir `NOT NULL` yeri
yoktur. Ve iki öznitelik okumak için tabloyu kendisiyle birleştirmek gerekir; öznitelik
sayısı arttıkça birleştirme sayısı da artar.

Bu yapının meşru kullanımı dardır: gerçekten önceden bilinmeyen, kullanıcı tarafından
tanımlanan ve üzerinde kısıt aranmayan öznitelikler. Şema değişikliğinden kaçınmak için
seçildiğinde, veritabanının verdiği bütün güvenceler geri verilmiş olur.

Üç karşı kalıp daha sık görülür. **Çok amaçlı yabancı anahtar**, tek bir sütunla birden
çok bağıntıya başvurmayı dener — hedef bağıntının adı ayrı bir sütunda tutulur ve
başvuru bütünlüğü uygulanamaz hâle gelir. **Sütun adında numara** — `telefon_1`,
`telefon_2` — birinci normal formu yatay yazmaktır. **Sihirli değer**, boş değer yerine
`-1` ya da `'1900-01-01'` gibi bir yer tutucu koymaktır; her sorgunun o değeri elemesini
gerektirir ve eleme unutulduğunda toplamlara karışır.

#### Özet

- Bire çok ilişki yabancı anahtarla, çoka çok ilişki bileşik anahtarlı bir bağlantı
  bağıntısıyla kurulur; bağlantı bağıntısı ilişkinin kendi özniteliklerini taşır.
- Hiyerarşi için komşuluk listesi yazmada ucuz okumada pahalıdır; yol numaralandırma, iç
  içe küme ve kapanış tablosu bu dengeyi farklı yönlere kaydırır.
- Alt türler ortak öznitelikleri üst bağıntıda, ayrık öznitelikleri tür başına alt
  bağıntıda tutularak modellenir.
- Geçerlilik aralığı ile sürüm geçmişi ayrı gereksinimlerdir; tek yapıyla ikisini birden
  karşılamaya çalışmak her ikisini de bozar.
- Anahtar-değer tablosu tip, alan ve zorunluluk kısıtlarını birden yitirir ve öznitelik
  başına bir birleştirme ekler; meşru kullanımı önceden bilinmeyen özniteliklerle sınırlıdır.

#### Sonraki Adım

Normalizasyon konusu burada tamamlanıyor: şemanın nasıl kurulacağı, nasıl bölüneceği ve
hangi durumlarda bilinçli olarak birleştirileceği belirlendi. Kalan soru, bu şemanın
hangi tür iş yükü altında çalışacağıdır. Tek tek ödünç işlemlerini kaydeden bir sistemle
yıllık ödünç istatistiği çıkaran bir sistem aynı veriyi ister ama aynı erişim biçimini
istemez. Sıradaki konu bu ayrımla başlar.

## Veri İşleme Biçimleri

İşlem ve çözümleme yüklerinin ayrımı, satır ve sütun yönelimli saklama, ACID özellikleri ve ilişkisel ile ilişkisel olmayan sistemler arasındaki seçimin ölçütleri.

### İşlem ve Çözümleme Yükleri

Source: https://academia.sh/tr/kurslar/iliskisel-kuram/islem-ve-cozumleme-yukleri

İşlem yükü ile çözümleme yükünün erişim biçimlerindeki farkı, satır ve sütun yönelimli saklamanın okunan bayt miktarına etkisi, kapsayan dizin ve yıldız şeması.

Normalizasyon konusu şemanın nasıl kurulacağını belirledi. Kalan soru, bu şemanın hangi
tür iş yükü altında çalışacağıdır. Tek tek ödünç işlemlerini kaydeden bir sistemle yıllık
ödünç istatistiği çıkaran bir sistem aynı veriyi ister ama aynı erişim biçimini istemez.
Bu dersin sorusu şudur: iki yük biçimi nasıl ayrılır ve bu ayrım saklama düzeninde ne
değiştirir?

#### İki Yük Biçimi

**İşlem yükü** (transactional workload), gişedeki günlük çalışmadır: bir üyenin kaydını
anahtarla getir, bir ödünç satırı ekle, bir iade satırını güncelle. Nitelikleri şunlardır:
her istek az sayıda satıra dokunur, satırın bütün sütunları istenir, gecikme
milisaniyelerle ölçülür ve aynı anda çok sayıda istek çalışır. Yazma oranı yüksektir.

**Çözümleme yükü** (analytical workload), rapor üretmedir: hangi şubede hangi ay kaç ödünç
verildi, gecikme cezalarının toplamı nedir, hangi konu başlığı yıllara göre nasıl değişti.
Nitelikleri terstir: her istek milyonlarca satıra dokunur, satırın yalnız birkaç sütununu
ister, gecikme saniyelerle ölçülür, eşzamanlı istek sayısı düşüktür ve yazma neredeyse
yoktur.

Bu iki yük aynı veriyi paylaşır ama birbirine zıt tasarım kararları gerektirir. İşlem yükü
normalleştirilmiş şemadan, dar satırlardan ve anahtar üzerinden erişimden kazanır.
Çözümleme yükü geniş taramalardan, önceden birleştirilmiş yapılardan ve yalnız gerekli
sütunun okunmasından kazanır.

#### Satır ve Sütun Yönelimi

Fark, saklama düzeninde somutlaşır. **Satır yönelimli saklama** (row-oriented storage) bir
satırın bütün sütunlarını diskte yan yana tutar. Bir satırı okumak tek bir blok okumasıdır
— işlem yükü için ideal. Buna karşılık tek bir sütunu toplamak, o sütunun yanındaki bütün
sütunları da okumayı gerektirir.

**Sütun yönelimli saklama** (column-oriented storage) tersini yapar: aynı sütunun bütün
değerleri yan yana durur. Bir sütunu toplamak yalnız o sütunu okur; buna karşılık tek bir
satırın tamamını getirmek her sütundan ayrı okuma gerektirir.

Aradaki fark ölçülebilir. Satır yönelimli bir motorda sütun yönelimine öykünmenin yolu,
tek bir sütunu kendi bağıntısında tutmaktır:

```sh
mkdir -p yuk && cd yuk && rm -f satir.db sutun.db

sqlite3 satir.db <<'SQL'
PRAGMA page_size = 4096;
CREATE TABLE odunc (
  odunc_no    INTEGER PRIMARY KEY,
  uye_no      INTEGER NOT NULL,
  isbn        TEXT    NOT NULL,
  sube_kodu   TEXT    NOT NULL,
  alis_tarihi TEXT    NOT NULL,
  iade_tarihi TEXT    NOT NULL,
  gecikme_gun INTEGER NOT NULL,
  ceza_kurus  INTEGER NOT NULL,
  kanal       TEXT    NOT NULL,
  not_metni   TEXT    NOT NULL
);
WITH RECURSIVE sayac(n) AS (
  SELECT 1 UNION ALL SELECT n + 1 FROM sayac WHERE n < 200000
)
INSERT INTO odunc (uye_no, isbn, sube_kodu, alis_tarihi, iade_tarihi,
                   gecikme_gun, ceza_kurus, kanal, not_metni)
SELECT n % 5000, '978-975-' || (n % 900), 'MRK', '2025-03-02', '2025-03-16',
       n % 7, (n % 7) * 250, 'gise', 'kayit notu alani ' || n
FROM sayac;
SQL

sqlite3 sutun.db "PRAGMA page_size = 4096;
CREATE TABLE ceza_kurus (deger INTEGER NOT NULL);"
sqlite3 satir.db "ATTACH 'sutun.db' AS s;
INSERT INTO s.ceza_kurus SELECT ceza_kurus FROM odunc;"

echo "satir.db $(wc -c < satir.db | tr -d ' ') bayt"
echo "sutun.db $(wc -c < sutun.db | tr -d ' ') bayt"
sqlite3 satir.db "SELECT SUM(ceza_kurus) FROM odunc;"
sqlite3 sutun.db "SELECT SUM(deger) FROM ceza_kurus;"
```

```
satir.db 16568320 bayt
sutun.db 1957888 bayt
149999250
149999250
```

İki dosya aynı iki yüz bin değeri taşır ve aynı toplamı verir. Ceza toplamını hesaplamak
için satır yönelimli düzende 16,5 milyon bayt taranmalıdır; sütun düzeninde 2 milyon
bayt. Sayfa boyutu blokta açıkça belirlendiği için ölçüm yinelenebilir; **bayt sayıları
sayfa boyutuna ve motorun satır kodlamasına göre değişir**, oran ise sütun sayısı ve
sütun genişlikleriyle belirlenir.

#### Sıkıştırmanın Payı

Sütun yöneliminin ikinci kazancı sıkıştırmadadır. Aynı sütunun değerleri aynı tipten ve
çoğu zaman birbirine benzer olduğu için, satır yönelimli düzende elde edilemeyecek
oranlar yakalanır. Üç yöntem yaygındır: **sözlük kodlaması** (dictionary encoding)
tekrarlanan değerleri küçük tam sayılara eşler — şube kodu sütunu için birkaç bit yeter;
**koşu uzunluğu kodlaması** (run-length encoding) sıralı sütunlarda ardışık aynı
değerleri tek bir çift olarak saklar; **bit paketleme** dar aralıklı tam sayıları tam bayt
sınırına hizalamadan yazar.

Sıkıştırma yalnız yer kazandırmaz. Okunan bayt azaldığı için tarama da hızlanır ve bazı
işlemler değerleri açmadan, kodlanmış hâl üzerinde yapılabilir.

#### Satır Yönelimli Motorda Karşılık

Satır yönelimli bir motorun bu kazanca en yakın karşılığı **kapsayan dizindir** (covering
index): sorgunun ihtiyaç duyduğu bütün sütunları içeren bir dizin. Motor tabloya hiç
gitmeden yanıtı dizinden üretir.

```sh
sqlite3 :memory: <<'SQL'
CREATE TABLE odunc (
  odunc_no    INTEGER PRIMARY KEY,
  uye_no      INTEGER NOT NULL,
  sube_kodu   TEXT    NOT NULL,
  not_metni   TEXT    NOT NULL,
  ceza_kurus  INTEGER NOT NULL
);
EXPLAIN QUERY PLAN SELECT sube_kodu, SUM(ceza_kurus) FROM odunc GROUP BY sube_kodu;
CREATE INDEX odunc_ceza ON odunc (sube_kodu, ceza_kurus);
EXPLAIN QUERY PLAN SELECT sube_kodu, SUM(ceza_kurus) FROM odunc GROUP BY sube_kodu;
SQL
```

```
QUERY PLAN
|--SCAN odunc
`--USE TEMP B-TREE FOR GROUP BY
QUERY PLAN
`--SCAN odunc USING COVERING INDEX odunc_ceza
```

Dizin eklendikten sonra plan iki yönden değişir: tarama tablo yerine dizin üzerinde
yapılır — yani yalnız iki sütun okunur — ve gruplama için geçici bir yapı kurulmasına
gerek kalmaz, çünkü dizin zaten şube koduna göre sıralıdır. Kapsayan dizin, dar bir
sorgu için sütun yönelimine yaklaşır; genişleyen sorgu kümesi için her birine ayrı dizin
gerekeceğinden ölçeklenmez.

#### Çözümleme Şeması

Çözümleme yükü şema biçimini de değiştirir. **Yıldız şeması** (star schema) merkeze bir
**olgu bağıntısı** (fact table), çevresine **boyut bağıntıları** (dimension table) koyar.
Kütüphane örneğinde olgu bağıntısı ödünç olaylarıdır; boyutlar üye, kitap, şube ve
takvimdir.

Olgu bağıntısının satırı, ölçülebilir büyüklükleri ve boyutlara giden anahtarları taşır:
gecikme günü, ceza tutarı, üye anahtarı, kitap anahtarı, şube anahtarı, tarih anahtarı.
Boyut bağıntıları ise kasten normalleştirilmemiştir — şube boyutu şube adını, ilini ve
bölgesini bir arada tutar. Gerekçe önceki dersteki denormalizasyon gerekçesidir: boyutlar
küçüktür, nadiren değişir ve her sorguda birleştirilir.

Tasarımın ilk kararı **taneciktir** (grain): olgu bağıntısının bir satırı neyi temsil
eder? "Bir ödünç işlemi" ile "bir şubenin bir günlük özeti" farklı taneciklerdir ve
sonradan değiştirilemez — daha ince tanecikten kabaya inilebilir, tersi yapılamaz.

#### İki Yükü Ayırmak

İki yükü aynı veritabanında çalıştırmak, ikisini de bozar. Uzun süren bir çözümleme
sorgusu işlem yükünün kaynaklarını tüketir; işlem yükünün yazmaları çözümleme sorgusunun
gördüğü veriyi değiştirir. Yaygın karşılık, çözümleme yükünü ayrı bir kopyaya taşımaktır:
veri işlem sisteminden düzenli aralıklarla çıkarılır, dönüştürülür ve çözümleme
sistemine yüklenir.

Bunun bedeli tazeliktir. Çözümleme kopyası her zaman bir miktar geridedir ve bu gecikmenin
ne kadar olabileceği, sistemin karşılaması gereken açık bir gereksinimdir.

#### Özet

- İşlem yükü az satıra çok sütunla, çözümleme yükü çok satıra az sütunla erişir; ikisi
  zıt tasarım kararları gerektirir.
- Satır yönelimli saklama satırın bütün sütunlarını yan yana tutar; sütun yönelimli
  saklama aynı sütunun değerlerini yan yana tutar ve toplamada okunan baytı düşürür.
- Aynı sütunun değerleri benzer olduğu için sütun düzeni sözlük kodlaması ve koşu uzunluğu
  kodlaması gibi yöntemlerle daha iyi sıkıştırılır.
- Kapsayan dizin, satır yönelimli motorda dar sorgular için benzer bir kazanç verir ama
  genişleyen sorgu kümesine ölçeklenmez.
- Yıldız şeması olgu bağıntısını boyutlarla çevreler; taneciğin seçimi geri alınamaz bir
  karardır.

#### Sonraki Adım

Bu ders iki kez aynı varsayıma dayandı: bir ödünç kaydının yazılması ya tümüyle olur ya
hiç olmaz ve bir çözümleme sorgusu yarım yazılmış bir durumu görmemelidir. Bu varsayımlar
kursun ilk dersinde de geçmişti — dosyaya eşzamanlı yazan iki süreç bunları sağlayamamıştı.
Sıradaki ders, veritabanı yönetim sisteminin bu güvenceleri hangi adla verdiğini tanımlar:
atomiklik, tutarlılık, yalıtım ve kalıcılık.

### ACID Özellikleri

Source: https://academia.sh/tr/kurslar/iliskisel-kuram/acid-ozellikleri

İşlem kavramı ve atomiklik, tutarlılık, yalıtım, kalıcılık güvencelerinin tanımı; geri alma ve kalıcılığın çalıştırılarak gösterimi, istemcinin hata karşısındaki sorumluluğu.

Önceki ders iki kez aynı varsayıma dayandı: bir ödünç kaydının yazılması ya tümüyle olur ya
hiç olmaz ve bir çözümleme sorgusu yarım yazılmış bir durumu görmez. Aynı varsayımlar
kursun ilk dersinde de geçmişti — dosyaya eşzamanlı yazan iki süreç ikisini de
sağlayamamıştı. Bu dersin sorusu şudur: veritabanı yönetim sistemi bu güvenceleri hangi
adla, hangi sınırlar içinde verir?

#### İşlem

**İşlem** (transaction), veritabanı üzerinde tek bir birim gibi ele alınan deyimler
dizisidir. Bir nüshanın bir şubeden diğerine aktarılması iki güncelleme gerektirir; ikisi
birlikte olmalıdır, çünkü aralarında kalan durumda kütüphanede bir nüsha eksiktir.

İşlemin sınırları açıkça bildirilir: başlangıç, ardından ya **onaylama** (commit) ya da
**geri alma** (rollback). Bildirilmediğinde her deyim kendi başına bir işlem sayılır.

İşlemlerin verdiği güvenceler dört başlık altında toplanır ve baş harfleriyle **ACID**
diye anılır: atomiklik, tutarlılık, yalıtım, kalıcılık.

#### Atomiklik

**Atomiklik** (atomicity), işlemin ya tamamen uygulanmasını ya da hiç uygulanmamasını
ister. Ara durum hiçbir zaman kalıcı olmaz.

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode box
CREATE TABLE stok (sube_kodu TEXT NOT NULL, isbn TEXT NOT NULL, adet INTEGER NOT NULL,
                   PRIMARY KEY (sube_kodu, isbn), CHECK (adet >= 0));
INSERT INTO stok VALUES ('MRK', '975-01', 3), ('BHC', '975-01', 1);

BEGIN;
UPDATE stok SET adet = adet - 1 WHERE sube_kodu = 'MRK' AND isbn = '975-01';
UPDATE stok SET adet = adet + 1 WHERE sube_kodu = 'BHC' AND isbn = '975-01';
SELECT 'islem icinde' AS an, sube_kodu, adet FROM stok ORDER BY sube_kodu;
ROLLBACK;
SELECT 'geri alindiktan sonra' AS an, sube_kodu, adet FROM stok ORDER BY sube_kodu;
SQL
```

```
┌──────────────┬───────────┬──────┐
│      an      │ sube_kodu │ adet │
├──────────────┼───────────┼──────┤
│ islem icinde │ BHC       │ 2    │
│ islem icinde │ MRK       │ 2    │
└──────────────┴───────────┴──────┘
┌───────────────────────┬───────────┬──────┐
│          an           │ sube_kodu │ adet │
├───────────────────────┼───────────┼──────┤
│ geri alindiktan sonra │ BHC       │ 1    │
│ geri alindiktan sonra │ MRK       │ 3    │
└───────────────────────┴───────────┴──────┘
```

İşlemin içinde iki güncelleme de görünür; geri alma sonrasında ikisi de yoktur. Atomiklik
yalnız hata durumu için değildir — geri alma, uygulamanın bilerek verdiği bir karar da
olabilir. Kursun ilk dersindeki dosya örneğinde bu düzenek yoktu: yarım kalan yazma
dosyada yarım kalıyordu.

#### Tutarlılık

**Tutarlılık** (consistency), işlemin veritabanını bir geçerli durumdan başka bir geçerli
duruma taşımasını ister. "Geçerli" olan, şemadaki kısıtların tümünün sağlandığı durumdur.
İşlemin ortasında kısıt geçici olarak bozulabilir; işlem sonunda bozuk kalamaz.

```sh
rm -f kutuphane.db

sqlite3 kutuphane.db <<'SQL'
CREATE TABLE stok (sube_kodu TEXT NOT NULL, isbn TEXT NOT NULL, adet INTEGER NOT NULL,
                   PRIMARY KEY (sube_kodu, isbn), CHECK (adet >= 0));
INSERT INTO stok VALUES ('MRK', '975-01', 3), ('BHC', '975-01', 1);
SQL

sqlite3 kutuphane.db <<'SQL'
.bail on
BEGIN;
UPDATE stok SET adet = adet - 1 WHERE sube_kodu = 'BHC' AND isbn = '975-01';
UPDATE stok SET adet = adet - 1 WHERE sube_kodu = 'BHC' AND isbn = '975-01';
COMMIT;
SQL

sqlite3 kutuphane.db <<'SQL'
.headers on
.mode box
SELECT sube_kodu, adet FROM stok ORDER BY sube_kodu;
SQL
```

```
Runtime error near line 4: CHECK constraint failed: adet >= 0 (19)
┌───────────┬──────┐
│ sube_kodu │ adet │
├───────────┼──────┤
│ BHC       │ 1    │
│ MRK       │ 3    │
└───────────┴──────┘
```

İkinci güncelleme adedi eksiye düşüreceği için reddedildi; birincisi de geri alındı ve
adet başlangıçtaki bire döndü.

Bu sonucun bir koşulu vardır ve gözden kaçar. Bloktaki ilk satır, istemciye ilk hatada
durmasını söyler; adı ve yazılışı motora ve araca göre değişir. O satır olmasaydı istemci
hatayı yok sayıp `COMMIT` gönderir ve **yalnız birinci güncellemenin uygulandığı** yarım
bir iş kalıcı olurdu. Tutarlılık güvencesi motorun kısıtları uygulamasıyla başlar ama
istemcinin hata karşısında doğru davranmasıyla tamamlanır. Hatayı yutan bir istemci,
motorun verdiği güvenceyi elinden alır.

#### Yalıtım

**Yalıtım** (isolation), aynı anda çalışan işlemlerin birbirinin yarım durumunu
görmemesini ister. En katı yorumu şudur: eşzamanlı çalışan işlemlerin sonucu, aynı
işlemlerin bir sırayla teker teker çalıştırılmasından elde edilebilecek bir sonuçla aynı
olmalıdır.

Bu güvence pahalıdır ve çoğu sistem onu tümüyle uygulamak yerine kademelendirir. Gevşetme
belirli anormallikleri serbest bırakır ve bunların adları vardır: bir işlemin başka bir
işlemin onaylanmamış yazmasını görmesi, aynı satırın iki okumada farklı değer vermesi, bir
koşula uyan satır kümesinin iki okumada farklı olması. Hangi kademede hangi anormalliğin
görülebileceği, kilitleme ile sürüm tabanlı yaklaşımların farkı ve kilitlenme durumları bu
müfredatın ilerleyen kurslarına aittir. Burada tanım düzeyinde bırakılıyor: kursun ilk
dersindeki kayıp güncelleme, yalıtımın bulunmadığı bir ortamda ne olduğunun örneğiydi.

#### Kalıcılık

**Kalıcılık** (durability), onaylanmış bir işlemin sonucunun kalıcı olmasını ister —
sistem hemen ardından çökse bile. Onay bildirimi bir sözdür: veri artık kalıcı depolamada
güvencededir.

```sh
rm -f kutuphane.db

sqlite3 kutuphane.db <<'SQL'
CREATE TABLE odunc (odunc_no INTEGER PRIMARY KEY, uye_no INTEGER NOT NULL);
BEGIN;
INSERT INTO odunc VALUES (1001, 41);
COMMIT;
BEGIN;
INSERT INTO odunc VALUES (1002, 52);
ROLLBACK;
SQL

sqlite3 kutuphane.db <<'SQL'
.headers on
.mode box
SELECT odunc_no, uye_no FROM odunc;
SQL
```

```
┌──────────┬────────┐
│ odunc_no │ uye_no │
├──────────┼────────┤
│ 1001     │ 41     │
└──────────┴────────┘
```

İkinci çağrı ayrı bir süreçtir; veriyi yazan sürecin belleğinden hiçbir şey devralmaz.
Onaylanan satırı görür, geri alınan satırı görmez. Ölçüt işlemin sınırıdır: onaydan önce
yazılan hiçbir şey kalıcı sayılmaz, onaydan sonra yazılan her şey sayılır.

Kalıcılığın gerçekleştirimi genellikle bir kütüğe dayanır: değişiklikler veri sayfalarına
uygulanmadan önce ardışık bir **yazma öncesi günlükne** (write-ahead log) yazılır ve o
yazma disk üzerinde güvenceye alınır. Çökme sonrası açılışta kütük okunur; onaylanmış ama
sayfalara işlenmemiş değişiklikler tamamlanır, onaylanmamış olanlar geri alınır. **Kütük
biçimi, adı ve diske yazmanın hangi noktada güvenceye alındığı motora göre değişir**;
bazı motorlarda bu davranış ayarlanabilir ve gevşetildiğinde kalıcılık güvencesi de
gevşer.

#### Bedeli ve Sınırları

Dört güvence bedava değildir. Atomiklik ve kalıcılık ek yazma gerektirir; yalıtım ya
kilit ya da sürüm saklama gerektirir. İşlemi gereğinden uzun tutmak, ne kadar veriyi ne
kadar süre kilitlediğini büyütür — kullanıcı girdisi bekleyen bir işlem yaygın bir
tasarım hatasıdır.

İki sınır da belirtilmelidir. Birincisi, ACID tek bir veritabanı içindir; iki ayrı sistemi
kapsayan bir iş için aynı güvence kendiliğinden gelmez. İkincisi, tutarlılık sözü
**şemada yazılı** kısıtlarla sınırlıdır. Şemaya yazılmamış bir iş kuralı, onaylanmış bir
işlem sonunda bozulmuş olabilir ve motor bunu bilmez. Bütünlük Kısıtları dersindeki
"kuralı şemaya yaz" ilkesinin buradaki karşılığı budur.

#### Özet

- İşlem, tek bir birim gibi ele alınan deyimler dizisidir; sınırı onaylama ya da geri
  alma ile belirlenir.
- Atomiklik ara durumun kalıcı olmamasını, tutarlılık işlem sonunda bütün kısıtların
  sağlanmasını ister.
- Motorun kısıtı reddetmesi tek başına yetmez; hatayı yok sayıp onay gönderen bir istemci
  yarım işi kalıcı kılar.
- Yalıtım, eşzamanlı işlemlerin birbirinin yarım durumunu görmemesidir; kademeleri ve
  gerçekleştirimi ayrı bir kursun konusudur.
- Kalıcılık onay anına bağlıdır ve genellikle yazma öncesi günlükyle sağlanır; güvencenin
  ne kadar sıkı olduğu motora göre değişir ve ayarlanabilir.

#### Sonraki Adım

Bu ders ilişkisel veritabanının verdiği sözleri tam olarak adlandırdı. Aynı adlandırma,
bir karşılaştırmayı da mümkün kılar: bu sözlerin bir bölümünden vazgeçen sistemler vardır
ve karşılığında başka şeyler sunarlar. Kursun son dersi bu seçimi ele alır — hangi veri ve
hangi erişim biçimi ilişkisel modeli gerektirir, hangi durumda başka bir veri modeli daha
uygun olur ve bu karar hangi ölçütlerle verilir?

### İlişkisel ve İlişkisel Olmayan Seçimi

Source: https://academia.sh/tr/kurslar/iliskisel-kuram/iliskisel-ve-iliskisel-olmayan-secimi

İlişkisel olmayan veri modeli ailelerinin neyi bırakıp neyi verdiği, belge saklamanın ölçülen ödünleşimi, şema esnekliği yanılgısı ve seçimin karar ölçütleri.

Önceki ders ilişkisel veritabanının verdiği sözleri adlandırdı. Aynı adlandırma bir
karşılaştırmayı mümkün kılar: bu sözlerin bir bölümünden vazgeçen sistemler vardır ve
karşılığında başka şeyler sunarlar. Bu dersin sorusu şudur: hangi veri ve hangi erişim
biçimi ilişkisel modeli gerektirir, hangi durumda başka bir veri modeli daha uygundur ve
bu karar hangi ölçütlerle verilir?

#### Vazgeçilen Nedir

İlişkisel model dört şey verir: veriden bağımsız bildirimsel sorgu, şemada uygulanan
kısıtlar, keyfî birleştirme ve işlem güvenceleri. İlişkisel olmayan sistemler bunların
bir bölümünü bırakır. Bırakma nedeni genellikle üçünden biridir: veriyi birden çok
makineye dağıtmak, tek bir erişim biçimini çok ucuza indirmek, ya da şemanın önceden
bilinmediği bir alanı karşılamak.

Karar bu yüzden "hangisi daha iyi" sorusu değildir. Soru şudur: bu iş, bırakılan
güvencelerden hangisine ihtiyaç duyuyor?

#### Veri Modeli Aileleri

**Anahtar–değer deposu** (key-value store) tek bir erişim biçimini destekler: anahtarla
getir, anahtarla yaz. Değerin içi sistem için anlamsızdır, dolayısıyla içine göre sorgu
yapılamaz. Oturum durumu ve önbellek gibi işlerde yerindedir.

**Belge deposu** (document store) değeri yapılandırılmış bir belge olarak tutar ve
içindeki alanlara göre sorguya izin verir. Doğal birimi **birleşik kayıttır** (aggregate):
birlikte okunan ve birlikte yazılan veri tek belgede durur.

**Sütun ailesi deposu** (wide-column store) satırları anahtara göre bölümler ve her satırda
seyrek, çok sayıda sütun tutar. Çok büyük yazma hacimlerini dağıtmaya yönelir.

**Çizge veritabanı** (graph database) düğüm ve kenarları birinci sınıf nesne yapar. Uzun
yollar boyunca dolaşmayı — "bu üyenin ödünç aldığı kitapları alan üyelerin başka ne aldığı"
gibi — ilişkisel birleştirme zincirinden ucuza getirir.

**Arama dizini** (search index) metni sözcüklerine ayırıp ters dizin kurar; alaka sırasına
göre sonuç döndürür. Kataloğun serbest metin araması buna örnektir.

**Zaman serisi deposu** (time series store) zaman damgalı ölçümleri sıkıştırarak saklar ve
aralık sorgularına göre düzenlenir.

Bu aileler birbirini dışlamaz; birçok ilişkisel motor belge, metin arama ve çizge
sorgulamayı kendi içinde sunar. **Hangi yeteneğin hangi motorda bulunduğu değişir** ve bu
yüzden karar ürün adıyla değil gereksinimle verilir.

#### Belge Saklamanın Ödünleşimi

Belge yaklaşımının kazancı ve bedeli aynı yerden gelir: veri, tek bir okuma biçimine göre
bir araya getirilmiştir.

```sh
sqlite3 :memory: <<'SQL'
.headers on
.mode box
.nullvalue (bos)
CREATE TABLE uye_belge (uye_no INTEGER PRIMARY KEY, belge TEXT NOT NULL);
INSERT INTO uye_belge VALUES
 (41, '{"ad":"Ayse Demir","odunc":[{"isbn":"975-01"},{"isbn":"975-02"}]}'),
 (52, '{"ad":"Mert Kaya","odunc":[{"isbn":"975-01"}]}'),
 (63, '{"odunc":[]}');

SELECT uye_no, belge ->> '$.ad' AS ad,
       json_array_length(belge, '$.odunc') AS odunc_sayisi
FROM uye_belge ORDER BY uye_no;

SELECT o.value ->> '$.isbn' AS isbn, COUNT(*) AS kez
FROM uye_belge u, json_each(u.belge, '$.odunc') o
GROUP BY isbn ORDER BY isbn;
SQL
```

```
┌────────┬────────────┬──────────────┐
│ uye_no │     ad     │ odunc_sayisi │
├────────┼────────────┼──────────────┤
│ 41     │ Ayse Demir │ 2            │
│ 52     │ Mert Kaya  │ 1            │
│ 63     │ (bos)      │ 0            │
└────────┴────────────┴──────────────┘
┌────────┬─────┐
│  isbn  │ kez │
├────────┼─────┤
│ 975-01 │ 2   │
│ 975-02 │ 1   │
└────────┴─────┘
```

Belge alanlarına erişen işleçlerin adı ve yazılışı **motora göre değişir**; burada
gösterilen davranış her yerde aynıdır.

Üç sonuç okunabilir. Birincisi kazanç: bir üyenin adı ve ödünç listesi tek satırdan gelir,
hiçbir birleştirme yoktur. İkincisi bedel: "hangi kitap kaç kez alındı" sorusu bütün
belgeleri açmayı ve içlerindeki dizileri düzleştirmeyi gerektirir. Belge sınırının dışına
çıkan her soru pahalıdır. Üçüncüsü kayıp: `63` numaralı üyenin belgesinde `ad` alanı hiç
yoktur ve bu ekleme hatasız geçmiştir. `NOT NULL` yazılabilecek bir yer bulunmadığı için
kural, belgeyi yazan her programa geri dönmüştür.

#### Şema Esnekliği Yanılgısı

Belge yaklaşımının en sık dile getirilen üstünlüğü şema esnekliğidir: yeni bir alan eklemek
şema değişikliği gerektirmez. Bu doğrudur, ama eksik ifade edilmiştir.

Şema ortadan kalkmaz; yer değiştirir. **Yazma anında şemada** (schema-on-write) yapı
veritabanında tanımlıdır ve her yazma ona uymak zorundadır. **Okuma anında şemada**
(schema-on-read) yapı, belgeyi okuyan kodun beklentisinde durur. Alanın adı değiştiğinde
ya da tipi farklılaştığında sorun kaybolmaz; yazma anında bir hata olarak görünmek yerine,
okuma anında bir davranış farkı olarak ortaya çıkar. Üstelik depoda artık her iki biçimden
belgeler vardır ve okuyan kodun ikisini de karşılaması gerekir.

Ölçüt şudur: yapı gerçekten öngörülemez ve üzerinde kısıt aranmıyorsa esneklik kazançtır.
Yapı bilinebiliyorsa, onu şemaya yazmamak yalnızca doğrulamayı ertelemektir.

#### Ölçek ve Dağıtım

Veriyi birden çok makineye dağıtmak — **veri bölümleme** (partitioning) — ilişkisel
modelin iki yeteneğini zorlar. Birleştirme, parçaları farklı makinelerde duran iki bağıntı
arasında ağ üzerinden yapılır. İşlem güvenceleri, birden çok makineyi kapsadığında ek
eşgüdüm ve gecikme gerektirir.

İlişkisel olmayan sistemlerin bir bölümü bu iki yeteneği baştan sınırlayarak dağıtımı
ucuzlatır: birleştirme sunulmaz, işlem tek bir birleşik kayıtla sınırlanır ve okumanın
en son yazmayı görmesi güvence altına alınmaz — buna **sonunda tutarlılık** (eventual
consistency) denir.

Burada dikkat edilecek nokta sıralamadır. Dağıtım, veri tek makineye sığmadığında ya da
tek makinenin dayanabileceğinden çok istek geldiğinde gerekir. Bu eşiğe ulaşmadan
dağıtımın maliyetini üstlenmek, hiçbir karşılığı olmayan bir kayıptır.

#### Karar Ölçütleri

Beş soru, kararın çoğunu belirler.

- **Veri şekli.** Varlıklar arasında çoka çok ilişkiler var mı, yoksa veri doğal olarak
  bağımsız birleşik kayıtlara ayrılıyor mu? Çoka çok ilişki yoğunsa ilişkisel model
  kazanır.
- **Erişim biçimi.** Sorgular önceden biliniyor ve hep aynı anahtardan mı geliyor, yoksa
  sonradan tanımlanan keyfî sorular mı sorulacak? İkincisi bildirimsel sorgu gerektirir.
- **Kısıt gereksinimi.** Birden çok varlığı kapsayan değişmezler var mı? "Bir nüsha aynı
  anda tek üyede olabilir" kuralı, belge sınırının dışına taşan bir kısıttır.
- **Tutarlılık gereksinimi.** Okumanın en son yazmayı görmesi zorunlu mu? Ödünç verme
  gişesinde zorunludur; katalog önerilerinde değildir.
- **Şema kararlılığı.** Yapı biliniyor ve nadiren mi değişiyor, yoksa alan başına
  öngörülemez mi?

Yanıtlar tek bir sisteme işaret etmeyebilir. **Çoklu depo** (polyglot persistence),
farklı işler için farklı depolar kullanmaktır: kayıtların ilişkisel motorda, serbest metin
aramanın arama dizininde tutulması gibi. Bedeli belirgindir — iki depo arasındaki
tutarlılık artık uygulamanın sorumluluğundadır ve bu sorumluluk kursun ilk dersindeki
dosya sorununun daha büyük ölçekte geri dönüşüdür. Bu yüzden yaygın sıralama şudur: kayıt
sistemi ilişkisel motorda tutulur, ikincil depolar ondan türetilir.

#### Özet

- İlişkisel olmayan sistemler bildirimsel sorgu, şema kısıtları, keyfî birleştirme ve
  işlem güvencelerinden bir bölümünü dağıtım ya da tek bir erişim biçiminin ucuzluğu için
  bırakır.
- Belge saklama, birleşik kayıt içindeki okumayı birleştirmesiz yapar; belge sınırının
  dışına çıkan soru bütün belgeleri açmayı gerektirir.
- Şema esnekliği şemayı ortadan kaldırmaz, onu okuyan koda taşır; doğrulama silinmez,
  ertelenir.
- Dağıtım birleştirmeyi ve işlem güvencelerini pahalılaştırır; eşiğe ulaşmadan üstlenilen
  dağıtım karşılıksız maliyettir.
- Seçim veri şekli, erişim biçimi, kısıt ve tutarlılık gereksinimi ile şema kararlılığı
  üzerinden yapılır; çoklu depo kullanılıyorsa kayıt sistemi tek olmalıdır.

#### Kurs Kapanışı

Bu kurs, veriyi dosyada tutmanın sorunlarıyla başladı ve ilişkisel modelin bu sorunlara
verdiği yanıtı adım adım kurdu. Bağıntının küme kuramından gelen tanımı, satır sırasının
neden veriye ait olmadığını ve tablonun bağıntı gibi davranmasının neden şemada
istendiğini açıkladı. Anahtarlar satırı ayırt etme kuralını, bütünlük kısıtları bu kuralın
motor tarafından uygulanmasını verdi. Boş değer, doğru–yanlış ikilisini üçe çıkararak
sorgu yazımının sessiz tuzaklarını açtı. Veri tipleri, bir sütunun ne tutacağının
gösterimden ayrı bir karar olduğunu gösterdi.

İkinci konu tekrarın kaynağını biçimselleştirdi. İşlevsel bağımlılık, anahtar kavramının
genel hâli olarak tanımlandı; normal formlar, sol tarafı anahtar olmayan bağımlılıkları
basamak basamak eledi. Denormalizasyon ters yöndeki kararı ölçüye bağladı, şema tasarım
kalıpları da tekrar eden durumların yerleşik karşılıklarını ve karşı kalıplarını verdi.

Üçüncü konu şemayı iş yüküyle buluşturdu: işlem ile çözümleme yüklerinin zıt gereksinimleri,
ACID güvencelerinin tanımı ve sınırları ve ilişkisel modelin ne zaman doğru seçim
olduğu.

Kurs boyunca kasıtlı olarak açık bırakılan bir boşluk var. Her derste sorgular yazıldı ama
sorgu dilinin kendisi hiç öğretilmedi; `SELECT` deyiminin bölümleri, koşulların yazılışı,
birleştirme türleri, gruplama ve sıralama — hepsi örnek içinde kullanıldı, kural olarak
tanımlanmadı. Boş değerlerle çalışmanın araçları, ödünç geçmişini özetleyen toplama
işlevleri, veriyi değiştiren deyimlerin ayrıntısı da aynı biçimde bekliyor.

SQL Temelleri kursu bu boşluğu doldurur. Burada kurulan modelin — bağıntıların,
anahtarların, kısıtların ve boş değerin — üzerine, o modeli sorgulayan dili sistemli
biçimde kurar. Kütüphane şeması orada da sürer: bu kursta tasarlanan tablolar, orada
yazılacak her sorgunun konusu olacaktır.

