İçeriğe geç
academia.sh

Ders 13 / 34

Sayfalama

Koleksiyonun parça parça sunulması: atlama tabanlı sayfalamada araya kayıt girdiğinde oluşan tekrar ve atlama, imleç tabanlı sayfalamanın aynı senaryoda kaymaması, derin sayfalamanın sorgu planı ve süre üzerindeki maliyeti.

İçindekiler

Bir önceki ders koleksiyon yanıtlarının zarfla sarılmasını kararlaştırdı ve zarfın sayfalama alanını boş bıraktı. Bu ders o alanı doldurur.

Sorun sayılarla başlar. Ödünç koleksiyonu yirmi kayıtken tümünü tek yanıtta göndermek sorun değildir; iki yüz bin kayıtta aynı yanıt sunucunun belleğini, ağı ve istemcinin çözümleyicisini birlikte zorlar. Koleksiyon parça parça verilmelidir. Parçanın nerede başlayacağını söylemenin iki yolu vardır ve aralarındaki fark, koleksiyon istekler arasında değiştiğinde ortaya çıkar.

İki Yöntem

Atlama tabanlı sayfalamada istemci kaçıncı sayfayı istediğini söyler; sunucu sıralı sonucun başından o kadar satır atlar ve sonrakini döndürür. SQL Temelleri kursundaki LIMIT ve OFFSET yan tümceleri doğrudan bunu karşılar. İstemci ?sayfa=3 yazar, sunucu OFFSET 10 uygular.

İmleç tabanlı sayfalamada istemci en son gördüğü kaydın yerini bildirir; sunucu ondan sonrasını döndürür. Sayfa numarası yoktur; her yanıt bir sonraki isteğin başlangıç noktasını taşır. Ölçüt sıralama anahtarıdır, dolayısıyla yöntem yalnızca belirlenimci sıralama varken çalışır: sıralama anahtarı çift değer üretebiliyorsa yanına benzersiz bir alan eklenir.

Aşağıdaki sunucu ikisini de sunar; aynı tablo, aynı sıralama, iki adres.

// sayfa-sunucusu.mjs — ayni koleksiyonu iki sayfalama yontemiyle sunar
import { createServer } from "node:http";
import { DatabaseSync } from "node:sqlite";

const db = new DatabaseSync("kutuphane.db");
const yanitla = (yanit, kod, nesne) => {
  yanit.writeHead(kod, { "content-type": "application/json; charset=utf-8" });
  yanit.end(JSON.stringify(nesne));
};

const sunucu = createServer((istek, yanit) => {
  const adres = new URL(istek.url, "http://127.0.0.1");
  const s = adres.searchParams;
  const boyut = Math.min(Number(s.get("boyut") ?? 5), 50);

  if (adres.pathname === "/atlama/oduncler") {
    const sayfa = Number(s.get("sayfa") ?? 1);
    const satirlar = db.prepare(
      "SELECT id, verilis FROM odunc ORDER BY id DESC LIMIT ? OFFSET ?"
    ).all(boyut, (sayfa - 1) * boyut);
    return yanitla(yanit, 200, {
      veri: satirlar.map((r) => r.id),
      sayfalama: { sayfa, boyut },
    });
  }

  if (adres.pathname === "/imlec/oduncler") {
    // Imlec yoksa en bastan baslanir; varsa o kimlikten kucukler istenir.
    const imlec = s.get("imlec");
    const satirlar = imlec === null
      ? db.prepare("SELECT id FROM odunc ORDER BY id DESC LIMIT ?").all(boyut)
      : db.prepare("SELECT id FROM odunc WHERE id < ? ORDER BY id DESC LIMIT ?")
          .all(Number(imlec), boyut);
    const son = satirlar.at(-1);
    return yanitla(yanit, 200, {
      veri: satirlar.map((r) => r.id),
      sayfalama: { sonrakiImlec: son ? String(son.id) : null },
    });
  }

  yanitla(yanit, 404, { hata: "yol_yok" });
});

sunucu.listen(8480, "127.0.0.1", () => console.log("sayfa sunucusu 127.0.0.1:8480"));

Sayfa boyutu istemciden alınırken üst sınırla kesiliyor. Bu, sözleşmenin ayrılmaz parçasıdır: sınırsız bir boyut parametresi, tek bir istekle tüm koleksiyonu çekmeye açık kapı bırakır.

Kayma Ölçümü

Ölçüm şu senaryoyu kurar: istemci ilk sayfayı alır, bu arada koleksiyon değişir, sonra ikinci sayfayı ister. Değişimin iki türü ayrı ayrı denenir — araya kayıt eklenmesi ve kayıt silinmesi. Her turda veri baştan tohumlanır, böylece dört senaryo aynı başlangıçtan çalışır.

# Yirmi kayitlik koleksiyon; ilk sayfa alinir, koleksiyon degisir, ikinci sayfa alinir.
rm -f kutuphane.db && sqlite3 kutuphane.db < sema.sql
tohum() {
  sqlite3 kutuphane.db <<'SQL'
DELETE FROM odunc;
WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n < 20)
INSERT INTO odunc (uye, isbn, verilis, iade)
SELECT 'U-1001', '978-0262033848', date('2026-01-01', '+' || n || ' day'), NULL FROM s;
SQL
}
tohum
node sayfa-sunucusu.mjs & sunucu=$!
sleep 0.4

ekle() { sqlite3 kutuphane.db "INSERT INTO odunc (uye, isbn, verilis, iade)
  VALUES ('U-1002','978-0201896831','2026-02-01',NULL),
         ('U-1002','978-0201896831','2026-02-02',NULL);"; }
sil() { sqlite3 kutuphane.db "DELETE FROM odunc WHERE id IN (20, 19);"; }
al() { curl -sS "http://127.0.0.1:8480$1"; echo; }

for etki in ekle sil; do
  tohum; printf 'atlama / %s  sayfa 1 : ' "$etki"; al "/atlama/oduncler?sayfa=1&boyut=5"
  $etki;  printf 'atlama / %s  sayfa 2 : ' "$etki"; al "/atlama/oduncler?sayfa=2&boyut=5"
  tohum; printf 'imlec  / %s  sayfa 1 : ' "$etki"; al "/imlec/oduncler?boyut=5"
  $etki;  printf 'imlec  / %s  sayfa 2 : ' "$etki"; al "/imlec/oduncler?imlec=16&boyut=5"
done

kill $sunucu
sayfa sunucusu 127.0.0.1:8480
atlama / ekle  sayfa 1 : {"veri":[20,19,18,17,16],"sayfalama":{"sayfa":1,"boyut":5}}
atlama / ekle  sayfa 2 : {"veri":[17,16,15,14,13],"sayfalama":{"sayfa":2,"boyut":5}}
imlec  / ekle  sayfa 1 : {"veri":[20,19,18,17,16],"sayfalama":{"sonrakiImlec":"16"}}
imlec  / ekle  sayfa 2 : {"veri":[15,14,13,12,11],"sayfalama":{"sonrakiImlec":"11"}}
atlama / sil  sayfa 1 : {"veri":[20,19,18,17,16],"sayfalama":{"sayfa":1,"boyut":5}}
atlama / sil  sayfa 2 : {"veri":[13,12,11,10,9],"sayfalama":{"sayfa":2,"boyut":5}}
imlec  / sil  sayfa 1 : {"veri":[20,19,18,17,16],"sayfalama":{"sonrakiImlec":"16"}}
imlec  / sil  sayfa 2 : {"veri":[15,14,13,12,11],"sayfalama":{"sonrakiImlec":"11"}}

Dört senaryonun ikisi kusurlu.

Atlama, ekleme durumunda tekrar üretti. İlk sayfa 20’den 16’ya kadar geldi. Araya iki yeni kayıt girince sıralamanın başı iki basamak kaydı; ikinci sayfa artık 17’den başlıyor. İstemci 17 ve 16 numaralı kayıtları iki kez gördü. Sayfaları biriktirip liste kuran bir ekranda aynı ödünç kaydı iki satır olarak belirir.

Atlama, silme durumunda atlama üretti. İki kayıt silinince sıralama iki basamak yukarı çekildi ve ikinci sayfa 13’ten başladı. 15 ile 14 numaralı kayıtlar hiçbir sayfada görünmedi. Bu, tekrardan daha tehlikelidir: eksik olan şey fark edilmez.

İmleç her iki durumda da aynı sonucu verdi: 15’ten 11’e. Neden açıktır. İmleç sayfanın kaçıncı sırada olduğunu değil, nerede kalındığını taşır. Koleksiyonun başına kayıt eklenmesi ya da başından kayıt silinmesi, “16’dan küçük kimlikler” ölçütünü etkilemez. Yeni eklenen kayıtlar bir sonraki turda başa geldiklerinde görülür; bu, kaymanın değil tazeliğin sonucudur.

Derin Sayfalamanın Maliyeti

Kaymadan bağımsız ikinci bir fark daha vardır ve o da sayfa numarası büyüdükçe belirir. Atlanan satırlar bedavaya atlanmaz: motor onları bulmak zorundadır.

# Iki milyon kayitta derin sayfalama: plan ve sure karsilastirmasi.
rm -f derin.db
sqlite3 derin.db <<'SQL'
CREATE TABLE odunc (id INTEGER PRIMARY KEY, uye TEXT NOT NULL, verilis TEXT NOT NULL);
WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n < 2000000)
INSERT INTO odunc (uye, verilis)
SELECT 'U-' || (1000 + n % 500), date('2020-01-01', '+' || (n % 2000) || ' day') FROM s;
CREATE INDEX odunc_verilis ON odunc (verilis DESC, id DESC);
SQL

sqlite3 derin.db <<'SQL'
.echo on
EXPLAIN QUERY PLAN
SELECT id FROM odunc ORDER BY verilis DESC, id DESC LIMIT 20 OFFSET 1999980;
EXPLAIN QUERY PLAN
SELECT id FROM odunc WHERE (verilis, id) < ('2020-01-02', 100) ORDER BY verilis DESC, id DESC LIMIT 20;
SQL

echo "--- sureler ---"
sqlite3 derin.db <<'SQL'
.timer on
SELECT count(*) FROM (SELECT id FROM odunc ORDER BY verilis DESC, id DESC LIMIT 20 OFFSET 1999980);
SELECT count(*) FROM (SELECT id FROM odunc WHERE (verilis, id) < ('2020-01-02', 100) ORDER BY verilis DESC, id DESC LIMIT 20);
SQL
EXPLAIN QUERY PLAN
SELECT id FROM odunc ORDER BY verilis DESC, id DESC LIMIT 20 OFFSET 1999980;
QUERY PLAN
`--SCAN odunc USING COVERING INDEX odunc_verilis
EXPLAIN QUERY PLAN
SELECT id FROM odunc WHERE (verilis, id) < ('2020-01-02', 100) ORDER BY verilis DESC, id DESC LIMIT 20;
QUERY PLAN
`--SEARCH odunc USING COVERING INDEX odunc_verilis (verilis<?)
--- sureler ---
20
Run Time: real 0.014 user 0.008816 sys 0.004918
20
Run Time: real 0.000 user 0.000066 sys 0.000017

Süre değerleri makineye ve çalıştırmaya göre değişir; sabit olan iki şey plan satırlarıdır. İleri SQL kursunda okunan plan sözlüğüne göre birincisi SCAN, ikincisi SEARCH diyor. Atlama tabanlı sorgu, dizini baştan taramak ve iki milyona yakın girdiyi atmak zorunda; imleç tabanlı sorgu doğrudan aranan noktaya iniyor. İkisi de aynı yirmi satırı döndürür, ikisi de kapsayan dizin kullanır, ama biri sayfa derinliğiyle birlikte pahalanır, diğeri pahalanmaz.

Ölçümde iki büyüklük derecesi fark çıktı. Ölçek arttıkça fark açılır, çünkü atlama maliyeti sayfa numarasıyla doğru orantılıdır: son sayfa, tablonun tamamını okumak demektir.

Hangi Yöntem Nerede

İki yöntemin de yeri vardır ve seçim ölçütü kullanım biçimidir.

Atlama tabanlı sayfalama, kullanıcının sayfa numarasına atlayabildiği yönetim ekranlarına uyar: “toplam 412 kayıt, 21 sayfa” bilgisi verilebilir, yedinci sayfaya doğrudan gidilebilir. Koşulu, veri kümesinin istekler arasında büyük ölçüde durağan olması ve sayfa sayısının makul kalmasıdır. Kayma riski kabul ediliyorsa açıkça kabul edilmelidir.

İmleç tabanlı sayfalama, akış biçiminde ilerleyen listelere uyar: aşağı kaydırdıkça yüklenen kayıt listeleri, dışa aktarım işleri, eşleme işleri. Toplam sayı ve sayfa numarası veremez; buna karşılık kaymaz ve derinlikten etkilenmez.

İmlecin kendisi de bir tasarım kararıdır. Yukarıdaki sunucuda imleç ham bir kimlik olarak gidiyor; bu, sıralama ölçütünün kimlik olduğunu dışarıya söyler. Sıralama başka bir alana göreyse imlecin o alanı ve benzersizleştirici alanı birlikte taşıması gerekir. İmleci kodlanmış tek bir dizgi olarak yollamak, istemcinin onun içeriğine bağımlı hâle gelmesini önler: imleç istemci için anlamsız bir işarettir, yalnızca geri gönderilir. Sıralama ölçütü değiştiğinde eski imleçlerin geçersiz sayılması ve reddedilmesi de sözleşmenin parçasıdır.

Özet

  • Koleksiyonlar parça parça sunulur; parçanın nereden başlayacağı ya sayfa numarasıyla ya da en son görülen kaydın yerini bildiren bir imleçle söylenir.
  • Atlama tabanlı sayfalamada araya kayıt girdiğinde kayıtlar tekrar eder, kayıt silindiğinde atlanır; ölçümde iki kayıt iki kez göründü ve iki kayıt hiç görünmedi.
  • İmleç tabanlı sayfalama aynı iki senaryoda da aynı sonucu verdi, çünkü sıra numarasını değil koleksiyondaki yeri taşır.
  • İmleç yalnızca belirlenimci sıralamayla çalışır; sıralama anahtarı benzersiz değilse yanına benzersiz bir alan eklenir.
  • Derin sayfalamada atlama tabanlı sorgu dizini baştan tarar, imleç tabanlı sorgu aranan noktaya iner; plan satırları SCAN ile SEARCH olarak ayrışır.
  • Sayfa boyutu istemciden alınır ama üst sınırla kesilir; sınırsız boyut tüm koleksiyonu tek istekte çekmeye açık kapı bırakır.

Sonraki Adım

Sayfalama, koleksiyonun ne kadarının verileceğini çözdü; hangi kayıtların verileceğini çözmedi. Kütüphane görevlisi “yalnızca geciken ödünçler”, “Merkez şubesindeki kitaplar”, “yazar adına göre sıralı” gibi isteklerle gelir. Bunların hepsi sorgu bölümünde taşınacak, hepsi SQL’e dönüşecek ve dönüşüm sırasında iki tuzak belirecek: istemciden gelen değerin sorguya nasıl taşındığı ve sıralamanın benzersiz olmadığında sayfalamayı nasıl bozduğu. Sonraki ders sorgu parametrelerinin tasarımını bu iki tuzakla birlikte ele alır.

İ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