İçeriğe geç
academia.sh

Ders 03 / 19

Geniş Sütun Depoları

Yerleşimin alanlara göre ayrılmasının iki yönlü bedeli: aynı verinin satır yerleşiminde ve sütun ailesi yerleşiminde okunan sayfa sayısı, tek anahtarın altında binlerce sütun toplayan geniş satırın aralık dilimi, bölüm anahtarının erişim örüntüsünü belirlemesi ve iki örüntüyü birden karşılamanın yazma çarpanı, eklemeli yazma yolunda silme işaretinin yer çarpanına ve okunan girdiye etkisi.

İçindekiler

Önceki iki ailede kaydın bütün alanları diskte yan yana duruyordu. Anahtar–değer deposunda değer tek bir bayt dizisiydi; belge veritabanı ağacı tanıdı ama belgeyi yine tek yerde tuttu. Bunun bir sonucu vardı: yalnız iki alanı isteyen bir sorgu, o kayıtların tamamının bulunduğu yeri okur. Geniş sütun deposu (wide column store) yerleşimi bu noktadan böler.

Model şudur: bir satır anahtarıyla adlanır, satırın sütunları önceden bildirilmiş sütun ailelerine (column family) ayrılır ve aynı ailenin sütunları diskte birlikte durur. Bu ders üç şeyi ölçer: yerleşimin okunan bayta etkisi, tek anahtarın altında binlerce sütunun toplanması, ve yazma yolunun yerinde değiştirme yerine eklemeli olması.

Yerleşim Neyi Değiştirir

Okuma birimi baytın kendisi değildir; depo diski sayfa sayfa okur. Bu yüzden yerleşim sorusunun karşılığı “kaç alan okundu” değil, kaç sayfaya dokunuldudur. NS1: katalog 20.000 kayıt taşır ve dört aileye ayrılır — kimlik, basim, dolasim, metin; özet alanı kaydın en büyük parçasıdır. NS2: sayfa 4096 bayttır ve okuma bir sayfanın herhangi bir baytına dokunduğunda o sayfanın tamamını okur.

// sutun/yerlesim.mjs — ayni kayitlarin satir yerlesiminde ve sutun ailesi yerlesiminde
// diske serilmesi, sonra iki sorgunun dokundugu 4096 baytlik sayfalarin sayilmasi.
// Okuma birimi sayfadir; sutun ailesi yerlesiminde erisim birimi ailedir, sutun degil.
const KAYIT = 20_000, SAYFA = 4096;
const AILE = { kimlik: ["kitap_id", "baslik", "yazar"], basim: ["yil", "dil", "isbn"],
               dolasim: ["odunc_sayisi", "sube_id", "durum"], metin: ["ozet"] };
const kitap = (i) => ({ kitap_id: i, baslik: `Kitap ${i} ${"a".repeat(30)}`,
  yazar: `Yazar ${i % 500} ${"b".repeat(12)}`, yil: 1960 + (i % 65), dil: i % 4 === 0 ? "en" : "tr",
  isbn: `978-${String(1000000000 + i)}`, odunc_sayisi: i % 97, sube_id: 1 + (i % 9),
  durum: i % 3 === 0 ? "rafta" : "oduncte", ozet: `${"c".repeat(280)} ${i}` });

function seril(satirYerlesimi) {           // her alan icin [baslangic, bitis] bayt araligi
  const yer = new Map(); let ofset = 0;
  const koy = (i, alan) => { const n = Buffer.byteLength(String(kitap(i)[alan]));
    yer.set(`${i}|${alan}`, [ofset, ofset + n]); ofset += n; };
  if (satirYerlesimi) { for (let i = 1; i <= KAYIT; i += 1)
      for (const a of Object.values(AILE)) for (const alan of a) koy(i, alan); }
  else { for (const a of Object.values(AILE))
      for (let i = 1; i <= KAYIT; i += 1) for (const alan of a) koy(i, alan); }
  return { yer, boyut: ofset };
}
const sayfaSayisi = (araliklar) => { const s = new Set();
  for (const [b, u] of araliklar)
    for (let p = b / SAYFA | 0; p <= (u - 1) / SAYFA | 0; p += 1) s.add(p);
  return s.size; };

const satir = seril(true), aile = seril(false);
console.log(`${KAYIT} kayit, toplam ${satir.boyut} bayt, ${Math.ceil(satir.boyut / SAYFA)} sayfa`);
for (const [ad, alanlar] of Object.entries(AILE)) {
  let n = 0; for (let i = 1; i <= KAYIT; i += 1) for (const alan of alanlar)
    n += aile.yer.get(`${i}|${alan}`)[1] - aile.yer.get(`${i}|${alan}`)[0];
  console.log(`  aile ${ad.padEnd(8)} ${String(n).padStart(9)} bayt`);
}

const H = 4244;
const S1 = [], A1 = [];                    // S1: butun kayitlarda dolasim ailesi
for (let i = 1; i <= KAYIT; i += 1) for (const alan of AILE.dolasim) {
  S1.push(satir.yer.get(`${i}|${alan}`)); A1.push(aile.yer.get(`${i}|${alan}`)); }
const S2 = [], A2 = [];                    // S2: tek kaydin butun alanlari
for (const a of Object.values(AILE)) for (const alan of a) {
  S2.push(satir.yer.get(`${H}|${alan}`)); A2.push(aile.yer.get(`${H}|${alan}`)); }

console.log(`\n${"sorgu".padEnd(38)}${"satir yerlesimi".padStart(17)}${"aile yerlesimi".padStart(16)}${"oran".padStart(7)}`);
for (const [ad, s, a] of [["butun kitaplarin dolasim ozeti", S1, A1], [`tek kitabin (${H}) butun alanlari`, S2, A2]]) {
  const ps = sayfaSayisi(s), pa = sayfaSayisi(a);
  console.log(ad.padEnd(38) + `${ps} sayfa`.padStart(17) + `${pa} sayfa`.padStart(16) +
    (ps / pa).toFixed(2).padStart(7));
}
20000 kayit, toplam 7646881 bayt, 1867 sayfa
  aile kimlik     1353388 bayt
  aile basim       400000 bayt
  aile dolasim     184599 bayt
  aile metin      5708894 bayt

sorgu                                   satir yerlesimi  aile yerlesimi   oran
butun kitaplarin dolasim ozeti               1867 sayfa        46 sayfa  40.59
tek kitabin (4244) butun alanlari               1 sayfa         4 sayfa   0.25

Sayılar ölçüm sınıfındadır: iki yerleşim de gerçekten serildi, sayfalar gerçekten sayıldı.

İki satır birbirinin tersini söylüyor ve modelin tamamı bu iki satırdadır. Bütün kayıtların dolaşım özetini almak satır yerleşiminde 1.867 sayfa, aile yerleşiminde 46 sayfa okuyor: kırk kat fark. Neden basit bir orandır — dolaşım ailesi verinin 184.599 baytıdır, geri kalan 7,4 MB’ın çoğu özet metnidir ve satır yerleşiminde her dolaşım alanı bir özet metninin yanında durduğu için o metin de sayfayla birlikte okunur.

İkinci satır bedeli gösteriyor. Tek bir kitabın bütün alanlarını okumak satır yerleşiminde tek sayfa, aile yerleşiminde dört sayfadır — her aile ayrı bir yerdedir, dolayısıyla kaydı toplamak için dört ayrı yere gitmek gerekir. Oran 0,25: aile yerleşimi bu sorguda dört kat kötüdür. Yerleşim kararı bir sorgu sınıfını ucuzlatırken ötekini pahalılaştırır; ailenin sınırı, birlikte okunan alanların sınırı olmalıdır.

Geniş Satır

Modelin ikinci ayırt edici yanı satırın genişliğinin sınırsız olmasıdır. Bir satır anahtarının altında binlerce, on binlerce sütun toplanabilir ve sütun adı bir şema alanı değil, bir veridir: kütüphane örneğinde her ödünç, kitap satırının altında bir sütundur ve sütun adı ödüncün zaman damgasıdır. Sütunlar satır içinde ada göre sıralı durduğu için bir zaman aralığı tek istekte dilimlenir. Bunun adı geniş satırdır (wide row).

// sutun/genis-satir.mjs — odunc gecmisinin genis satir olarak tutulmasi. Sutunlar satir
// icinde sirali durur, aralik dilimi tek istektir; ayni soru baska bir bolum anahtariyla
// sorulunca taramaya duser. Sayilar kosumdan bagimsizdir.
const KITAP = 5_000, POPULER = 4_000;      // ilk bes kitap POPULER kez odunc verilir
class GenisSutunDeposu {
  #satir = new Map();
  gidis = 0; sutun = 0; girdi = 0;
  yaz(anahtar, ad, deger) { if (!this.#satir.has(anahtar)) this.#satir.set(anahtar, new Map());
    this.#satir.get(anahtar).set(ad, deger); this.girdi += 1; }
  dilim(anahtar, alt, ust) {               // satir icinde sirali aralik: tek istek
    this.gidis += 1; const s = this.#satir.get(anahtar) ?? new Map(); const c = [];
    for (const [ad, d] of [...s].sort(([x], [y]) => (x < y ? -1 : 1)))
      if (ad >= alt && ad < ust) { this.sutun += 1; c.push(d); }
    return c; }
  tara(sart) { this.gidis += 1; const c = [];
    for (const [a, s] of this.#satir) for (const [ad, d] of s) { this.sutun += 1;
      if (sart(a, ad, d)) c.push(d); }
    return c; }
  enGenis() { let e = 0, k = null;
    for (const [a, s] of this.#satir) if (s.size > e) { e = s.size; k = a; }
    return [k, e]; }
  sifirla() { this.gidis = 0; this.sutun = 0; }
}

const kitapDepo = new GenisSutunDeposu(), uyeDepo = new GenisSutunDeposu();
for (let i = 1; i <= KITAP; i += 1)
  for (let j = 0, n = i <= 5 ? POPULER : 1 + (i % 97); j < n; j += 1) {
    const ay = 1 + ((i + j) % 12), gun = 1 + ((i * 3 + j * 7) % 28), uye = 1 + ((i * 13 + j) % 20_000);
    const t = `2024-${String(ay).padStart(2, "0")}-${String(gun).padStart(2, "0")}`;
    kitapDepo.yaz(`kitap:${i}`, `odunc|${t}|${j}`, uye);        // bolum anahtari kitap
    uyeDepo.yaz(`uye:${uye}`, `odunc|${t}|${i}|${j}`, i);       // bolum anahtari uye
  }
const [ea, es] = kitapDepo.enGenis();
console.log(`${KITAP} kitap, ${kitapDepo.girdi} odunc girdisi; ortalama satir ` +
  `${(kitapDepo.girdi / KITAP).toFixed(1)} sutun, en genis satir ${ea} ${es} sutun`);

const H = 3, ALT = "odunc|2024-03", UST = "odunc|2024-04";
const yaz = (ad, depo, sonuc) => console.log(ad.padEnd(36) + String(depo.gidis).padStart(7) +
  String(depo.sutun).padStart(14) + String(sonuc).padStart(7));
console.log(`\n${"yol".padEnd(36)}${"gidis".padStart(7)}${"okunan sutun".padStart(14)}${"sonuc".padStart(7)}`);
kitapDepo.sifirla(); let r = kitapDepo.dilim(`kitap:${H}`, ALT, UST);
yaz("kitap bolumu, aralik dilimi", kitapDepo, r.length);
kitapDepo.sifirla(); r = kitapDepo.dilim(`kitap:${H}`, "odunc|", "odunc}");
yaz("kitap bolumu, butun satir", kitapDepo, r.length);
uyeDepo.sifirla(); r = uyeDepo.tara((a, ad, d) => ad.startsWith(ALT) && d === H);
yaz("uye bolumu, ayni soru", uyeDepo, r.length);

console.log(`\niki erisim oruntusu birden: ${kitapDepo.girdi} + ${uyeDepo.girdi} = ` +
  `${kitapDepo.girdi + uyeDepo.girdi} girdi yazilir, yazma carpani ` +
  `${((kitapDepo.girdi + uyeDepo.girdi) / kitapDepo.girdi).toFixed(1)}`);
5000 kitap, 263867 odunc girdisi; ortalama satir 52.8 sutun, en genis satir kitap:1 4000 sutun

yol                                   gidis  okunan sutun  sonuc
kitap bolumu, aralik dilimi               1           333    333
kitap bolumu, butun satir                 1          4000   4000
uye bolumu, ayni soru                     1        263867    333

iki erisim oruntusu birden: 263867 + 263867 = 527734 girdi yazilir, yazma carpani 2.0

Bir ayın ödünçleri, dört bin sütunluk satırın içinden 333 sütun okunarak alınıyor: sütunlar sıralı olduğu için depo aralığın başına inip sonuna kadar yürüyor, satırın geri kalanına hiç dokunmuyor. Aynı satırın tamamı istendiğinde okunan sütun 4.000’e çıkıyor — geniş satır bir avantaj değil, doğru dilimlendiğinde avantajdır.

Üçüncü satır bölüm anahtarının ne kadar belirleyici olduğunu gösteriyor. Aynı veri üye anahtarıyla bölümlendiğinde bir kitabın ödünçleri hiçbir satırın içinde bir arada değildir ve soru 263.867 sütunluk bir taramaya düşer. Karşılığı bilinen bir karardır: aynı veri ikinci bir bölüm anahtarıyla ikinci kez yazılır — Veri Katmanı Ölçekleme kursunda bu karar sorgu sayısı üzerinden ölçülmüştü, buradaki karşılığı yazma tarafındadır ve tam olarak iki katıdır.

Geniş satırın kendi sınırı da bu tabloda duruyor. Satır bölümlemenin birimi olduğu için tek bir satır sınırsız büyüyemez: en geniş satır 4.000 sütun taşırken ortalama satır 52,8 sütundur. Sınırsız büyüyen bir satır, Veri Katmanı Ölçekleme kursunda adı konan sıcak noktanın ta kendisidir; çözüm satır anahtarına bir zaman kutusu eklemektir.

Eklemeli Yazma Yolu

Bu ailenin yazma başarımı yerleşimden değil yazma yolundan gelir. Yazma yerinde değişiklik yapmaz: her yazma yeni bir girdidir ve sıraya eklenir. Bellekteki tablo dolduğunda değiştirilemez bir dosyaya çevrilir ve bir daha dokunulmaz. Silme de bir yazmadır — kaydın üstüne silme işareti (tombstone) konur. Okuma, dosyaları yeniden eskiye doğru tarar ve ilk bulduğu sürümü döndürür. Geçersiz sürümleri ve silme işaretlerini düşürmek ayrı bir işin — toparlamanın (compaction) — konusudur.

// sutun/eklemeli.mjs — eklemeli yazma yolu: her yazma yeni girdidir, silme bir silme
// isaretidir. Bellek tablosu dolunca degistirilemez bir dosyaya cevrilir. Okuma dosyalari
// yeniden eskiye tarar. Toparlama, gecersiz surumleri ve silme isaretlerini duser.
const DOSYA_BOYU = 25_000, SILME = Symbol("silme isareti");
class EklemeliDepo {
  #bellek = new Map(); #dosyalar = []; yazilanGirdi = 0;
  bakilanDosya = 0; okunanGirdi = 0;
  #cevir() { if (this.#bellek.size >= DOSYA_BOYU) { this.#dosyalar.push(this.#bellek); this.#bellek = new Map(); } }
  yaz(k, d) { this.#bellek.set(k, d); this.yazilanGirdi += 1; this.#cevir(); }
  sil(k) { this.yaz(k, SILME); }
  #yeniden() { return [this.#bellek, ...this.#dosyalar.slice().reverse()]; }   // yeniden eskiye
  oku(k) { const kaynak = this.#yeniden();
    for (const s of kaynak) { this.bakilanDosya += 1;
      if (s.has(k)) { this.okunanGirdi += 1; const d = s.get(k); return d === SILME ? undefined : d; } }
    return undefined; }
  tara() { const gorulen = new Set(), c = [];
    for (const s of this.#yeniden()) { this.bakilanDosya += 1;
      for (const [k, d] of s) { this.okunanGirdi += 1;
        if (gorulen.has(k)) continue; gorulen.add(k); if (d !== SILME) c.push(k); } }
    return c; }
  toparla() { const y = new Map();
    for (const s of [...this.#dosyalar, this.#bellek]) for (const [k, d] of s) y.set(k, d);
    for (const [k, d] of y) if (d === SILME) y.delete(k);
    this.#dosyalar = []; this.#bellek = new Map();
    for (const [k, d] of y) this.yaz(k, d);
    this.yazilanGirdi = y.size; return y.size; }
  durum() { return [this.#dosyalar.length + 1, this.yazilanGirdi]; }
  sifirla() { this.bakilanDosya = 0; this.okunanGirdi = 0; }
}

const depo = new EklemeliDepo(), N = 100_000;
for (let i = 1; i <= N; i += 1) depo.yaz(`odunc:${i}`, { kitap: i % 5000, iade: null });
for (let i = 1; i <= N; i += 1) if (i % 5 < 2) depo.yaz(`odunc:${i}`, { kitap: i % 5000, iade: "2024-06-01" });
for (let i = 1; i <= N; i += 1) if (i % 5 === 4) depo.sil(`odunc:${i}`);
const [dosya, girdi] = depo.durum();
const canli = N - Math.floor(N / 5);
console.log(`${N} kayit, ${girdi} yazilan girdi, ${canli} canli kayit; ` +
  `${dosya} dosya, yer carpani ${(girdi / canli).toFixed(2)}`);

const olcum = (ad, is) => { depo.sifirla(); const r = is();
  console.log(ad.padEnd(30) + String(depo.bakilanDosya).padStart(15) +
    String(depo.okunanGirdi).padStart(15) + String(r).padStart(9)); };
console.log(`\n${"okuma".padEnd(30)}${"bakilan dosya".padStart(15)}${"okunan girdi".padStart(15)}${"sonuc".padStart(9)}`);
olcum("guncellenen anahtar", () => (depo.oku("odunc:99996") ? "bulundu" : "yok"));
olcum("hic dokunulmamis anahtar", () => (depo.oku("odunc:2") ? "bulundu" : "yok"));
olcum("silinmis anahtar", () => (depo.oku("odunc:99994") ? "bulundu" : "yok"));
olcum("hic yazilmamis anahtar", () => (depo.oku("odunc:999999") ? "bulundu" : "yok"));
olcum("butun kayitlarin taranmasi", () => depo.tara().length);

const sonra = depo.toparla();
console.log(`\ntoparlama sonrasi: ${sonra} girdi, ${depo.durum()[0]} dosya, ` +
  `yer carpani ${(sonra / canli).toFixed(2)}`);
depo.sifirla(); const t = depo.tara();
console.log(`ayni tarama artik ${depo.okunanGirdi} girdi okuyor, sonuc ${t.length}`);
100000 kayit, 160000 yazilan girdi, 80000 canli kayit; 7 dosya, yer carpani 2.00

okuma                           bakilan dosya   okunan girdi    sonuc
guncellenen anahtar                         2              1  bulundu
hic dokunulmamis anahtar                    7              1  bulundu
silinmis anahtar                            1              1      yok
hic yazilmamis anahtar                      7              0      yok
butun kayitlarin taranmasi                  7         160000    80000

toparlama sonrasi: 80000 girdi, 4 dosya, yer carpani 1.00
ayni tarama artik 80000 girdi okuyor, sonuc 80000

Yazma tarafı ucuzdur ve bu tablo nedenini gösteriyor: hiçbir yazma bir okuma gerektirmedi, hiçbir dosya değiştirilmedi. Bedel okuma tarafında ve yerdedir. 80.000 canlı kayıt için diskte 160.000 girdi duruyor; yer çarpanı 2,00. Silinen 20.000 kayıt hâlâ oradadır — silme işareti yeri boşaltmaz, doldurur.

Okuma satırları maliyetin nereye dağıldığını gösteriyor. Yakın zamanda güncellenen bir anahtar 2 dosyada bulunuyor. Uzun süredir dokunulmamış bir anahtar 7 dosya gezdiriyor, çünkü en eski dosyadadır ve ondan yenisinde olmadığı ancak hepsine bakılarak anlaşılır. En pahalısı hiç yazılmamış anahtardır: 7 dosyanın hepsi taranır ve sonuç yoktur. Silinmiş anahtar ise tek dosyada biter — silme işareti en yeni girdidir ve okuma orada durur.

Tarama satırı silme işaretinin asıl bedelini taşıyor: 80.000 canlı kayıt için 160.000 girdi okunuyor. Toparlama bu iki sayıyı eşitliyor — girdi 80.000’e, yer çarpanı 1,00’e, dosya sayısı 7’den 4’e iniyor ve aynı tarama 80.000 girdi okuyor. Toparlamanın kendisi bütün veriyi bir kez daha yazar; yani eklemeli yazma yolu yazma işini ucuzlatmaz, arka plana erteler.

Özet

  • Geniş sütun modelinde satır bir anahtarla adlanır, sütunlar sütun ailelerine ayrılır ve aynı ailenin sütunları diskte birlikte durur.
  • Yerleşim iki yönlü çalışıyor: dolaşım özeti satır yerleşiminde 1.867, aile yerleşiminde 46 sayfa okuyor (40,6 kat); tek kaydın bütün alanları ise 1 sayfaya karşı 4 sayfadır.
  • Geniş satırda sütun adı bir veridir ve sütunlar sıralıdır: 4.000 sütunluk satırdan bir ayın 333 ödüncü tek istekte dilimleniyor, aynı soru başka bir bölüm anahtarıyla 263.867 sütunluk taramaya düşüyor.
  • İki erişim örüntüsünü birden karşılamak aynı verinin ikinci kez yazılmasıdır: 263.867 girdi 527.734’e çıkar, yazma çarpanı 2,0.
  • Eklemeli yazma yolunda hiçbir yazma okuma gerektirmez; bedeli yer çarpanı 2,00 ve hiç yazılmamış bir anahtarın 7 dosya gezdirmesidir. Silme işareti yeri boşaltmaz.
  • Toparlama girdi sayısını 160.000’den 80.000’e, yer çarpanını 1,00’e indirir; karşılığında bütün veriyi bir kez daha yazar.

Sonraki Adım

Üç ailenin üçü de aynı varsayımı paylaşıyordu: sorgunun konusu kaydın kendisidir. Anahtar biliniyorsa kayıt gelir, bir alan üzerinde koşul varsa dizin kurulur, bir aralık isteniyorsa yerleşim ona göre seçilir. Kütüphane verisinde ise bazı sorular kayıtlarla değil kayıtlar arasındaki bağlarla ilgilidir: aynı kitabı ödünç almış üyeler üzerinden iki üye arasındaki uzaklık, bir yazarın ortak çalıştığı yazarlardan yürüyerek ulaşılan yazarlar. İlişkisel modelde bu bağ bir birleştirme tablosudur ve her adım yeni bir birleştirme demektir. Sonraki ders bağın kendisini birinci sınıf nesne yapan aileyi ele alıyor ve şunu ölçüyor: aynı soruda komşuluk listesinin ve birleştirme tablosunun dokunduğu kayıt sayısı, ve bu sayının derinlik arttıkça nasıl büyüdüğü.

İ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