Ders 05 / 19
Zaman Serisi Depoları
Zaman damgalı ölçüm verisinin iki yerleşimde saklanmasının bedeli: satır başına tam damga ile bölüt başına taban damga, fark kodlaması ve blok sıkıştırmanın saklanan bayt farkı, aralık sorgusunun taradığı bölüt ve giriş sayısının aralık daraldıkça tersine dönmesi, saklama politikasının bölüt sınırına hizalandığında dokunulan girişi sıfıra indirmesi.
İçindekiler
Buraya kadarki dört ailede kaydın kendisi bir varlıktı — bir kitap, bir üye, bir ödünç — ve en son ele alınan model bağın kendisini birinci sınıf nesne yaptı. Kütüphanenin verisinin bir bölümü ise ne varlıktır ne bağ: raf bölgelerinin sıcaklığı, turnikenin saydığı kişi, ödünç sayacının değeri. Bunlar zaman serisidir (time series) — bir kaynağa ait, zaman damgalı, sırayla gelen ölçümler.
Bu verinin yük profili öteki ailelerinkine benzemez. Yazma yalnız sona eklemedir; geçmiş bir ölçüm neredeyse hiç güncellenmez. Okuma tekil kayda değil aralığa gelir. Ve verinin bir son kullanma tarihi vardır: üç ay önceki dakikalık ölçüm hiçbir soruya girmez. Ders bu üç özelliğin depo tarafındaki karşılığını ölçüyor: yerleşimin saklanan bayta etkisi, aralık sorgusunun taradığı birim, saklama politikasının bedeli.
İki Yerleşim, Aynı Ölçümler
Ölçüm satırı küçüktür: bir kaynak kimliği, bir zaman damgası ve tek bir sayı — damga, yanında taşıdığı değerden büyük olabilir. Zaman serisi deposunun ilk kararı buradan gelir: ölçümler tek tek değil, bölütler (segment) hâlinde saklanır; her bölüt bir kaynağın bir zaman kutusudur, taban damgasını bir kez yazar, geri kalanı fark olarak tutar.
NS25: on iki raf bölgesinde birer sıcaklık ölçeri vardır, her biri 60 saniyede bir ölçüm yazar ve otuz günlük veri incelenir. NS26: satır yerleşiminde bir ölçüm 2 bayt kaynak kimliği, 8 bayt zaman damgası ve 8 bayt değer taşır. NS6: bölüt birimi bir kaynak–gün çiftidir.
// zaman/yerlesim.mjs — ayni olcum dizisi iki yerlesimde seriliyor: (a) satir basina tam // zaman damgasi + deger, (b) bolut basina taban damga + fark kodlamasi + blok sikistirma. // Sayilan sey saklanan bayttir; sikistirma gercekten calistirilir, tohum gorunurdur. import { deflateSync } from "node:zlib"; const SERI = 12, GUN = 30, ARALIK = 60_000, T0 = Date.UTC(2024, 5, 1); const NOKTA = 86_400_000 / ARALIK; // bolut basina giris: gunluk olcum sayisi let tohum = 20240601; // gorunur tohum; uretec kendi yazildi const gurultu = () => ((tohum = (tohum * 1103515245 + 12345) % 2147483648) / 2147483648 - 0.5); const olcum = (s, i) => // ondalik derece cinsinden tamsayi: 18,4 -> 184 Math.round((18 + s * 0.25 + 3 * Math.sin((i / NOKTA) * 2 * Math.PI) + gurultu() * 0.6) * 10); const varint = (n) => { const b = []; let x = ((n << 1) ^ (n >> 31)) >>> 0; // zigzag + varint do { let y = x & 0x7f; x >>>= 7; if (x) y |= 0x80; b.push(y); } while (x); return b; }; const satir = Buffer.alloc(SERI * GUN * NOKTA * 18); // 2 bayt seri + 8 damga + 8 deger let ofset = 0, hamBayt = 0, bolutBayt = 0, bolutSayisi = 0; for (let s = 0; s < SERI; s += 1) for (let g = 0; g < GUN; g += 1) { const govde = []; // bolut govdesi: farklarin varinti let oncekiFark = 0, oncekiDeger = null; for (let i = 0; i < NOKTA; i += 1) { const t = T0 + g * 86_400_000 + i * ARALIK, d = olcum(s, g * NOKTA + i); satir.writeUInt16LE(s, ofset); satir.writeDoubleLE(t, ofset + 2); satir.writeDoubleLE(d / 10, ofset + 10); ofset += 18; if (oncekiDeger === null) { govde.push(...varint(d)); oncekiDeger = d; continue; } govde.push(...varint(ARALIK - oncekiFark)); oncekiFark = ARALIK; // damga farkinin farki govde.push(...varint(d - oncekiDeger)); oncekiDeger = d; // deger farki } const bas = Buffer.alloc(14); // taban damga + seri + giris sayisi bas.writeDoubleLE(T0 + g * 86_400_000, 0); bas.writeUInt16LE(s, 8); bas.writeUInt32LE(NOKTA, 10); hamBayt += bas.length + govde.length; bolutBayt += bas.length + deflateSync(Buffer.from(govde)).length; bolutSayisi += 1; } const N = SERI * GUN * NOKTA; console.log(`${SERI} seri x ${GUN} gun x ${NOKTA} olcum = ${N} olcum; ` + `${bolutSayisi} bolut, bolut basina ${NOKTA} giris`); console.log(`\n${"yerlesim".padEnd(34)}${"bayt".padStart(11)}${"olcum basina".padStart(14)}${"kat".padStart(7)}`); for (const [ad, b] of [["satir basina tam damga + deger", satir.length], ["bolut, yalniz fark kodlamasi", hamBayt], ["bolut, fark + blok sikistirma", bolutBayt]]) console.log(ad.padEnd(34) + String(b).padStart(11) + (b / N).toFixed(2).padStart(14) + (satir.length / b).toFixed(2).padStart(7));
12 seri x 30 gun x 1440 olcum = 518400 olcum; 360 bolut, bolut basina 1440 giris yerlesim bayt olcum basina kat satir basina tam damga + deger 9331200 18.00 1.00 bolut, yalniz fark kodlamasi 1042560 2.01 8.95 bolut, fark + blok sikistirma 329013 0.63 28.36
Sayılar ölçüm sınıfındadır: iki yerleşim de gerçekten serildi, sıkıştırma gerçekten çalıştırıldı.
Aynı 518.400 ölçüm satır yerleşiminde 9.331.200 bayt, bölütlenmiş yerleşimde 329.013 bayttır: ölçüm başına 18 bayta karşı 0,63 bayt, yirmi sekiz kat. Ara satır kazancın nereden geldiğini ayırıyor. Yalnız fark kodlaması, blok sıkıştırma olmadan, ölçüm başına 2,01 bayta iniyor — dokuz kat. Nedeni verinin biçimidir: ölçüm aralığı sabit olduğu için ardışık damgaların farkı hep aynıdır, farkın farkı sıfırdır ve tek baytla yazılır; değer farkı da küçük bir tamsayıdır. Sekiz baytlık damga ve sekiz baytlık değer, iki bayta düşer. Blok sıkıştırma bunun üstüne üç kat daha ekliyor: tekrarlayan sıfır ve küçük fark dizisi sıkıştırıcının en verimli çalıştığı girdidir.
Bedel aynı kararın içindedir. Bölüt sıkıştırılmış bir bloktur; içindeki tek bir ölçüme bölütün tamamı çözülmeden erişilemez — erişimin birimi ölçüm değil, bölüttür. Kazanç da verinin sıralı ve düzenli aralıklı olmasına bağlıdır: ölçümler gecikmeli ve karışık sırada gelirse damga farkının farkı sıfır olmaz ve bölüt şişer.
Zaman Aralığı Sorgusu
Bu ailenin sorguları hemen her zaman bir zaman aralığı taşır. Satır yerleşimi zamana göre sıralı tek dizidir: aralığın başı ikili aramayla bulunur, ama pencere içindeki bütün kaynakların ölçümleri okunur. Bölüt yerleşiminde üstveri (kaynak kimliği ve taban damga) sorguyla kesişmeyen bölütleri hiç açmadan eler.
// zaman/aralik.mjs — ayni olcum kumesi iki yerlesimde sorgulaniyor. Satir yerlesimi zamana // gore sirali tek dizidir: aralik ikili aramayla bulunur, pencere icindeki butun seriler // taranir. Bolut yerlesiminde ustveri (seri, taban damga) eleme yapar, ama acilan bolut // butunuyle cozulur. Sayilar kosumdan bagimsizdir. const SERI = 12, GUN = 30, ARALIK = 60_000, T0 = Date.UTC(2024, 5, 1), NOKTA = 1440; const seri = [], damga = []; // satir yerlesimi: zamana gore sirali for (let g = 0; g < GUN; g += 1) for (let i = 0; i < NOKTA; i += 1) for (let s = 0; s < SERI; s += 1) { seri.push(s); damga.push(T0 + g * 86_400_000 + i * ARALIK); } const bolut = []; // bolut yerlesimi: bolut = (seri, gun) for (let s = 0; s < SERI; s += 1) for (let g = 0; g < GUN; g += 1) bolut.push({ seri: s, taban: T0 + g * 86_400_000, son: T0 + (g + 1) * 86_400_000, giris: NOKTA }); function satirSorgu(s, alt, ust) { // ikili arama + pencere taramasi let a = 0, b = damga.length; while (a < b) { const o = (a + b) >> 1; if (damga[o] < alt) a = o + 1; else b = o; } let taranan = 0, sonuc = 0; for (let i = a; i < damga.length && damga[i] < ust; i += 1) { taranan += 1; if (s === null || seri[i] === s) sonuc += 1; } return { birim: 0, taranan, sonuc }; } function bolutSorgu(s, alt, ust) { // ustveriyle eleme, sonra bolutu ac let acilan = 0, taranan = 0, sonuc = 0; for (const b of bolut) { if (s !== null && b.seri !== s) continue; if (b.son <= alt || b.taban >= ust) continue; acilan += 1; taranan += b.giris; // bolut butunuyle cozulur for (let i = 0; i < b.giris; i += 1) { const t = b.taban + i * ARALIK; if (t >= alt && t < ust) sonuc += 1; } } return { birim: acilan, taranan, sonuc }; } const G12 = T0 + 11 * 86_400_000; const sorgular = [["3 nolu raf, 12 Haziran (tam gun)", 3, G12, G12 + 86_400_000], ["butun raflar, 12-14 Haziran", null, G12, G12 + 3 * 86_400_000], ["3 nolu raf, 12 Haziran 09:00-10:00", 3, G12 + 9 * 3_600_000, G12 + 10 * 3_600_000], ["3 nolu raf, butun ay", 3, T0, T0 + GUN * 86_400_000]]; console.log(`satir yerlesimi ${damga.length} giris; bolut yerlesimi ${bolut.length} bolut`); console.log(`\n${"sorgu".padEnd(35)}${"satir: taranan".padStart(15)}${"bolut".padStart(7)}` + `${"bolut: taranan".padStart(15)}${"sonuc".padStart(8)}`); for (const [ad, s, alt, ust] of sorgular) { const x = satirSorgu(s, alt, ust), y = bolutSorgu(s, alt, ust); if (x.sonuc !== y.sonuc) throw new Error("iki yol ayni sonucu vermedi"); console.log(ad.padEnd(35) + String(x.taranan).padStart(15) + String(y.birim).padStart(7) + String(y.taranan).padStart(15) + String(x.sonuc).padStart(8)); }
satir yerlesimi 518400 giris; bolut yerlesimi 360 bolut sorgu satir: taranan bolut bolut: taranan sonuc 3 nolu raf, 12 Haziran (tam gun) 17280 1 1440 1440 butun raflar, 12-14 Haziran 51840 36 51840 51840 3 nolu raf, 12 Haziran 09:00-10:00 720 1 1440 60 3 nolu raf, butun ay 518400 30 43200 43200
Dört satırın üçü bölüt yerleşiminin lehinedir; modelin sınırı dördüncüsündedir.
Tek bir rafın bir günü satır yerleşiminde 17.280 giriş taratıyor, bölüt yerleşiminde tek bölüt açılıyor ve 1.440 giriş okunuyor. Oran tam olarak kaynak sayısıdır: sıralı dizide bir rafın ölçümleri on iki rafınkiyle iç içe durur ve aralık daraltılsa da o iç içelik kalır. Aynı rafın bütün ayında katsayı korunuyor: 518.400 giriş taramaya karşı 30 bölüt ve 43.200 giriş.
İkinci satır bölütlemenin hiçbir şey kazandırmadığı durumu gösteriyor: sorgu bütün rafları istediğinde eleyecek üstveri kalmaz, iki yol da 51.840 giriş okur ve bölüt yoluna yalnız 36 bölüt açma işi eklenir.
Üçüncü satır bedeli taşıyor. Bir saatlik pencere satır yerleşiminde 720 giriş taratıyor; bölüt yerleşiminde aynı soru 1.440 giriş çözdürüyor, çünkü açılan bölüt bir günlüktür ve sıkıştırılmış blok kısmen çözülemez. Sonuç iki yolda da 60 ölçümdür. Bölütün süresi, aralık sorgusunun en küçük okuma birimidir: bölüt büyüdükçe sıkıştırma kazancı artar, dar aralık sorgusunun israfı da artar. Dar aralıklar sık soruluyorsa karşılığı ölçümü daha kaba çözünürlükte ikinci kez saklamaktır — bu kararın adı seyreltmedir ve Vaka Çalışmaları kursunda ölçülmüştür.
Saklama Politikası
Zaman serisi verisinin ötekilerden ayrıldığı son nokta silinmesidir. Bir kitap kaydı silinmez; bir dakikalık ölçüm belirli bir yaştan sonra hiçbir soruya girmez ve saklama (retention) politikası onu düşürür. Soru bu düşürmenin neye mal olduğudur. NS27: saklama süresi yedi gündür.
// zaman/saklama.mjs — saklama politikasinin iki yerlesimde uygulanmasi. Satir yerlesiminde // suresi dolan her kayit tek tek dusurulur. Bolut yerlesiminde tamami suresi dolmus bolut // birim olarak dusurulur; yalniz siniri kesen bolut yeniden yazilir. Sayilar kosumdan // bagimsizdir. const SERI = 12, GUN = 30, ARALIK = 60_000, T0 = Date.UTC(2024, 5, 1), NOKTA = 1440; const SIMDI = T0 + GUN * 86_400_000; const damga = []; // satir yerlesimi: zamana gore sirali for (let g = 0; g < GUN; g += 1) for (let i = 0; i < NOKTA; i += 1) for (let s = 0; s < SERI; s += 1) damga.push(T0 + g * 86_400_000 + i * ARALIK); const bolut = []; for (let s = 0; s < SERI; s += 1) for (let g = 0; g < GUN; g += 1) bolut.push({ taban: T0 + g * 86_400_000, son: T0 + (g + 1) * 86_400_000, giris: NOKTA }); function satirSaklama(sinir) { // her kayit tek tek dusurulur let dokunulan = 0, dusen = 0; for (const t of damga) { dokunulan += 1; if (t < sinir) dusen += 1; else break; } return { birim: "kayit", dokunulan, dusen, kalan: damga.length - dusen }; } function bolutSaklama(sinir) { // ustveri okunur, bolut birim olarak duser let dokunulan = 0, dusen = 0, yeniden = 0; for (const b of bolut) { if (b.son <= sinir) { dusen += b.giris; continue; } // hicbir giris acilmaz if (b.taban >= sinir) continue; dokunulan += b.giris; // siniri kesen bolut for (let i = 0; i < b.giris; i += 1) if (b.taban + i * ARALIK < sinir) dusen += 1; else yeniden += 1; } return { birim: "bolut", dokunulan, dusen, kalan: SERI * GUN * NOKTA - dusen, yeniden }; } console.log(`${damga.length} olcum, ${bolut.length} bolut, bolut basina ${NOKTA} giris`); console.log(`\n${"saklama".padEnd(22)}${"yerlesim".padEnd(9)}${"dokunulan giris".padStart(16)}` + `${"dusen kayit".padStart(13)}${"kalan".padStart(8)}`); for (const [ad, sure] of [["7 gun (bolut siniri)", 7 * 86_400_000], ["7 gun 12 saat", 7.5 * 86_400_000]]) { const sinir = SIMDI - sure; for (const r of [satirSaklama(sinir), bolutSaklama(sinir)]) console.log(ad.padEnd(22) + r.birim.padEnd(9) + String(r.dokunulan).padStart(16) + String(r.dusen).padStart(13) + String(r.kalan).padStart(8)); } const y = bolutSaklama(SIMDI - 7.5 * 86_400_000); console.log(`\nsiniri kesen bolutler: ${y.dokunulan / NOKTA} bolut acildi, ` + `${y.yeniden} giris yeniden yazildi`);
518400 olcum, 360 bolut, bolut basina 1440 giris saklama yerlesim dokunulan giris dusen kayit kalan 7 gun (bolut siniri) kayit 397441 397440 120960 7 gun (bolut siniri) bolut 0 397440 120960 7 gun 12 saat kayit 388801 388800 129600 7 gun 12 saat bolut 17280 388800 129600 siniri kesen bolutler: 12 bolut acildi, 8640 giris yeniden yazildi
Birinci çift bölütlemenin en keskin kazancıdır. İki yerleşim de aynı 397.440 kaydı düşürüp arkasında aynı 120.960 kaydı bırakıyor, ama satır yerleşimi bunun için 397.441 girişe dokunuyor: düşen her kayda bir kez, artı sınırı gösteren bir kayda daha. Bölüt yerleşiminde dokunulan giriş sıfırdır — yalnız 360 bölüt üstverisi okunur ve süresi dolmuş 276 bölüt (397.440 ÷ 1.440, hesap) dosya olarak düşürülür. Silme burada veri işlemi değil, dosya işlemidir.
Sıfır, saklama sınırının bölüt sınırına denk gelmesine bağlıdır. İkinci çift bunu bozuyor: yedi buçuk günlük saklamada sınır bölütlerin ortasından geçer, on iki bölüt açılır, 17.280 girişe dokunulur ve bunun 8.640’ı bölütler yeniden yazıldığı için ikinci kez saklanır. Düşen kayıt sayısı gene iki yolda aynıdır; değişen yalnız iş miktarıdır. Kural şudur: saklama süresi bölüt süresinin katı seçildiğinde silme bedeli sıfıra iner; seçilmediğinde bedel, sınırı kesen bölüt sayısı çarpı bölüt boyudur.
Bölüt süresi kararının üçüncü yüzü budur: bölüt büyüdükçe saklanan bayt düşer, dar aralık sorgusunun israfı ve saklama sınırındaki yeniden yazma artar. Üçünü birden en iyileyen bir değer yoktur.
Özet
- Zaman serisi verisi sona eklemeli yazılır, aralıkla okunur ve yaşlandığında düşürülür; depo bu üç özelliği veri modelinin içine alır.
- Satır başına tam damga ve değer 18 bayt tutarken, taban damga + fark kodlaması 2,01 bayta, blok sıkıştırma eklendiğinde 0,63 bayta iniyor — 9.331.200 bayta karşı 329.013 bayt, 28,4 kat. Kazanç verinin sıralı ve düzenli aralıklı olmasına bağlıdır; bedeli, erişim biriminin ölçüm değil bölüt olmasıdır.
- Bölüt üstverisi aralık sorgusunda eleme yapar: bir rafın bir günü 17.280 giriş taraması yerine tek bölüt ve 1.440 giriştir. Sorgu bütün kaynakları istediğinde eleme yoktur, iki yol da 51.840 giriş okur.
- Bölüt süresi en küçük okuma birimidir: bir saatlik pencere satır yerleşiminde 720 giriş, bölüt yerleşiminde 1.440 giriş çözdürür ve sonuç 60 ölçümdür.
- Saklama sınırı bölüt sınırına denk geldiğinde 397.440 kayıt hiçbir girişe dokunulmadan, 276 bölüt dosya olarak düşürülerek silinir; denk gelmediğinde 12 bölüt açılır, 17.280 girişe dokunulur, 8.640 giriş yeniden yazılır.
Sonraki Adım
Beş aile de aynı sırayla anlatıldı: modelin ayırt edici mekaniği, ucuzlattığı ve pahalılaştırdığı sorgu sınıfları. Ortaya çıkan tablo bir sıralama değildir — her aile bir erişim örüntüsünü ucuzlatmak için başkasını pahalılaştırdı ve her ölçümde ters yönlü satır da göründü.
Geriye karar kalıyor: eldeki iş için hangi aile. Sonraki ders bu soruyu tercihle değil ölçütle karşılıyor — erişim örüntüsünün hangi özelliği hangi aileye işaret eder, aynı iş yükü iki ailede koşturulduğunda fark hangi eşikte anlam kazanır, ve bir aile yanlış seçildiğinde bedel nerede görünür.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.