NoSQL Engine Internals: LSM Trees & SSTables
How modern write-heavy databases like Apache Cassandra, RocksDB, Google Bigtable, and ScyllaDB process 500,000+ writes per second. Master the Write-Ahead Log (WAL), MemTable, immutable SSTables, Bloom Filters, and Compaction strategies.
๐ฅ 1. Why B-Trees Hit a Wall on Heavy Writes
In Module 3.4, we learned that B-Trees are kings of read operations. But for massive write workloads (financial transaction logs, IoT sensor pings, messaging queues), B-Trees suffer from a catastrophic hardware physics bottleneck: Random In-Place Overwrites.
When you update 1 row in a B-Tree, the engine must locate the exact 16 KB page on disk, read it, modify bytes in place, and rewrite the 16 KB block. Updating 100 random rows requires 100 random disk writes across the entire drive!
On both HDDs and NVMe SSDs, Sequential Append Writes are 10x to 100x faster than Random Overwrites. Writing sequentially saturates disk bus bandwidth at peak gigabytes/sec.
๐๏ธ 2. The 4 Core Pillars of an LSM-Tree Storage Engine
When you order at a restaurant, the waiter doesn't run to the back office and re-sort the company ledger alphabetically. They quickly scribble your order on a top notepad in 1 second. At the end of the shift, all notepad pages are sorted and filed away.
An append-only log file on disk. When a write arrives, it is immediately appended to the WAL so that if power fails 1 millisecond later, the data is 100% durable.
An in-memory sorted data structure (usually a SkipList or Red-Black Tree). Writes are stored in RAM in sorted order for fast O(log N) lookups and range scans.
When the MemTable reaches capacity (e.g. 64 MB), it is flushed to disk as an immutable, sorted file called an SSTable. Once written, an SSTable is never modified!
Kept in RAM to accelerate reads. The Bloom Filter instantly confirms if a key definitely does NOT exist in an SSTable, skipping useless disk seeks.
๐ 3. Step-by-Step Architecture: The Write & Read Path
Follow how writes enter the Write-Ahead Log (WAL) and MemTable in RAM, flush to Level 0 SSTables on disk, and undergo background Compaction.
๐ชฆ 4. Deletions & Updates: The Tombstone Pattern
If SSTables on disk are 100% immutable (read-only), how do you delete or update a key without going back and editing old files?
When a user calls delete("user_42"), the database does NOT delete anything from disk! It simply inserts a special marker called a Tombstone:
set("user_42", TOMBSTONE_DELETED)๐งน 5. Compaction Strategies: Size-Tiered vs Leveled Compaction
Without compaction, hundreds of flushed SSTables would pile up on disk. Reading a single key would require searching 50 different files (high Read Amplification). Databases use one of two compaction strategies:
Waits until 4 SSTables of roughly equal size accumulate, then merges them together into 1 larger SSTable.
Divides disk into levels ($L_0, L_1, L_2 \dots$). Each level is 10x larger than the previous. Keys within levels $L_1+$ are strictly non-overlapping!
โ๏ธ 6. Architectural Showdown: B-Tree vs LSM-Tree
| Metric / Attribute | Traditional B-Tree (MySQL InnoDB, Postgres) | LSM-Tree (RocksDB, Cassandra, ScyllaDB) |
|---|---|---|
| Write Speed | Medium (Random page overwrites) | Blazing Fast (Sequential append to WAL + RAM) |
| Point Read Speed | Ultra-Fast (1โ3 direct page reads) | Fast with Bloom Filters (MemTable + SSTables) |
| Storage Fragmentation | High (Internal page holes & padding) | Low (Packed immutable sequential blocks) |
| Write Amplification | High (Rewrites entire 16 KB page for 50-byte change) | Medium-Low (Background compaction merges) |
| Ideal Production Use Case | ACID relational transactions, complex multi-table joins | High-volume writes, time-series, messaging, analytics |
๐ป 7. Low-Level LSM-Tree MemTable & Sparse Index Implementation
Below is the core in-memory MemTable buffering and sparse index lookup implementation:
// Python 3.10+ LSM-Tree Core Engine with WAL and MemTable import os, bisect class LSMStorageEngine: def __init__(self, wal_path: str = "wal.log", max_memtable_size: int = 1000): self.wal_path = wal_path self.max_size = max_memtable_size self.memtable = {} # In-memory sorted buffer (SkipList) self.sstables = [] # List of immutable on-disk SSTables self.wal = open(wal_path, "a", encoding="utf-8") def put(self, key: str, value: str): # 1. Append sequentially to Write-Ahead Log on disk for durability self.wal.write(f"{key}:{value}\n") self.wal.flush() # 2. Insert into sorted in-memory MemTable self.memtable[key] = value # 3. If MemTable exceeds capacity, flush to immutable SSTable on disk if len(self.memtable) >= self.max_size: self.flush_memtable() def get(self, key: str): # 1. Search Active MemTable in RAM (O(1) / O(log N)) if key in self.memtable: val = self.memtable[key] return None if val == "__TOMBSTONE__" else val # 2. Search SSTables from newest to oldest for sstable in reversed(self.sstables): if sstable.bloom_filter.contains(key): val = sstable.search(key) if val is not None: return None if val == "__TOMBSTONE__" else val return None
๐งช 8. Interactive Lab: Live LSM-Tree & Compaction Simulator
Interactive MemTable, SSTables & Compaction Lab
๐ฏ 9. Test Your Understanding
A backend service deletes 2,000,000 expired rows from an Apache Cassandra table. Right after, read queries on that table start timing out and throwing OutOfMemory (OOM) errors. Why?
You are architecting a clickstream analytics ingestion pipeline recording 400,000 events/second. Storage costs are high, but point queries are rare. Which storage engine and compaction strategy should you choose?