İçeriğe geç
academia.sh

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.
  • OFFSET atlanan 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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat