---
title: Sayfalama
source: 'https://academia.sh/tr/kurslar/api-tasarimi/sayfalama'
course: 'Web API Tasarımı'
language: tr
updated: '2026-08-17T18:06:45+00:00'
license: 'CC BY-SA 4.0'
---

# 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.

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.

```js
// 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.

```bash
# 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.

```bash
# 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.
