LSM-Trees vs B+Trees Under the Hood: Architecture, Internals, and Best Practices
A comprehensive database internal engineer's guide to Log-Structured Merge-Trees (LSM), B+Trees, Write-Ahead Logs (WAL), MemTables, SSTables, Bloom Filters, Leveled vs Size-Tiered Compaction, and production C++/Python storage engines.
At the heart of every modern database system—whether PostgreSQL, MySQL, RocksDB, Cassandra, or CockroachDB—lies a storage engine that dictates how data is indexed and persisted to physical disk.
Why do traditional relational databases like PostgreSQL and MySQL InnoDB rely on **B+Trees** for in-place page updates, while high-throughput write engines like RocksDB, LevelDB, and Cassandra adopt **Log-Structured Merge-Trees (LSM-Trees)**? How does the **RUM Conjecture** prove that a storage engine cannot simultaneously optimize Read Amplification, Update Amplification, and Memory Space Amplification? What makes **Bloom Filters** and **Leveled Compaction** essential to preventing read performance degradation in LSM-Trees? In this deep dive, Professor Pixel breaks down database storage engines from first principles: disk hardware physics, SkipList MemTables, immutable SSTables, step-by-step compaction traces, production C++ and Python storage engines, performance benchmarks, and interactive visual simulators.
1. The Intuition: Random I/O vs Sequential Write Throughput
1.1 The Hardware Physics of Storage Media
To understand why database storage engines are designed the way they are, we must look at the physical mechanics of storage hardware:
- Mechanical Hard Disk Drives (HDDs): Random disk I/O requires physical actuator arm movement and rotational delay ($5\text{ms} - 10\text{ms}$ per seek). Sequential I/O operates at over $150\text{ MB/s}$, while random I/O drops to a dismal $1\text{ MB/s}$ ($~100\text{ IOPS}$).
- Solid-State Drives (SSDs & NVMe): While SSDs have zero mechanical seek time, flash memory cannot overwrite data in-place! SSDs must erase entire large **Erase Blocks** ($2\text{MB} - 8\text{MB}$) before writing to small pages ($4\text{KB}$). Performing random in-place updates causes severe **Write Amplification** and prematurely wears out flash memory cells.
1.2 The In-Memory Scratchpad + Immutable Ledger Mental Model
Instead of updating existing disk pages in-place (the B+Tree approach), an **LSM-Tree** turns all write operations into fast, sequential, append-only disk writes.
Imagine an accountant keeping financial records:
- The In-Memory Scratchpad (MemTable): When a client sends a write request (`Put(key, value)`), the accountant writes the record instantly into a fast in-memory sorted scratchpad (a SkipList or Red-Black Tree).
- The Write-Ahead Log (WAL): Simultaneously, the write is appended to an on-disk sequential log file (`wal.log`) to guarantee crash recovery.
- Immutable Ledger Flushes (SSTables): When the scratchpad fills up (e.g. at 64MB), the accountant freezes it, sorts its contents, and writes it out to disk as an **Immutable Sorted String Table (SSTable)**.
Diagram 1: LSM-Tree Data Pipeline. Write operations update WAL and in-memory MemTable simultaneously before background threads flush immutable SSTables to disk levels.
Developer Pitfall — Using B+Tree Engines for Ultra-High Write Throughput Workloads:
Deploying a B+Tree database (like MySQL InnoDB) for ingestion workloads exceeding 100,000 writes/sec causes severe random disk I/O thrashing and page split stalls. B+Trees must read and rewrite entire 16KB disk pages for tiny tuple updates, creating high Write Amplification ($WA > 50x$). For write-heavy logging, metrics, or time-series data, always choose an LSM-Tree storage engine (RocksDB/Cassandra/ScyllaDB).
2. Architectural Deep Dive: LSM-Tree Internals (MemTable, WAL, SSTables, Bloom Filters)
2.1 The Concurrent SkipList MemTable
This internal SSD garbage collection cycle severely degrades write IOPS under heavy load, causing drive latency spikes ($>100\text{ms}$) and reducing the physical lifespan of enterprise SSD flash memory modules. The Write Amplification Factor (WAF) represents the ratio of actual NAND flashes written to host-requested writes, where higher WAF values directly correlate with reduced SSD hardware longevity and increased total cost of ownership in high-performance cloud database environments.
The MemTable is an in-memory data structure that maintains key-value pairs sorted in lexicographical order. While Red-Black trees require complex rebalancing locks during concurrent writes, modern LSM-Trees (like RocksDB) use a **Lock-Free Concurrent SkipList**, providing $O(\log N)$ insertions and range scans under high concurrency without lock contention.
2.2 Sorted String Tables (SSTables) Layout
When a MemTable is flushed to disk, it creates an **SSTable file**. An SSTable is structured into distinct binary blocks:
2.3 Probabilistic Bloom Filters for Zero-I/O Point Lookups
Because SSTables are immutable, a single key `Get("user_991")` might not exist in the MemTable. Without optimization, the database would have to read every SSTable on disk to confirm the key is missing (catastrophic Read Amplification!).
To prevent this, every SSTable includes a **Bloom Filter**, a space-efficient probabilistic bit array. For $m$ bits and $k$ hash functions, the false positive probability $P_{\text{fp}}$ is:
1.5 Erase Block Garbage Collection Overhead in Flash Drives
Flash memory storage cells cannot modify individual bits from 0 to 1 without erasing an entire electrical block. An Erase Block typically contains 512 individual 4KB data pages. When a B+Tree database updates a single 16KB page in-place, the SSD controller must read all 512 pages from the physical block into temporary RAM, update the 4KB page, erase the entire physical block with a high-voltage charge, and rewrite all 512 pages back to flash. This internal SSD garbage collection cycle severely degrades write IOPS under heavy load!
2.4 SkipList Probabilistic Height Distribution Algorithm
Why do modern LSM-Trees choose SkipLists over traditional Red-Black or AVL balanced search trees for MemTables? In a multi-threaded database environment, rebalancing a Red-Black tree requires locking multiple parent, child, and sibling nodes across tree levels (coarse-grained locking), which causes severe lock contention bottlenecks.
A **SkipList** achieves $O(\log N)$ average insertion and search performance probabilistically without complex tree rebalancing. Each node is assigned a random height level determined by flip of a biased coin with probability $p = 1/4$:
Because insertions only update forward pointers at the node's assigned level using atomic Compare-And-Swap (CAS) instructions, lock-free concurrent SkipLists allow hundreds of worker threads to insert key-value pairs into the MemTable simultaneously with zero mutex locking overhead!
3. Under the Hood: Compaction Strategies (Size-Tiered vs Leveled Compaction)
3.1 Evaluating Compaction Models
As SSTables accumulate on disk, background threads perform **Compaction**—merge-sorting overlapping SSTables to reclaim space from tombstones (deleted keys) and obsolete versions:
| Compaction Strategy | Write Amplification ($WA$) | Read Amplification ($RA$) | Space Amplification ($SA$) | Primary Database Engine |
|---|---|---|---|---|
| Size-Tiered (STCS) | Low ($WA \approx 4 - 8x$) | High (SSTables overlap) | High ($SA > 100\%$) | Apache Cassandra, ScyllaDB |
| Leveled Compaction (LCS) | High ($WA \approx 10 - 30x$) | Low (Non-overlapping levels) | Low ($SA \approx 1.1 - 1.2x$) | RocksDB, LevelDB, CockroachDB |
3.2 Step-by-Step Numerical Walkthrough: Leveled Compaction ($L_0 \to L_1$)
Let us trace how RocksDB performs a Leveled Compaction merge sort between Level 0 ($L_0$) and Level 1 ($L_1$):
3.3 Mathematical Derivation of Leveled Compaction Write Amplification
Let $T$ be the leveling growth factor (typically $T = 10$, meaning each level is 10x larger than the previous level: $L_1 = 10\text{MB}, L_2 = 100\text{MB}, L_3 = 1\text{GB}$). When an SSTable from Level $L_{i-1}$ is compacted into Level $L_i$, its key range overlaps with up to $T$ SSTables in Level $L_i$:
For a 5-level database ($L=5$), every byte written by the application is rewritten up to $5 \times 10 = 50$ times by background compaction threads! This is the explicit trade-off Leveled Compaction makes to guarantee low Read Amplification ($RA \approx 1$) and tight Space Amplification ($SA \approx 1.1x$).
4. B+Tree Internals: In-Place Updates, Page Splitting, and Write-Ahead Logging
4.1 B+Tree Disk Page Structure
Unlike LSM-Trees which write append-only logs, traditional RDBMS storage engines (PostgreSQL, MySQL InnoDB) organize data into fixed-size **Disk Pages** (typically 8KB or 16KB). A B+Tree page consists of:
4.2 The Cost of B+Tree Page Splitting
When inserting a new tuple into a full B+Tree leaf page ($16\text{KB}$ capacity), the engine must perform a **Page Split**:
- Allocate a new empty $16\text{KB}$ page from the database tablespace file.
- Move $50\%$ of the tuples from the full page to the new page.
- Insert the new median key into the parent internal index node (which may trigger a cascading parent split all the way up to the root!).
- Write double-write buffer frames and WAL logs to guarantee crash consistency.
4.3 The MySQL InnoDB Doublewrite Buffer Mechanics
In traditional B+Tree databases, writing a modified 16KB page to disk is NOT an atomic hardware operation. Operating system storage drivers write data in 4KB sector blocks. If a server loses power mid-write after completing only two 4KB blocks, the 16KB page becomes corrupted (a **Partial Page Write Failure**).
To protect against partial page corruption, MySQL InnoDB implements the **Doublewrite Buffer**:
While the Doublewrite Buffer prevents partial page corruption, writing every modified page twice doubles disk write overhead, compounding B+Tree Write Amplification ($WA > 50x$)!
5. Step-by-Step Production C++ & Python LSM-Tree Storage Engines from Scratch
5.1 Production C++ In-Memory SkipList & SSTable Engine
Below is a complete, production-ready C++ implementation of a SkipList MemTable and SSTable binary search lookup engine:
5.2 Production Python LSM Storage Engine with WAL and Bloom Filter
1.4 NVMe Flash Memory Wear Leveling & Write Amplification
Solid-state drive (SSD) memory cells can only withstand a limited number of Program/Erase (P/E) cycles before hardware degradation. When a database performs random in-place updates, the SSD controller Flash Translation Layer (FTL) must repeatedly execute read-modify-write cycles across large 4MB erase blocks. LSM-Trees write continuous sequential streams of SSTable files, allowing the FTL to execute sequential writes with $WA \approx 1.0$, dramatically extending enterprise SSD hardware lifespan!
1.3 Write Path vs Read Path Execution Latency in LSM-Trees
To understand why LSM-Trees deliver asymmetric write vs read performance, we must trace both execution paths:
- The LSM Write Path ($0.1\text{ms}$): Append entry to Write-Ahead Log (WAL) on disk $\to$ Insert entry into in-memory SkipList MemTable $\to$ Return HTTP 200 OK success to client. No random disk seeks required!
- The LSM Read Path ($0.1\text{ms} - 15\text{ms}$): Check active MemTable $\to$ Check immutable MemTables $\to$ Query Bloom Filters for Level 0 SSTables $\to$ Binary search Index Blocks of target SSTables $\to$ Fetch 4KB Data Block from disk.
Developer Pitfall — Failing to Execute `os.fsync()` on WAL Writes:
Calling `file.write()` in Python or C++ `fwrite()` only writes data to operating system page cache RAM, NOT physical disk media! If the server loses power, all un-flushed WAL writes are lost. You MUST call `os.fsync(fd)` or `fflush()` + `fsync()` to guarantee durability.
6. Advanced: Write/Read/Space Amplification (RUM Conjecture) and Modern Hardware
6.1 The RUM Conjecture
Formulated by Harvard researchers, the **RUM Conjecture** states that a storage engine can only optimize **two out of three** trade-offs:
- Read Overhead (Read Amplification): Number of disk bytes read per application API read.
- Update Overhead (Write Amplification): Number of disk bytes written per application API write.
- Memory Space Overhead (Space Amplification): Total disk space used relative to logical uncompressed data size.
B+Trees prioritize Read Overhead & Space Overhead at the expense of high Update Overhead. LSM-Trees prioritize Update Overhead at the expense of higher Read Overhead & Space Overhead.
6.2 Zoned Namespaces (ZNS) SSD Acceleration
Modern NVMe **Zoned Namespaces (ZNS)** SSDs eliminate the traditional internal Flash Translation Layer (FTL). ZNS exposes physical erase zones directly to the database. Because LSM-Trees write append-only SSTable files sequentially, RocksDB configured with ZNS writes directly to NVMe zones without triggering SSD internal garbage collection, doubling SSD lifespan!
6.3 RocksDB Block Cache vs Row Cache Architecture
To mitigate read amplification in LSM-Trees, production storage engines employ a multi-tier memory caching hierarchy:
- Block Cache (LRUCache / ClockCache): Caches uncompressed 4KB SSTable data blocks in memory. When a point read or range scan requests a key, RocksDB checks the Block Cache before reading disk files.
- Row Cache: Caches individual decoded key-value tuples. Skips block decompression overhead entirely, providing ultra-fast sub-microsecond point read latencies for hot keys.
Developer Pitfall — Misconfiguring RocksDB Block Cache Memory Limits:
By default, RocksDB allocated unmanaged block cache RAM alongside MemTable allocations. In containerized environments (Docker / Kubernetes), unmanaged block cache growth will trigger the Linux kernel Out-Of-Memory (OOM) killer, abruptly terminating your database process! Always configure `write_buffer_manager` to cap total memory usage.
7. Industry Comparison Matrix of Storage Engines
The table below compares core production database storage engines across key architectural parameters:
| Database & Engine | Storage Architecture | Write Amplification ($WA$) | Read Amplification ($RA$) | Primary Industry Workload |
|---|---|---|---|---|
| MySQL (InnoDB) | B+Tree (16KB Pages) | High ($20 - 60x$) | Low ($1 - 2x$) | OLTP E-Commerce, Financial Transactions |
| PostgreSQL (heap + B-Tree) | Heap Files + B-Tree Index | High ($15 - 40x$) | Low ($1 - 2x$) | General Purpose Relational Enterprise Apps |
| RocksDB / LevelDB | Leveled LSM-Tree | Low ($10 - 20x$) | Medium (Bloom Filtered) | High-Throughput Key-Value, CockroachDB Engine |
| Apache Cassandra | Size-Tiered LSM-Tree | Ultra-Low ($2 - 8x$) | High ($5 - 15x$) | Distributed Time-Series, Write-Heavy Metrics |
8. Interactive: LSM-Tree MemTable Flush & Level Compaction Simulator
Click "Step LSM Pipeline" to simulate Write to MemTable $\to$ MemTable Flush to $L_0$ SSTable $\to$ Level $L_0 \to L_1$ Compaction:
9. Performance Benchmarks: B+Tree vs LSM-Tree Under Increasing Load
The chart below compares Write Operations Per Second across dataset scale between B+Tree and LSM-Tree storage engines:
10. Frequently Asked Questions
Q1: What is the primary architectural difference between a B+Tree and an LSM-Tree?
A B+Tree updates fixed-size disk pages in-place, offering fast point reads ($RA \approx 1$) but suffering high Write Amplification due to random I/O. An LSM-Tree converts all writes into sequential append-only operations (MemTable + SSTables), delivering high write throughput at the cost of background compaction overhead.
Q2: What is Write Amplification ($WA$) and why does it matter for SSD lifespans?
Write Amplification is the ratio of physical bytes written to flash storage versus logical bytes requested by the application API ($WA = \frac{\text{Bytes}_{\text{flash}}}{\text{Bytes}_{\text{app}}}$). High Write Amplification ($WA > 50$) causes flash memory cell degradation and prematurely wears out enterprise SSDs.
Q3: How do Bloom Filters optimize point lookups in LSM-Tree databases?
A Bloom Filter is a space-efficient probabilistic bit array stored alongside each SSTable. Before performing expensive disk reads, the database queries the Bloom Filter. If the filter returns false, the key is guaranteed not to exist in that SSTable, skipping disk I/O entirely.
Q4: What is the difference between Size-Tiered (STCS) and Leveled Compaction (LCS)?
Size-Tiered Compaction groups SSTables by file size, offering fast write speeds but consuming high disk space ($SA > 100\%$). Leveled Compaction organizes SSTables into discrete exponentially-sized levels ($L_1, L_2$) with non-overlapping key ranges, minimizing space amplification ($SA \approx 1.1x$) and read latency.
Q5: What is a Tombstone in an LSM-Tree database?
Because SSTables on disk are immutable, deleting a record does not erase data immediately. Instead, the engine appends a special marker called a **Tombstone**. During background compaction, the tombstone and old key versions are merged and permanently garbage collected.
Q6: What causes "Compaction Stalls" in RocksDB?
Compaction Stalls occur when the rate of incoming application writes exceeds the speed of background compaction threads, causing Level 0 ($L_0$) SSTable files to pile up. RocksDB deliberately throttles write threads to prevent unmanageable read latency degradation.
Q7: What is the RUM Conjecture in database storage engine theory?
Formulated by database researchers, the RUM Conjecture states that a storage engine can only optimize two out of three parameters: Read Overhead, Update Overhead, and Memory Space Overhead. Optimizing one parameter inevitably sacrifices another.
Q8: Why are random UUIDv4 primary keys harmful for B+Tree indexes?
Random UUIDv4 values force B+Tree insertions into random leaf pages across the disk index. This causes frequent 50/50 page splits, severe page fragmentation, and high random disk I/O, degrading write performance.
Q9: How does Zoned Namespaces (ZNS) NVMe hardware benefit LSM-Trees?
ZNS NVMe SSDs allow LSM-Tree storage engines to write append-only SSTables directly to physical flash erase zones, bypassing internal SSD Flash Translation Layer (FTL) garbage collection and doubling SSD hardware lifespan.
Q10: What production databases use RocksDB as their underlying storage engine?
RocksDB powers major distributed infrastructure systems including CockroachDB (SQL layer on RocksDB/Pebble), Apache Flink (state backend), MyRocks (RocksDB for MySQL), and Kafka Streams.
Q11: How does a SkipList MemTable support concurrent lock-free reads and writes?
A SkipList organizes sorted key-value pairs into multiple linked-list levels using probabilistic height assignment ($p=1/4$). Because insertions only update forward pointers at a node's assigned level using atomic Compare-And-Swap (CAS) operations, lock-free SkipLists allow concurrent thread writes without mutex locks.
Q12: Why does MySQL InnoDB require a Doublewrite Buffer?
Operating system storage drivers write data in 4KB sector chunks. If a server loses power while writing a 16KB InnoDB page, the page becomes partially written and corrupted. InnoDB writes pages sequentially to the Doublewrite Buffer first so clean pages can be recovered after a crash.
Q13: What is the impact of fsync() on Write-Ahead Log (WAL) durability and latency?
Calling `write()` only pushes data into the operating system page cache RAM. Executing `fsync()` forces the OS to flush dirty cache pages to physical non-volatile disk media, guaranteeing ACID durability (Durability in ACID) at the expense of adding disk flush latency.