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-র হিসাব।
আগে এটা বুঝি
গত লেসনের শেষ হিসাবটা মনে করুন: প্রতিটা 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-ই হবে না।
- Memtable: write ঢোকে in-memory sorted structure-এ (skiplist/dict)। Durability-র জন্য প্রতিটা write আগে WAL- এ append — sequential! (WAL-ARIES লেসনে পূর্ণ গল্প।)
- Flush: memtable limit ছুঁলে পুরোটা sorted অবস্থায় এক নতুন file- এ ঢালা হয় — SSTable (sorted string table)। একবার লেখা, আর কখনো বদলায় না — immutable। Sequential write-ই শেষ।
- 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-tiered | Leveled | |
|---|---|---|
| ভাবনা | সম-মাপের run-রা পরস্পরে merge | level-প্রতি আকার-ceiling (×T), run overlap-free |
| Layout L2-এ | একাধিক overlapping run | T-টা non-overlapping run |
| Write amp | কম (কম ঘনঘন rewrite) | বেশি (প্রতি level-এ rewrite) |
| Read amp | বেশি (অনেক run ছুঁতে হয়) | কম (level-এ সর্বোচ্চ ১ run) |
| Space amp | বেশি (duplicate জমে) | কম |
| ব্যবহার | Cassandra STCS, RocksDB Universal | LevelDB/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) | LSM | sequential append, memtable absorb |
| Point/range read-heavy OLTP | B+ tree | predictable ৩–৪ page, warm-cache sub-ms |
| বড় scan analytics | প্রায় সমান | দুজনেই sequential; columnar-রা আলাদা গল্প |
| p99 latency-sensitive mixed | B+ tree | compaction-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 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নিজে চালিয়ে দেখুন
Un-compacted read-decay আর bloom-filter FPR — দুটো নিজের চোখে
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-র ধনুক-বক্ররেখা অনুসরণ করে।
নিজে বানান
MiniLSM — memtable, SSTable flush, k-way newest-wins compaction, WAF + latency benchmark
- memtable dict; limit-এ sorted items = নতুন immutable table (newest-first list)
- put = memtable write; delete = tombstone put
- get: memtable → tables newest→oldest; tombstone মানে নিশ্চিত-absent
- compact: flush + heapq.merge over (key, rank, value) streams — rank=newest
- first-per-key wins, tombstone নিচতলায় drop; result = এক immutable run
- 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 এখানেই।
বুঝেছেন কি না দেখুন
1LSM-এর এক write-এর পুরো পথ আর read-এর প্রথম দুই stop — ক্রম অনুযায়ী?
স্মরণ
Write: WAL append (durability) → memtable insert → (full) flush = immutable SSTable। Read: memtable → newest SSTable → … → oldest; প্রতি table-এ bloom-gate, প্রথম match-এই থামা।
2Size 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।
3Tombstone 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, সারণি-বদ্ধ।
আরও পড়ুন
- "The Log-Structured Merge-Tree (LSM-Tree)" (Acta Informatica, 1996) — Patrick O'Neil, Edward Cheng, Dieter Gawlick, Elizabeth O'Neil · মূল পেপার — memtable, multi-level runs আর cost model-এর জন্মসনদ
- RocksDB Wiki — Compaction — Facebook/Meta RocksDB team · Leveled আর universal (tiered) compaction-র production-grade বর্ণনা
- Apache Cassandra Documentation — Operating: Compaction — The Apache Cassandra project · STCS বনাম LCS-র বাস্তব trade-off — tiered-vs-leveled-এর field report