Foundationপ্রথম নীতি থেকে
LEVEL 8লেসন ৪/১৯রিসার্চ-লেভেল১ ঘণ্টা

LSM Tree ও SSTable — write-optimized-এর বিপ্লব

LSM-Trees and SSTables

প্রতিটা INSERT-এ random page-write নয় — লিখুন memory-তে, flush করুন immutable sorted file-এ, background-এ merge করুন: LSM-tree-র বিপ্লব, compaction-এর দুই school, amplification ত্রিভুজ, bloom filter আর tombstone-র হিসাব।

এই লেসন শেষে আপনি পারবেন

  • B+ tree-র random-write bill থেকে শুরু করে LSM-এর memtable → immutable SSTable → compaction pipeline আঁকতে ও trace করতে পারবেন
  • Size-tiered আর leveled compaction-এর পার্থক্য ব্যাখ্যা করে write/read/space amplification-র ত্রিভুজে কোন strategy কোথায় দাঁড়ায় বলতে পারবেন
  • Bloom filter-এর false-positive math (bits-per-key, optimal hash-count) দিয়ে point-lookup-এর খরচ হিসাব করতে পারবেন
  • Tombstone-এর জীবনচক্র trace করে বলতে পারবেন delete কেন তৎক্ষণাৎ জায়গা ফেরত দেয় না, আর read path কীভাবে একে নিশ্চিত-absent হিসেবে মানে
  • Workload দেখে LSM বনাম B+ tree সিদ্ধান্ত নিতে পারবেন — write-heavy ingest বনাম read-heavy point/range-এর economics মিলিয়ে

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

গত লেসনের শেষ হিসাবটা মনে করুন: প্রতিটা INSERT প্রতিটা index-এ random leaf-write দেয়। OLTP-তে চলে — page গরম থাকে। কিন্তু এবার ভাবুন সেন্সর- জগৎ: সেকেন্ডে ৫ লাখ event, প্রতি event-এ ২টা index, SSD-তেও প্রতি random write ~১০০ µs — গণিতটা ভেঙে পড়ে। এই অধ্যায়ের বিপ্লবী উত্তর: লেখো memory-তে, ঢালো immutable file-এ, মিশাও background-এ — LSM-tree, যার ওপরে দাঁড়িয়ে আছে LevelDB, RocksDB, Cassandra, HBase, TiKV।

আজকের প্রশ্ন: কোন অসুবিধাটা এত বড় ছিল যে ইঞ্জিনিয়াররা sorted-file-ইউনিয়ন বানাল? Read path কীভাবে দশেক file ঘুরে তবু দ্রুত থাকে? আর ঠিক কখন LSM বাছবেন, কখন আমাদের পুরনো বন্ধু B+ tree?

মূল ধারণা

B+ tree-র দামি চুক্তি

B+ tree insert = leaf-page খোঁজা (৩–৪ read) + leaf modify (+ split হলে parent-chain-ও) — সবই random page-write, প্রতিটার পেছনে WAL record। Ingest-heavy workload-এ এই random-write-ই ceiling। Write amplification (WA) মানে: logical ১ byte-এর জন্য disk-এ কত byte গেল — index-entry, split-rewrite, WAL সব মিলিয়ে।

মূল চাল — sequential-এ রূপান্তর

LSM-এর প্রতিশ্রুতি: disk-এ কখনো random write-ই হবে না।

  1. Memtable: write ঢোকে in-memory sorted structure-এ (skiplist/dict)। Durability-র জন্য প্রতিটা write আগে WAL- এ append — sequential! (WAL-ARIES লেসনে পূর্ণ গল্প।)
  2. Flush: memtable limit ছুঁলে পুরোটা sorted অবস্থায় এক নতুন file- এ ঢালা হয় — SSTable (sorted string table)। একবার লেখা, আর কখনো বদলায় না — immutable। Sequential write-ই শেষ।
  3. Compaction: file জমে গেলে background-এ k-way merge — নতুন version জেতে, পুরনো ফেলে যায়। এটাও sequential bulk I/O।

Immutable-ity-ই জাদু: file যখন কখনো update হয় না, page-allocation থেকে locking পর্যন্ত সব সরল হয়ে যায়।

SSTable — গুছানো, জন্মস্থির

প্রতিটা SSTable = sorted key-value pair-দের block-সারি + sparse block-index (প্রতি block-এ প্রথম key)। খোঁজা = index-এ block বের করা + block-এর ভেতরে binary search। যেহেতু কখনো mutate হয় না, index/block checksum একবার লিখলেই চিরকালের জন্য নিরাপদ — OS pagecache-ও নিশ্চিন্তে cache করে।

Read path — বিপ্লবের বিল

get(k)-এর যাত্রা: memtable → সবচেয়ে নতুন SSTable → … → পুরনোতম। Key যে table-এ প্রথম পাওয়া গেল, সেটাই newest version — থামুন। কিন্তু absent key-র জন্য প্রতিটা table check করতেই হয়! ৫০-table-এ ৫০ binary search — এই হলো read amplification (RA)।

প্রতিষেধ দুই স্তরে: প্রতি SSTable-এ একটা bloom filter — “এই key এই file-এ সম্ভবত আছে / নিশ্চিত নেই” (hash-tables লেসনের probability-সুদ)। “নিশ্চিত নেই” বললেই file skip — এক query-ও নষ্ট হয় না। সাথে per-level key-range hint (“এই file-এ key ১০০–২০০”)।

Compaction — দুই school

Size-tieredLeveled
ভাবনাসম-মাপের run-রা পরস্পরে mergelevel-প্রতি আকার-ceiling (×T), run overlap-free
Layout L2-এএকাধিক overlapping runT-টা non-overlapping run
Write ampকম (কম ঘনঘন rewrite)বেশি (প্রতি level-এ rewrite)
Read ampবেশি (অনেক run ছুঁতে হয়)কম (level-এ সর্বোচ্চ ১ run)
Space ampবেশি (duplicate জমে)কম
ব্যবহারCassandra STCS, RocksDB UniversalLevelDB/RocksDB default, Cassandra LCS

Leveled-এ T (size ratio, সাধারণত ১০): L0-এর fresh runs L1-এ merge, L1 ভরলে এক run L2-তে নামে… L6 পর্যন্ত। মোট আকার ১০⁶× memtable পর্যন্ত — ১৬ MB memtable × ১০⁶ = ১৬ TB!

Amplification ত্রিভুজ

Write, read, space — তিনের একটা কমালে আরেকটা বাড়ে (RUM conjecture)। Tiered: write-ফ্রেন্ডলি, read/space-ছাড়। Leveled: read/space- শৃঙ্খলিত, write-ছাড়। কোনো magic configuration নেই — workload-ই voter।

Tombstone — অদৃশ্য কবর

Delete মানে value মোছা না — memtable/SSTable-এ এক tombstone marker ঢোকা। Read path-এ tombstone পাওয়া মাত্র উত্তর “নিশ্চিত-absent” (নিচের পুরনো table-এ value থাকলেও!)। আসল মুছতে হয় compaction-এ, তবু সাবধান: upper-level-এ tombstone থাকলে নিচে shadowed data এখনো বেঁচে — তাই marker নিচতলা পর্যন্ত যেতে হয়, নইলে পুরনো value পুনরুত্থান করবে।

LSM বনাম B+ tree — সিদ্ধান্ত-টেবিল

Workloadবিজয়ীকারণ
Write-heavy ingest (log, metric, queue)LSMsequential append, memtable absorb
Point/range read-heavy OLTPB+ treepredictable ৩–৪ page, warm-cache sub-ms
বড় scan analyticsপ্রায় সমানদুজনেই sequential; columnar-রা আলাদা গল্প
p99 latency-sensitive mixedB+ treecompaction-stall-এর tail-risk নেই

ভেতরে কী ঘটছে

Flush-cycle-র সংখ্যা

RocksDB-ধাঁচ: memtable 64 MB, L1 256 MB, প্রতি level ×10 → L6 = 256 GB। Fresh flush L0-তে; L0-এ ৪টা run হলে merge-down L1-এ; L1 ভরলে এক run L2- তে… এক key-র গড় যাত্রা: প্রতি level-এ একবার লেখা → leveled WA ≈ levels × T ÷ (T−1) ≈ ১০–২০× (byte-in প্রতি), tiered-এ ছোট levels-এ কম কিন্তু বড়গুলোতে হু-হু করে বাড়ে। এই সংখ্যাগুলোই production tuning-এর dashboard।

Bloom filter হাতে-গোনা

m bit/key, k hash: FPR ≈ (1 − e^(−k·n/m))^k; optimal k = ln2 · (m/n)। 10 bits/key → k=7 → FPR ~০.৮%; memory ১.২৫ MB প্রতি লাখ key। ৫০-table read path-এ ভাবুন: FPR ১% মানে গড়ে ০.৫টা বৃথা file-touch — ছাড়া হলে ২৫টা! Experiment-এ measured-vs-theory পাশাপাশি দেখবেন।

Postgres কেন LSM নয়

পরিবারের অন্যরা

ClickHouse-র MergeTree, Kafka-র log segment, Parquet row-group — সবাই একই সূত্রের ভাষান্তর: immutable sorted/batched file + background merge। Log-structured ভাবনা storage-stack-এর সর্বত্র।

উদাহরণ

হাতে-টেনে এক flush-cycle

Stream: put(a,1) put(b,2) put(a,3) delete(b) → memtable full → flush → T1; তারপর put(c,4) put(a,7) → flush → T2:

memtable পরে T1: [(a,3), (b,TOMB)]      ← a overwrite, b tombstone
memtable পরে T2: [(a,7), (c,4)]

get(a): memtable? না → T2 (newest): (a,7) ✓ থামুন — T1 দেখাই দরকার নেই
get(b): memtable? না → T2: নেই → T1: (b,TOMB) → উত্তর None (নিশ্চিত!)
get(c): memtable? না → T2: (c,4) ✓

এবার compaction (T2+T1 → M): k-way merge, key-প্রতি newest-wins —

merge order: (a,rank0,7) (a,rank1,3) (b,rank1,TOMB) (c,rank0,4)
M: [(a,7), (c,4)]          ← a-র পুরনো 3 বাদ; b-র tombstone+shadow সব বাদ

get(b) এখন: একটাই table, নেই → None — আগের চেয়ে সস্তা, উত্তর এক

লক্ষ্য করুন: compaction-এর পরে read সস্তা হলো, কিন্তু খরচটা আগেই গেছে — entries দুইবার লেখা হয়েছে। এটাই WAF-এর জন্ম।

ছবিতে

 WRITE:  put(k,v) ──► WAL append ──► MEMTABLE (sorted)
                                        │ full
                                        ▼ flush (sequential)
        L0: [run4][run3][run2][run1]   ← overlapping, newest ডানে
                                        │ compaction
        L1: [────── run ──────]        ← non-overlapping
                                        │ ▼
        L2: [────────── run ──────────]   size ×10 প্রতি level
                                       ...
 READ:  get(k) ─► memtable ─► L0 runs (newest→old)
                    │ প্রতি run-এ: bloom(k)? ── না ──► skip
                    ▼ হ্যাঁ                          ▼
                 block-index → block → binary search → found? return
LSM pipeline — write বাঁয়ে sequential, read দিয়ে bloom-gate পেরিয়ে:
 SIZE-TIERED (L3):      [a--z][b--y][c--f]      ৩ run overlap করে —
                         ▲ পরের merge-এ তিনটাই একসাথে  read-এ তিনটাই ছোঁয়া

 LEVELED   (L3):       [ac][df][gj][mo][pr][sz]  পাশাপাশি, overlap-শূন্য —
                        এক key = ঠিক এক run = এক binary search
Tiered বনাম leveled — একই data, দুই নকশা (overlap-ই সব):

নিজে চালিয়ে দেখুন

EXPERIMENT

Un-compacted read-decay আর bloom-filter FPR — দুটো নিজের চোখে

Python 3.8+· ২০ মিনিট
import bisect
import math
import random
import time
import zlib

# ---------- Part A: flush-cycle স্থির write, ক্ষয়িষ্ণু read ----------
FLUSH_EVERY, N_ROWS, N_PROBE = 20_000, 200_000, 3_000
random.seed(7)
memtable, tables = {}, []


def put(k):
    memtable[k] = 'payload'
    if len(memtable) >= FLUSH_EVERY:
        tables.insert(0, sorted(memtable.items()))   # immutable run
        memtable.clear()


for i in range(N_ROWS):
    put(f'key:{i}')                                   # unique ascending keys
probe = [f'key:{random.randrange(N_ROWS)}' for _ in range(N_PROBE)]


def avg_read_us(n_tables):
    total = 0.0
    for k in probe:
        t0 = time.perf_counter()
        found = k in memtable
        for ti in range(n_tables):
            row = tables[ti]
            i = bisect.bisect_left(row, (k,))
            if i < len(row) and row[i][0] == k:
                found = True
                break                                 # newest-hit থামে
        total += (time.perf_counter() - t0) * 1e6
    return total / N_PROBE


print(f'{len(tables)} SSTables piled up, zero compaction')
print('tables searched   avg point-read')
for n in (1, 3, 5, 7, 10):
    print(f'{n:>8}          {avg_read_us(n):>8.2f} µs')

# ---------- Part B: bloom filter FPR across bits-per-key ----------
print('\nbloom filter: measured FPR vs theory')
for bpk in (4, 8, 16, 32):
    n, tests = 100_000, 20_000
    m = 1 << ((bpk * n - 1).bit_length())             # কাছের ২-ঘাত
    k = max(1, round(0.693 * bpk))                    # optimal k = ln2·m/n
    bits = bytearray(m >> 3)
    for x in range(n):
        h1 = zlib.crc32(x.to_bytes(4, 'little'))
        h2 = (x * 2654435761) & 0xffffffff
        for i in range(k):
            pos = (h1 + i * h2) & (m - 1)
            bits[pos >> 3] |= 1 << (pos & 7)
    fp = 0
    for x in range(n, n + tests):                     # unseen keys
        h1 = zlib.crc32(x.to_bytes(4, 'little'))
        h2 = (x * 2654435761) & 0xffffffff
        for i in range(k):
            pos = (h1 + i * h2) & (m - 1)
            if not (bits[pos >> 3] >> (pos & 7)) & 1:
                break                                 # নিশ্চিত-absent
        else:
            fp += 1                                   # false positive!
    theo = (1 - math.exp(-k * n / m)) ** k
    print(f'{bpk:>3} bits/key  k={k}  measured={fp / tests * 100:5.2f}%'
          f'  theory={theo * 100:5.2f}%')

মোটামুটি প্রত্যাশিত output (timing machine-ভেদে):

10 SSTables piled up, zero compaction
tables searched   avg point-read
       1             1.27 µs
       3             2.92 µs
       5             4.60 µs
       7             6.49 µs
      10             8.47 µs

bloom filter: measured FPR vs theory
  4 bits/key  k=3  measured= 6.40%  theory= 8.27%
  8 bits/key  k=6  measured= 0.52%  theory= 0.68%
 16 bits/key  k=11  measured= 0.01%  theory= 0.01%
 32 bits/key  k=22  measured= 0.00%  theory= 0.00%

দুইটা পাঠ: read-latency প্রায় রৈখিকভাবে বাড়ছে — প্রতি extra table মানে গড়ে আরেকটা binary search (absent-key তো সবগুলোই scan করে!)। Bloom-এ measured column theory-র ঢাল অনুসরণ করে; bpk=৪-এ ফারাকটা double-hashing- এর হালকা correlation — FPR ছোট হলেই ফারাকও চিড়ের মতো।

নিজে বাড়ান: (১) probe-গুলো hot-set (৯০% same ১০০ key)-এ সরান — hit path থামে বলে decay কীভাবে ভাঙে দেখুন; (২) bpk=6, k=4 জোর করে suboptimal — theory থেকে যায়গা কি না মাপুন।

এটা কী প্রমাণ করে

Compaction না চললে point-read latency table-সংখ্যার সাথে প্রায় রৈখিকভাবে বাড়ে (read amplification-র সংজ্ঞাই), আর bloom filter-এর measured false-positive rate bits-per-key বাড়ালে theory-র ধনুক-বক্ররেখা অনুসরণ করে।

নিজে বানান

BUILD IT

MiniLSM — memtable, SSTable flush, k-way newest-wins compaction, WAF + latency benchmark

Python · ●●●●○
  1. memtable dict; limit-এ sorted items = নতুন immutable table (newest-first list)
  2. put = memtable write; delete = tombstone put
  3. get: memtable → tables newest→oldest; tombstone মানে নিশ্চিত-absent
  4. compact: flush + heapq.merge over (key, rank, value) streams — rank=newest
  5. first-per-key wins, tombstone নিচতলায় drop; result = এক immutable run
  6. benchmark: 100k burst → read percentiles before/after → WAF report
import heapq, random, time
from bisect import bisect_left

TOMBSTONE = object()              # delete-marker — value-র জায়গায় বসে
MEM_LIMIT = 4_000                 # এত entry হলেই flush


class MiniLSM:
    """memtable + immutable SSTable + k-way newest-wins compaction"""

    def __init__(self):
        self.memtable, self.tables = {}, []   # tables newest-first
        self.flushes = self.compactions = 0
        self.entries_written = 0               # WAF-এর numerator

    def put(self, key, val):
        self.memtable[key] = val
        if len(self.memtable) >= MEM_LIMIT:
            self.flush()

    def delete(self, key):
        self.put(key, TOMBSTONE)               # delete-ও একটা write!

    def flush(self):
        if not self.memtable:
            return
        table = sorted(self.memtable.items())  # immutable run — আর বদলায় না
        self.tables.insert(0, table)
        self.entries_written += len(table)
        self.memtable, self.flushes = {}, self.flushes + 1

    def get(self, key):
        if key in self.memtable:               # freshest source
            v = self.memtable[key]
            return None if v is TOMBSTONE else v
        for table in self.tables:              # নতুন → পুরনো
            i = bisect_left(table, (key,))
            if i < len(table) and table[i][0] == key:
                v = table[i][1]
                return None if v is TOMBSTONE else v   # tombstone = নিশ্চিত-absent
        return None

    def _rows(self, tbl, rank):
        """এক table-কে (key, rank, value)-stream-এ — rank ছোট মানে newest"""
        for k, v in tbl:
            yield k, rank, v

    def compact(self):
        """সব run এক merge-এ: newest-wins; tombstone নিচতলায় ফেলে দেয়"""
        self.flush()
        streams = [self._rows(t, r) for r, t in enumerate(self.tables)]
        merged, last_key = [], None
        for key, _rank, v in heapq.merge(*streams):
            if key == last_key:
                continue                       # (k, rank)-sort → newest আগে
            last_key = key
            if v is not TOMBSTONE:
                merged.append((key, v))        # পুরনো version + tombstone বাদ
        self.tables = [merged]
        self.entries_written += len(merged)
        self.compactions += 1


if __name__ == '__main__':
    random.seed(7)
    KEYSPACE = 30_000
    db, ref, deleted = MiniLSM(), {}, set()
    t0 = time.perf_counter()
    for i in range(100_000):
        k, v = f'key:{random.randrange(KEYSPACE)}', f'profile-{i}'
        db.put(k, v); ref[k] = v
    dt = time.perf_counter() - t0
    print(f'write burst : 100,000 puts -> {db.flushes} SSTables '
          f'in {dt:.2f}s ({100_000 / dt:,.0f} ops/s)')
    probe = [f'key:{random.randrange(KEYSPACE)}' for _ in range(2_000)]

    def read_stats():
        lat = []
        for k in probe:
            t0 = time.perf_counter(); db.get(k)
            lat.append((time.perf_counter() - t0) * 1e6)
        lat.sort()
        return lat[len(lat) // 2], lat[int(len(lat) * .99)]

    n_before = len(db.tables)
    med_b, p99_b = read_stats()
    for k in random.sample(sorted(ref), 500):   # 500 delete → tombstones
        db.delete(k); deleted.add(k); ref.pop(k)
    db.compact()                                # newest-wins full merge
    med_a, p99_a = read_stats()
    live = len(db.tables[0])
    assert all(db.get(k) == ref[k] for k in random.sample(sorted(ref), 300))
    assert all(db.get(k) is None for k in list(deleted)[:50])
    print(f'uncompacted : {n_before} tables | p50={med_b:.1f}µs p99={p99_b:.1f}µs')
    print(f'compacted   : 1 table      | p50={med_a:.1f}µs p99={p99_a:.1f}µs')
    print(f'WAF = {db.entries_written / live:.1f}x  '
          f'({db.entries_written:,} written vs {live:,} live)')

প্রত্যাশিত output (timing machine-ভেদে):

write burst : 100,000 puts -> 23 SSTables in 0.21s (468,000 ops/s)
uncompacted : 23 tables | p50=7.9µs p99=45.6µs
compacted   : 1 table      | p50=2.4µs p99=3.4µs
WAF = 4.3x  (122,127 written vs 28,388 live)

তিনটা পাঠ: write throughput বিশাল (সব memory-append); compaction-এর পরে p99 ১৩× কমে গেছে — read-amplification কাটা; কিন্তু WAF ৪.৩× — প্রতিটা live byte গড়ে ৪.৩ বার disk-এ গেছে। এই ত্রিভুজটাই আজকের লেসনের হৃদয়।

নিজে বাড়ান: (১) per-table bloom filter — absent-key probe-এ table-skip দেখুন; (২) size-tiered mode — সম-মাপের run pairwise merge, leveled-এর সাথে WAF/p99 তুলনা; (৩) WAL append-log + crash-recovery (replay করে memtable পুনর্গঠন)।

বাস্তব সিস্টেমে

কারা কোথায় চালায়

  • LevelDB/RocksDB (embedded): Chrome IndexedDB, Android, Bitcoin Core, TiKV/CockroachDB-র নিচে — প্রতিটা laptop-এ আজও LSM ঘুরছে।
  • Cassandra/HBase/ScyllaDB: wide-column store-দের মেরুদণ্ড; STCS/LCS tuning production-এর নিত্যদিন।
  • ClickHouse: MergeTree — classic LSM না, কিন্তু immutable-part + background-merge দর্শনেরই cousin।
  • PostgreSQL: না — read-heavy mixed OLTP-তে nbtree-র predictable latency আর HOT-update-ই বিজয়ী (hood-এর callout)।

যে ভুলগুলো সবাই করে

“LSM সবসময় write-এ B+ tree-কে হারায়।”

Steady ingest-এ হ্যাঁ; কিন্তু small-hot-updates, fsync-bound WAL আর compaction-stall মিললে write-tail-ও খারাপ হয়। Space-pressure-triggered aggressive compaction উল্টে write-throughput ডুবিয়ে দিতে পারে।

“Compaction তো background — ফ্রি লাঞ্চ।”

Background-এর disk-CPU budget-ও একটাই — foreground query-র সাথে I/O ঝগড়া করে; stall হলে write পুরো থামে। Tuning মানে budget-রাজনীতি, মুক্তি নয়।

“Bloom filter false-positive মানে ভুল data return করে।”

FP-র একমাত্র দোষ: একটা অপ্রয়োজনীয় file-check — খরচ, ভুল নয়। “নিশ্চিত নেই” সবসময় সঠিক; “সম্ভবত আছে” শুধু verification বাধ্য করে। Answer-এর correctness-এ ফাটল নেই।

“DELETE দিলে জায়গা তৎক্ষণাৎ ফেরত আসে।”

Tombstone-ই লেখা হয় (extra space!); shadowed value আর marker দুটোই বেঁচে থাকে নিচতলার compaction পর্যন্ত। Marker আটকে গেলে zombie-data + read-penalty — Cassandra-র tombstone-storm war-story এখানেই।

বুঝেছেন কি না দেখুন

1

LSM-এর এক write-এর পুরো পথ আর read-এর প্রথম দুই stop — ক্রম অনুযায়ী?

স্মরণ

Write: WAL append (durability) → memtable insert → (full) flush = immutable SSTable। Read: memtable → newest SSTable → … → oldest; প্রতি table-এ bloom-gate, প্রথম match-এই থামা।

2

Size ratio T=10, memtable flush 64 MB। L1..L5 পর্যন্ত leveled layout-এ প্রতিটা level-এর মোট size আর L3-এ কতগুলো run?

প্রয়োগ

L1=256 MB, L2=2.56 GB, L3=25.6 GB, L4=256 GB, L5=2.56 TB। L3-এ run-size ≈ L2 = 2.56 GB → ২৫.৬/২.৫৬ = ১০টা non-overlapping run — এক key = ঠিক এক run, এই-ই leveled-এর RA=O(level) guarantee।

3

Tombstone upper level-এ পাওয়া মাত্র মুছে দিলে কী বিপদ? নিচতলা পর্যন্ত রাখতে হয় কেন?

যুক্তি

নিচের পুরনো run-এ shadowed value এখনো বেঁচে; marker সরালে সেই value resurrect করে — deleted key ফেরত জীবিত! তাই marker নিচতলায় গিয়ে তবেই data-সহ drop হয়।

4

৫০-table read path, প্রতি table-এ FPR ১%। Bloom-ছাড়া বনাম সহ গড়ে কত file-check বাঁচে, absent-key ধরে?

যুক্তি

ছাড়া: ৫০টা binary search। সহ: গড়ে ৫০×০.০১ = ০.৫টা false-positive check (+ memtable) — ~৪৯টা touch বাঁচে, প্রতিটা = index-read + possibly block-read। এই ৯৮% saving-ই LSM read-কে বাঁচিয়ে রাখে।

5

(A) IoT gateway: ৮০k writes/s, reads শুধু batch analytics; (B) session-store: read-heavy point lookup p99-critical, writes মৃদু। Engine-storage choice?

ডিজাইন

(A) LSM (tiered/universal) — SLA write-throughput; read batch তাই RA সহনীয়, space amp vacuum-window-এ সামলানো যায়। (B) B+ tree — point-read latency/tail king; মৃদু write-এ LSM-এর লাভই নেই, stall-risk অর্থহীন ঝুঁকি। মাঝের খামার (mixed): measurement-driven — আজকের WAF/RA tools-ই মাপকাঠি।

এরপর কী

এরপর কী

B+ tree জিতেছে equality আর range-এ; LSM জিতেছে write-এ। কিন্তু আধুনিক query-র রূপ আরো অদ্ভুত: “এই point-এর ৫ km-এর মধ্যে restaurant?” — “এই jsonb doc-এ tags-এ ‘vegan’ আছে?” — “গত ১০ মিনিটের timestamp rows?” B-tree এসবে অসহায়। পরের লেসনে index-zoo: Hash (equality-only revival), Bitmap (low-cardinality AND/OR), GiST (geometry + nearest-neighbor), GIN (inverted index — jsonb/full-text), BRIN (append-only timestamp-এর তুচ্ছ-footprint জাদু)। শেষে এক selection-flowchart — কোন query-shape-এ কোন index, সারণি-বদ্ধ।

আরও পড়ুন