Ders 07 / 19
Belge Veri Tipleri
Belgenin kendi alan adlarını ve tip etiketlerini taşımasının bayt cinsinden bedeli: aynı 20.000 kitap kaydının açık adlı, kısa adlı ve sabit şemalı üç gösterimde kapladığı yer, kendini tanımlamanın payı ve aynı alanın iki tipte yazılmasının aralık sorgusunda ürettiği fark.
İçindekiler
Önceki konu seçim ölçütleriyle kapandı ve arkasında tamamlanmamış bir karar bıraktı: erişim örüntüsü bir aileye işaret ettiğinde iş bitmiyor. Aile seçmek şema seçmek değildir. Belge modeli aynı alan için birbirinden çok farklı şemalar kurmaya izin verir ve o izin bedelsiz değildir.
Bu konu o bedeli sayar; ilk soru en alttan başlar. İlişkisel bir tabloda alanın adı ve tipi tablo tanımında bir kez yazılır, satırda yalnız değer durur. Belge modelinde böyle bir tanım yoktur: her kayıt kendi alan adlarını ve tiplerini kendisi taşır. Bu dersin iki ölçüsü budur — o taşımanın bayt cinsinden tuttuğu yer ve aynı alanın iki farklı tipte yazılabilmesinin bir aralık sorgusunda ürettiği fark.
Kendini Tanımlayan Kayıt
Belge (document), adlandırılmış alanların sıralı listesidir; bir alanın değeri sayı ya da metin olabildiği gibi iç içe belge veya dizi de olabilir. Belgeler bir derlem (collection) içinde durur. Depo bu yapıyı ikili belge gösterimiyle (binary document representation) saklar: her alan bir tip etiketiyle (type tag) başlar, adını sonlandırıcı bayta kadar yazar, değeri tipin gerektirdiği biçimde koyar. Bu gösterime kendini tanımlayan denir; kaydı çözmek için dışarıdan bir şema gerekmez.
Gösterimin üç kuralı vardır. Sabit boylu tipler (tam sayı, ondalık, mantıksal, zaman)
uzunluk taşımaz, boyu etiketinden bilinir. Değişken boylu tipler (metin, iç içe belge,
dizi) kendi uzunluklarını dört baytlık bir ön ekle yazar; böylece okuyucu ilgilenmediği
bir alanı çözmeden atlayabilir. Dizi, alan adları "0", "1" diye giden bir iç
belgedir — dizinin öge sayısı da bu adlar üzerinden taşınır.
// ikilibelge.mjs — kendini tanimlayan ikili belge gosterimi. // Kodlama: [4 bayt uzunluk] (tip etiketi + alan adi + NUL + deger)* [0x00] export const ETIKET = { bos: 0x0a, tam32: 0x10, ondalik: 0x01, metin: 0x02, belge: 0x03, dizi: 0x04, mantik: 0x08, zaman: 0x09 }; const SIRA = ["bos", "tam32", "ondalik", "metin", "belge", "dizi", "mantik", "zaman"]; export function tipAdi(d) { if (d === null) return "bos"; if (Array.isArray(d)) return "dizi"; if (d instanceof Date) return "zaman"; if (typeof d === "boolean") return "mantik"; if (typeof d === "number") return Number.isInteger(d) ? "tam32" : "ondalik"; return typeof d === "string" ? "metin" : "belge"; } export function kodla(belge, s = { ad: 0, etiket: 0, uzunluk: 0, deger: 0 }) { const parca = []; for (const [ad, d] of Object.entries(belge)) { parca.push(Buffer.from([ETIKET[tipAdi(d)]]), Buffer.from(ad + "\0", "utf8"), deger(d, s)); s.etiket += 1; s.ad += Buffer.byteLength(ad) + 1; } const govde = Buffer.concat([...parca, Buffer.from([0])]); const bas = Buffer.alloc(4); bas.writeInt32LE(govde.length + 4); s.uzunluk += 4; s.deger += 1; // kapanis NUL return Buffer.concat([bas, govde]); } function deger(d, s) { const t = tipAdi(d), n = { bos: 0, mantik: 1, tam32: 4, ondalik: 8, zaman: 8 }[t]; if (n !== undefined) { s.deger += n; const b = Buffer.alloc(n); if (t === "mantik") b[0] = d ? 1 : 0; if (t === "tam32") b.writeInt32LE(d); if (t === "ondalik") b.writeDoubleLE(d); if (t === "zaman") b.writeBigInt64LE(BigInt(d.getTime())); return b; } if (t === "metin") { const m = Buffer.from(d + "\0", "utf8"), b = Buffer.alloc(4); b.writeInt32LE(m.length); s.uzunluk += 4; s.deger += m.length; return Buffer.concat([b, m]); } return kodla(t === "dizi" ? { ...d } : d, s); // dizi: alan adlari "0","1",... } export function coz(t, konum = 0) { const son = konum + t.readInt32LE(konum), cikan = {}; let i = konum + 4; while (i < son - 1) { const et = t[i], kes = t.indexOf(0, i + 1), ad = t.toString("utf8", i + 1, kes); i = kes + 1; if (et === ETIKET.bos) cikan[ad] = null; else if (et === ETIKET.mantik) cikan[ad] = t[i++] === 1; else if (et === ETIKET.tam32) { cikan[ad] = t.readInt32LE(i); i += 4; } else if (et === ETIKET.ondalik) { cikan[ad] = t.readDoubleLE(i); i += 8; } else if (et === ETIKET.zaman) { cikan[ad] = new Date(Number(t.readBigInt64LE(i))); i += 8; } else if (et === ETIKET.metin) { const n = t.readInt32LE(i); cikan[ad] = t.toString("utf8", i + 4, i + 3 + n); i += 4 + n; } else { const ic = coz(t, i); cikan[ad] = et === ETIKET.dizi ? Object.values(ic) : ic; i += t.readInt32LE(i); } } return cikan; } // Siralama tipe, sonra degere bakar: tip sirasi. export function kiyas(a, b) { const f = SIRA.indexOf(tipAdi(a)) - SIRA.indexOf(tipAdi(b)); if (f !== 0) return f; if (a === null) return 0; if (a instanceof Date) return a.getTime() - b.getTime(); return a < b ? -1 : a > b ? 1 : 0; }
Adın Bayt Bedeli
Ölçüm, önceki kursların kütüphane katalogunu belge biçiminde kurar. NS7 (varsayım): katalogda 20.000 kitap belgesi vardır; her kitabın 1–5 kopyası ve 2–4 etiketi bulunur, üretim tohumu 424242’dir. Aynı veri üç kararla kodlanır: alan adları açık yazılır, alan adları kısaltılır, ya da adlar ve tipler kayıttan çıkarılıp sabit bir şemaya taşınır. Üçüncüsü ilişkisel satırın yaptığı şeydir ve burada karşılaştırma tabanı olarak durur.
// ad-maliyeti.mjs — ayni 20.000 kitap belgesi iki semada kodlanir: acik adlar ve kisa // adlar. Ayni veri, ucuncu bir karar olarak sabit semali satirda da kodlanir. // Ayni dizinde ikilibelge.mjs bulunur. import { kodla, coz } from "./ikilibelge.mjs"; let cekirdek = 424242; // gorunur tohum const rast = () => (cekirdek = (cekirdek * 1103515245 + 12345) % 2147483648) / 2147483648; const SUBE = ["Merkez", "Bahcelievler", "Kadikoy", "Beyoglu", "Konak", "Nilufer"]; const DURUM = ["rafta", "oduncte", "onarimda"]; const ETIKETLER = ["roman", "tarih", "cocuk", "siir", "bilim", "basvuru"]; function kitaplar(n) { const cikan = []; for (let i = 1; i <= n; i += 1) { const kopyaSayisi = 1 + Math.floor(rast() * 5); const kopya = []; for (let j = 0; j < kopyaSayisi; j += 1) kopya.push({ barkod: `B${String(i * 10 + j).padStart(7, "0")}`, sube: SUBE[Math.floor(rast() * SUBE.length)], durum: DURUM[Math.floor(rast() * DURUM.length)] }); const etiketSayisi = 2 + Math.floor(rast() * 3); const etiket = []; for (let j = 0; j < etiketSayisi; j += 1) etiket.push(ETIKETLER[Math.floor(rast() * 6)]); cikan.push({ anahtar: `K-${String(i).padStart(5, "0")}`, baslik: `Kitap ${i} uzerine incelemeler`, yazar: `Yazar ${i % 4000}`, yayin_yili: 1950 + (i % 75), etiket, kopya }); } return cikan; } const KISA = { anahtar: "_k", baslik: "b", yazar: "y", yayin_yili: "yy", etiket: "e", kopya: "k", barkod: "bk", sube: "s", durum: "d" }; const kisalt = (b) => Object.fromEntries(Object.entries(b).map(([a, d]) => [KISA[a] ?? a, Array.isArray(d) ? d.map((x) => (typeof x === "object" ? kisalt(x) : x)) : d])); const KAYIT = kitaplar(20000); const olc = (kume) => { const s = { etiket: 0, ad: 0, uzunluk: 0, deger: 0 }; let bayt = 0; for (const b of kume) bayt += kodla(b, s).length; return { bayt, ...s }; }; const acik = olc(KAYIT), kisa = olc(KAYIT.map(kisalt)); // Sabit semali satir: tip ve alan adi semada bir kez durur, kayitta yalniz deger vardir. function sabitSema(kume) { let bayt = 0; for (const b of kume) { bayt += 4; // yayin_yili: tam32 for (const alan of ["anahtar", "baslik", "yazar"]) bayt += 2 + Buffer.byteLength(b[alan]); bayt += 2 + b.etiket.reduce((t, e) => t + 1 + Buffer.byteLength(e), 0); bayt += 2; for (const k of b.kopya) bayt += 2 + Buffer.byteLength(k.barkod) + 1 + Buffer.byteLength(k.sube) + 1 + Buffer.byteLength(k.durum); } return bayt; } const sabit = sabitSema(KAYIT); const alan = KAYIT.reduce((t, b) => t + 6 + b.etiket.length + b.kopya.length * 4, 0); console.log(`belge sayisi ${KAYIT.length}`); console.log(`toplam alan (ic ice dahil) ${alan}`); console.log(`acik adli sema ${acik.bayt} bayt belge basina ${(acik.bayt / 20000).toFixed(1)}`); console.log(`kisa adli sema ${kisa.bayt} bayt belge basina ${(kisa.bayt / 20000).toFixed(1)}`); console.log(`sabit semali satir ${sabit} bayt belge basina ${(sabit / 20000).toFixed(1)}`); console.log(`acik semada bayt dagilimi: ad ${acik.ad} tip etiketi ${acik.etiket} ` + `uzunluk alani ${acik.uzunluk} deger ${acik.deger}`); console.log(`kendini tanimlama payi acik %${(100 * (acik.bayt - sabit) / acik.bayt).toFixed(1)}` + ` kisa %${(100 * (kisa.bayt - sabit) / kisa.bayt).toFixed(1)}`); const tur = kodla(KAYIT[0]); console.log(`ilk belge ${tur.length} bayt, cozuldugunde ilk kopya: ` + JSON.stringify(coz(tur).kopya[0]));
belge sayisi 20000
toplam alan (ic ice dahil) 417706
acik adli sema 7357008 bayt belge basina 367.9
kisa adli sema 6082574 bayt belge basina 304.1
sabit semali satir 3139138 bayt belge basina 157.0
acik semada bayt dagilimi: ad 2209340 tip etiketi 417706 uzunluk alani 1670824 deger 3059138
kendini tanimlama payi acik %57.3 kisa %48.4
ilk belge 492 bayt, cozuldugunde ilk kopya: {"barkod":"B0000010","sube":"Beyoglu","durum":"rafta"}
Ölçüm 417.706 alan üzerinden yapılır — bir kitap belgesi ortalama 20,9 alan taşır, çünkü her kopya ve her etiket kendi alanıdır. Açık adlı gösterim 7.357.008 bayt tutar, sabit şemalı satır 3.139.138 bayt. Fark 4.217.870 bayttır ve bunun tamamı verinin kendisi değil, verinin kendini anlatmasıdır: 2.209.340 bayt alan adlarına, 417.706 bayt tip etiketlerine, 1.670.824 bayt uzunluk ön eklerine gider. Hesap: açık gösterimde saklanan her üç bayttan yaklaşık ikisi (%57,3) veri değil tanımdır.
Kısaltma bu payı %48,4’e düşürür. Aradaki 1.274.434 bayt yalnız alan adlarının
kısalmasından gelir; belge başına 63,8 bayt, ölçekle çarpıldığında 20.000 belgede 1,2 MB
civarı. Kararın öteki yüzü de sayılabilir: kısa adlı belge kendi başına okunduğunda bk
alanının barkod olduğunu söylemez, o bilgi artık depoda değil uygulamadadır. Kendini
tanımlama, tam da bu bilginin kayıtta durmasıdır ve bedeli ölçülmüş durumdadır.
Aynı Alan İki Tipte
Kayıtta tip taşımanın karşılığı, aynı alanın iki belgede iki farklı tipte olabilmesidir.
İlişkisel şema bunu tanım anında engeller; belge modelinde engel yoktur ve engelin
yokluğu sorgu tarafında ortaya çıkar. NS8 (varsayım): dış kaynaktan alınan kayıtların
sekizde birinde yayın yılı metin olarak yazılmıştır — 2010 yerine "2010".
Bir aralık sorgusunun bu belgelere ne yapacağı, işlecin semantiğine bağlıdır ve iki seçenek vardır. Tip sırası (type ordering), farklı tipteki değerleri de karşılaştırır: önce tipler, tip eşitse değerler sıralanır. Tip parantezi (type bracketing) ise karşılaştırmayı yalnız işlecin sağ tarafıyla aynı tipteki değerlere uygular, ötekiler eşleşmez.
// tip-karisikligi.mjs — ayni alan iki tipte: yayin_yili kayitlarin bir bolumunde tam // sayi, bir bolumunde metin. Ayni araligi iki islec semantigi ile sayar. // Ayni dizinde ikilibelge.mjs bulunur. import { kiyas, tipAdi, kodla } from "./ikilibelge.mjs"; const N = 20000; const kume = []; for (let i = 1; i <= N; i += 1) { const yil = 1950 + (i % 75); kume.push({ anahtar: `K-${String(i).padStart(5, "0")}`, yayin_yili: i % 8 === 0 ? String(yil) : yil }); // her sekizinci belge metin yazar } const metinli = kume.filter((b) => tipAdi(b.yayin_yili) === "metin").length; // A karari: karsilastirma tip sirasini kullanir (siralamadaki toplam duzen). const A = (b, d, yon) => (yon === "ge" ? kiyas(b.yayin_yili, d) >= 0 : kiyas(b.yayin_yili, d) < 0); // B karari: karsilastirma tip parantezi uygular, farkli tipteki deger eslesmez. const B = (b, d, yon) => tipAdi(b.yayin_yili) === tipAdi(d) && A(b, d, yon); const say = (f, d, yon) => kume.filter((b) => f(b, d, yon)).length; console.log(`belge ${N}, yayin_yili metin olan ${metinli} (%${(100 * metinli / N).toFixed(1)})`); console.log(`A tip sirasi >= 2010 -> ${say(A, 2010, "ge")} < 2010 -> ${say(A, 2010, "lt")}` + ` toplam ${say(A, 2010, "ge") + say(A, 2010, "lt")}`); console.log(`B tip parantezi >= 2010 -> ${say(B, 2010, "ge")} < 2010 -> ${say(B, 2010, "lt")}` + ` toplam ${say(B, 2010, "ge") + say(B, 2010, "lt")}`); console.log(`tek tipli sema >= 2010 -> ` + `${kume.filter((b) => Number(b.yayin_yili) >= 2010).length}`); console.log(`B ile hicbir yana dusmeyen belge ${N - say(B, 2010, "ge") - say(B, 2010, "lt")}`); console.log(`B, operand metin olursa: >= "2010" -> ${say(B, "2010", "ge")}`); // Siralama her zaman toplam duzeni kullanir: sonuc tipe gore obeklenir. const dizili = [...kume].sort((a, b) => kiyas(a.yayin_yili, b.yayin_yili)); const gecis = dizili.findIndex((b) => tipAdi(b.yayin_yili) === "metin"); console.log(`siralamada ilk metin ${gecis}. sirada ${JSON.stringify(dizili[gecis].yayin_yili)}` + `, bir oncesi ${JSON.stringify(dizili[gecis - 1].yayin_yili)}`); const bayt = (d) => kodla({ yayin_yili: d }).length; console.log(`tek alanli belge: tam sayi ${bayt(2010)} bayt, metin ${bayt("2010")} bayt`);
belge 20000, yayin_yili metin olan 2500 (%12.5) A tip sirasi >= 2010 -> 5991 < 2010 -> 14009 toplam 20000 B tip parantezi >= 2010 -> 3491 < 2010 -> 14009 toplam 17500 tek tipli sema >= 2010 -> 3990 B ile hicbir yana dusmeyen belge 2500 B, operand metin olursa: >= "2010" -> 499 siralamada ilk metin 17500. sirada "1950", bir oncesi 2024 tek alanli belge: tam sayi 21 bayt, metin 26 bayt
Doğru yanıt 3.990’dır: alanın tamamı tam sayı olsaydı 2010 ve sonrası için bu kadar belge dönerdi. İki semantik de bu sayıyı vermez, ama farklı biçimde vermez. Tip sırası 5.991 döndürür; fazlalık 2.001 belgenin tamamı metin yazılmış kayıtlardır ve içlerinde 1950 yılına ait olanlar da vardır — metin tipi sırada tam sayıdan sonra geldiği için her metin değer her tam sayıdan büyük sayılır. Tip parantezi 3.491 döndürür; eksik 499 belge, yılı metin yazılmış ve gerçekte 2010 ya da sonrası olan kayıtlardır.
Tip parantezinin daha sinsi sonucu son satırda görünür. >= 2010 ile < 2010 bir
aralığın iki yarısıdır ve toplamları 20.000 olmalıdır; ölçüm 17.500 verir. 2.500 belge iki
sorgunun hiçbirine düşmez. Bu belgeler bir raporda eksik, ters sorguda da eksik görünür;
hiçbir yanıt onları göstermediği için hata fark edilmez. Sıralama ise her zaman toplam
düzeni kullanır: 17.500. sıraya kadar tam sayılar, oradan sonra metinler gelir, yani
"1950" değeri 2024 değerinden sonraya düşer.
Bayt tarafı da aynı yöne işaret eder: tek alanlı bir belge yıl tam sayı olduğunda 21, metin olduğunda 26 bayt tutar. 2.500 belgede fark 12.500 bayttır — küçük bir sayı, ama tipin belgede taşınması yalnız yer değil, anlam da ayırır. Ders kuralı şudur: belge modeli alanın tipini yazma anında bağlamaz, bu yüzden tipi bağlama işi ya yazan uygulamaya ya da sonradan konacak bir kurala kalır. Bu kuralın nasıl konacağı ve neyi yakalayıp neyi kaçırdığı bu konunun altıncı dersinin sorusudur.
Özet
- İkili belge gösterimi her alanı tip etiketi, alan adı ve gerekiyorsa uzunluk ön ekiyle saklar; kayıt kendini tanımlar, çözmek için dış şema gerekmez.
- Aynı 20.000 kitap belgesi açık adlarla 7.357.008, kısa adlarla 6.082.574, sabit şemalı satırda 3.139.138 bayt tutar; açık gösterimde baytın %57,3’ü veri değil tanımdır.
- Alan adlarını kısaltmak 1.274.434 bayt (belge başına 63,8 bayt) kazandırır ve alanın ne olduğu bilgisini depodan uygulamaya taşır.
- Aynı alanın iki tipte yazılması aralık sorgusunu bozar: tip sırası 3.990 yerine 5.991, tip parantezi 3.491 belge döndürür.
- Tip parantezinde
>= 2010ile< 2010toplamı 20.000 değil 17.500’dür; 2.500 belge iki yanıtın hiçbirinde görünmez.
Sonraki Adım
Bu ders tek bir belgenin içine baktı ve alanın adıyla tipinin bedelini saydı. Katalog ise tek bir belgeden ibaret değil: kitap, kopya, üye ve ödünç kayıtları birbirine bağlıdır ve belge modeli bu bağı iki farklı yoldan kurmaya izin verir — ilgili veriyi belgenin içine gömmek ya da ayrı bir derlemde tutup anahtarıyla göstermek. İkisi de aynı katalogu saklar, ama aynı okumayı farklı sayıda gidişle karşılar, aynı güncellemede farklı sayıda belgeyi yeniden yazar ve belge boyutu sınırına farklı hızda yaklaşır. Sonraki ders bu iki kararı aynı iş üstünde koşturup farkı sayar.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.