---
title: 'İlişkisel Veritabanı Yönetimi'
source: 'https://academia.sh/tr/kurslar/veritabani-yonetimi'
language: tr
updated: '2026-08-17T04:40:20+00:00'
license: 'CC BY-SA 4.0'
lessons: 25
outcomes:
    - 'Motorun süreç, bellek ve depolama mimarisini açıklar'
    - 'Yedekleme ve kurtarma stratejisi tasarlar'
    - 'Çoğaltma ve yüksek erişilebilirlik topolojisi kurar'
---

# İlişkisel Veritabanı Yönetimi

Motorun süreç, bellek ve depolama mimarisini; yazma öncesi günlük, denetim noktası ve çok sürümlü eşzamanlılık denetimini; dizin türleri, bakımı ve tablo bölümlemeyi; yetkilendirme, yedekleme, kurtarma ve çoğaltmayı öğreten ileri düzey kurs.

## Motor Mimarisi

Süreç ve bellek düzeni, verinin sayfalar halinde dosyalara yerleşmesi, yazma öncesi günlüğün kalıcılığı nasıl sağladığı, denetim noktalarının günlük ile veri dosyasını eşitlemesi, çok sürümlü eşzamanlılık denetimi, ölü satır temizliği ve sistem kataloğunun sorgulanması.

### Süreç ve Bellek Mimarisi

Source: https://academia.sh/tr/kurslar/veritabani-yonetimi/surec-ve-bellek-mimarisi

Sunucu tabanlı ve süreç içi motor kurulumlarının farkı, arka plan süreçlerinin işbölümü, paylaşılan tampon havuzu ile bağlantı başına ayrılan bellek arasındaki ayrım, havuz boyutunun isabet oranına ölçülen etkisi ve paylaşılan havuzun özel önbelleklere üstünlüğü.

İleri SQL kursu, sorgunun ne kadar işle çalıştığını ölçmeyi kurdu: dizin tanımı, koşul
yazımı, birleştirme sırası, istatistik tazeliği. Bütün bu kararlar sorgu düzeyindeydi ve
motorun kendisi kara kutu olarak kaldı. Plan çıktısında `SCAN odunc` yazdığında iki milyon
satırın okunduğu biliniyordu; o satırların nereden okunduğu, bellekte mi diskte mi
bulundukları, okuyanın kim olduğu sorulmadı.

Bu ders o kutuyu açar. Sorusu şudur: bir sorgu geldiğinde onu hangi süreç karşılar, veri
hangi bellekten okunur ve o belleğin boyutu sonucu ne kadar değiştirir? Bir tabloyu bellekte
tutmakla diskten okumak arasındaki fark, iyi bir dizinle kötü bir dizin arasındaki farktan
küçük değildir.

#### Motorun İki Kurulum Biçimi

İlişkisel motorlar iki ayrı biçimde kurulur ve bu seçim mimarinin geri kalanını belirler.

**Sunucu tabanlı motor**, uygulamadan bağımsız bir süreç kümesi olarak çalışır. Uygulama bir
ağ bağlantısı üzerinden istek gönderir, motor isteği kendi süreçlerinde işler, sonucu geri
yollar. Veri dosyalarına yalnız bu süreçler dokunur. Aynı veritabanına yüzlerce uygulama
örneği bağlanabilir; hepsi tek bir yetkili sürece konuşur.

**Süreç içi motor**, uygulamanın kendi sürecine bağlanan bir kitaplıktır. Ayrı bir sunucu
yoktur; sorgu, uygulamanın çağırdığı bir işlev çağrısıyla aynı süreçte çalışır. Veri
dosyasına doğrudan işletim sistemi çağrılarıyla erişilir; eşzamanlılık, dosya kilitleri
üzerinden yürütülür.

Ayrım bir kalite sıralaması değildir. Süreç içi motor ağ gecikmesini ve süreçler arası
iletişimi tümüyle ortadan kaldırır, kurulum ve yönetim gerektirmez; buna karşılık merkezî
bir denetim noktası olmadığı için çok yazarlı yükleri ve ayrıntılı yetkilendirmeyi
sunucu tabanlı kurulum kadar iyi karşılamaz. Bu kurstaki koşumlar süreç içi bir motorla
(`sqlite3`) yapılır; sunucu tabanlı kurulumun farklı davrandığı yerler metinde ayrıca
belirtilir.

#### Arka Plan Süreçlerinin İşbölümü

Sunucu tabanlı bir motorda gelen sorguyu karşılayan süreç, işin yalnız bir bölümünü yapar.
Geri kalanı, kimsenin isteği olmadan sürekli çalışan **arka plan süreçlerine (background
process)** düşer. Adları motora göre değişir; işlevleri değişmez.

**Bağlantı karşılayıcı.** Yeni bağlantıları kabul eder, kimlik doğrulamasını yapar ve o
bağlantıya bir çalışan süreç ya da iş parçacığı atar. Sorguyu ayrıştıran, planlayan ve
yürüten bu çalışandır.

**Günlük yazıcı.** Değişiklik kayıtlarını, kesinleştirme anında diske yazılmayı bekleyen
tampondan alıp dosyaya aktarır. Sonraki iki ders bu sürecin ne yazdığını ayrıntısıyla ele
alır.

**Denetim noktası süreci.** Bellekte değişmiş sayfaları belirli aralıklarla veri
dosyalarına yazar. Görevi, kurtarmanın ne kadar geriye gitmesi gerektiğini sınırlamaktır.

**Temizlik süreci.** Silinmiş ya da güncellenmiş satırların artık kimsenin görmediği eski
sürümlerini toplar ve yerini yeniden kullanılabilir kılar. Bu kursta Ölü Satır Temizliği
dersinin konusudur.

**İstatistik toplayıcı.** Tabloların satır sayısı, değer dağılımı ve erişim sayaçları gibi
bilgileri günceller. İleri SQL kursundaki İstatistikler ve Kardinalite dersinde planlayıcının
tahmin kaynağı olarak geçen sayılar bu süreçten gelir.

Bu ayrıştırmanın nedeni, sorguyu bekletmemektir. Bir sorgu, kendi ürettiği çöpü toplamak ya
da bütün değişmiş sayfaları diske yazmak zorunda kalsaydı, yanıt süresi kullanıcının işiyle
ilgisiz işlerle şişerdi. Arka plan süreçleri bu işleri sorgunun dışına taşır; karşılığında
yönetim, bu süreçlerin ne sıklıkta ve ne kadar agresif çalışacağını ayarlamak olur.

#### Paylaşılan Bellek ve Bağlantı Başına Bellek

Motorun kullandığı bellek tek bir havuz değildir; iki ayrı sınıfa ayrılır ve bu ayrım
sunucu boyutlandırmasının temelidir.

**Paylaşılan tampon havuzu (shared buffer pool)**, veri dosyasından okunan sayfaların
tutulduğu ortak alandır. Bütün bağlantılar aynı havuzu kullanır. Bir sorgu bir sayfayı
okuduğunda sayfa havuza girer; başka bir bağlantı aynı sayfayı isterse disk hiç
okunmaz. Havuz aynı zamanda yazma yolunda da durur: değiştirilen sayfa önce havuzda
değiştirilir, diske yazılması sonraya bırakılır.

**Bağlantı başına bellek**, tek bir sorgunun kendi işi için ayırdığı alandır: sıralama
tamponu, karma birleştirmenin karma tablosu, geçici sonuç kümeleri. Bu alan paylaşılmaz;
her bağlantı kendi payını ayrı ayrı ister.

İki sınıfın ayrımı boyutlandırmada işe yarar, çünkü ölçekleme biçimleri farklıdır. Paylaşılan
havuz sabit bir maliyettir: iki kat bağlantı, iki kat havuz gerektirmez. Bağlantı başına
bellek ise bağlantı sayısıyla çarpılır; sorgu başına ayrılan sıralama tamponu iki yüz eşzamanlı
bağlantıda iki yüz kez ayrılabilir. Bellek yetersizliğiyle sonuçlanan yapılandırmaların çoğu
bu çarpımın gözden kaçmasından doğar.

#### Tampon Havuzunun Ölçülen Etkisi

Süreç içi motorda sayfa önbelleği bağlantı başınadır ve boyutu bir yapılandırma değeriyle
belirlenir. Aşağıdaki koşum aynı sorguyu iki farklı önbellek boyutuyla üç kez çalıştırır ve
sayfa önbelleği isabet/ıska sayaçlarını yazdırır. Veri kümesi, önceki kurslardan devam eden
kütüphane ödünç kayıtlarıdır.

```sh
rm -f kutuphane.db
sqlite3 kutuphane.db <<'SQL'
CREATE TABLE kitap (
  kitap_id INTEGER PRIMARY KEY,
  baslik   TEXT NOT NULL,
  yazar    TEXT NOT NULL,
  yil      INTEGER NOT NULL,
  sube_id  INTEGER NOT NULL
);
INSERT INTO kitap (kitap_id, baslik, yazar, yil, sube_id)
WITH RECURSIVE sayac(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM sayac WHERE n < 200000)
SELECT n, 'Kitap ' || n, 'Yazar ' || (n % 4000), 1950 + (n % 75), 1 + (n % 8)
FROM sayac;
SQL

sqlite3 kutuphane.db "SELECT 'kitap tablosu sayfa sayisi:', count(*) FROM dbstat WHERE name='kitap';"

for boyut in 200 2000; do
  printf '=== onbellek %s sayfa ===\n' "$boyut"
  sqlite3 kutuphane.db <<SQL 2>&1 | grep -E 'Page cache (hits|misses)'
PRAGMA cache_size = $boyut;
.stats on
SELECT count(*) FROM kitap WHERE yil > 2000;
SELECT count(*) FROM kitap WHERE yil > 2000;
SELECT count(*) FROM kitap WHERE yil > 2000;
SQL
done
```

```
kitap tablosu sayfa sayisi:|1777
=== onbellek 200 sayfa ===
Page cache hits:                     2
Page cache misses:                   1778
Page cache hits:                     2
Page cache misses:                   1776
Page cache hits:                     2
Page cache misses:                   1776
=== onbellek 2000 sayfa ===
Page cache hits:                     2
Page cache misses:                   1778
Page cache hits:                     1778
Page cache misses:                   0
Page cache hits:                     1778
Page cache misses:                   0
```

Sayılar sorunun tamamını anlatıyor. Tablo 1777 sayfa tutuyor. Önbellek 200 sayfa olduğunda
her tarama neredeyse bütün sayfaları yeniden okumak zorunda: ilk taramanın sonunda önbellekte
yalnız son 200 sayfa vardır, ikinci tarama ise baştan başlar ve orada bulunmayan sayfaları
ister. İsabet oranı üç taramada da sıfıra yakın kalır.

Önbellek 2000 sayfa olduğunda birinci tarama yine 1778 ıska üretir — veri henüz bellekte
değildir. İkinci ve üçüncü taramalar ise tek bir ıska bile üretmez: bütün sayfalar havuzdadır.
Aynı sorgu, aynı plan, aynı adım sayısı; tek fark sayfaların nereden geldiğidir.

Çıktının okunmasında bir uyarı gerekir: `.stats on` sayaçları motorun kendi sayfa
önbelleğine aittir ve işletim sisteminin dosya önbelleğini kapsamaz. Bir ıska, verinin
mutlaka diskten geldiği anlamına gelmez — yalnız motorun havuzunda bulunmadığı anlamına
gelir. Sayfa sayısı ise ortamdan bağımsızdır: aynı veri ve aynı sayfa boyutu her makinede
1777 sayfa verir.

#### Havuz Boyutu ve Çalışma Kümesi

Ölçüm bir uçtan diğerine geçişi gösterdi. Aradaki eğri, havuz boyutunu seçerken asıl
bakılması gereken şeydir ve gerçek bir motorda tek tek denemek pahalıdır. Aşağıdaki model
onu ucuza gösterir: sayfa isteklerinin bir bölümü tablonun küçük bir kesimine yığılır,
havuz en az kullanılanı atarak yer açar.

Bu bir **modeldir**; motorun kendi tampon havuzu değildir. Tahliye politikası gerçek
motorlarda daha ayrıntılıdır ve okuma dışında yazma da havuzu etkiler. Modelin gösterdiği
şey, boyut ile isabet oranı arasındaki ilişkinin biçimidir.

```sh
cat > havuz.mjs <<'JS'
// Model: en az kullanilani atan bir sayfa havuzu.
class Havuz {
  constructor(kapasite) { this.kapasite = kapasite; this.sayfalar = new Map(); this.isabet = 0; this.iska = 0; }
  oku(sayfa) {
    if (this.sayfalar.has(sayfa)) { this.sayfalar.delete(sayfa); this.sayfalar.set(sayfa, 1); this.isabet++; return; }
    this.iska++;
    if (this.sayfalar.size >= this.kapasite) this.sayfalar.delete(this.sayfalar.keys().next().value);
    this.sayfalar.set(sayfa, 1);
  }
  oran() { return this.isabet / (this.isabet + this.iska); }
}
function uretec(tohum) {                 // belirlenimci sozde rastgele uretec
  let a = tohum >>> 0;
  return () => { a = (a + 0x9e3779b9) >>> 0;
    let t = a; t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296; };
}
const TABLO = 2000, SICAK = 200, ISTEK = 500000;
function yukUret(tohum) {                // isteklerin %90'i sicak 200 sayfaya gidiyor
  const r = uretec(tohum), l = new Int32Array(ISTEK);
  for (let i = 0; i < ISTEK; i++)
    l[i] = r() < 0.9 ? Math.floor(r() * SICAK) : SICAK + Math.floor(r() * (TABLO - SICAK));
  return l;
}

console.log('— tek havuz: kapasite ile isabet oranı —');
const yuk = yukUret(2024);
for (const k of [20, 50, 100, 200, 400, 1000, 2000]) {
  const h = new Havuz(k);
  for (const s of yuk) h.oku(s);
  console.log(String(k).padStart(6), 'sayfa →', (h.oran() * 100).toFixed(1).padStart(5), '%');
}

console.log('— aynı toplam bellek: dört özel önbellek / bir paylaşılan havuz —');
const yukler = [1, 2, 3, 4].map((i) => yukUret(2024 + i));
const ozel = [1, 2, 3, 4].map(() => new Havuz(100));
let i_ozel = 0, k_ozel = 0;
yukler.forEach((y, i) => { for (const s of y) ozel[i].oku(s); });
ozel.forEach((h) => { i_ozel += h.isabet; k_ozel += h.iska; });
const paylasilan = new Havuz(400);
for (let i = 0; i < ISTEK; i++) for (const y of yukler) paylasilan.oku(y[i]);
console.log('dört × 100 sayfa özel →', ((i_ozel / (i_ozel + k_ozel)) * 100).toFixed(1), '%');
console.log('bir × 400 sayfa paylaşılan →', (paylasilan.oran() * 100).toFixed(1), '%');
JS
node havuz.mjs
```

```
— tek havuz: kapasite ile isabet oranı —
    20 sayfa →   8.1 %
    50 sayfa →  20.1 %
   100 sayfa →  39.4 %
   200 sayfa →  73.6 %
   400 sayfa →  91.1 %
  1000 sayfa →  94.4 %
  2000 sayfa →  99.6 %
— aynı toplam bellek: dört özel önbellek / bir paylaşılan havuz —
dört × 100 sayfa özel → 39.4 %
bir × 400 sayfa paylaşılan → 91.1 %
```

Eğrinin biçimi tektiptir: havuz, sık istenen sayfa kümesini alacak kadar büyüyene kadar
isabet oranı hızla yükselir, o noktadan sonra kazanç yassılaşır. Bu kümeye **çalışma kümesi
(working set)** denir. Modelde çalışma kümesi 200 sayfadır; havuz 200'e ulaştığında oran
%73,6, 400'de %91,1 olur, sonrasında iki katı bellek yalnız üç puan getirir.

Yönetim kararı buradan çıkar: havuzu tablonun tamamına göre değil, gerçekten dokunulan
bölümüne göre boyutlandırmak gerekir. Yüz gigabaytlık bir veritabanının çalışma kümesi
birkaç gigabayt olabilir; belleği o eşiğin biraz üstüne çıkarmak, veritabanının tamamını
belleğe sığdırmaya çalışmaktan hem ucuz hem de neredeyse aynı ölçüde etkilidir.

Modelin ikinci bölümü, mimarinin başındaki ayrımı sayıya çevirir. Toplam bellek her iki
düzende de 400 sayfadır. Dört bağlantı kendi 100 sayfalık önbelleğini tuttuğunda isabet
oranı %39,4'te kalır; aynı bellek tek bir paylaşılan havuz olduğunda %91,1'e çıkar. Neden
açıktır: dört bağlantının çalışma kümeleri büyük ölçüde örtüşür, özel önbelleklerde aynı
sıcak sayfalar dört kez saklanır ve her kopya için ayrı ayrı yer harcanır. Sunucu tabanlı
motorların tampon havuzunu paylaştırmasının gerekçesi budur.

#### Özet

- Sunucu tabanlı motor ayrı bir süreç kümesi olarak çalışır ve veri dosyalarına yalnız o
  süreçler dokunur; süreç içi motor uygulamanın sürecine bağlanan bir kitaplıktır.
- Arka plan süreçleri günlük yazma, denetim noktası, temizlik ve istatistik toplama
  işlerini sorgunun yanıt yolundan çıkarır.
- Bellek iki sınıfa ayrılır: paylaşılan tampon havuzu bağlantı sayısıyla ölçeklenmez,
  bağlantı başına ayrılan sıralama ve karma alanları ölçeklenir.
- Ölçümde 1777 sayfalık bir tablo, 200 sayfalık önbellekte her taramada yeniden okundu;
  2000 sayfalık önbellekte ikinci taramadan sonra hiç ıska üretmedi.
- Model, isabet oranının çalışma kümesi havuza sığana kadar hızla yükselip sonra
  yassılaştığını ve aynı toplam belleğin paylaşılan havuzda %39,4 yerine %91,1 isabet
  verdiğini gösterdi.

#### Sonraki Adım

Bu derste sayfa, bellekte tutulan bir birim olarak geçti: havuza giren, atılan, isabet ya da
ıska üreten bir şey. Sayfanın kendisinin ne olduğu, içine kaç satırın girdiği, satır sığmadığında
ne olduğu ve bütün bu sayfaların diskte hangi dosyalara nasıl yerleştiği sorulmadı. Sonraki
ders veriyi diskteki hâliyle ele alır: sayfa boyutu, sayfa sayısı ve dosya boyutu arasındaki
ilişki ölçülür, tablo ile dizinin dosya içindeki payı ayrı ayrı sayılır ve tablo alanı kavramı
tanımlanır.

### Fiziksel Depolama Düzeni

Source: https://academia.sh/tr/kurslar/veritabani-yonetimi/fiziksel-depolama-duzeni

Sayfanın motorun en küçük okuma ve yazma birimi olması, sayfa boyutu ile sayfa sayısının dosya boyutunu tam olarak belirlemesi, sayfa içi doluluk ve sayfa başına satır sayısının ölçülmesi, taşma sayfaları ve tablo alanı kavramı.

Önceki ders sayfayı bellekteki bir birim olarak kullandı: tampon havuzuna giren, atılan,
isabet ya da ıska üreten bir şey. Sayfanın kendisinin ne olduğu sorulmadı. Neden satır satır
değil de sayfa sayfa okunuyor, bir sayfaya kaç satır giriyor, bir satır sayfaya sığmazsa ne
oluyor?

Bu dersin sorusu verinin diskteki hâlidir. Ölçülecek üç ilişki vardır: sayfa boyutu ile
dosya boyutu arasındaki ilişki, sayfa içindeki doluluk ve tablo ile dizinin toplam yer
içindeki payı. Üçü de sayılabilir; hiçbiri tahmine bırakılmaz.

#### Sayfa Neden Vardır

**Sayfa (page)**, motorun disk ile bellek arasında taşıdığı en küçük birimdir. Tek bir
satır okunmak istendiğinde bile o satırın bulunduğu sayfanın tamamı okunur.

Nedeni donanımdadır. Bilgisayarlar Nasıl Çalışır kursundaki bellek hiyerarşisi
tartışmasının aynısıdır bu: disk erişiminin sabit bir maliyeti vardır ve bu maliyet, okunan
bayt sayısıyla değil, erişim sayısıyla ölçülür. Yüz baytlık bir satırı okumak için yapılan
iş ile dört kilobaytlık bir sayfayı okumak için yapılan iş neredeyse aynıdır. Sabit boyutlu
bir birim ayrıca defter tutmayı kolaylaştırır: tampon havuzundaki her yuva aynı büyüklüktedir,
bir sayfanın yerine başka bir sayfa parçalanma üretmeden konabilir.

Sayfa boyutu veritabanı oluşturulurken belirlenir ve sonradan değiştirilmesi verinin
yeniden yazılmasını gerektirir. Küçük sayfa, seçici okumalarda daha az boşa taşınan bayt
demektir; büyük sayfa, sıralı taramalarda daha az erişim ve daha az defter tutma demektir.

#### Sayfa Sayısı Dosya Boyutunu Belirler

İlişki tam olarak ölçülebilir. Aşağıdaki blok, önceki kurslardan devam eden kütüphane veri
kümesini kurar ve üç sayıyı yan yana koyar.

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

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

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

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

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

sqlite3 -header -column kutuphane.db <<'SQL'
SELECT (SELECT page_size FROM pragma_page_size) AS sayfa_boyutu,
       (SELECT page_count FROM pragma_page_count) AS sayfa_sayisi,
       (SELECT page_size FROM pragma_page_size) * (SELECT page_count FROM pragma_page_count) AS hesaplanan_bayt;
.shell echo "dosyanin gercek boyutu: $(wc -c < kutuphane.db)"
SQL
```

```
sayfa_boyutu  sayfa_sayisi  hesaplanan_bayt
------------  ------------  ---------------
4096          20783         85127168       
dosyanin gercek boyutu: 85127168
```

Hesaplanan değer ile dosyanın gerçek boyutu birbirinin aynısı. Bu bir yaklaşıklık değil,
tanımdır: veri dosyası, art arda dizilmiş eşit boyutlu sayfalardan başka bir şey içermez.
Dosya büyüdüğünde sayfa sayısı artar; sayfa sayısı azalmadan dosya küçülmez. Bu iki cümle,
ileride şişme ve boşluk geri kazanımı tartışılırken doğrudan kullanılacaktır.

Sayfa boyutu bu ortamda 4096 bayttır. Sayfa sayısı ve dolayısıyla dosya boyutu veriye
bağlıdır; farklı bir sayfa boyutuyla kurulmuş bir veritabanında iki sayı da değişir, ilişki
değişmez.

#### Sayfanın İçinde Ne Var

Sayfalar hepsi aynı işi görmez. B-ağacında yer alan bir tablo için üç sayfa türü ayırt
edilir: **yaprak sayfa (leaf page)** satırların kendisini tutar, **iç sayfa (internal page)**
alt ağaçlara yönlendiren anahtarları tutar, **taşma sayfası (overflow page)** ise bir sayfaya
sığmayan değerin artan bölümünü tutar.

```sh
[ -f kutuphane.db ] || sqlite3 kutuphane.db < kurulum.sql   # veri kümesi ilk blokta kuruldu

sqlite3 -header -column kutuphane.db <<'SQL'
SELECT pagetype AS sayfa_turu, count(*) AS sayfa, sum(ncell) AS hucre,
       round(1.0 * sum(ncell) / count(*), 1) AS sayfa_basina_hucre
FROM dbstat WHERE name = 'odunc' GROUP BY pagetype;

SELECT name AS nesne, count(*) AS sayfa, sum(ncell) AS hucre,
       round(100.0 * sum(payload) / sum(pgsize), 1) AS doluluk_yuzde
FROM dbstat GROUP BY name ORDER BY sayfa DESC;
SQL
```

```
sayfa_turu  sayfa  hucre    sayfa_basina_hucre
----------  -----  -------  ------------------
internal    46     17901    389.2             
leaf        17902  2000000  111.7             
nesne          sayfa  hucre    doluluk_yuzde
-------------  -----  -------  -------------
odunc          17948  2017901  82.8         
kitap          1777   201771   82.5         
uye            1056   121051   82.9         
sube           1      8        3.4          
sqlite_schema  1      4        16.2         
```

İki milyon satır 17.902 yaprak sayfaya yerleşmiş: sayfa başına ortalama 111 satır. Bunların
üstünde 46 iç sayfa var ve her biri ortalama 389 anahtar taşıyor. Ağacın yüksekliği buradan
okunur: 46 iç sayfanın biri köktür, kalan 45'i onun altındadır ve yapraklara işaret eder.
Yani iki milyon satırlık bir tabloda herhangi bir satıra ulaşmak üç sayfa okumakla
tamamlanır. Veri Yapıları kursundaki B-Ağaçları dersinin "yükseklik kayıt sayısının
logaritmasıyla artar" sonucu, burada ölçülmüş bir sayıdır.

Doluluk sütunu ikinci bir bilgi verir: sayfaların yaklaşık %83'ü gerçek veriyle doludur.
Kalan pay, hücre işaretçileri ve sayfa başlığı gibi defter bilgisiyle, bir de kasıtlı
bırakılmış boşlukla gider. Boşluğun bir kısmı gereklidir: sayfa tıka basa dolu olursa
araya eklenen tek bir satır sayfayı bölmeye zorlar. `sube` tablosunun %3,4 dolulukta
görünmesi bir kusur değildir; sekiz satır tek bir sayfaya girmiş, sayfanın geri kalanı
boş kalmıştır. Küçük tablolarda en küçük birim yine bir sayfadır.

#### Satır Sayfaya Sığmadığında

Sayfa boyutu sabit olduğuna göre, bir satırın uzunluğu sayfa boyutunu aşarsa ne olur? Motor
satırın başlangıcını normal sayfada tutar ve artanı taşma sayfalarına zincirler.

```sh
rm -f tasma.db
sqlite3 -header -column tasma.db <<'SQL'
CREATE TABLE kitap_ozet(kitap_id INTEGER PRIMARY KEY, ozet TEXT NOT NULL);
INSERT INTO kitap_ozet VALUES (1, replace(hex(zeroblob(40)),'0','a'));
INSERT INTO kitap_ozet VALUES (2, replace(hex(zeroblob(600)),'0','b'));
INSERT INTO kitap_ozet VALUES (3, replace(hex(zeroblob(6000)),'0','c'));
SELECT kitap_id, length(ozet) AS ozet_uzunlugu FROM kitap_ozet;
SELECT pageno AS sayfa, pagetype AS tur, ncell AS hucre, payload AS yuk, unused AS bos
FROM dbstat WHERE name = 'kitap_ozet' ORDER BY pageno;
SQL
```

```
kitap_id  ozet_uzunlugu
--------  -------------
1         80           
2         1200         
3         12000        
sayfa  tur       hucre  yuk   bos 
-----  --------  -----  ----  ----
2      internal  1      0     4077
3      overflow  0      4092  0   
4      overflow  0      4092  0   
5      leaf      2      1288  2791
6      leaf      1      3821  258 
```

İlk iki özet — 80 ve 1200 karakter — tek bir yaprak sayfaya birlikte sığmış (sayfa 5).
Üçüncüsü 12.000 karakter ve tek başına bir yaprak sayfayı doldurmuş: o sayfada 3.821 bayt
duruyor, kalanı iki taşma sayfasına yayılmış (3.821 + 4.092 + 4.092 = 12.005; fark satır
başlığındandır). Sayfa numaralarının sıralı olmaması, sayfaların ayrılma sırasını yansıtır;
mantıksal sıra değil, ayırma sırasıdır.

Bunun bedeli okumada ortaya çıkar. Uzun bir metin sütunu, o sütuna hiç bakmayan sorgular
için bile satırı ağırlaştırır — sayfa başına daha az satır sığar, tarama daha çok sayfa
okur. Şema tasarımındaki bir alışkanlığın gerekçesi budur: nadiren okunan uzun metin
alanlarını ayrı bir tabloya taşımak, ana tablonun sayfa başına satır yoğunluğunu korur.

#### Tablo ve Dizinin Dosyadaki Payı

Dizin de bir B-ağacıdır ve o da sayfalarda durur. Aynı sayaç, dizinin yer maliyetini
tabloyla aynı ölçekte gösterir.

```sh
[ -f kutuphane.db ] || sqlite3 kutuphane.db < kurulum.sql   # veri kümesi ilk blokta kuruldu

sqlite3 kutuphane.db 'CREATE INDEX odunc_uye ON odunc(uye_id);'
sqlite3 -header -column kutuphane.db "
SELECT name AS nesne, count(*) AS sayfa, count(*) * 4096 / 1048576 AS mib
FROM dbstat GROUP BY name ORDER BY sayfa DESC;"
```

```
nesne          sayfa  mib
-------------  -----  ---
odunc          17948  70 
odunc_uye      5738   22 
kitap          1777   6  
uye            1056   4  
sube           1      0  
sqlite_schema  1      0  
```

Tek bir tamsayı sütunu üzerindeki dizin, tablonun yaklaşık üçte biri kadar yer kaplıyor:
70 MiB'lik tabloya 22 MiB. Oran şaşırtıcı değildir — dizin girdisi anahtar değerini ve
satıra ulaşmayı sağlayan başvuruyu tutar, yani satırın tamamını değil ama satır başına
sabit bir maliyeti. İleri SQL kursundaki Dizin Kavramı dersinde "dizin yer kaplar" cümlesiyle
geçilen maliyet budur; burada nesne başına sayılabilir hâldedir.

Bu sayaç yönetimde doğrudan işe yarar. Bir veritabanının hangi tablo ve dizin tarafından
şişirildiği, tahminle değil bu sorguyla belirlenir; kapasite planlaması da gereksiz dizinlerin
tespiti de aynı listeden başlar.

#### Dosya Düzeni ve Tablo Alanı

Sayfaların hangi dosyalara yerleştiği motora göre değişir ve bu, yönetimin görünür yüzüdür.
Üç düzen yaygındır: bütün veritabanının tek bir dosyada tutulması, her tablo ve dizinin ayrı
bir dosyaya yazılması, ya da birden çok dosyanın önceden ayrılmış bir alan kümesi olarak
yönetilmesi.

**Tablo alanı (tablespace)**, bu dosyaların bulunduğu yeri adlandıran bir soyutlamadır.
Tablo oluşturulurken bir tablo alanı adı verilir; o adın hangi dizine ya da hangi diske
karşılık geldiği yönetim tarafından ayrıca tanımlanır. Kazancı, verinin fiziksel yerini
şemadan ayırmasıdır: sık okunan bir tablo hızlı bir depolama birimine, arşiv nitelikli bir
tablo ucuz bir birime yerleştirilebilir ve bunun için tablo tanımına dokunulmaz.

Bu ayrımın gösterilemediği bir ortamda çalışıldığı için açıkça söylemek gerekir: buradaki
koşumlarda veritabanının tamamı tek bir dosyadır ve tablo alanı kavramının karşılığı yoktur.
Sayfa, doluluk ve taşma üzerine ölçülen her şey düzenden bağımsızdır; dosya sayısı ve tablo
alanı adlandırması ise motora özgüdür.

#### Özet

- Sayfa, motorun disk ile bellek arasında taşıdığı en küçük birimdir; erişimin sabit
  maliyeti tek satır okumayı anlamsız kılar.
- Dosya boyutu, sayfa boyutu ile sayfa sayısının çarpımıdır; ölçümde 4096 × 20.783 =
  85.127.168 bayt, dosyanın gerçek boyutuna birebir eşit çıktı.
- İki milyon satır 17.902 yaprak sayfaya yerleşti (sayfa başına 111 satır) ve üstlerinde 46
  iç sayfa var; ağacın yüksekliği üçtür ve sayfaların doluluğu %83 civarındadır.
- Sayfaya sığmayan değer taşma sayfalarına zincirlenir; 12.000 karakterlik bir metin yaprak
  sayfada 3.821 bayt bırakıp iki taşma sayfası kullandı.
- Dizin de sayfalarda durur: tek tamsayı sütunlu bir dizin, 70 MiB'lik tablonun yanında
  22 MiB yer kapladı.

#### Sonraki Adım

Bu ders verinin diskte nasıl durduğunu gösterdi, ancak oraya nasıl vardığını sormadı. Bir
satır güncellendiğinde ilgili sayfa bellekte değişir; o sayfa diske yazılmadan önce
elektrik kesilirse ne olur? Kesinleştirilmiş bir işlemin sonucunun kaybolmaması, sayfaların
diske yazılmasına bağlı olsaydı her kesinleştirme rastgele yerlere yazma anlamına gelirdi.
Sonraki ders bu sorunun çözümünü ele alır: değişiklikleri veri dosyasından önce sıralı bir
günlüğe yazmak. Günlük dosyasının oluşması, büyümesi ve bir çökmeden sonra verinin oradan
geri gelmesi ölçülerek gösterilecek.

### Yazma Öncesi Günlük

Source: https://academia.sh/tr/kurslar/veritabani-yonetimi/yazma-oncesi-gunluk

Kesinleştirmenin veri sayfalarını diske yazmaya bağlanmasının maliyeti, yazma öncesi kuralın tanımı, günlük dosyasının kesinleştirmede büyüyüp veri dosyasının değişmemesi, kesinleştirilmiş verinin yalnız günlükte bulunduğunun gösterilmesi, çökme sonrası kurtarma ve günlük kipleriyle eşitleme ayarının ölçülen etkisi.

Önceki ders verinin diskte sayfalar hâlinde durduğunu gösterdi. Bir satır güncellendiğinde
önce bellekteki sayfa değişir. Peki o sayfa diske yazılmadan makine kapanırsa? İşlemler
konusunda kesinleştirilen bir değişikliğin kalıcı olduğu söylenmişti; bu söz, sayfa henüz
diskte değilken nasıl tutulur?

Akla gelen ilk cevap — kesinleştirme anında değişmiş bütün sayfaları diske yazmak — hem
pahalıdır hem de yetmez. Bu ders neden yetmediğini ve gerçekte ne yapıldığını ele alır.
Ölçülecek şey nettir: kesinleştirilmiş bir veri, kesinleştirmenin hemen ardından hangi
dosyanın içindedir?

#### Sayfaları Yazmak Neden Çözüm Değil

Kesinleştirmede değişmiş sayfaları diske yazmanın üç ayrı sorunu vardır.

**Yazmalar dağınıktır.** Tek bir işlem birbirinden uzak sayfalara dokunabilir: bir ödünç
satırı, üyenin bulunduğu sayfa, üç ayrı dizinin farklı yaprakları. Bunları yazmak dosyanın
dört bir yanına küçük yazmalar yapmak demektir. Sıralı bir yazmanın maliyeti ise erişim
başına değil, bayt başınadır.

**Yazma bölünebilir.** Dört kilobaytlık bir sayfanın diske yazılması, donanım düzeyinde tek
bir bölünmez işlem olmayabilir. Yazmanın ortasında elektrik kesilirse sayfanın bir kısmı yeni,
bir kısmı eski olur. Bu, kaybolmuş bir güncellemeden kötüdür: bozulmuş bir sayfadır.

**Aynı sayfa tekrar tekrar yazılır.** Sık güncellenen bir sayfa her kesinleştirmede yeniden
yazılır. Oysa o sayfanın son hâlinin bir kez yazılması yeterlidir.

#### Yazma Öncesi Kural

Çözüm, kalıcılığı veri sayfalarından ayırmaktır. Motor, değişikliği önce **yazma öncesi
günlüğe (write-ahead log)** yazar: dosyanın sonuna eklenen, ne değiştiğini anlatan sıralı
kayıtlar. Kural tek cümledir:

> Bir veri sayfası diske yazılmadan önce, o sayfadaki değişikliği anlatan günlük kaydı
> diske yazılmış olmalıdır.

Kesinleştirme, günlük kayıtlarının kalıcı olmasıyla tamamlanır. Veri sayfaları bellekte
kalabilir; onları yazmak sonraya bırakılır.

Bu düzenlemenin üç sorunu da çözmesi doğrudandır. Yazma sıralıdır, çünkü günlük yalnız
sonuna eklenir. Bölünmüş yazma tehlikesi ortadan kalkar, çünkü eksik yazılmış bir günlük
kaydı kurtarma sırasında tanınıp yok sayılır — veri sayfası ise henüz hiç dokunulmamıştır.
Sık güncellenen sayfa da tek seferde yazılır, çünkü sayfanın diske inmesi kesinleştirmeye
bağlı değildir.

Kurtarma buradan çıkar. Çökmeden sonra açılışta motor günlüğü okur ve veri dosyasına
yansımamış kesinleştirilmiş değişiklikleri uygular. Kesinleştirilmemiş olanlar uygulanmaz.
Sonuç, çökme anındaki kesinleştirilmiş duruma tam olarak dönmektir.

İlişkisel Kuram kursunda ACID'in kalıcılık maddesi anlatılırken bu yapıdan **yazma öncesi
günlük** adıyla söz edilmişti; aynı yapıdır, bu kurs boyunca katalogdaki adlandırma
kullanılacaktır.

#### Günlük Dosyasının Ortaya Çıkışı

İddia ölçülebilir. Aşağıdaki koşum bir veritabanı kurar, günlük kipini açar, sonra beş bin
satır ekleyip iki dosyanın boyutuna bakar. Denetim noktası devre dışı bırakılmıştır; nedeni
sonraki dersin konusudur.

```sh
rm -f g.db g.db-wal g.db-shm
sqlite3 g.db <<'SQL'
CREATE TABLE odunc(odunc_id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT,
                   alis_tarihi TEXT, iade_tarihi TEXT);
INSERT INTO odunc SELECT n, 1+(n*7)%200000, 1+(n*13)%120000, '2024-01-01', NULL
  FROM (WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n<50000) SELECT n FROM s);
PRAGMA journal_mode = WAL;
PRAGMA wal_autocheckpoint = 0;
UPDATE odunc SET iade_tarihi = '2024-02-01' WHERE odunc_id = 1;
.shell echo "--- baslangic ---"
.shell echo "veri dosyasi: $(wc -c < g.db)   gunluk: $(wc -c < g.db-wal)"
INSERT INTO odunc SELECT 100000+n, 1+(n*7)%200000, 1+(n*13)%120000, '2024-03-01', NULL
  FROM (WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n<5000) SELECT n FROM s);
.shell echo "--- 5000 satirlik islem kesinlestikten sonra ---"
.shell echo "veri dosyasi: $(wc -c < g.db)   gunluk: $(wc -c < g.db-wal)"
SELECT count(*) AS toplam_satir FROM odunc;
SQL
```

```
wal
0
--- baslangic ---
veri dosyasi: 1372160   gunluk: 4152
--- 5000 satirlik islem kesinlestikten sonra ---
veri dosyasi: 1372160   gunluk: 152472
55000
```

İlk iki satır iki ayarın döndürdüğü değerlerdir: günlük kipi `wal` oldu, kendiliğinden
denetim noktası sıfırlandı. Asıl sonuç alttadır. Beş bin satırlık ekleme kesinleştikten
sonra veri dosyasının boyutu bir bayt bile değişmedi — 1.372.160 idi, 1.372.160 kaldı.
Günlük dosyası ise 4.152 bayttan 152.472 bayta çıktı. Sorgu ise yeni satırları görüyor:
55.000.

Dosya boyutları ortama ve sayfa boyutuna bağlıdır; değişmeyecek olan, hangi dosyanın
büyüdüğüdür.

#### Kesinleştirilmiş Veri Nerede

Veri dosyasının değişmemesi, kesinleştirilmiş satırların orada olmadığı anlamına gelir.
Bunu doğrudan sınamanın yolu, dosyaları ayırıp ayrı ayrı açmaktır.

```sh
rm -f g.db g.db-wal g.db-shm yalniz_veri.db veri_ve_gunluk.db veri_ve_gunluk.db-wal
sqlite3 g.db <<'SQL'
CREATE TABLE odunc(odunc_id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT,
                   alis_tarihi TEXT, iade_tarihi TEXT);
INSERT INTO odunc SELECT n, 1+(n*7)%200000, 1+(n*13)%120000, '2024-01-01', NULL
  FROM (WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n<50000) SELECT n FROM s);
PRAGMA journal_mode = WAL;
PRAGMA wal_autocheckpoint = 0;
INSERT INTO odunc SELECT 100000+n, 1+(n*7)%200000, 1+(n*13)%120000, '2024-03-01', NULL
  FROM (WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n<5000) SELECT n FROM s);
.shell cp g.db yalniz_veri.db
.shell cp g.db veri_ve_gunluk.db
.shell cp g.db-wal veri_ve_gunluk.db-wal
SQL
echo "--- yalniz veri dosyasi kopyalandi ---"
sqlite3 yalniz_veri.db "SELECT count(*) FROM odunc;"
echo "--- veri dosyasi + gunluk kopyalandi ---"
sqlite3 veri_ve_gunluk.db "SELECT count(*) FROM odunc;"
```

```
wal
0
--- yalniz veri dosyasi kopyalandi ---
50000
--- veri dosyasi + gunluk kopyalandi ---
55000
```

Sonuç tartışmasızdır. Yalnız veri dosyası kopyalandığında elli bin satır görünüyor:
kesinleştirilmiş beş bin satır orada yok. Günlük de kopyalandığında elli beş bin satır
görünüyor. Kesinleştirilmiş veri, kesinleştirmenin hemen ardından günlüğün içindedir.

Bu gözlemin yedekleme tarafında doğrudan bir sonucu vardır ve bu kursun İşletim konusunda
yeniden karşılaşılacaktır: çalışan bir veritabanının dosya düzeyinde yedeği, günlüğü de
kapsamak zorundadır. Yalnız veri dosyasını kopyalayan bir yedek, en son kesinleştirilmiş
işlemleri içermez.

#### Çökmeden Sonra Kurtarma

Kurtarmanın gerçekten çalıştığını görmek için gerçek bir çökme gerekir. Aşağıdaki betik iki
bin satırı tek bir işlemde kesinleştirir, sonra süreci hiçbir kapanış işlemi yapmadan
öldürür: dosyalar kapatılmaz, denetim noktası alınmaz, tampon boşaltılmaz.

```sh
rm -f c.db c.db-wal c.db-shm
cat > cokme.mjs <<'JS'
import { DatabaseSync } from 'node:sqlite';
const db = new DatabaseSync('c.db');
db.exec('PRAGMA journal_mode = WAL');
db.exec('PRAGMA wal_autocheckpoint = 0');
db.exec(`CREATE TABLE odunc(odunc_id INTEGER PRIMARY KEY, uye_id INT, alis_tarihi TEXT)`);
db.exec('BEGIN');
for (let i = 1; i <= 2000; i++) db.exec(`INSERT INTO odunc VALUES (${i}, ${1 + (i * 13) % 500}, '2024-03-01')`);
db.exec('COMMIT');
console.log('kesinleştirildi; işlem içindeki satır sayısı:',
            db.prepare('SELECT count(*) AS c FROM odunc').get().c);
process.kill(process.pid, 'SIGKILL');   // temizlik yapmadan, kapanış olmadan sonlan
JS
node cokme.mjs
echo "süreç çıkış kodu: $?"
echo "--- çökme anındaki dosyalar ---"
echo "veri dosyası: $(wc -c < c.db)   günlük: $(wc -c < c.db-wal)"
echo "--- veritabanı yeniden açılıyor ---"
sqlite3 c.db "SELECT count(*) FROM odunc;"
```

```
kesinleştirildi; işlem içindeki satır sayısı: 2000
süreç çıkış kodu: 137
--- çökme anındaki dosyalar ---
veri dosyası: 4096   günlük: 61832
--- veritabanı yeniden açılıyor ---
2000
```

Çıkış kodu 137, sürecin öldürülerek sonlandığını gösterir. Çökme anında veri dosyası tek
bir sayfa büyüklüğünde: ne tablo tanımı ne de satırlar oraya yazılmış. Her şey 61.832
baytlık günlüktedir. Veritabanı yeniden açıldığında iki bin satırın tamamı yerindedir.

Kalıcılığın nasıl sağlandığı budur. Kesinleştirme, veriyi son duracağı yere yazmak değil,
neyin değiştiğini geri döndürülemez biçimde kaydetmektir. Verinin yerine yerleşmesi
sonraki dersin konusudur.

#### Günlük Kipi ve Eşitleme Ayarı

Günlük tutmanın tek bir biçimi yoktur ve seçim ölçülebilir bir fark yaratır. İki eksen
vardır.

Birinci eksen **günlük kipidir**. Yazma öncesi günlükte değişiklikler ayrı bir dosyaya
eklenir, veri dosyası olduğu gibi kalır. Bunun alternatifi **geri alma günlüğüdür (rollback
journal)**: değişiklikten önceki sayfanın bir kopyası ayrı dosyaya yazılır, sayfanın yeni
hâli doğrudan veri dosyasına işlenir; işlem geri alınırsa eski sayfalar geri yazılır.
Kipin adı ve seçenekleri motora özgüdür.

İkinci eksen **eşitleme düzeyidir**: kesinleştirmede motorun işletim sisteminden yazmanın
gerçekten diske indiğini beklemesi mi, yoksa yazmayı teslim edip devam etmesi mi.

{/* run: skip */}
```sh
cat > kip.mjs <<'JS'
import { DatabaseSync } from 'node:sqlite';
import { rmSync } from 'node:fs';

function olc(kip, esitleme) {
  for (const ek of ['', '-wal', '-shm', '-journal']) rmSync('k.db' + ek, { force: true });
  const db = new DatabaseSync('k.db');
  db.exec(`PRAGMA journal_mode = ${kip}`);
  db.exec(`PRAGMA synchronous = ${esitleme}`);
  db.exec('CREATE TABLE odunc(odunc_id INTEGER PRIMARY KEY, uye_id INT, alis_tarihi TEXT)');
  const ekle = db.prepare('INSERT INTO odunc VALUES (?, ?, ?)');
  const bas = process.hrtime.bigint();
  for (let i = 1; i <= 2000; i++) {              // her satır ayrı bir işlem
    db.exec('BEGIN');
    ekle.run(i, 1 + (i * 13) % 500, '2024-03-01');
    db.exec('COMMIT');
  }
  const sure = Number(process.hrtime.bigint() - bas) / 1e6;
  db.close();
  return sure;
}

const olculer = [['delete', 'FULL'], ['wal', 'FULL'], ['wal', 'NORMAL']];
const sonuc = olculer.map(([k, e]) => [k, e, olc(k, e)]);
const taban = sonuc[0][2];
console.log('günlük kipi   eşitleme   2000 işlem (ms)   orana göre');
for (const [k, e, s] of sonuc)
  console.log(k.padEnd(13), e.padEnd(10), s.toFixed(0).padStart(14), (taban / s).toFixed(1).padStart(12) + '×');
JS
node kip.mjs
```

Çıktı makineden makineye değişir; örnek bir koşum:

```
günlük kipi   eşitleme   2000 işlem (ms)   orana göre
delete        FULL                   49          1.0×
wal           FULL                   11          4.5×
wal           NORMAL                 10          5.0×
```

Süreler ortama bağlıdır; okunacak olan oranlardır. Yazma öncesi günlük, geri alma günlüğüne
göre bu koşumda dört buçuk kat hızlı kesinleştirdi. Nedeni yazma biçimidir: geri alma
günlüğünde her işlem hem günlük dosyasını hem veri dosyasını değiştirir ve sonunda günlük
dosyasını siler; yazma öncesi günlükte tek bir dosyanın sonuna eklenir.

Bu koşum bellekte duran bir dosya sisteminde alındı; orada eşitleme diske inmeyi beklemediği için,
gerçek bir diskte gevşetmenin kazancı tablodakinden büyük olabilir.

Üçüncü satır ise ödünleşimin kendisidir. Eşitleme gevşetildiğinde ölçüm biraz daha
hızlanır, çünkü kesinleştirme artık yazmanın diske indiğini beklemez; kazanç, diskin
eşitleme maliyetine göre çok değişir. Bunun bedeli
kalıcılıktır: ani bir güç kesintisinde en son kesinleştirilmiş birkaç işlem kaybolabilir.
Veritabanı bozulmaz — günlük yapısı bunu engeller — ama "kesinleştirildi" denen bir işlem
geri gelmeyebilir. Bu ayar, ACID'in kalıcılık maddesinden bilerek vazgeçmek anlamına gelir
ve yalnız kaybı göze alınabilen veriler için savunulabilir.

#### Özet

- Kesinleştirmede veri sayfalarını diske yazmak dağınık yazma, bölünmüş yazma ve aynı
  sayfanın tekrar tekrar yazılması sorunlarını doğurur.
- Yazma öncesi kural, bir veri sayfası diske inmeden önce o değişikliği anlatan günlük
  kaydının kalıcı olmasını ister; kesinleştirme günlüğün kalıcı olmasıyla tamamlanır.
- Ölçümde beş bin satırlık bir ekleme kesinleştiğinde veri dosyası hiç değişmedi, günlük
  dosyası 4.152 bayttan 152.472 bayta çıktı.
- Yalnız veri dosyası kopyalandığında 50.000, günlükle birlikte kopyalandığında 55.000
  satır görüldü: kesinleştirilmiş veri günlüğün içindedir.
- Öldürülen bir süreçten sonra veri dosyası tek sayfalık kalmasına rağmen iki bin satırın
  tamamı günlükten kurtarıldı.
- Günlük kipi ve eşitleme düzeyi ölçülebilir fark yaratır; eşitlemeyi gevşetmek örnek koşumda
  biraz daha hızlandırdı ve karşılığında kalıcılıktan vazgeçti.

#### Sonraki Adım

Bu dersteki koşumlarda kendiliğinden denetim noktası kapatılmıştı ve günlük dosyası hiç
küçülmeden büyüdü. Bu sürdürülebilir değildir: sınırsız büyüyen bir günlük diski doldurur
ve kurtarma süresini uzatır, çünkü açılışta okunacak kayıt sayısı artar. Günlükteki
değişikliklerin veri dosyasına işlenip günlüğün kısalması gerekir. Sonraki ders bu işlemi
ele alır: denetim noktası ne yapar, ne sıklıkta yapılmalıdır ve sıklık ile kurtarma süresi
arasındaki ödünleşim nasıl kurulur.

### Denetim Noktaları

Source: https://academia.sh/tr/kurslar/veritabani-yonetimi/denetim-noktalari

Günlükteki değişikliklerin veri dosyasına işlenmesi, denetim noktasının veri dosyasını ve günlük dosyasını nasıl değiştirdiği, günlüğün yeniden kullanılması ile kısaltılması arasındaki fark, denetim noktası sıklığının yazma maliyeti ile kurtarma işi arasındaki ödünleşimi ve okuyucuların denetim noktasını nasıl geciktirdiği.

Önceki derste günlük dosyası büyüdü ve hiç küçülmedi, çünkü kendiliğinden denetim noktası
kapatılmıştı. Bu durum uzun süre sürdürülemez. Sınırsız büyüyen bir günlük diski doldurur;
üstelik açılışta okunması gereken kayıt sayısı arttıkça kurtarma uzar. Veri dosyası ise
günlerce hiç değişmeden durabilir ve içindeki bilgi giderek eskir.

Bu iki dosyanın er geç eşitlenmesi gerekir. **Denetim noktası (checkpoint)**, bu eşitlemenin
adıdır. Dersin soruları şunlardır: eşitleme sırasında hangi dosya nasıl değişir, günlük
niçin bazen kısalıp bazen kısalmaz ve bu işlem ne sıklıkta yapılmalıdır?

#### Denetim Noktası Ne Yapar

Denetim noktası, günlükte biriken değişiklikleri veri dosyasındaki sayfalara işler. Adım
sırası kısadır: günlükteki kayıtlar okunur, karşılık gelen sayfaların son hâli veri
dosyasına yazılır, yazmanın kalıcı olduğu doğrulanır ve ancak ondan sonra o günlük alanı
serbest sayılır.

Sıralama zorunludur. Günlük alanı, veri dosyası yazması kalıcı olmadan serbest bırakılırsa
ortada iki kopya da bulunmayan bir değişiklik kalır. Yazma öncesi kuralın ikinci yüzü budur:
günlük kaydı veri sayfasından **önce** kalıcı olur, günlük alanı ise veri sayfasından
**sonra** serbest kalır.

Bir sınırlama daha vardır. Denetim noktası, o anda okuma yapan işlemlerin gördüğü sayfaların
üzerine yazamaz; bir sonraki ders bunun nedenini kuracaktır. Bu yüzden uzun süren bir okuma
denetim noktasını geciktirebilir ve günlük bu sırada büyümeye devam eder. Yönetimde
karşılaşılan "günlük neden şişti" sorusunun sık rastlanan cevaplarından biri, unutulmuş bir
uzun okuma işlemidir.

#### Ölçülen Denetim Noktası

Aşağıdaki koşum, denetim noktasından önceki ve sonraki durumu üç ölçüyle karşılaştırır: veri
dosyasının boyutu, günlük dosyasının boyutu ve veritabanının mantıksal sayfa sayısı.

```sh
rm -f n.db n.db-wal n.db-shm
sqlite3 n.db <<'SQL'
CREATE TABLE odunc(odunc_id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT,
                   alis_tarihi TEXT, iade_tarihi TEXT);
INSERT INTO odunc SELECT n, 1+(n*7)%200000, 1+(n*13)%120000, '2024-01-01', NULL
  FROM (WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n<50000) SELECT n FROM s);
PRAGMA journal_mode = WAL;
PRAGMA wal_autocheckpoint = 0;
INSERT INTO odunc SELECT 100000+n, 1+(n*7)%200000, 1+(n*13)%120000, '2024-03-01', NULL
  FROM (WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n<20000) SELECT n FROM s);
.shell echo "--- denetim noktasi oncesi ---"
.shell echo "veri: $(wc -c < n.db)  gunluk: $(wc -c < n.db-wal)"
SELECT 'veri dosyasindaki sayfa sayisi', page_count FROM pragma_page_count;
PRAGMA wal_checkpoint(PASSIVE);
.shell echo "--- PASSIVE denetim noktasi sonrasi ---"
.shell echo "veri: $(wc -c < n.db)  gunluk: $(wc -c < n.db-wal)"
SELECT 'veri dosyasindaki sayfa sayisi', page_count FROM pragma_page_count;
PRAGMA wal_checkpoint(TRUNCATE);
.shell echo "--- TRUNCATE denetim noktasi sonrasi ---"
.shell echo "veri: $(wc -c < n.db)  gunluk: $(wc -c < n.db-wal)"
SQL
```

```
wal
0
--- denetim noktasi oncesi ---
veri: 1372160  gunluk: 576832
veri dosyasindaki sayfa sayisi|472
0|140|140
--- PASSIVE denetim noktasi sonrasi ---
veri: 1933312  gunluk: 576832
veri dosyasindaki sayfa sayisi|472
0|0|0
--- TRUNCATE denetim noktasi sonrasi ---
veri: 1933312  gunluk: 0
```

Denetim noktasından önce veri dosyası 1.372.160 bayt, yani 335 sayfa. Ama sorgu sayfa
sayısını 472 olarak veriyor. Veri dosyasında hiç bulunmayan 137 sayfa günlüktedir. Bu,
ilk dersteki mimarinin somut hâlidir: veritabanının **mantıksal** durumu, veri dosyası ile
günlüğün birleşiminden okunur; veri dosyası tek başına eski bir andır.

`PRAGMA wal_checkpoint` çıktısı üç sayı verir: engellenme durumu, günlükteki çerçeve sayısı
ve işlenen çerçeve sayısı. İlk çağrıda `0|140|140` — engellenme yok, günlükte 140 çerçeve
var, hepsi işlendi. Çerçeve sayısı dosya boyutuyla doğrulanabilir: her çerçeve bir sayfa
artı 24 baytlık başlıktır ve dosyanın başında 32 baytlık bir bölüm bulunur;
32 + 140 × (4096 + 24) = 576.832, ölçülen boyutun aynısı. Çerçeve sayısının 137'den fazla
olması, bazı sayfaların günlükte birden çok kez yer almasındandır: eklemeler sırasında
ağacın üst düzeyleri de değişir ve her değişiklik ayrı bir çerçeve yazar.

Denetim noktasından sonra veri dosyası 1.933.312 bayta çıktı: tam 472 sayfa. Mantıksal
sayfa sayısı değişmedi, çünkü veri değişmedi — yalnız yeri değişti. İkinci çağrı `0|0|0`
döndürüyor: işlenecek bir şey kalmamış.

#### Yeniden Kullanma ve Kısaltma

İlk denetim noktasından sonra günlük dosyasının boyutu 576.832 baytta kaldı; içi işlenmiş
olduğu hâlde dosya küçülmedi. Bu bir kusur değil, kasıtlı bir davranıştır: işlenmiş bir
günlük dosyası **baştan yeniden yazılabilir**. Dosyayı kısaltıp sonra yeniden büyütmek,
dosya sistemine gereksiz iş çıkarır; aynı alanın üzerine yazmak ucuzdur.

İkinci çağrıdaki `TRUNCATE` biçimi dosyayı gerçekten sıfırlar. Bunun yeri, günlüğün bir
kereye mahsus şiştiği ve alanın geri istendiği durumlardır. Sürekli çalışan bir sistemde
günlük dosyasının belli bir boyutta durması beklenen davranıştır; sürekli sıfırlanması
değil.

Buradaki biçim adları (`PASSIVE`, `TRUNCATE`) bu araca özgüdür. Ayrımın kendisi — işlenmiş
günlük alanının yeniden kullanılması ile dosyanın kısaltılması — motordan bağımsızdır.

#### Sıklık Ödünleşimi

Denetim noktası ne sıklıkta alınmalıdır? Soru bir ayar değeri sorusu gibi görünür; aslında
iki maliyet arasında bir seçimdir. Sık denetim noktası, aynı sayfayı defalarca veri
dosyasına yazar ve yazma yolunu ağırlaştırır. Seyrek denetim noktası, günlüğün büyümesine
ve kurtarmanın uzamasına yol açar.

Aşağıdaki koşum eşiği değiştirerek beş bin küçük işlemi ölçer ve iki sonucu yan yana koyar:
geçen süre ve işlem sonunda günlükte kalan çerçeve sayısı. İkinci sayı, bir çökme olsaydı
kurtarmanın işlemek zorunda kalacağı iş miktarıdır.

```sh
cat > sikilik.mjs <<'JS'
import { DatabaseSync } from 'node:sqlite';
import { rmSync, statSync } from 'node:fs';

const CERCEVE = 4096 + 24;                       // sayfa + cerceve basligi
function olc(esik) {
  for (const ek of ['', '-wal', '-shm']) rmSync('s.db' + ek, { force: true });
  const db = new DatabaseSync('s.db');
  db.exec('PRAGMA journal_mode = WAL');
  db.exec('PRAGMA synchronous = NORMAL');
  db.exec('CREATE TABLE odunc(odunc_id INTEGER PRIMARY KEY, uye_id INT, iade_tarihi TEXT)');
  db.exec(`INSERT INTO odunc SELECT n, 1+(n*13)%500, NULL
           FROM (WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n<20000) SELECT n FROM s)`);
  db.exec('PRAGMA wal_checkpoint(TRUNCATE)');
  db.exec(`PRAGMA wal_autocheckpoint = ${esik}`);
  const guncelle = db.prepare('UPDATE odunc SET iade_tarihi = ? WHERE odunc_id = ?');
  const bas = process.hrtime.bigint();
  for (let i = 1; i <= 5000; i++) { db.exec('BEGIN'); guncelle.run('2024-04-01', 1 + (i * 37) % 20000); db.exec('COMMIT'); }
  const sure = Number(process.hrtime.bigint() - bas) / 1e6;
  const cerceve = Math.round((statSync('s.db-wal').size - 32) / CERCEVE);
  db.close();
  return [sure, cerceve];
}
console.log('eşik (sayfa)   5000 işlem (ms)   günlükte kalan çerçeve');
for (const e of [1, 10, 100, 1000, 0]) {
  const [s, c] = olc(e);
  console.log(String(e === 0 ? 'kapalı' : e).padStart(12), s.toFixed(0).padStart(16), String(c).padStart(22));
}
JS
node sikilik.mjs
```

{/* mask: (?<=\S)[ ]+[0-9]+(?=[ ]+[0-9]+$) */}
```
eşik (sayfa)   5000 işlem (ms)   günlükte kalan çerçeve
           1               56                      6
          10               28                     15
         100               29                    103
        1000               28                   1000
      kapalı               31                   5719
```

Süreler ortama bağlıdır; ilişkiye oranlar üzerinden bakılır. Her sayfada bir denetim
noktası alındığında beş bin işlem, bin sayfalık eşiğe göre bu koşumda yaklaşık iki kat uzun sürdü.
Karşılığında günlükte yalnız altı çerçeve kaldı: bir çökme neredeyse hiç iş bırakmazdı.
Eşik büyüdükçe süre düşüyor ve on sayfadan sonra yassılaşıyor; günlükte biriken
çerçeve sayısı ise doğrusal olarak artıyor.

Bu koşum bellekte duran bir dosya sisteminde alındı; orada denetim noktasının diske inmeyi
beklemesi gerekmediği için, gerçek bir diskte süre farkı tablodakinden büyük olabilir.

Eğrinin biçimi seçimi kolaylaştırır. Çok sık denetim noktası pahalıdır ve karşılığında
kazandırdığı kurtarma süresi zaten küçüktür. Çok seyrek denetim noktası ise süreyi daha
fazla kısaltmaz — son satırda "kapalı" durumun süresi bin sayfalık eşikten daha iyi değil —
ama günlüğü ve kurtarma işini sınırsız büyütür. Verimli aralık ortadadır ve bu, bir başarım
ayarından çok bir kurtarma hedefi kararıdır: "çökmeden sonra en fazla ne kadar sürede
açılmış olmalıyız" sorusunun cevabı, günlüğün ne kadar birikmesine izin verileceğini
belirler.

#### Kurtarmanın İşi

Denetim noktasının kurtarma tarafındaki etkisi doğrudandır. Açılışta motor, son denetim
noktasından sonraki günlük kayıtlarını işlemek zorundadır; denetim noktasından önceki
kayıtlar zaten veri dosyasındadır. Kurtarmanın yapacağı iş, tablonun büyüklüğüyle değil,
denetim noktasından bu yana biriken kayıt sayısıyla orantılıdır.

Burada bir ölçüm uyarısı gerekir. Bu ortamda kurtarma, günlüğü tarayıp iç dizinini yeniden
kurmaktan ibarettir ve dosya işletim sisteminin önbelleğinde olduğu için süresi bu
büyüklüklerde ölçülebilir bir değer vermez. Bu yüzden yukarıdaki tabloda süre değil, işin
kendisi — çerçeve sayısı — ölçü olarak kullanıldı. Sayfaları veri dosyasına ayrıca yazması
gereken motorlarda aynı sayı, kurtarma süresinin doğrudan belirleyicisidir.

Sıklık kararının ikinci yüzü de burada görünür: denetim noktası, biriken sayfaları topluca
yazdığı için o an yazma yükünde bir sıçrama üretir. Sürekli çalışan bir sistemde bu
sıçrama, gecikme ölçümlerinde düzenli tepeler olarak görünür. Motorlar bunu, denetim
noktası işini aralığa yayarak yumuşatır; ayarın adı motora göre değişir, amacı aynıdır.

#### Özet

- Denetim noktası, günlükte biriken değişiklikleri veri dosyasındaki sayfalara işler;
  günlük alanı ancak bu yazma kalıcı olduktan sonra serbest kalır.
- Veritabanının mantıksal durumu veri dosyası ile günlüğün birleşimidir; ölçümde veri
  dosyası 335 sayfa iken mantıksal sayfa sayısı 472 çıktı, aradaki fark günlükteydi.
- İşlenmiş bir günlük dosyası genellikle kısaltılmaz, baştan yeniden yazılır; kısaltma
  ayrı ve isteğe bağlı bir adımdır.
- Denetim noktası sıklığı iki maliyet arasında seçimdir: her sayfada denetim noktası almak
  örnek koşumda yaklaşık iki kat yavaşlattı, hiç almamak günlükte 5.719 çerçeve biriktirdi.
- Kurtarmanın işi tablo büyüklüğüyle değil, son denetim noktasından sonra biriken kayıt
  sayısıyla orantılıdır.

#### Sonraki Adım

Bu derste bir sınırlama geçildi: denetim noktası, o anda okuma yapan işlemlerin gördüğü
sayfaların üzerine yazamaz. Bu cümlenin arkasında, motorun eşzamanlılığı sağlama biçimi
duruyor. İleri SQL kursunda bir işlem içindeki okumanın, dışarıdaki kesinleştirmelerden
etkilenmediği gözlenmişti; bunun nasıl mümkün olduğu sorulmamıştı. Sonraki ders o
mekanizmayı kurar: satırın birden çok sürümünün aynı anda saklanması, hangi işlemin hangi
sürümü gördüğünü belirleyen kural ve okuyucu ile yazıcının birbirini engellememesi.

### Çok Sürümlü Eşzamanlılık Denetimi

Source: https://academia.sh/tr/kurslar/veritabani-yonetimi/cok-surumlu-eszamanlilik-denetimi

Kilit tabanlı ve sürüm tabanlı eşzamanlılık denetiminin farkı, aynı okumanın iki günlük kipinde yazarı engelleyip engellememesinin ölçülmesi, sürüm zinciri ile görünürlük kuralının model üzerinden kurulması, eski sürümlerin ne zaman kaldırılabildiği ve çok sürümlülüğün ortadan kaldırmadığı çakışmalar.

Önceki dersin sonunda bir sınırlama kaldı: denetim noktası, o anda okuma yapan işlemlerin
gördüğü sayfaların üzerine yazamaz. İleri SQL kursunda da benzer bir gözlem yapılmıştı — bir
işlem içindeki okuma, dışarıda kesinleştirilen değişikliklerden etkilenmiyordu. İki gözlem
de aynı mekanizmaya işaret eder ve o mekanizma henüz kurulmadı.

Soru şudur: bir satır güncellenirken aynı satırı okumakta olan bir işlem varsa, o okuma neyi
görür? İki tutarlı cevap vardır. Ya okuyucu yazarı bekletir, ya da satırın eski hâli bir
yerde saklanır. Bu ders ikinci yolu ele alır ve ilk yoldan farkını ölçer.

#### İki Yaklaşım

**Kilit tabanlı denetim**, tutarlılığı beklemeyle sağlar. Bir satırı okuyan işlem o satır
üzerinde paylaşımlı bir kilit tutar; değiştirmek isteyen işlem dışlayıcı kilit ister ve
okuma bitene kadar bekler. Sonuç doğrudur ama okuma ile yazma birbirini sıraya sokar.

**Çok sürümlü eşzamanlılık denetimi (multiversion concurrency control)**, tutarlılığı
saklamayla sağlar. Bir satır güncellendiğinde eski hâli silinmez; yeni bir sürüm eklenir ve
her sürüm hangi işlem tarafından oluşturulup hangi işlem tarafından geçersiz kılındığını
taşır. Okuyan işlem, kendi başlangıç anına uygun sürümü seçer. Yazar beklemez, okuyucu
beklemez; iki taraf farklı sürümlere bakar.

İleri SQL kursunda tanıtılan **anlık görüntü (snapshot)** kavramının karşılığı budur. Anlık
görüntü, kopyalanmış bir veritabanı değildir; hangi sürümlerin görüleceğini belirleyen bir
ölçüttür.

#### Ölçülen Fark

Fark gözlemlenebilir. Aşağıdaki koşum aynı senaryoyu iki günlük kipiyle çalıştırır: bir
okuyucu işlem açar ve okur, sonra ayrı bir bağlantıdaki yazar aynı tabloyu güncellemeye
çalışır.

```sh
rm -f e.db e.db-wal e.db-shm e.db-journal
cat > engel.mjs <<'JS'
import { DatabaseSync } from 'node:sqlite';
import { rmSync } from 'node:fs';

function dene(kip) {
  for (const ek of ['', '-wal', '-shm', '-journal']) rmSync('e.db' + ek, { force: true });
  const kur = new DatabaseSync('e.db');
  kur.exec(`PRAGMA journal_mode = ${kip}`);
  kur.exec('CREATE TABLE odunc(odunc_id INTEGER PRIMARY KEY, uye_id INT, iade_tarihi TEXT)');
  kur.exec("INSERT INTO odunc VALUES (1,4,NULL),(2,4,NULL),(3,7,'2024-03-09')");
  kur.close();

  const okuyucu = new DatabaseSync('e.db');
  const yazar = new DatabaseSync('e.db');
  const acik = (b) => b.prepare('SELECT count(*) AS c FROM odunc WHERE iade_tarihi IS NULL').get().c;

  okuyucu.exec('BEGIN');                       // okuyucu islemi acik kaliyor
  const once = acik(okuyucu);
  let yazma;
  try { yazar.exec("UPDATE odunc SET iade_tarihi = '2024-03-20' WHERE odunc_id = 1"); yazma = 'başarılı'; }
  catch (h) { yazma = 'engellendi: ' + h.message.split('\n')[0]; }
  const sonra = acik(okuyucu);
  okuyucu.exec('COMMIT');
  const bitince = acik(okuyucu);
  okuyucu.close(); yazar.close();
  console.log(`[${kip}] okuyucu işlem içinde: ${once} → ${sonra} | yazar: ${yazma} | işlem bitince: ${bitince}`);
}
dene('delete');
dene('wal');
JS
node engel.mjs
```

```
[delete] okuyucu işlem içinde: 2 → 2 | yazar: engellendi: database is locked | işlem bitince: 2
[wal] okuyucu işlem içinde: 2 → 2 | yazar: başarılı | işlem bitince: 1
```

Okuyucunun gördüğü sayı iki durumda da işlem boyunca değişmiyor: 2 → 2. Yalıtım her iki
düzende de sağlanmış. Değişen, bunun nasıl sağlandığıdır.

Geri alma günlüğü kipinde yazar hiç ilerleyemedi: veritabanı kilitli. Okuma bitmeden yazma
yapılamaz. Yazma öncesi günlük kipinde ise yazar işini tamamladı ve kesinleştirdi; okuyucu
buna rağmen eski değeri görmeye devam etti ve ancak kendi işlemi bittikten sonra yeni değeri
gördü (1). İki taraf da beklemedi.

Buradaki gözlem, çok sürümlülüğün **gözlenen özelliğini** doğrular: okuyucu ile yazıcı
birbirini engellemez. Bu özelliğin nasıl gerçekleştirildiği motora göre değişir; kullanılan
araçta yol, satır başına sürüm zincirleri değil, günlük üzerinden tanımlanan anlık
görüntülerdir. Mekanizmanın kendisi bir sonraki bölümde model üzerinden kurulacak.

#### Sürüm Zinciri ve Görünürlük Kuralı

Satır başına sürüm tutan bir motorda her sürüm iki ek alan taşır: onu **oluşturan** işlemin
numarası ve onu **silen** işlemin numarası. Silme alanı boşsa sürüm hâlâ geçerlidir.
Güncelleme, tek bir işlemde iki şey yapar: mevcut sürümün silen alanını doldurur ve yeni bir
sürüm ekler. Silme ise yalnız birincisini yapar.

Görünürlük kuralı tek cümledir: bir sürüm, anlık görüntüde **oluşturan işlemi
kesinleşmişse** ve **silen işlemi kesinleşmemişse** görünür. Aşağıdaki model kuralı işletir.
Gerçek motorların kuralı daha ayrıntılıdır — kendi işlemin görmediği değişiklikler, iptal
edilmiş işlemler ve alt işlemler ayrıca ele alınır — ama çekirdek aynıdır.

```sh
cat > surum.mjs <<'JS'
// Model: satir surumleri ve gorunurluk kurali. Gercek motorun kendisi degil.
class Depo {
  constructor() { this.surumler = []; this.kesinlesen = new Set(); this.sonrakiIslem = 10; }
  islemBaslat() { const t = this.sonrakiIslem; this.sonrakiIslem += 10; return t; }
  kesinlestir(t) { this.kesinlesen.add(t); }
  anlikGoruntu() { return new Set(this.kesinlesen); }        // o andaki kesinlesmis islemler
  ekle(t, satir_id, deger) { this.surumler.push({ satir_id, deger, olusturan: t, silen: null }); }
  guncelle(t, satir_id, deger) {
    const eski = this.surumler.find((s) => s.satir_id === satir_id && s.silen === null);
    if (eski) eski.silen = t;
    this.surumler.push({ satir_id, deger, olusturan: t, silen: null });
  }
  gorunur(s, anlik) { return anlik.has(s.olusturan) && (s.silen === null || !anlik.has(s.silen)); }
  oku(satir_id, anlik) {
    const s = this.surumler.find((v) => v.satir_id === satir_id && this.gorunur(v, anlik));
    return s ? s.deger : '(yok)';
  }
  temizlenebilir(anliklar) {                                  // hicbir anlik goruntunun gormedigi surumler
    return this.surumler.filter((s) => anliklar.every((a) => !this.gorunur(s, a)));
  }
}

const d = new Depo();
const t1 = d.islemBaslat();                       // odunc kaydini olusturan islem
d.ekle(t1, 501, 'iade_tarihi = NULL'); d.kesinlestir(t1);

const okuyucuAnlik = d.anlikGoruntu();            // uzun okuma buradan basliyor
const t2 = d.islemBaslat();
d.guncelle(t2, 501, "iade_tarihi = '2024-03-20'"); d.kesinlestir(t2);
const yeniAnlik = d.anlikGoruntu();
const t3 = d.islemBaslat();
d.guncelle(t3, 501, "iade_tarihi = '2024-03-21'"); d.kesinlestir(t3);
const enYeniAnlik = d.anlikGoruntu();

console.log('sürüm zinciri (satır 501):');
for (const s of d.surumler)
  console.log(`  oluşturan=${s.olusturan}  silen=${s.silen ?? '—'}  ${s.deger}`);
console.log('okuyucunun anlık görüntüsü  →', d.oku(501, okuyucuAnlik));
console.log('ikinci anlık görüntü        →', d.oku(501, yeniAnlik));
console.log('en yeni anlık görüntü       →', d.oku(501, enYeniAnlik));
console.log('okuyucu açıkken temizlenebilir sürüm sayısı :',
            d.temizlenebilir([okuyucuAnlik, enYeniAnlik]).length);
console.log('okuyucu kapandıktan sonra                   :',
            d.temizlenebilir([enYeniAnlik]).length);
JS
node surum.mjs
```

```
sürüm zinciri (satır 501):
  oluşturan=10  silen=20  iade_tarihi = NULL
  oluşturan=20  silen=30  iade_tarihi = '2024-03-20'
  oluşturan=30  silen=—  iade_tarihi = '2024-03-21'
okuyucunun anlık görüntüsü  → iade_tarihi = NULL
ikinci anlık görüntü        → iade_tarihi = '2024-03-20'
en yeni anlık görüntü       → iade_tarihi = '2024-03-21'
okuyucu açıkken temizlenebilir sürüm sayısı : 1
okuyucu kapandıktan sonra                   : 2
```

Aynı ödünç kaydının üç sürümü aynı anda depoda duruyor ve üç ayrı anlık görüntü üçünü ayrı
ayrı görüyor. Hiçbir bekleme yok; her okuyucu kendi ölçütüne uyan sürümü buluyor.

Bu düzenin bir sonucu, `UPDATE` deyiminin fiziksel olarak bir güncelleme olmamasıdır. Tek
bir sütunun tek bir baytını değiştiren bir güncelleme bile satırın tamamının yeni bir
kopyasını yazar. İleri SQL kursunda "anahtar sütunu güncelleyen `UPDATE` için de aynı bedel
geçerlidir" denen dizin maliyetinin arkasında da bu vardır: yeni sürüm yeni bir yerdedir ve
o satıra işaret eden dizin girdilerinin de güncellenmesi gerekir.

#### Sürümler Ne Zaman Kalkabilir

Modelin son iki satırı asıl yönetim sorununu gösteriyor. Eski sürümler sonsuza kadar
tutulamaz; artık hiçbir anlık görüntünün görmediği bir sürüm kaldırılabilir. Ölçüt de
buradan çıkar: **en eski açık anlık görüntüden daha eski** olan sürümler temizlenebilir.

Modelde okuyucunun anlık görüntüsü açıkken yalnız bir sürüm temizlenebilir durumda; okuyucu
kapandığında sayı ikiye çıkıyor. Tek bir uzun okuma, üç sürümlük bir zincirin iki sürümünü
yerinde tutmaya yetiyor.

Bu sonucun ölçeği önemlidir. Saatlerce açık kalan bir işlem, o süre boyunca yapılan **bütün**
güncellemelerin eski sürümlerini yaşatır. Tablo satır sayısı değişmese bile diskte kapladığı
yer sürekli büyür. Önceki derste denetim noktasının okuyucular tarafından geciktirilmesi de
aynı kuralın başka bir yüzüdür.

#### Çok Sürümlülüğün Kaldırmadığı Çakışma

Okuyucu ile yazıcının birbirini engellememesi, bütün çakışmaların kalktığı anlamına gelmez.
Aşağıdaki koşum iki noktayı birlikte gösterir: iki yazarın çakışması ve açık bir okumanın
denetim noktasını durdurması.

```sh
rm -f y.db y.db-wal y.db-shm
cat > engel2.mjs <<'JS'
import { DatabaseSync } from 'node:sqlite';
import { rmSync } from 'node:fs';
for (const ek of ['', '-wal', '-shm']) rmSync('y.db' + ek, { force: true });
const kur = new DatabaseSync('y.db');
kur.exec('PRAGMA journal_mode = WAL');
kur.exec('CREATE TABLE odunc(odunc_id INTEGER PRIMARY KEY, uye_id INT, iade_tarihi TEXT)');
kur.exec(`INSERT INTO odunc SELECT n, 1+(n*13)%500, NULL
          FROM (WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n<20000) SELECT n FROM s)`);
kur.exec('PRAGMA wal_checkpoint(TRUNCATE)');
kur.close();

const a = new DatabaseSync('y.db'), b = new DatabaseSync('y.db');
a.exec('BEGIN IMMEDIATE');
a.exec("UPDATE odunc SET iade_tarihi = '2024-04-01' WHERE odunc_id = 1");
try { b.exec('BEGIN IMMEDIATE'); b.exec("UPDATE odunc SET iade_tarihi = '2024-04-02' WHERE odunc_id = 2"); console.log('iki yazar: ikisi de ilerledi'); }
catch (h) { console.log('iki yazar → ikinci yazar:', h.message.split('\n')[0]); }
a.exec('COMMIT');
a.close(); b.close();

// uzun okuma denetim noktasini geciktiriyor mu
const okuyucu = new DatabaseSync('y.db'), yazar = new DatabaseSync('y.db');
yazar.exec('PRAGMA wal_autocheckpoint = 0');
okuyucu.exec('BEGIN');
okuyucu.prepare('SELECT count(*) AS c FROM odunc').get();
for (let i = 1; i <= 3000; i++) yazar.exec(`UPDATE odunc SET iade_tarihi='2024-05-01' WHERE odunc_id=${1+(i*37)%20000}`);
console.log('okuyucu açıkken denetim noktası :', JSON.stringify(yazar.prepare('PRAGMA wal_checkpoint(PASSIVE)').get()));
okuyucu.exec('COMMIT');
console.log('okuyucu bitince denetim noktası :', JSON.stringify(yazar.prepare('PRAGMA wal_checkpoint(PASSIVE)').get()));
okuyucu.close(); yazar.close();
JS
node engel2.mjs
```

```
iki yazar → ikinci yazar: database is locked
okuyucu açıkken denetim noktası : {"busy":0,"log":3532,"checkpointed":0}
okuyucu bitince denetim noktası : {"busy":0,"log":3532,"checkpointed":3532}
```

Birinci satır sınırı gösteriyor: iki işlem aynı anda yazmaya kalktığında ikincisi
ilerleyemiyor. Kullanılan araçta bu sınır veritabanının tamamı için geçerlidir; satır
düzeyinde çakışma çözen motorlarda ise sınır yalnız **aynı satırı** güncelleyen işlemler
arasındadır. Her iki durumda da ilke aynıdır: sürümler okumayı serbestleştirir, aynı veriyi
değiştiren iki yazma yine sıraya girer. İleri SQL kursundaki kilitlenme tartışması bu yüzden
geçerliliğini korur.

İkinci ve üçüncü satırlar önceki dersin açık bıraktığı noktayı kapatıyor. Okuyucunun işlemi
açıkken günlükte 3.532 çerçeve birikmiş ve denetim noktası bunların **sıfırını** işleyebilmiş.
Okuyucu işlemini bitirdikten sonra aynı çağrı 3.532 çerçevenin tamamını işledi. Sebep tektir:
o çerçeveler okuyucunun gördüğü sürümleri taşıyor ve okuyucu bitmeden üzerlerine yazılamaz.

#### Özet

- Kilit tabanlı denetim tutarlılığı beklemeyle, çok sürümlü denetim ise satırın eski hâlini
  saklayarak sağlar.
- Ölçümde geri alma günlüğü kipinde yazar engellendi; yazma öncesi günlük kipinde aynı
  yazar tamamlandı ve okuyucu işlemi boyunca eski değeri görmeye devam etti.
- Her sürüm kendisini oluşturan ve silen işlemleri taşır; bir sürüm, oluşturanı kesinleşmiş
  ve sileni kesinleşmemişse anlık görüntüde görünür.
- Bir sürüm ancak hiçbir açık anlık görüntü tarafından görülmediğinde kaldırılabilir; tek
  bir uzun okuma modelde üç sürümün ikisini yerinde tuttu.
- Çok sürümlülük okuma ile yazmayı ayırır, aynı veriyi değiştiren iki yazmayı ayırmaz;
  açık bir okuma denetim noktasını da durdurur (3.532 çerçevenin sıfırı işlenebildi).

#### Sonraki Adım

Eski sürümlerin kaldırılabilir hâle gelmesi, kendiliğinden kaldırıldıkları anlamına gelmez.
Kaldırma işini yapan ayrı bir süreç vardır ve bu süreç yetişemezse tablo, satır sayısı hiç
artmasa bile büyümeye devam eder: her güncelleme yeni bir sürüm bırakır, eskisi yerinde
durur. Sonraki ders bu birikmeyi ele alır: ölü satırların ölçülmesi, boşluğun geri
kazanılması, geri kazanılan alanın neden her zaman işletim sistemine dönmediği ve temizlik
süreci ile şişme oranı arasındaki denge.

### Ölü Satır Temizliği

Source: https://academia.sh/tr/kurslar/veritabani-yonetimi/olu-satir-temizligi

Silme ve güncellemenin geride bıraktığı ölü sürümler, silinen yerin dosyadan geri gelmemesi, boşluğun neden her zaman yeniden kullanılamadığı, tam yeniden yazmanın maliyeti ve şişme oranının temizlik sıklığı ile açık işlem süresine bağlılığı.

Önceki ders, bir sürümün ancak hiçbir açık anlık görüntü tarafından görülmediğinde
kaldırılabilir hâle geldiğini gösterdi. Kaldırılabilir olmak ile kaldırılmış olmak ayrı
şeylerdir. Sürümleri toplayan ayrı bir süreç vardır ve bu süreç, üretilen ölü sürümlere
yetişemezse tablo, satır sayısı hiç artmasa bile büyümeye devam eder.

Bu dersin soruları ölçülebilir: silinen satırların yeri nereye gider, o yer ne zaman
yeniden kullanılır, kullanılmadığında dosya ne kadar büyür ve bu büyümeyi geri almanın
bedeli nedir?

#### Ölü Satır ve Şişme

**Ölü satır (dead tuple)**, artık hiçbir işlemin görmediği bir satır sürümüdür. İki
kaynaktan doğar: silinen satırın kendisi ve güncellenen satırın eski sürümü. Önceki dersteki
model gereği güncelleme de bir ölü sürüm bırakır, çünkü fiziksel olarak yeni bir sürüm
yazılır.

**Şişme (bloat)**, bir tablonun ya da dizinin kapladığı yerin, içindeki canlı veriyi tutmak
için gereken yerden ne kadar büyük olduğudur. Ölçüsü bir orandır: kaplanan sayfa sayısının,
aynı verinin sıkıştırılmış hâlde kaplayacağı sayfa sayısına bölümü.

Şişme kendi başına bir hata değildir; bir miktarı kaçınılmazdır ve yararlıdır, çünkü boşluk
yeni satırlar için yer bırakır. Sorun, oranın büyümeyi sürdürmesidir. Şişmiş bir tablonun
taranması daha çok sayfa okur, tampon havuzunda daha çok yer tutar ve yedekleri büyütür —
hepsi aynı veri için.

#### Silinen Yer Nereye Gider

Aşağıdaki koşum dört aşamayı aynı üç ölçüyle izler: satır sayısı, sayfa sayısı ve sayfa
içi doluluk oranı. Dosya boyutu her aşamada ayrıca yazdırılır.

```sh
rm -f s.db
cat > durum.sql <<'EOF'
SELECT (SELECT count(*) FROM odunc) AS satir,
       (SELECT page_count FROM pragma_page_count) AS sayfa,
       (SELECT round(100.0*sum(payload)/sum(pgsize),1) FROM dbstat WHERE name='odunc') AS doluluk;
EOF
sqlite3 s.db <<'SQL'
.mode column
.headers on
CREATE TABLE odunc(odunc_id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT,
                   alis_tarihi TEXT, iade_tarihi TEXT);
INSERT INTO odunc SELECT n, 1+(n*7)%200000, 1+(n*13)%120000,
       date('2018-01-01','+'||((n*37)%2437)||' days'), NULL
  FROM (WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n<400000) SELECT n FROM s);
.print '--- baslangic ---'
.read durum.sql
.shell echo "dosya: $(wc -c < s.db)"
DELETE FROM odunc WHERE alis_tarihi < '2020-01-01';
.print '--- 119822 satir silindi ---'
.read durum.sql
.shell echo "dosya: $(wc -c < s.db)"
INSERT INTO odunc SELECT 500000+n, 1+(n*7)%200000, 1+(n*13)%120000, '2025-01-01', NULL
  FROM (WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n<100000) SELECT n FROM s);
.print '--- 100000 yeni satir (artan anahtarla) ---'
.read durum.sql
.shell echo "dosya: $(wc -c < s.db)"
VACUUM;
.print '--- VACUUM sonrasi ---'
.read durum.sql
.shell echo "dosya: $(wc -c < s.db)"
SQL
```

```
--- baslangic ---
satir   sayfa  doluluk
------  -----  -------
400000  2705   77.9   
dosya: 11079680
--- 119822 satir silindi ---
satir   sayfa  doluluk
------  -----  -------
280178  2705   54.6   
dosya: 11079680
--- 100000 yeni satir (artan anahtarla) ---
satir   sayfa  doluluk
------  -----  -------
380178  3381   59.2   
dosya: 13848576
--- VACUUM sonrasi ---
satir   sayfa  doluluk
------  -----  -------
380178  2571   77.8   
dosya: 10530816
```

İkinci aşama ilk sonucu veriyor. Satırların yaklaşık üçte biri silindi; sayfa sayısı bir
tane bile azalmadı, dosya boyutu bir bayt bile küçülmedi. Değişen tek şey doluluk: %77,9'dan
%54,6'ya düştü. Silme, sayfaları serbest bırakmadı — sayfaların **içini** boşalttı.

Bu davranışın nedeni doğrudandır. Silinen satırlar dosyanın her yerine dağılmıştır; bir
sayfanın tamamen boşalması ancak o sayfadaki bütün satırların silinmesiyle olur. Kısmen
boşalmış bir sayfa dosyadan çıkarılamaz, çünkü Fiziksel Depolama Düzeni dersinde ölçülen
ilişki gereği dosya, sayfaların art arda dizilmesinden ibarettir; ortadaki bir sayfayı
çıkarmak sonrasındaki her şeyin yerini değiştirmek demektir.

#### Boşluk Neden Her Zaman Yeniden Kullanılmaz

Üçüncü aşama daha ilginç bir sonuç veriyor. Tabloda 119.822 satırlık boşluk varken 100.000
satır eklendi ve dosya küçülmek bir yana **büyüdü**: 2.705 sayfadan 3.381 sayfaya.

Nedeni anahtar seçimidir. `odunc_id` sürekli artan bir değerdir ve yeni satırlar ağacın en
sağ ucuna eklenir. Silinen satırların bıraktığı boşluklar ise ağacın ortalarındadır — yeni
anahtarlar oraya ait değildir. Motor, sıralı bir yapıda bir anahtarı ait olmadığı yere
koyamaz.

Bu, şema tasarımıyla depolama arasındaki en somut bağlardan biridir. Sürekli artan bir
anahtarla eklenen ve eski kayıtları tarihe göre silinen bir tabloda boşluk kendiliğinden
kapanmaz: yeni veri hep sona yazılır, boşluk hep ortada kalır. Aynı tablo rastgele
anahtarlarla doldurulsaydı boşlukların bir kısmı doğal olarak kullanılırdı — bu kez de
sayfa bölünmeleri artardı. Bölümleme dersi bu soruna yapısal bir cevap getirecek: eski veriyi
silmek yerine, eski bölümün tamamını kaldırmak.

#### Boşluğun Geri Kazanılması

Dördüncü aşama `VACUUM` çalıştırıyor ve tablo 3.381 sayfadan 2.571 sayfaya iniyor; doluluk
%77,8'e, yani ilk günkü değerine (%77,9) neredeyse dönüyor. Şişme oranı 3381 / 2571 ≈ 1,31 idi; yeniden yazma
onu 1,00'e indirdi.

Bu işlemin ne yaptığını bilmek, ne zaman kullanılacağını bilmekten önce gelir: tablo baştan
sona okunur ve yeni bir yere sıkıştırılmış olarak yazılır, sonra eskisinin yeri bırakılır.
Üç bedeli vardır.

**Kilit.** Tam yeniden yazma sırasında tablo (çoğu motorda) değiştirilemez, bazılarında hiç
okunamaz. Çalışan bir sistemde bu, planlanmış bir kesinti demektir.

**Geçici alan.** Yeni kopya yazılırken eski kopya hâlâ yerindedir; işlem sırasında tablonun
boyutu kadar ek yer gerekir. Diski dolmak üzere olan bir sunucuda yer açmak için yeniden
yazma çalıştırmak, elde olmayan bir alanı gerektirdiği için işe yaramaz.

**Süre.** Maliyet tablonun tamamıyla orantılıdır, geri kazanılacak boşlukla değil.

Bu yüzden motorlar iki hafifletilmiş yol sunar: boşluğu kilitlemeden yeniden kullanılabilir
hâle getiren arka plan temizliği ve tabloyu çalışır durumda tutarak yeni bir kopya kuran
yeniden düzenleme. Adları ve sınırları motora özgüdür; tam yeniden yazma, kural olarak son
çaredir.

#### Temizliğin Dengesi

Asıl yönetim sorusu "ne zaman yeniden yazmalı" değil, "şişmenin belli bir oranın üstüne
çıkmasını ne engelliyor" sorusudur. Aşağıdaki model iki değişkeni ayrı ayrı değiştirir:
temizliğin çalışma sıklığı ve açık bir işlemin süresi. Gerçek bir temizlik sürecinin
kendisi değildir; ölçüde tuttuğu şey, ayrılan en yüksek yerin canlı veriye oranıdır.

```sh
cat > temizlik.mjs <<'JS'
// Model: olu surumlerin birikmesi ile temizligin dengesi. Gercek motorun kendisi degil.
// Her adimda GUNCELLEME satir guncellenir; her guncelleme bir olu surum birakir.
// Temizlik ARALIK adimda bir calisir ve yalnizca en eski acik anlik goruntuden
// daha eski olu surumleri toplar. Ayrilan yer isletim sistemine geri verilmez.
function calistir({ aralik, uzunIslem = null, adim = 600 }) {
  const CANLI = 100000, GUNCELLEME = 2000;
  let olu = [];                                   // her ogesi: olusturuldugu adim
  let ayrilan = CANLI, enYuksek = CANLI;
  for (let i = 1; i <= adim; i++) {
    for (let k = 0; k < GUNCELLEME; k++) olu.push(i);
    ayrilan = Math.max(ayrilan, CANLI + olu.length);
    enYuksek = Math.max(enYuksek, ayrilan);
    if (i % aralik === 0) {
      const sinir = (uzunIslem && i >= uzunIslem.bas && i <= uzunIslem.bit) ? uzunIslem.bas : i;
      olu = olu.filter((d) => d >= sinir);         // acik anlik goruntuden eskiler toplanir
    }
  }
  return { kalan: olu.length, sisme: enYuksek / CANLI };
}
console.log('— temizlik sıklığı —');
console.log('aralık (adım)   kalan ölü sürüm   şişme oranı');
for (const a of [1, 5, 25, 100, 600])
  { const s = calistir({ aralik: a });
    console.log(String(a).padStart(13), String(s.kalan).padStart(17), (s.sisme.toFixed(2) + '×').padStart(14)); }
console.log('— aynı temizlik (aralık 5), açık bir uzun işlemle —');
for (const uzunluk of [0, 100, 300, 600]) {
  const s = calistir({ aralik: 5, uzunIslem: uzunluk ? { bas: 1, bit: uzunluk } : null });
  console.log(`uzun işlem ${String(uzunluk).padStart(3)} adım →`, String(s.kalan).padStart(7),
              'ölü sürüm,', (s.sisme.toFixed(2) + '×').padStart(6), 'şişme');
}
JS
node temizlik.mjs
```

```
— temizlik sıklığı —
aralık (adım)   kalan ölü sürüm   şişme oranı
            1              2000          1.04×
            5              2000          1.12×
           25              2000          1.52×
          100              2000          3.02×
          600              2000         13.00×
— aynı temizlik (aralık 5), açık bir uzun işlemle —
uzun işlem   0 adım →    2000 ölü sürüm,  1.12× şişme
uzun işlem 100 adım →    2000 ölü sürüm,  3.10× şişme
uzun işlem 300 adım →    2000 ölü sürüm,  7.10× şişme
uzun işlem 600 adım → 1200000 ölü sürüm, 13.00× şişme
```

Birinci tablo beklendiği gibi: temizlik ne kadar seyrek çalışırsa ölü sürümler o kadar
birikir ve ayrılan en yüksek yer o kadar büyür. Her adımda çalışan bir temizlik şişmeyi
1,04'te tutuyor; altı yüz adımda bir çalışan ise 13,00'e çıkarıyor. Sona kalan ölü sürüm
sayısının hepsinde aynı olması yanıltmasın — ölçülen şey son durum değil, süreç boyunca
ulaşılan en yüksek yerdir; yer bir kez ayrıldıktan sonra geri verilmez.

İkinci tablo asıl uyarıyı veriyor. Temizlik sıklığı hiç değişmedi — her beş adımda bir
çalışıyor — ama üç yüz adım açık kalan tek bir işlem şişmeyi 1,12'den 7,10'a çıkardı.
Temizlik çalışıyor, ancak toplayacak bir şey bulamıyor: o işlemin anlık görüntüsünden sonra
üretilen bütün ölü sürümler hâlâ görünebilir durumda.

Sonuç iki cümlede toplanır. Şişmenin üst sınırını belirleyen şey, temizliğin çalışma sıklığı
ile **en eski açık işlemin yaşından** hangisi büyükse odur. Bu yüzden şişme sorunlarının
araştırılması, temizlik ayarlarından değil, uzun süredir açık kalan işlemlerin listesinden
başlar.

#### Özet

- Ölü satır, artık hiçbir işlemin görmediği bir satır sürümüdür; hem silme hem güncelleme
  üretir. Şişme, kaplanan yerin gerekli yere oranıdır.
- Ölçümde satırların üçte biri silindiğinde sayfa sayısı ve dosya boyutu hiç değişmedi;
  yalnız sayfa içi doluluk %77,9'dan %54,6'ya düştü.
- Sürekli artan anahtarla eklenen yeni satırlar ortadaki boşlukları kullanamaz; 119.822
  satırlık boşluk varken 100.000 satır eklendiğinde dosya 2.705 sayfadan 3.381 sayfaya çıktı.
- Tam yeniden yazma doluluğu ilk günkü değere yaklaştırdı (2.571 sayfa), ancak kilit, tablo
  boyutu kadar geçici alan ve tablo büyüklüğüyle orantılı süre gerektirir.
- Modelde şişme oranını belirleyen iki etken ayrıştı: temizliğin sıklığı ve en eski açık
  işlemin yaşı; üç yüz adım açık kalan tek bir işlem şişmeyi 1,12'den 7,10'a çıkardı.

#### Sonraki Adım

Bu kursta şimdiye kadar sorulan her soru bir sayıyla yanıtlandı: sayfa sayısı, doluluk,
çerçeve sayısı, isabet oranı. Bu sayılar ortada durmuyor; motor kendi yapısı hakkındaki
bilgiyi sıradan tablolarda tutar ve sıradan sorgularla verir. Sonraki ders bu bilgiyi ele
alır: tabloların, sütunların, dizinlerin ve kısıtların tanımının nerede saklandığı, oradan
hangi yönetim sorularının yanıtlanabileceği ve şemayı sorgulayarak yazılan denetimlerin
elle tutulan listelerden neden üstün olduğu.

### Sistem Kataloğu

Source: https://academia.sh/tr/kurslar/veritabani-yonetimi/sistem-katalogu

Şema tanımının sorgulanabilir tablolarda tutulması, envanterin ve yönetim kurallarının sorguya çevrilmesi, iki kopya arasındaki şema sapmasının ölçülmesi ve kataloğun neden okunup yazılmadığı.

Bu kursta şimdiye kadar sorulan her soru bir sayıyla yanıtlandı: sayfa sayısı, doluluk,
çerçeve sayısı, isabet oranı. Bu sayıların kaynağı motorun kendi tuttuğu kayıtlardır ve o
kayıtlar özel bir arayüzün arkasında değildir. Motor, şema hakkındaki bilgiyi sıradan
tablolarda tutar ve sıradan sorgularla verir. Bu tabloların bütününe **sistem kataloğu
(system catalog)** denir.

Kataloğun sorgulanabilir olması, yönetim işini bir belge işi olmaktan çıkarır. Hangi
tabloların hangi sütunları taşıdığı, hangi dizinlerin var olduğu, hangi kısıtların
tanımlandığı elle tutulan bir listede değil, veritabanının kendisinde yazılıdır — ve o
liste her zaman günceldir, çünkü değişikliği yapan deyimin kendisi onu günceller.

#### Katalog Sıradan Bir Tablodur

Aşağıdaki blok kütüphane şemasını kurar. Şema önceki kurslardan tanıdıktır; buraya
dizinler, bir görünüm ve bir değer denetimi eklenmiştir.

```sh
rm -f kutuphane.db
sqlite3 kutuphane.db <<'SQL'
CREATE TABLE sube (
  sube_id INTEGER PRIMARY KEY,
  ad      TEXT NOT NULL,
  sehir   TEXT NOT NULL
);
CREATE TABLE uye (
  uye_id       INTEGER PRIMARY KEY,
  ad           TEXT NOT NULL,
  eposta       TEXT UNIQUE,
  kayit_tarihi TEXT NOT NULL,
  durum        TEXT NOT NULL DEFAULT 'etkin' CHECK (durum IN ('etkin','askida','kapali'))
);
CREATE TABLE kitap (
  kitap_id   INTEGER PRIMARY KEY,
  baslik     TEXT NOT NULL,
  yazar      TEXT NOT NULL,
  isbn       TEXT UNIQUE,
  sube_id    INTEGER NOT NULL REFERENCES sube(sube_id)
);
CREATE TABLE odunc (
  odunc_id    INTEGER PRIMARY KEY,
  uye_id      INTEGER NOT NULL REFERENCES uye(uye_id),
  kitap_id    INTEGER NOT NULL REFERENCES kitap(kitap_id),
  alis_tarihi TEXT NOT NULL,
  iade_tarihi TEXT
);
CREATE INDEX odunc_uye ON odunc(uye_id);
CREATE INDEX odunc_kitap_tarih ON odunc(kitap_id, alis_tarihi);
CREATE VIEW acik_odunc AS SELECT * FROM odunc WHERE iade_tarihi IS NULL;
INSERT INTO sube VALUES (1,'Merkez','Ankara'),(2,'Bahçelievler','Ankara');
INSERT INTO uye (uye_id,ad,eposta,kayit_tarihi) VALUES (1,'Ayşe','ayse@ornek.test','2023-02-14');
INSERT INTO kitap VALUES (1,'Gökbilim El Kitabı','Y. Aydın','978-0201896831',1);
INSERT INTO odunc VALUES (1,1,1,'2025-06-01',NULL);
SQL
```

Şemadaki nesnelerin listesi tek bir sorguyla alınır.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
SELECT type, name, tbl_name FROM sqlite_schema ORDER BY type, name;
SQL
```

```
type   name                      tbl_name  
-----  ------------------------  ----------
index  odunc_kitap_tarih         odunc     
index  odunc_uye                 odunc     
index  sqlite_autoindex_kitap_1  kitap     
index  sqlite_autoindex_uye_1    uye       
table  kitap                     kitap     
table  odunc                     odunc     
table  sube                      sube      
table  uye                       uye       
view   acik_odunc                acik_odunc
```

Listede yazılmayan iki satır dikkat çeker: `sqlite_autoindex_` önekli dizinler. Bunları
tanımlayıcı yazmadı; benzersizlik kısıtını uygulayabilmek için motor kendisi oluşturdu.
Katalog, yalnız yazılanı değil **motorun kurduğu** yapıyı da gösterir. Bu ayrım
yönetimde işe yarar: bir sütunun benzersiz olması, arka planda bir dizin maliyeti
demektir ve o maliyet burada görünür.

Nesne adlarının önekli ayrılması bir gelenektir ve motora göre değişir; değişmeyen, iç
nesnelerin de katalogda bulunmasıdır.

#### Envanteri Sorguyla Çıkarmak

Katalog sorgulanabilir olduğu için, şema hakkındaki her soru bir sorguya çevrilebilir.
Aşağıdaki sorgu tablo başına sütun ve kısıt sayımını üretir.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
-- Sema envanteri: tablo basina sutun ve kisit sayilari.
SELECT m.name AS tablo, count(*) AS sutun, sum(ti."notnull") AS not_null,
       sum(ti.pk > 0) AS birincil_anahtar
FROM sqlite_schema m JOIN pragma_table_info(m.name) ti
WHERE m.type='table' GROUP BY m.name ORDER BY m.name;
SQL
```

```
tablo  sutun  not_null  birincil_anahtar
-----  -----  --------  ----------------
kitap  5      3         1               
odunc  5      3         1               
sube   3      2         1               
uye    5      3         1
```

Bu tablo bir şema tasarımı tartışmasının başlangıç noktasıdır: hangi tablo kaç sütun
taşıyor, kaç sütun zorunlu, birincil anahtarı olmayan tablo var mı. Elle tutulan bir
belgede bu üç soru için üç ayrı bölüm gerekir ve üçü de eskir; katalogdan çıkarıldığında
sorgu her çalıştırıldığında güncel yanıtı verir.

#### Katalogla Yazılan Denetimler

Kataloğun asıl değeri, **kural**ların sorgu olarak yazılabilmesidir. Dizinler ve
Bölümleme konusunda görülecek bir kuralı şimdiden uygulayalım: yabancı anahtar taşıyan
her sütunun kendi dizini olmalıdır, yoksa ana tablodaki silme ve güncelleme işlemleri
çocuk tabloyu baştan sona taramak zorunda kalır.

```sh
sqlite3 kutuphane.db <<'SQL'
.headers on
.mode column
-- 1. Yabanci anahtari dizinsiz kalan sutunlar: silme ve guncelleme maliyetini artirir.
SELECT m.name AS tablo, f."from" AS sutun, f."table" AS hedef
FROM sqlite_schema m
JOIN pragma_foreign_key_list(m.name) f
WHERE m.type = 'table'
  AND NOT EXISTS (
    SELECT 1 FROM pragma_index_list(m.name) il
    JOIN pragma_index_info(il.name) ii
    WHERE ii.seqno = 0 AND ii.name = f."from"
  );
SQL
```

```
tablo  sutun    hedef
-----  -------  -----
kitap  sube_id  sube
```

Denetim tek bir bulgu üretti: kitap tablosundaki şube başvurusu dizinsiz. Ödünç
tablosundaki iki başvuru kapsanıyor — biri kendi dizinine, diğeri bileşik dizinin ilk
sütunu olduğu için ona dayanıyor. Bileşik dizinin yalnız **ilk** sütununun bu işi
gördüğü, Sorgu Başarımı konusunda kurulan kuralın buradaki karşılığıdır.

Bu denetimin elle tutulan listeye üstünlüğü üç noktadadır. Yeni bir tablo eklendiğinde
denetim onu kendiliğinden kapsar. Bir dizin kaldırıldığında bulgu kendiliğinden geri
gelir. Ve denetim, tanımlayıcının ne yazdığını değil motorun ne bildiğini okur — ikisi
ayrıştığında ayrışma görünür olur.

Aynı kalıpla yazılabilecek başka denetimler vardır: birincil anahtarı olmayan tablolar,
hiç kullanılmayan dizinler, aynı sütun listesini paylaşan yinelenen dizinler, değer
denetimi olmayan durum sütunları. Her biri bir yönetim kuralının sorgu biçimidir.

#### Şema Sapmasının Ölçülmesi

Katalog sorgulanabilir olduğu için iki veritabanının şeması karşılaştırılabilir. Bu,
sürüm yükseltmelerinde ve ortamlar arası tutarlılık denetiminde en sık ihtiyaç duyulan
işlemdir.

```sh
# Iki semanin farki: gelistirme ve uretim kopyalari ayni mi?
sqlite3 uretim.db <<'SQL'
CREATE TABLE sube (sube_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, sehir TEXT NOT NULL);
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, eposta TEXT UNIQUE,
                  kayit_tarihi TEXT NOT NULL);
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL, yazar TEXT NOT NULL,
                    isbn TEXT UNIQUE, sube_id INTEGER NOT NULL REFERENCES sube(sube_id));
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, uye_id INTEGER NOT NULL REFERENCES uye(uye_id),
                    kitap_id INTEGER NOT NULL REFERENCES kitap(kitap_id), alis_tarihi TEXT NOT NULL,
                    iade_tarihi TEXT);
CREATE INDEX odunc_uye ON odunc(uye_id);
SQL

esle() {
  sqlite3 "$1" "SELECT type||' '||name||' ('||coalesce((SELECT group_concat(ti.name||':'||ti.type, ', ')
                  FROM pragma_table_info(m.name) ti), '')||')'
                FROM sqlite_schema m WHERE name NOT LIKE 'sqlite_%' ORDER BY type, name;"
}
esle kutuphane.db > gelistirme.liste
esle uretim.db    > uretim.liste
echo "--- yalniz gelistirmede olan ---"
comm -23 gelistirme.liste uretim.liste
echo "--- yalniz uretimde olan ---"
comm -13 gelistirme.liste uretim.liste
```

```
--- yalniz gelistirmede olan ---
index odunc_kitap_tarih ()
table uye (uye_id:INTEGER, ad:TEXT, eposta:TEXT, kayit_tarihi:TEXT, durum:TEXT)
view acik_odunc (odunc_id:INTEGER, uye_id:INTEGER, kitap_id:INTEGER, alis_tarihi:TEXT, iade_tarihi:TEXT)
--- yalniz uretimde olan ---
table uye (uye_id:INTEGER, ad:TEXT, eposta:TEXT, kayit_tarihi:TEXT)
```

Fark üç kalem gösteriyor. Bir dizin ve bir görünüm yalnız geliştirme kopyasında var.
Üye tablosu ise iki tarafta da var ama **aynı değil**: geliştirmede bir durum sütunu
eklenmiş, üretimde yok. Sütun listesini karşılaştırmaya katmak bu farkı görünür kılar;
yalnız nesne adları karşılaştırılsaydı iki tablo eşit sayılırdı.

Sapmanın yönü de bilgidir. Geliştirmede olup üretimde olmayan bir sütun, henüz
uygulanmamış bir göç demektir. Üretimde olup geliştirmede olmayan bir nesne ise daha
kaygı vericidir: doğrudan üretime yazılmış, sürüm kontrolünde karşılığı olmayan bir
değişikliğe işaret eder.

#### Kataloğa Yazmak

Katalog okunur; **elle yazılmaz**. Şema değişikliği veri tanımlama deyimleriyle yapılır
ve katalog güncellemesini motor üstlenir. Katalog tablolarına doğrudan yazmak — bazı
motorlarda teknik olarak olanaklı olsa da — motorun iç tutarlılığını bozar: veri
dosyalarındaki fiziksel düzenle katalogdaki tanım ayrışır ve bu ayrışma çoğu zaman
sonraki bir okumada, tanısı zor bir hata olarak görünür.

Kuralın pratik karşılığı şudur: göçler deyimlerle yazılır ve sürüm kontrolünde durur;
katalog ise bu göçlerin uygulanıp uygulanmadığını **doğrulamak** için okunur.

#### Özet

- Sistem kataloğu, şemanın tanımını tutan ve sıradan sorgularla okunabilen tablolardır;
  değişikliği yapan deyimin kendisi onu güncellediği için her zaman günceldir.
- Katalog yalnız yazılanı değil motorun kendiliğinden kurduğu nesneleri de gösterir:
  benzersizlik kısıtının arkasındaki dizin listede görünür.
- Şema hakkındaki her yönetim kuralı bir sorguya çevrilebilir; dizinsiz yabancı anahtar
  denetimi modelde tek bulgu üretti ve yeni tabloları kendiliğinden kapsar.
- İki kopyanın şeması sütun listeleriyle birlikte karşılaştırıldığında sapma görünür
  olur; yalnız nesne adları karşılaştırılırsa değişmiş bir tablo eşit sayılır.
- Katalog okunur, elle yazılmaz: şema değişikliği deyimlerle yapılır, katalog o
  değişikliğin uygulandığını doğrulamak için sorgulanır.

#### Sonraki Adım

Motorun içyapısı bu dersle tamamlanıyor: süreçler, sayfalar, günlük, denetim noktaları,
sürüm zincirleri, temizlik ve bunların hepsini kayda geçiren katalog. Bu bölümde bir
bulgu askıda kaldı — dizinsiz yabancı anahtar. Sonraki konu bu bulgunun ait olduğu alanı
açar: dizin türlerinin birbirinden farkı, bileşik dizinde sütun sırasının neden sonucu
değiştirdiği, kısmi ve kapsayan dizinlerin ne zaman kazandırdığı ve bir tablonun
bölümlere ayrılmasının hangi sorunu çözüp hangisini doğurduğu.

## Dizinler ve Bölümleme

Ağaç, karma ve özel amaçlı dizin türleri, bileşik dizinlerde sütun sırası, kısmi ve kapsayan dizinler, dizin şişmesi ile bakım maliyeti, aralık ve karma tablo bölümleme ve yatay parçalamanın uygulama üzerindeki etkisi.

### Dizin Türleri

Source: https://academia.sh/tr/kurslar/veritabani-yonetimi/dizin-turleri

Dizinsiz yabancı anahtarın ölçülen bedeli, ağaç tabanlı dizinin aralık ve sıra yeteneği, karma tabanlı yaklaşımın yalnız eşitlikte çalışması ve sözcük düzeyinde arama için ters dizin.

Motor Mimarisi konusu sistem kataloğuyla kapandı ve arkasında tek bir bulgu bıraktı:
kitap tablosundaki şube başvurusu dizinsizdi. O bulgu bir tavsiye biçiminde yazılmıştı —
"yabancı anahtar taşıyan her sütunun kendi dizini olmalıdır". Tavsiyeler ölçülmediği
sürece kanı olarak kalır.

Bu ders o bulguyu önce bir sayıya çevirir, sonra asıl sorusuna geçer. Dizin, Sorgu
Başarımı konusunda tek bir yapı gibi anlatılmıştı: sıralı tutulan bir kopya. Gerçekte
dizin bir yapı değil bir yapı ailesidir ve aileler birbirinden, hangi soruya yanıt
verebildikleriyle ayrılır. Bir dizin türü eşitliği yanıtlar ama aralığı yanıtlamaz;
başka biri değerin tamamını değil içindeki sözcükleri bulur. Seçim, sorunun biçimine
bakılarak yapılır.

#### Askıda Kalan Bulgu

Yabancı anahtar kısıtı, bir tablodaki değerin başka bir tabloda var olmasını zorunlu
kılar. Bu zorunluluğun bir de ters yönü vardır: ana tablodan bir satır silinirken motor,
o satıra başvuran bir çocuk satır kalmadığını doğrulamak zorundadır. Doğrulamanın nasıl
yapılacağını belirleyen şey, çocuk sütunda dizin olup olmamasıdır.

```sh
rm -f fk.db fk_dizinli.db
sqlite3 fk.db <<'SQL'
PRAGMA foreign_keys = ON;
CREATE TABLE sube (sube_id INTEGER PRIMARY KEY, ad TEXT NOT NULL);
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL,
                    sube_id INTEGER NOT NULL REFERENCES sube(sube_id));
INSERT INTO sube VALUES (1,'Merkez'),(2,'Bahcelievler'),(3,'Kadikoy'),(4,'Beyoglu'),
                        (5,'Konak'),(6,'Nilufer'),(7,'Selcuklu'),(8,'Cankaya'),(9,'Depo');
INSERT INTO kitap (kitap_id, baslik, sube_id)
WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n < 200000)
SELECT n, 'Kitap ' || n, 1 + (n % 8) FROM s;
SQL
cp fk.db fk_dizinli.db
sqlite3 fk_dizinli.db 'CREATE INDEX kitap_sube ON kitap(sube_id);'

for db in fk.db fk_dizinli.db; do
  printf '%s: ' "$db"
  sqlite3 "$db" <<'SQL'
PRAGMA foreign_keys = ON;
.stats vmstep
DELETE FROM sube WHERE sube_id = 9;
SQL
done
```

```
fk.db: VM-steps: 600013
fk_dizinli.db: VM-steps: 15
```

Silinen şube hiç kitap taşımıyordu; iki koşumda da silinen satır sayısı birdir. Fark
yapılan işte: dizinsiz kopyada motor, iki yüz bin kitabın hepsine tek tek bakıp hiçbirinin
dokuzuncu şubeye ait olmadığını doğruladı. Dizinli kopyada aynı doğrulama on beş adım
sürdü — dizine girip o anahtarın hiç girdisi olmadığını görmek yeterliydi.

Buradaki oran kırk bin kattır ve tabloyla birlikte büyür. Sonuç, kataloğa yazılan
denetimin gerekçesidir: yabancı anahtar sütununda dizin, okuma başarımı için değil,
**ana tablodaki her silme ve anahtar güncellemesi** için gereklidir. Bu maliyet sorgu
planlarında görünmez, çünkü onu doğuran deyim `DELETE` deyimidir ve pahalı olan kısım
kısıt denetimidir.

#### Ağaç Tabanlı Dizin

Sorgu Başarımı konusunda kullanılan dizin türü **ağaç tabanlı dizindir (tree index)**:
girdileri anahtar değerine göre sıralı tutan, çoğunlukla B-ağacı biçiminde bir yapı.
Ölçümler kütüphane veri kümesi üzerinde yapılır; aşağıdaki kurulum bütün konu boyunca
kullanılacaktır.

```sh
rm -f kutuphane.db
cat > kurulum.sql <<'SQL'
CREATE TABLE uye (
  uye_id       INTEGER PRIMARY KEY,
  ad           TEXT NOT NULL,
  sehir        TEXT NOT NULL,
  kayit_tarihi TEXT NOT NULL
);
CREATE TABLE odunc (
  odunc_id    INTEGER PRIMARY KEY,
  kitap_id    INTEGER NOT NULL,
  uye_id      INTEGER NOT NULL,
  alis_tarihi TEXT NOT NULL,
  iade_tarihi TEXT
);
INSERT INTO uye (uye_id, ad, sehir, kayit_tarihi)
WITH RECURSIVE sayac(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM sayac WHERE n < 120000)
SELECT n, 'Uye ' || n,
       CASE n % 5 WHEN 0 THEN 'Ankara' WHEN 1 THEN 'Istanbul' WHEN 2 THEN 'Izmir'
                  WHEN 3 THEN 'Bursa' ELSE 'Konya' END,
       date('2015-01-01', '+' || (n % 3200) || ' days')
FROM sayac;
INSERT INTO odunc (odunc_id, kitap_id, uye_id, alis_tarihi, iade_tarihi)
WITH RECURSIVE sayac(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM sayac WHERE n < 2000000)
SELECT n, 1 + ((n * 7) % 200000), 1 + ((n * 13) % 120000),
       date('2018-01-01', '+' || ((n * 37) % 2437) || ' days'),
       CASE WHEN n % 9 = 0 THEN NULL
            ELSE date('2018-01-01', '+' || (((n * 37) % 2437) + 14) || ' days') END
FROM sayac;
SQL
sqlite3 kutuphane.db < kurulum.sql
sqlite3 kutuphane.db <<'SQL'
CREATE INDEX odunc_alis ON odunc(alis_tarihi);
.print '--- esitlik ---'
EXPLAIN QUERY PLAN SELECT count(*) FROM odunc WHERE alis_tarihi = '2021-03-15';
.stats vmstep
SELECT count(*) FROM odunc WHERE alis_tarihi = '2021-03-15';
.stats off
.print '--- aralik ---'
EXPLAIN QUERY PLAN
SELECT count(*) FROM odunc WHERE alis_tarihi BETWEEN '2021-03-15' AND '2021-03-21';
.stats vmstep
SELECT count(*) FROM odunc WHERE alis_tarihi BETWEEN '2021-03-15' AND '2021-03-21';
.stats off
.print '--- en kucuk deger ---'
EXPLAIN QUERY PLAN SELECT min(alis_tarihi) FROM odunc;
.stats vmstep
SELECT min(alis_tarihi) FROM odunc;
SQL
```

```
--- esitlik ---
QUERY PLAN
`--SEARCH odunc USING COVERING INDEX odunc_alis (alis_tarihi=?)
820
VM-steps: 2471
--- aralik ---
QUERY PLAN
`--SEARCH odunc USING COVERING INDEX odunc_alis (alis_tarihi>? AND alis_tarihi<?)
5744
VM-steps: 17245
--- en kucuk deger ---
QUERY PLAN
`--SEARCH odunc USING COVERING INDEX odunc_alis
2018-01-01
VM-steps: 14
```

Üç sorunun üçü de dizinden yanıtlandı ve nedeni tektir: **sıra**. Eşitlikte ağaçtan tek
bir anahtara inilir. Aralıkta alt uca inilir ve üst uç geçilene kadar yan yana ilerlenir;
2.471 adımdan 17.245 adıma çıkış, yedi katlık satır sayısıyla orantılıdır. En küçük değer
sorusu ise on dört adımda yanıtlandı — dizinin en solundaki girdi okundu ve iş bitti.

Sıranın getirdiği yetenekler bir listedir: eşitlik, aralık, ön ek, en küçük ve en büyük,
`ORDER BY` için hazır düzen, gruplamada tekrarların yan yana gelmesi. Ağaç tabanlı dizinin
her motorda varsayılan tür olmasının nedeni budur; tek bir yapı bu soruların hepsini
karşılar.

#### Karma Tabanlı Dizin

**Karma tabanlı dizin (hash index)**, anahtarın kendisini değil, anahtarın bir karma
fonksiyonundan geçirilmiş değerini dizinler. Veri Yapıları kursundaki Karma Tabloları
dersinde kurulan yapı budur; buradaki fark, tablonun bellekte değil diskte durmasıdır.

Karma dizin her motorda bulunmaz. Aşağıdaki koşum onu bir **ifade dizini** ile modeller:
tarihin gün numarası çarpımsal bir karma fonksiyonundan geçirilir ve dizinlenen değer bu
karma olur. Model, gerçek bir karma dizinin belirleyici özelliğini birebir taşır —
dizindeki sıra, değerin sırası değildir.

```sh
rm -f karma.db
sqlite3 karma.db < kurulum.sql
sqlite3 karma.db <<'SQL'
-- Degerin kendisi degil, degerin carpimsal karmasi dizinlenir.
CREATE INDEX odunc_alis_karma ON odunc(
  (CAST(julianday(alis_tarihi) AS INTEGER) * 2654435761) % 4294967296 );
.print '--- aranan tarihin karma degeri ---'
SELECT (CAST(julianday('2021-03-15') AS INTEGER) * 2654435761) % 4294967296 AS karma;
.print '--- esitlik, karma uzerinden sorulunca ---'
EXPLAIN QUERY PLAN SELECT count(*) FROM odunc
  WHERE (CAST(julianday(alis_tarihi) AS INTEGER) * 2654435761) % 4294967296 = 2436359960;
.stats vmstep
SELECT count(*) FROM odunc
  WHERE (CAST(julianday(alis_tarihi) AS INTEGER) * 2654435761) % 4294967296 = 2436359960;
.stats off
.print '--- ayni esitlik, tarihin kendisi uzerinden sorulunca ---'
EXPLAIN QUERY PLAN SELECT count(*) FROM odunc WHERE alis_tarihi = '2021-03-15';
.stats vmstep
SELECT count(*) FROM odunc WHERE alis_tarihi = '2021-03-15';
.stats off
.print '--- aralik ---'
EXPLAIN QUERY PLAN
SELECT count(*) FROM odunc WHERE alis_tarihi BETWEEN '2021-03-15' AND '2021-03-21';
.stats vmstep
SELECT count(*) FROM odunc WHERE alis_tarihi BETWEEN '2021-03-15' AND '2021-03-21';
SQL
```

```
--- aranan tarihin karma degeri ---
2436359960
--- esitlik, karma uzerinden sorulunca ---
QUERY PLAN
`--SEARCH odunc USING INDEX odunc_alis_karma (<expr>=?)
820
VM-steps: 3292
--- ayni esitlik, tarihin kendisi uzerinden sorulunca ---
QUERY PLAN
`--SCAN odunc
820
VM-steps: 6000831
--- aralik ---
QUERY PLAN
`--SCAN odunc
5744
VM-steps: 7046372
```

Üç sonuç üç ayrı şey söyler. Karma üzerinden sorulan eşitlik 3.292 adımda yanıtlandı —
ağaç dizinin 2.471 adımına yakın, aynı büyüklük sınıfında.

İkinci ölçüm modelin sınırını gösterir: aynı eşitlik, tarih sütunu üzerinden yazıldığında
dizin kullanılmadı. Gerçek bir karma dizinde bu dönüşümü motor kendisi yapar; kullanıcı
sıradan bir eşitlik yazar, motor arama anahtarının karmasını hesaplar. Modelde dönüşümü
sorguyu yazan üstlenmek zorundadır. Değişmeyen nokta şudur: **arama anahtarının karması
hesaplanabiliyorsa** eşitlik yanıtlanır.

Üçüncü ölçüm asıl ayrımdır. Aralık sorgusu, dizin var olduğu hâlde tam tablo taramasına
düştü ve 7.046.372 adım harcadı. Neden yapısaldır: karma fonksiyonu bilerek düzensizdir.
Birbirini izleyen iki tarihin karma değerleri dizinin iki ucunda olabilir. "15 Mart ile
21 Mart arası" diye bir aralığın karma karşılığı yoktur, çünkü aradaki değerlerin hangi
karmalara düştüğü hesaplanamaz — hepsi tek tek denenmedikçe.

Buradan karma dizinin kullanım alanı çıkar: yalnız eşitlikle sorgulanan, aralığı ve
sırası hiç istenmeyen sütunlar. Karşılığında kazandığı şey yerdir; uzun metin anahtarlar
sabit boyutlu bir karmaya indiğinde dizin küçülür. Ağaç dizinin sunduğu her şeyden
vazgeçilmesi karşılığında elde edilen bu kazanç, çoğu kurulumda tercih edilmez.

#### Ters Dizin

Üçüncü aile başka bir soruyu hedefler. Ağaç dizin **değerin tamamını** anahtar sayar;
bu yüzden ön ek sorgularını yanıtlar ama değerin ortasındaki bir sözcüğü bulamaz.
Kütüphane kataloğunda konu başlıklarında arama yapmak tam olarak bu sorudur.

```sh
rm -f konu.db
sqlite3 konu.db <<'SQL'
CREATE TABLE kitap_konu (kitap_id INTEGER PRIMARY KEY, konu TEXT NOT NULL);
INSERT INTO kitap_konu (kitap_id, konu)
WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n < 200000),
sozcuk(i, w) AS (VALUES (0,'gokbilim'),(1,'cografya'),(2,'tarih'),(3,'siir'),(4,'roman'),
                        (5,'atlas'),(6,'sozluk'),(7,'elkitabi'),(8,'denizcilik'),
                        (9,'mimarlik'),(10,'muzik'),(11,'felsefe'),(12,'kartografya'),
                        (13,'botanik'),(14,'mineraloji'),(15,'hukuk'),(16,'tiyatro'),
                        (17,'mantik'),(18,'iktisat'),(19,'anatomi'))
SELECT n, (SELECT w FROM sozcuk WHERE i = n % 20)       || ' ' ||
          (SELECT w FROM sozcuk WHERE i = (n/20) % 20)  || ' ' ||
          (SELECT w FROM sozcuk WHERE i = (n/400) % 20)
FROM s;
CREATE INDEX konu_agac ON kitap_konu(konu);
CREATE VIRTUAL TABLE konu_ters USING fts5(konu, content='kitap_konu', content_rowid='kitap_id');
INSERT INTO konu_ters(konu_ters) VALUES('rebuild');
SQL
sqlite3 konu.db <<'SQL'
PRAGMA case_sensitive_like = ON;
.print '--- agac dizini, sol ucu bagli kalip ---'
EXPLAIN QUERY PLAN SELECT count(*) FROM kitap_konu WHERE konu LIKE 'kartografya denizcilik%';
.stats vmstep
SELECT count(*) FROM kitap_konu WHERE konu LIKE 'kartografya denizcilik%';
.stats off
.print '--- ayni agac dizini, sol ucu acik kalip ---'
EXPLAIN QUERY PLAN SELECT count(*) FROM kitap_konu WHERE konu LIKE '%kartografya denizcilik%';
.stats vmstep
SELECT count(*) FROM kitap_konu WHERE konu LIKE '%kartografya denizcilik%';
.stats off
.print '--- ters dizin, ayni sozcuk obegi ---'
EXPLAIN QUERY PLAN SELECT count(*) FROM konu_ters WHERE konu_ters MATCH '"kartografya denizcilik"';
.stats vmstep
SELECT count(*) FROM konu_ters WHERE konu_ters MATCH '"kartografya denizcilik"';
SQL
```

```
--- agac dizini, sol ucu bagli kalip ---
QUERY PLAN
`--SEARCH kitap_konu USING COVERING INDEX konu_agac (konu>? AND konu<?)
500
VM-steps: 1513
--- ayni agac dizini, sol ucu acik kalip ---
QUERY PLAN
`--SCAN kitap_konu
1000
VM-steps: 801011
--- ters dizin, ayni sozcuk obegi ---
QUERY PLAN
`--SCAN konu_ters VIRTUAL TABLE INDEX 0:M1
1000
VM-steps: 2013
```

İlk ölçüm ağaç dizinin yapabildiğini gösteriyor: sol ucu bağlı kalıp bir aralığa çevrildi
ve 1.513 adımda yanıtlandı. İkinci ölçümde aynı dizin duruyor, aranan aynı sözcük öbeği,
tek fark kalıbın sol ucunun açılması — ve dizin devre dışı kaldı. Sorgu Başarımı konusunda
kurulan kural burada tekrar görünüyor: sol ucu açık bir kalıp, sıralı bir yapıda giriş
noktası bırakmaz.

**Ters dizin (inverted index)** bu duvarı yapıyı değiştirerek aşar. Anahtar olarak değerin
tamamını değil, değerin içinden çıkarılan her sözcüğü alır; her sözcüğün karşısında o
sözcüğü içeren kayıtların listesi durur. Sözcük öbeği araması iki listenin kesişimi ve
konum karşılaştırmasıyla yanıtlanır: 2.013 adım, tarama yolunun dört yüzde biri.

Bedeli vardır. Ters dizin, kaynak metnin sözcük sayısıyla orantılı büyür ve her metin
değişikliğinde birden çok sözcük girdisi güncellenir. Ayrıca sözcüklere ayırma kuralı
dile bağlıdır: kök bulma, ek ayırma ve büyük–küçük harf eşlemesi Türkçe için ayrı
ayarlanır. Bu ayarlar motorlar arasında en çok farklılaşan alandır.

#### Aile Listesi ve Seçim Ölçütü

Yaygın dizin aileleri, yanıtladıkları soruya göre ayrılır. Ağaç tabanlı dizin sıra
gerektiren her soruyu karşılar. Karma tabanlı dizin yalnız eşitliği karşılar. Ters dizin
metin içindeki sözcüğü bulur. Bunların yanında iki aile daha sık geçer: çok az sayıda
farklı değer alan sütunlar için her değere bir bit örüntüsü tutan **bit eşlem dizini
(bitmap index)** ve iki boyutlu kutuları kapsayan dikdörtgenlerle örten **uzamsal dizin
(spatial index)**. İkincisi Veri Yapıları kursundaki k-d ağacıyla aynı sorunu çözer:
sıralaması tek boyutlu olmayan verinin aranması.

Seçim ölçütü tek cümlede toplanır: dizin türü, sütunun tipine göre değil **sorgunun
biçimine** göre seçilir. Aynı tarih sütunu, yalnız eşitlikle sorgulanıyorsa karma
tabanlı bir yapıya, aralıkla sorgulanıyorsa ağaç tabanlı bir yapıya uygundur. Hangi
türlerin bulunduğu motora özgüdür; değişmeyen, türü belirleyen sorunun sorgu tarafında
durmasıdır.

#### Özet

- Dizinsiz bir yabancı anahtar sütunu, ana tablodaki her silmeyi çocuk tablonun taranmasına
  çevirir; ölçümde tek satırlık bir silme 600.013 adımdan 15 adıma indi.
- Ağaç tabanlı dizin girdileri sıralı tuttuğu için eşitlik, aralık, ön ek, en küçük–en
  büyük ve hazır sıra sorularının hepsini karşılar.
- Karma tabanlı dizin anahtarın karmasını sıralar; eşitliği ağaç dizine yakın maliyetle
  yanıtlar, aralık sorgusunda ise dizin kullanılamaz ve sorgu taramaya düşer.
- Ters dizin, değerin tamamını değil içindeki sözcükleri anahtar alır; sol ucu açık kalıpla
  801.011 adım süren arama, sözcük öbeği araması olarak 2.013 adımda yanıtlandı.
- Dizin türü sütunun tipine göre değil sorgunun biçimine göre seçilir; sıra istenmeyen bir
  sütunda ağaç dizinin sunduğu yeteneklerin bedeli ödenmiş ama karşılığı alınmamış olur.

#### Sonraki Adım

Bu dersteki bütün dizinler tek sütunluydu ve tablonun bütün satırlarını kapsıyordu.
İki kısıtlama da gevşetilebilir. Bir dizin birden çok sütun taşıdığında, sütunların
yazıldığı sıra dizinin hangi sorguya yarayacağını belirler — aynı iki sütun iki ayrı
sırayla dizinlendiğinde ortaya iki farklı yapı çıkar. Bir dizin tablonun yalnız bir
bölümünü kapsadığında ise hem küçülür hem de kapsadığı bölümde daha ucuz çalışır. Sonraki
ders bu iki kararı ölçer: sütun sırasının üç ayrı sorgu üzerindeki etkisi ve koşullu bir
dizinin boyut ile plan tarafındaki kazancı.

### Bileşik ve Kısmi Dizinler

Source: https://academia.sh/tr/kurslar/veritabani-yonetimi/bilesik-ve-kismi-dizinler

Bileşik dizinde sütun sırasının üç ayrı sorgu üzerindeki etkisi, sol ön ek kuralı, kısmi dizinin sayfa sayısıyla ölçülen boyut kazancı ve kapsamadığı sorguda ne olduğu.

Önceki ders dizin türlerini birbirinden ayırdı ve hepsinde tek sütunlu, tablonun tamamını
kapsayan dizinler kullandı. İki kısıtlama da tasarım kararıdır, zorunluluk değil.

Bir dizin birden çok sütun taşıyabilir. Bu durumda ortaya yeni bir soru çıkar: sütunlar
hangi sırayla yazılacak? Sorgu Planı Okuma dersinde bu sorunun tek bir sorgu üzerindeki
karşılığı ölçülmüştü ve bir sıra kırk dört bin kat kazanmıştı. O ölçüm eksik bir izlenim
bırakır: sanki sıralardan biri doğru, diğeri yanlıştır. Gerçekte her sıra bir sorgu
kümesine hizmet eder ve başka bir kümeyi dışarıda bırakır. Bu ders üç sorguyla o dengeyi
gösterir, sonra ikinci kısıtlamayı gevşetir: tablonun yalnız bir bölümünü kapsayan dizin.

#### Üç Sorgu, İki Sıra

Ölçümler kütüphane ödünç kayıtları üzerinde yapılır. Aşağıdaki kurulum bu ders için
gereken iki tabloyu kurar.

```sh
rm -f kutuphane.db
cat > kurulum.sql <<'SQL'
CREATE TABLE uye (
  uye_id       INTEGER PRIMARY KEY,
  ad           TEXT NOT NULL,
  sehir        TEXT NOT NULL,
  kayit_tarihi TEXT NOT NULL
);
CREATE TABLE odunc (
  odunc_id    INTEGER PRIMARY KEY,
  kitap_id    INTEGER NOT NULL,
  uye_id      INTEGER NOT NULL,
  alis_tarihi TEXT NOT NULL,
  iade_tarihi TEXT
);
INSERT INTO uye (uye_id, ad, sehir, kayit_tarihi)
WITH RECURSIVE sayac(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM sayac WHERE n < 120000)
SELECT n, 'Uye ' || n,
       CASE n % 5 WHEN 0 THEN 'Ankara' WHEN 1 THEN 'Istanbul' WHEN 2 THEN 'Izmir'
                  WHEN 3 THEN 'Bursa' ELSE 'Konya' END,
       date('2015-01-01', '+' || (n % 3200) || ' days')
FROM sayac;
INSERT INTO odunc (odunc_id, kitap_id, uye_id, alis_tarihi, iade_tarihi)
WITH RECURSIVE sayac(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM sayac WHERE n < 2000000)
SELECT n, 1 + ((n * 7) % 200000), 1 + ((n * 13) % 120000),
       date('2018-01-01', '+' || ((n * 37) % 2437) || ' days'),
       CASE WHEN n % 9 = 0 THEN NULL
            ELSE date('2018-01-01', '+' || (((n * 37) % 2437) + 14) || ' days') END
FROM sayac;
SQL
sqlite3 kutuphane.db < kurulum.sql
```

Üç sorgu, ödünç masasının gerçekten sorduğu üç sorudur. Birincisi bir üyenin ödünç
geçmişini tarih sırasında ister. İkincisi belli bir günde alınan bütün kitapları sayar.
Üçüncüsü bir üyenin belli bir tarihten sonraki kayıtlarını ister. Aynı iki sütun, iki
ayrı sırayla dizinlenir ve üç sorgu her iki dizinle de çalıştırılır.

```sh
for sira in "uye_id, alis_tarihi" "alis_tarihi, uye_id"; do
  rm -f sira.db
  sqlite3 sira.db < kurulum.sql
  sqlite3 sira.db "CREATE INDEX odunc_bilesik ON odunc($sira);"
  printf '=== dizin: (%s) ===\n' "$sira"
  sqlite3 sira.db <<'SQL'
.print '-- S1: uye esitligi, tarihe gore sirali'
EXPLAIN QUERY PLAN SELECT odunc_id FROM odunc WHERE uye_id = 4 ORDER BY alis_tarihi;
.stats vmstep
SELECT count(*) FROM (SELECT odunc_id FROM odunc WHERE uye_id = 4 ORDER BY alis_tarihi);
.stats off
.print '-- S2: yalniz tarih esitligi'
EXPLAIN QUERY PLAN SELECT count(*) FROM odunc WHERE alis_tarihi = '2021-03-15';
.stats vmstep
SELECT count(*) FROM odunc WHERE alis_tarihi = '2021-03-15';
.stats off
.print '-- S3: uye esitligi + tarih araligi'
EXPLAIN QUERY PLAN SELECT count(*) FROM odunc WHERE uye_id = 4 AND alis_tarihi >= '2022-01-01';
.stats vmstep
SELECT count(*) FROM odunc WHERE uye_id = 4 AND alis_tarihi >= '2022-01-01';
SQL
done
```

```
=== dizin: (uye_id, alis_tarihi) ===
-- S1: uye esitligi, tarihe gore sirali
QUERY PLAN
`--SEARCH odunc USING COVERING INDEX odunc_bilesik (uye_id=?)
17
VM-steps: 135
-- S2: yalniz tarih esitligi
QUERY PLAN
`--SCAN odunc USING COVERING INDEX odunc_bilesik
820
VM-steps: 6000831
-- S3: uye esitligi + tarih araligi
QUERY PLAN
`--SEARCH odunc USING COVERING INDEX odunc_bilesik (uye_id=? AND alis_tarihi>?)
8
VM-steps: 37
=== dizin: (alis_tarihi, uye_id) ===
-- S1: uye esitligi, tarihe gore sirali
QUERY PLAN
`--SCAN odunc USING COVERING INDEX odunc_bilesik
17
VM-steps: 6000101
-- S2: yalniz tarih esitligi
QUERY PLAN
`--SEARCH odunc USING COVERING INDEX odunc_bilesik (alis_tarihi=?)
820
VM-steps: 2471
-- S3: uye esitligi + tarih araligi
QUERY PLAN
`--SEARCH odunc USING COVERING INDEX odunc_bilesik (alis_tarihi>?)
8
VM-steps: 2402957
```

Altı ölçüm bir tabloya sığar. Sütun sırası `(uye_id, alis_tarihi)` iken S1 135 adım, S2
6.000.831 adım, S3 37 adım harcadı. Ters sırayla S1 6.000.101, S2 2.471, S3 2.402.957
oldu. Hiçbir sıra üç sorgunun üçünde birden kazanmıyor.

Nedeni tek bir yapısal olgudur. Bileşik dizin, girdileri önce birinci sütuna, o eşitken
ikinci sütuna göre sıralar. Böyle bir sıra ancak bir **giriş noktası** verirse işe yarar
ve giriş noktası hep baştan başlar.

#### Sol Ön Ek Kuralı

Kuralın adı **sol ön ek kuralıdır (leftmost prefix rule)**: bir bileşik dizin, sütun
listesinin yalnız soldan başlayan bir parçasına giriş noktası verir. `(a, b, c)` dizini
`a` koşuluna, `a` ve `b` koşuluna, `a`, `b` ve `c` koşuluna yanıt verir; yalnız `b`
koşuluna ya da yalnız `b` ile `c` koşuluna yanıt vermez.

Ölçümler bunu üç kez doğruluyor. S2 yalnız tarihi süzüyor: `(uye_id, alis_tarihi)`
dizininde tarih ikinci sütun olduğu için giriş noktası yok, plan `SCAN` diyor. Aynı sorgu
`(alis_tarihi, uye_id)` dizininde birinci sütunu süzdüğü için 2.471 adımda bitiyor.

S3 kuralın ikinci yüzünü gösterir. `(uye_id, alis_tarihi)` dizininde iki koşul da anahtara
giriyor — plan bunu `(uye_id=? AND alis_tarihi>?)` yazarak söylüyor — ve iş 37 adımda
bitiyor. Ters sırada ise yalnız tarih aralığı anahtar oluyor; motor 2022 sonrasındaki
bütün kayıtları dizinden okuyup üye koşulunu tek tek sınıyor. Sonuç aynı sekiz satır, bedel
2,4 milyon adım.

Buradan sütun sırasını seçme kuralı çıkar: **eşitlikle süzülen sütunlar başa, aralık ve
sıralama sütunları sona.** Eşitlik dizindeki konumu tek bir noktaya sabitler ve sonraki
sütunun sırasını korur; aralık ise bir kez girildiğinde sonraki sütunların sırasını
dağıtır. `(uye_id, alis_tarihi)` dizininde tarih sırası her üye için ayrı ayrı korunduğu
için S1'de sıralama düğümü hiç oluşmadı.

Bir sütunun iki dizinde birden yer alması yasak değildir. Yukarıdaki üç sorguyu birlikte
hızlandırmanın yolu iki bileşik dizin kurmaktır; bedeli, iki ağacın da her yazmada
güncellenmesidir. Bu bedelin ölçüsü Dizin Bakımı dersinin konusudur.

#### Kısmi Dizin

İkinci gevşetme kapsamla ilgilidir. **Kısmi dizin (partial index)**, tablonun yalnız bir
koşulu sağlayan satırlarını içeren dizindir. Kütüphanede bunun karşılığı doğrudandır:
ödünç kayıtlarının çoğu kapanmıştır, günlük işin sorduğu ise açık ödünçlerdir.

```sh
rm -f temel.db tam.db kismi.db
sqlite3 temel.db < kurulum.sql
cp temel.db tam.db
cp temel.db kismi.db
sqlite3 tam.db   'CREATE INDEX odunc_uye  ON odunc(uye_id);'
sqlite3 kismi.db 'CREATE INDEX odunc_acik ON odunc(uye_id) WHERE iade_tarihi IS NULL;'

echo "-- kapsanan satir sayisi --"
sqlite3 temel.db "SELECT count(*) AS tumu FROM odunc;
                  SELECT count(*) AS acik FROM odunc WHERE iade_tarihi IS NULL;"
echo "-- sayfa sayisi --"
for db in temel.db tam.db kismi.db; do
  printf '%-10s %s\n' "$db" "$(sqlite3 "$db" 'PRAGMA page_count;')"
done
for db in tam.db kismi.db; do
  printf '\n=== %s ===\n' "$db"
  sqlite3 "$db" <<'SQL'
.print '-- bir uyenin acik oduncleri'
EXPLAIN QUERY PLAN SELECT count(*) FROM odunc WHERE uye_id = 4 AND iade_tarihi IS NULL;
.stats vmstep
SELECT count(*) FROM odunc WHERE uye_id = 4 AND iade_tarihi IS NULL;
.stats off
.print '-- ayni uyenin butun oduncleri'
EXPLAIN QUERY PLAN SELECT count(*) FROM odunc WHERE uye_id = 4;
.stats vmstep
SELECT count(*) FROM odunc WHERE uye_id = 4;
SQL
done
```

```
-- kapsanan satir sayisi --
2000000
222222
-- sayfa sayisi --
temel.db   19005
tam.db     24743
kismi.db   19644

=== tam.db ===
-- bir uyenin acik oduncleri
QUERY PLAN
`--SEARCH odunc USING INDEX odunc_uye (uye_id=?)
6
VM-steps: 103
-- ayni uyenin butun oduncleri
QUERY PLAN
`--SEARCH odunc USING COVERING INDEX odunc_uye (uye_id=?)
17
VM-steps: 62

=== kismi.db ===
-- bir uyenin acik oduncleri
QUERY PLAN
`--SEARCH odunc USING INDEX odunc_acik (uye_id=?)
6
VM-steps: 36
-- ayni uyenin butun oduncleri
QUERY PLAN
`--SCAN odunc
17
VM-steps: 6000028
```

Boyut farkı dizinin kapsadığı satır oranını birebir izliyor. Tam dizin veritabanına 5.738
sayfa ekledi (24.743 − 19.005), kısmi dizin ise 639 sayfa (19.644 − 19.005). Oran
5738 / 639 ≈ 9,0; kapsanan satır oranı da 2.000.000 / 222.222 = 9,0. Kısmi dizin
küçüktür çünkü az satır tutar — başka bir hüner yoktur.

Plan tarafında da kazanıyor. Açık ödünç sorgusunda tam dizin 103, kısmi dizin 36 adım
harcadı. Kazanç iki kaynaktan gelir: kısmi dizinin ağacı daha kısadır ve dizinden gelen
her girdi zaten koşulu sağladığı için tabloya dönülüp `iade_tarihi` denetlenmesi
gerekmez.

#### Kısmi Dizinin Sınırı

İkinci ölçüm bedeli gösterir. Aynı üyenin **bütün** ödünçleri sorulduğunda kısmi dizin
kullanılamadı ve sorgu tam tablo taramasına düştü: 6.000.028 adım. Bunun bir eksiklik
değil tanım gereği olduğunu görmek gerekir — kısmi dizinde kapalı ödünçlerin girdisi hiç
yoktur; kullanılsaydı yanlış cevap verirdi.

Planlayıcı kısmi dizini ancak sorgunun koşulu dizinin koşulunu **gerektiriyorsa**
kullanır. Bu, uygulama tarafında somut bir yazım disiplini demektir: kısmi dizinden
yararlanacak sorgu, dizinin koşulunu kendi `WHERE` yan tümcesinde taşımalıdır. Açık
ödünçleri getiren sorgu `iade_tarihi IS NULL` koşulunu yazmıyorsa dizin devrede olmaz.

Kazanç yazma tarafında da vardır ve aynı orandan gelir: kapsanmayan satırların eklenmesi
dizine hiç dokunmaz.

```sh
rm -f y_tam.db y_kismi.db
sqlite3 y_tam.db < kurulum.sql
cp y_tam.db y_kismi.db
sqlite3 y_tam.db   'CREATE INDEX odunc_uye  ON odunc(uye_id);'
sqlite3 y_kismi.db 'CREATE INDEX odunc_acik ON odunc(uye_id) WHERE iade_tarihi IS NULL;'
for db in y_tam.db y_kismi.db; do
  printf '%-11s ' "$db"
  sqlite3 "$db" <<'SQL'
.timer on
INSERT INTO odunc (kitap_id, uye_id, alis_tarihi, iade_tarihi)
WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n < 200000)
SELECT 1 + ((n*7) % 200000), 1 + ((n*13) % 120000),
       date('2024-01-01','+'||(n%300)||' days'),
       date('2024-01-15','+'||(n%300)||' days')
FROM s;
SQL
done
```

{/* mask: (?<=real )[0-9.]+ user [0-9.]+ sys [0-9.]+ */}
```
y_tam.db    Run Time: real 0.366 user 0.195421 sys 0.171062
y_kismi.db  Run Time: real 0.104 user 0.097107 sys 0.007069
```

Eklenen iki yüz bin satırın hepsi iade edilmiş kayıt olduğu için kısmi dizine tek girdi
bile yazılmadı; ekleme bu koşumda yaklaşık 3,5 kat kısa sürdü. Süre değerleri ortama bağlıdır ve
başka bir makinede değişir; kararlı olan, aynı ortamdaki iki koşumun oranıdır.

Kısmi dizinin doğal kullanım alanı buradan çıkar: **dağılımı çok dengesiz olan ve yalnız
küçük tarafı sorgulanan sütunlar.** Açık ödünçler, iptal edilmemiş kayıtlar, işlenmemiş
kuyruk satırları, boş olmayan alanlar. Bunların tersi de doğrudur: sorgular kapsamın her
iki tarafını da soruyorsa kısmi dizin yanıltıcı bir tasarruftur, çünkü ikinci bir tam
dizin gerekecektir.

#### Özet

- Bileşik dizin girdileri önce birinci sütuna göre sıralar; bu yüzden yalnız soldan
  başlayan sütun parçalarına giriş noktası verir — sol ön ek kuralı.
- Aynı iki sütunun iki sırası üç sorguda üç ayrı sonuç verdi: hiçbir sıra üçünde birden
  kazanmadı, sıra bir sorgu kümesi seçimidir.
- Sütun sırası seçiminde eşitlikle süzülen sütunlar başa, aralık ve sıralama sütunları
  sona konur; aralık koşulu kendisinden sonraki sütunların sırasını dağıtır.
- Kısmi dizin yalnız koşulu sağlayan satırları tutar; ölçümde tam dizin 5.738 sayfa
  eklerken kısmi dizin 639 sayfa ekledi ve oran kapsanan satır oranını birebir izledi.
- Kısmi dizin ancak sorgunun koşulu dizinin koşulunu gerektirdiğinde kullanılır; kapsam
  dışı sorgu tam tablo taramasına düşer ve kapsanmayan satırların yazımı dizine hiç
  dokunmaz.

#### Sonraki Adım

Bu derste bir plan satırı iki kez `COVERING` sözcüğüyle çıktı, bir kez çıkmadı. Fark
sessiz görünüyordu ama sayıya yansıdı: aynı dizin, aynı üye, biri 62 diğeri 103 adım.
Aradaki iş, dizinden bulunan her girdi için tabloya dönmektir. Sonraki ders bu dönüşü
ölçer ve ortadan kaldırmanın yolunu kurar: dizine sorgunun istediği bütün sütunları
koymak. Kazancın eşleşen satır sayısıyla nasıl büyüdüğü, hangi sütunların dizine
eklenmeye değdiği ve bu yaklaşımın nerede kendi bedelini aştığı ölçümle görülecek.

### Kapsayan Dizinler

Source: https://academia.sh/tr/kurslar/veritabani-yonetimi/kapsayan-dizinler

Dizinden tabloya dönüşün gerçek bedeli, kapsayan dizinin bu dönüşü ortadan kaldırması, kazancın eşleşen satır sayısıyla büyümesi ve sütun eklendikçe dizinin tablonun kopyasına yaklaşması.

Önceki dersin son ölçümünde aynı dizin ve aynı üye iki ayrı sayı verdi: 62 adım ve 103
adım. Farkı yapan, 103 adımlık sorgunun dizinde bulunmayan bir sütunu (`iade_tarihi`) de
süzmesiydi; plan bu yüzden birinde `COVERING` sözcüğünü taşıyor, diğerinde taşımıyor.

O sözcük tek bir işi anlatır. Dizin, sorgunun istediği sütunların hepsini taşıyorsa
veritabanı yanıtı doğrudan dizinden üretir. Taşımıyorsa, dizinde bulunan her girdi için
tabloya dönüp eksik sütunu okumak zorundadır. Bu ders o dönüşün ne kadar pahalı olduğunu
ölçer — ve ölçünün hangi araçla yapılacağının kendisi de bir bulgu üretir.

#### Dönüşün İzi

Ölçümler kütüphane ödünç kayıtları üzerinde yapılır. Aylık bir rapor sorusu seçilmiştir:
bir ay içinde ödünç alınan kaç ayrı kitap var? Sorgu iki sütuna dokunur — `alis_tarihi`
süzme için, `kitap_id` sayım için.

```sh
rm -f temel.db dar.db kapsayan.db
cat > kurulum.sql <<'SQL'
CREATE TABLE uye (
  uye_id       INTEGER PRIMARY KEY,
  ad           TEXT NOT NULL,
  sehir        TEXT NOT NULL,
  kayit_tarihi TEXT NOT NULL
);
CREATE TABLE odunc (
  odunc_id    INTEGER PRIMARY KEY,
  kitap_id    INTEGER NOT NULL,
  uye_id      INTEGER NOT NULL,
  alis_tarihi TEXT NOT NULL,
  iade_tarihi TEXT
);
INSERT INTO uye (uye_id, ad, sehir, kayit_tarihi)
WITH RECURSIVE sayac(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM sayac WHERE n < 120000)
SELECT n, 'Uye ' || n,
       CASE n % 5 WHEN 0 THEN 'Ankara' WHEN 1 THEN 'Istanbul' WHEN 2 THEN 'Izmir'
                  WHEN 3 THEN 'Bursa' ELSE 'Konya' END,
       date('2015-01-01', '+' || (n % 3200) || ' days')
FROM sayac;
INSERT INTO odunc (odunc_id, kitap_id, uye_id, alis_tarihi, iade_tarihi)
WITH RECURSIVE sayac(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM sayac WHERE n < 2000000)
SELECT n, 1 + ((n * 7) % 200000), 1 + ((n * 13) % 120000),
       date('2018-01-01', '+' || ((n * 37) % 2437) || ' days'),
       CASE WHEN n % 9 = 0 THEN NULL
            ELSE date('2018-01-01', '+' || (((n * 37) % 2437) + 14) || ' days') END
FROM sayac;
SQL
sqlite3 temel.db < kurulum.sql
cp temel.db dar.db
cp temel.db kapsayan.db
sqlite3 dar.db      'CREATE INDEX odunc_alis       ON odunc(alis_tarihi);'
sqlite3 kapsayan.db 'CREATE INDEX odunc_alis_kitap ON odunc(alis_tarihi, kitap_id);'

echo "-- sayfa sayisi --"
for db in temel.db dar.db kapsayan.db; do
  printf '%-13s %s\n' "$db" "$(sqlite3 "$db" 'PRAGMA page_count;')"
done
for db in dar.db kapsayan.db; do
  printf '\n=== %s ===\n' "$db"
  sqlite3 "$db" <<'SQL'
EXPLAIN QUERY PLAN SELECT count(DISTINCT kitap_id) FROM odunc
  WHERE alis_tarihi BETWEEN '2021-03-01' AND '2021-03-31';
.stats vmstep
SELECT count(DISTINCT kitap_id) FROM odunc
  WHERE alis_tarihi BETWEEN '2021-03-01' AND '2021-03-31';
SQL
done
```

```
-- sayfa sayisi --
temel.db      19005
dar.db        28318
kapsayan.db   30226

=== dar.db ===
QUERY PLAN
|--USE TEMP B-TREE FOR count(DISTINCT)
`--SEARCH odunc USING INDEX odunc_alis (alis_tarihi>? AND alis_tarihi<?)
25441
VM-steps: 203543

=== kapsayan.db ===
QUERY PLAN
|--USE TEMP B-TREE FOR count(DISTINCT)
`--SEARCH odunc USING COVERING INDEX odunc_alis_kitap (alis_tarihi>? AND alis_tarihi<?)
25441
VM-steps: 178101
```

Plan farkı beklendiği gibi: ikinci dizin `kitap_id` sütununu da taşıdığı için plan
`COVERING INDEX` diyor. Boyut farkı da beklendiği gibi: kapsayan dizin bir sütun fazla
taşıdığı için veritabanına 1.908 sayfa daha ekledi.

Adım sayısı ise beklentiyi boşa çıkarıyor. 203.543'ten 178.101'e, yalnızca yüzde on iki.
Yirmi beş bin satır için yirmi beş bin tablo dönüşünün maliyeti bu mudur?

#### Doğru Ölçü

Adım sayısı, sorgunun yürüttüğü sanal makine komutlarını sayar. Tablodan bir satır
okumak tek bir komuttur — o komutun arkasında bir disk sayfasının bulunup okunması yatar
ama sayaç bunu görmez. Bir başka deyişle: adım sayısı **yapılan işlemleri** sayar,
**dokunulan sayfaları** değil.

İşlemci Önbelleği dersinde kurulan ayrım burada geri gelir. Aynı sayıda komut, verinin
nerede olduğuna göre yüz kat farklı sürebilir. Ölçünün değiştirilmesi gerekir: sayfa
ıskası sayısı. Aşağıdaki koşum aynı sorguyu üç ayrı genişlikte çalıştırır ve her koşum
için hem sayfa ıskasını hem geçen süreyi yazar. Her sorgu ayrı bir süreçte çalışır, bu
yüzden sayaçlar soğuk önbellekten başlar.

```sh
echo "-- eslesen satir sayilari --"
sqlite3 temel.db "SELECT '1 gun', count(*) FROM odunc WHERE alis_tarihi BETWEEN '2021-03-15' AND '2021-03-15'
UNION ALL SELECT '1 ay', count(*) FROM odunc WHERE alis_tarihi BETWEEN '2021-03-01' AND '2021-03-31'
UNION ALL SELECT '1 yil', count(*) FROM odunc WHERE alis_tarihi BETWEEN '2021-01-01' AND '2021-12-31';"

for aralik in "2021-03-15 2021-03-15" "2021-03-01 2021-03-31" "2021-01-01 2021-12-31"; do
  bas=${aralik% *}; bit=${aralik#* }
  printf '\n### %s .. %s\n' "$bas" "$bit"
  for db in dar.db kapsayan.db; do
    printf '%-13s ' "$db"
    sqlite3 "$db" <<SQL 2>&1 | grep -E 'Page cache misses|Run Time' | tr '\n' ' '
.stats on
.timer on
SELECT count(DISTINCT kitap_id) FROM odunc WHERE alis_tarihi BETWEEN '$bas' AND '$bit';
SQL
    echo
  done
done
```

{/* mask: (?<=real )[0-9.]+ user [0-9.]+ sys [0-9.]+ */}
```
-- eslesen satir sayilari --
1 gun|820
1 ay|25441
1 yil|299547

### 2021-03-15 .. 2021-03-15
dar.db        Page cache misses:                   874 Run Time: real 0.002 user 0.001034 sys 0.000764 
kapsayan.db   Page cache misses:                   8 Run Time: real 0.000 user 0.000000 sys 0.000224 

### 2021-03-01 .. 2021-03-31
dar.db        Page cache misses:                   26960 Run Time: real 0.039 user 0.022426 sys 0.016385 
kapsayan.db   Page cache misses:                   146 Run Time: real 0.007 user 0.005727 sys 0.000947 

### 2021-01-01 .. 2021-12-31
dar.db        Page cache misses:                   317372 Run Time: real 0.426 user 0.202033 sys 0.224197 
kapsayan.db   Page cache misses:                   1684 Run Time: real 0.074 user 0.073163 sys 0.001029 
```

Sayılar tek bir örüntüyü gösteriyor. Dar dizinde sayfa ıskası sayısı, eşleşen satır
sayısını yaklaşık birebir izliyor: 820 satır için 874 sayfa, 25.441 satır için 26.960
sayfa, 299.547 satır için 317.372 sayfa. Kapsayan dizinde aynı üç sayı 8, 146 ve 1.684.

Oranın kaynağı fiziksel depolama düzenidir. Dizin, tarihe göre sıralı olduğu için
eşleşen girdiler yan yanadır ve bir sayfa okuması yüzlerce girdi getirir. Tabloya dönüş
ise `odunc_id` sırasına göre dizilmiş bir yapıda **rastgele** noktalara gitmektir; art
arda gelen iki dizin girdisinin satırları tabloda birbirinden uzaktır. Her satır için
ayrı bir sayfa okunur, aynı sayfa birden çok kez okunur ve tampon havuzu bu erişimi
tutamaz.

Süre bunu doğruluyor: yıllık sorguda 0,426 saniyeye karşı 0,074 saniye, altı kata yakın.
Süreler ortama bağlıdır ve başka bir makinede değişir; kararlı olan, sayfa ıskası
oranıdır — yıllık sorguda 317.372 / 1.684 ≈ 188.

Buradan ölçüm disiplininin bir kuralı çıkar: bir plan değişikliğinin etkisi
değerlendirilirken, ölçünün değişen işi görüyor olması gerekir. Adım sayısı sorgunun
mantığını iyi ölçer, disk erişimini ölçmez.

#### Kapsamanın Sınırı

Kapsayan dizin bedelsiz değildir. Dizine konan her sütun, iki milyon girdinin her birinde
tekrarlanır. Aşağıdaki ölçüm aynı dizini dört ayrı genişlikte kurar ve her birinin
kapladığı sayfa sayısını tablonun kendisiyle karşılaştırır.

```sh
rm -f genislik.db
sqlite3 genislik.db < kurulum.sql
sqlite3 genislik.db <<'SQL'
.mode column
.headers on
CREATE INDEX d1 ON odunc(alis_tarihi);
CREATE INDEX d2 ON odunc(alis_tarihi, kitap_id);
CREATE INDEX d3 ON odunc(alis_tarihi, kitap_id, uye_id);
CREATE INDEX d4 ON odunc(alis_tarihi, kitap_id, uye_id, iade_tarihi);
SELECT name AS nesne, count(*) AS sayfa FROM dbstat
WHERE name IN ('odunc','d1','d2','d3','d4') GROUP BY name ORDER BY sayfa;
SQL
```

```
nesne  sayfa
-----  -----
d1     9313 
d2     11221
d3     13026
d4     17941
odunc  17948
```

Dört sütunlu dizin 17.941 sayfa; tablonun kendisi 17.948 sayfa. Sorgunun istediği her
sütun dizine eklendiğinde dizin, tablonun sıralanmış bir kopyası hâline gelir. O noktada
kapsama artık bir eniyileme değil, verinin iki kez saklanmasıdır: yer iki katına çıkar,
her yazma iki yapıya birden gider ve yedeğin boyutu büyür.

Karar ölçütü buradan çıkar. Kapsamaya değen sütunlar, **sorgunun sonucunda geçen ama
tablonun geri kalanına göre dar olan** sütunlardır. Bir sayım için gereken tek bir kimlik
sütununu eklemek 1.908 sayfa ödeyip 188 kat sayfa ıskası kazandırdı; uzun bir metin
sütununu eklemek aynı hesabı ters çevirir.

İkinci ölçüt sorgunun genişliğidir. Bir gün süzen sorguda kazanç 874 sayfadan 8 sayfaya —
mutlak değeri küçük bir tasarruf. Aynı dizin yıllık raporda üç yüz bin sayfa okumayı
engelledi. Kapsayan dizin, **çok satır eşleştiren ve az sütun isteyen** sorgular için
kurulur; tek satır getiren aramalarda kurulmasına gerek yoktur, çünkü orada dönüş zaten
tek sayfadır.

#### Kapsama ve Sıra Birlikte

Kapsayan dizinin sütun listesi iki işi birden yapar ve bu iki iş çakışabilir. Önceki
derste kurulan sol ön ek kuralı süzme ve sıralama için sütun sırasını belirliyordu;
kapsama ise yalnız sütunun **var olmasını** ister, sırasını değil.

İki gereksinim bir listede toplanır: süzme ve sıralama sütunları sırası önemli olacak
biçimde başa, yalnız sonuçta geçtiği için eklenen sütunlar sona. `(alis_tarihi, kitap_id)`
dizini tam olarak budur — tarih giriş noktasını verir, kitap kimliği yalnız taşınır.
Ters sıra, `(kitap_id, alis_tarihi)`, aynı sütunları taşır ve aynı yeri kaplar ama tarih
aralığına giriş noktası vermez.

Bazı motorlar bu ayrımı sözdizimine taşır ve yalnız taşınacak sütunları dizin anahtarının
dışında tutan bir yazım sunar; böyle bir sütun benzersizlik denetimine katılmaz ve
sıralamada yer tutmaz. Bu yazımın adı ve varlığı motora özgüdür; kavram aynıdır — anahtar
olan sütunlarla yalnız taşınan sütunların ayrılması.

#### Özet

- Kapsayan dizin, sorgunun istediği bütün sütunları taşıyan dizindir; plan çıktısında
  `COVERING` sözcüğüyle görünür ve tabloya hiç dönülmediğini bildirir.
- Tabloya dönüşün maliyeti adım sayısında görünmez: aylık sorguda adım sayısı yüzde on iki
  düşerken sayfa ıskası 26.960'tan 146'ya indi.
- Sayfa ıskası dar dizinde eşleşen satır sayısını birebir izler, çünkü tabloya dönüş
  rastgele erişimdir; kapsayan dizinde okuma dizin içinde sıralı ilerler.
- Kazanç eşleşen satır sayısıyla büyür: bir günlük sorguda 874 sayfaya karşı 8 sayfa,
  yıllık sorguda 317.372 sayfaya karşı 1.684 sayfa.
- Dizine sütun eklendikçe dizin tablonun kopyasına yaklaşır; dört sütunlu dizin 17.941
  sayfa, tablonun kendisi 17.948 sayfa oldu.

#### Sonraki Adım

Buraya kadarki üç ders dizini bir kazanç olarak ele aldı ve bedelini yalnız kurulduğu
andaki yerle ölçtü. Dizin kurulduğu anda kalmaz: satırlar silindikçe ve güncellendikçe
dizin ağacında boşluklar birikir, dosya büyür, her yeni dizin yazma yolunu bir kat daha
uzatır. Sonraki ders bu tarafı ölçer — silme sonrası dizinin ne kadar şiştiği, yeniden
oluşturmanın neyi geri kazandırdığı ve tabloya eklenen her dizinin ekleme süresini ne
kadar uzattığı.

### Dizin Bakımı

Source: https://academia.sh/tr/kurslar/veritabani-yonetimi/dizin-bakimi

Silme ve güncellemenin dizin sayfalarında bıraktığı boşluk, dizin sırasının bu boşluğun geri gelip gelmemesini belirlemesi, yeniden oluşturma ile tam yeniden yazmanın farkı ve yazma maliyetinin dizin sayısıyla artışı.

Buraya kadarki üç ders dizini bir kazanç olarak ele aldı; bedelini yalnız kurulduğu
andaki yerle ölçtü. Kurulduğu andaki yer, bir dizinin ömrü boyunca kapladığı yerin en
küçük değeridir.

Ölü Satır Temizliği dersinde tablo için ölçülen şişme dizinlerde de olur ve iki nedenle
daha inatçıdır. Bir satır silindiğinde o satırın her dizindeki girdisi de ölür. Bir sütun
güncellendiğinde ise o sütunun dizinindeki girdi yerinde kalmaz — eski konumdan silinip
yeni konuma yazılır, yani tek bir güncelleme dizinde iki iş yapar. Bu ders o iki işin
bıraktığı izi ölçer ve bakımın hangi aracının neyi geri kazandırdığını ayırt eder.

#### Şişmenin Ölçülmesi

Aşağıdaki koşum kütüphane ödünç kayıtları üzerinde beş aşamayı aynı ölçülerle izler:
satır sayısı, tablonun sayfa sayısı, iki dizinin ayrı ayrı sayfa sayısı, veritabanının
toplam sayfa sayısı ve dosya boyutu.

```sh
rm -f bakim.db
cat > kurulum.sql <<'SQL'
CREATE TABLE uye (
  uye_id       INTEGER PRIMARY KEY,
  ad           TEXT NOT NULL,
  sehir        TEXT NOT NULL,
  kayit_tarihi TEXT NOT NULL
);
CREATE TABLE odunc (
  odunc_id    INTEGER PRIMARY KEY,
  kitap_id    INTEGER NOT NULL,
  uye_id      INTEGER NOT NULL,
  alis_tarihi TEXT NOT NULL,
  iade_tarihi TEXT
);
INSERT INTO uye (uye_id, ad, sehir, kayit_tarihi)
WITH RECURSIVE sayac(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM sayac WHERE n < 120000)
SELECT n, 'Uye ' || n,
       CASE n % 5 WHEN 0 THEN 'Ankara' WHEN 1 THEN 'Istanbul' WHEN 2 THEN 'Izmir'
                  WHEN 3 THEN 'Bursa' ELSE 'Konya' END,
       date('2015-01-01', '+' || (n % 3200) || ' days')
FROM sayac;
INSERT INTO odunc (odunc_id, kitap_id, uye_id, alis_tarihi, iade_tarihi)
WITH RECURSIVE sayac(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM sayac WHERE n < 2000000)
SELECT n, 1 + ((n * 7) % 200000), 1 + ((n * 13) % 120000),
       date('2018-01-01', '+' || ((n * 37) % 2437) || ' days'),
       CASE WHEN n % 9 = 0 THEN NULL
            ELSE date('2018-01-01', '+' || (((n * 37) % 2437) + 14) || ' days') END
FROM sayac;
SQL
sqlite3 bakim.db < kurulum.sql

cat > olcu.sql <<'EOF'
SELECT (SELECT count(*) FROM odunc) AS satir,
       (SELECT count(*) FROM dbstat WHERE name='odunc') AS tablo_sayfa,
       (SELECT count(*) FROM dbstat WHERE name='odunc_uye') AS uye_dizin,
       (SELECT count(*) FROM dbstat WHERE name='odunc_alis') AS alis_dizin,
       (SELECT page_count FROM pragma_page_count) AS toplam;
EOF
sqlite3 bakim.db <<'SQL'
.mode column
.headers on
CREATE INDEX odunc_uye  ON odunc(uye_id);
CREATE INDEX odunc_alis ON odunc(alis_tarihi);
.print '--- kurulus ---'
.read olcu.sql
.shell echo "dosya: $(wc -c < bakim.db)"
DELETE FROM odunc WHERE alis_tarihi < '2020-01-01';
.print '--- eski kayitlar silindi ---'
.read olcu.sql
.shell echo "dosya: $(wc -c < bakim.db)"
UPDATE odunc SET alis_tarihi = date(alis_tarihi, '+900 days') WHERE odunc_id % 3 = 0;
.print '--- ucte birinin tarihi guncellendi ---'
.read olcu.sql
.shell echo "dosya: $(wc -c < bakim.db)"
REINDEX;
.print '--- REINDEX sonrasi ---'
.read olcu.sql
.shell echo "dosya: $(wc -c < bakim.db)"
VACUUM;
.print '--- VACUUM sonrasi ---'
.read olcu.sql
.shell echo "dosya: $(wc -c < bakim.db)"
SQL
```

```
--- kurulus ---
satir    tablo_sayfa  uye_dizin  alis_dizin  toplam
-------  -----------  ---------  ----------  ------
2000000  17948        5738       9313        34056 
dosya: 139493376
--- eski kayitlar silindi ---
satir    tablo_sayfa  uye_dizin  alis_dizin  toplam
-------  -----------  ---------  ----------  ------
1400893  17948        5738       6525        34056 
dosya: 139493376
--- ucte birinin tarihi guncellendi ---
satir    tablo_sayfa  uye_dizin  alis_dizin  toplam
-------  -----------  ---------  ----------  ------
1400893  17948        5738       8102        34056 
dosya: 139493376
--- REINDEX sonrasi ---
satir    tablo_sayfa  uye_dizin  alis_dizin  toplam
-------  -----------  ---------  ----------  ------
1400893  17948        4022       6524        34056 
dosya: 139493376
--- VACUUM sonrasi ---
satir    tablo_sayfa  uye_dizin  alis_dizin  toplam
-------  -----------  ---------  ----------  ------
1400893  12565        4022       6524        24168 
dosya: 98992128
```

Beş satırlık bu tablo dersin bütün bulgularını taşıyor. Aşağıdaki bölümler onu üç
soruya ayırarak okur: silme iki dizinde neden farklı davrandı, yeniden oluşturma ile tam
yeniden yazma neyi geri kazandırdı ve toplam sayfa sayısı hangi aşamada değişti.

#### İki Dizin, İki Ayrı Davranış

İkinci aşama tek başına dersin ana bulgusudur. Satırların yaklaşık yüzde otuzu silindi.
Tarih dizini 9.313 sayfadan 6.525 sayfaya indi — silinen orana yakın bir düşüş. Üye
dizini ise 5.738 sayfada **kaldı**; tek bir sayfa bile geri gelmedi.

İki dizin aynı satırları kaybetti; farkı yaratan, girdilerin nasıl sıralandığıdır. Silme
koşulu bir tarih aralığıydı ve tarih dizininde o aralığın girdileri yan yanadır: bütün
sayfalar boşaldı ve serbest listeye geçti. Üye dizininde ise aynı satırların girdileri
yüz yirmi bin üye arasına dağılmıştır; hiçbir sayfa tamamen boşalmadı, hepsi kısmen
boşaldı. Bir sayfanın içi boşalması onu geri vermez.

Kural, tablolar için Ölü Satır Temizliği dersinde kurulan kuralın dizin karşılığıdır:
**silme ölçütü dizin sırasıyla örtüşüyorsa yer geri gelir, örtüşmüyorsa gelmez.** Aynı
silme işlemi, bir dizinde tasarruf, diğerinde ölü boşluk üretir.

Üçüncü aşama güncellemenin dizindeki ikinci işini gösteriyor. Satırların üçte birinin
tarihi ileri alındı; tablonun sayfa sayısı değişmedi, tarih dizini 6.525'ten 8.102
sayfaya çıktı. Silinen satır yok, eklenen satır yok — yalnız dizin girdileri eski
konumlarından yeni konumlarına taşındı ve eski konumların bıraktığı boşluk kapanmadı.
Üye dizini bu aşamada hiç değişmedi (5.738), çünkü güncellenen sütun onun anahtarı değil.

#### Yeniden Oluşturma ve Tam Yeniden Yazma

Dördüncü aşama `REINDEX` çalıştırıyor: dizinler baştan kuruluyor. Üye dizini 5.738'den
4.022 sayfaya iniyor — canlı satır oranına (1.400.893 / 2.000.000 ≈ 0,70) tam olarak
uyan bir düşüş, çünkü ölü boşluk kalmadı. Tarih dizini 8.102'den 6.524'e iniyor, yani
silme sonrasındaki değerine (6.525) bir sayfa farkla.

Dikkat edilecek olan, aynı satırdaki toplam sayfa sayısıdır: 34.056, hiç değişmedi. Dosya
boyutu da değişmedi. Yeniden oluşturma boşluğu **veritabanının içinde** serbest bıraktı;
işletim sistemine geri vermedi. Bu boşluk yeni veri için kullanılabilir, ama disk
doluluğunu düşürmez ve yedek alma süresini kısaltmaz.

Beşinci aşama tam yeniden yazma çalıştırıyor. Toplam 34.056'dan 24.168 sayfaya, dosya
139.493.376 bayttan 98.992.128 bayta iniyor. Tablo da sıkışıyor (17.948 → 12.565), çünkü
tam yeniden yazma tabloyu ve dizinleri birlikte yeni bir dosyaya kopyalar.

İki aracın ayrımı bir cümlede toplanır: **yeniden oluşturma yalnız dizinleri onarır ve
boşluğu veritabanının içinde bırakır; tam yeniden yazma her şeyi onarır ve boşluğu dosya
sisteminde geri verir.** Bedelleri de bu ayrımı izler. Yeniden oluşturma yalnız ilgili
dizini kilitler ve tablo boyutundan bağımsız olarak dizinin büyüklüğüyle orantılıdır. Tam
yeniden yazma tabloyu kilitler, tablonun boyutu kadar geçici alan ister ve bütün veriyi
okur. Çalışan bir sistemde ilki bir bakım penceresine sığar, ikincisi çoğu zaman
planlanmış bir kesinti gerektirir.

Motorların bir kısmı yeniden oluşturmayı tabloyu yazmaya kapatmadan yapan bir yol sunar:
yeni dizin arka planda kurulur, hazır olduğunda eskisiyle değiştirilir. Bu yolun adı ve
sınırları motora özgüdür; kavram, dizinin **çift kopya** olarak kurulup değiştirilmesidir
ve bedeli, geçici olarak iki dizinin birden yer kaplamasıdır.

#### Yazma Maliyeti Dizin Sayısıyla Artar

Bakımın ikinci yüzü, dizinin var olmasının her yazmaya eklediği maliyettir. Aşağıdaki
koşum aynı iki yüz bin satırı, dizin sayısı sıfırdan dörde çıkan beş kopyaya ekler.

```sh
rm -f sablon.db y0.db y1.db y2.db y3.db y4.db
sqlite3 sablon.db < kurulum.sql
for k in 0 1 2 3 4; do cp sablon.db "y$k.db"; done
sqlite3 y1.db 'CREATE INDEX i1 ON odunc(uye_id);'
sqlite3 y2.db 'CREATE INDEX i1 ON odunc(uye_id); CREATE INDEX i2 ON odunc(alis_tarihi);'
sqlite3 y3.db 'CREATE INDEX i1 ON odunc(uye_id); CREATE INDEX i2 ON odunc(alis_tarihi);
               CREATE INDEX i3 ON odunc(kitap_id);'
sqlite3 y4.db 'CREATE INDEX i1 ON odunc(uye_id); CREATE INDEX i2 ON odunc(alis_tarihi);
               CREATE INDEX i3 ON odunc(kitap_id); CREATE INDEX i4 ON odunc(iade_tarihi);'
for k in 0 1 2 3 4; do
  printf 'dizin sayisi %s: ' "$k"
  sqlite3 "y$k.db" <<'SQL' | grep 'Run Time'
.timer on
INSERT INTO odunc (kitap_id, uye_id, alis_tarihi, iade_tarihi)
WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n < 200000)
SELECT 1 + ((n*7) % 200000), 1 + ((n*13) % 120000),
       date('2024-01-01','+'||(n%300)||' days'),
       CASE WHEN n % 9 = 0 THEN NULL ELSE date('2024-01-20','+'||(n%300)||' days') END
FROM s;
SQL
done
```

{/* mask: (?<=real )[0-9.]+ user [0-9.]+ sys [0-9.]+ */}
```
dizin sayisi 0: Run Time: real 0.086 user 0.085793 sys 0.000638
dizin sayisi 1: Run Time: real 0.319 user 0.196078 sys 0.122217
dizin sayisi 2: Run Time: real 0.644 user 0.327066 sys 0.317115
dizin sayisi 3: Run Time: real 0.854 user 0.457828 sys 0.395828
dizin sayisi 4: Run Time: real 1.152 user 0.574279 sys 0.577455
```

Artış doğrusala yakın: her dizin bu koşumda ekleme süresine yaklaşık 0,27 saniye ekliyor
ve dört dizinli kopya dizinsiz kopyanın on üç katından fazla sürüyor. Süreler ortama bağlıdır;
kararlı olan, maliyetin dizin sayısıyla orantılı artmasıdır. Nedeni doğrudandır: her yeni
satır için her dizin ağacına ayrı bir anahtar yazılır.

Güncellemede hesap farklıdır — yalnız **güncellenen sütunu içeren** dizinler bedel öder.
Aşağıdaki koşum ikisi de iki dizinli iki kopyada aynı güncellemeyi çalıştırır; tek fark,
güncellenen sütunun bir kopyada dizinli olmasıdır.

```sh
rm -f g_disi.db g_ici.db
cp sablon.db g_disi.db
cp sablon.db g_ici.db
sqlite3 g_disi.db 'CREATE INDEX i1 ON odunc(uye_id); CREATE INDEX i2 ON odunc(kitap_id);'
sqlite3 g_ici.db  'CREATE INDEX i1 ON odunc(uye_id); CREATE INDEX i2 ON odunc(alis_tarihi);'
for db in g_disi.db g_ici.db; do
  printf '%-10s ' "$db"
  sqlite3 "$db" <<'SQL' | grep 'Run Time'
.timer on
UPDATE odunc SET alis_tarihi = date(alis_tarihi, '+1 day') WHERE odunc_id % 5 = 0;
SQL
done
```

{/* mask: (?<=real )[0-9.]+ user [0-9.]+ sys [0-9.]+ */}
```
g_disi.db  Run Time: real 0.159 user 0.100926 sys 0.057693
g_ici.db   Run Time: real 1.765 user 0.741978 sys 1.022017
```

Aynı satır sayısı, aynı dizin sayısı, on bir kat süre farkı. Ayrımın pratik karşılığı şudur:
sık güncellenen bir sütunu dizinlemenin bedeli, hiç güncellenmeyen bir sütunu
dizinlemenin bedeliyle aynı değildir. Dizin seçilirken sütunun yalnız nasıl sorgulandığına
değil, ne sıklıkta değiştiğine de bakılır.

#### Ödenmiş Ama Karşılığı Alınmamış Dizinler

Bakımın son işi, var olmaması gereken dizinleri bulmaktır. Sistem Kataloğu dersinde
kurulan kalıp burada ikinci kez kullanılır: sütun listesi başka bir dizinin sol ön eki
olan dizin, sol ön ek kuralı gereği hiçbir sorguya tek başına gerekli değildir.

```sh
rm -f denetim.db
cp sablon.db denetim.db
sqlite3 denetim.db <<'SQL'
.mode column
.headers on
CREATE INDEX odunc_uye       ON odunc(uye_id);
CREATE INDEX odunc_uye_tarih ON odunc(uye_id, alis_tarihi);
CREATE INDEX odunc_tarih     ON odunc(alis_tarihi);
CREATE INDEX odunc_kitap     ON odunc(kitap_id);
-- Sutun listesi baska bir dizinin on eki olan dizinler: yeri bosuna kaplarlar.
WITH d AS (
  SELECT m.name AS tablo, il.name AS dizin,
         (SELECT group_concat(ii.name, ',') FROM pragma_index_info(il.name) ii) AS sutunlar
  FROM sqlite_schema m JOIN pragma_index_list(m.name) il
  WHERE m.type = 'table' AND il.origin = 'c'
)
SELECT a.dizin AS gereksiz, a.sutunlar AS sutunlari, b.dizin AS kapsayan,
       (SELECT count(*) FROM dbstat WHERE name = a.dizin) AS sayfa
FROM d a JOIN d b ON a.tablo = b.tablo AND a.dizin <> b.dizin
WHERE b.sutunlar LIKE a.sutunlar || ',%';
SQL
```

```
gereksiz   sutunlari  kapsayan         sayfa
---------  ---------  ---------------  -----
odunc_uye  uye_id     odunc_uye_tarih  5738
```

Denetim tek bulgu üretti ve bedelini de yazdı: `uye_id` üzerindeki tek sütunlu dizin,
`(uye_id, alis_tarihi)` dizininin sol ön eki olduğu için gereksizdir ve 5.738 sayfa yer
kaplamaktadır. Kaldırılması hem o yeri hem her yazmada ödenen payı geri kazandırır.

Bu denetimin bulamayacağı ikinci bir kategori vardır: sütun listesi eşsiz olduğu hâlde
hiçbir sorgunun kullanmadığı dizinler. Onlar katalogdan değil, motorun tuttuğu kullanım
sayaçlarından bulunur; sayaçların adı ve varlığı motora özgüdür. Ortak yöntem şudur:
sayaç sıfırlanır, bir iş döngüsü boyunca beklenir, sonra hiç artmayan dizinler aday
listesine alınır. Karar hemen verilmez — ayda bir çalışan bir rapor, bir haftalık gözlemde
görünmez.

#### Özet

- Silme ve güncelleme dizinlerde boşluk bırakır; güncelleme dizinde iki iş yapar, eski
  konumdan siler ve yeni konuma yazar.
- Yerin geri gelip gelmeyeceğini dizin sırası belirler: tarih aralığıyla silmede tarih
  dizini 9.313'ten 6.525 sayfaya indi, aynı silmede üye dizini 5.738 sayfada kaldı.
- Yeniden oluşturma dizinleri onarır ve boşluğu veritabanının içinde bırakır (toplam 34.056
  sayfa değişmedi); tam yeniden yazma dosyayı da küçültür (24.168 sayfa, 98.992.128 bayt).
- Ekleme maliyeti dizin sayısıyla orantılı artar; ölçümde her dizin yaklaşık 0,27 saniye
  ekledi ve dört dizinli kopya dizinsizin on üç katından fazla sürdü.
- Güncellemede yalnız güncellenen sütunu içeren dizinler bedel öder: aynı dizin sayısıyla
  fark on bir kat çıktı; sol ön ek denetimi ise 5.738 sayfalık gereksiz bir dizini buldu.

#### Sonraki Adım

Bu derste iki kez aynı olguya çarpıldı: silme ölçütü dizin sırasıyla örtüştüğünde yer
kendiliğinden geri geldi, örtüşmediğinde bakım gerekti. Aynı olgunun tablo tarafındaki
karşılığı Ölü Satır Temizliği dersinde ölçülmüş ve orada bir soru askıda bırakılmıştı:
eski verinin silinmesi yerine, eski verinin **ayrı bir yerde** durması sağlanabilir mi?
Sonraki ders bu soruyu yanıtlar. Bir tablo aralığa, listeye ya da karmaya göre bölümlere
ayrıldığında sorgunun yalnız ilgili bölüme dokunduğu okunan sayfa sayısıyla gösterilecek, eski
verinin silinmesi bir bölümün kaldırılmasına dönüşecek ve bölüm anahtarının dağılımı sayıyla
ölçülecek.

### Tablo Bölümleme

Source: https://academia.sh/tr/kurslar/veritabani-yonetimi/tablo-bolumleme

Bir tablonun aralığa, listeye ve karmaya göre parçalara ayrılması, sorgunun yalnız ilgili bölüme dokunduğunun ölçümle gösterilmesi, eski verinin silinmek yerine düşürülmesi ve bölüm anahtarının dağılımının hesaplanması.

Dizin Bakımı dersi bir soruyu askıda bıraktı. Eski ödünç kayıtlarını silmek, dizin
sırasıyla örtüşmediği her yerde ölü boşluk bıraktı ve o boşluğu geri almak tabloyu baştan
yazmayı gerektirdi. Sorunun kaynağı silmenin kendisi değil, silinecek verinin canlı
verinin arasına karışmış olmasıdır.

**Bölümleme (partitioning)**, bu karışmayı baştan önler: mantıksal olarak tek bir tablo,
fiziksel olarak birden çok parçada saklanır. Her parçaya **bölüm (partition)** denir ve
bir satırın hangi bölüme düşeceğini **bölüm anahtarı (partition key)** belirler. Bu ders
üç bölümleme biçimini kurar, kazancını ölçer ve kazancın nereden gelmediğini de gösterir.

#### Aralık Bölümlemesi

En yaygın biçim **aralık bölümlemesidir (range partitioning)**: bölüm anahtarının değer
aralıkları bölümlere karşılık gelir. Kütüphane kayıtlarında doğal anahtar alış tarihidir
ve doğal aralık yıldır.

Bazı motorlar bölümlemeyi sözdizimiyle sunar; sunmayanlarda aynı yapı ayrı tablolar ve
bir birleşim görünümüyle kurulur. Aşağıdaki koşum ikinci yolu izler. Her bölüm tablosu
kendi aralığını bir değer denetimiyle bildirir ve bir görünüm bölümleri tek bir ad altında
toplar.

```sh
rm -f bolumsuz.db bolumlu.db bolumle.sql
cat > kurulum.sql <<'SQL'
CREATE TABLE odunc (
  odunc_id    INTEGER PRIMARY KEY,
  kitap_id    INTEGER NOT NULL,
  uye_id      INTEGER NOT NULL,
  alis_tarihi TEXT NOT NULL,
  iade_tarihi TEXT
);
INSERT INTO odunc (odunc_id, kitap_id, uye_id, alis_tarihi, iade_tarihi)
WITH RECURSIVE sayac(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM sayac WHERE n < 2000000)
SELECT n, 1 + ((n * 7) % 200000), 1 + ((n * 13) % 120000),
       date('2018-01-01', '+' || ((n * 37) % 2437) || ' days'),
       CASE WHEN n % 9 = 0 THEN NULL
            ELSE date('2018-01-01', '+' || (((n * 37) % 2437) + 14) || ' days') END
FROM sayac;
SQL
sqlite3 bolumsuz.db < kurulum.sql
sqlite3 bolumsuz.db 'CREATE INDEX odunc_alis ON odunc(alis_tarihi);'

# Yil basina bir bolum tablosu, deger denetimi ve bolumleri toplayan gorunum.
{
  echo "ATTACH 'bolumsuz.db' AS kaynak;"
  for y in 2018 2019 2020 2021 2022 2023 2024; do
    s=$((y + 1))
    cat <<EOF
CREATE TABLE odunc_$y (
  odunc_id    INTEGER PRIMARY KEY,
  kitap_id    INT NOT NULL,
  uye_id      INT NOT NULL,
  alis_tarihi TEXT NOT NULL CHECK (alis_tarihi >= '$y-01-01' AND alis_tarihi < '$s-01-01'),
  iade_tarihi TEXT);
INSERT INTO odunc_$y SELECT * FROM kaynak.odunc
  WHERE alis_tarihi >= '$y-01-01' AND alis_tarihi < '$s-01-01';
CREATE INDEX odunc_${y}_alis ON odunc_$y(alis_tarihi);
EOF
  done
  echo "CREATE VIEW odunc AS"
  echo "  SELECT * FROM odunc_2018 UNION ALL SELECT * FROM odunc_2019 UNION ALL"
  echo "  SELECT * FROM odunc_2020 UNION ALL SELECT * FROM odunc_2021 UNION ALL"
  echo "  SELECT * FROM odunc_2022 UNION ALL SELECT * FROM odunc_2023 UNION ALL"
  echo "  SELECT * FROM odunc_2024;"
} > bolumle.sql
sqlite3 bolumlu.db < bolumle.sql
sqlite3 bolumlu.db <<'SQL'
.mode column
.headers on
SELECT count(*) AS gorunum_satir FROM odunc;
SELECT name AS bolum, count(*) AS sayfa FROM dbstat
WHERE name LIKE 'odunc\_20__' ESCAPE '\' GROUP BY name ORDER BY name;
SQL
```

```
gorunum_satir
-------------
2000000      
bolum       sayfa
----------  -----
odunc_2018  2967 
odunc_2019  2954 
odunc_2020  2952 
odunc_2021  2951 
odunc_2022  2947 
odunc_2023  2957 
odunc_2024  1984 
```

Görünüm iki milyon satırın hepsini veriyor: uygulama tarafından bakıldığında tablo hâlâ
tektir. Altında yedi ayrı tablo, yedi ayrı dizin ve her biri yaklaşık üç bin sayfalık yedi
ayrı ağaç vardır.

#### Yalnız İlgili Bölüme Dokunmak

Bölümlemenin ilk iddiası şudur: bölüm anahtarını süzen bir sorgu diğer bölümleri hiç
okumaz. Bu iddia plan çıktısında **görünmez** — plan yedi bölümü de listeler.

```sh
sqlite3 bolumlu.db "EXPLAIN QUERY PLAN
SELECT count(*) FROM odunc WHERE alis_tarihi BETWEEN '2021-03-01' AND '2021-03-31';"

for hedef in "bolumlu.db|odunc|bolumlu gorunum" \
             "bolumlu.db|odunc_2021|dogrudan 2021 bolumu" \
             "bolumsuz.db|odunc|bolumsuz tek tablo"; do
  db=${hedef%%|*}; kalan=${hedef#*|}; tablo=${kalan%%|*}; ad=${kalan#*|}
  printf '%-22s ' "$ad"
  sqlite3 "$db" <<SQL 2>&1 | grep -E 'Page cache misses|Run Time' | tr '\n' ' '
.stats on
.timer on
SELECT count(*) FROM $tablo WHERE alis_tarihi BETWEEN '2021-03-01' AND '2021-03-31';
SQL
  echo
done
```

{/* mask: (?<=real )[0-9.]+ user [0-9.]+ sys [0-9.]+ */}
```
QUERY PLAN
|--CO-ROUTINE odunc
|  `--COMPOUND QUERY
|     |--LEFT-MOST SUBQUERY
|     |  `--SEARCH odunc_2018 USING INDEX odunc_2018_alis (alis_tarihi>? AND alis_tarihi<?)
|     |--UNION ALL
|     |  `--SEARCH odunc_2019 USING INDEX odunc_2019_alis (alis_tarihi>? AND alis_tarihi<?)
|     |--UNION ALL
|     |  `--SEARCH odunc_2020 USING INDEX odunc_2020_alis (alis_tarihi>? AND alis_tarihi<?)
|     |--UNION ALL
|     |  `--SEARCH odunc_2021 USING INDEX odunc_2021_alis (alis_tarihi>? AND alis_tarihi<?)
|     |--UNION ALL
|     |  `--SEARCH odunc_2022 USING INDEX odunc_2022_alis (alis_tarihi>? AND alis_tarihi<?)
|     |--UNION ALL
|     |  `--SEARCH odunc_2023 USING INDEX odunc_2023_alis (alis_tarihi>? AND alis_tarihi<?)
|     `--UNION ALL
|        `--SEARCH odunc_2024 USING INDEX odunc_2024_alis (alis_tarihi>? AND alis_tarihi<?)
`--SCAN odunc
bolumlu gorunum        Page cache misses:                   139 Run Time: real 0.002 user 0.002315 sys 0.000000 
dogrudan 2021 bolumu   Page cache misses:                   121 Run Time: real 0.001 user 0.000000 sys 0.000757 
bolumsuz tek tablo     Page cache misses:                   123 Run Time: real 0.001 user 0.000000 sys 0.000800
```

Sayfa ıskası sayıları iddiayı doğruluyor. Yalnız 2021 bölümüne doğrudan sorulan sorgu 121
sayfa okudu; görünüm üzerinden sorulan sorgu 139 sayfa. Aradaki 18 sayfa, altı bölümün
dizinine yapılan altı boş yoklamadır — bölüm başına üç sayfa. Yedi bölümün altısı planda
görünüyor ama **verisi hiç okunmuyor**.

Ayrımın adı **bölüm ayıklamasıdır (partition pruning)**. Yerleşik bölümleme sunan
motorlarda ayıklama plan kurulurken yapılır ve ilgisiz bölümler plana hiç girmez; buradaki
modelde çalışma zamanında oluyor, çünkü her boş yoklama kendi dizininde hemen sonuçsuz
kalıyor. İki yolun sonucu yakındır, maliyeti değil: bölüm sayısı yüzlere çıktığında yüzlerce
boş yoklamanın toplamı ihmal edilemez olur.

Üçüncü satır ise beklentiyi düzeltir. Bölümsüz tek tablo aynı sorguyu 123 sayfayla
yanıtladı; bölüm sorgusundan yalnız iki sayfa fazla. Dizin varken bölümleme **okuma başarımı kazandırmaz**; dizin zaten aynı işi
yapıyordu. Bölümlemenin kazancı başka yerdedir.

#### Eski Veriyi Silmek ve Bölümü Düşürmek

Asıl kazanç, verinin bütün olarak kaldırılabilmesidir. Aşağıdaki koşum aynı işi iki
kurulumda yapar: 2018 yılının kayıtlarını sistemden çıkarmak.

```sh
rm -f s1.db s2.db
cp bolumsuz.db s1.db
cp bolumlu.db  s2.db
olc() { printf '%-8s dosya=%s  toplam_sayfa=%s  serbest=%s\n' "$2" "$(wc -c < "$1")" \
  "$(sqlite3 "$1" 'PRAGMA page_count;')" "$(sqlite3 "$1" 'PRAGMA freelist_count;')"; }

echo "=== bolumsuz: eski yili silme ==="
olc s1.db once
sqlite3 s1.db <<'SQL' | grep 'Run Time'
.timer on
DELETE FROM odunc WHERE alis_tarihi < '2019-01-01';
SQL
olc s1.db silme
sqlite3 s1.db <<'SQL' | grep 'Run Time'
.timer on
VACUUM;
SQL
olc s1.db vacuum

echo
echo "=== bolumlu: eski bolumu dusurme ==="
olc s2.db once
sqlite3 s2.db <<'SQL' | grep 'Run Time'
.timer on
DROP VIEW odunc;
DROP TABLE odunc_2018;
CREATE VIEW odunc AS
  SELECT * FROM odunc_2019 UNION ALL SELECT * FROM odunc_2020 UNION ALL
  SELECT * FROM odunc_2021 UNION ALL SELECT * FROM odunc_2022 UNION ALL
  SELECT * FROM odunc_2023 UNION ALL SELECT * FROM odunc_2024;
SQL
olc s2.db dusurme
sqlite3 s2.db "SELECT count(*) FROM odunc;"
```

{/* mask: (?<=real )[0-9.]+ user [0-9.]+ sys [0-9.]+ */}
```
=== bolumsuz: eski yili silme ===
once     dosya=111665152  toplam_sayfa=27262  serbest=0
Run Time: real 0.643 user 0.214861 sys 0.427962
silme    dosya=111665152  toplam_sayfa=27262  serbest=1393
Run Time: real 0.267 user 0.139513 sys 0.127755
vacuum   dosya=94892032  toplam_sayfa=23167  serbest=0

=== bolumlu: eski bolumu dusurme ===
once     dosya=118927360  toplam_sayfa=29035  serbest=0
Run Time: real 0.001 user 0.000000 sys 0.000149
Run Time: real 0.019 user 0.004862 sys 0.014623
Run Time: real 0.000 user 0.000027 sys 0.000087
dusurme  dosya=118927360  toplam_sayfa=29035  serbest=4363
1700444
```

İki yol aynı 299.556 satırı sistemden çıkardı; maliyetleri karşılaştırılamaz. Silme yolu
0,643 saniye sürdü ve yalnızca 1.393 sayfayı serbest bırakabildi — kalan boşluk sayfaların
içine dağılmıştı. Boşluğu geri almak için çalıştırılan tam yeniden yazma 0,267 saniye daha
aldı ve bu süre boyunca tablo kilitliydi. Toplam 0,91 saniyeyi buldu.

Bölümü düşürme yolu 0,019 saniye sürdü ve 4.363 sayfayı tek işlemde serbest bıraktı.
Yapılan iş satır silmek değil, bir nesneyi kaldırmaktır: maliyet satır sayısıyla değil,
sabit bir katalog işlemiyle ölçülür. Süreler ortama bağlıdır, yukarıdaki değerler örnek bir koşumdan alınmıştır; kararlı olan, birinin
veriyle orantılı, diğerinin veriden bağımsız olmasıdır.

Bu, saklama süresi kuralı olan her sistemde bölümlemenin ana gerekçesidir. Aynı gerekçe
ters yönde de çalışır: yeni bir dönem başladığında boş bir bölüm eklemek de sabit
maliyetlidir.

#### Liste ve Karma Bölümlemesi

İki biçim daha vardır. **Liste bölümlemesinde (list partitioning)** her bölüm, anahtarın
belirli değerlerini alır — şehir, şube, ülke. **Karma bölümlemesinde (hash partitioning)**
bölüm, anahtarın bir karma fonksiyonundan geçirilip bölüm sayısına bölünmesiyle bulunur;
amaç anlamlı bir gruplama değil, dengeli bir dağıtımdır.

Seçim, bölümlerin ne kadar dengeli dolacağına bağlıdır ve bu hesaplanabilir. Aşağıdaki
model üye başına ödünç sayısını güç yasasına yakın dağıtır — az sayıda üye çok ödünç alır,
çoğu üye birkaç kayıt bırakır — ve bölüm doluluklarını üç anahtar seçimi için sayar.

```sh
cat > dagilim.mjs <<'JS'
// Model: odunc kayitlarinin bolumlere dagilimi. Gercek bir motorun bolumleyicisi degil;
// olculen sey anahtar secimi ile bolum dolulugu arasindaki iliskidir.
// Uye basina odunc sayisi guc yasasina yakin dagitilir: az sayida uye cok odunc alir.
const UYE = 120000, KAYIT = 2000000;
const sehirler = [['Istanbul', 0.42], ['Ankara', 0.24], ['Izmir', 0.16],
                  ['Bursa', 0.11], ['Konya', 0.07]];

function uyeler() {
  const agirlik = [];
  let toplam = 0;
  for (let i = 1; i <= UYE; i++) { const a = 1 / Math.pow(i, 0.8); agirlik.push(a); toplam += a; }
  return agirlik.map((a, i) => {
    let esik = ((i * 7919) % 100) / 100, k = 0, birikim = 0;
    while (k < sehirler.length - 1 && (birikim += sehirler[k][1]) <= esik) k++;
    return { uye_id: i + 1, odunc: Math.max(1, Math.round(a / toplam * KAYIT)), sehir: sehirler[k][0] };
  });
}
const M = 2654435761n;
const karmaDusuk  = (x) => Number((BigInt(x) * M) % 4294967296n);          // dusuk bitler
const karmaYuksek = (x) => Number(((BigInt(x) * M) % 4294967296n) >> 20n); // yuksek bitler

function rapor(ad, kova) {
  const enCok = Math.max(...kova), enAz = Math.min(...kova);
  const bos = kova.filter((s) => s === 0).length;
  const oran = enAz === 0 ? 'bölüm boş: ' + bos : (enCok / enAz).toFixed(2) + '×';
  console.log(ad.padEnd(24), 'bölüm', String(kova.length).padStart(2),
              '| en dolu', String(enCok).padStart(8), '| en boş', String(enAz).padStart(8),
              '| oran', oran);
}
function dagit(uye, P, anahtar) {
  const kova = new Array(P).fill(0);
  for (const u of uye) kova[anahtar(u.uye_id) % P] += u.odunc;
  return kova;
}
const uye = uyeler();
console.log('modeldeki toplam ödünç:', uye.reduce((t, u) => t + u.odunc, 0));
console.log('— karma bölümleme, güç yasasına yakın anahtar —');
for (const P of [8, 16, 64]) {
  rapor('uye_id % P', dagit(uye, P, (x) => x));
  rapor('karma_yuksek % P', dagit(uye, P, karmaYuksek));
}
console.log('— yapılı anahtar: üyelik numarası dörtün katı —');
const dortluk = uye.map((u) => ({ ...u, uye_id: u.uye_id * 4 }));
rapor('uye_id % 8',       dagit(dortluk, 8, (x) => x));
rapor('karma_dusuk % 8',  dagit(dortluk, 8, karmaDusuk));
rapor('karma_yuksek % 8', dagit(dortluk, 8, karmaYuksek));
console.log('— liste bölümleme: şehir —');
const listeKova = sehirler.map(([ad]) =>
  uye.filter((u) => u.sehir === ad).reduce((t, u) => t + u.odunc, 0));
rapor('şehir', listeKova);
console.log(sehirler.map(([ad], i) => `${ad}=${listeKova[i]}`).join('  '));
JS
node dagilim.mjs
```

```
modeldeki toplam ödünç: 2001046
— karma bölümleme, güç yasasına yakın anahtar —
uye_id % P               bölüm  8 | en dolu   278850 | en boş   238066 | oran 1.17×
karma_yuksek % P         bölüm  8 | en dolu   273855 | en boş   233420 | oran 1.17×
uye_id % P               bölüm 16 | en dolu   158156 | en boş   116396 | oran 1.36×
karma_yuksek % P         bölüm 16 | en dolu   154631 | en boş   114336 | oran 1.35×
uye_id % P               bölüm 64 | en dolu    69604 | en boş    27466 | oran 2.53×
karma_yuksek % P         bölüm 64 | en dolu    69318 | en boş    26434 | oran 2.62×
— yapılı anahtar: üyelik numarası dörtün katı —
uye_id % 8               bölüm  8 | en dolu  1014448 | en boş        0 | oran bölüm boş: 6
karma_dusuk % 8          bölüm  8 | en dolu  1014448 | en boş        0 | oran bölüm boş: 6
karma_yuksek % 8         bölüm  8 | en dolu   285482 | en boş   236957 | oran 1.20×
— liste bölümleme: şehir —
şehir                    bölüm  5 | en dolu   876922 | en boş   139966 | oran 6.27×
Istanbul=876922  Ankara=464332  Izmir=312596  Bursa=207230  Konya=139966
```

Üç bulgu çıkıyor.

Birincisi: karma bölümlemesi **anahtarları** dengeler, satırları değil. Sekiz bölümde
dengesizlik oranı 1,17; altmış dört bölümde 2,53. Bölüm küçüldükçe tek bir çok etkin
üyenin ağırlığı bölümün tamamına göre büyür ve karma fonksiyonunun kalitesi bunu
değiştirmez — üstteki iki satırın hemen hemen aynı çıkması bunu gösteriyor.

İkincisi: karma fonksiyonunun kalitesi **anahtarın yapılı olduğu** durumda belirleyicidir.
Üyelik numaraları dördün katı olduğunda doğrudan bölme sekiz bölümün altısını boş bıraktı.
Çarpımsal karmanın düşük bitleri de aynı sonucu verdi; nedeni aritmetiktir — tek bir
çarpanla çarpıp ikinin kuvvetine bölmek düşük bitleri korur. Yüksek bitler alındığında
dağılım 1,20 oranına düzeldi. Kural: karma değerinin **düşük bitleri bölüm seçmek için
kullanılmaz**.

Üçüncüsü: liste bölümlemesi dengeyi hiç hedeflemez. Şehre göre bölünen kayıtlarda oran
6,27 çıktı ve bu bir kusur değil, gerçekliğin kendisidir. Liste bölümlemesi bölümlerin
ayrı yönetilmesi gerektiğinde seçilir — bir şehrin verisinin ayrı saklanması, ayrı
yedeklenmesi ya da ayrı silinmesi gerekiyorsa dengesizlik kabul edilir.

#### Özet

- Bölümleme, mantıksal olarak tek bir tabloyu bölüm anahtarına göre birden çok fiziksel
  parçada saklar; aralık, liste ve karma biçimleri anahtarın bölüme nasıl çevrildiğiyle
  ayrılır.
- Bölüm anahtarını süzen sorgu ilgisiz bölümlerin verisini okumaz: görünüm üzerinden 139,
  doğrudan tek bölümden 121 sayfa okundu; aradaki fark altı boş dizin yoklamasıdır.
- Dizin varken bölümleme okuma başarımı kazandırmadı; bölümsüz tablo aynı sorguyu 123
  sayfayla yanıtladı.
- Asıl kazanç veriyi bütün olarak kaldırmaktır: bir yılın silinip boşluğun geri alınması
  0,91 saniye sürdü ve 1.393 sayfa serbest bıraktı; bölümün düşürülmesi 0,02 saniyede
  4.363 sayfa serbest bıraktı.
- Karma bölümlemesi anahtarları dengeler, satırları değil; bölüm sayısı arttıkça dengesizlik
  büyüdü (1,17 → 2,53). Yapılı anahtarlarda karma değerinin düşük bitleri bölüm seçmek için
  kullanılamaz.

#### Sonraki Adım

Bu dersteki bütün bölümler aynı veritabanının içindeydi. Bölümleri ayrı makinelere
dağıtmak, aynı fikri bir adım ileri götürür ve nitelik değiştirir: bölümler arası bir
sorgu artık ağ üzerinden yürür, bir işlem birden çok makineyi kapsar ve bölüm sayısı
değiştiğinde veri taşınmak zorunda kalır. Sonraki ders bu kalıbı ele alır — parça
anahtarının seçiminin uygulamaya ne dayattığı, çapraz parça sorgusunun maliyeti ve
yeniden dengelemede taşınan veri miktarı sayılarak.

### Parçalama Kalıpları

Source: https://academia.sh/tr/kurslar/veritabani-yonetimi/parcalama-kaliplari

Bölümlerin ayrı makinelere dağıtılması, parça anahtarının uygulamaya dayattığı sınırlar, çapraz parça sorgusunun kuyruk gecikmesi, parçalar arası birleştirme ve benzersizlik sorunu ile yeniden dengelemede taşınan veri miktarı.

Önceki ders bir tabloyu bölümlere ayırdı ve bütün bölümler aynı veritabanının içindeydi.
Bölümleri ayrı makinelere dağıtmak, aynı fikrin bir adım ötesidir ve o adımda nitelik
değişir.

**Parçalama (sharding)**, verinin yatay olarak bölünüp birden çok bağımsız veritabanı
örneğine dağıtılmasıdır. Her parça kendi süreçlerini, kendi tampon havuzunu, kendi
günlüğünü ve kendi kilit yöneticisini taşır. Motor Mimarisi konusunda kurulan her yapı
parça başına bir kez vardır ve parçalar birbirinin varlığından haberli değildir. Kazanç
budur: yazma yükü, bellek ve disk gerçekten bölünür. Bedel de budur: bir tablonun tek
bir motor içinde bedelsiz sayılan üç yeteneği — birleştirme, benzersizlik ve işlem —
parçalar arasında bedelsiz olmaktan çıkar.

Bu ders o üç bedeli sayar. Ölçümler `node` ile kurulan modellerdir; gerçek bir dağıtık
veritabanı değildirler. Modelin sayabildiği şey ilişkilerdir — hangi seçim hangi maliyeti
kaç kat büyütür — mutlak değerler değil.

#### Parça Anahtarı Uygulamaya Ne Dayatır

**Parça anahtarı (shard key)**, bir satırın hangi parçaya düşeceğini belirleyen sütundur.
Kütüphane verisinde iki aday vardır ve seçim, hangi sorgunun ucuz olacağını belirler.

Üye kimliğine göre parçalanan bir kurulumda bir üyenin bütün ödünç kayıtları tek bir
parçadadır. "Bu üyenin açık ödünçleri" sorusu tek parçaya gider; sorgu, tek makineli bir
kurulumdaki sorgudan farksızdır. Aynı kurulumda "bu kitabı kimler ödünç aldı" sorusunun
yanıtı bütün parçalara dağılmıştır.

Kitap kimliğine göre parçalanan kurulumda ilişki tersine döner. Hangi seçimin doğru
olduğu veriden değil, **iş yükünün soru dağılımından** çıkar: en sık sorulan ve en düşük
gecikme beklenen soru hangi anahtarı süzüyorsa parça anahtarı odur.

Bu karar, tek makineli bir kurulumda geri alınabilir bir dizin kararına benzemez. Dizin
kaldırılıp yeniden kurulabilir; parça anahtarı değiştirmek verinin tamamının taşınması
demektir. Parçalamanın uygulamaya ilk dayattığı şey budur: **sorgu biçimleri veri düzenini
belirler ve sonradan değiştirilmesi pahalıdır.**

#### Çapraz Parça Sorgusu

Parça anahtarını süzmeyen bir sorgu bütün parçalara gönderilir, her parçadan kısmi sonuç
alınır ve eşgüdümcü bunları birleştirir. Bu kalıba **dağıt–topla (scatter–gather)** denir
ve maliyeti sezgiye aykırıdır: yapılan toplam iş aynı kalsa bile yanıt süresi bozulur,
çünkü sorgu **en yavaş parçayı** beklemek zorundadır.

```sh
cat > capraz.mjs <<'JS'
// Model: 8 parcaya dagitilmis odunc kayitlarinda sorgu turlerinin dokundugu parca sayisi
// ve yanit suresi. Gercek bir dagitik veritabani degil; sayilan sey dokunulan parca
// sayisi ile kuyruk gecikmesi arasindaki iliskidir.
const PARCA = 8, SORGU = 20000;
// Yinelenebilir sozde rastgele uretec (dogrusal kongruansiyel).
let durum = 20240728;
const rastgele = () => (durum = (durum * 1103515245 + 12345) % 2147483648) / 2147483648;

// Bir parcanin yanit suresi: 2 ms taban + agirlikli kuyruk. Yuzde bir olasilikla yavas.
function parcaSuresi() {
  const r = rastgele();
  return r < 0.99 ? 2 + 3 * r : 40 + 60 * r;
}
function olc(dokunulanParca) {
  const sureler = [];
  for (let i = 0; i < SORGU; i++) {
    let enYavas = 0;
    for (let p = 0; p < dokunulanParca; p++) enYavas = Math.max(enYavas, parcaSuresi());
    sureler.push(enYavas + 1);            // 1 ms eşgüdüm payı
  }
  sureler.sort((a, b) => a - b);
  const yuzde = (q) => sureler[Math.floor(SORGU * q)];
  return { orta: yuzde(0.5), p95: yuzde(0.95), p99: yuzde(0.99), enKotu: sureler[SORGU - 1] };
}
console.log('dokunulan parça   ortanca      %95      %99   en kötü');
for (const d of [1, 2, 4, 8]) {
  const s = olc(d);
  console.log(String(d).padStart(14),
              s.orta.toFixed(1).padStart(9), s.p95.toFixed(1).padStart(8),
              s.p99.toFixed(1).padStart(8), s.enKotu.toFixed(1).padStart(9));
}
console.log('\n— sorgu karışımı: parça anahtarı var mı —');
const karisim = [
  ['üyenin ödünçleri (parça anahtarı var)', 1],
  ['kitabın ödünç geçmişi (anahtar yok)',   PARCA],
  ['aylık rapor (anahtar yok)',             PARCA],
];
for (const [ad, d] of karisim) {
  const s = olc(d);
  console.log(ad.padEnd(38), 'parça', String(d).padStart(2),
              '| ortanca', s.orta.toFixed(1).padStart(5),
              '| %99', s.p99.toFixed(1).padStart(6), 'ms');
}
JS
node capraz.mjs
```

```
dokunulan parça   ortanca      %95      %99   en kötü
             1       4.5      5.9      6.0     101.0
             2       5.1      5.9    100.6     101.0
             4       5.5      6.0    100.8     101.0
             8       5.8    100.5    100.9     101.0

— sorgu karışımı: parça anahtarı var mı —
üyenin ödünçleri (parça anahtarı var)  parça  1 | ortanca   4.5 | %99    6.0 ms
kitabın ödünç geçmişi (anahtar yok)    parça  8 | ortanca   5.8 | %99  100.9 ms
aylık rapor (anahtar yok)              parça  8 | ortanca   5.8 | %99  100.9 ms
```

Modelde her parçanın yüzde bir olasılıkla yavaş yanıt verdiği varsayılmıştır. Ortanca
süre bu varsayımdan neredeyse etkilenmiyor: tek parçada 4,5 ms, sekiz parçada 5,8 ms.
Kuyruk ise çöküyor. Tek parçada yüzde 99'luk dilim 6,0 ms iken sekiz parçada 100,9 ms
oldu — on yedi kat.

Aritmetiği doğrudandır. Tek bir parçanın yavaş olma olasılığı yüzde bir ise, sekiz
parçadan **en az birinin** yavaş olma olasılığı $1 - 0{,}99^8 \approx 0{,}077$, yani
yüzde 7,7. Dağıt–topla, her parçanın nadir kusurunu sorgunun sık kusuruna çevirir.

Buradan parçalamanın en çok yanlış anlaşılan yanı çıkar: parçalama **çıktıyı (throughput)**
büyütür, tek bir sorgunun gecikmesini küçültmez. Ortalama sürelere bakan bir ölçüm bu
bozulmayı hiç görmez; görülmesi için kuyruk dilimlerine bakmak gerekir.

#### Birleştirme, Benzersizlik ve İşlem

Üç yetenek parça sınırında niteliğini değiştirir.

**Birleştirme.** Üye kimliğine göre parçalanmış ödünç kayıtları, aynı anahtara göre
parçalanmış üye tablosuyla parça içinde birleştirilebilir; iki tablonun ilgili satırları
aynı makinededir. Buna **birlikte yerleştirme (co-location)** denir ve parçalı bir
tasarımda iki büyük tabloyu ucuza birleştirmenin tek yoludur. Kitap tablosu ise ödünç kayıtlarıyla
birlikte yerleştirilemez, çünkü ödünç kaydı iki farklı anahtara birden ait olamaz. Yaygın
çözüm, küçük ve seyrek değişen tabloların her parçaya **kopyalanmasıdır**; kütüphane
örneğinde şube listesi böyle bir tablodur.

**Benzersizlik.** Tek bir motorda benzersizlik kısıtı bir dizinle uygulanır. Parçalar
arasında böyle bir dizin yoktur: her parça yalnız kendi satırlarını görür, dolayısıyla
yalnız **parça içinde** benzersizlik verebilir. Genel benzersizlik gerektiren bir alan —
üyenin e-posta adresi gibi — ya parça anahtarı olur, ya ayrı bir arama tablosunda tutulur.
Otomatik artan kimlikler de aynı nedenle çalışmaz; parçalı kurulumlarda kimlikler
merkezden değil, parça numarasını içeren ya da çakışma olasılığı ihmal edilebilir olacak
kadar geniş bir üreteçten alınır.

**İşlem.** ACID Özellikleri dersinde tanımlanan atomiklik tek bir motorun günlüğüne
dayanır. İki parçaya birden yazan bir işlem iki ayrı günlüğe yazar ve ikisinin birlikte
kesinleşmesi ayrı bir protokol gerektirir: eşgüdümcü önce bütün parçalara hazırlanma
buyruğu verir, hepsinden onay alırsa kesinleştirir. Bu protokolün bedeli ek gidiş
dönüşler değil, **askıda kalma** olasılığıdır — eşgüdümcü hazırlık ile kesinleştirme
arasında düşerse parçalar kilitlerini tutarak bekler. Parçalı tasarımın altın kuralı bu
yüzden şudur: bir işlem tek bir parçanın içinde kalmalıdır. Kalamıyorsa, tasarım ya parça
anahtarını ya işlem sınırını yanlış seçmiştir.

#### Yeniden Dengeleme

Parça sayısı sabit kalmaz. Yeni bir parça eklendiğinde anahtar–parça eşlemesi değişir ve
değişen her eşleme, veri taşınması demektir. Taşınan miktar eşlemenin nasıl kurulduğuna
bağlıdır ve bu hesaplanabilir.

```sh
cat > dengele.mjs <<'JS'
// Model: parca sayisi degistiginde tasinmasi gereken satir orani. Gercek bir yeniden
// dengeleme yordami degil; sayilan sey yalnizca eslemenin tasima maliyetine etkisidir.
const UYE = 120000, KOVA = 4096;
const M = 2654435761n;
const karma = (x) => Number(((BigInt(x) * M) % 4294967296n) >> 8n);

// Uye basina odunc sayisi: az sayida uye cok odunc alir.
const uye = [];
{
  let toplam = 0;
  const agirlik = [];
  for (let i = 1; i <= UYE; i++) { const a = 1 / Math.pow(i, 0.8); agirlik.push(a); toplam += a; }
  for (let i = 0; i < UYE; i++)
    uye.push({ id: i + 1, odunc: Math.max(1, Math.round(agirlik[i] / toplam * 2000000)) });
}
const toplamSatir = uye.reduce((t, u) => t + u.odunc, 0);

function modTasima(eski, yeni) {
  let tasinan = 0;
  for (const u of uye) if (karma(u.id) % eski !== karma(u.id) % yeni) tasinan += u.odunc;
  return tasinan;
}
// Sanal kova: anahtar sabit sayida kovaya duser, kovalar parcalara atanir.
function kovaAtama(P) {
  const atama = new Array(KOVA);
  for (let b = 0; b < KOVA; b++) atama[b] = b % P;
  return atama;
}
function kovaTasima(eski, yeni) {
  const eskiAtama = kovaAtama(eski);
  const hedef = Math.floor(KOVA / yeni);
  const sayac = new Array(yeni).fill(0);
  const yeniAtama = new Array(KOVA).fill(-1);
  for (let b = 0; b < KOVA; b++) {                 // once yerinde kalabilecekler
    const p = eskiAtama[b];
    if (p < yeni && sayac[p] < hedef) { yeniAtama[b] = p; sayac[p]++; }
  }
  let p = 0;
  for (let b = 0; b < KOVA; b++) {                 // bosta kalanlar aza dogru dagitilir
    if (yeniAtama[b] !== -1) continue;
    while (p < yeni - 1 && sayac[p] >= hedef) p++;
    yeniAtama[b] = p; sayac[p]++;
  }
  let tasinan = 0;
  for (const u of uye) {
    const b = karma(u.id) % KOVA;
    if (eskiAtama[b] !== yeniAtama[b]) tasinan += u.odunc;
  }
  return tasinan;
}
console.log('modeldeki toplam ödünç:', toplamSatir);
console.log('geçiş        doğrudan bölme        sanal kova');
for (const [e, y] of [[8, 9], [8, 12], [8, 16]]) {
  const a = modTasima(e, y), b = kovaTasima(e, y);
  const yuzde = (x) => (100 * x / toplamSatir).toFixed(1) + '%';
  console.log(`${e} → ${y}`.padEnd(12),
              (a.toLocaleString('tr-TR') + ' (' + yuzde(a) + ')').padStart(21),
              (b.toLocaleString('tr-TR') + ' (' + yuzde(b) + ')').padStart(18));
}
JS
node dengele.mjs
```

```
modeldeki toplam ödünç: 2001046
geçiş        doğrudan bölme        sanal kova
8 → 9            1.793.865 (89.6%)    228.270 (11.4%)
8 → 12           1.337.553 (66.8%)    675.360 (33.8%)
8 → 16           1.006.912 (50.3%)    982.390 (49.1%)
```

Doğrudan bölme — anahtarın karmasını parça sayısına bölüp kalanı almak — sekizden dokuza
geçişte kayıtların yüzde 89,6'sını taşıttı. Tek bir parça eklemek için verinin neredeyse
tamamının yerinden oynaması gerekiyor, çünkü bölen değiştiğinde kalan da neredeyse her
anahtar için değişir.

**Sanal kova (virtual bucket)** yaklaşımı araya bir katman koyar: anahtar önce sabit
sayıda kovaya (burada 4.096) düşürülür, kovalar parçalara atanır. Parça sayısı
değiştiğinde anahtar–kova eşlemesi hiç değişmez; yalnız kova–parça ataması güncellenir ve
yalnız yeni parçaya devredilen kovaların verisi taşınır. Sekizden dokuza geçişte taşınan
oran yüzde 11,4 oldu — kuramsal alt sınır olan 1/9 ≈ yüzde 11,1'e çok yakın.

Üçüncü satır bir uyarı taşır. Sekizden on altıya geçişte iki yöntem eşitleniyor (yüzde
50,3'e karşı yüzde 49,1), çünkü parça sayısını ikiye katlamak doğrudan bölmenin en şanslı
durumudur: her parça tam ikiye ayrılır. Bu, ikiye katlamayı geçerli bir büyüme
stratejisi yapar — ama ancak ikiye katlanabildiği sürece. Kaynak planlaması buna
zorlandığında maliyet, gerekmeyen kapasitenin peşinen alınmasıdır.

Taşınan oranın kendisi de son bedel değildir. Taşıma sırasında iki kopya bir süre birlikte
yaşar, okumalar iki yerden yanıtlanabilir olmalıdır ve yazmalar taşınmakta olan kovalar
için ya durdurulur ya iki yere birden yazılır. Bu yüzden yeniden dengeleme, parça
sayısından çok, taşımanın **kesintisiz yapılıp yapılamadığıyla** ölçülen bir yetenektir.

#### Parçalamaya Ne Zaman Gerek Yoktur

Parçalama, sayılan bedellerin hepsini uygulama koduna taşır. Bu yüzden son soru, ondan
kaçınmanın yollarının tükenip tükenmediğidir. Bu kursta ölçülen üç yol çoğu zaman daha
önce gelir: doğru dizinler okuma maliyetini büyüklük sırasıyla düşürdü, bölümleme eski
verinin kaldırılmasını sabit maliyete indirdi, okuma yükü ise çoğaltmayla dağıtılabilir.

Parçalamayı gerçekten zorunlu kılan tek etken **yazma** yüküdür ve o da tek bir motorun
günlük ile denetim noktası kapasitesi aşıldığında. Karar bu ölçüyle verilir; "tablo büyük"
ölçüsüyle değil.

#### Özet

- Parçalama veriyi bağımsız veritabanı örneklerine dağıtır; parça anahtarı hangi sorgunun
  tek parçada kalacağını belirler ve sonradan değiştirilmesi verinin tamamının taşınması
  demektir.
- Dağıt–topla kalıbı ortanca süreyi az etkiler, kuyruğu bozar: modelde tek parçada 6,0 ms
  olan yüzde 99'luk dilim sekiz parçada 100,9 ms oldu.
- Birleştirme büyük tablolarda ancak birlikte yerleştirilmişse ucuz kalır; benzersizlik yalnız parça
  içinde uygulanabilir; iki parçaya yazan işlem ayrı bir protokol ve askıda kalma riski
  getirir.
- Doğrudan bölmede sekizden dokuz parçaya geçiş kayıtların yüzde 89,6'sını taşıttı; sanal
  kova katmanıyla aynı geçiş yüzde 11,4'e indi.
- Parçalamayı zorunlu kılan etken yazma yükünün tek motorun kapasitesini aşmasıdır; dizin,
  bölümleme ve çoğaltma bu eşiğe kadar daha ucuz yollardır.

#### Sonraki Adım

Bu konu verinin nasıl düzenlendiğini ve hangi yollardan okunduğunu tamamladı: dizin
türleri, sütun sırası, kapsama, bakım, bölümleme ve parçalama. Motor Mimarisi konusuyla
birlikte artık verinin nerede durduğu ve nasıl bulunduğu kurulmuş durumda — sistem
çalışır hâldedir. Çalışır olmak ile işletiliyor olmak ayrı şeylerdir. Sonraki konu bu
ayrımla açılır ve ilk sorusu en eskisidir: bu veriye kim erişebilir ve ne yapabilir? Ödünç
masasındaki uygulamanın ödünç kaydı açması gerekir, üye kaydını silmesi gerekmez; bu
ayrımın yetki düzeyinde nasıl kurulacağı sonraki dersin konusudur.

## İşletim

Roller ve satır düzeyi güvenlik, yedekleme türleri ve zaman noktasına kurtarma, yedek doğrulama, fiziksel ve mantıksal çoğaltma, devralma, bağlantı havuzlayıcıları, izleme, toplu yükleme ve sürüm yükseltme.

### Roller ve Yetkiler

Source: https://academia.sh/tr/kurslar/veritabani-yonetimi/roller-ve-yetkiler

En az yetki ilkesinin veritabanında uygulanması: rol tabanlı erişim denetimi, rol devralmasının ürettiği fazla yetkinin sayılması, salt okunur bağlantıyla ikinci savunma katmanı ve uygulama hesabı ile bakım hesabının ayrılması.

Bu kursun buraya kadarki bölümü verinin nerede durduğunu ve hangi yoldan okunduğunu
kurdu: sayfa düzeni, günlük, dizin türleri, bölümleme ve parçalama kalıpları. Veri düzeni
ve erişim yolları yerine oturduğunda sistem çalışır hâle gelir — ama henüz işletilmiş
sayılmaz. İşletim, çalışan bir sistemin zaman içinde güvenli, kurtarılabilir ve
erişilebilir kalmasıdır.

Bu konunun ilk sorusu en eskisidir: bu veriye kim erişebilir ve ne yapabilir? Kütüphane
ödünç kayıtları örneğinde soru somutlaşır. Ödünç masasındaki uygulamanın ödünç kaydı
açması gerekir; üye kaydını silmesi gerekmez. Rapor aracının ödünç sayılarını okuması
gerekir; hiçbir satırı değiştirmesi gerekmez. Bu ayrımların yapılmadığı bir kurulumda
sistem yıllarca sorunsuz çalışabilir; tek bir hatalı sorgu ya da tek bir ele geçirilmiş
uygulama anahtarı, ayrımın yapılmamış olmasının bedelini bir defada ödetir.

#### Kimlik Doğrulama ve Yetkilendirme

İki soru birbirine karışır. **Kimlik doğrulama (authentication)** "sen kimsin" sorusudur;
İnternet Nasıl Çalışır kursunda HTTPS bağlamında tanıtılmıştı. **Yetkilendirme
(authorization)** ise "kim olduğun belli, ne yapabilirsin" sorusudur. Veritabanı
motorları ikisini ayrı katmanlarda tutar: bağlantı kurulurken kimlik doğrulanır, her
deyimde yetki denetlenir.

SQL Temelleri kursunda `GRANT` ve `REVOKE` deyimleriyle nesne düzeyi izinler tanıtılmıştı.
O ders dili anlattı; bu ders, aynı deyimlerin işletimde nasıl düzenleneceğini anlatır.
Aradaki fark ölçektir: on tablo ve iki kullanıcı için izinleri tek tek vermek işler,
yüz tablo ve otuz hesap için işlemez. Ölçeklendiren yapı **rol (role)** kavramıdır.

#### Rol Tabanlı Erişim Denetimi

**Rol tabanlı erişim denetimi (role-based access control)**, izinleri kişilere değil
görevlere bağlar. Önce görev tanımlanır — okuyucu, ödünç görevlisi, katalogcu — ve izinler
o göreve verilir. Kişi ya da uygulama hesabı sonradan role atanır.

Bunun işletimdeki karşılığı şudur: bir çalışan görev değiştirdiğinde ya da bir uygulama
kaldırıldığında değiştirilecek tek şey rol ataması olur. İzinler dağınık biçimde
hesaplara yazılmışsa, ayrılan bir çalışanın erişimini kapatmak bir arama işine dönüşür ve
o aramanın eksik kalması olağandır.

Rollerin ikinci özelliği **rol devralması (role inheritance)** — bir rolün başka bir rolün
izinlerini kapsamasıdır. Katalogcu, okuyucunun her şeyini yapabilsin diye okuyucu rolünü
devralır. Devralma yazımı kısaltır, ama bir yan etkisi vardır: etkin izin kümesi artık
yazıldığı yerde görünmez, hesaplanması gerekir. Bir hesabın gerçekte neye erişebildiğini
görmek için devralma zincirinin kapanışını almak gerekir.

#### Fazla Yetkinin Hesaplanması

Aşağıdaki betik bir yetki çizelgesini modeller: roller, rollerin devraldığı roller,
hesapların rol atamaları ve her hesabın işini yapabilmesi için gerçekten gereken izinler.
Betik, devralmayı izleyerek etkin izin kümesini üretir ve gerekenden fazlasını çıkarır.

Bu bir modeldir; gerçek bir motorun yetki çizelgesi değildir. Modellenen şey devralmanın
kapanışı ve fark hesabıdır — motor hangisi olursa olsun bu iki adım aynıdır.

```bash
cat > yetki.mjs <<'EOF'
// Rol -> dogrudan verilmis izinler. Izin bicimi: "eylem:nesne".
const ROL_IZIN = {
  okuyucu:   ["select:kitap", "select:sube"],
  gorevli:   ["select:uye", "select:odunc", "insert:odunc", "update:odunc"],
  katalogcu: ["insert:kitap", "update:kitap", "delete:kitap"],
  yonetici:  ["delete:uye", "delete:odunc", "alter:*"],
};

// Rol -> devraldigi roller.
const ROL_DEVRAL = {
  okuyucu:   [],
  gorevli:   ["okuyucu"],
  katalogcu: ["okuyucu"],
  yonetici:  ["gorevli", "katalogcu"],
};

// Hesap -> atanmis roller.
const HESAP_ROL = {
  odunc_masasi:  ["gorevli"],
  katalog_ekibi: ["katalogcu"],
  rapor_araci:   ["gorevli"],
  bakim_hesabi:  ["yonetici"],
};

// Hesabin isini yapabilmesi icin gercekten gereken izinler.
const GEREKEN = {
  odunc_masasi:  ["select:kitap", "select:uye", "select:odunc", "insert:odunc", "update:odunc"],
  katalog_ekibi: ["select:kitap", "insert:kitap", "update:kitap"],
  rapor_araci:   ["select:kitap", "select:odunc"],
  bakim_hesabi:  ["select:kitap", "select:uye", "select:odunc", "alter:*"],
};

function etkinIzinler(rol, gorulen = new Set()) {
  if (gorulen.has(rol)) return new Set();          // devralma dongusune karsi
  gorulen.add(rol);
  const kume = new Set(ROL_IZIN[rol] ?? []);
  for (const ust of ROL_DEVRAL[rol] ?? []) {
    for (const izin of etkinIzinler(ust, gorulen)) kume.add(izin);
  }
  return kume;
}

function hesapIzinleri(hesap) {
  const kume = new Set();
  for (const rol of HESAP_ROL[hesap]) {
    for (const izin of etkinIzinler(rol)) kume.add(izin);
  }
  return kume;
}

const yaziciMi = (izin) => !izin.startsWith("select:");

console.log("hesap           | etkin | gereken | fazla | fazla yazici izin");
console.log("----------------|-------|---------|-------|------------------");
for (const hesap of Object.keys(HESAP_ROL)) {
  const etkin = hesapIzinleri(hesap);
  const gereken = new Set(GEREKEN[hesap]);
  const fazla = [...etkin].filter((i) => !gereken.has(i)).sort();
  console.log(
    hesap.padEnd(15) + " | " +
    String(etkin.size).padStart(5) + " | " +
    String(gereken.size).padStart(7) + " | " +
    String(fazla.length).padStart(5) + " | " +
    fazla.filter(yaziciMi).join(", ")
  );
}
EOF
node yetki.mjs
```

```text
hesap           | etkin | gereken | fazla | fazla yazici izin
----------------|-------|---------|-------|------------------
odunc_masasi    |     6 |       5 |     1 | 
katalog_ekibi   |     5 |       3 |     2 | delete:kitap
rapor_araci     |     6 |       2 |     4 | insert:odunc, update:odunc
bakim_hesabi    |    12 |       4 |     8 | delete:kitap, delete:odunc, delete:uye, insert:kitap, insert:odunc, update:kitap, update:odunc
```

Tablo dört ayrı sorunu aynı anda gösterir.

`odunc_masasi` bir izin fazlaya sahip ve o izin bir okuma iznidir (`select:sube`). Bu, en
az yetki ilkesinin ihlalidir ama etkisi sınırlıdır; devralmadan gelen kaçınılmaz artıktır.

`katalog_ekibi` kitap silme iznine sahip, oysa gerekenler arasında silme yok. Katalog
düzeltmesi yapan bir ekibin yanlışlıkla kayıt silmesi, sonradan onarılması pahalı bir
kazadır.

`rapor_araci` durumu en açık olanıdır. Yalnız okuma yapması beklenen bir hesap, `gorevli`
rolü üzerinden ödünç kaydı ekleme ve güncelleme iznine sahip. Bu, "uygun bir rol vardı,
onu verdik" kararının tipik sonucudur: rol adı işi anlatıyordu ama izin kümesi işten
genişti.

`bakim_hesabi` on iki izne sahip, dördü gerekli. Bakım hesabının geniş olması beklenir;
sorun genişliği değil, o hesabın gündelik iş için kullanılmasıdır.

#### Salt Okunur Bağlantı

Yetki çizelgesi tek savunma katmanı değildir. İkinci katman **bağlantı düzeyi kısıtıdır**:
bağlantı, yazma yeteneği olmadan açılır. Rapor aracının hangi rolü taşıdığından bağımsız
olarak, bağlantısı salt okunur açılmışsa yazma denemesi bağlantı katmanında reddedilir.

```bash
rm -f kutuphane.db
sqlite3 kutuphane.db <<'SQL'
CREATE TABLE sube(id INTEGER PRIMARY KEY, ad TEXT NOT NULL);
INSERT INTO sube VALUES (1,'Merkez'),(2,'Sahil');
SQL

echo "--- salt okunur baglantidan yazma ---"
sqlite3 -readonly kutuphane.db "INSERT INTO sube VALUES (3,'Yeni');"
echo "cikis kodu: $?"

echo "--- ayni baglantidan okuma ---"
sqlite3 -readonly -box kutuphane.db "SELECT * FROM sube ORDER BY id;"
rm -f kutuphane.db
```

```text
--- salt okunur baglantidan yazma ---
Error: stepping, attempt to write a readonly database (8)
cikis kodu: 8
--- ayni baglantidan okuma ---
┌────┬────────┐
│ id │   ad   │
├────┼────────┤
│ 1  │ Merkez │
│ 2  │ Sahil  │
└────┴────────┘
```

Buradaki `-readonly` seçeneği ve hata iletisinin sözcükleri bu motora özgüdür; sunucu
tabanlı motorlarda karşılığı bir bağlantı parametresi ya da oturum ayarı olur. Ortak olan
düzenek şudur: yazma yeteneği bağlantı açılırken kapatıldığında, yetki çizelgesindeki bir
yanlışlık tek başına veri kaybına dönüşemez. İki katman birlikte bulunur; biri diğerinin
yerine geçmez.

#### Hesapların Ayrılması

İşletimde üç hesap sınıfı birbirinden ayrı tutulur.

**Uygulama hesabı** gündelik işi yapar. Yalnız uygulamanın dokunduğu tablolar üzerinde,
yalnız yaptığı eylemler için izin taşır. Şema değiştirme izni taşımaz — uygulama kodunun
çalışma anında tablo yapısını değiştirmesi beklenmez.

**Rapor hesabı** yalnız okur. Ayrı bir hesap olmasının nedeni yalnız güvenlik değildir:
ayrı hesap, yavaş bir rapor sorgusunun kaynak tükettiğini izleme tarafında ayırt
edilebilir kılar.

**Bakım hesabı** şema değişikliği, yedek alma ve kurtarma için kullanılır. Bu hesabın
geniş olması işin gereğidir; disiplini, gündelik bağlantılarda kullanılmamasıdır.
Uygulamanın bağlantı dizgisinde bakım hesabı yazıyorsa, uygulamadaki her kusur şema
kaybına dönüşme yeteneği kazanır.

Ayrımın sınanabilir bir sonucu vardır: uygulama hesabıyla açılmış bir bağlantıda tablo
silme denemesi yetki hatasıyla dönmelidir. Bu denemeyi bir kez elle yapmak, "izinleri
verdik" cümlesini "izinleri doğruladık" cümlesine çevirir.

#### Yetkinin Zaman İçindeki Kayması

Yetki çizelgesi kurulduğu gün doğrudur; sonraki her ay biraz daha yanlıştır. Bir raporun
geçici olarak bir tabloya daha erişmesi gerekir, izin verilir, rapor kaldırılır, izin
kalır. Bu birikmeye **yetki sürünmesi (privilege creep)** denir ve tek çaresi düzenli
denetimdir.

Denetimin somut biçimi yukarıdaki betiğin yaptığı şeydir: hesabın etkin izin kümesi
çıkarılır, işin gerektirdiği kümeyle karşılaştırılır, fark listelenir. Farkın sıfır olması
gerekmez — devralmadan gelen artık izinler kabul edilebilir. Gerekli olan, farkın
**bilinmesi** ve yazıcı izinlerin fark listesinde bulunmamasıdır.

İkinci denetim sorusu erişim yollarıyla ilgilidir: yetki çizelgesi bir tabloyu kapatırken,
o tablonun sütunlarını gösteren bir görünüm açık kalmış olabilir. Görünümler SQL Temelleri
kursunda tanıtılmıştı; işletim tarafında görünüm, izin denetiminin atlanabildiği bir yol
hâline gelmemelidir. Denetim, tablo listesini değil erişilebilir nesnelerin tamamını
kapsar.

#### Özet

- Kimlik doğrulama "kimsin", yetkilendirme "ne yapabilirsin" sorusudur; motorlar ikisini
  ayrı katmanlarda denetler.
- Rol tabanlı erişim denetimi izinleri kişiye değil göreve bağlar; görev değişikliğinde
  değişen tek şey rol ataması olur.
- Rol devralması etkin izin kümesini yazıldığı yerde görünmez kılar; etkin küme,
  devralma zincirinin kapanışı alınarak hesaplanır.
- En az yetki denetimi, etkin izin kümesi ile işin gerektirdiği kümenin farkını çıkarmaktır;
  fark listesinde yazıcı izin bulunmamalıdır.
- Salt okunur bağlantı, yetki çizelgesinden bağımsız ikinci bir savunma katmanıdır.
- Uygulama, rapor ve bakım hesapları ayrı tutulur; yetki sürünmesi düzenli denetimle
  karşılanır.

#### Sonraki Adım

Bu dersteki izinlerin tamamı nesne düzeyindeydi: bir hesap bir tabloyu ya okuyabiliyordu
ya okuyamıyordu. Kütüphane örneğinde bu ayrım yetmez — Sahil şubesinin görevlisi ödünç
tablosunu okuyabilmeli, ama yalnız kendi şubesinin satırlarını. Aynı tabloda farklı
hesaplara farklı satır kümesi göstermek nesne düzeyi izinle ifade edilemez. Sonraki ders,
satırın hangi koşulla görüneceğini bir politika ifadesiyle tanımlayan satır düzeyi
güvenliği ele alıyor ve bir politikanın sızdırdığı satırların nasıl sayılacağını
gösteriyor.

### Satır Düzeyi Güvenlik

Source: https://academia.sh/tr/kurslar/veritabani-yonetimi/satir-duzeyi-guvenlik

Aynı tabloda hesaba göre farklı satır kümesi göstermek: politika ifadesi, oturum değişkeni, politikanın sızdırdığı satırların sayılması, yazma tarafındaki denetim ve politikanın erişim yoluyla etkileşimi.

Önceki ders izinleri nesne düzeyinde kurdu: bir hesap ödünç tablosunu ya okuyabiliyordu ya
okuyamıyordu. Kütüphanenin gerçek gereksinimi bu ikilikle karşılanmaz. Sahil şubesinin
görevlisi ödünç kayıtlarını okuyabilmelidir, ama yalnız kendi şubesinin satırlarını.
Merkez şubesinin satırlarını görmesi bir gereksinim değil, bir sızıntıdır.

Bu, nesne düzeyi izinle ifade edilemeyen bir kısıttır. Çözüm, tabloya erişimi tümden
açmak ya da kapatmak yerine, tabloya bir **politika (policy)** iliştirmektir: satırın
görünür olması için sağlaması gereken koşul. Motor, o tabloya giden her sorguya bu koşulu
kendiliğinden ekler. Buna **satır düzeyi güvenlik (row-level security)** denir.

#### Politika İfadesi ve Oturum Değişkeni

Politika bir yüklemdir — satırın sütunlarını ve oturuma ait bir bilgiyi kullanan bir
koşul. Oturuma ait bilgi, bağlantı açılırken belirlenen ve sorgu içinden okunabilen bir
değerdir: hangi hesap bağlandı, o hesap hangi şubeye ait. Buna **oturum değişkeni
(session variable)** denir.

Kütüphane örneğindeki politika şudur: `sube_id`, oturumun şube numarasına eşit olmalı.
Sorgu `WHERE` yan tümcesine hiçbir şey yazmasa bile motor bu koşulu ekler; sorgu kendi
koşulunu yazmışsa iki koşul birlikte uygulanır.

Aşağıdaki örnek politikayı bir görünüm üzerinden modelliyor. Gerçek satır düzeyi güvenlik
politikayı taban tabloya iliştirir ve görünüm gerektirmez; burada modellenen şey politika
ifadesinin satır kümesini nasıl daralttığıdır, bu daraltma her iki durumda da aynıdır.

```bash
rm -f kutuphane.db
sqlite3 -box -header kutuphane.db <<'SQL'
CREATE TABLE odunc(
  id INTEGER PRIMARY KEY, kitap_id INT NOT NULL, uye_id INT NOT NULL,
  sube_id INT NOT NULL, alis TEXT NOT NULL, iade TEXT);
CREATE TABLE oturum(anahtar TEXT PRIMARY KEY, deger TEXT NOT NULL);

INSERT INTO odunc(kitap_id,uye_id,sube_id,alis,iade) VALUES
 (11,101,1,'2024-03-01','2024-03-14'),(12,102,1,'2024-03-03',NULL),
 (13,103,1,'2024-03-05',NULL),(14,104,2,'2024-03-02','2024-03-16'),
 (15,105,2,'2024-03-06',NULL),(16,106,2,'2024-03-08',NULL),
 (17,107,2,'2024-03-09','2024-03-20'),(18,108,3,'2024-03-04','2024-03-18'),
 (19,109,3,'2024-03-11',NULL);
INSERT INTO oturum VALUES ('sube_id','2');

CREATE VIEW odunc_gorunur AS
  SELECT * FROM odunc
  WHERE sube_id = (SELECT CAST(deger AS INTEGER) FROM oturum WHERE anahtar='sube_id');

SELECT 'taban tablo' AS kaynak, COUNT(*) AS satir FROM odunc
UNION ALL
SELECT 'politika sonrasi (sube 2)', COUNT(*) FROM odunc_gorunur;

UPDATE oturum SET deger='1' WHERE anahtar='sube_id';
SELECT 'politika sonrasi (sube 1)' AS kaynak, COUNT(*) AS satir FROM odunc_gorunur;
SQL
rm -f kutuphane.db
```

```text
┌───────────────────────────┬───────┐
│          kaynak           │ satir │
├───────────────────────────┼───────┤
│ taban tablo               │ 9     │
│ politika sonrasi (sube 2) │ 4     │
└───────────────────────────┴───────┘
┌───────────────────────────┬───────┐
│          kaynak           │ satir │
├───────────────────────────┼───────┤
│ politika sonrasi (sube 1) │ 3     │
└───────────────────────────┴───────┘
```

Sorgu metni değişmedi; yalnız oturum değeri değişti ve sonuç kümesi 4 satırdan 3 satıra
düştü. Politikanın anlamı budur: aynı sorgu, kim çalıştırdığına göre farklı satır kümesi
döndürür. Uygulama kodunda `WHERE sube_id = ?` yazmayı unutmak artık bir sızıntı
üretmez, çünkü koşulu uygulama değil motor koyar.

#### Politikanın Sızdırdığı Satırların Sayılması

Politika bir kez yazılır ve yıllarca durur. Yazıldığı gün doğru olan bir koşul, sonradan
eklenen bir "kolaylık" ile genişleyebilir. Sık görülen genişleme şudur: iade edilmiş
kayıtların "artık gizli sayılmaması" gerektiği kararı verilir ve politikaya bir `OR`
eklenir.

Bu tür bir değişikliğin etkisi tahmin edilmez, **sayılır**. Amaçlanan satır kümesi ile
yürürlükteki politikanın döndürdüğü küme arasındaki fark, politikanın sızdırdığı
satırlardır.

```bash
rm -f politika.db
sqlite3 -box -header politika.db <<'SQL'
CREATE TABLE odunc(
  id INTEGER PRIMARY KEY, kitap_id INT NOT NULL, uye_id INT NOT NULL,
  sube_id INT NOT NULL, alis TEXT NOT NULL, iade TEXT);
INSERT INTO odunc(kitap_id,uye_id,sube_id,alis,iade) VALUES
 (11,101,1,'2024-03-01','2024-03-14'),(12,102,1,'2024-03-03',NULL),
 (13,103,1,'2024-03-05',NULL),(14,104,2,'2024-03-02','2024-03-16'),
 (15,105,2,'2024-03-06',NULL),(16,106,2,'2024-03-08',NULL),
 (17,107,2,'2024-03-09','2024-03-20'),(18,108,3,'2024-03-04','2024-03-18'),
 (19,109,3,'2024-03-11',NULL);

WITH
 amaclanan AS (SELECT id FROM odunc WHERE sube_id = 2),
 genis      AS (SELECT id FROM odunc WHERE sube_id = 2 OR iade IS NOT NULL)
SELECT
 (SELECT COUNT(*) FROM odunc)     AS taban,
 (SELECT COUNT(*) FROM amaclanan) AS amaclanan,
 (SELECT COUNT(*) FROM genis)     AS genis_politika,
 (SELECT COUNT(*) FROM genis WHERE id NOT IN (SELECT id FROM amaclanan)) AS sizan;

SELECT id, uye_id, sube_id, iade FROM odunc
WHERE (sube_id = 2 OR iade IS NOT NULL) AND sube_id <> 2 ORDER BY id;
SQL
rm -f politika.db
```

```text
┌───────┬───────────┬────────────────┬───────┐
│ taban │ amaclanan │ genis_politika │ sizan │
├───────┼───────────┼────────────────┼───────┤
│ 9     │ 4         │ 6              │ 2     │
└───────┴───────────┴────────────────┴───────┘
┌────┬────────┬─────────┬────────────┐
│ id │ uye_id │ sube_id │    iade    │
├────┼────────┼─────────┼────────────┤
│ 1  │ 101    │ 1       │ 2024-03-14 │
│ 8  │ 108    │ 3       │ 2024-03-18 │
└────┴────────┴─────────┴────────────┘
```

Sızan satır sayısı iki ve hangileri olduğu listelendi: Merkez ve Tepe şubelerinin iade
edilmiş kayıtları, üye numaralarıyla birlikte Sahil görevlisine açılmış oldu. Dokuz
satırlık bir tabloda bu fark gözle görülür; on milyon satırlık bir tabloda yalnız bu
sorguyla görülür.

Bu sorgu bir denetim aracıdır ve politikanın her değişikliğinde çalıştırılır. Yazılışı
şablon hâline getirilebilir: amaçlanan koşulu bir ortak tablo ifadesine, yürürlükteki
politikayı bir başkasına koy, farkı say. Sıfırdan büyük her sonuç, politikanın metnine
bakmayı gerektirir.

#### Yazma Tarafındaki Denetim

Politika okumayı daraltır. Yazmayı kendiliğinden daraltmaz. Bu ayrım, satır düzeyi
güvenliğin en sık atlanan yanıdır: Sahil görevlisi, Tepe şubesine ait bir ödünç kaydı
yazabilir ve yazdığı satırı sonradan göremez.

Aşağıdaki örnek bu durumu üretiyor. Görünüm üzerinden yapılan ekleme taban tabloya
gidiyor, ama eklenen satır politikanın dışında kaldığı için görünümde yok:

```bash
rm -f yazma.db
sqlite3 -box -header yazma.db <<'SQL'
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT NOT NULL, uye_id INT NOT NULL,
                   sube_id INT NOT NULL, alis TEXT NOT NULL, iade TEXT);
CREATE TABLE oturum(anahtar TEXT PRIMARY KEY, deger TEXT NOT NULL);
INSERT INTO oturum VALUES ('sube_id','2');
INSERT INTO odunc(kitap_id,uye_id,sube_id,alis,iade) VALUES
 (15,105,2,'2024-03-06',NULL),(16,106,2,'2024-03-08',NULL);

CREATE VIEW odunc_gorunur AS
  SELECT * FROM odunc
  WHERE sube_id = (SELECT CAST(deger AS INTEGER) FROM oturum WHERE anahtar='sube_id');

CREATE TRIGGER odunc_gorunur_ekle INSTEAD OF INSERT ON odunc_gorunur
BEGIN
  INSERT INTO odunc(kitap_id,uye_id,sube_id,alis,iade)
  VALUES (NEW.kitap_id, NEW.uye_id, NEW.sube_id, NEW.alis, NEW.iade);
END;

INSERT INTO odunc_gorunur(kitap_id,uye_id,sube_id,alis,iade)
VALUES (20,110,3,'2024-03-12',NULL);

SELECT 'gorunumden' AS kaynak, COUNT(*) AS satir FROM odunc_gorunur
UNION ALL SELECT 'taban tablo', COUNT(*) FROM odunc;
SQL
rm -f yazma.db
```

```text
┌─────────────┬───────┐
│   kaynak    │ satir │
├─────────────┼───────┤
│ gorunumden  │ 2     │
│ taban tablo │ 3     │
└─────────────┴───────┘
```

Taban tabloda üç satır var, görünümde iki. Üçüncü satır yazıldı ve kayboldu. Bu, veri
bütünlüğü açısından okumadan sızıntı yapmaktan daha kötüdür: kimsenin göremediği ama var
olan kayıtlar birikir, sayımlar tutmaz ve neden bulunamaz.

Çözüm, aynı koşulun yazma yolunda da denetlenmesidir. Aşağıdaki tetikleyici, politikaya
uymayan satırı yazmadan reddediyor:

```bash
rm -f yazma2.db
sqlite3 yazma2.db <<'SQL'
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT NOT NULL, uye_id INT NOT NULL,
                   sube_id INT NOT NULL, alis TEXT NOT NULL, iade TEXT);
CREATE TABLE oturum(anahtar TEXT PRIMARY KEY, deger TEXT NOT NULL);
INSERT INTO oturum VALUES ('sube_id','2');

CREATE VIEW odunc_gorunur AS
  SELECT * FROM odunc
  WHERE sube_id = (SELECT CAST(deger AS INTEGER) FROM oturum WHERE anahtar='sube_id');

CREATE TRIGGER odunc_gorunur_ekle INSTEAD OF INSERT ON odunc_gorunur
BEGIN
  SELECT CASE WHEN NEW.sube_id <>
      (SELECT CAST(deger AS INTEGER) FROM oturum WHERE anahtar='sube_id')
    THEN RAISE(ABORT, 'politika disi satir yazilamaz') END;
  INSERT INTO odunc(kitap_id,uye_id,sube_id,alis,iade)
  VALUES (NEW.kitap_id, NEW.uye_id, NEW.sube_id, NEW.alis, NEW.iade);
END;

INSERT INTO odunc_gorunur(kitap_id,uye_id,sube_id,alis,iade)
VALUES (20,110,3,'2024-03-12',NULL);
SQL
echo "cikis kodu: $?"
rm -f yazma2.db
```

```text
Runtime error near line 19: politika disi satir yazilamaz (19)
cikis kodu: 1
```

Tetikleyici sözdizimi ve hata iletisinin biçimi motora göre değişir; satır düzeyi
güvenliği yerleşik olarak destekleyen motorlarda aynı denetim politikanın ayrı bir
yanında tanımlanır. Değişmeyen kural şudur: her politikanın okuma tarafı ile yazma tarafı
ayrı ayrı yazılır ve ikisinin uyumu ayrıca sınanır.

#### Politika ve Erişim Yolu

Politika yüklemi, sorgunun kendi koşuluna eklenen bir koşuldur; planlayıcı ikisini
birlikte görür. Bunun iki sonucu vardır.

Birincisi olumludur: politika sütunu üzerinde uygun bir dizin varsa, politika sorguyu
yavaşlatmaz — aksine seçiciliği artırdığı için hızlandırabilir. Önceki konuda kurulan
dizin ve bölümleme kararları burada işe yarar; şube numarasına göre bölümlenmiş bir tablo,
şube politikasıyla doğal biçimde uyuşur.

İkincisi dikkat gerektirir: politika yüklemi ile sorgunun kendi yüklemi arasındaki
değerlendirme sırası her zaman tanımlı değildir. Politika dışındaki bir satırın varlığı,
o satır üzerinde çalışan bir işlevin hata vermesi ya da yavaşlaması yoluyla dolaylı
olarak sezilebilir. Bu tür çıkarımlara karşı korunan motorlar politika yüklemini önce
değerlendirmeyi güvence altına alır. İşletim tarafındaki sorumluluk, politika taşıyan
tablolarda kullanıcı tanımlı işlevlerin yüklem olarak kullanılmasına izin vermemektir.

#### Politikanın Kapsamadığı Yollar

Satır düzeyi güvenlik yalnız uygulandığı tablo üzerinden geçen erişimi denetler. Denetim
listesi şu soruları içerir.

**Tablo sahibi ve geniş yetkili hesaplar.** Motorlar genellikle tablo sahibini ve yönetici
hesabını politikadan muaf tutar. Uygulama hesabının tablo sahibi olmaması bu yüzden
önemlidir.

**Politikasız kopyalar.** Aynı verinin bir rapor tablosuna, bir maddeleştirilmiş görünüme
ya da bir dışa aktarım dosyasına kopyalanması, politikayı arkada bırakır. Kopyanın
kendisine de politika gerekir; gerekmiyorsa kopya politikanın kapsadığı alanı daraltacak
biçimde üretilir.

**Toplama işlevleriyle çıkarım.** Politika satırları gizler, sayıları gizlemeyebilir. Bir
hesabın göremediği satırlar üzerinde toplam ya da sayım alabildiği bir yol varsa, satır
içeriği olmasa da bilgi sızar. Toplama sorgularının politikadan sonra çalıştığını
doğrulamak, denetimin bir parçasıdır.

**Yedekler.** Bu ders boyunca kurulan her koruma, çalışan sisteme aittir. Yedek dosyası
politikasız bir kopyadır ve ayrı korunur — bu, sonraki dersin konusudur.

#### Özet

- Satır düzeyi güvenlik, tabloya iliştirilen bir politika yüklemiyle aynı sorgunun kim
  çalıştırdığına göre farklı satır kümesi döndürmesini sağlar.
- Politika, oturum değişkenini kullanır; koşulu uygulama kodu değil motor ekler.
- Politika değişikliğinin etkisi tahmin edilmez, sayılır: amaçlanan küme ile yürürlükteki
  kümenin farkı sızan satırları verir.
- Okuma politikası yazmayı daraltmaz; yazma tarafı ayrıca denetlenmezse görünmeyen
  satırlar birikir.
- Politika yüklemi sorgu planına girer; politika sütununa uygun dizin ve bölümleme
  seçimi başarımı korur.
- Tablo sahibi, politikasız kopyalar, toplama üzerinden çıkarım ve yedek dosyaları
  politikanın kapsamadığı yollardır.

#### Sonraki Adım

Erişim denetimi verinin yanlış kişiye gitmesini engeller; verinin yok olmasını
engellemez. Bir disk arızası, yanlış yazılmış bir `DELETE` ya da hatalı bir şema
değişikliği, en özenli yetki çizelgesine sahip veritabanını da kullanılamaz hâle getirir.
Sonraki ders yedeklemeye geçiyor: mantıksal, fiziksel ve artımlı yedeklerin ne olduğunu,
hangi maliyetle alındıklarını ve bir yedeğin geri yüklendiğinde kaynakla eşit olduğunun
nasıl kanıtlanacağını ele alıyor.

### Yedekleme Türleri

Source: https://academia.sh/tr/kurslar/veritabani-yonetimi/yedekleme-turleri

Mantıksal, fiziksel ve artımlı yedeğin ölçülen maliyetleri: dosya boyutu ve alma süresi karşılaştırması, geri yüklenen kopyanın kaynakla eşitliğinin içerik karmasıyla kanıtlanması, su işareti tabanlı artımlı yedeğin sınırı.

Önceki ders satır düzeyi güvenliği kurarken bir noktayı açık bıraktı: bütün o koruma
çalışan sisteme aitti. Yedek dosyası politikasız bir kopyadır ve daha temel bir şeydir —
veritabanının var olmayı sürdürmesinin tek güvencesidir. Disk arızası, yanlış yazılmış bir
`DELETE`, hatalı bir şema göçü ya da silinen bir dizin: hiçbiri yetki çizelgesiyle
önlenmez.

Bu ders yedeğin ne olduğunu değil, hangi biçimlerde alındığını ve her biçimin ne kadara
mal olduğunu ele alıyor. Kütüphane veritabanı üzerinde üç biçim ölçülecek ve geri
yüklenen kopyanın kaynakla eşit olduğu sözle değil sorguyla gösterilecek.

#### Mantıksal ve Fiziksel Yedek

**Mantıksal yedek (logical backup)** veritabanının içeriğini yeniden üretecek deyimlerden
oluşur: tablo tanımları ve satırları anlatan `CREATE` ve `INSERT` deyimleri. Metin
dosyasıdır, okunabilir, düzenlenebilir ve farklı bir motora ya da farklı bir sürüme
yüklenebilir.

**Fiziksel yedek (physical backup)** veri dosyalarının kendisinin kopyasıdır. Sayfa
düzeyinde birebir kopyadır; anlamını yalnız aynı motor okur. Buna karşılık alınması
ucuzdur, çünkü hiçbir şey yeniden üretilmez — baytlar taşınır.

İkisi arasındaki seçim bir zevk meselesi değildir; ölçülebilir bir ödünleşimdir.

#### Üç Biçimin Ölçümü

Aşağıdaki blok kütüphane veritabanını kurar, ardından dört yedekleme yolunu aynı veri
üzerinde ölçer. Süre ve boyut değerleri makineye, dosya sistemine ve sıkıştırma
kütüphanesine bağlıdır; sizde farklı sayılar çıkacaktır. Anlamlı olan mutlak değerler
değil, sütunlar arasındaki oranlardır.

```bash
cat > kur.sql <<'SQL'
CREATE TABLE sube(id INTEGER PRIMARY KEY, ad TEXT NOT NULL);
CREATE TABLE uye(id INTEGER PRIMARY KEY, ad TEXT NOT NULL, sube_id INT NOT NULL);
CREATE TABLE kitap(id INTEGER PRIMARY KEY, baslik TEXT NOT NULL, sube_id INT NOT NULL);
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT NOT NULL, uye_id INT NOT NULL,
                   sube_id INT NOT NULL, alis TEXT NOT NULL, iade TEXT);
INSERT INTO sube VALUES (1,'Merkez'),(2,'Sahil'),(3,'Tepe');
WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n<20000)
INSERT INTO uye(id,ad,sube_id) SELECT n,'Uye-'||n,(n%3)+1 FROM s;
WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n<50000)
INSERT INTO kitap(id,baslik,sube_id) SELECT n,'Kitap-'||n,(n%3)+1 FROM s;
WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n<300000)
INSERT INTO odunc(id,kitap_id,uye_id,sube_id,alis,iade)
SELECT n,(n%50000)+1,(n%20000)+1,(n%3)+1,
       date('2023-01-01','+'||(n%900)||' day'),
       CASE WHEN n%4=0 THEN NULL ELSE date('2023-01-01','+'||((n%900)+14)||' day') END
FROM s;
CREATE INDEX odunc_uye ON odunc(uye_id);
CREATE INDEX odunc_sube_alis ON odunc(sube_id, alis);
SQL
rm -f kutuphane.db
sqlite3 kutuphane.db < kur.sql

cat > olc.mjs <<'EOF'
import { execSync } from "node:child_process";
import { statSync, rmSync } from "node:fs";

const olc = (ad, komut, cikti) => {
  rmSync(cikti, { force: true });
  const t0 = process.hrtime.bigint();
  execSync(komut);
  const ms = Number(process.hrtime.bigint() - t0) / 1e6;
  return { ad, ms, bayt: statSync(cikti).size };
};

const kaynak = statSync("kutuphane.db").size;
const olcumler = [
  olc("mantiksal (.dump)", "sqlite3 kutuphane.db .dump > mantiksal.sql", "mantiksal.sql"),
  olc("mantiksal + sikistirma", "sqlite3 kutuphane.db .dump | gzip -9 > mantiksal.sql.gz", "mantiksal.sql.gz"),
  olc("fiziksel (dosya kopyasi)", "cp kutuphane.db fiziksel.db", "fiziksel.db"),
  olc("fiziksel (tutarli anlik goruntu)", "sqlite3 kutuphane.db \"VACUUM INTO 'anlik.db'\"", "anlik.db"),
];

console.log("kaynak veritabani: " + kaynak.toLocaleString("tr-TR") + " bayt\n");
console.log("yedek bicimi                     |     bayt | kaynagin % | sure (ms)");
console.log("---------------------------------|----------|------------|----------");
for (const o of olcumler) {
  console.log(
    o.ad.padEnd(32) + " | " +
    String(o.bayt).padStart(8) + " | " +
    (100 * o.bayt / kaynak).toFixed(1).padStart(10) + " | " +
    o.ms.toFixed(0).padStart(8)
  );
}
EOF
node olc.mjs
```

{/* mask: (?<=\|)[ ]+[0-9]+$ */}
```text
kaynak veritabani: 21.684.224 bayt

yedek bicimi                     |     bayt | kaynagin % | sure (ms)
---------------------------------|----------|------------|----------
mantiksal (.dump)                | 24561891 |      113.3 |      524
mantiksal + sikistirma           |  3997313 |       18.4 |     1243
fiziksel (dosya kopyasi)         | 21684224 |      100.0 |        9
fiziksel (tutarli anlik goruntu) | 21684224 |      100.0 |       57
```

Dört satır dört ayrı karar anlatıyor.

Mantıksal yedek kaynaktan **büyük** çıktı. Sezgiye aykırı görünür ama nedeni açıktır:
sayıların ve tarihlerin ikili gösterimi metne çevrildiğinde şişer, her satır için ekleme
deyiminin metni yeniden yazılır. Buna karşılık dizinler dosyaya yazılmaz — yalnız
tanımları yazılır — bu da kısmen dengeler.

Sıkıştırılmış mantıksal yedek kaynağın beşte birinin altına indi. Metin yedeği son derece
sıkıştırılabilirdir, çünkü aynı deyim kalıbı satır sayısı kadar tekrarlanır. Bunun bedeli
süredir: sıkıştırma bu ölçümde yedek alma süresini birkaç katına çıkardı.

Dosya kopyası, mantıksal yedekten belirgin biçimde hızlıdır. Aradaki fark bir ölçek
sorusudur: mantıksal yedek her satırı okur, metne çevirir ve yazar; dosya kopyası yalnız
baytları taşır.

#### Fiziksel Yedeğin Tutarlılık Sorunu

Yukarıdaki tabloda iki fiziksel satır var ve boyutları aynı. Aralarındaki fark boyutta
değil, güvenilirliktedir.

Çalışan bir veritabanının dosyasını doğrudan kopyalamak **tutarsız kopya** üretebilir.
Kopyalama sürerken motor dosyanın başına ve sonuna yazmayı sürdürür; kopyanın başı bir
ana, sonu başka bir ana aittir. Ortaya çıkan dosya hiçbir zaman var olmamış bir duruma
karşılık gelir ve açıldığında bozuk görünür — ya da daha kötüsü, açılır ve yanlış veri
verir.

Bu yüzden fiziksel yedek ya veritabanı durdurulmuşken alınır, ya da motorun kendi tutarlı
anlık görüntü aracıyla alınır. Tablodaki dördüncü satır bu araçtır: motor, tek bir tutarlı
noktadan okuyarak yeni bir dosya üretir. Ölçümde ham dosya kopyasının birkaç katı sürmesinin
nedeni budur; ödenen fark tutarlılığın bedelidir.

Aracın adı ve çağrılış biçimi motora göre değişir. Değişmeyen kural: **çalışan bir
veritabanının dosyası `cp` ile yedeklenmez.**

#### Artımlı Yedek

Tam yedek her seferinde bütün veriyi taşır. Veritabanı büyüdükçe bu, hem sürede hem
saklama alanında dayanılmaz olur. **Artımlı yedek (incremental backup)**, son yedekten bu
yana değişen kısmı taşır.

Değişenin ne olduğunu belirlemenin en yalın yolu bir **su işaretidir (watermark)**: son
yedekte yer alan en büyük birincil anahtar değeri. Aşağıdaki blok tam yedeği alıyor,
sonra altı bin yeni ödünç kaydı ekliyor, ardından yalnız su işaretinden sonraki satırları
çıkarıyor ve iki dosyayı karşılaştırıyor.

Blok aynı zamanda geri yüklemeyi de yapıyor. **Geri yükleme yıkıcı bir işlemdir:** hedef
veritabanının üzerine yazar. Buradaki `rm -f` ve geri yükleme komutları yalnız bu blokta
üretilen geçici dosyalar üzerinde çalışır; gerçek bir kurulumda geri yükleme hedefi her
zaman yeni ve boş bir veritabanıdır, mevcut olan değil.

```bash
cat > kur.sql <<'SQL'
CREATE TABLE sube(id INTEGER PRIMARY KEY, ad TEXT NOT NULL);
CREATE TABLE uye(id INTEGER PRIMARY KEY, ad TEXT NOT NULL, sube_id INT NOT NULL);
CREATE TABLE kitap(id INTEGER PRIMARY KEY, baslik TEXT NOT NULL, sube_id INT NOT NULL);
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT NOT NULL, uye_id INT NOT NULL,
                   sube_id INT NOT NULL, alis TEXT NOT NULL, iade TEXT);
INSERT INTO sube VALUES (1,'Merkez'),(2,'Sahil'),(3,'Tepe');
WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n<20000)
INSERT INTO uye(id,ad,sube_id) SELECT n,'Uye-'||n,(n%3)+1 FROM s;
WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n<50000)
INSERT INTO kitap(id,baslik,sube_id) SELECT n,'Kitap-'||n,(n%3)+1 FROM s;
WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n<300000)
INSERT INTO odunc(id,kitap_id,uye_id,sube_id,alis,iade)
SELECT n,(n%50000)+1,(n%20000)+1,(n%3)+1,
       date('2023-01-01','+'||(n%900)||' day'),
       CASE WHEN n%4=0 THEN NULL ELSE date('2023-01-01','+'||((n%900)+14)||' day') END
FROM s;
CREATE INDEX odunc_uye ON odunc(uye_id);
CREATE INDEX odunc_sube_alis ON odunc(sube_id, alis);
SQL
rm -f kutuphane.db geri.db tam.sql artimli.sql
sqlite3 kutuphane.db < kur.sql

sqlite3 kutuphane.db .dump > tam.sql

sqlite3 kutuphane.db "WITH RECURSIVE s(n) AS (SELECT 300001 UNION ALL SELECT n+1 FROM s WHERE n<306000)
INSERT INTO odunc(id,kitap_id,uye_id,sube_id,alis,iade)
SELECT n,(n%50000)+1,(n%20000)+1,(n%3)+1,date('2025-06-01','+'||(n%30)||' day'),NULL FROM s;"

sqlite3 kutuphane.db <<'ART' > artimli.sql
.mode insert odunc
SELECT * FROM odunc WHERE id > 300000 ORDER BY id;
ART

sqlite3 geri.db < tam.sql
sqlite3 geri.db < artimli.sql

echo "tam yedek     : $(wc -c < tam.sql | tr -d ' ') bayt"
echo "artimli yedek : $(wc -c < artimli.sql | tr -d ' ') bayt"
echo "kaynak satir  : $(sqlite3 kutuphane.db 'SELECT COUNT(*) FROM odunc;')"
echo "geri satir    : $(sqlite3 geri.db 'SELECT COUNT(*) FROM odunc;')"
echo "kaynak karma  : $(sqlite3 kutuphane.db '.sha3sum --schema --sha3-256')"
echo "geri karma    : $(sqlite3 geri.db '.sha3sum --schema --sha3-256')"
```

```text
tam yedek     : 24561891 bayt
artimli yedek : 381792 bayt
kaynak satir  : 306000
geri satir    : 306000
kaynak karma  : d97019d7e61335911bef2079db99030dd2f5e9938d4e24a769d85e56edef1eb6
geri karma    : d97019d7e61335911bef2079db99030dd2f5e9938d4e24a769d85e56edef1eb6
```

Artımlı yedek tam yedeğin yüzde bir buçuğunun biraz üzerinde. Günlük tam yedek yerine
haftalık tam ve günlük artımlı almanın nedeni bu orandır.

#### Eşitliğin Kanıtı

Son iki satır bu dersin asıl noktasıdır. Karma değeri, veritabanının **içeriğinden**
hesaplanır: tablo adları, sütun tanımları ve satırların değerleri. Dosyanın fiziksel
düzeninden, boş sayfalarından ya da satırların diskteki sırasından etkilenmez.

İki karmanın eşit çıkması, geri yüklenen veritabanının kaynakla mantıksal olarak aynı
olduğunu gösterir. Satır saymak bunu göstermez — sayılar tutup değerler bozuk olabilir.
Gözle bakmak hiç göstermez.

Bu alışkanlığın adı vardır ve sonraki derslerin ikisi buna ayrılmıştır: bir yedeğin
geçerliliği, alındığı anda değil geri yüklendiği anda bilinir. "Yedek alıyoruz" cümlesi
bir güvence değildir; "yedeği geri yükleyip karmasını karşılaştırıyoruz" cümlesi bir
güvencedir.

#### Su İşaretinin Kaçırdıkları

Yukarıdaki artımlı yedek yalnız **eklenen** satırları yakalar. Su işareti birincil anahtar
üzerinde çalışır ve birincil anahtar artan değerler üretir; dolayısıyla:

- Su işaretinden **önceki** bir satır güncellenmişse yakalanmaz.
- Su işaretinden önceki bir satır silinmişse yakalanmaz.
- Şema değişmişse yakalanmaz.

Kütüphane örneğinde bu üç boşluk somuttur. Bir kitabın iadesi, eski bir ödünç satırının
`iade` sütununu günceller — su işaretinden önceki bir satırdır ve artımlı yedeğe girmez.
Tam yedek ile artımlı yedeği birleştirerek geri yüklenen veritabanında o kitap hâlâ
ödünçte görünür.

Değişim zamanı taşıyan bir sütun eklemek güncellemeleri yakalar ama silmeleri yakalamaz —
silinen satırın değişim zamanı da silinmiştir. Silmeleri yakalamanın yolu, satırların
değil **işlemlerin** kaydını tutmaktır. Bu kayıt zaten vardır: motorun kalıcılık için
yazdığı günlük.

#### Yedek Planının Üç Sayısı

Bir yedekleme planı üç sayıyla anlatılır.

**Kurtarma noktası hedefi (recovery point objective)**, kabul edilen en fazla veri kaybı
süresidir. Günde bir yedek alınıyorsa bu sayı yirmi dört saattir: arıza yedekten hemen
önce olursa bir günlük ödünç kaydı kaybolur.

**Kurtarma süresi hedefi (recovery time objective)**, sistemin yeniden çalışır hâle
gelmesi için kabul edilen en uzun süredir. Bu sayı yedeğin boyutuyla değil, geri yükleme
süresiyle ilgilidir — ve yukarıdaki ölçümde geri yükleme, yedek almadan uzun sürdü.
Mantıksal yedeğin ucuz saklanması, pahalı geri yüklenmesiyle ödenir.

**Saklama süresi (retention)**, yedeklerin ne kadar tutulacağıdır. Yalnız en son yedeği
tutmak tehlikelidir: bozulma fark edilmeden yedeklenmişse, tek yedek de bozuktur. Yedekler
farklı zamanlardan ve farklı ortamlardan oluşan bir küme olarak tutulur; en az bir kopyası
veritabanının bulunduğu makineden ayrı bir yerde durur.

#### Özet

- Mantıksal yedek deyimlerden oluşur, taşınabilirdir ve iyi sıkışır; fiziksel yedek
  dosyaların kopyasıdır, alınması hızlıdır ve aynı motora bağlıdır.
- Mantıksal yedek kaynaktan büyük olabilir; sıkıştırma boyutu birkaç kata düşürür,
  bedeli süredir.
- Çalışan bir veritabanının dosyası doğrudan kopyalanamaz; tutarlı fiziksel yedek ya
  durdurulmuş sistemden ya motorun anlık görüntü aracıyla alınır.
- Artımlı yedek tam yedeğin küçük bir bölümü kadar yer tutar; su işareti tabanlı artımlı
  yedek eklemeleri yakalar, güncelleme ve silmeleri kaçırır.
- Geri yüklenen kopyanın kaynakla eşitliği içerik karmasıyla kanıtlanır; satır saymak
  yeterli değildir.
- Yedek planı kurtarma noktası hedefi, kurtarma süresi hedefi ve saklama süresiyle
  tanımlanır.

#### Sonraki Adım

Su işareti tabanlı artımlı yedeğin üç boşluğu — güncellemeler, silmeler ve şema
değişiklikleri — hepsi aynı nedenden kaynaklanıyordu: satırların son hâline bakmak,
aralarında ne olduğunu anlatmaz. Aradaki her değişikliği sırayla kaydeden bir kaynak
zaten var; motorun kalıcılık için yazdığı günlük. Sonraki ders bu günlüğün yedekle
birlikte kullanılmasını ele alıyor: bir tam yedeğin üzerine günlük kayıtlarını belirli bir
ana kadar uygulayarak veritabanını dün öğlen on birdeki hâline döndürmek.

### Zaman Noktasına Kurtarma

Source: https://academia.sh/tr/kurslar/veritabani-yonetimi/zaman-noktasina-kurtarma

Taban yedek ile günlük arşivinin birlikte kullanılması: kurtarma penceresi, hedef anın seçilmesi, günlüğün hedefe kadar ileri sarılması, kaza sonrası işin kaybı ve kısmi kurtarmanın hesaplanması.

Önceki ders su işareti tabanlı artımlı yedeğin üç boşluğunu bıraktı: güncellemeler,
silmeler ve şema değişiklikleri yakalanmıyordu. Üçünün ortak nedeni aynıydı — satırların
son hâline bakmak, aralarında ne olduğunu anlatmaz.

Aradaki her değişikliği sırayla kaydeden bir kaynak zaten var. Motor, kalıcılığı sağlamak
için her değişikliği veri dosyalarına yazmadan önce bir **günlüğe** yazar; bu düzenek bu
kursun Motor Mimarisi konusunda ele alındı. O günlük saklanırsa, elde yalnız veritabanının
iki anlık görüntüsü değil, ikisi arasındaki bütün hareket bulunur.

**Zaman noktasına kurtarma (point-in-time recovery)** bunun kullanımıdır: bir taban yedek
geri yüklenir, üzerine günlük kayıtları seçilen ana kadar uygulanır ve veritabanı o andaki
hâline döner.

#### Kurtarma Penceresi

Yöntem iki parçadan oluşur.

**Taban yedek (base backup)**, belirli bir andaki tam kopyadır — önceki dersteki mantıksal
ya da fiziksel yedek. Kurtarmanın başlangıç noktasıdır.

**Günlük arşivi (log archive)**, taban yedeğin alındığı andan bugüne kadarki günlük
kayıtlarının saklanmış hâlidir. Motor günlüğü kendi çalışma alanında sınırlı süre tutar;
arşivleme, dolan günlük parçalarının ayrı bir yere kopyalanmasıdır.

İkisi birlikte bir **kurtarma penceresi** tanımlar: taban yedeğin anı ile arşivin son anı
arasındaki her noktaya dönülebilir. Pencerenin başı taban yedeğin yaşıyla, sonu arşivin
sürekliliğiyle belirlenir. Arşivleme sessizce durursa pencere büyümeyi bırakır ve kimse
fark etmeden kurtarma yeteneği taban yedeğin anına geriler.

#### Bir Kaza ve Kurtarılması

Aşağıdaki blok bir günü canlandırıyor. Sabah dokuzda taban yedek alınıyor. Gün boyunca
ödünç kayıtları açılıyor ve iadeler işleniyor; her değişiklik zaman damgasıyla günlüğe
yazılıyor. Saat 11:15'te koşulsuz bir silme deyimi çalışıyor — Sahil şubesinin bütün ödünç
kayıtları gidiyor. Kimse fark etmiyor, iş akşama kadar sürüyor.

Kurtarma, taban yedeği yeni bir veritabanına yükleyip günlüğü 11:15'ten **öncesine** kadar
uygulamaktır.

Bloktaki `DELETE FROM odunc WHERE sube_id=2;` deyimi kasıtlı olarak yıkıcıdır ve dersin
konusunu oluşturur. Yalnız bu blokta üretilen geçici dosyalar üzerinde çalışır. Gerçek bir
sistemde koşulsuz ya da geniş koşullu silme deyimleri, önce aynı koşulla bir sayım sorgusu
çalıştırılarak sınanır; kurtarma bu alışkanlığın yerine geçmez.

```bash
rm -f kutuphane.db kurtarilan.db taban_ani.db taban.sql gunluk.log

sqlite3 kutuphane.db <<'SQL'
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT NOT NULL, uye_id INT NOT NULL,
                   sube_id INT NOT NULL, alis TEXT NOT NULL, iade TEXT);
WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n<2000)
INSERT INTO odunc(id,kitap_id,uye_id,sube_id,alis,iade)
SELECT n,(n%500)+1,(n%300)+1,(n%3)+1,'2025-06-01',NULL FROM s;
SQL

# 09:00 — taban yedek
sqlite3 kutuphane.db .dump > taban.sql

# Gunluk: her satir "zaman|deyim". 11:15'te kosulsuz bir silme var.
node - <<'EOF' > gunluk.log
const satir = [];
const zaman = (dk) => new Date(Date.UTC(2025, 5, 10, 9, 0, 0) + dk * 60000)
  .toISOString().slice(0, 19);
for (let i = 1; i <= 300; i++) {
  const dk = i * 0.44;
  if (i % 3 === 0) {
    satir.push(zaman(dk) + "|UPDATE odunc SET iade='2025-06-10' WHERE id=" + (i * 5) + ";");
  } else {
    const id = 2000 + i;
    satir.push(zaman(dk) + "|INSERT INTO odunc VALUES(" + id + "," + ((id % 500) + 1) +
               "," + ((id % 300) + 1) + "," + ((id % 3) + 1) + ",'2025-06-10',NULL);");
  }
}
satir.push("2025-06-10T11:15:00|DELETE FROM odunc WHERE sube_id=2;");
for (let i = 1; i <= 200; i++) {
  const id = 3000 + i;
  satir.push(zaman(136 + i * 1.7) + "|INSERT INTO odunc VALUES(" + id + "," + ((id % 500) + 1) +
             "," + ((id % 300) + 1) + "," + ((id % 3) + 1) + ",'2025-06-10',NULL);");
}
console.log(satir.join("\n"));
EOF

# Gunlugun tamami canli veritabanina uygulanir.
awk -F'|' '{print $2}' gunluk.log | sqlite3 kutuphane.db

# Kurtarma: taban yedek + gunlugun hedef ana kadarki bolumu.
HEDEF='2025-06-10T11:15:00'
sqlite3 kurtarilan.db < taban.sql
awk -F'|' -v t="$HEDEF" '$1 < t {print $2}' gunluk.log | sqlite3 kurtarilan.db

sqlite3 taban_ani.db < taban.sql

echo "gunluk kaydi   : $(wc -l < gunluk.log | tr -d ' ')"
echo "uygulanan kayit: $(awk -F'|' -v t="$HEDEF" '$1 < t' gunluk.log | wc -l | tr -d ' ')"
echo "atlanan kayit  : $(awk -F'|' -v t="$HEDEF" '$1 >= t' gunluk.log | wc -l | tr -d ' ')"

sqlite3 -box -header kutuphane.db "
ATTACH 'kurtarilan.db' AS k; ATTACH 'taban_ani.db' AS t;
SELECT 'taban yedek (09:00)' AS durum, COUNT(*) AS satir, SUM(sube_id=2) AS sube2, SUM(iade IS NOT NULL) AS iadeli FROM t.odunc
UNION ALL SELECT 'kurtarilan (11:15 oncesi)', COUNT(*), SUM(sube_id=2), SUM(iade IS NOT NULL) FROM k.odunc
UNION ALL SELECT 'canli (17:00, kazadan sonra)', COUNT(*), SUM(sube_id=2), SUM(iade IS NOT NULL) FROM main.odunc;

SELECT (SELECT COUNT(*) FROM k.odunc WHERE id NOT IN (SELECT id FROM main.odunc)) AS yalniz_kurtarilanda,
       (SELECT COUNT(*) FROM main.odunc WHERE id NOT IN (SELECT id FROM k.odunc)) AS yalniz_canlida;"
```

```text
gunluk kaydi   : 501
uygulanan kayit: 300
atlanan kayit  : 201
┌──────────────────────────────┬───────┬───────┬────────┐
│            durum             │ satir │ sube2 │ iadeli │
├──────────────────────────────┼───────┼───────┼────────┤
│ taban yedek (09:00)          │ 2000  │ 667   │ 0      │
│ kurtarilan (11:15 oncesi)    │ 2200  │ 767   │ 100    │
│ canli (17:00, kazadan sonra) │ 1633  │ 67    │ 100    │
└──────────────────────────────┴───────┴───────┴────────┘
┌─────────────────────┬────────────────┐
│ yalniz_kurtarilanda │ yalniz_canlida │
├─────────────────────┼────────────────┤
│ 767                 │ 200            │
└─────────────────────┴────────────────┘
```

#### Sonuçların Okunması

İlk tablo üç durumu yan yana koyuyor.

Taban yedek anında iki bin satır vardı ve hiçbiri iade edilmemişti. Yalnız bu yedekten
dönmek, sabah dokuzdan sonra yapılan her şeyi silmek demektir — iki yüz yeni ödünç kaydı
ve yüz iade işlemi.

Kurtarılan kopya, günlüğün üç yüz kaydı uygulandıktan sonraki hâl. Sahil şubesinin yedi
yüz altmış yedi kaydı yerinde, yüz iade işlenmiş durumda. Bu, kazadan hemen önceki gerçek
durumdur ve taban yedekten değil, taban yedek artı günlükten elde edildi.

Canlı veritabanında Sahil şubesinden yalnız altmış yedi kayıt var — hepsi kazadan sonra
açılmış yeni kayıtlar. Yedi yüz kayıt silinmiş durumda.

#### Kurtarmanın Bedeli

İkinci tablo, kurtarmanın gerçek maliyetini gösteriyor. Kurtarılan kopyada olup canlıda
olmayan yedi yüz altmış yedi satır, silmenin götürdükleri. Canlıda olup kurtarılan kopyada
olmayan iki yüz satır ise **kurtarmanın götürecekleri**: kazadan sonra yapılmış olağan
işler.

Kurtarılan kopyayı doğrudan canlının yerine koymak, o iki yüz kaydı silmek demektir. Bu,
zaman noktasına kurtarmanın çoğu kez atlanan yanıdır: hedef anı seçmek, o andan sonraki
her şeyi feda etmektir.

Kaza ile fark ediliş arasında geçen süre uzadıkça bu maliyet büyür. Altı saat sonra fark
edilen bir silme, iki yüz kayıtlık iş kaybı üretir; iki gün sonra fark edilen aynı silme,
iki günlük iş kaybı üretir.

Bu yüzden işletim pratiğinde tercih edilen yol, kurtarılan kopyayı canlının yerine koymak
değil, **yanına** koymaktır. Kurtarılan kopya ayrı bir veritabanı olarak açılır, eksik
satırlar oradan çekilir ve canlıya eklenir. İkinci tablonun ilk sütunu tam olarak bu
işlemin girdisidir: hangi satırların taşınacağı sayılmış durumdadır. Bu yol yalnız
silmelerde işler; şema bozulması ya da yaygın bir güncelleme hatasında tam geri dönüş
gerekir.

#### Hedef Anın Seçilmesi

Kurtarma hedefi iki biçimde verilebilir.

**Zaman damgasıyla.** Yukarıdaki örnekteki gibi: "şu ana kadar uygula". Okunması kolaydır
ama kesin değildir — aynı saniye içinde birden çok işlem bitmiş olabilir ve hepsi ya
alınır ya alınmaz.

**İşlem tanımlayıcısıyla.** Motorların günlüğe yazdığı sıra numarası ya da işlem
kimliğiyle: "şu işleme kadar uygula, o dâhil değil". Kesin sınır verir. Hatalı deyimin
tanımlayıcısını bulmak, günlüğü okuyabilmeyi gerektirir; motorların çoğu bunun için bir
inceleme aracı sağlar.

Pratikte iki adım birlikte kullanılır: önce zaman damgasıyla kabaca daraltılır, sonra o
aralıktaki günlük kayıtları okunarak hatalı deyimin tam sınırı bulunur. Kurtarmanın önce
bir sınama ortamında yapılması, hedefin yanlış seçilmesi hâlinde ikinci bir deneme hakkı
bırakır.

#### Bu Modelin Gerçekten Ayrıldığı Nokta

Yukarıdaki günlük, deyim metinlerinden oluşuyor ve yeniden çalıştırılarak uygulanıyor. Bu
bir modeldir; kurtarma penceresi, ileri sarma ve hedef seçimi kavramlarını doğru gösterir
ama gerçek motorların günlüğü böyle değildir.

Gerçek günlükler değişikliği fiziksel düzeyde kaydeder: hangi sayfanın hangi baytları ne
oldu, ya da hangi satır hangi değerlere geçti. Bunun nedeni **belirlenimci olmayan
deyimlerdir**. Şimdiki zamanı, rastgele bir sayıyı ya da bir sıra üretecini kullanan bir
deyim, yeniden çalıştırıldığında farklı sonuç üretir. Deyim metnini tekrar oynatan bir
kurtarma, kaynaktan sapmış bir veritabanı üretir.

İkinci fark işlem sınırlarıdır. Gerçek günlük, hangi kaydın hangi işleme ait olduğunu ve
o işlemin kesinleşip kesinleşmediğini taşır. Kurtarma sırasında kesinleşmemiş işlemlerin
kayıtları geri alınır. Yukarıdaki modelde her satır bağımsızdır; işlem kavramı yoktur.

Kavramsal olarak değişmeyen üç şey vardır: kurtarma taban yedekten başlar, günlük sırayla
ileri sarılır, hedef anda durur.

#### Özet

- Zaman noktasına kurtarma, taban yedeğin üzerine günlük kayıtlarının seçilen ana kadar
  uygulanmasıdır.
- Kurtarma penceresinin başı taban yedeğin anı, sonu günlük arşivinin son anıdır;
  arşivleme durursa pencere sessizce daralır.
- Hedef anın seçilmesi, o andan sonraki bütün işi feda etmek anlamına gelir; kayıp,
  kazanın fark edilme süresiyle büyür.
- Kurtarılan kopyayı canlının yanına koyup eksik satırları taşımak, silme kazalarında iş
  kaybını önler.
- Hedef, zaman damgasıyla kabaca, işlem tanımlayıcısıyla kesin biçimde verilir.
- Gerçek günlükler deyim değil fiziksel değişiklik kaydeder; belirlenimci olmayan
  deyimler, deyim tekrarına dayalı kurtarmayı güvenilmez kılar.

#### Sonraki Adım

Bu dersteki kurtarma işledi, çünkü taban yedek okunabilir ve günlük eksiksizdi. İkisinin
de doğru olduğu, ancak kurtarma denenerek bilinir — ve kurtarmanın denendiği an, gerçek
bir arıza anı olmamalıdır. Sonraki ders yedek doğrulamayı ele alıyor: bir yedeğin
bütünlüğünün karma ile denetlenmesi, bozulmuş bir yedeğin geri yüklemeden önce fark
edilmesi ve düzenli kurtarma tatbikatının neyi güvence altına aldığı.

### Yedek Doğrulama

Source: https://academia.sh/tr/kurslar/veritabani-yonetimi/yedek-dogrulama

Yedeğin geçerliliğinin geri yükleme anından önce bilinmesi: dosya karmasıyla bütünlük denetimi, sessiz bozulmanın içerik karmasıyla yakalanması, yapısal denetimin kapsamı ve düzenli kurtarma tatbikatının kurulması.

Önceki iki ders yedek almayı ve yedekten dönmeyi kurdu. İkisi de aynı varsayıma
dayanıyordu: yedek dosyası okunabilir ve içeriği doğru. Bu varsayım sınanmadığı sürece bir
varsayımdır ve sınanmadığı sürece de doğru kabul edilir — arıza anına kadar.

Yedeklemenin en pahalı kusuru, alınmayan yedek değildir; **alındığı sanılan yedektir**.
Disk dolduğu için yarım yazılmış bir dosya, kopyalama sırasında bozulmuş bir bayt ya da
haftalardır sessizce başarısız olan bir yedekleme görevi, hepsi aynı sonucu verir:
kurtarma anında elde kullanılamaz bir dosya.

Bu ders yedeğin geçerliliğini **geri yükleme anından önce** bilmenin yollarını kuruyor.
Üç ayrı denetim vardır ve üçü ayrı şeyleri yakalar.

#### Üç Denetim, Üç Kapsam

**Dosya karması.** Yedek alındığı anda dosyanın karması hesaplanır ve dosyanın yanında
saklanır. Sonradan yapılan karşılaştırma, dosyanın tek baytının bile değişmediğini
gösterir. Aktarım ve saklama sırasındaki bozulmaları yakalar; yedeğin **içeriğinin** doğru
olduğunu göstermez.

**Yapısal bütünlük denetimi.** Motorun kendi denetimi, veritabanı dosyasının sayfa
yapısının, ağaç bağlantılarının ve dizinlerin tutarlı olup olmadığını sınar. Yapıyı
denetler, değerleri denetlemez.

**İçerik karması.** Veritabanının satır değerlerinden hesaplanan karma, yedek alındığı
anda kaydedilir ve geri yüklendikten sonra yeniden hesaplanarak karşılaştırılır. Değerdeki
bozulmayı yakalayan tek denetim budur.

#### Mantıksal Yedeğin İki Bozulma Biçimi

Aşağıdaki blok bir yedek alıyor, iki ayrı biçimde bozuyor ve üç dosyayı aynı tatbikattan
geçiriyor. Birinci bozulma tek bir baytın değişmesi — bir ödünç kaydının kitap numarası
başka bir sayıya dönüşüyor. İkincisi dosyanın son onda birinin kesilmesi, yani yarım
yazılmış bir yedek.

Blok yalnız kendi ürettiği geçici dosyalar üzerinde çalışır ve hiçbir yedeği yerinde
değiştirmez; bozuk sürümler ayrı dosyalara yazılır. Gerçek bir kurulumda tatbikat da
böyledir: yedek dosyasına dokunulmaz, kopyası üzerinde çalışılır.

```bash
rm -f kutuphane.db yedek.sql bozuk.sql kesik.sql tatbikat.db

sqlite3 kutuphane.db <<'SQL'
CREATE TABLE uye(id INTEGER PRIMARY KEY, ad TEXT NOT NULL, sube_id INT NOT NULL);
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT NOT NULL, uye_id INT NOT NULL,
                   sube_id INT NOT NULL, alis TEXT NOT NULL, iade TEXT);
WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n<5000)
INSERT INTO uye(id,ad,sube_id) SELECT n,'Uye-'||n,(n%3)+1 FROM s;
WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n<40000)
INSERT INTO odunc(id,kitap_id,uye_id,sube_id,alis,iade)
SELECT n,(n%500)+1,(n%5000)+1,(n%3)+1,'2025-06-01',NULL FROM s;
SQL

sqlite3 kutuphane.db .dump > yedek.sql
shasum -a 256 yedek.sql | awk '{print "yedek karmasi (alindigi an): " substr($1,1,16)}'

# Iki bozulma bicimi uretilir: tek bayt degisimi ve dosyanin kesilmesi.
node -e '
const fs = require("node:fs");
const b = fs.readFileSync("yedek.sql");
const im = b.indexOf("VALUES(20000,");
const yer = im + "VALUES(20000,".length;       // kitap_id alaninin ilk basamagi
const c = Buffer.from(b);
c[yer] = b[yer] === 0x39 ? 0x38 : b[yer] + 1;  // basamagi degistir
fs.writeFileSync("bozuk.sql", c);
fs.writeFileSync("kesik.sql", b.subarray(0, Math.floor(b.length * 0.9)));
console.log("kaynak: " + b.subarray(im, im + 37).toString());
console.log("bozuk : " + c.subarray(im, im + 37).toString());
'

kaynak_karma=$(sqlite3 kutuphane.db '.sha3sum --schema --sha3-256')
echo
echo "yedek       | dosya karmasi | geri yukleme | odunc satir | icerik karmasi"
echo "------------|---------------|--------------|-------------|---------------"
for dosya in yedek.sql bozuk.sql kesik.sql; do
  rm -f tatbikat.db
  dosya_karma=$(shasum -a 256 "$dosya" | awk '{print substr($1,1,8)}')
  if sqlite3 tatbikat.db < "$dosya" 2>/dev/null; then yukleme="tamam"; else yukleme="HATA"; fi
  satir=$(sqlite3 tatbikat.db 'SELECT COUNT(*) FROM odunc;' 2>/dev/null || echo "yok")
  icerik=$(sqlite3 tatbikat.db '.sha3sum --schema --sha3-256' 2>/dev/null | tail -1)
  if [ "$icerik" = "$kaynak_karma" ]; then esit="ESIT"; else esit="FARKLI"; fi
  printf "%-11s | %-13s | %-12s | %11s | %s\n" "$dosya" "$dosya_karma" "$yukleme" "$satir" "$esit"
done
rm -f tatbikat.db
```

```text
yedek karmasi (alindigi an): d33cffac34951285
kaynak: VALUES(20000,1,1,3,'2025-06-01',NULL)
bozuk : VALUES(20000,2,1,3,'2025-06-01',NULL)

yedek       | dosya karmasi | geri yukleme | odunc satir | icerik karmasi
------------|---------------|--------------|-------------|---------------
yedek.sql   | d33cffac      | tamam        |       40000 | ESIT
bozuk.sql   | fb6cf157      | tamam        |       40000 | FARKLI
kesik.sql   | 799bd4e0      | HATA         |         yok | FARKLI
```

Orta satır bu dersin gerekçesidir. Bozuk yedek **hatasız geri yüklendi**. Satır sayısı
kaynakla aynı çıktı. Tablolar yerinde, dizinler kurulu, sorgular çalışıyor. Yalnız
içerik karması farklı — ve fark, kırk bin satırın birindeki bir sayıdan ibaret.

Bu bozulma biçimi kendini göstermez. Kırk bin ödünç kaydından birinin kitap numarası
yanlış olduğunda hiçbir sorgu hata vermez; yalnız o kitabı arayan kişi bulamaz. Yedeği
"başarıyla geri yükledik" diye kabul eden bir tatbikat bu kusuru geçirir.

Alt satır ise farklı bir şey öğretiyor. Kesik yedek geri yüklenmedi ve tablo bile
oluşmadı. Nedeni, mantıksal yedeğin tek bir işlem olarak yazılmasıdır: dosyanın başında
bir işlem başlatılır, sonunda kesinleştirilir. Dosya yarım kaldığında kesinleştirme
gelmez ve her şey geri alınır. Bu, kısmi geri yüklemeden iyidir — yarım bir veritabanı,
boş bir veritabanından tehlikelidir, çünkü kullanılabilir görünür.

Dosya karması sütunu üç dosya için de farklıdır. Yedek alındığı anda kaydedilen karma ile
karşılaştırıldığında iki bozulma da **geri yüklemeye hiç girişmeden** yakalanırdı. Bu, en
ucuz denetimdir ve her yedeğin yanında saklanması gereken tek satırlık bilgidir.

#### Fiziksel Yedekte Yapısal Denetim

Fiziksel yedekte durum başkadır: dosya bir metin değil, sayfalardan oluşan bir yapıdır.
Motor bu yapıyı kendisi sınayabilir. Aşağıdaki blok aynı veritabanının iki kopyasını iki
ayrı konumdan bozuyor — biri bir sayfa başlığını, diğeri bir veri değerini.

```bash
rm -f fiziksel.db

sqlite3 fiziksel.db <<'SQL'
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT NOT NULL, uye_id INT NOT NULL,
                   sube_id INT NOT NULL, alis TEXT NOT NULL);
WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n<40000)
INSERT INTO odunc(id,kitap_id,uye_id,sube_id,alis)
SELECT n,(n%500)+1,(n%5000)+1,(n%3)+1,'2025-06-01' FROM s;
CREATE INDEX odunc_uye ON odunc(uye_id);
SQL

kaynak_karma=$(sqlite3 fiziksel.db '.sha3sum --schema --sha3-256')

# Iki ayri konumda tek bayt bozulur: sayfa basligi ve bir veri degeri.
# Veri degerinin konumu dosyada ilk tarih metni aranarak bulunur.
yer_veri=$(node -e '
const b = require("node:fs").readFileSync("fiziksel.db");
console.log(b.indexOf("2025-06-01") + 5);
')
bozdur() {
  cp fiziksel.db "$1"
  node -e "
const fs = require('node:fs');
const fd = fs.openSync('$1', 'r+');
const b = Buffer.alloc(1);
fs.readSync(fd, b, 0, 1, $2);
fs.writeSync(fd, Buffer.from([b[0] ^ 0xff]), 0, 1, $2);
fs.closeSync(fd);
"
}
bozdur bozuk-40960.db 40960
bozdur bozuk-veri.db "$yer_veri"

echo "dosya              | integrity_check | icerik karmasi"
echo "-------------------|-----------------|---------------"
for d in fiziksel.db bozuk-40960.db bozuk-veri.db; do
  ic=$(sqlite3 "$d" 'PRAGMA integrity_check;' 2>&1 | head -1)
  karma=$(sqlite3 "$d" '.sha3sum --schema --sha3-256' 2>/dev/null | tail -1)
  if [ "$karma" = "$kaynak_karma" ]; then esit="ESIT"; else esit="FARKLI"; fi
  printf "%-18s | %-15s | %s\n" "$d" "${ic:0:15}" "$esit"
done
rm -f bozuk-40960.db bozuk-veri.db
```

```text
dosya              | integrity_check | icerik karmasi
-------------------|-----------------|---------------
fiziksel.db        | ok              | ESIT
bozuk-40960.db     | Error: stepping | FARKLI
bozuk-veri.db      | ok              | FARKLI
```

Bozulan bayt bir sayfa başlığındaysa yapısal denetim bunu yakalıyor: motor sayfayı
okuyamıyor ve dosyanın bozuk olduğunu bildiriyor. Bozulan bayt bir veri değerinin içindeyse
yapısal denetim **`ok` diyor** — yapı tutarlıdır, yalnız değer yanlıştır.

Sayfa başlığının konumu (`40960`) bu motorun sayfa boyutuna bağlıdır. Veri değerinin
konumu ise sabit yazılmaz: blok dosyada ilk tarih metnini arar ve bir baytını bozar. Rastgele
seçilmiş bir konum bir kayıt başlığına denk gelebilir ve bu, sonucu değiştirir. Sonucun
kendisi motora özgü değildir: sayfa yapısını denetleyen hiçbir araç, sayfanın içindeki bir sayının doğru sayı
olduğunu bilemez.

Bu, yapısal denetimin sınırıdır ve içerik karmasının neden ayrıca gerektiğini gösterir.

#### Kurtarma Tatbikatı

Yukarıdaki iki blok bir tatbikatın parçalarıdır. Bütünü şu adımlardan oluşur ve düzenli
aralıklarla, kimsenin acelesi yokken çalıştırılır.

**Yedek dosyasının karması, alındığı andaki karmayla karşılaştırılır.** Uymuyorsa
saklama ya da aktarım katmanında bir sorun var demektir; geri yüklemeye girişilmez.

**Yedek, üretim dışı bir ortama geri yüklenir.** Hedef her zaman yeni ve boş bir
veritabanıdır. Çalışan bir veritabanının üzerine tatbikat yapılmaz.

**Yapısal bütünlük denetimi çalıştırılır.** Fiziksel yedeklerde sayfa ve dizin
tutarlılığını doğrular.

**İçerik karması, yedeğin alındığı anda kaydedilmiş karmayla karşılaştırılır.** Bu adım,
satır sayısı karşılaştırmasının yerine geçer — sayılar tutup değerler bozuk olabilir.

**Süre ölçülür.** Geri yüklemenin ne kadar sürdüğü, kurtarma süresi hedefinin
karşılanabilir olup olmadığını söyleyen tek sayıdır. Bu sayı veri büyüdükçe artar;
ölçülmediği sürece plan bir varsayımdır.

**Uygulama, kurtarılan kopyaya bağlanır.** Yedeğin geri yüklenmiş olması, uygulamanın
çalışacağı anlamına gelmez: eksik bir dizin, kaydedilmemiş bir görünüm ya da yedeğe
girmemiş bir yapılandırma tablosu ancak burada ortaya çıkar.

Tatbikatın çıktısı bir rapor değil, bir karardır: yedek geçerli ya da değil. Aradaki
"muhtemelen iyidir" durumu, yedeğin doğrulanmamış olmasıyla aynı şeydir.

#### Tatbikatın Sıklığı ve Kapsamı

Sıklık, kurtarma noktası hedefiyle birlikte belirlenir. Yedeği doğrulanmamış geçen her
gün, o günün yedeğinin güvenilmez olması riskidir. Yedekleme günlükse tatbikat da sık
olmalıdır; tam tatbikat pahalıysa, hafif bir bütünlük denetimi her yedek için, tam geri
yükleme tatbikatı daha seyrek çalıştırılır.

Kapsam, yedeklenen her nesneyi içerir. Kütüphane örneğinde ödünç tablosu yedekleniyorsa
ama şube tanımları ayrı bir yapılandırma dosyasında duruyorsa, tatbikat kütüphaneyi
çalışır hâle getirmez. Yedeğin kapsamı, uygulamanın çalışması için gereken her şeydir;
tatbikat bunu ortaya çıkarmanın yoludur.

Son olarak tatbikatın kendisi kayıt bırakır: hangi yedek, hangi tarihte, ne kadar sürede
geri yüklendi ve karmalar uydu mu. Bu kayıt, bir sonraki arızada "en son ne zaman
denedik" sorusuna yanıt verir.

#### Özet

- Yedeğin geçerliliği alındığı anda değil, doğrulandığı anda bilinir; doğrulanmamış yedek
  alınmamış yedekle aynı güvenceyi verir.
- Dosya karması bozulmayı geri yüklemeye girişmeden yakalar ve her yedeğin yanında
  saklanır.
- Tek bayt değişmiş bir mantıksal yedek hatasız geri yüklenebilir ve satır sayısı
  tutabilir; bu bozulmayı yalnız içerik karması yakalar.
- Yapısal bütünlük denetimi sayfa ve dizin tutarlılığını sınar, değerlerin doğruluğunu
  sınamaz.
- Kesik bir mantıksal yedek, tek işlem olarak yazıldığı için kısmi değil boş bir
  veritabanı üretir.
- Kurtarma tatbikatı karma karşılaştırması, yapısal denetim, süre ölçümü ve uygulamanın
  bağlanmasını içerir; çıktısı geçerli/geçersiz kararıdır.

#### Sonraki Adım

Yedek ve kurtarma, veriyi kaybetmemeyi güvence altına alır; sistemin kesintisiz
çalışmasını değil. Kırk dakika süren bir geri yükleme, o kırk dakika boyunca kütüphanenin
ödünç veremeyeceği anlamına gelir. Kesintiyi kısaltmanın yolu, verinin bir kopyasının
başka bir makinede zaten hazır durmasıdır. Sonraki ders fiziksel çoğaltmayı ele alıyor:
birincil sunucunun günlük kayıtlarının bir yedek sunucuya sürekli akıtılması, bu akışın
gecikmesi ve gecikmenin okuma tutarlılığına etkisi.

### Fiziksel Çoğaltma

Source: https://academia.sh/tr/kurslar/veritabani-yonetimi/fiziksel-cogaltma

Günlük çerçevelerinin bir yedek sunucuya sürekli akıtılması: taban kopya ve akışın birlikte kurduğu ikinci kopya, çoğaltma gecikmesinin ölçülmesi, gecikmenin bayat okuma ve veri kaybı olarak görünmesi, senkron onayın bedeli.

Önceki ders yedeğin geçerliliğini doğrulamayı kurdu. Doğrulanmış bir yedek veriyi
kaybetmemeyi güvence altına alır, ama kesintiyi kısaltmaz: kırk dakika süren bir geri
yükleme, kırk dakika boyunca kütüphanenin ödünç veremeyeceği anlamına gelir. Kesintiyi
kısaltmanın yolu, verinin bir kopyasının başka bir makinede zaten hazır durmasıdır.

Bu kopyanın nasıl hazır tutulacağı sorusunun en doğrudan yanıtı, kursun Motor Mimarisi
konusunda kurulan düzenekten gelir. Motor her değişikliği veri dosyalarına yazmadan önce
günlüğe yazar. Günlük, veritabanının bütün geçmişini sırayla taşır. O günlük ikinci bir
makineye akıtılır ve orada uygulanırsa, ikinci makinedeki dosyalar birincinin peşinden
gelir. **Fiziksel çoğaltma (physical replication)** budur: değişiklikler sayfa düzeyinde,
günlük kayıtları biçiminde taşınır ve yedek sunucuda yeniden oynatılır.

#### Taban Kopya ve Akış

Fiziksel çoğaltma iki parçadan oluşur; ikisi de yedekleme derslerinden tanıdıktır.

**Taban kopya (base copy)**, belirli bir andaki veri dosyalarının birebir kopyasıdır —
fiziksel yedeğin ta kendisi. Yedek sunucu buradan başlar.

**Günlük akışı**, taban kopyanın alındığı andan itibaren üretilen günlük kayıtlarının
kesintisiz gönderilmesidir. Yedek sunucu gelen kayıtları sırayla uygular ve birincinin
durumuna yaklaşır.

Zaman noktasına kurtarmadaki taban yedek ile günlük arşivinin aynı çifti olduğunu fark
etmek önemlidir. Aradaki fark amaçtır: kurtarmada günlük **bir kez**, seçilen bir ana
kadar uygulanır; çoğaltmada **sürekli**, akış devam ettiği sürece uygulanır. Aynı düzenek,
iki farklı işletim ihtiyacını karşılar.

Aşağıdaki blok bunu ölçülebilir biçimde kuruyor. Kullanılan motorda günlük dosyası sabit
boyutlu **çerçevelerden (frame)** oluşur; her çerçeve değişmiş bir sayfanın kopyasını ve
bir başlığı taşır. Blok, birincil veritabanında beş parti ödünç kaydı açıyor, her partiden
sonra günlük dosyasının boyutunu not ediyor, sonra taban kopyaya günlüğün yalnız ilk
bölümünü uygulayarak yedek sunucunun her ara durumunu üretiyor.

Çerçeve boyutu ve sayısı bu motora özgüdür; başka motorlar günlüğü bayt konumu ya da sıra
numarasıyla ölçer. Değişmeyen şey, gönderilmemiş günlük miktarının gecikmenin ölçüsü
olmasıdır.

```bash
rm -f birincil.db birincil.db-wal birincil.db-shm taban.db yedek.db yedek.db-wal yedek.db-shm

node - <<'EOF'
const { DatabaseSync } = require("node:sqlite");
const fs = require("node:fs");
const crypto = require("node:crypto");
const SAYFA = 4096, CERCEVE = SAYFA + 24, GUNLUK_BASLIK = 32;

const birincil = new DatabaseSync("birincil.db");
birincil.exec("PRAGMA page_size=4096");
birincil.exec("PRAGMA journal_mode=WAL");
birincil.exec("PRAGMA wal_autocheckpoint=0");
birincil.exec("CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT NOT NULL," +
              " uye_id INT NOT NULL, sube_id INT NOT NULL, alis TEXT NOT NULL)");
birincil.exec("PRAGMA wal_checkpoint(TRUNCATE)");

// Taban kopya: yedek sunucu bu dosyadan baslar.
fs.copyFileSync("birincil.db", "taban.db");

const ekle = birincil.prepare(
  "INSERT INTO odunc(kitap_id,uye_id,sube_id,alis) VALUES(?,?,?,?)");
const kesim = [];
for (let parti = 0; parti < 5; parti++) {
  birincil.exec("BEGIN");
  for (let i = 1; i <= 200; i++) ekle.run(i % 500, i % 300, i % 3, "2025-06-01");
  birincil.exec("COMMIT");
  kesim.push(fs.statSync("birincil.db-wal").size);
}

const gunluk = fs.readFileSync("birincil.db-wal");
const toplamCerceve = (gunluk.length - GUNLUK_BASLIK) / CERCEVE;
const ozet = (db) => crypto.createHash("sha256").update(
  db.prepare("SELECT id,kitap_id,uye_id,sube_id FROM odunc ORDER BY id").all()
    .map((r) => r.id + "," + r.kitap_id + "," + r.uye_id + "," + r.sube_id).join("\n")
).digest("hex").slice(0, 8);

const birincilOzet = ozet(birincil);
console.log("birincil: " + birincil.prepare("SELECT COUNT(*) c FROM odunc").get().c +
            " satir, " + toplamCerceve + " gunluk cercevesi, icerik ozeti " + birincilOzet);
console.log();
console.log("gonderilen cerceve | yedek satir | gecikme | icerik ozeti | esitlik");
console.log("-------------------|-------------|---------|--------------|--------");
for (const boyut of kesim) {
  fs.rmSync("yedek.db-wal", { force: true });
  fs.rmSync("yedek.db-shm", { force: true });
  fs.copyFileSync("taban.db", "yedek.db");
  fs.writeFileSync("yedek.db-wal", gunluk.subarray(0, boyut));
  const yedek = new DatabaseSync("yedek.db");
  const cerceve = (boyut - GUNLUK_BASLIK) / CERCEVE;
  const o = ozet(yedek);
  console.log(String(cerceve).padStart(18) + " | " +
              String(yedek.prepare("SELECT COUNT(*) c FROM odunc").get().c).padStart(11) +
              " | " + String(toplamCerceve - cerceve).padStart(7) + " | " +
              o.padStart(12) + " | " + (o === birincilOzet ? "ESIT" : "GERIDE"));
  yedek.close();
}
EOF
```

```text
birincil: 1000 satir, 20 gunluk cercevesi, icerik ozeti 0a8630f5

gonderilen cerceve | yedek satir | gecikme | icerik ozeti | esitlik
-------------------|-------------|---------|--------------|--------
                 4 |         200 |      16 |     e2b63ba7 | GERIDE
                 8 |         400 |      12 |     10a42e86 | GERIDE
                12 |         600 |       8 |     cc62491f | GERIDE
                16 |         800 |       4 |     10142745 | GERIDE
                20 |        1000 |       0 |     0a8630f5 | ESIT
```

Tablonun her satırı bir zaman anıdır. Yedek sunucu, o ana kadar gönderilmiş çerçevelerin
uygulandığı durumdadır: tutarlı, sorgulanabilir, ama geride. Son satırda gecikme sıfıra
indiğinde içerik özeti birincilinkiyle aynıdır — kopya, sayfa düzeyinde birebir eşittir.

Ara satırların hiçbiri bozuk değildir. Yedek sunucu yarım bir işlemin ortasında durmaz;
uygulama işlem sınırlarında ilerler, dolayısıyla her ara durum geçerli bir veritabanıdır.
Bu, fiziksel çoğaltmanın temel güvencesidir: yedek geride olabilir, tutarsız olamaz.

#### Gecikmenin İki Görünümü

Çoğaltma gecikmesi bir sayı olarak zararsız görünür. İşletimde iki ayrı biçimde ortaya
çıkar ve ikisi farklı sorunlardır.

Aşağıdaki blok bunu gösteriyor: yedek sunucu yetişmiş durumdayken ödünç masası üç yeni
kayıt açıyor ve o anda akış duruyor.

```bash
rm -f birincil.db birincil.db-wal birincil.db-shm taban.db yedek.db yedek.db-wal yedek.db-shm

node - <<'EOF'
const { DatabaseSync } = require("node:sqlite");
const fs = require("node:fs");
const CERCEVE = 4096 + 24;

const birincil = new DatabaseSync("birincil.db");
birincil.exec("PRAGMA page_size=4096");
birincil.exec("PRAGMA journal_mode=WAL");
birincil.exec("PRAGMA wal_autocheckpoint=0");
birincil.exec("CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT NOT NULL," +
              " uye_id INT NOT NULL, sube_id INT NOT NULL, alis TEXT NOT NULL)");
birincil.exec("PRAGMA wal_checkpoint(TRUNCATE)");
fs.copyFileSync("birincil.db", "taban.db");

const ekle = birincil.prepare(
  "INSERT INTO odunc(id,kitap_id,uye_id,sube_id,alis) VALUES(?,?,?,?,?)");
birincil.exec("BEGIN");
for (let i = 1; i <= 1000; i++) ekle.run(i, i % 500, i % 300, i % 3, "2025-06-01");
birincil.exec("COMMIT");

// Yedek sunucu buraya kadar yetismis durumda.
const gonderilen = fs.statSync("birincil.db-wal").size;
fs.copyFileSync("taban.db", "yedek.db");
fs.writeFileSync("yedek.db-wal", fs.readFileSync("birincil.db-wal"));
let yedek = new DatabaseSync("yedek.db");
console.log("yetismis yedek, satir sayisi: " +
            yedek.prepare("SELECT COUNT(*) c FROM odunc").get().c);
yedek.close();

// Odunc masasi uc yeni kayit acar; cerceveler henuz gonderilmemistir.
birincil.exec("BEGIN");
for (let i = 1001; i <= 1003; i++) ekle.run(i, i % 500, i % 300, 2, "2025-06-10");
birincil.exec("COMMIT");
const bekleyen = (fs.statSync("birincil.db-wal").size - gonderilen) / CERCEVE;

fs.rmSync("yedek.db-shm", { force: true });
fs.copyFileSync("taban.db", "yedek.db");
fs.writeFileSync("yedek.db-wal",
                 fs.readFileSync("birincil.db-wal").subarray(0, gonderilen));
yedek = new DatabaseSync("yedek.db");
const say = (db) => db.prepare("SELECT COUNT(*) c FROM odunc WHERE id>=1001").get().c;
console.log();
console.log("bekleyen cerceve         : " + bekleyen);
console.log("birincilde yeni kayit    : " + say(birincil));
console.log("yedekte yeni kayit       : " + say(yedek));
console.log("yedekten okuyan rapor    : " +
  yedek.prepare("SELECT COUNT(*) c FROM odunc WHERE sube_id=2 AND alis='2025-06-10'").get().c +
  " kayit gorur");
yedek.close();

// Bekleyen cerceveler de gonderilir.
fs.rmSync("yedek.db-shm", { force: true });
fs.copyFileSync("taban.db", "yedek.db");
fs.writeFileSync("yedek.db-wal", fs.readFileSync("birincil.db-wal"));
yedek = new DatabaseSync("yedek.db");
console.log("gonderim sonrasi yedekte : " + say(yedek) + " yeni kayit");
yedek.close();
EOF
```

```text
yetismis yedek, satir sayisi: 1000

bekleyen cerceve         : 4
birincilde yeni kayit    : 3
yedekte yeni kayit       : 0
yedekten okuyan rapor    : 0 kayit gorur
gonderim sonrasi yedekte : 3 yeni kayit
```

**Birinci görünüm: bayat okuma.** Yedek sunucu salt okunur bir kopyadır ve okuma yükünü
buraya yöneltmek çoğaltmanın en sık kullanılan yararıdır. Raporlar, arama sorguları ve
gösterge panoları birinciyi meşgul etmez. Karşılığında okunan veri gecikme kadar eskidir.
Yukarıda rapor sıfır kayıt görüyor; oysa üç kayıt açılmış durumda.

Bu, her uygulama için sorun değildir. Aylık ödünç sayısını hesaplayan bir rapor için birkaç
saniyelik gecikme fark etmez. Ödünç masasındaki görevlinin az önce kaydettiği ödüncü hemen
sorgulaması ise farklıdır: kendi yazdığını okuyamaz. Bu duruma yol açmamanın işletim
kuralı basittir — yazma yapan oturumun sonraki okumaları birinciye gider, gecikmeye
duyarsız okumalar yedeğe.

**İkinci görünüm: devralmada veri kaybı.** Birincil sunucu bu anda kaybolsaydı, yedek
sunucu 1000 satırla devralırdı; gönderilmemiş dört çerçevedeki üç ödünç kaydı kaybolurdu.
Gecikme, kurtarma noktası hedefinin karşılığıdır: hedef "en çok beş saniyelik veri kaybı"
diyorsa, gecikmenin beş saniyeyi aşmadığının izlenmesi gerekir. İzlenmeyen gecikme,
ölçülmemiş bir kurtarma noktası hedefidir.

#### Senkron Onayın Bedeli

Yukarıdaki kayıp, çoğaltmanın **asenkron** olmasından kaynaklanır: birincil sunucu işlemi
kesinleştirir, kullanıcıya "tamam" der, günlüğü sonra gönderir. Alternatifi, kesinleştirme
onayını yedek sunucunun kaydı aldığını bildirmesine bağlamaktır — **senkron onay**. Bu
durumda kayıp sıfırdır, çünkü onay verilen her işlem iki makinededir.

Bedeli her işleme eklenen ağ gidiş-dönüş süresidir. Aşağıdaki blok yerel kesinleştirme
süresini gerçekten ölçüyor, ağ gecikmesini ise **modelliyor**: gidiş-dönüş süreleri
verilmiş sayılardır, ölçülmemiştir. Yerel süre de makineye, dosya sistemine ve eşitleme
ayarına bağlıdır; sizde farklı çıkacaktır. Anlamlı olan sütunlar arasındaki orandır.

```bash
rm -f onay.db onay.db-wal onay.db-shm

node - <<'EOF'
const { DatabaseSync } = require("node:sqlite");
const db = new DatabaseSync("onay.db");
db.exec("PRAGMA journal_mode=WAL");
db.exec("PRAGMA synchronous=FULL");
db.exec("CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT)");
const ekle = db.prepare("INSERT INTO odunc(kitap_id,uye_id) VALUES(?,?)");

// Yerel kesinlestirme suresi olculur: her kayit ayri islem.
const sure = [];
for (let i = 0; i < 300; i++) {
  const t = process.hrtime.bigint();
  db.exec("BEGIN"); ekle.run(i % 500, i % 300); db.exec("COMMIT");
  sure.push(Number(process.hrtime.bigint() - t) / 1e6);
}
sure.sort((a, b) => a - b);
const yerel = sure[Math.floor(sure.length / 2)];
console.log("yerel kesinlestirme (ortanca): " + yerel.toFixed(3) + " ms");
console.log();
console.log("onay kipi          | ag gidis-donusu | islem suresi | saniyede islem | kayip riski");
console.log("-------------------|-----------------|--------------|----------------|------------");
const satir = (ad, rtt, kayip) => {
  const t = yerel + rtt;
  console.log(ad.padEnd(18) + " | " + (rtt ? rtt + " ms" : "-").padStart(15) + " | " +
              (t.toFixed(3) + " ms").padStart(12) + " | " +
              Math.round(1000 / t).toString().padStart(14) + " | " + kayip);
};
satir("asenkron", 0, "gecikme kadar");
satir("senkron (yerel ag)", 1, "yok");
satir("senkron (bolgeler)", 10, "yok");
satir("senkron (kitalar)", 60, "yok");
EOF
```

{/* mask: (?<=ortanca\): )[0-9.]+(?= ms) */}
{/* mask: [ ]*[0-9]+\.[0-9]{3}(?= ms \|) */}
{/* mask: [ ]*[0-9]+(?= \| [gy]) */}
```text
yerel kesinlestirme (ortanca): 0.005 ms

onay kipi          | ag gidis-donusu | islem suresi | saniyede islem | kayip riski
-------------------|-----------------|--------------|----------------|------------
asenkron           |               - |     0.005 ms |         186047 | gecikme kadar
senkron (yerel ag) |            1 ms |     1.005 ms |            995 | yok
senkron (bolgeler) |           10 ms |    10.005 ms |            100 | yok
senkron (kitalar)  |           60 ms |    60.005 ms |             17 | yok
```

Tablo tek bir bağlantının ardışık işlem hızını verir; birden çok bağlantı paralel
çalıştığında toplam iş hacmi artar, ama tek bir kullanıcının beklediği süre değişmez.
Kütüphane bağlamında okunuşu şudur: ödünç masasında bir kaydın açılması, aynı şehirdeki
bir yedek sunucuyla bir milisaniye dolayında bir gecikmeyle sürer; kıtalar arası bir yedekle
her ödünç işlemi altmış milisaniye bekler. İkincisi işlemez bir arayüz üretir.

Bu yüzden yaygın topoloji ikisini birleştirir: yakındaki bir yedek sunucu senkron
onaylanır (veri kaybı sıfırdır), uzaktaki bir yedek sunucu asenkron beslenir (bölge
çapında bir arızaya karşı kopya bulunur, gecikme fiyatı ödenmez).

Senkron onayın atlanan bir yan etkisi vardır: yedek sunucu yanıt vermeyi bıraktığında
birincil sunucu da yazamaz hâle gelir, çünkü hiçbir işlem onaylanamaz. Erişilebilirliği
artırmak için kurulan düzenek, tek bir yedek sunucuyla yapılandırıldığında erişilebilirliği
düşürür. Bunun önlenmesi, onayı birden çok yedek sunucudan **herhangi birine** bağlamakla
ya da yedek yanıt vermediğinde asenkron kipe düşmekle sağlanır; ikinci seçenek, kayıp
riskinin geri geldiği anlamına gelir ve bilinerek seçilmelidir.

#### Yedek Sunucunun Sınırları

Fiziksel çoğaltma sayfa kopyalar. Bu, üç sınırı doğrudan doğurur.

**Kopya bütündür, seçilemez.** Yalnız ödünç tablosunu çoğaltmak olanaklı değildir; bütün
veritabanı gelir. Sayfa numarası düzeyinde çalışan bir düzenek, o sayfanın hangi tabloya
ait olduğunu umursamaz.

**Kopya salt okunurdur.** Yedek sunucuya yazmak, gelen günlük akışıyla çatışırdı. Bu
nedenle yedek sunucudaki tek yazar akıştır; uygulamalar oradan yalnız okur. Yedek sunucuda
eksik bir dizini tamamlamak ya da geçici bir tablo oluşturmak olanaklı değildir.

**Kopya aynı sürümü ve aynı düzeni gerektirir.** Günlük kayıtları sayfa yapısını
anlatır; sayfa yapısını farklı yorumlayan bir sürüm onları uygulayamaz. Bu, sürüm
yükseltmede belirleyici bir kısıttır ve bu konunun son dersinde geri gelecektir.

Bir sınır daha vardır ve işletimde en sık yaşananıdır: yedek sunucuda uzun süren bir okuma
sorgusu, gelen değişikliklerin uygulanmasını geciktirebilir. Neden, kursun Motor Mimarisi
konusunda kurulan çok sürümlü eşzamanlılık denetimidir — okuyucunun gördüğü sürümlerin
korunması gerekir. Sonuç, saatlerce süren bir raporun çoğaltma gecikmesini büyütmesidir.

#### Özet

- Fiziksel çoğaltma, taban kopya ile sürekli günlük akışının birleşimidir; yedek sunucu
  gelen günlük kayıtlarını sırayla uygulayarak birincinin peşinden gelir.
- Yedek sunucunun her ara durumu tutarlıdır; gecikme, gönderilmemiş günlük miktarıyla
  ölçülür ve sıfırlandığında iki kopyanın içeriği birebir eşittir.
- Gecikme iki biçimde görünür: yedekten okuyan raporlar bayat veri görür, devralma anında
  gönderilmemiş kayıtlar kaybolur.
- Senkron onay veri kaybını sıfırlar ve karşılığında her işleme bir ağ gidiş-dönüşü ekler;
  uzak bir yedekle bu, işlem süresini üç mertebe büyütebilir.
- Tek yedek sunucuya bağlı senkron onay, yedek yanıt vermediğinde birincinin de yazamaz
  hâle gelmesine yol açar.
- Fiziksel kopya bütündür, salt okunurdur ve aynı sayfa düzenini gerektirir.

#### Sonraki Adım

Fiziksel çoğaltmanın üç sınırı da aynı yerden geliyor: taşınan şey satır değil, sayfadır.
Sayfa yerine değişen satırların kendisi taşınsaydı, kopya seçilebilir olurdu — yalnız
ödünç ve üye tabloları gönderilebilirdi. Hedefteki tablo farklı bir düzende, hatta farklı
bir motor sürümünde olabilirdi, çünkü uygulanan şey bir sayfa görüntüsü değil "şu satır şu
değerlere geçti" bilgisi olurdu. Sonraki ders mantıksal çoğaltmayı ele alıyor: seçici
kopyalamanın kurulması, gönderilen veri hacminin fiziksel çoğaltmayla karşılaştırılması,
farklı şemaya sahip bir hedefe yazma ve bu yolun sürüm geçişlerinde açtığı kapı.

### Mantıksal Çoğaltma

Source: https://academia.sh/tr/kurslar/veritabani-yonetimi/mantiksal-cogaltma

Satır düzeyinde değişikliklerin seçilen tablolar için taşınması: yayın ve abonelik, gönderilen veri hacminin fiziksel çoğaltmayla karşılaştırılması, farklı şemaya sahip hedef, çakışmanın akışı durdurması ve şema değişikliğinin uygulanma sırası.

Fiziksel çoğaltmanın üç sınırı da aynı yerden geliyordu: taşınan şey satır değil, sayfaydı.
Sayfa yerine değişen satırların kendisi taşınırsa üç sınır da kalkar. Kopya seçilebilir
olur — yalnız ödünç ve üye tabloları gönderilebilir. Hedefteki tablo farklı bir düzende,
farklı bir dizin kümesiyle, hatta farklı bir motor sürümünde bulunabilir, çünkü uygulanan
şey bir sayfa görüntüsü değil "şu satır şu değerlere geçti" bilgisidir.

**Mantıksal çoğaltma (logical replication)** budur. Motor, günlüğü okur ve içindeki sayfa
değişikliklerinden satır düzeyindeki olayları çıkarır: hangi tabloya hangi satır eklendi,
hangi satır hangi değerlere güncellendi, hangi satır silindi. Bu çıkarma işlemine
**mantıksal çözümleme (logical decoding)** denir. Sonuç, sayfa numaralarından bağımsız,
kendi başına anlamlı bir değişiklik akışıdır.

#### Yayın ve Abonelik

Mantıksal çoğaltmanın iki ucu vardır.

**Yayın (publication)**, kaynak sunucuda hangi tabloların çoğaltılacağını belirtir. Kapsam
dışındaki tabloların değişiklikleri akışa hiç girmez.

**Abonelik (subscription)**, hedef sunucuda akışı okuyup uygulayan taraftır. Nereye kadar
uyguladığını bir konum bilgisiyle saklar; bağlantı koptuğunda o konumdan sürdürür.
Motorların çoğu, henüz uygulanmamış değişikliklerin silinmemesi için kaynak tarafta bir
**çoğaltma yuvası (replication slot)** tutar.

Aşağıdaki blok bunu kütüphane veritabanında kuruyor. Kaynakta dört tablo var: üye, kitap,
ödünç ve oturum kaydı. Yayın yalnız iki tabloyu kapsıyor. Değişiklik akışı gerçek bir
motorda günlükten çözümlenerek üretilir; burada aynı bilgi **tetikleyicilerle** üretiliyor.
Modelin model olan yanı budur: akışın nereden çıktığı. İçeriği, uygulanma biçimi ve kapsam
davranışı gerçeğiyle aynıdır.

```bash
rm -f birincil.db abone.db

node - <<'EOF'
const { DatabaseSync } = require("node:sqlite");
const crypto = require("node:crypto");

const birincil = new DatabaseSync("birincil.db");
birincil.exec(`
CREATE TABLE uye(id INTEGER PRIMARY KEY, ad TEXT NOT NULL, sube_id INT NOT NULL);
CREATE TABLE kitap(id INTEGER PRIMARY KEY, baslik TEXT NOT NULL, raf TEXT);
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT NOT NULL, uye_id INT NOT NULL,
                   sube_id INT NOT NULL, alis TEXT NOT NULL, iade TEXT);
CREATE TABLE oturum_kaydi(id INTEGER PRIMARY KEY, hesap TEXT, an TEXT);
CREATE TABLE yayin(sira INTEGER PRIMARY KEY, tablo TEXT, islem TEXT, anahtar INT, satir TEXT);
`);

// Yayin kapsami: yalniz uye ve odunc.
for (const [tablo, alan] of [
  ["uye", "'id',NEW.id,'ad',NEW.ad,'sube_id',NEW.sube_id"],
  ["odunc", "'id',NEW.id,'kitap_id',NEW.kitap_id,'uye_id',NEW.uye_id," +
            "'sube_id',NEW.sube_id,'alis',NEW.alis,'iade',NEW.iade"]]) {
  for (const [olay, kod] of [["INSERT", "E"], ["UPDATE", "G"]]) {
    birincil.exec(`CREATE TRIGGER y_${tablo}_${kod} AFTER ${olay} ON ${tablo} BEGIN
      INSERT INTO yayin(tablo,islem,anahtar,satir)
      VALUES('${tablo}','${kod}',NEW.id,json_object(${alan})); END`);
  }
  birincil.exec(`CREATE TRIGGER y_${tablo}_S AFTER DELETE ON ${tablo} BEGIN
    INSERT INTO yayin(tablo,islem,anahtar,satir)
    VALUES('${tablo}','S',OLD.id,NULL); END`);
}

// Bir gunluk is: uye ve kitap kayitlari, odunc acilislari, iadeler, silmeler, oturum kaydi.
birincil.exec("BEGIN");
const uye = birincil.prepare("INSERT INTO uye(id,ad,sube_id) VALUES(?,?,?)");
for (let i = 1; i <= 500; i++) uye.run(i, "Uye-" + i, (i % 3) + 1);
const kitap = birincil.prepare("INSERT INTO kitap(id,baslik,raf) VALUES(?,?,?)");
for (let i = 1; i <= 300; i++) kitap.run(i, "Kitap-" + i, "R" + (i % 20));
const odunc = birincil.prepare(
  "INSERT INTO odunc(id,kitap_id,uye_id,sube_id,alis) VALUES(?,?,?,?,?)");
for (let i = 1; i <= 2000; i++)
  odunc.run(i, (i % 300) + 1, (i % 500) + 1, (i % 3) + 1, "2025-06-01");
const iade = birincil.prepare("UPDATE odunc SET iade='2025-06-20' WHERE id=?");
for (let i = 1; i <= 400; i++) iade.run(i * 3);
const sil = birincil.prepare("DELETE FROM odunc WHERE id=?");
for (let i = 1; i <= 50; i++) sil.run(i * 37);
const oturum = birincil.prepare("INSERT INTO oturum_kaydi(hesap,an) VALUES(?,?)");
for (let i = 1; i <= 800; i++) oturum.run("odunc_masasi", "2025-06-10");
birincil.exec("COMMIT");

// Abone tarafinda yalniz iki tablo tanimli.
const abone = new DatabaseSync("abone.db");
abone.exec(`
CREATE TABLE uye(id INTEGER PRIMARY KEY, ad TEXT NOT NULL, sube_id INT NOT NULL);
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT NOT NULL, uye_id INT NOT NULL,
                   sube_id INT NOT NULL, alis TEXT NOT NULL, iade TEXT);
`);
const uygula = (db, k) => {
  if (k.islem === "S") { db.prepare(`DELETE FROM ${k.tablo} WHERE id=?`).run(k.anahtar); return; }
  const satir = JSON.parse(k.satir), sutun = Object.keys(satir);
  db.prepare(`INSERT OR REPLACE INTO ${k.tablo}(${sutun.join(",")})` +
             ` VALUES(${sutun.map(() => "?").join(",")})`).run(...sutun.map((s) => satir[s]));
};
const kayitlar = birincil.prepare("SELECT * FROM yayin ORDER BY sira").all();
abone.exec("BEGIN");
for (const k of kayitlar) uygula(abone, k);
abone.exec("COMMIT");

console.log("yayin akisindaki kayitlar:");
for (const r of birincil.prepare("SELECT tablo, islem, COUNT(*) n FROM yayin" +
    " GROUP BY tablo, islem ORDER BY tablo, islem").all())
  console.log("  " + r.tablo.padEnd(6) + " " + r.islem + " : " + r.n);
console.log("  toplam  : " + kayitlar.length);

const ozet = (db, t, s) => crypto.createHash("sha256").update(
  db.prepare(`SELECT ${s} FROM ${t} ORDER BY id`).all()
    .map((r) => Object.values(r).join(",")).join("\n")).digest("hex").slice(0, 8);
const say = (db, t) => { try { return db.prepare(`SELECT COUNT(*) c FROM ${t}`).get().c; }
                         catch { return "tablo yok"; } };
console.log();
console.log("tablo        | birincil |     abone | icerik ozeti (birincil / abone)");
console.log("-------------|----------|-----------|-------------------------------");
for (const [t, s] of [["uye", "id,ad,sube_id"],
                      ["odunc", "id,kitap_id,uye_id,sube_id,alis,iade"]])
  console.log(t.padEnd(12) + " | " + String(say(birincil, t)).padStart(8) + " | " +
              String(say(abone, t)).padStart(9) + " | " +
              ozet(birincil, t, s) + " / " + ozet(abone, t, s));
for (const t of ["kitap", "oturum_kaydi"])
  console.log(t.padEnd(12) + " | " + String(say(birincil, t)).padStart(8) + " | " +
              String(say(abone, t)).padStart(9) + " |");
EOF
```

```text
yayin akisindaki kayitlar:
  odunc  E : 2000
  odunc  G : 400
  odunc  S : 50
  uye    E : 500
  toplam  : 2950

tablo        | birincil |     abone | icerik ozeti (birincil / abone)
-------------|----------|-----------|-------------------------------
uye          |      500 |       500 | 840815bb / 840815bb
odunc        |     1950 |      1950 | 3ec52d07 / 3ec52d07
kitap        |      300 | tablo yok |
oturum_kaydi |      800 | tablo yok |
```

Kapsamın etkisi sayılarda görünüyor. Kaynakta dört bin elli satır değişti; akışa yalnız
iki bin dokuz yüz elli tanesi girdi. Kitap ve oturum kaydı tablolarının bin yüz
değişikliği hiç taşınmadı — ağ trafiği, hedefteki disk ve orada yapılan yazma işi bu kadar
azaldı.

Abonedeki iki tablonun içerik özeti kaynağınkiyle aynı. Bu, üç işlem türünün de doğru
uygulandığını gösterir: eklemeler, dört yüz iade güncellemesi ve elli silme. Silme
kayıtları yalnız anahtar taşır; silinen satırın diğer alanları gereksizdir. Bu, mantıksal
çoğaltmanın satırı **anahtarıyla** bulduğu anlamına gelir ve
birincil anahtarı olmayan bir tabloda çoğaltmanın neden zorlaştığını açıklar: hedefteki
satırı seçecek bir ölçüt yoktur. Motorlar bunun için tabloya bir **çoğaltma kimliği
(replica identity)** atanmasını ister.

#### Taşınan Veri Hacmi

İki yöntem arasındaki en somut fark taşınan bayt miktarıdır ve bu fark iş yüküne göre
mertebe değiştirir. Aşağıdaki blok aynı iş yükünü iki ayrı veritabanında çalıştırıyor:
biri günlüğü ölçülen fiziksel taraf, diğeri değişiklik kayıtları ölçülen mantıksal taraf.

Sayılar sayfa boyutuna, satır genişliğine ve parti büyüklüğüne bağlıdır; sizde farklı
çıkacaktır. Mantıksal tarafta yalnız değişiklik gövdesi sayılıyor, kayıt başına protokol
yükü sayılmıyor. Okunması gereken şey oranların yönüdür.

```bash
rm -f fiziksel.db fiziksel.db-wal fiziksel.db-shm mantiksal.db mantiksal.db-wal mantiksal.db-shm

node - <<'EOF'
const { DatabaseSync } = require("node:sqlite");
const fs = require("node:fs");
const SEMA = "CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT NOT NULL," +
             " uye_id INT NOT NULL, sube_id INT NOT NULL, alis TEXT NOT NULL, iade TEXT)";

function kur(dosya, tetikleyici) {
  const db = new DatabaseSync(dosya);
  db.exec("PRAGMA page_size=4096"); db.exec("PRAGMA journal_mode=WAL");
  db.exec("PRAGMA wal_autocheckpoint=0");
  db.exec(SEMA);
  db.exec("CREATE TABLE akis(sira INTEGER PRIMARY KEY, tablo TEXT, islem TEXT, satir TEXT)");
  const ekle = db.prepare(
    "INSERT INTO odunc(id,kitap_id,uye_id,sube_id,alis) VALUES(?,?,?,?,?)");
  db.exec("BEGIN");
  for (let i = 1; i <= 20000; i++) ekle.run(i, i % 500, i % 300, i % 3, "2025-06-01");
  db.exec("COMMIT");
  if (tetikleyici) {
    for (const [olay, kod] of [["UPDATE", "G"], ["INSERT", "E"]])
      db.exec(`CREATE TRIGGER t_${kod} AFTER ${olay} ON odunc BEGIN
        INSERT INTO akis(tablo,islem,satir) VALUES('odunc','${kod}',
          json_object('id',NEW.id,'kitap_id',NEW.kitap_id,'uye_id',NEW.uye_id,
                      'sube_id',NEW.sube_id,'alis',NEW.alis,'iade',NEW.iade)); END`);
  }
  db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
  return db;
}

function olc(ad, isyuku) {
  const f = kur("fiziksel.db", false);
  const m = kur("mantiksal.db", true);
  isyuku(f); isyuku(m);
  const fizik = fs.statSync("fiziksel.db-wal").size;
  const mant = m.prepare("SELECT IFNULL(SUM(LENGTH(satir)),0) b, COUNT(*) n FROM akis").get();
  f.close(); m.close();
  for (const d of ["fiziksel.db", "fiziksel.db-wal", "fiziksel.db-shm",
                   "mantiksal.db", "mantiksal.db-wal", "mantiksal.db-shm"])
    fs.rmSync(d, { force: true });
  console.log(ad.padEnd(20) + " | " + String(mant.n).padStart(7) + " | " +
              String(fizik).padStart(8) + " | " + String(mant.b).padStart(9) + " | " +
              (fizik / mant.b).toFixed(1).padStart(5));
}

console.log("is yuku              | degisim | fiziksel | mantiksal | oran");
console.log("---------------------|---------|----------|-----------|------");
olc("daginik guncelleme", (db) => {
  const g = db.prepare("UPDATE odunc SET iade='2025-06-20' WHERE id=?");
  for (let p = 0; p < 100; p++) {
    db.exec("BEGIN");
    for (let i = 0; i < 20; i++) g.run(((p * 20 + i) * 7919) % 20000 + 1);
    db.exec("COMMIT");
  }
});
olc("sirali guncelleme", (db) => {
  const g = db.prepare("UPDATE odunc SET iade='2025-06-20' WHERE id=?");
  for (let p = 0; p < 100; p++) {
    db.exec("BEGIN");
    for (let i = 0; i < 20; i++) g.run(p * 20 + i + 1);
    db.exec("COMMIT");
  }
});
olc("toplu ekleme", (db) => {
  const e = db.prepare(
    "INSERT INTO odunc(id,kitap_id,uye_id,sube_id,alis) VALUES(?,?,?,?,?)");
  for (let p = 0; p < 100; p++) {
    db.exec("BEGIN");
    for (let i = 0; i < 20; i++) { const n = 20001 + p * 20 + i;
      e.run(n, n % 500, n % 300, n % 3, "2025-06-10"); }
    db.exec("COMMIT");
  }
});
EOF
```

```text
is yuku              | degisim | fiziksel | mantiksal | oran
---------------------|---------|----------|-----------|------
daginik guncelleme   |    2000 |  9014592 |    181716 |  49.6
sirali guncelleme    |    2000 |   964112 |    179685 |   5.4
toplu ekleme         |    2000 |   572712 |    166790 |   3.4
```

Üç satır da iki bin satırlık değişiklik taşıyor; taşınan bayt miktarı on beş kat aralıkta
değişiyor. Nedeni fiziksel tarafın birimidir: değişen satır ne kadar küçük olursa olsun,
o satırı içeren **sayfanın tamamı** gönderilir.

Dağınık güncellemede her satır ayrı bir sayfada olduğu için iki bin sayfa taşınır; bir
tarihin yazılması, dört kilobaytın gönderilmesine mal olur. Sıralı güncellemede aynı sayfa
içindeki çok sayıda satır tek bir partide değiştiği için sayfa bir kez gönderilir ve oran
beşe düşer. Toplu eklemede satırlar zaten yan yana yerleşir; fiziksel taraf en verimli
durumundadır.

Mantıksal tarafın gizli maliyeti ise hedefte ortaya çıkar: gelen her satır normal bir
yazma işlemi olarak uygulanır, dizinler güncellenir, kısıtlar denetlenir. Fiziksel tarafta
bunların hiçbiri yoktur; sayfa olduğu gibi yerine konur. Az veri taşıyan yöntem, hedefte
daha çok iş yapar.

#### Farklı Şema, Çakışma ve Şema Değişikliği

Esnekliğin üç sonucu vardır; aşağıdaki blok üçünü sırayla gösteriyor: hedefin fazladan
sütunu ve dizini vardır, yerel bir satır akışla çakışır ve kaynakta bir sütun eklenir.

```bash
rm -f kaynak.db hedef.db

node - <<'EOF'
const { DatabaseSync } = require("node:sqlite");

const kaynak = new DatabaseSync("kaynak.db");
kaynak.exec(`
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT NOT NULL,
                   uye_id INT NOT NULL, alis TEXT NOT NULL);
CREATE TABLE yayin(sira INTEGER PRIMARY KEY, satir TEXT);
CREATE TRIGGER y_e AFTER INSERT ON odunc BEGIN
  INSERT INTO yayin(satir) VALUES(json_object('id',NEW.id,'kitap_id',NEW.kitap_id,
    'uye_id',NEW.uye_id,'alis',NEW.alis)); END;
`);
const ekle = kaynak.prepare("INSERT INTO odunc(id,kitap_id,uye_id,alis) VALUES(?,?,?,?)");
for (let i = 1; i <= 100; i++) ekle.run(i, i % 50, i % 30, "2025-06-01");

// Hedef: fazladan sutunu ve fazladan dizini olan bir tablo.
const hedef = new DatabaseSync("hedef.db");
hedef.exec(`
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT NOT NULL, uye_id INT NOT NULL,
                   alis TEXT NOT NULL, gecikme_gunu INT NOT NULL DEFAULT 0);
CREATE INDEX odunc_uye ON odunc(uye_id);
`);
// Hedefte acilmis yerel bir kayit: cakismayi bu uretecek.
hedef.prepare("INSERT INTO odunc(id,kitap_id,uye_id,alis) VALUES(?,?,?,?)")
     .run(60, 9, 9, "2025-06-05");

const uygula = (kayit) => {
  const satir = JSON.parse(kayit.satir), sutun = Object.keys(satir);
  hedef.prepare(`INSERT INTO odunc(${sutun.join(",")})` +
                ` VALUES(${sutun.map(() => "?").join(",")})`).run(...sutun.map((s) => satir[s]));
};
let konum = 0;
const ilerle = () => {
  for (const k of kaynak.prepare("SELECT sira,satir FROM yayin ORDER BY sira").all()) {
    if (k.sira <= konum) continue;
    try { uygula(k); konum = k.sira; }
    catch (h) { return "sira " + k.sira + " durdu: " + h.message; }
  }
  return "akis bitti";
};
const durum = () => "   uygulanan kayit: " + konum + ", hedefteki satir: " +
                    hedef.prepare("SELECT COUNT(*) c FROM odunc").get().c;

console.log("1. gecis: " + ilerle());
console.log(durum());

// Cozum: catisan yerel satir kaldirilir, akis kaldigi yerden surer.
hedef.prepare("DELETE FROM odunc WHERE id=?").run(60);
console.log("2. gecis: " + ilerle());
console.log(durum());
console.log("   hedefe ozgu sutunun degeri: " +
  hedef.prepare("SELECT gecikme_gunu FROM odunc WHERE id=1").get().gecikme_gunu);

// Kaynakta sema degisikligi: yeni sutun yayina da girer.
kaynak.exec("ALTER TABLE odunc ADD COLUMN gecikme_bildirimi TEXT");
kaynak.exec("DROP TRIGGER y_e");
kaynak.exec(`CREATE TRIGGER y_e AFTER INSERT ON odunc BEGIN
  INSERT INTO yayin(satir) VALUES(json_object('id',NEW.id,'kitap_id',NEW.kitap_id,
    'uye_id',NEW.uye_id,'alis',NEW.alis,'gecikme_bildirimi',NEW.gecikme_bildirimi)); END`);
kaynak.prepare("INSERT INTO odunc(id,kitap_id,uye_id,alis,gecikme_bildirimi)" +
               " VALUES(?,?,?,?,?)").run(101, 7, 7, "2025-06-11", "ikinci uyari");
console.log("3. gecis: " + ilerle());

// Sema degisikligi once hedefe uygulanir.
hedef.exec("ALTER TABLE odunc ADD COLUMN gecikme_bildirimi TEXT");
console.log("4. gecis: " + ilerle());
console.log(durum());
EOF
```

```text
1. gecis: sira 60 durdu: UNIQUE constraint failed: odunc.id
   uygulanan kayit: 59, hedefteki satir: 60
2. gecis: akis bitti
   uygulanan kayit: 100, hedefteki satir: 100
   hedefe ozgu sutunun degeri: 0
3. gecis: sira 101 durdu: table odunc has no column named gecikme_bildirimi
4. gecis: akis bitti
   uygulanan kayit: 101, hedefteki satir: 101
```

**Fazladan sütun ve dizin sorun değildir.** Hedefteki `gecikme_gunu` sütunu akışta yoktur;
varsayılan değerini alır. Hedefteki dizin de kaynakta bulunmaz ve gelen her satırda
güncellenir. Bu, aynı verinin iki tarafta farklı erişim örüntülerine göre dizinlenmesine
olanak verir.

**Çakışma akışı durdurur.** Hedefte yerel olarak açılmış altmış numaralı kayıt, akıştaki
aynı numaralı kayıtla çatıştı ve uygulama elli dokuzuncu kayıtta durdu. Bu, veri
kaybetmemenin bilinçli yoludur: çakışan kaydı sessizce ezmek de, sessizce atlamak da iki
tarafı sessizce ayrıştırır. Durma, bir kararın verilmesini bekler. Karar verilip çakışan
satır kaldırıldıktan sonra akış kaldığı konumdan sürdü ve yüz kaydın tamamı uygulandı.

Çakışmanın kaynağı çoğu zaman aynıdır: hedefe yerel yazma yapılması. Mantıksal çoğaltmanın
hedefi salt okunur değildir ve bu, esnekliğin ikinci yüzüdür. Çakışmayı önlemenin işletim
kuralı, hedefteki tabloların yalnız akış tarafından yazılmasıdır; yerel yazma gerekiyorsa
anahtar aralıkları ayrılır.

**Şema değişikliği çoğaltılmaz.** Kaynakta eklenen sütun akışa girdi ve hedefte karşılığı
olmadığı için uygulama durdu. Aynı sütun hedefe eklendikten sonra akış sürdü. Buradan tek
bir işletim kuralı çıkar: **sütun ekleme önce hedefe, sonra kaynağa uygulanır**; sütun
kaldırma ise ters sırada, önce kaynaktan. Sıra karıştığında çoğaltma durur ve durduğu
sürece kaynakta biriken değişiklikler saklanmaya devam eder.

#### Sürüm Geçişindeki Rolü

Bu üç davranışın toplamı, mantıksal çoğaltmayı bir sürüm geçişi aracına dönüştürür.
Fiziksel çoğaltma hedefin aynı sayfa düzenini anlamasını gerektirir; mantıksal çoğaltma
gerektirmez. Yeni sürümde kurulmuş boş bir veritabanı, eski sürümdeki veritabanına abone
edilebilir; veri akarken eski sürüm çalışmayı sürdürür. İki taraf eşitlendiğinde
uygulamalar yeni tarafa yönlendirilir.

Bu yolun kesinti penceresi, geri yükleme süresi kadar değil, yalnız yönlendirme kadardır.
Aynı düzenek bir tabloyu ayrı bir veritabanına taşımak ya da bir rapor sunucusunu sürekli
beslemek için de kullanılır. Sürüm yükseltmenin bütünü, bu konunun son dersinin konusudur.

#### Özet

- Mantıksal çoğaltma, günlükten çözümlenen satır düzeyindeki değişiklikleri taşır; yayın
  kapsamı dışındaki tabloların değişiklikleri akışa hiç girmez.
- Silme kayıtları yalnız anahtar taşır; hedefteki satırın anahtarla bulunabilmesi
  gerektiği için çoğaltma kimliği tanımlanmamış tablolar sorun çıkarır.
- Taşınan hacim iş yüküne bağlıdır: dağınık küçük güncellemelerde fiziksel çoğaltma
  mantıksala göre elli kat fazla veri gönderir, toplu eklemede fark küçülür.
- Az veri taşıyan yöntem hedefte daha çok iş yapar; gelen satırlar normal yazma olarak
  uygulanır, dizinler ve kısıtlar işletilir.
- Hedefin fazladan sütunu ve farklı dizinleri olabilir; çakışma akışı durdurur ve karar
  verilmesini bekler.
- Şema değişikliği çoğaltılmaz: sütun ekleme önce hedefe, sütun kaldırma önce kaynağa
  uygulanır.

#### Sonraki Adım

İki çoğaltma biçimi de aynı varsayımı taşıyor: birincil sunucu kimdir sorusunun yanıtı
bellidir. Birincil kaybolduğunda bu yanıt kaybolur ve yerine yenisinin konması gerekir.
Kararı kim verir, ne zaman verir ve iki ayrı taraf aynı anda "birincil benim" derse ne
olur? Sonraki ders yüksek erişilebilirliği ele alıyor: devralma ile planlı geçişin farkı,
küme yöneticisinin rolü, çoğunluk oyunun neyi güvence altına aldığı ve bölünmüş beyin
riskinin nasıl doğduğu.

### Yüksek Erişilebilirlik ve Devralma

Source: https://academia.sh/tr/kurslar/veritabani-yonetimi/yuksek-erisilebilirlik-ve-devralma

Birincil sunucunun kaybında hizmetin sürmesi: küme yöneticisinin kararı, çoğunluk kuralının bölünmüş beyni nasıl engellediği, iki birincilin ürettiği veri çatışması ve algılama süresi ile yanlış devralma arasındaki ödünleşim.

Önceki iki ders çoğaltmayı kurdu; ikisi de aynı varsayımı taşıyordu: birincil sunucunun
kim olduğu bellidir. Birincil kaybolduğunda bu yanıt da kaybolur ve yerine yenisinin
konması gerekir. Verinin ikinci bir makinede hazır durması, o makinenin kendiliğinden
hizmet vermeye başlaması anlamına gelmez — birinin karar vermesi, yedek sunucuyu
yükseltmesi ve uygulamaları oraya yönlendirmesi gerekir.

Bu dersin konusu o karardır: kim verir, hangi bilgiye dayanarak verir ve iki ayrı taraf
aynı anda "birincil benim" derse ne olur.

#### Devralma, Planlı Geçiş ve Küme Yöneticisi

İki işlem birbirine karıştırılır.

**Planlı geçiş (switchover)**, birincil ve yedek rollerinin isteyerek değiştirilmesidir.
Birincil sunucu çalışır durumdadır: yeni yazma kabul etmeyi bırakır, yedek sunucunun
yetişmesi beklenir, roller değiştirilir. Veri kaybı yoktur, kesinti saniyeler
mertebesindedir. Bakım, donanım değişimi ve sürüm yükseltme bu yolla yapılır.

**Devralma (failover)**, birincil sunucu erişilemez durumdayken yedek sunucunun birincil
olarak yükseltilmesidir. Birincilin yetişmeyi bekleme olanağı yoktur; asenkron çoğaltmada
gönderilmemiş her değişiklik kaybolur. Kayıp miktarı, fiziksel çoğaltma dersinde ölçülen
çoğaltma gecikmesidir.

Kararı veren bileşene **küme yöneticisi (cluster manager)** denir. Veritabanı motorunun
dışında çalışır, düğümleri düzenli aralıklarla yoklar ve üç işi yapar: birincilin
erişilebilirliğini izlemek, gerektiğinde bir yedeği yükseltmek, uygulamaların gittiği
adresi yeni birincile çevirmek. Bu son adım olmadan yükseltme işe yaramaz — uygulamalar
hâlâ eski adrese bağlanmaya çalışır.

#### Çoğunluk Kuralı

Küme yöneticisinin en zor sorusu, birincilin gerçekten mi öldüğü yoksa yalnız görünmez mi
olduğudur. İkisi dışarıdan aynıdır: yanıt gelmiyor. Fakat sonuçları taban tabana zıttır.
Birincil ölmüşse yedeğin yükseltilmesi gerekir; birincil çalışıyor ama ağ bölünmesi
nedeniyle görünmüyorsa, yükseltme ikinci bir birincil yaratır.

Aşağıdaki blok iki kuralı aynı bölünme senaryolarında karşılaştırıyor. Bu bir modeldir;
gerçek bir küme yazılımı çalıştırılmıyor, kararı veren kural doğrudan uygulanıyor.

```bash
node - <<'EOF'
// Kume bir ag bolunmesinde gruplara ayrilir; birincil her zaman 1 numarali dugumdedir.
const senaryo = [
  [2, [[1], [2]]],
  [3, [[1, 2], [3]]],
  [3, [[1], [2, 3]]],
  [4, [[1, 2], [3, 4]]],
  [5, [[1, 2, 3], [4, 5]]],
  [5, [[1, 2], [3, 4, 5]]],
  [5, [[1, 2], [3, 4], [5]]],
];
const satir = (a, b, c, d) => console.log(String(a).padStart(5) + " | " + b.padEnd(7) +
  " | " + c.padEnd(28) + " | " + d);

console.log("COGUNLUK KURALI: bir grup, ancak dugumlerin yarisindan fazlasini");
console.log("bir araya getiriyorsa yazma kabul eder.");
console.log();
satir("dugum", "bolunme", "cogunluk", "sonuc");
console.log("------|---------|------------------------------|--------------------------");
for (const [n, gruplar] of senaryo) {
  const cogunluk = gruplar.find((g) => g.length * 2 > n);
  const birincilde = cogunluk && cogunluk.includes(1);
  satir(n, gruplar.map((g) => g.length).join("+"),
    cogunluk ? cogunluk.join(",") + " grubu (" + cogunluk.length + "/" + n + ")" : "yok",
    cogunluk ? (birincilde ? "birincil yerinde kalir" : "devralma; eski birincil durur")
             : "hicbir tarafta yazma yok");
}

console.log();
console.log('COGUNLUKSUZ KURAL: "birincili goremiyorsam olmustur, ben birincil olurum".');
console.log();
satir("dugum", "bolunme", "birincil sayisi", "sonuc");
console.log("------|---------|------------------------------|--------------------------");
for (const [n, gruplar] of senaryo)
  satir(n, gruplar.map((g) => g.length).join("+"), gruplar.length + " birincil",
    gruplar.length > 1 ? "bolunmus beyin" : "sorun yok");
EOF
```

```text
COGUNLUK KURALI: bir grup, ancak dugumlerin yarisindan fazlasini
bir araya getiriyorsa yazma kabul eder.

dugum | bolunme | cogunluk                     | sonuc
------|---------|------------------------------|--------------------------
    2 | 1+1     | yok                          | hicbir tarafta yazma yok
    3 | 2+1     | 1,2 grubu (2/3)              | birincil yerinde kalir
    3 | 1+2     | 2,3 grubu (2/3)              | devralma; eski birincil durur
    4 | 2+2     | yok                          | hicbir tarafta yazma yok
    5 | 3+2     | 1,2,3 grubu (3/5)            | birincil yerinde kalir
    5 | 2+3     | 3,4,5 grubu (3/5)            | devralma; eski birincil durur
    5 | 2+2+1   | yok                          | hicbir tarafta yazma yok

COGUNLUKSUZ KURAL: "birincili goremiyorsam olmustur, ben birincil olurum".

dugum | bolunme | birincil sayisi              | sonuc
------|---------|------------------------------|--------------------------
    2 | 1+1     | 2 birincil                   | bolunmus beyin
    3 | 2+1     | 2 birincil                   | bolunmus beyin
    3 | 1+2     | 2 birincil                   | bolunmus beyin
    4 | 2+2     | 2 birincil                   | bolunmus beyin
    5 | 3+2     | 2 birincil                   | bolunmus beyin
    5 | 2+3     | 2 birincil                   | bolunmus beyin
    5 | 2+2+1   | 3 birincil                   | bolunmus beyin
```

Alt tablo, tek başına makul görünen bir kuralın her bölünmede iki birincil ürettiğini
gösteriyor. Sorun, bir düğümün "karşı taraf öldü" ile "karşı tarafı göremiyorum"
arasındaki farkı yerel bilgiyle ayırt edememesidir. **Çoğunluk kuralı (quorum)** bu ayrımı
başka bir yerden getirir: düğümlerin yarısından fazlasını bir araya getiren grup en fazla
bir tane olabilir, dolayısıyla yazma hakkını yalnız ona vermek iki birincili olanaksız
kılar.

Üst tablonun üç okuması vardır. Üçüncü satırda birincil azınlıkta kalıyor ve çoğunluk
tarafında devralma yapılıyor; eski birincilin yazmayı **kendiliğinden** durdurması
zorunludur. İkinci ve dördüncü satırda hiçbir grup çoğunlukta değildir ve yazma tümüyle
durur — küme kendini kullanılamaz kılarak veri çatışmasını önler. Erişilebilirlik
tutarlılığa feda edilmiştir; bu bir kusur değil, bilinçli bir seçimdir.

Dört düğümlü satır, küme boyutu hakkındaki en pratik sonucu verir: dört düğüm, üç düğüme
göre bölünme dayanıklılığı katmaz. İkisi de tek düğüm kaybını tolere eder, ama dört düğüm
2+2 bölünmesinde tamamen durur. Bu nedenle küme boyutları tek sayı seçilir. İki düğümlü
bir kurulumda veri taşımayan üçüncü bir oy verici — **tanık düğüm (witness)** —
eklenerek aynı sonuç ucuza elde edilir.

#### İki Birincilin Bedeli

Bölünmüş beynin zararı soyut değildir. Aşağıdaki blok bölünmeyi gerçekten üretiyor: aynı
veritabanının iki kopyası ayrı ayrı yazma kabul ediyor ve bölünme kapandığında iki taraf
karşılaştırılıyor.

```bash
rm -f taraf-a.db taraf-b.db

node - <<'EOF'
const { DatabaseSync } = require("node:sqlite");
const fs = require("node:fs");

const SEMA = `CREATE TABLE odunc(id INTEGER PRIMARY KEY AUTOINCREMENT, kitap_id INT NOT NULL,
                uye_id INT NOT NULL, sube_id INT NOT NULL, alis TEXT NOT NULL, iade TEXT)`;
const a = new DatabaseSync("taraf-a.db");
a.exec(SEMA);
const ekleA = a.prepare(
  "INSERT INTO odunc(kitap_id,uye_id,sube_id,alis,iade) VALUES(?,?,?,?,?)");
a.exec("BEGIN");
for (let i = 1; i <= 1000; i++)
  ekleA.run(i % 400 + 1, i % 250 + 1, i % 3 + 1, "2025-06-01", "2025-06-15");
a.exec("COMMIT");
a.close();

// Bolunme ani: iki taraf ayni durumdan yola cikar.
fs.copyFileSync("taraf-a.db", "taraf-b.db");
const sol = new DatabaseSync("taraf-a.db");
const sag = new DatabaseSync("taraf-b.db");
const ac = (db, kitap, uye, sube) => db.prepare(
  "INSERT INTO odunc(kitap_id,uye_id,sube_id,alis) VALUES(?,?,?,?)")
  .run(kitap, uye, sube, "2025-06-16");
// A tarafi: 1 ve 2 numarali sube masalari. B tarafi: 3 numarali sube.
for (let i = 0; i < 40; i++) ac(sol, 101 + i, 10 + i, (i % 2) + 1);
for (let i = 0; i < 25; i++) ac(sag, 131 + i, 60 + i, 3);

const say = (db, s) => db.prepare(s).get().c;
console.log("bolunmeden sonra");
console.log("  A tarafi satir : " + say(sol, "SELECT COUNT(*) c FROM odunc"));
console.log("  B tarafi satir : " + say(sag, "SELECT COUNT(*) c FROM odunc"));

// Bolunme kapanir: iki tarafin kayitlari karsilastirilir.
sol.exec("ATTACH 'taraf-b.db' AS b");
const cakisanId = say(sol, `SELECT COUNT(*) c FROM main.odunc m JOIN b.odunc x USING(id)
                            WHERE m.kitap_id <> x.kitap_id OR m.uye_id <> x.uye_id`);
const ciftKitap = say(sol, `SELECT COUNT(*) c FROM
  (SELECT kitap_id FROM main.odunc WHERE iade IS NULL
   INTERSECT SELECT kitap_id FROM b.odunc WHERE iade IS NULL)`);
console.log();
console.log("  ayni kimlik, farkli kayit      : " + cakisanId);
console.log("  iki tarafta da odunc olan kitap: " + ciftKitap);

// Birlestirme denemesi: B tarafinin yeni kayitlari A tarafina eklenir.
let hata = 0, eklenen = 0;
for (const r of sol.prepare("SELECT * FROM b.odunc WHERE id > 1000").all()) {
  try {
    sol.prepare("INSERT INTO main.odunc(id,kitap_id,uye_id,sube_id,alis,iade)" +
                " VALUES(?,?,?,?,?,?)")
       .run(r.id, r.kitap_id, r.uye_id, r.sube_id, r.alis, r.iade);
    eklenen++;
  } catch { hata++; }
}
console.log();
console.log("birlestirme denemesi: " + eklenen + " eklendi, " + hata + " kimlik catismasi");
EOF
```

```text
bolunmeden sonra
  A tarafi satir : 1040
  B tarafi satir : 1025

  ayni kimlik, farkli kayit      : 25
  iki tarafta da odunc olan kitap: 10

birlestirme denemesi: 0 eklendi, 25 kimlik catismasi
```

Sonuçlar iki farklı zorluk gösteriyor. Yirmi beş kimlik çakışması **teknik** bir sorundur:
iki taraf da aynı sıra üretecinden aynı numaraları dağıttı. Kayıtlara yeni numara vererek
çözülebilir, ama o numaraları taşıyan makbuzlar, dış sistem kayıtları ve raporlar
geçersizleşir.

On kitabın iki tarafta da ödünçte görünmesi **teknik olmayan** bir sorundur. Aynı kitap
iki ayrı üyeye verilmiş görünüyor ve hangisinin gerçekten rafta olmadığını veritabanı
bilemez. Bu, otomatik birleştirmenin sınırıdır: çatışan iki gerçeklikten hangisinin doğru
olduğuna karar vermek veri işi değil, işletme işidir. Bölünmüş beynin maliyeti bu yüzden
kesinti maliyetiyle karşılaştırılmaz — kesinti biter, ayrışan veri kalır.

Çoğunluk kuralı bunu önlemenin birinci katmanıdır. İkinci katman **çitlemedir (fencing)**:
azınlıkta kalan eski birincilin yazmasının dışarıdan engellenmesi. Yerleşik yollar,
sunucunun ağ üzerinden kapatılması ya da paylaşılan depolamaya erişiminin iptalidir.
Gerekçesi, azınlıkta kalan düğümün kendini durduracağına güvenilemeyeceğidir: tıkanmış bir
süreç, kararı verirken zaten yanıt veremeyecek durumdadır.

#### Algılama Süresi ile Yanlış Devralma Ödünleşimi

Küme yöneticisinin ayarları tek bir eksende toplanır: ne kadar hızlı karar verilecek.
Hızlı karar kesintiyi kısaltır ve geçici bir tıkanmayı arıza sanma olasılığını artırır.
Aşağıdaki blok bunu ölçüyor. Yanıt verme durumu ve tıkanma süreleri **modeldir**; gerçek
bir sunucu yoklanmıyor. Terfi, yönlendirme ve yeniden bağlanma maliyeti sabit on saniye
alınmıştır.

```bash
node - <<'EOF'
// Model: 3600 saniyelik bir pencere. Birincil sunucu ara sira 1-4 saniye yanit
// vermiyor (gecici tikaniklik); 3000. saniyede gercekten ariza veriyor.
const SURE = 3600, ARIZA = 3000, TIKANMA_OLASILIGI = 0.004;
let tohum = 20250616;
const rastgele = () => (tohum = (tohum * 1103515245 + 12345) % 2147483648) / 2147483648;

const yanit = new Array(SURE).fill(true);
let tikanma = 0;
for (let t = 0; t < ARIZA; t++) {
  if (rastgele() < TIKANMA_OLASILIGI) {
    const uzunluk = 1 + Math.floor(rastgele() * 4);
    tikanma++;
    for (let i = 0; i < uzunluk && t + i < ARIZA; i++) yanit[t + i] = false;
    t += uzunluk;
  }
}
for (let t = ARIZA; t < SURE; t++) yanit[t] = false;

// Terfi, yonlendirme ve uygulamanin yeniden baglanmasi (model degeri).
const DEVRALMA_MALIYETI = 10;
console.log("gecici tikanma: " + tikanma + " kez, her biri 1-4 sn. " +
            "Gercek ariza: " + ARIZA + ". saniye.");
console.log();
console.log("kontrol | ard arda | yanlis    | algilama | kesinti");
console.log("araligi | hata     | devralma  | suresi   | (RTO)");
console.log("--------|----------|-----------|----------|--------");
for (const [aralik, k] of [[1, 1], [1, 2], [1, 3], [2, 3], [5, 2], [5, 3], [10, 3], [30, 3]]) {
  let ardisik = 0, yanlis = 0, algilama = null, tetiklendi = false;
  for (let t = aralik; t < SURE; t += aralik) {
    if (yanit[t]) { ardisik = 0; tetiklendi = false; continue; }
    ardisik++;
    if (ardisik < k || tetiklendi) continue;
    tetiklendi = true;
    if (t < ARIZA) yanlis++;
    else if (algilama === null) algilama = t - ARIZA;
  }
  console.log(String(aralik + " sn").padStart(7) + " | " + String(k).padStart(8) + " | " +
    String(yanlis).padStart(9) + " | " +
    (algilama === null ? "-" : algilama + " sn").padStart(8) + " | " +
    (algilama === null ? "-" : algilama + DEVRALMA_MALIYETI + " sn").padStart(7));
}
EOF
```

```text
gecici tikanma: 10 kez, her biri 1-4 sn. Gercek ariza: 3000. saniye.

kontrol | ard arda | yanlis    | algilama | kesinti
araligi | hata     | devralma  | suresi   | (RTO)
--------|----------|-----------|----------|--------
   1 sn |        1 |        10 |     0 sn |   10 sn
   1 sn |        2 |        10 |     1 sn |   11 sn
   1 sn |        3 |         4 |     2 sn |   12 sn
   2 sn |        3 |         0 |     4 sn |   14 sn
   5 sn |        2 |         0 |     5 sn |   15 sn
   5 sn |        3 |         0 |    10 sn |   20 sn
  10 sn |        3 |         0 |    20 sn |   30 sn
  30 sn |        3 |         0 |    60 sn |   70 sn
```

Sütunların yönü zıttır. Kontrol aralığı büyüdükçe ve üst üste beklenen hata sayısı
arttıkça yanlış devralma sıfıra iner, kesinti süresi yetmiş saniyeye çıkar. Her satır bir
saatlik pencerede on geçici tıkanma içeriyor; en agresif ayar bunların hepsini arıza sayıp
on kez gereksiz devralma yapıyor.

Gereksiz devralmanın zararsız olmadığını görmek önemlidir. Her devralma, asenkron
çoğaltmada gecikme kadar veri kaybı, bütün bağlantıların kopması ve eski birincilin kümeye
yeniden katılma işi demektir. Çalışan bir sunucunun bir saniye yavaşladığı için indirilmesi,
önlemeye çalıştığı kesintiden büyük bir kesinti üretebilir.

Ayarın seçimi kurtarma süresi hedefine bağlanır. Hedef "en çok bir dakika" ise on saniyelik
kontrol aralığı yeterlidir ve yanlış devralma riski taşımaz. Hedef saniyeler mertebesindeyse
tıkanma kaynaklarının azaltılması gerekir; agresif ayar, dengesiz bir sistemin üzerine
kurulduğunda dengesizliği büyütür.

Devralma sonrasında bir iş daha kalır: eski birincilin kümeye yedek sunucu olarak
döndürülmesi. Eski birincil, gönderilmemiş değişiklikler nedeniyle yeni birincilden
ayrışmış bir tarihe sahiptir; olduğu gibi bağlanamaz. Motorların çoğu bu ayrışmayı bulup
geri saran bir araç sağlar; sağlamıyorsa yol, eski birincili yeni birincilden alınmış bir
taban kopyayla baştan kurmaktır.

#### Özet

- Planlı geçiş roller arasında isteyerek yapılan, kayıpsız bir değişimdir; devralma ise
  erişilemeyen bir birincilin yerine yedeğin yükseltilmesidir ve asenkron çoğaltmada
  gecikme kadar veri kaybettirir.
- Bir düğüm "öldü" ile "görünmüyor" arasındaki farkı yerel bilgiyle ayırt edemez; çoğunluk
  kuralı bu ayrımı düğüm sayısından türetir ve iki birincili olanaksız kılar.
- Çift sayılı küme boyutu dayanıklılık katmaz; bölünmede hiçbir taraf çoğunluk
  sağlayamayabilir. Tanık düğüm, veri taşımadan oy sayısını tekleştirir.
- Bölünmüş beynin bıraktığı kimlik çakışmaları teknik olarak çözülebilir; iki tarafta
  birden değişen gerçeklik çözülemez ve otomatik birleştirmenin sınırıdır.
- Çitleme, azınlıkta kalan eski birincilin yazmasını dışarıdan engeller; düğümün kendini
  durduracağına güvenilmez.
- Sağlık denetimi sıklaştıkça algılama süresi kısalır ve geçici tıkanmaların yanlış
  devralma üretme olasılığı artar; ayar, kurtarma süresi hedefine göre seçilir.

#### Sonraki Adım

Devralmanın son adımı uygulamaların yeni birincile yönlendirilmesiydi. Bu, uygulamanın
veritabanına doğrudan değil, araya giren bir katman üzerinden bağlandığı anlamına gelir.
Aynı katmanın çözdüğü ikinci bir sorun daha vardır ve arıza olmadan da her gün
karşılaşılır: her uygulama süreci kendi bağlantısını açtığında, birkaç yüz süreç
veritabanının kaldırabileceğinden çok bağlantı üretir. Sonraki ders bağlantı
havuzlayıcılarını ele alıyor: bağlantı açmanın gerçek maliyeti, havuz boyutunun kuyruk
süresiyle ilişkisi ve oturum ile işlem düzeyinde havuzlamanın farkı.

### Bağlantı Havuzlayıcıları

Source: https://academia.sh/tr/kurslar/veritabani-yonetimi/baglanti-havuzlayicilari

Bağlantı sayısının yönetilmesi: bağlantı açmanın ölçülen maliyeti, oturum ve işlem düzeyinde havuzlamanın farkı, havuzdan devralınan oturum durumunun satır düzeyi güvenliği nasıl delebildiği ve havuz boyutunun kuyruk süresiyle ilişkisi.

Devralmanın son adımı uygulamaların yeni birincile yönlendirilmesiydi; bu, uygulamanın
veritabanına doğrudan değil, araya giren bir katman üzerinden bağlandığı anlamına gelir.
Aynı katman, arıza olmadan da her gün karşılaşılan ikinci bir sorunu çözer.

Sorun şudur: uygulama sunucularının sayısı arttıkça bağlantı sayısı da artar. Her uygulama
örneği kendi bağlantılarını açtığında, on örnek ve örnek başına yirmi bağlantı iki yüz
bağlantı eder. Veritabanı motoru için bağlantı ucuz bir nesne değildir — her biri bellek
tüketir, çoğu motorda ayrı bir süreç ya da iş parçacığı gerektirir ve boşta beklerken bile
maliyeti sürer. Bağlantı sayısı belirli bir eşiği aştığında motor yavaşlar, sonra yeni
bağlantı kabul etmeyi bırakır.

#### Bir Bağlantının Maliyeti

Bir bağlantının açılması, uzaktan bakıldığında tek bir işlem gibi görünür. İçeride birkaç
adım vardır: taşıma katmanı bağlantısının kurulması, şifreli oturumun anlaşması, kimlik
doğrulama, sunucu tarafında bir süreç ya da iş parçacığının başlatılması, oturum
durumunun hazırlanması. Bunların hiçbiri veri okumaz.

Aşağıdaki blok bu maliyetin **en düşük** biçimini ölçüyor: dosya tabanlı bir motorda ağ
yoktur, şifreleme yoktur, kimlik doğrulama yoktur, sunucu tarafında süreç başlatılmaz.
Ölçülen şey yalnız dosyanın açılması ve şema bilgisinin okunmasıdır. Sunucu tabanlı bir
motorda bu maliyetin üzerine sayılan adımlar eklenir; ölçüm bir taban değer verir, üst
değer değil. Süreler makineye bağlıdır.

{/* run: skip */}
```bash
rm -f havuz.db

node - <<'EOF'
const { DatabaseSync } = require("node:sqlite");
const SORGU = "SELECT COUNT(*) c FROM odunc WHERE uye_id=?";
const N = 3000;

const kur = new DatabaseSync("havuz.db");
kur.exec("CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT)");
kur.exec("CREATE INDEX odunc_uye ON odunc(uye_id)");
const ekle = kur.prepare("INSERT INTO odunc(kitap_id,uye_id,alis) VALUES(?,?,?)");
kur.exec("BEGIN");
for (let i = 1; i <= 20000; i++) ekle.run(i % 400, i % 250, "2025-06-01");
kur.exec("COMMIT");
kur.close();

const olc = (ad, is) => {
  const t = process.hrtime.bigint();
  is();
  const ms = Number(process.hrtime.bigint() - t) / 1e6;
  console.log(ad.padEnd(38) + " | " + ms.toFixed(1).padStart(9) + " | " +
              (ms * 1000 / N).toFixed(1).padStart(11));
};

console.log("yol                                    | toplam ms | istek basi us");
console.log("---------------------------------------|-----------|--------------");
olc("her istekte yeni baglanti + deyim", () => {
  for (let i = 0; i < N; i++) {
    const db = new DatabaseSync("havuz.db");
    db.prepare(SORGU).get(i % 250);
    db.close();
  }
});
const paylasilan = new DatabaseSync("havuz.db");
olc("paylasilan baglanti, her istekte deyim", () => {
  for (let i = 0; i < N; i++) paylasilan.prepare(SORGU).get(i % 250);
});
const hazir = paylasilan.prepare(SORGU);
olc("paylasilan baglanti, hazir deyim", () => {
  for (let i = 0; i < N; i++) hazir.get(i % 250);
});
EOF
```

Çıktı makineden makineye değişir; örnek bir koşum:

```text
yol                                    | toplam ms | istek basi us
---------------------------------------|-----------|--------------
her istekte yeni baglanti + deyim      |     104.9 |        35.0
paylasilan baglanti, her istekte deyim |      25.3 |         8.4
paylasilan baglanti, hazir deyim       |      18.7 |         6.2
```

Aynı sorgu üç yolla çalıştırıldı ve bu koşumda istek başına süre beş buçuk kata yakın
değişti; oran makineye göre farklı çıkar.
Sorgunun kendisi her üç satırda da aynı işi yapıyor; fark, sorgunun etrafındaki
hazırlıktır.

İkinci ile üçüncü satır arasındaki fark ayrı bir konuyu gösterir: **hazır deyim (prepared
statement)** yeniden kullanıldığında ayrıştırma ve planlama işi tekrarlanmaz. Bu, İleri SQL
kursunda plan önbelleği bağlamında ele alınan kazancın işletim tarafındaki karşılığıdır.

**Bağlantı havuzu (connection pool)** bu tabloyu birinci satırdan üçüncüye taşır. Havuz,
önceden açılmış bir bağlantı kümesi tutar; uygulama bağlantı istediğinde havuzdan ödünç
alır, işi bitince geri verir. Bağlantı kapanmaz, bir sonraki istek onu hazır bulur.

#### Havuzlama Düzeyleri

Havuzun bağlantıyı ne zaman geri aldığı, uygulamanın nelerden vazgeçmesi gerektiğini
belirler. Üç düzey vardır.

**Oturum düzeyinde havuzlama**, bağlantıyı istemci bağlantısı kapanana kadar aynı istemciye
verir. Uygulama açısından hiçbir şey değişmez; oturuma bağlı her özellik çalışır. Karşılığı,
eşzamanlı istemci sayısı kadar veritabanı bağlantısı gerekmesidir — bağlantı sayısı sorunu
tam olarak çözülmez.

**İşlem düzeyinde havuzlama**, bağlantıyı yalnız bir işlem süresince verir; işlem bitince
bağlantı havuza döner ve başka bir istemciye gidebilir. Az sayıda bağlantıyla çok sayıda
istemciye hizmet edilmesini bu düzey sağlar. Karşılığı, oturuma ait her şeyin
güvenilmez hâle gelmesidir: geçici tablolar, oturum değişkenleri, oturum düzeyindeki
kilitler ve bağlantıya iliştirilmiş hazır deyimler.

**Deyim düzeyinde havuzlama** bağlantıyı her deyimden sonra geri alır. Çok istemcili
kurulumlarda en yüksek paylaşımı verir; çok deyimli işlem yazılamadığı için uygulamanın
işlem kullanamamasına yol açar.

İşlem düzeyindeki havuzlamanın kısıtı bir başarım ayrıntısı değildir; güvenlik sonucu
doğurabilir. Aşağıdaki blok bunu Satır Düzeyi Güvenlik dersindeki düzenek üzerinde
gösteriyor: politika oturum değişkenine bakar, bağlantı ise iki istek arasında el
değiştirir.

```bash
rm -f masa.db

node - <<'EOF'
const { DatabaseSync } = require("node:sqlite");

const db = new DatabaseSync("masa.db");   // havuzdaki tek baglanti
db.exec(`
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, sube_id INT, alis TEXT);
CREATE TABLE oturum(anahtar TEXT PRIMARY KEY, deger TEXT NOT NULL);
`);
const ekle = db.prepare("INSERT INTO odunc(kitap_id,uye_id,sube_id,alis) VALUES(?,?,?,?)");
db.exec("BEGIN");
for (let i = 1; i <= 900; i++)
  ekle.run(i % 400, i % 250, i <= 500 ? 1 : (i <= 800 ? 2 : 3), "2025-06-01");
db.exec("COMMIT");

// Politika oturum degiskenine bakar (Satir Duzeyi Guvenlik dersindeki dizenek).
const gorunen = () => db.prepare(
  "SELECT COUNT(*) c FROM odunc WHERE sube_id=" +
  "(SELECT CAST(deger AS INTEGER) FROM oturum WHERE anahtar='sube_id')").get().c;
const ayarla = (sube) => db.prepare(
  "INSERT INTO oturum VALUES('sube_id',?)" +
  " ON CONFLICT(anahtar) DO UPDATE SET deger=excluded.deger").run(String(sube));
const gercek = (s) => db.prepare("SELECT COUNT(*) c FROM odunc WHERE sube_id=?").get(s).c;

console.log("subelerin gercek kayit sayisi: 1 -> " + gercek(1) + ", 2 -> " + gercek(2));
console.log();

// 1. istek: 1 numarali subenin masasi baglantiyi alir.
ayarla(1);
console.log("1. istek (sube 1) gorunen satir      : " + gorunen());

// Baglanti havuza geri verilir. Havuz oturum durumunu temizlemiyor.
console.log("2. istek (sube 2), oturum ayarlanmadi: " + gorunen() +
            "  <- sube 1'in satirlari");

// Ayni sira, havuz devirde oturum durumunu temizliyor.
db.exec("DELETE FROM oturum");
console.log("2. istek (sube 2), havuz temizledi   : " + gorunen() +
            "  <- hicbir satir gorunmuyor");
ayarla(2);
console.log("2. istek (sube 2), oturum ayarlandi  : " + gorunen());
EOF
```

```text
subelerin gercek kayit sayisi: 1 -> 500, 2 -> 300

1. istek (sube 1) gorunen satir      : 500
2. istek (sube 2), oturum ayarlanmadi: 500  <- sube 1'in satirlari
2. istek (sube 2), havuz temizledi   : 0  <- hicbir satir gorunmuyor
2. istek (sube 2), oturum ayarlandi  : 300
```

İkinci satır sorunun kendisidir. İkinci şubenin masası üç yüz kayıt görmesi gerekirken beş
yüz kayıt görüyor; gördükleri başka bir şubenin ödünç kayıtları. Politika doğru yazılmış,
sorgu doğru, veri doğru — yanlış olan tek şey, bağlantının önceki kullanıcının oturum
değerini taşımasıdır.

Üçüncü satır çözümü gösterir: havuz, bağlantıyı devrederken oturum durumunu temizler.
Temizlenmiş durumda politika hiçbir satır döndürmez. Bu davranış istenen davranıştır —
**kapalıya düşmek**: bilgi eksikse veri gösterilmez. Uygulamanın her isteğin başında oturum
değerini yeniden ayarlaması, dördüncü satırdaki doğru sonucu verir.

Buradan çıkan işletim kuralı, işlem düzeyinde havuzlama kullanan her kurulum için geçerlidir:
oturuma bağlı hiçbir bilgi istekler arasında yaşadığı varsayılmadan yazılır ve havuzun
devir temizliği yapılandırmadan doğrulanır.

#### Havuz Boyutu ve Kuyruk

Havuz boyutu, veritabanına aynı anda kaç isteğin girebileceğini belirler. Boyut küçükse
istekler kuyrukta bekler; büyükse motorda eşzamanlı çalışan işlem sayısı artar ve işlemler
birbirini yavaşlatır — kilit çekişmesi, önbellek paylaşımı ve zamanlayıcı yükü nedeniyle.

Aşağıdaki blok bunu benzetimle inceliyor. Gelişler, taban işlem süresi ve çekişme katsayısı
**model değerleridir**; gerçek bir veritabanı çalıştırılmıyor. Çekişmenin biçimi de bir
varsayımdır: eşzamanlı işlem sayısı çekirdek sayısını aştıkça her işlemin süresi doğrusal
büyüyor. Gerçek bir sistemde bu eğri ölçülerek bulunur. Modelin gösterdiği şey eğrinin
**bir en iyisi olduğudur**.

```bash
node - <<'EOF'
// Model: istekler havuzdan bir baglanti bekler, sonra veritabaninda calisir.
// Es zamanli islem sayisi cekirdek sayisini astikca her islem yavaslar (cekisme).
const TABAN = 10, CEKIRDEK = 8, CEKISME = 0.25;   // ms, adet, katsayi
const HIZ = 700, SURE = 30000, ADIM = 0.25;       // istek/sn, ms, ms
const ZAMAN_ASIMI = 200;                          // ms

let tohum = 20250616;
const rastgele = () => (tohum = (tohum * 1103515245 + 12345) % 2147483648) / 2147483648;
const gelis = [];
for (let t = 0; t < SURE; ) {
  t += -Math.log(1 - rastgele()) * 1000 / HIZ;
  if (t < SURE) gelis.push(t);
}

console.log("yuk: " + HIZ + " istek/sn, taban islem suresi " + TABAN + " ms, " +
            CEKIRDEK + " cekirdek, " + gelis.length + " istek");
console.log();
console.log("havuz | tamamlanan | kuyrukta | islemde | toplam  | zaman");
console.log("boyu  | istek      | (ort ms) | (ort ms)| (ort ms)| asimi");
console.log("------|------------|----------|---------|---------|-------");
for (const havuz of [2, 4, 8, 16, 32, 64]) {
  const kuyruk = [], aktif = [];
  let sonraki = 0, biten = 0, kuyrukToplam = 0, islemToplam = 0, asim = 0;
  for (let t = 0; t < SURE * 1.5; t += ADIM) {
    while (sonraki < gelis.length && gelis[sonraki] <= t) kuyruk.push(gelis[sonraki++]);
    while (kuyruk.length && aktif.length < havuz) {
      const g = kuyruk.shift();
      if (t - g > ZAMAN_ASIMI) { asim++; continue; }
      aktif.push({ bekleme: t - g, kalan: TABAN, basladi: t });
    }
    const yavaslik = 1 + CEKISME * Math.max(0, aktif.length - CEKIRDEK);
    for (const i of aktif) i.kalan -= ADIM / yavaslik;
    for (let i = aktif.length - 1; i >= 0; i--) {
      if (aktif[i].kalan > 0) continue;
      kuyrukToplam += aktif[i].bekleme;
      islemToplam += t - aktif[i].basladi;
      biten++; aktif.splice(i, 1);
    }
    if (sonraki >= gelis.length && !kuyruk.length && !aktif.length) break;
  }
  console.log(String(havuz).padStart(5) + " | " + String(biten).padStart(10) + " | " +
    (kuyrukToplam / biten).toFixed(1).padStart(8) + " | " +
    (islemToplam / biten).toFixed(1).padStart(7) + " | " +
    ((kuyrukToplam + islemToplam) / biten).toFixed(1).padStart(7) + " | " +
    String(asim).padStart(6));
}
EOF
```

```text
yuk: 700 istek/sn, taban islem suresi 10 ms, 8 cekirdek, 21200 istek

havuz | tamamlanan | kuyrukta | islemde | toplam  | zaman
boyu  | istek      | (ort ms) | (ort ms)| (ort ms)| asimi
------|------------|----------|---------|---------|-------
    2 |       6040 |    197.4 |     9.8 |   207.2 |  15160
    4 |      12080 |    196.4 |     9.8 |   206.1 |   9120
    8 |      21200 |      3.9 |     9.8 |    13.6 |      0
   16 |      16116 |    192.9 |    29.7 |   222.6 |   5084
   32 |      13823 |    193.5 |    69.6 |   263.1 |   7377
   64 |      12919 |    190.9 |   149.2 |   340.1 |   8281
```

Tablo iki ayrı arıza biçimini aynı sütunda gösteriyor. İki ve dört boyutlu havuzlarda
işlemde geçen süre en düşük değerdedir; sorun kuyruktadır. Havuzun geçirdiği iş, gelen
işten azdır ve isteklerin çoğu zaman aşımına uğrar. Bu **havuz doygunluğudur**.

On altı ve üzeri boyutlarda kuyruk aynı derecede kötüdür, ama nedeni başkadır: işlemde
geçen süre üç kattan on beş kata kadar büyümüştür. Havuz daha çok isteği içeri aldı,
içeridekiler birbirini yavaşlattı ve geçen toplam iş azaldı. Havuzu büyütmek burada
durumu kötüleştirdi.

Sekiz boyutlu havuz iki uçtan da uzaktır: bekleme dört milisaniye, toplam yanıt on üç
milisaniye, zaman aşımı yok. Bu sayı çekirdek sayısıyla aynıdır ve rastlantı değildir —
en iyi havuz boyutu, motorun gerçekten paralel yürütebildiği iş miktarına yakındır. Disk
beklemesi baskın olan iş yüklerinde bu sayı çekirdek sayısının üzerine çıkar; işlemci
baskın iş yüklerinde çekirdek sayısında kalır. Doğru değeri veren tek yol ölçmektir.

Havuz boyutu tek bir uygulama örneği için değil, sistem bütünü için düşünülür. Toplam
bağlantı sayısı, havuz boyutu ile uygulama örneği sayısının çarpımıdır; on örnekli bir
kurulumda örnek başına yirmi bağlantı, motora iki yüz bağlantı olarak gelir. Uygulama
içindeki havuzların bu çarpımı görmemesi, ortada tek bir havuzlayıcı bulundurmanın başlıca
gerekçesidir.

Son bir ayar, boşta bekleyen bağlantıların ömrüdür. Yük düştüğünde havuzun küçülmesi
kaynak boşaltır; her seferinde yeniden açmak ise birinci tablodaki maliyeti geri getirir.
İki uç arasındaki denge, yükün gün içindeki değişimine göre kurulur.

#### Özet

- Bağlantı açmak, sorgunun kendisinden bağımsız bir hazırlık maliyeti taşır; ölçümde aynı
  sorgu, yeni bağlantı ve hazır deyim yolları arasında örnek koşumda beş buçuk kata yakın fark verdi.
- Havuz, önceden açılmış bağlantıları ödünç vererek bu maliyeti ortadan kaldırır ve motora
  gelen bağlantı sayısını sınırlar.
- Oturum düzeyinde havuzlama uygulamayı kısıtlamaz ama bağlantı sayısını azaltmaz; işlem
  düzeyinde havuzlama azaltır ve oturuma bağlı her şeyi güvenilmez kılar.
- Havuzdan devralınan oturum değişkeni, satır düzeyi güvenlik politikasının başka bir
  hesabın satırlarını göstermesine yol açabilir; havuzun devir temizliği doğrulanmalıdır.
- Havuz boyutu küçükse kuyruk büyür, büyükse eşzamanlı işlemler birbirini yavaşlatır; en
  iyi değer motorun paralel yürütebildiği iş miktarına yakındır ve ölçülerek bulunur.
- Toplam bağlantı sayısı havuz boyutu ile uygulama örneği sayısının çarpımıdır.

#### Sonraki Adım

Bu dersteki her karar bir ölçüme dayanıyordu: bağlantı maliyeti, kuyruk süresi, eşzamanlı
işlem sayısı, zaman aşımına uğrayan istek oranı. Bunların hiçbiri kendiliğinden görünmez;
havuz doygunluğu, uygulama tarafında yalnız "veritabanı yavaş" olarak fark edilir ve
yanlış yerde aranır. Sonraki ders izlemeyi ele alıyor: hangi büyüklüklerin toplanacağı,
ortalamanın neden yanıltıcı olduğu ve yüzdelik dilimlerin ne gösterdiği, yavaş sorguların
eşikle yakalanması, bekleme olaylarının darboğazı nasıl adlandırdığı ve bir uyarının ne
zaman anlamlı olduğu.

### İzleme ve Uyarı

Source: https://academia.sh/tr/kurslar/veritabani-yonetimi/izleme-ve-uyari

Veritabanının durumunun sayılarla bilinmesi: duvar saati ile işlemci süresi arasındaki farkın anlamı, sorgu sürelerinin dağılımı ve yüzdelik dilimler, eşiğe dayalı yavaş sorgu kütüğünün kaçırdıkları, kilit beklemesinin ölçülmesi ve uyarının ne zaman anlamlı olduğu.

Önceki dersteki her karar bir ölçüme dayanıyordu: bağlantı maliyeti, kuyruk süresi,
eşzamanlı işlem sayısı, zaman aşımına uğrayan istek oranı. Bunların hiçbiri kendiliğinden
görünmez. Havuz doygunluğu, uygulama tarafında yalnız "veritabanı yavaş" olarak fark
edilir ve genellikle yanlış yerde aranır.

İzlemenin işi bu boşluğu doldurmaktır: sistemin durumunu, arıza anında değil sürekli
olarak sayılara çevirmek. Bu ders hangi sayıların toplanacağını, nasıl okunacağını ve bir
uyarının ne zaman anlamlı olduğunu ele alıyor.

#### Bir Sorgunun Süresi Neyi Ölçer

En temel büyüklük bir deyimin ne kadar sürdüğüdür; ama "süre" tek bir sayı değildir.
Aşağıdaki blok aynı tablo üzerinde iki sorguyu ölçüyor. Süreler makineye ve dosya
sistemine bağlıdır; sizde farklı çıkacaktır.

```bash
rm -f olcum.db

sqlite3 olcum.db <<'SQL'
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT NOT NULL, uye_id INT NOT NULL,
                   sube_id INT NOT NULL, alis TEXT NOT NULL, iade TEXT);
WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n<400000)
INSERT INTO odunc(id,kitap_id,uye_id,sube_id,alis)
SELECT n,(n%400)+1,(n%900)+1,(n%3)+1,'2025-06-01' FROM s;
CREATE INDEX odunc_uye ON odunc(uye_id);
SQL

sqlite3 olcum.db <<'SQL'
.header on
.timer on
SELECT COUNT(*) AS dizinli FROM odunc WHERE uye_id=42;
SELECT COUNT(*) AS taramali FROM odunc WHERE alis LIKE '2025-06-01';
SQL
```

{/* mask: (?<=Run Time: real )[0-9.]+ user [0-9.]+ sys [0-9.]+ */}
```text
dizinli
445
Run Time: real 0.000 user 0.000060 sys 0.000036
taramali
400000
Run Time: real 0.014 user 0.012269 sys 0.001597
```

Üç sayı üç ayrı şeyi söylüyor. **Duvar saati süresi** (`real`) sorgunun başlangıcı ile
bitişi arasında geçen zamandır — kullanıcının beklediği süre budur. **İşlemci süresi**
(`user` ve `sys`) o sorgu için gerçekten hesaplama yapılan zamandır.

İkinci sorguda duvar saati süresi işlemci süresine yakındır: sorgu dört yüz bin satırı
okurken hep meşguldü, hiçbir şey beklemedi. Bu, izlemenin en yararlı tek kuralını verir:
**duvar saati süresi işlemci süresinden belirgin biçimde büyükse, aradaki fark
beklemedir.** Bekleme; diskten veri gelmesi, bir kilidin bırakılması, günlüğün diske
yazılması ya da ağdan yanıt gelmesi olabilir. Yalnız işlemci süresine bakan bir izleme, en
sık rastlanan yavaşlık nedenlerini göremez.

Motorların çoğu bunu daha ayrıntılı sunar: her oturumun o anda **hangi olayı beklediği**
kayıt altındadır. Bekleme olayları sınıflara ayrılır — kilit beklemesi, veri dosyasından
okuma, günlüğün diske yazılması, istemcinin veri göndermesini bekleme. Bu sınıfların
dağılımı, darboğazın adını doğrudan verir; ölçmeden yapılan tahminler genellikle yanlış
çıkar.

#### Ortalama, Dağılım ve Toplam Etki

Tek bir çalıştırmanın süresi bir sayıdır; işletimde gereken şey binlerce çalıştırmanın
dağılımıdır. Aşağıdaki blok bir günün iş yükünü canlandırıyor: dört farklı sorgu biçimi,
farklı çağrı sayılarıyla çalışıyor ve her çalıştırmanın süresi kaydediliyor.

Üye dağılımı kasıtlı olarak çarpıktır: bir numaralı üye, kütüphanenin toplu ödünç
hesabıdır ve çok sayıda kaydı vardır. Süreler ortama bağlıdır; anlamlı olan sütunlar
arasındaki ilişkidir.

{/* run: skip */}
```bash
rm -f izleme.db

node - <<'EOF'
const { DatabaseSync } = require("node:sqlite");
const db = new DatabaseSync("izleme.db");
db.exec(`CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT NOT NULL, uye_id INT NOT NULL,
                            sube_id INT NOT NULL, alis TEXT NOT NULL, iade TEXT)`);
// Uye dagilimi carpik: 1 numarali uye kutuphanenin toplu odunc hesabidir.
const ekle = db.prepare("INSERT INTO odunc(kitap_id,uye_id,sube_id,alis) VALUES(?,?,?,?)");
db.exec("BEGIN");
for (let i = 1; i <= 200000; i++)
  ekle.run(i % 400 + 1, i % 12 === 0 ? 1 : (i % 900) + 2, (i % 3) + 1, "2025-06-01");
db.exec("COMMIT");
db.exec("CREATE INDEX odunc_uye ON odunc(uye_id)");

const kutuk = [];   // her calistirma icin (imza, sure)
const calistir = (imza, deyim, ...p) => {
  const t = process.hrtime.bigint();
  deyim.all(...p);
  kutuk.push([imza, Number(process.hrtime.bigint() - t) / 1e6]);
};
const uyeSorgu = db.prepare("SELECT id,kitap_id,alis FROM odunc WHERE uye_id=?");
const anahtarSorgu = db.prepare("SELECT kitap_id,uye_id FROM odunc WHERE id=?");
const subeSorgu = db.prepare("SELECT COUNT(*) c FROM odunc WHERE sube_id=? AND iade IS NULL");
const raporSorgu = db.prepare(
  "SELECT sube_id, COUNT(*) c FROM odunc WHERE alis>=? GROUP BY sube_id");

for (let i = 0; i < 2000; i++) calistir("uye kayitlari", uyeSorgu, i % 12 === 0 ? 1 : i % 900 + 2);
for (let i = 0; i < 20000; i++) calistir("anahtarla arama", anahtarSorgu, i % 200000 + 1);
for (let i = 0; i < 200; i++) calistir("sube sayimi", subeSorgu, (i % 3) + 1);
for (let i = 0; i < 20; i++) calistir("gunluk rapor", raporSorgu, "2025-01-01");

const dilim = (a, p) => a[Math.min(a.length - 1, Math.floor(a.length * p))];
const toplamSure = kutuk.reduce((t, k) => t + k[1], 0);
console.log("imza            | cagri | ortalama | p50   | p95   | p99   | toplam  | pay");
console.log("----------------|-------|----------|-------|-------|-------|---------|------");
for (const imza of [...new Set(kutuk.map((k) => k[0]))]) {
  const s = kutuk.filter((k) => k[0] === imza).map((k) => k[1]).sort((a, b) => a - b);
  const toplam = s.reduce((t, v) => t + v, 0);
  console.log(imza.padEnd(15) + " | " + String(s.length).padStart(5) + " | " +
    (toplam / s.length).toFixed(3).padStart(8) + " | " + dilim(s, 0.5).toFixed(2).padStart(5) +
    " | " + dilim(s, 0.95).toFixed(2).padStart(5) + " | " + dilim(s, 0.99).toFixed(2).padStart(5) +
    " | " + (toplam.toFixed(0) + " ms").padStart(7) + " | %" +
    (100 * toplam / toplamSure).toFixed(1).padStart(4));
}
const ESIK = 10;
const yavas = kutuk.filter((k) => k[1] > ESIK);
const yavasSure = yavas.reduce((t, k) => t + k[1], 0);
console.log();
console.log("toplam calistirma: " + kutuk.length + ", toplam sure: " +
            toplamSure.toFixed(0) + " ms");
console.log(ESIK + " ms esigini asan: " + yavas.length + " calistirma, " +
            yavasSure.toFixed(0) + " ms (%" + (100 * yavasSure / toplamSure).toFixed(1) + ")");
EOF
```

Çıktı makineden makineye değişir; örnek bir koşum:

```text
imza            | cagri | ortalama | p50   | p95   | p99   | toplam  | pay
----------------|-------|----------|-------|-------|-------|---------|------
uye kayitlari   |  2000 |    0.819 |  0.10 |  7.43 | 10.60 | 1638 ms | %47.4
anahtarla arama | 20000 |    0.005 |  0.00 |  0.01 |  0.01 |   93 ms | % 2.7
sube sayimi     |   200 |    5.874 |  5.09 | 10.88 | 14.55 | 1175 ms | %34.0
gunluk rapor    |    20 |   27.633 | 26.55 | 35.79 | 35.79 |  553 ms | %16.0

toplam calistirma: 22220, toplam sure: 3459 ms
10 ms esigini asan: 61 calistirma, 1063 ms (%30.7)
```

Tablo, izlemenin üç ayrı dersini aynı anda veriyor.

**Ortalama dağılımı anlatmaz.** Üye kayıtları sorgusunun ortalaması bir milisaniyeye
yakın; ortanca değeri ise bunun yaklaşık sekizde biri. Aradaki fark, çağrıların on ikide
birinin toplu ödünç hesabına gitmesinden geliyor. Ortalama, bu iki ayrı davranışı tek bir
sayıya karıştırır ve ikisini de doğru anlatmaz. **Yüzdelik dilimler (percentile)** ayrımı
korur: p50 sıradan kullanıcının gördüğü süredir, p95 ve p99 kuyruğu tarif eder.

**Yavaş olan ile pahalı olan aynı şey değildir.** Günlük rapor tek çalıştırmada en yavaş
sorgudur; toplam yükün altıda birini üretir. Üye kayıtları sorgusu her çalıştırmada
hızlıdır ve toplam yükün neredeyse yarısını üretir. Sistemi hızlandırmak için ilk
bakılacak yer, ortalama süresi en yüksek sorgu değil, **çağrı sayısı ile süresinin çarpımı
en yüksek** olandır.

**Eşiğe dayalı yavaş sorgu kütüğü yalnız bir dilimi görür.** On milisaniyelik eşik, iş
yükünün yaklaşık yüzde otuzunu yakaladı; kalan yüzde yetmiş görünmez kaldı. Eşik, tek başına
patolojik olan deyimleri bulmak için değerlidir; iş yükünün nereye gittiğini söylemez.
Bunun için her çalıştırmanın **sorgu imzasına (query fingerprint)** göre toplanması
gerekir — parametreleri soyulmuş deyim metni, aynı biçimin bütün çalıştırmalarını tek
satırda toplar.

#### Beklemenin Ölçülmesi

Sorgunun kendi süresi, yanıt süresinin tamamı değildir. Aşağıdaki blok bunu doğrudan
ölçüyor: aynı güncelleme deyimi önce boş bir sistemde, sonra başka bir sürecin aynı
tabloyu kilitli tuttuğu sırada çalışıyor. İkinci süre, kilidi tutan işlemin ne kadar
sürdüğüne bağlıdır.

```bash
rm -f bekleme.db kilit.isaret

node - <<'EOF'
const { DatabaseSync } = require("node:sqlite");
const { spawn } = require("node:child_process");
const fs = require("node:fs");
const bekle = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);

const db = new DatabaseSync("bekleme.db");
db.exec("PRAGMA busy_timeout=5000");
db.exec("CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, iade TEXT)");
const ekle = db.prepare("INSERT INTO odunc(kitap_id,uye_id) VALUES(?,?)");
db.exec("BEGIN");
for (let i = 1; i <= 5000; i++) ekle.run(i % 400, i % 250);
db.exec("COMMIT");

const yaz = () => {
  const t = process.hrtime.bigint();
  db.exec("BEGIN IMMEDIATE");
  db.prepare("UPDATE odunc SET iade='2025-06-30' WHERE id=?").run(7);
  db.exec("COMMIT");
  return Number(process.hrtime.bigint() - t) / 1e6;
};
const serbest = yaz();

// Ikinci bir surec ayni tabloyu 1200 ms boyunca kilitli tutar.
const cocukKod = `
const { DatabaseSync } = require("node:sqlite");
const fs = require("node:fs");
const db = new DatabaseSync("bekleme.db");
db.exec("PRAGMA busy_timeout=5000");
db.exec("BEGIN IMMEDIATE");
db.prepare("UPDATE odunc SET iade='2025-06-29' WHERE id=?").run(9);
fs.writeFileSync("kilit.isaret", "1");
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 1200);
db.exec("COMMIT");
`;
spawn(process.execPath, ["-e", cocukKod], { stdio: "ignore" });
while (!fs.existsSync("kilit.isaret")) bekle(10);

const kilitli = yaz();
console.log("cakismasiz yazma        : " + serbest.toFixed(1) + " ms");
console.log("kilit altinda ayni yazma: " + kilitli.toFixed(1) + " ms");
console.log("beklemenin payi         : %" +
            (100 * (kilitli - serbest) / kilitli).toFixed(2));
fs.rmSync("kilit.isaret", { force: true });
EOF
```

{/* mask: (?<=: )[0-9.]+(?= ms) */}
{/* mask: (?<=%)[0-9.]+$ */}
```text
cakismasiz yazma        : 0.0 ms
kilit altinda ayni yazma: 1197.6 ms
beklemenin payi         : %100.00
```

Aynı deyim, aynı satır, aynı veri; süre birkaç mertebe büyüdü ve büyümenin tamamı beklemeden
geldi. Deyimin kendisi hakkında toplanan hiçbir ölçüm bunu açıklayamaz: sorgu planı aynı,
okunan satır sayısı aynı, işlemci süresi aynı. Açıklama yalnız "bu oturum ne bekliyordu"
sorusunun yanıtındadır.

İşletimde bu fark şöyle görünür: uygulama tarafında ölçülen yanıt süresi büyür, veritabanı
tarafında sorgu süreleri normaldir. İki taraf birbirini işaret eder. Bekleme olaylarının
izlenmesi bu tartışmayı bitirir — süre, kilidi bekleyen tarafta değil, kilidi **tutan**
uzun işlemde harcanmaktadır.

#### Uyarının Anlamlı Olması

Toplanan her sayı bir uyarıya dönüştürülemez. İşleyen bir uyarı üç koşulu sağlar.

**Bir eşiği vardır ve eşik taban çizgisinden türetilmiştir.** "Sorgu süresi yüksek" bir
uyarı değildir; "p95 değeri son bir saat boyunca normal değerinin üç katında" bir
uyarıdır. Taban çizgisi, sistemin sağlıklı olduğu bilinen dönemde ölçülerek elde edilir.

**Süreklilik koşulu vardır.** Anlık bir sıçrama uyarı üretmez; belirli bir pencerede
sürmesi gerekir. Bu, önceki dersteki devralma ayarıyla aynı ödünleşimdir: duyarlılık
arttıkça yanlış uyarı sayısı artar.

**Bir eyleme karşılık gelir.** Karşılığında yapılacak bir iş yoksa, uyarı yalnız gürültü
üretir ve zamanla bakılmaz hâle gelir. Uyarı yorgunluğu, izleme kurulumlarının en sık
görülen başarısızlık biçimidir: yüzlerce uyarı arasında gerçek olan fark edilmez.

Uyarılacak büyüklüklerin seçiminde işe yarayan bir ayrım, **belirti** ile **neden**
arasındaki farktır. Belirti, kullanıcının hissettiği şeydir: yanıt süresi, hata oranı,
başarısız ödünç işlemi sayısı. Neden, o belirtiyi üreten iç durumdur: çoğaltma gecikmesi,
havuz doygunluğu, disk doluluğu, uzun süren işlem. Uyarılar belirtiler üzerine kurulur —
kullanıcı etkilenmediğinde kimse uyandırılmaz. Nedenler ise gösterge panolarında tutulur;
uyarı geldiğinde bakılacak yer orasıdır.

Bu ayrımın dışında kalan bir sınıf vardır: kullanıcı henüz etkilenmediği hâlde uyarı
gerektiren durumlar. Diskin dolmasına iki gün kalması, yedeklemenin üç gündür
çalışmaması, çoğaltma gecikmesinin kurtarma noktası hedefini aşması. Bunlar tükenmekte
olan bir kaynağı gösterir ve etki oluşmadan önce ele alınması gerekir.

Son olarak izlemenin kendisinin de kapsamı vardır. Bu konudaki her ders bir izlenecek
büyüklük bıraktı: yedeğin yaşı ve doğrulanma tarihi, kurtarma tatbikatının süresi,
çoğaltma gecikmesi, küme üyelerinin durumu, havuzdaki boş bağlantı sayısı ve kuyruk süresi.
İzlenmeyen bir güvence, güvence değildir.

#### Özet

- Duvar saati süresi ile işlemci süresi arasındaki fark beklemedir; yalnız işlemci
  süresine bakan izleme, yavaşlığın en sık nedenlerini göremez.
- Ortalama, çarpık dağılımları gizler; p50 sıradan durumu, p95 ve p99 kuyruğu anlatır.
- İş yükünün nereye gittiği, çağrı sayısı ile süre çarpımının sorgu imzasına göre
  toplanmasıyla bulunur; en yavaş sorgu genellikle en pahalı sorgu değildir.
- Eşiğe dayalı yavaş sorgu kütüğü tek başına patolojik deyimleri yakalar, toplam yükün
  büyük bölümünü görmez.
- Kilit beklemesi deyimin kendi ölçümlerinde hiç görünmez; süre, kilidi tutan işlemde
  harcanır ve ancak bekleme olayları izlenerek bulunur.
- Anlamlı uyarı, taban çizgisinden türetilmiş bir eşiğe, bir süreklilik koşuluna ve
  karşılık gelen bir eyleme sahiptir; uyarılar belirtiler üzerine, gösterge panoları
  nedenler üzerine kurulur.

#### Sonraki Adım

İzleme, sistemin olağan yükü altında ne yaptığını gösterir. Bazı işler ise olağan yükün
dışındadır ve sistemi bilerek zorlar: bir arşivin içeri aktarılması, başka bir kurumdan
gelen üye listesinin yüklenmesi, yıllık ödünç kayıtlarının dışarı çıkarılması. Bu işler
milyonlarca satırı tek seferde taşır ve satır satır yazma alışkanlığıyla yapıldığında
saatler sürer. Sonraki ders toplu veri yüklemeyi ele alıyor: işlem sınırının süreye
etkisi, dizinlerin yükleme sırasındaki maliyeti, hazır deyim ile içe aktarım komutunun
karşılaştırılması ve bozuk satırların yüklemeyi durdurmadan ayıklanması.

### Toplu Veri Yükleme

Source: https://academia.sh/tr/kurslar/veritabani-yonetimi/toplu-veri-yukleme

Yüksek hacimli içe ve dışa aktarımın maliyeti: işlem sınırının yükleme hızına etkisi, dizinlerin yükleme sırasında mı sonrasında mı kurulacağı, hazırlık tablosuyla bozuk satırların ayıklanması ve dışa aktarımın biçimi.

İzleme, sistemin olağan yükü altında ne yaptığını gösterir. Bazı işler ise olağan yükün
dışındadır ve sistemi bilerek zorlar: bir arşivin içeri aktarılması, başka bir kurumdan
gelen üye listesinin yüklenmesi, yıllık ödünç kayıtlarının dışarı çıkarılması. Bu işler
milyonlarca satırı tek seferde taşır.

Toplu yüklemenin sıradan yazmadan ayrıldığı nokta şudur: sıradan yazmada bir satırın
maliyeti önemsizdir, toplu yüklemede satır başına düşen her maliyet satır sayısıyla
çarpılır. Bu ders o çarpanların nereden geldiğini ve nasıl kaldırıldığını ele alıyor.

#### İşlem Sınırının Bedeli

Her kesinleştirme, motorun değişikliği kalıcı hâle getirmesini gerektirir: günlük diske
yazılır ve yazmanın gerçekten tamamlandığı doğrulanır. Bu adım kursun Motor Mimarisi
konusunda ele alınmıştı ve orada bir satırın güvencesi olarak sunulmuştu. Toplu yüklemede
aynı adım, satır sayısı kadar tekrarlanan bir maliyete dönüşür.

Aşağıdaki blok aynı otuz bin satırı beş farklı parti büyüklüğüyle yüklüyor. Mutlak süreler
makineye, diske ve dosya sistemine bağlıdır; anlamlı olan satırlar arasındaki orandır.

{/* run: skip */}
```bash
rm -f yukleme.db yukleme.db-journal

node - <<'EOF'
const { DatabaseSync } = require("node:sqlite");
const fs = require("node:fs");
const N = 30000;
const SEMA = `CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT NOT NULL,
                uye_id INT NOT NULL, sube_id INT NOT NULL, alis TEXT NOT NULL)`;

const olc = (ad, parti) => {
  fs.rmSync("yukleme.db", { force: true });
  fs.rmSync("yukleme.db-journal", { force: true });
  const db = new DatabaseSync("yukleme.db");
  db.exec("PRAGMA synchronous=FULL");
  db.exec(SEMA);
  const ekle = db.prepare(
    "INSERT INTO odunc(kitap_id,uye_id,sube_id,alis) VALUES(?,?,?,?)");
  const t = process.hrtime.bigint();
  for (let i = 0; i < N; i += parti) {
    if (parti > 1) db.exec("BEGIN");
    for (let j = i; j < Math.min(i + parti, N); j++)
      ekle.run(j % 400 + 1, j % 900 + 1, j % 3 + 1, "2025-06-01");
    if (parti > 1) db.exec("COMMIT");
  }
  const ms = Number(process.hrtime.bigint() - t) / 1e6;
  db.close();
  console.log(ad.padEnd(26) + " | " + String(Math.ceil(N / parti)).padStart(6) + " | " +
    (ms.toFixed(0) + " ms").padStart(9) + " | " +
    Math.round(N / (ms / 1000)).toLocaleString("tr-TR").padStart(11));
};

console.log("yol                        | islem  | sure      | satir/saniye");
console.log("---------------------------|--------|-----------|------------");
olc("satir basina bir islem", 1);
olc("100 satirlik partiler", 100);
olc("1.000 satirlik partiler", 1000);
olc("10.000 satirlik partiler", 10000);
olc("tek islem", N);
EOF
```

Çıktı makineden makineye değişir; örnek bir koşum:

```text
yol                        | islem  | sure      | satir/saniye
---------------------------|--------|-----------|------------
satir basina bir islem     |  30000 |    635 ms |      47.275
100 satirlik partiler      |    300 |     19 ms |   1.581.326
1.000 satirlik partiler    |     30 |     12 ms |   2.454.749
10.000 satirlik partiler   |      3 |     13 ms |   2.238.340
tek islem                  |      1 |     14 ms |   2.135.966
```

Satır başına bir işlem ile tek işlem arasındaki fark bu koşumda kırk kattan fazla; oran
diske ve dosya sistemine göre çok değişir. Bu koşum bellekte duran bir dosya sisteminde
alındı; orada kesinleştirme diske inmeyi beklemediği için gerçek bir diskteki fark tablodakinden
büyük olabilir. Yüklenen veri
aynı, yazılan satırlar aynı, yapılan iş aynı — fark yalnız kaç kez kesinleştirildiğidir.

Eğrinin biçimi de önemlidir. Kazancın büyük bölümü ilk adımda, yüzlük partilere geçişte
elde ediliyor; binden sonrası anlamlı bir iyileşme vermiyor. Bu, parti büyüklüğünün olabildiğince
büyük seçilmesi gerektiği anlamına gelmez. Tek bir dev işlemin üç sakıncası vardır:
başarısız olduğunda baştan başlanır, geri alma bilgisi işlem süresince birikir ve uzun
süren yazma işlemi kursun Motor Mimarisi konusunda ele alınan ölü satır temizliğini
geciktirir. Yüz bin ile bir milyon satır arasındaki bir parti büyüklüğü, ölçülen kazancın
neredeyse tamamını verirken bu üç sakıncayı da sınırlar.

Parti sınırı aynı zamanda bir ilerleme noktasıdır. Her partinin sonunda kaç satırın
yüklendiği bilinir; yükleme kesildiğinde kalınan yerden sürdürülebilir. Bunun için gelen
verinin sırasının belirlenimci olması ve yüklenen son anahtarın kaydedilmesi yeter.

#### Dizinlerin Yükleme Sırasındaki Maliyeti

İkinci çarpan dizinlerdir. Tabloya eklenen her satır, her dizine de bir giriş ekler ve bu
girişin ağacın doğru yerine yerleştirilmesi gerekir. Satırlar dizin anahtarına göre
sıralı gelmediğinde, her ekleme ağacın rastgele bir yerine dokunur.

Alternatif, dizinleri yükleme bittikten sonra kurmaktır: motor o zaman bütün anahtarları
bir kerede sıralar ve ağacı aşağıdan yukarı, sırayla oluşturur.

```bash
rm -f dizinli.db dizinli.db-journal

node - <<'EOF'
const { DatabaseSync } = require("node:sqlite");
const fs = require("node:fs");
const N = 200000;
const SEMA = `CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT NOT NULL,
                uye_id INT NOT NULL, sube_id INT NOT NULL, alis TEXT NOT NULL)`;
const DIZIN = ["CREATE INDEX odunc_uye ON odunc(uye_id)",
               "CREATE INDEX odunc_kitap ON odunc(kitap_id)",
               "CREATE INDEX odunc_sube_alis ON odunc(sube_id, alis)"];
const sure = (is) => { const t = process.hrtime.bigint(); is();
                       return Number(process.hrtime.bigint() - t) / 1e6; };

const olc = (ad, oncedenDizin) => {
  fs.rmSync("dizinli.db", { force: true });
  fs.rmSync("dizinli.db-journal", { force: true });
  const db = new DatabaseSync("dizinli.db");
  db.exec(SEMA);
  if (oncedenDizin) for (const d of DIZIN) db.exec(d);
  const ekle = db.prepare(
    "INSERT INTO odunc(kitap_id,uye_id,sube_id,alis) VALUES(?,?,?,?)");
  const yukleme = sure(() => {
    db.exec("BEGIN");
    for (let i = 0; i < N; i++) ekle.run(i % 400 + 1, i % 900 + 1, i % 3 + 1, "2025-06-01");
    db.exec("COMMIT");
  });
  const dizinKurma = oncedenDizin ? 0 : sure(() => { for (const d of DIZIN) db.exec(d); });
  db.close();
  console.log(ad.padEnd(24) + " | " + (yukleme.toFixed(0) + " ms").padStart(9) + " | " +
    (dizinKurma ? dizinKurma.toFixed(0) + " ms" : "-").padStart(9) + " | " +
    ((yukleme + dizinKurma).toFixed(0) + " ms").padStart(9) + " | " +
    (Math.round(fs.statSync("dizinli.db").size / 1024) + " KB").padStart(9));
};

console.log(N.toLocaleString("tr-TR") + " satir, uc dizin");
console.log();
console.log("yol                      | yukleme   | dizin     | toplam    | dosya");
console.log("-------------------------|-----------|-----------|-----------|----------");
olc("dizinler onceden var", true);
olc("dizinler sonradan", false);
EOF
```

{/* mask: [ ]*[0-9]+(?= ms) */}
```text
200.000 satir, uc dizin

yol                      | yukleme   | dizin     | toplam    | dosya
-------------------------|-----------|-----------|-----------|----------
dizinler onceden var     |    488 ms |         - |    488 ms |  14128 KB
dizinler sonradan        |     74 ms |     96 ms |    169 ms |  13352 KB
```

Yükleme süresi bu koşumda altı kattan fazla düştü; dizin kurma süresi eklendiğinde bile
toplam yaklaşık üçte bire indi. Dosya da küçüldü: toplu kurulan bir dizin sayfalarını sıkı doldurur, satır satır
büyütülen dizin ise sayfa bölünmeleri nedeniyle boşluk bırakır. Bu boşluk, dizinlerin
bakımı konusunda — bu kursun Dizinler ve Bölümleme konusunda — şişme başlığı altında ele
alınan durumdur.

Yöntemin bedeli, dizinlerin bulunmadığı süre boyunca tablonun okuma başarımının düşmesidir.
İlk yükleme yapılan boş bir tabloda bu bedel yoktur. Var olan ve kullanılan bir tabloya
büyük bir aktarım yapılıyorsa dizinleri düşürmek seçenek değildir; o durumda parti
büyüklüğü ayarlanır ve yükleme yoğun olmayan bir saate alınır.

Aynı gerekçe kısıtlar için de geçerlidir. Yabancı anahtar denetimi her satırda bir arama
yapar; benzersizlik kısıtı her satırda bir denetim gerektirir. Motorların çoğu kısıtların
işlem sonuna ertelenmesine izin verir; ertelenen denetim satır başına değil, bir kerede
yapılır.

#### Hazırlık Tablosu ve Bozuk Satırlar

Dışarıdan gelen veri temiz gelmez. Tek bir bozuk satır, doğrudan hedef tabloya yazan bir
yüklemeyi işlemin ortasında durdurur ve o ana kadar yapılan iş geri alınır. Yirmi bin
satırın bin üç yüzüncüsünde duran bir yükleme, hem zaman kaybettirir hem de sorunun
tamamını göstermez — sıradaki bozuk satırlar henüz görülmemiştir.

Yerleşik çözüm bir **hazırlık tablosudur (staging table)**: gelen dosya, her sütunu metin
olan ve hiçbir kısıt taşımayan bir tabloya olduğu gibi alınır. Doğrulama, veri veritabanına
girdikten sonra sorguyla yapılır. Geçerli satırlar hedefe taşınır, geçersizler nedeniyle
birlikte ayrılır.

```bash
rm -f aktarim.db odunc_gelen.csv temiz.csv

# Disaridan gelen dosya: 20.000 satirlik bir arsiv aktarimi, icinde bozuk satirlar var.
node - <<'EOF' > odunc_gelen.csv
console.log("id,kitap_id,uye_id,sube_id,alis");
for (let i = 1; i <= 20000; i++) {
  if (i === 77) { console.log("yetmisyedi,12,34,1,2025-05-04"); continue; }   // sayi degil
  if (i === 512) { console.log("512,12,34,1,04.05.2025"); continue; }         // tarih bicimi
  if (i === 900) { console.log("900,12,99999,1,2025-05-04"); continue; }      // olmayan uye
  if (i === 1301) { console.log("1301,12,34,1"); continue; }                  // eksik alan
  console.log([i, (i % 400) + 1, (i % 900) + 1, (i % 3) + 1, "2025-05-04"].join(","));
}
EOF

sqlite3 aktarim.db <<'SQL'
CREATE TABLE uye(id INTEGER PRIMARY KEY, ad TEXT NOT NULL);
WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n<900)
INSERT INTO uye(id,ad) SELECT n,'Uye-'||n FROM s;
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT NOT NULL, uye_id INT NOT NULL
                     REFERENCES uye(id), sube_id INT NOT NULL, alis TEXT NOT NULL);
-- Hazirlik tablosu: her sutun metin, hicbir kisit yok.
CREATE TABLE hazirlik(id TEXT, kitap_id TEXT, uye_id TEXT, sube_id TEXT, alis TEXT);
CREATE TABLE hatali(satir TEXT, neden TEXT);
SQL

sqlite3 aktarim.db ".import --csv --skip 1 odunc_gelen.csv hazirlik"
echo "hazirlik tablosuna alinan satir: $(sqlite3 aktarim.db 'SELECT COUNT(*) FROM hazirlik;')"

sqlite3 -box -header aktarim.db <<'SQL'
INSERT INTO hatali(satir, neden)
SELECT id || ',' || IFNULL(kitap_id,'') || ',' || IFNULL(uye_id,'') || ',' ||
       IFNULL(sube_id,'') || ',' || IFNULL(alis,''),
       CASE
         WHEN id GLOB '*[^0-9]*' OR uye_id GLOB '*[^0-9]*' THEN 'sayisal olmayan alan'
         WHEN alis IS NULL OR alis NOT GLOB '[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]'
              THEN 'tarih bicimi'
         WHEN NOT EXISTS (SELECT 1 FROM uye WHERE uye.id = CAST(hazirlik.uye_id AS INTEGER))
              THEN 'olmayan uye'
       END
FROM hazirlik
WHERE id GLOB '*[^0-9]*' OR uye_id GLOB '*[^0-9]*'
   OR alis IS NULL OR alis NOT GLOB '[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]'
   OR NOT EXISTS (SELECT 1 FROM uye WHERE uye.id = CAST(hazirlik.uye_id AS INTEGER));

INSERT INTO odunc(id,kitap_id,uye_id,sube_id,alis)
SELECT CAST(id AS INTEGER), CAST(kitap_id AS INTEGER), CAST(uye_id AS INTEGER),
       CAST(sube_id AS INTEGER), alis
FROM hazirlik
WHERE id NOT GLOB '*[^0-9]*' AND uye_id NOT GLOB '*[^0-9]*'
  AND alis GLOB '[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]'
  AND EXISTS (SELECT 1 FROM uye WHERE uye.id = CAST(hazirlik.uye_id AS INTEGER));

SELECT (SELECT COUNT(*) FROM hazirlik) AS gelen,
       (SELECT COUNT(*) FROM odunc) AS alinan,
       (SELECT COUNT(*) FROM hatali) AS ayrilan;
SELECT neden, COUNT(*) AS satir FROM hatali GROUP BY neden ORDER BY satir DESC;
SQL

# Disari aktarim: ayni bicimde bir dosya uretilir.
sqlite3 aktarim.db <<'SQL'
.mode csv
.headers on
.output temiz.csv
SELECT id, kitap_id, uye_id, sube_id, alis FROM odunc WHERE sube_id=1;
SQL
echo "disari yazilan satir: $(($(wc -l < temiz.csv) - 1))"
head -2 temiz.csv
```

```text
odunc_gelen.csv:1302: expected 5 columns but found 4 - filling the rest with NULL
hazirlik tablosuna alinan satir: 20000
┌───────┬────────┬─────────┐
│ gelen │ alinan │ ayrilan │
├───────┼────────┼─────────┤
│ 20000 │ 19996  │ 4       │
└───────┴────────┴─────────┘
┌──────────────────────┬───────┐
│        neden         │ satir │
├──────────────────────┼───────┤
│ tarih bicimi         │ 2     │
│ sayisal olmayan alan │ 1     │
│ olmayan uye          │ 1     │
└──────────────────────┴───────┘
disari yazilan satir: 6665
id,kitap_id,uye_id,sube_id,alis
3,4,4,1,2025-05-04
```

Yükleme durmadı. Yirmi bin satırın tamamı veritabanına girdi, on dokuz bin dokuz yüz doksan
altısı hedefe taşındı ve dört satır nedeniyle birlikte ayrıldı. Ayrılan satırların
listesi düzeltilip yeniden yüklenebilir; bu, tek bir hatalı satır yüzünden bütün aktarımın
tekrarlanmasından çok daha ucuzdur.

Çıktının ilk satırı ayrıca öğreticidir. Eksik alanlı satır, içe aktarım aracı tarafından
bir uyarıyla karşılandı ve eksik sütun boş değerle dolduruldu — yani sessizce
reddedilmedi, sessizce **kabul edildi**. Doğrulama katmanı olmasaydı bu satır tarih alanı
boş olarak hedefe girerdi. İçe aktarma araçlarının hatalı biçimi nasıl karşıladığı motora
ve araca göre değişir; hangi davranışın geçerli olduğu varsayılmaz, sınanır.

Dışa aktarım aynı sözleşmenin ters yönüdür: sütun sırası, ayırıcı, başlık satırı ve tarih
biçimi karşı taraf için belirlenir. Aktarımların iki yönü de aynı biçim tanımına bağlıysa,
dışarı verilen dosya kendi hazırlık tablosuna geri yüklenerek sınanabilir.

#### Yükleme Sırasındaki Diğer Kararlar

Üç ayar daha yükleme süresini belirler ve üçü de aynı ödünleşimi taşır: dayanıklılığın
geçici olarak azaltılması.

**Günlük ayarı.** Motorların bir bölümü, toplu yükleme sırasında günlüğe daha az yazan bir
kip sunar. Kazanç büyüktür; karşılığında yükleme sırasında bir çökme olursa tablo
kurtarılamaz duruma gelebilir ve yükleme baştan yapılır. Boş bir tabloya ilk yükleme
yapılırken kabul edilebilir, var olan veriye eklerken değil.

**Eşitleme ayarı.** Kesinleştirmede diske yazmanın tamamlanmasını beklemeyen bir ayar,
küçük partilerde büyük fark yaratır. Yükleme bittikten sonra eski değere döndürülmesi
gerekir; bu adımın unutulması, sistemin sonraki aylarını sessizce risk altında bırakır.

**Kopyalama komutu.** Motorların çoğu, satır satır ekleme yerine akış hâlinde veri alan bir
komut sağlar. Yukarıdaki içe aktarım bunun bir örneğidir. Kazancı, deyim ayrıştırma ve
tekil kayıt işleme yükünün ortadan kalkmasıdır.

Son olarak yüklemenin ardından istatistiklerin yenilenmesi gerekir. Planlayıcı, tabloda kaç
satır olduğunu ve değerlerin nasıl dağıldığını kendi topladığı istatistiklerden bilir;
İleri SQL kursunda ele alınan bu bilgi, toplu yüklemeden sonra gerçekle bağını yitirir.
Yenilenmediğinde planlayıcı boş sandığı bir tabloyu tarar ve yükleme sonrası sorgular
beklenmedik biçimde yavaşlar.

#### Özet

- Kesinleştirme sayısı toplu yüklemenin birinci maliyetidir; satır başına bir işlem ile
  partili yükleme arasında örnek koşumda kırk katı aşan fark çıktı.
- Kazancın büyük bölümü ilk birkaç yüz satırlık partide elde edilir; çok büyük partiler
  yeniden başlama maliyetini ve geri alma bilgisini büyütür.
- Dizinler yükleme sırasında satır başına bakım gerektirir; sonradan toplu kurulduğunda
  hem süre düşer hem de dizin daha az yer kaplar.
- Hazırlık tablosu, gelen veriyi kısıtsız biçimde alıp doğrulamayı sorguya taşır; bozuk
  satırlar yüklemeyi durdurmadan nedeniyle birlikte ayrılır.
- İçe aktarma araçları hatalı biçimli satırları sessizce kabul edebilir; davranış
  varsayılmaz, sınanır.
- Yükleme sonrası istatistiklerin yenilenmesi gerekir; yenilenmediğinde planlayıcı eski
  bilgiyle çalışır.

#### Sonraki Adım

Bu konudaki dersler bir veritabanının yaşamını sürdürmesi için gereken her işi kurdu:
yetkilendirme, yedekleme, kurtarma, çoğaltma, erişilebilirlik, bağlantı yönetimi, izleme ve
toplu aktarım. Geriye bir tanesi kaldı ve hepsini birden ilgilendirir. Motorun sürümü
zamanla değişir; güvenlik düzeltmeleri, hata gidermeleri ve desteğin sona ermesi yükseltmeyi
zorunlu kılar. Yükseltme, çalışan bir sistemin altındaki katmanı değiştirmektir: kesinti
penceresi, uyumluluk denetimi, geri dönüş planı ve uygulamanın iki sürümle birden
çalışabilmesi gerekir. Son ders bunu ele alıyor.

### Sürüm Yükseltme

Source: https://academia.sh/tr/kurslar/veritabani-yonetimi/surum-yukseltme

Çalışan bir sistemin altındaki katmanın değiştirilmesi: yükseltme yollarının kesinti penceresi, şemanın katalogdan çıkarılıp karşılaştırılmasıyla uyumluluk denetimi, genişlet–daralt göçünün her adımında hangi uygulama sürümünün çalıştığı ve geri dönüş planı.

Bu konudaki dersler bir veritabanının yaşamını sürdürmesi için gereken işleri kurdu:
yetkilendirme, yedekleme, kurtarma, çoğaltma, erişilebilirlik, bağlantı yönetimi, izleme ve
toplu aktarım. Geriye bir tanesi kaldı ve hepsini birden ilgilendirir.

Motorun sürümü zamanla değişir. Güvenlik düzeltmeleri, giderilen hatalar ve desteğin sona
ermesi yükseltmeyi eninde sonunda zorunlu kılar. Yükseltme, çalışan bir sistemin altındaki
katmanı değiştirmektir; üstteki uygulama çalışmaya devam ederken yapılır ve yanlış giderse
geri dönülebilmesi gerekir.

#### Yükseltme Yolları ve Kesinti Penceresi

Üç yol vardır ve aralarındaki fark kesintinin ne kadar sürdüğüdür.

**Yerinde yükseltme (in-place upgrade)**, motor yazılımının değiştirilip aynı veri
dosyalarıyla başlatılmasıdır. Kesinti, sunucunun durup yeniden başlaması kadardır. Koşulu,
yeni sürümün eski dosya biçimini okuyabilmesidir; okuyamıyorsa dosyaların dönüştürülmesi
gerekir ve kesinti veri büyüklüğüne bağlanır.

**Dök ve yükle**, verinin mantıksal yedek olarak dışarı alınıp yeni sürüme yüklenmesidir.
Her sürüm arasında çalışır, çünkü taşınan şey dosya değil deyimlerdir. Kesintisi,
yedeklemenin ve geri yüklemenin toplam süresidir.

**Mantıksal çoğaltmayla geçiş**, yeni sürümde kurulan bir veritabanının eskisine abone
edilmesidir. Veri akarken eski sürüm çalışmayı sürdürür; kesinti yalnız uygulamaların
yönlendirilmesi kadardır. Bu yol Mantıksal Çoğaltma dersinde kurulmuştu.

Aşağıdaki blok ikinci yolun kesintisini ölçüyor: aynı veritabanı iki büyüklükte dökülüp
geri yükleniyor. Süreler makineye ve diske bağlıdır; anlamlı olan iki satır arasındaki
orandır.

```bash
rm -f eski.db yeni.db dokum.sql

node - <<'EOF'
const { DatabaseSync } = require("node:sqlite");
const { spawnSync } = require("node:child_process");
const fs = require("node:fs");

const kur = (dosya, n) => {
  fs.rmSync(dosya, { force: true });
  const db = new DatabaseSync(dosya);
  db.exec(`CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT NOT NULL,
             uye_id INT NOT NULL, sube_id INT NOT NULL, alis TEXT NOT NULL, iade TEXT)`);
  const e = db.prepare("INSERT INTO odunc(kitap_id,uye_id,sube_id,alis) VALUES(?,?,?,?)");
  db.exec("BEGIN");
  for (let i = 0; i < n; i++) e.run(i % 400 + 1, i % 900 + 1, i % 3 + 1, "2025-06-01");
  db.exec("COMMIT");
  db.exec("CREATE INDEX odunc_uye ON odunc(uye_id)");
  db.exec("CREATE INDEX odunc_sube_alis ON odunc(sube_id, alis)");
  db.close();
};
const sure = (is) => { const t = process.hrtime.bigint(); is();
                       return Number(process.hrtime.bigint() - t) / 1e6; };

console.log("satir     | dosya    | dokum   | geri yukleme | kesinti");
console.log("----------|----------|---------|--------------|--------");
for (const n of [100000, 400000]) {
  kur("eski.db", n);
  const boyut = fs.statSync("eski.db").size;
  const dokum = sure(() => {
    const c = spawnSync("sqlite3", ["eski.db", ".dump"], { maxBuffer: 1 << 30 });
    fs.writeFileSync("dokum.sql", c.stdout);
  });
  fs.rmSync("yeni.db", { force: true });
  const yukleme = sure(() => {
    spawnSync("sqlite3", ["yeni.db"], { input: fs.readFileSync("dokum.sql"),
                                        maxBuffer: 1 << 30 });
  });
  const dogru = new DatabaseSync("yeni.db").prepare("SELECT COUNT(*) c FROM odunc").get().c;
  console.log(n.toLocaleString("tr-TR").padStart(9) + " | " +
    (Math.round(boyut / 1024) + " KB").padStart(8) + " | " +
    (dokum.toFixed(0) + " ms").padStart(7) + " | " +
    (yukleme.toFixed(0) + " ms").padStart(12) + " | " +
    ((dokum + yukleme).toFixed(0) + " ms").padStart(7) + "   (" + dogru + " satir)");
}
EOF
```

{/* mask: [ ]*[0-9]+(?= ms) */}
```text
satir     | dosya    | dokum   | geri yukleme | kesinti
----------|----------|---------|--------------|--------
  100.000 |  5712 KB |  303 ms |       218 ms |  521 ms   (100000 satir)
  400.000 | 23040 KB | 2144 ms |      1093 ms | 3237 ms   (400000 satir)
```

Dört kat veri, birkaç kat süre. Dök ve yükle yolunun kesintisi veri büyüklüğüyle
büyür ve bu, planlamanın dayanağıdır (süreler makineye göre değişir): küçük bir veritabanında ölçülen süre,
üretim büyüklüğüne oranlanarak kesinti penceresi tahmin edilir. Oranın büyük veride de
doğrusal kalacağı bir varsayımdır; gerçek pencere, üretim büyüklüğündeki bir kopyayla
sınanarak bulunur.

Geri yükleme, dökümle aynı büyüklük sırasında bir süre tutuyor. Nedeni, önceki derste
ölçülen şeydir: döküm yalnız okur, geri yükleme ise satırları yeniden yazar ve dizinleri
yeniden kurar. Kesinti penceresi hesaplanırken iki tarafın toplamına bakılır.

#### Uyumluluk Denetimi

Yükseltmenin kırdığı şey genellikle motor değil, uygulamayla veritabanı arasındaki
sözleşmedir. Yeni sürümle birlikte gelen şema, uygulamanın beklediği şemadan farklıysa
uygulama çalışmaz. Bu farkın yükseltmeden **önce**, katalogdan okunarak bulunması gerekir.

Sistem kataloğu bu kursun Motor Mimarisi konusunda ele alınmıştı. Aşağıdaki blok onu bir
denetim aracı olarak kullanıyor: üretimdeki şema ile adaydaki şema aynı sorguyla
karşılaştırılıyor.

```bash
rm -f uretim.db aday.db

# Uretimdeki sema.
sqlite3 uretim.db <<'SQL'
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT NOT NULL, uye_id INT NOT NULL,
                   sube_id INT, alis TEXT NOT NULL, iade TEXT);
CREATE INDEX odunc_uye ON odunc(uye_id);
CREATE INDEX odunc_alis ON odunc(alis);
SQL

# Yeni surumle birlikte gelen aday sema.
sqlite3 aday.db <<'SQL'
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT NOT NULL, uye_id INT NOT NULL,
                   sube_id INT NOT NULL, alis_tarihi TEXT NOT NULL, iade TEXT,
                   gecikme_gunu INT DEFAULT 0);
CREATE INDEX odunc_uye ON odunc(uye_id);
CREATE INDEX odunc_sube_alis ON odunc(sube_id, alis_tarihi);
SQL

sqlite3 -box -header uretim.db <<'SQL'
ATTACH 'aday.db' AS aday;

WITH u AS MATERIALIZED (SELECT cid, name, type, "notnull" FROM pragma_table_info('odunc','main')),
     a AS MATERIALIZED (SELECT cid, name, type, "notnull" FROM pragma_table_info('odunc','aday')),
     fark AS (
  SELECT u.name AS alan, u.cid AS sira,
         u.type || CASE WHEN u."notnull" THEN ' NOT NULL' ELSE '' END AS uretim,
         IFNULL(a.type || CASE WHEN a."notnull" THEN ' NOT NULL' ELSE '' END, '-') AS aday,
         CASE WHEN a.name IS NULL THEN 'kaldirildi'
              WHEN u.type <> a.type OR u."notnull" <> a."notnull" THEN 'degisti'
              ELSE 'ayni' END AS durum
  FROM u LEFT JOIN a ON a.name = u.name
  UNION ALL
  SELECT a.name, 100 + a.cid, '-',
         a.type || CASE WHEN a."notnull" THEN ' NOT NULL' ELSE '' END, 'eklendi'
  FROM a WHERE a.name NOT IN (SELECT name FROM u))
SELECT alan, uretim, aday, durum FROM fark ORDER BY sira;

SELECT IFNULL(u.name, a.name) AS dizin,
       CASE WHEN u.name IS NULL THEN 'eklendi'
            WHEN a.name IS NULL THEN 'kaldirildi' ELSE 'ayni' END AS durum
FROM (SELECT name FROM main.sqlite_schema WHERE type='index' AND sql IS NOT NULL) u
FULL OUTER JOIN
     (SELECT name FROM aday.sqlite_schema WHERE type='index' AND sql IS NOT NULL) a
USING(name) ORDER BY 1;
SQL
```

```text
┌──────────────┬───────────────┬───────────────┬────────────┐
│     alan     │    uretim     │     aday      │   durum    │
├──────────────┼───────────────┼───────────────┼────────────┤
│ id           │ INTEGER       │ INTEGER       │ ayni       │
│ kitap_id     │ INT NOT NULL  │ INT NOT NULL  │ ayni       │
│ uye_id       │ INT NOT NULL  │ INT NOT NULL  │ ayni       │
│ sube_id      │ INT           │ INT NOT NULL  │ degisti    │
│ alis         │ TEXT NOT NULL │ -             │ kaldirildi │
│ iade         │ TEXT          │ TEXT          │ ayni       │
│ alis_tarihi  │ -             │ TEXT NOT NULL │ eklendi    │
│ gecikme_gunu │ -             │ INT           │ eklendi    │
└──────────────┴───────────────┴───────────────┴────────────┘
┌─────────────────┬────────────┐
│      dizin      │   durum    │
├─────────────────┼────────────┤
│ odunc_alis      │ kaldirildi │
│ odunc_sube_alis │ eklendi    │
│ odunc_uye       │ ayni       │
└─────────────────┴────────────┘
```

Farklar üç sınıfa ayrılır ve üçü ayrı risk taşır.

**Eklenenler zararsızdır.** Varsayılan değeri olan yeni bir sütun, eski uygulamayı
etkilemez; eski sürüm o sütunu hiç bilmez.

**Kaldırılanlar kırıcıdır.** Kaldırılan sütun, o sütunu okuyan ya da yazan her deyimi
bozar. Kaldırılan dizin ise sözdizimsel bir hata üretmez; bunun yerine o dizine dayanan
sorgular sessizce yavaşlar. İkinci durum daha tehlikelidir, çünkü yükseltme "başarılı"
görünür ve sorun ancak yük geldiğinde ortaya çıkar.

**Değişenler koşulludur.** Zorunlu olmayan bir sütunun zorunlu hâle gelmesi, o sütunu boş
bırakan her ekleme deyimini bozar. Tip daralması aynı biçimde davranır.

Bu denetimin çıktısı bir listedir; kararı veren şey listenin uygulama koduyla
karşılaştırılmasıdır. Kaldırılan sütuna hiçbir sorgu dokunmuyorsa risk yoktur;
dokunuyorsa yükseltme, uygulamanın o sorgusu değişmeden yapılamaz.

#### Genişlet ve Daralt

Şema değişikliğini yükseltmeyle aynı ana sıkıştırmak, kaçınılmaz olarak kesinti üretir:
uygulamanın eski sürümü ile yeni şema bir arada çalışmaz. **Genişlet–daralt göçü
(expand–contract migration)** bu anı ikiye böler ve arada iki tarafın da çalıştığı bir
pencere açar.

Aşağıdaki blok bunu bir sütunun yeniden adlandırılması üzerinde gösteriyor. Üç uygulama
sürümü tanımlı: eski sürüm eski sütunu yazıp okur, geçiş sürümü iki sütuna da yazıp yeniyi
okur, son sürüm yalnız yeni sütunu kullanır. Her aşamada üçü de denenip hangisinin
çalıştığı yazılıyor.

```bash
rm -f goc.db

node - <<'EOF'
const { DatabaseSync } = require("node:sqlite");
const db = new DatabaseSync("goc.db");
db.exec(`CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT NOT NULL,
           uye_id INT NOT NULL, alis TEXT NOT NULL)`);
db.exec("INSERT INTO odunc(kitap_id,uye_id,alis) VALUES(1,1,'2025-06-01')");

let sira = 100;
// Uc uygulama surumu: her biri bir yazma ve bir okuma yapar.
const eskiSurum = () => {
  db.prepare("INSERT INTO odunc(id,kitap_id,uye_id,alis) VALUES(?,2,2,'2025-06-02')").run(sira++);
  db.prepare("SELECT alis FROM odunc WHERE id=1").get().alis;
};
const gecisSurumu = () => {   // cift yazma: iki sutuna da yazar, yeniden okur
  db.prepare("INSERT INTO odunc(id,kitap_id,uye_id,alis,alis_tarihi)" +
             " VALUES(?,3,3,'2025-06-03','2025-06-03')").run(sira++);
  db.prepare("SELECT alis_tarihi FROM odunc WHERE id=1").get().alis_tarihi;
};
const sonSurum = () => {
  db.prepare("INSERT INTO odunc(id,kitap_id,uye_id,alis_tarihi)" +
             " VALUES(?,4,4,'2025-06-04')").run(sira++);
  db.prepare("SELECT alis_tarihi FROM odunc WHERE id=1").get().alis_tarihi;
};
const dene = (is) => { try { is(); return "calisir"; } catch { return "HATA"; } };
const durum = (asama) => console.log(asama.padEnd(33) + " | " +
  dene(eskiSurum).padEnd(10) + " | " + dene(gecisSurumu).padEnd(12) + " | " + dene(sonSurum));

console.log("asama                             | eski surum | gecis surumu | son surum");
console.log("----------------------------------|------------|--------------|----------");
durum("0. baslangic");

// Dogrudan yol: sutun yeniden adlandirilir.
db.exec("ALTER TABLE odunc RENAME COLUMN alis TO alis_tarihi");
durum("dogrudan yeniden adlandirma");

// Basa donulur ve genislet-daralt yolu izlenir.
db.exec("ALTER TABLE odunc RENAME COLUMN alis_tarihi TO alis");
db.exec("ALTER TABLE odunc ADD COLUMN alis_tarihi TEXT");
db.exec("UPDATE odunc SET alis_tarihi = alis WHERE alis_tarihi IS NULL");
db.exec(`CREATE TRIGGER goc_esitle AFTER INSERT ON odunc WHEN NEW.alis_tarihi IS NULL
         BEGIN UPDATE odunc SET alis_tarihi = NEW.alis WHERE id = NEW.id; END`);
durum("1. genislet (sutun + esitleme)");

// Butun ornekler gecis surumune gectikten sonra eski sutun kaldirilir.
db.exec("DROP TRIGGER goc_esitle");
db.exec("ALTER TABLE odunc DROP COLUMN alis");
durum("2. daralt (eski sutun kaldirildi)");

console.log();
console.log("eski surumun yazdigi satirlarda bos kalan yeni sutun: " +
  db.prepare("SELECT COUNT(*) c FROM odunc WHERE alis_tarihi IS NULL").get().c);
EOF
```

```text
asama                             | eski surum | gecis surumu | son surum
----------------------------------|------------|--------------|----------
0. baslangic                      | calisir    | HATA         | HATA
dogrudan yeniden adlandirma       | HATA       | HATA         | calisir
1. genislet (sutun + esitleme)    | calisir    | calisir      | HATA
2. daralt (eski sutun kaldirildi) | HATA       | HATA         | calisir

eski surumun yazdigi satirlarda bos kalan yeni sutun: 0
```

Tabloda aranacak şey, her satırda kaç sürümün çalıştığıdır.

Doğrudan yeniden adlandırma satırında yalnız son sürüm çalışıyor. Şema değişikliği ile
uygulamanın yeni sürümünün yayılması arasında geçen her saniye kesintidir; üstelik geri
dönüş de yoktur, çünkü eski sürüme dönmek şemanın da geri alınmasını gerektirir.

Genişlet satırında iki sürüm birden çalışıyor. Bu, yayılmanın kademeli yapılabileceği
anlamına gelir: örnekler teker teker geçiş sürümüne alınır, bir sorun görülürse eski sürüme
dönülür. Şema değişikliği geri alınmaz, çünkü eklenen sütun eski sürümü rahatsız etmez.

Daralt satırı, bütün örnekler geçiş sürümüne alındıktan **sonra** uygulanır. Bu adımdan
sonra geri dönüş, kaldırılan sütunu geri getirmeyi gerektirir; bu yüzden daraltma
yükseltmenin en son ve en dikkatli adımıdır. Aradaki bekleme süresi, geri dönüş
olasılığının kabul edilebilir biçimde azaldığı süredir.

Son satır eşitlemenin işlediğini gösteriyor: eski sürümün yalnız eski sütuna yazdığı
satırlarda yeni sütun boş kalmadı. Bu eşitleme olmadan, geçiş penceresinde eski sürümün
yazdığı her satır yeni sütunda eksik kalır ve daraltma anında sessizce veri kaybedilir.

#### Yükseltmenin Çevresindeki İşler

Yükseltme kararı, bu konudaki bütün derslerin toplandığı yerdir.

**Sınama.** Yükseltme önce üretim kopyası üzerinde yapılır. Kopyanın kaynağı, doğrulanmış
bir yedektir; yedek doğrulama tatbikatı bu yüzden yükseltmenin ön koşuludur. Sınamada
ölçülecek şey yalnız "çalışıyor mu" değil, sorgu planlarının değişip değişmediğidir.
Planlayıcı sürümle birlikte değişir; iyi çalışan bir sorgu yeni sürümde farklı bir plan
seçebilir. Bu risk, üretim iş yükünün kopya üzerinde tekrar oynatılmasıyla ölçülür.

**Geri dönüş planı.** Yükseltmenin bir geri dönüşü olmalıdır ve bu, "yedekten döneriz"
cümlesinden ibaret olamaz — yedekten dönmek, yükseltmeden sonra yapılan işi silmektir.
Yerinde yükseltmede geri dönüş, yükseltme öncesi alınmış bir dosya kopyasıdır; mantıksal
çoğaltmayla geçişte ise ters yönde bir çoğaltma kurularak eski sürümün güncel tutulmasıdır.

**Sıra.** Çoğaltmalı bir kurulumda yükseltme sırası önemlidir. Yedek sunucular önce
yükseltilir, birincil sonra; böylece devralma gerekirse hedef hazırdır. Fiziksel çoğaltma
iki tarafın aynı sürümü kullanmasını gerektirdiği için bu sıra ancak mantıksal çoğaltmayla
ya da kısa bir kesintiyle uygulanabilir.

**Yükseltme sonrası.** İstatistikler yenilenir, izleme değerleri yükseltme öncesiyle
karşılaştırılır ve ilk yoğun saat gözlenir. Yükseltmenin başarılı sayılması, sürümün
değişmesi değil, izlenen büyüklüklerin taban çizgisine dönmesidir.

#### Özet

- Yükseltme yolları kesinti penceresine göre ayrışır: yerinde yükseltme yeniden başlama
  kadar, dök ve yükle veri büyüklüğüyle orantılı, mantıksal çoğaltmayla geçiş yalnız
  yönlendirme kadar sürer.
- Dök ve yükle yolunda geri yükleme dökümden belirgin biçimde uzun sürer; kesinti
  hesaplanırken bakılacak sayı odur.
- Uyumluluk denetimi katalogdan yapılır: eklenen sütunlar zararsız, kaldırılanlar kırıcı,
  zorunluluğu değişenler koşulludur; kaldırılan dizin hata değil sessiz yavaşlama üretir.
- Genişlet–daralt göçü, şema değişikliğini iki adıma bölerek eski ve yeni uygulama
  sürümünün birlikte çalıştığı bir pencere açar; kademeli yayılım ve geri dönüş bu
  pencerede olanaklıdır.
- Daraltma adımı, bütün örnekler yeni sürüme geçtikten sonra uygulanır ve geri dönüşü en
  pahalı adımdır.
- Sürümle birlikte sorgu planları da değişebilir; yükseltme, üretim iş yükünün bir kopya
  üzerinde tekrar oynatılmasıyla sınanır.

#### Kurs Kapanışı

Bu kurs bir veritabanı motoruna üç ayrı yerden baktı.

**Motor Mimarisi** konusu, verinin nerede durduğunu ve kalıcılığın nasıl sağlandığını
kurdu: süreç ve bellek düzeni, sayfa yapısı, yazma öncesi günlük, denetim noktaları, çok
sürümlü eşzamanlılık denetimi, ölü satır temizliği ve sistem kataloğu. Bu konudaki tek
kavram — günlüğün her değişikliği sırayla taşıması — sonraki iki konunun yarısını taşıdı:
zaman noktasına kurtarma da, fiziksel çoğaltma da o günlüğün başka amaçlarla kullanılmasıdır.

**Dizinler ve Bölümleme** konusu, verinin nasıl bulunduğunu kurdu: dizin türleri, bileşik
ve kısmi dizinler, kapsayan dizinler, dizin bakımı, tablo bölümleme ve parçalama kalıpları.
Buradaki ödünleşim işletimde tekrar tekrar karşımıza çıktı — her dizin okumayı hızlandırır
ve yazmayı yavaşlatır; toplu yükleme ölçümü bunun en açık kanıtıydı.

**İşletim** konusu, çalışan bir sistemin zaman içinde güvenli, kurtarılabilir ve erişilebilir
kalmasını kurdu: roller ve satır düzeyi güvenlik, yedekleme türleri, zaman noktasına
kurtarma, yedek doğrulama, fiziksel ve mantıksal çoğaltma, yüksek erişilebilirlik, bağlantı
havuzlayıcıları, izleme, toplu yükleme ve sürüm yükseltme.

Bu on iki dersin ortak bir savı vardı: **işletimde güvence, ölçülmemiş hiçbir şeyden
gelmez.** Doğrulanmamış yedek yedek değildir, izlenmeyen çoğaltma gecikmesi kurtarma
noktası hedefi değildir, ölçülmemiş havuz boyutu bir varsayımdır ve sınanmamış bir
yükseltme bir umuttur. Kütüphane ödünç kayıtları örneği boyunca her bölümde aynı soru
soruldu: bunu nereden biliyoruz? Yanıt her seferinde bir ölçüm ya da bir tatbikat oldu.

Kurs boyunca ilişkisel modelin kendisi hiç sorgulanmadı: tablolar, satırlar, şema, işlemler
ve SQL verili kabul edildi. Bu modelin karşılayamadığı gereksinimler de vardır. Şeması
sürekli değişen belgeler, tek bir anahtarla erişilen devasa anahtar–değer kümeleri,
ilişkilerin kendisinin sorgu konusu olduğu çizgeler ve saniyede milyonlarca ölçüm yazan
zaman serileri, ilişkisel modelin güçlü yanlarını kullanmadan bedelini ödetir.

Bir sonraki kurs — **İlişkisel Olmayan Veri Modelleri** — bu ailelere bakıyor: anahtar–değer
depoları, belge veritabanları, geniş sütun depoları, çizge ve zaman serisi sistemleri;
belge modelinde gömme ile referans arasındaki karar; ve dağıtık davranışın kaçınılmaz
soruları — çoğaltma kümeleri, parçalama, okuma ve yazma ilgileri, tutarlılık modelleri.
Bu kursta öğrenilen çoğaltma, devralma, çoğunluk ve tutarlılık kavramları orada aynı
adlarla geri gelecek; değişen şey, hangi güvenceden hangi başarım için vazgeçildiğidir.

