---
title: 'Belge Veritabanları'
source: 'https://academia.sh/tr/kurslar/nosql/belge-veritabanlari'
course: 'İlişkisel Olmayan Veri Modelleri'
language: tr
updated: '2026-08-17T18:08:58+00:00'
license: 'CC BY-SA 4.0'
---

# Belge Veritabanları

Değerin içini gören deponun kazandırdıkları ve ödettikleri: iç içe bir kitap kaydının tek belgede okunmasıyla dört ilişkisel tablodan birleştirilmesinin çarpım patlaması, alan yerinde güncellemenin yazma çarpanına etkisi, ve şema esnekliğinin okuma tarafına yıktığı yük — aynı derlemde üç şema sürümü bulunduğunda dizinli sorgunun sessizce kaçırdığı kayıt sayısı ile savunmalı okumanın taradığı belge sayısı.

Önceki ders tek bir soruyla kapandı: depo değerin içini görebilse ne değişirdi. Belge
veritabanı (document database) tam olarak bu değişikliktir. Kayıt yine tek bir anahtarla
adlanır, ama depo artık değerin alanlarını tanır: alan üzerinde koşul yazılabilir, alan
üzerinde dizin kurulabilir ve alanın kendisi yerinde güncellenebilir.

Bu ders iki şeyi ölçer. Birincisi kazanç tarafıdır: iç içe bir kitap kaydının tek okumada
gelmesi ile aynı kaydın dört ilişkisel tablodan birleştirilmesi arasındaki fark. İkincisi
bedel tarafıdır: alanların şema tarafından zorunlu kılınmaması, denetimi yazma anından okuma
anına taşır ve bu taşımanın bir sayısı vardır.

## Belge ve Derlem

Belge modelinde kayıt bir **belgedir (document)**: adlandırılmış alanlardan oluşan, alanları
kendi başına yine belge ya da dizi olabilen bir ağaç. Belgeler bir **derlem (collection)**
içinde toplanır. Derlem bir tablo değildir, çünkü içindeki belgelerin aynı alanları taşıması
zorunlu değildir; belge de bir satır değildir, çünkü alanı skaler olmak zorunda değildir.

Bu iki gevşetmenin doğrudan bir sonucu vardır: ilişkisel modelde ayrı tablolara dağılmak
zorunda olan çoklu değerler — bir kitabın yazarları, konuları, nüshaları — belgenin içinde
kalır. Birinci normal formun yasakladığı şey, belge modelinin tanımıdır.

**NS1:** katalog 50.000 kitap taşır; kitap `i`'nin 1 ile 3 arası yazarı, 2 ile 4 arası konusu
ve 1 ile 5 arası nüshası vardır. **NS2:** ilişkisel taraf `node:sqlite` ile kurulur ve
birleştirme sütunlarının hepsinde dizin vardır; baytlar iki tarafta da aynı JSON gösteriminden
sayılır.

```js
// belge/ic-ice.mjs — ayni kitap kaydinin dort iliskisel tabloda ve tek belgede okunmasi.
// Iliskisel taraf node:sqlite ile kurulur; sayilar kosumdan bagimsizdir.
import { DatabaseSync } from "node:sqlite";

const KITAP = 50_000, YAZARLAR = ["Atay", "Saramago", "Tanpinar", "Woolf", "Borges"];
const KONULAR = ["roman", "deneme", "siir", "tarih", "felsefe", "atlas"];
function* kitaplar() {                     // kitap i: 1+(i%3) yazar, 2+(i%3) konu, 1+(i%5) nusha
  for (let i = 1; i <= KITAP; i += 1) yield {
    kitap_id: i, baslik: `Kitap ${i}`,
    basim: { yil: 1960 + (i % 65), dil: i % 4 === 0 ? "en" : "tr" },
    yazarlar: Array.from({ length: 1 + (i % 3) }, (_, j) => YAZARLAR[(i + j) % 5]),
    konular: Array.from({ length: 2 + (i % 3) }, (_, j) => KONULAR[(i * 2 + j) % 6]),
    nushalar: Array.from({ length: 1 + (i % 5) }, (_, j) => ({
      nusha_id: i * 10 + j, sube_id: 1 + ((i + j) % 9), durum: j % 3 === 0 ? "rafta" : "oduncte" })),
  };
}

const db = new DatabaseSync(":memory:");
db.exec(`CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL,
           basim_yili INTEGER NOT NULL, dil TEXT NOT NULL);
         CREATE TABLE kitap_yazar (kitap_id INTEGER NOT NULL, sira INTEGER NOT NULL, ad TEXT NOT NULL);
         CREATE TABLE kitap_konu (kitap_id INTEGER NOT NULL, konu TEXT NOT NULL);
         CREATE TABLE nusha (nusha_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL,
           sube_id INTEGER NOT NULL, durum TEXT NOT NULL);`);
const eK = db.prepare("INSERT INTO kitap VALUES (?,?,?,?)");
const eY = db.prepare("INSERT INTO kitap_yazar VALUES (?,?,?)");
const eT = db.prepare("INSERT INTO kitap_konu VALUES (?,?)");
const eN = db.prepare("INSERT INTO nusha VALUES (?,?,?,?)");
const derlem = new Map();                  // belge derlemi: kitap_id -> belge
db.exec("BEGIN");
for (const k of kitaplar()) {
  eK.run(k.kitap_id, k.baslik, k.basim.yil, k.basim.dil);
  k.yazarlar.forEach((a, j) => eY.run(k.kitap_id, j, a));
  for (const t of k.konular) eT.run(k.kitap_id, t);
  for (const n of k.nushalar) eN.run(n.nusha_id, k.kitap_id, n.sube_id, n.durum);
  derlem.set(k.kitap_id, JSON.stringify(k));
}
db.exec("COMMIT");
db.exec(`CREATE INDEX ky ON kitap_yazar(kitap_id); CREATE INDEX kt ON kitap_konu(kitap_id);
         CREATE INDEX nk ON nusha(kitap_id);`);

const bayt = (x) => Buffer.byteLength(JSON.stringify(x));
const H = 4244;
const b = JSON.parse(derlem.get(H));
console.log(`hedef kitap ${H}: ${b.yazarlar.length} yazar, ${b.konular.length} konu, ` +
  `${b.nushalar.length} nusha; carpim ${b.yazarlar.length * b.konular.length * b.nushalar.length}`);

const satir = [];
{ let n = 0, by = 0;
  for (const s of ["SELECT * FROM kitap WHERE kitap_id = ?", "SELECT ad FROM kitap_yazar WHERE kitap_id = ?",
                   "SELECT konu FROM kitap_konu WHERE kitap_id = ?", "SELECT * FROM nusha WHERE kitap_id = ?"]) {
    const r = db.prepare(s).all(H); n += r.length; by += bayt(r); }
  satir.push(["iliskisel, dort sorgu", 4, n, by]); }
{ const r = db.prepare(`SELECT k.baslik, k.basim_yili, k.dil, y.ad, t.konu, n.sube_id, n.durum
    FROM kitap k JOIN kitap_yazar y ON y.kitap_id = k.kitap_id
                 JOIN kitap_konu  t ON t.kitap_id = k.kitap_id
                 JOIN nusha       n ON n.kitap_id = k.kitap_id WHERE k.kitap_id = ?`).all(H);
  satir.push(["iliskisel, tek birlestirme", 1, r.length, bayt(r)]); }
{ satir.push(["belge, tek okuma", 1, 1, Buffer.byteLength(derlem.get(H))]); }

console.log(`\n${"yol".padEnd(28)}${"gidis".padStart(7)}${"satir/belge".padStart(13)}${"bayt".padStart(7)}`);
for (const [a, g, k, y] of satir)
  console.log(a.padEnd(28) + String(g).padStart(7) + String(k).padStart(13) + String(y).padStart(7));

// Ayni kaydin tek alanini degistirmek: nusha durumu "rafta" -> "oduncte"
const oncesi = Buffer.byteLength(derlem.get(H));
const alan = Buffer.byteLength(JSON.stringify({ durum: "oduncte" }));
console.log(`\ntek alan guncellemesi (nusha durumu)`);
console.log(`${"model".padEnd(28)}${"yazilan bayt".padStart(14)}${"carpan".padStart(8)}`);
console.log("iliskisel, tek satir".padEnd(28) + String(alan).padStart(14) + "1.0".padStart(8));
console.log("belge, alan yerinde".padEnd(28) + String(alan).padStart(14) + "1.0".padStart(8));
console.log("anahtar-deger, butun deger".padEnd(28) + String(oncesi).padStart(14) +
  (oncesi / alan).toFixed(1).padStart(8));
```

```
hedef kitap 4244: 3 yazar, 4 konu, 5 nusha; carpim 60

yol                           gidis  satir/belge   bayt
iliskisel, dort sorgu             4           13    513
iliskisel, tek birlestirme        1           60   6718
belge, tek okuma                  1            1    412

tek alan guncellemesi (nusha durumu)
model                         yazilan bayt  carpan
iliskisel, tek satir                    19     1.0
belge, alan yerinde                     19     1.0
anahtar-deger, butun deger             412    21.7
```

Sayılar **ölçüm** sınıfındadır ve belirlenimlidir.

Üst tablo ilişkisel tarafın gerçek ikilemini gösteriyor. Dört ayrı sorgu 13 satır ve 513 bayt
getiriyor, ama dört gidiş ediyor. Gidişi bire indirmek için üç birleştirme yazıldığında sonuç
**60 satır** oluyor: üç yazar, dört konu ve beş nüshanın çarpımı. Kitabın başlığı, basım yılı
ve dili altmış kez tekrarlanıyor ve taşınan bayt 513'ten 6.718'e, on üç katına çıkıyor. Bu,
birleştirmenin bir kusuru değil tanımıdır — ilişkisel sonuç kümesi düz bir tablodur ve
birbirinden bağımsız üç çoklu değer aynı düzlemde yan yana getirildiğinde çarpım kaçınılmazdır.

Belge tarafında aynı kayıt **tek gidiş, tek belge, 412 bayttır**. Tekrar yoktur, çünkü sonuç
düzleştirilmemiştir: yazarlar bir dizi, konular bir dizi, nüshalar belgelerden oluşan bir dizi
olarak kalır. Uygulamanın istediği ağacın kendisi diskte de ağaç olarak durur.

Alt tablo, önceki dersteki yazma çarpanı ölçümünün belge tarafındaki karşılığıdır. Bir nüshanın
durumunu değiştirmek 19 bayt bilgidir. İlişkisel motor tek satırı günceller. Belge veritabanı
alanı belgenin içinde yerinde günceller — depo alanı tanıdığı için bütün belgeyi geri yazmak
zorunda değildir. Anahtar–değer deposunda aynı değişiklik 412 baytın tamamının yeniden
yazılmasıdır: çarpan 21,7. Belge modeli, önceki dersin ödünleşiminin bir ucunu — geniş değer,
tek okuma — koruyup öteki ucunu, yazma çarpanını ödemeden alır.

## Şemanın Okuma Tarafına Geçmesi

Bu kazancın bir bedeli vardır ve bedel şemanın kendisidir. İlişkisel şemada `NOT NULL` bir
yazma zamanı kuralıdır: alan yoksa kayıt reddedilir ve okuyan taraf alanın varlığını denetlemek
zorunda değildir. Belge derleminde böyle bir kural yoktur; alanın var olup olmadığı her okumada
sorulur.

Bu bir tasarım hatası değil, uygulamanın zamanla değişmesinin doğal sonucudur. Aynı derlemde
farklı zamanlarda yazılmış üç şema sürümü yan yana durur: ilk sürümde basım yılı üst düzeyde
`yil` alanıdır, ikinci sürümde `basim` alt belgesine taşınmıştır, üçüncü sürümde bir de
`dijital` alanı eklenmiştir. Soru değişmez — 2000 sonrası basılmış kitaplar hangileridir.

```js
// belge/sema.mjs — sema esnekliginin okuma tarafina yikilan yuku. Ayni derlemde uc sema
// surumu bulunur; ayni soru dort yolla sorulur. Iliskisel taraf node:sqlite ile kurulur.
import { DatabaseSync } from "node:sqlite";

const KITAP = 50_000, ESIK = 2000;
function surum(i) { return i <= 20_000 ? 1 : i <= 40_000 ? 2 : 3; }
function belge(i) {                        // v1: yil ust duzeyde; v2: basim.yil; v3: + dijital
  const yil = 1960 + (i % 65), ortak = { kitap_id: i, baslik: `Kitap ${i}` };
  if (surum(i) === 1) return { ...ortak, yil, dil: i % 4 === 0 ? "en" : "tr" };
  const b = { ...ortak, basim: { yil, dil: i % 4 === 0 ? "en" : "tr" } };
  return surum(i) === 3 ? { ...b, dijital: i % 2 === 0 } : b;
}

class BelgeDeposu {
  #belgeler = []; #dizin = new Map();
  gidis = 0; taranan = 0;
  ekle(b) { this.#belgeler.push(b); }
  al(b, yol) { return yol.split(".").reduce((o, k) => (o == null ? undefined : o[k]), b); }
  dizinKur(yol) { const g = [];
    for (const b of this.#belgeler) { const v = this.al(b, yol);
      if (v === undefined) continue;       // alani olmayan belge dizine hic girmez
      for (const x of Array.isArray(v) ? v : [v]) g.push([x, b.kitap_id]); }
    this.#dizin.set(yol, g.sort((p, q) => p[0] - q[0])); return g.length; }
  dizinliBuyuk(yol, esik) {                // sirali dizinde esigin ustundeki son ek okunur
    this.gidis += 1; const g = this.#dizin.get(yol);
    let a = 0, u = g.length;
    while (a < u) { const o = (a + u) >> 1; if (g[o][0] <= esik) a = o + 1; else u = o; }
    this.taranan += g.length - a; return g.slice(a).map(([, id]) => id); }
  taramaliBuyuk(sart) {                    // dizin yok: her belge tek tek bakilir
    this.gidis += 1; const c = [];
    for (const b of this.#belgeler) { this.taranan += 1; if (sart(b)) c.push(b.kitap_id); }
    return c; }
  sifirla() { this.gidis = 0; this.taranan = 0; }
}

const db = new DatabaseSync(":memory:");
db.exec(`CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL,
           basim_yili INTEGER NOT NULL, dil TEXT NOT NULL);`);
const ekle = db.prepare("INSERT INTO kitap VALUES (?,?,?,?)");
const depo = new BelgeDeposu();
db.exec("BEGIN");
for (let i = 1; i <= KITAP; i += 1) {
  const b = belge(i);
  ekle.run(i, b.baslik, b.yil ?? b.basim.yil, b.dil ?? b.basim.dil);
  depo.ekle(b);
}
db.exec("COMMIT");
db.exec("CREATE INDEX kitap_yil ON kitap(basim_yili);");
const d2 = depo.dizinKur("basim.yil"), d1 = depo.dizinKur("yil");
console.log(`${KITAP} kitap, esik ${ESIK}; surum dagilimi 1:20000 2:20000 3:10000`);
console.log(`dizin girdi sayisi — basim.yil: ${d2}, yil: ${d1}`);

const dogru = db.prepare("SELECT kitap_id FROM kitap WHERE basim_yili > ?").all(ESIK).map((r) => r.kitap_id);
const yol = [];
yol.push(["iliskisel, basim_yili dizini", 1, dogru.length, dogru.length]);
depo.sifirla(); let r = depo.dizinliBuyuk("basim.yil", ESIK);
yol.push(["belge, basim.yil dizini", depo.gidis, depo.taranan, r.length]);
depo.sifirla(); r = [...depo.dizinliBuyuk("basim.yil", ESIK), ...depo.dizinliBuyuk("yil", ESIK)];
yol.push(["belge, iki dizin birlesimi", depo.gidis, depo.taranan, r.length]);
depo.sifirla(); r = depo.taramaliBuyuk((b) => (b.basim?.yil ?? b.yil) > ESIK);
yol.push(["belge, savunmali tarama", depo.gidis, depo.taranan, r.length]);

console.log(`\n${"yol".padEnd(30)}${"gidis".padStart(7)}${"taranan giris".padStart(15)}` +
  `${"sonuc".padStart(7)}${"eksik".padStart(7)}`);
for (const [a, g, t, n] of yol)
  console.log(a.padEnd(30) + String(g).padStart(7) + String(t).padStart(15) +
    String(n).padStart(7) + String(dogru.length - n).padStart(7));

let redU = 0, kabulB = 0;                  // yil alani olmayan 500 kayit
for (let i = KITAP + 1; i <= KITAP + 500; i += 1) {
  try { ekle.run(i, `Kitap ${i}`, null, "tr"); } catch { redU += 1; }
  depo.ekle({ kitap_id: i, baslik: `Kitap ${i}` }); kabulB += 1;
}
console.log(`\nyil alani olmayan 500 kayit — iliskisel reddetti: ${redU}, belge derlemi kabul etti: ${kabulB}`);
depo.sifirla(); const sonra = depo.taramaliBuyuk((b) => (b.basim?.yil ?? b.yil) > ESIK);
console.log(`savunmali tarama artik ${depo.taranan} belge geziyor, sonuc ${sonra.length}`);
```

```
50000 kitap, esik 2000; surum dagilimi 1:20000 2:20000 3:10000
dizin girdi sayisi — basim.yil: 30000, yil: 20000

yol                             gidis  taranan giris  sonuc  eksik
iliskisel, basim_yili dizini        1          18456  18456      0
belge, basim.yil dizini             1          11083  11083   7373
belge, iki dizin birlesimi          2          18456  18456      0
belge, savunmali tarama             1          50000  18456      0

yil alani olmayan 500 kayit — iliskisel reddetti: 500, belge derlemi kabul etti: 500
savunmali tarama artik 50500 belge geziyor, sonuc 18456
```

İkinci satır bu dersin en pahalı sayısıdır. Sorgu hata vermiyor, boş dönmüyor, yavaş
çalışmıyor: **1 gidişte 11.083 sonuç dönüyor ve 7.373 kitap sessizce kayboluyor.** Neden dizin
tanımının kendisindedir — `basim.yil` alanı olmayan belge dizine hiç girmez, dolayısıyla
dizinden yanıtlanan bir sorgu için o belgeler yoktur. İlişkisel şemada bu durum oluşamaz, çünkü
alanın yokluğu yazma anında reddedilmiştir; son satır bunu doğruluyor: aynı 500 eksik kayıt
ilişkisel tarafta reddedildi, belge derleminde kabul edildi.

Doğru sonuca iki yol var ve ikisinin de bir bedeli var. **İki dizin birleşimi** doğru 18.456
kaydı veriyor ama gidiş sayısı ikiye çıkıyor ve her şema sürümü için ayrı bir dizin
tutulmasını, sorguyu yazan tarafın da bütün sürümleri bilmesini gerektiriyor. **Savunmalı
tarama** tek gidişte doğru yanıtı veriyor, ama hiçbir dizin kullanamıyor: koşul iki ayrı yol
arasında seçim yaptığı için dizinlenebilir bir ifade değil. Taranan giriş 18.456'dan 50.000'e
çıkıyor — sonucun 2,7 katı. Eksik alanlı 500 kayıt eklendikten sonra tarama 50.500 belgeye
uzuyor; sonuç değişmiyor.

Ödünleşim tek cümlede toplanır: **belge modeli şema denetimini kaldırmaz, yazma anından okuma
anına taşır.** Yazma tarafında kazanılan esneklik, okuma tarafında ya bir doğruluk açığı ya bir
dizin çoğalması ya da bir tarama olarak geri gelir. Bu yükün tamamen ortadan kalkması için
şemanın derleme geri konması gerekir; bunun nasıl yapıldığı ve neye mal olduğu bu kursun
ilerleyen konusudur.

## Özet

- Belge modelinde kayıt, alanları yine belge ya da dizi olabilen bir ağaçtır; derlem, belgelerin
  aynı alanları taşımasını zorunlu kılmaz.
- İç içe kitap kaydı belge tarafında 1 gidiş ve 412 bayt; ilişkisel tarafta ya 4 gidiş ve 513
  bayt, ya da tek birleştirmeyle 60 satır ve 6.718 bayttır — üç çoklu değerin çarpımı.
- Alanın yerinde güncellenmesi 19 bayt yazar; aynı değişiklik anahtar–değer modelinde bütün
  değerin yeniden yazılmasıdır, çarpan 21,7.
- Şema esnekliği denetimi okuma anına taşır: üç sürümlü bir derlemde `basim.yil` dizininden
  yanıtlanan sorgu 7.373 kaydı sessizce kaçırıyor, ilişkisel şema aynı 500 eksik kaydı yazma
  anında reddediyor.
- Doğru yanıtın iki yolu da ödetiyor: iki dizin birleşimi gidişi 2'ye çıkarıyor, savunmalı
  tarama dizini kullanamadığı için taranan girişi 18.456'dan 50.000'e çıkarıyor.

## Sonraki Adım

Buraya kadar iki aile de kaydı bir bütün olarak ele aldı: anahtar–değer deposu değeri opak bir
bütün saydı, belge veritabanı ağacı tanıdı ama yine bütün belgeyi tek yerde tuttu. İkisinde de
bir kaydın bütün alanları diskte yan yana durur, dolayısıyla tek bir alan için yapılan okuma da
o kaydın tamamının bulunduğu yeri okur. Katalog kaydının kırk alanı varsa ve sorgu yalnız
ikisini istiyorsa, geri kalan otuz sekizi de diskten geçer. Sonraki ders yerleşimi alanlara
göre ayıran aileyi ele alıyor ve üç şey ölçüyor: aynı sorgunun sütun ailesine göre yerleşimde
okuduğu bayt, tek anahtarın altına binlerce sütun toplanmasının anlamı, ve yazma yolunun
yerinde değiştirme yerine eklemeli olmasının silme işlemine ödettiği bedel.
