---
title: 'Sayfalama Maliyeti'
source: 'https://academia.sh/tr/kurslar/veri-erisim-katmani/sayfalama-maliyeti'
course: 'Veri Erişim Katmanı ve İş Mantığı'
language: tr
updated: '2026-08-17T18:06:51+00:00'
license: 'CC BY-SA 4.0'
---

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

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

```js
// 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);
```

```sh
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.

```js
// 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`);
}
```

```sh
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.

```js
// 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`);
}
```

```sh
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.

```js
// 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(","));
```

```sh
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.
