Ders 16 / 21
Sayfalama Maliyeti
Derin sayfalamanın iki sorunu: atlama değeri büyüdükçe artan süre, anahtar tabanlı sayfalamanın sabit maliyeti, sonuç kümesi değiştiğinde atlanan kayıtlar ve iki yaklaşımın arayüze getirdiği kısıtlar.
İçindekiler
Önceki dersteki sorguların hepsi sonuç kümesinin tamamını istedi. Uygulamalar çoğu listeyi parça parça gösterir; SQL Temelleri kursunda tanıtılan atlama–sınırlama çifti bunun en bilinen yoludur.
Yazımı kısa, planı sade, ilk sayfalarda hızlıdır. Derin sayfalarda maliyeti atlanan satır sayısıyla birlikte büyür ve bu büyüme sorgu metnine bakılarak görülmez. Bu ders maliyeti ölçer, alternatifi kurar ve iki yaklaşımın arayüze getirdiği farkı ele alır.
Ölçüm Verisi
Ölçüm için önceki dersteki elli bin kayıtlık veri kümesi kullanılır.
// uret.mjs — 50000 odunc kaydi import { DatabaseSync } from "node:sqlite"; import { rmSync } from "node:fs"; rmSync("kutuphane.db", { force: true }); const db = new DatabaseSync("kutuphane.db"); db.exec(` CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL); CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik 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);`); db.exec("BEGIN"); const u = db.prepare("INSERT INTO uye VALUES (?,?)"); for (let i = 1; i <= 60; i++) u.run(i, `Uye${i}`); const k = db.prepare("INSERT INTO kitap VALUES (?,?)"); for (let i = 1; i <= 200; i++) k.run(i, `Kitap ${i}`); const o = db.prepare("INSERT INTO odunc VALUES (?,?,?,?,?)"); let tohum = 20250727; // belirlenimci sozde rastgele dizi const sonraki = (n) => { tohum = (tohum * 48271) % 2147483647; return tohum % n; }; for (let i = 1; i <= 50000; i++) { const yil = 2022 + sonraki(4); const ay = String(sonraki(12) + 1).padStart(2, "0"); const gun = String(sonraki(28) + 1).padStart(2, "0"); o.run(i, sonraki(200) + 1, sonraki(60) + 1, `${yil}-${ay}-${gun}`, sonraki(5) === 0 ? null : `${yil}-12-31`); } db.exec("COMMIT"); db.exec("ANALYZE"); console.log("odunc:", db.prepare("SELECT count(*) AS n FROM odunc").get().n, " acik:", db.prepare("SELECT count(*) AS n FROM odunc WHERE iade_tarihi IS NULL").get().n);
node uret.mjs
odunc: 50000 acik: 10006
Atlamanın Bedeli
İki sorgu aynı sayfayı üretir. Birincisi kaç satır atlanacağını söyler; ikincisi bir önceki sayfanın son anahtarından devam eder.
// sayfalama.mjs — ayni sayfa boyutu, artan atlama degerleri ile anahtar tabanli yaklasim import { DatabaseSync } from "node:sqlite"; const db = new DatabaseSync("kutuphane.db"); const SAYFA = 20; const ATLAMALI = `SELECT odunc_id, alis_tarihi FROM odunc WHERE iade_tarihi IS NOT NULL ORDER BY odunc_id LIMIT ? OFFSET ?`; const ANAHTARLI = `SELECT odunc_id, alis_tarihi FROM odunc WHERE iade_tarihi IS NOT NULL AND odunc_id > ? ORDER BY odunc_id LIMIT ?`; const olc = (calistir, tekrar = 20) => { calistir(); const t = performance.now(); for (let i = 0; i < tekrar; i++) calistir(); return (performance.now() - t) / tekrar; }; console.log("atlama atlamali_ms anahtarli_ms oran"); for (const atlama of [0, 1000, 10000, 30000, 39000]) { // Anahtarli yaklasim icin ayni sayfanin baslangic anahtari bulunur. const sinir = db.prepare( `SELECT odunc_id FROM odunc WHERE iade_tarihi IS NOT NULL ORDER BY odunc_id LIMIT 1 OFFSET ?`).get(Math.max(0, atlama - 1)); const oncekiAnahtar = atlama === 0 ? 0 : sinir.odunc_id; const a = olc(() => db.prepare(ATLAMALI).all(SAYFA, atlama)); const b = olc(() => db.prepare(ANAHTARLI).all(oncekiAnahtar, SAYFA)); console.log(`${String(atlama).padStart(6)} ${a.toFixed(3).padStart(10)} ${b.toFixed(3).padStart(12)} ` + `${(a / b).toFixed(1).padStart(6)}x`); }
node sayfalama.mjs
atlama atlamali_ms anahtarli_ms oran
0 0.012 0.013 1.0x
1000 0.030 0.012 2.4x
10000 0.203 0.013 16.2x
30000 0.616 0.011 57.1x
39000 0.793 0.011 70.7x
İlk sayfada iki yaklaşım eşit. Atlama büyüdükçe atlamalı sorgunun süresi doğrusal artıyor: 10000’de yaklaşık on yedi, 39000’de yetmiş kat. Anahtar tabanlı sorgunun süresi değişmiyor.
Nedeni sorgu planında değil, planın anlamındadır. OFFSET bir “atla” komutu değildir;
veritabanı atlanan satırları üretmek zorundadır, sonra atar. Otuz bininci sayfayı almak
için otuz bin satır okunur ve çöpe atılır. Anahtar tabanlı sorguda ise koşulun kendisi
dizinde bir başlangıç noktası verir; okunan satır sayısı sayfa boyutu kadardır.
Bu, önceki dersin ölçütüne uyuyor: iki sorgu da dizin kullanır, ikisinin planı da “arama” der. Fark, planın kaç satır ürettiğindedir.
Gezinti Toplamı
Tek bir derin sayfa milisaniyeler alıyor; asıl fark bütün liste taranırken görünür. Dışa aktarım ve toplu işler bunu sürekli yapar.
// tam-gezinti.mjs — butun sayfalarin bastan sona gezilmesinin toplam maliyeti import { DatabaseSync } from "node:sqlite"; const db = new DatabaseSync("kutuphane.db"); const SAYFA = 100; function atlamaliGezinti() { const deyim = db.prepare(`SELECT odunc_id FROM odunc WHERE iade_tarihi IS NOT NULL ORDER BY odunc_id LIMIT ? OFFSET ?`); let atlama = 0, toplam = 0, sayfa = 0; for (;;) { const satirlar = deyim.all(SAYFA, atlama); if (satirlar.length === 0) break; toplam += satirlar.length; atlama += SAYFA; sayfa += 1; } return { toplam, sayfa }; } function anahtarliGezinti() { const deyim = db.prepare(`SELECT odunc_id FROM odunc WHERE iade_tarihi IS NOT NULL AND odunc_id > ? ORDER BY odunc_id LIMIT ?`); let son = 0, toplam = 0, sayfa = 0; for (;;) { const satirlar = deyim.all(son, SAYFA); if (satirlar.length === 0) break; son = satirlar[satirlar.length - 1].odunc_id; toplam += satirlar.length; sayfa += 1; } return { toplam, sayfa }; } for (const [ad, gez] of [["atlamali", atlamaliGezinti], ["anahtarli", anahtarliGezinti]]) { const t = performance.now(); const { toplam, sayfa } = gez(); console.log(`${ad.padEnd(10)} sayfa=${sayfa} satir=${toplam} sure=${(performance.now() - t).toFixed(0)} ms`); }
node tam-gezinti.mjs
atlamali sayfa=400 satir=39994 sure=170 ms anahtarli sayfa=400 satir=39994 sure=7 ms
Aynı dört yüz sayfa, aynı otuz dokuz bin satır, yirmi dört kat süre farkı. Atlamalı gezintide okunan toplam satır sayısı sayfa sayısının karesiyle orantılıdır; her sayfa kendinden öncekilerin tamamını yeniden üretir. Anahtar tabanlı gezintide her satır bir kez okunur.
Kayan Sonuç Kümesi
Maliyetin yanında bir doğruluk sorunu vardır. Sayfalar arasında sonuç kümesi değişirse, atlama değeri artık aynı satırı göstermez.
// kayan-sayfa.mjs — sayfalar arasinda kume degisince atlamali sayfalama kayit atlar import { DatabaseSync } from "node:sqlite"; import { rmSync } from "node:fs"; rmSync("kucuk.db", { force: true }); const db = new DatabaseSync("kucuk.db"); db.exec(`CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, iade_tarihi TEXT); INSERT INTO odunc (odunc_id, iade_tarihi) VALUES (1,NULL),(2,NULL),(3,NULL),(4,NULL),(5,NULL), (6,NULL),(7,NULL),(8,NULL),(9,NULL),(10,NULL);`); const atlamali = (atlama) => db.prepare( "SELECT odunc_id FROM odunc WHERE iade_tarihi IS NULL ORDER BY odunc_id LIMIT 5 OFFSET ?") .all(atlama).map((r) => r.odunc_id); const anahtarli = (sonAnahtar) => db.prepare( "SELECT odunc_id FROM odunc WHERE iade_tarihi IS NULL AND odunc_id > ? ORDER BY odunc_id LIMIT 5") .all(sonAnahtar).map((r) => r.odunc_id); const s1 = atlamali(0); db.prepare("UPDATE odunc SET iade_tarihi = '2025-07-20' WHERE odunc_id = 2").run(); // kume kucululdu const s2 = atlamali(5); console.log("atlamali sayfa1:", s1.join(","), " sayfa2:", s2.join(",")); console.log(" goruntulenen:", [...s1, ...s2].join(",")); db.prepare("UPDATE odunc SET iade_tarihi = NULL WHERE odunc_id = 2").run(); // durum geri alinir const a1 = anahtarli(0); db.prepare("UPDATE odunc SET iade_tarihi = '2025-07-20' WHERE odunc_id = 2").run(); const a2 = anahtarli(a1[a1.length - 1]); console.log("anahtarli sayfa1:", a1.join(","), " sayfa2:", a2.join(",")); console.log(" goruntulenen:", [...a1, ...a2].join(","));
node kayan-sayfa.mjs
atlamali sayfa1: 1,2,3,4,5 sayfa2: 7,8,9,10 goruntulenen: 1,2,3,4,5,6 numarali kayit atlandi anahtarli sayfa1: 1,2,3,4,5 sayfa2: 6,7,8,9,10 goruntulenen: 1,2,3,4,5,6,7,8,9,10
İki numaralı kayıt açık listeden çıkınca küme dokuza indi; ikinci sayfanın başlangıcı bir sıra kaydı ve altı numaralı kayıt hiç görünmedi. Anahtar tabanlı yaklaşımda ikinci sayfa “beşten büyük olanlar” diye soruldu; kümenin küçülmesi bu koşulu etkilemedi.
Ters yön de mümkündür: küme büyürse aynı kayıt iki sayfada birden görünür. İkisi de sessiz hatadır; kullanıcı eksik ya da yinelenen bir liste görür, hiçbir uyarı üretilmez. Yalıtım düzeyleri dersindeki gözlem burada tekrarlanıyor: ayrı ayrı doğru olan iki sorgu birlikte tutarsız olabilir.
İki Yaklaşımın Kısıtları
Anahtar tabanlı sayfalama bedelsiz değildir; arayüze iki kısıt getirir.
Sıra atlanamaz. “Yirmi yedinci sayfaya git” isteği karşılanamaz, çünkü o sayfanın başlangıç anahtarı bilinmiyor. Yalnız “sonraki” ve — sıralama tersine çevrilerek — “önceki” verilebilir. API Tasarımı kursunda tanıtılan imleç tabanlı sayfalama bu kısıta dayanır; istemciye sayfa numarası yerine bir imleç verilir.
Sıralama belirlenimci olmalıdır. Sıralama anahtarı benzersiz değilse eşit değerli satırlar arasındaki sıra tanımsızdır ve sayfa sınırında kayıt atlanır ya da yinelenir. Bu yüzden sıralama anahtarına benzersiz bir sütun eklenir; alış tarihine göre sıralanan bir listede koşul “tarih daha büyük, ya da tarih eşit ve kimlik daha büyük” biçimini alır.
Atlamalı sayfalamanın da yeri vardır. Sayfa sayısı küçük kalan listeler, yönetim ekranları ve toplam sayfa sayısının gösterilmesi gereken arayüzler için yeterlidir. Ölçüt basittir: atlama değerinin üst sınırı biliniyorsa atlamalı sayfalama kullanılabilir; liste sınırsız büyüyebiliyorsa anahtar tabanlı yaklaşım seçilir.
Üçüncü bir ayrıntı toplam sayı ile ilgilidir. Arayüzde “1245 kayıt” yazmak için ayrı bir sayma sorgusu çalışır ve bu sorgu bütün kümeyi üretir; sayfalama ne kadar ucuzlarsa ucuzlasın, sayma sorgusu maliyeti geri getirir. Bu yüzden büyük listelerde kesin toplam yerine “daha fazlası var” bilgisi verilir; bu bilgi, sayfa boyutundan bir fazla satır istenerek elde edilir.
Özet
- Atlamalı sayfalamada süre atlama değeriyle doğrusal arttı: 10000’de 16, 39000’de 70 kat. Anahtar tabanlı sorgunun süresi değişmedi.
OFFSETatlanan satırları üretmemeyi sağlamaz; veritabanı onları okur ve atar.- Dört yüz sayfanın tamamı gezildiğinde atlamalı yaklaşım 170 ms, anahtar tabanlı yaklaşım 7 ms sürdü; atlamalı gezintinin maliyeti sayfa sayısının karesiyle orantılıdır.
- Sayfalar arasında sonuç kümesi küçülünce atlamalı sayfalama bir kaydı atladı; anahtar tabanlı sayfalama etkilenmedi.
- Anahtar tabanlı sayfalama sıra atlamaya izin vermez ve belirlenimci sıralama gerektirir; atlamalı sayfalama, atlama değerinin üst sınırı bilinen listeler için yeterlidir.
Sonraki Adım
Başarım Sorunları konusu veri erişim katmanının ölçülebilir yanını kapattı: sorgu sayısı, aktarılan bayt, deyim sayısı, sorgu planı ve sayfalama maliyeti. Bu ölçütlerin hepsi katmanın nasıl çalıştığıyla ilgiliydi; hiçbiri katmanın ne yapması gerektiğini söylemiyor. Ödünç verme kuralı nerede duruyor, doğrulama hangi katmanda yapılıyor, dışarıya dönen kayıt ile alan modeli aynı şey mi? Sonraki konu bu soruları ele alır ve ilk ders sunum, uygulama, alan ve altyapı katmanlarının sorumluluklarını ayırıp bağımlılık yönünü ithal grafiği üzerinden denetler.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.