DSA DSA In Depth
0%
lesson lsm-treecompactionrum-conjecture

LSM-Trees vs B+ Trees: MemTables, SSTables, Compaction & RUM Conjecture

7 min read Module 3.2: Self-Balancing, Augmented, Disk-Backed & Persistent Trees Updated Oct 23, 2026
📄 Cheat Sheet
The Secret Sauce Behind NoSQL: Log-Structured Merge (LSM) Trees
ByteByteGo The Secret Sauce Behind NoSQL: Log-Structured Merge (LSM) Trees Watch on YouTube ↗

Explains the MemTable + SSTable pipeline, Size-Tiered vs Leveled Compaction in RocksDB/Cassandra, Bloom filters, and write/read/space amplification tradeoffs.

0:00 / --:--

The LSM Write/Read Pipeline, Tombstones & Compaction Strategies

Because SSTables on disk are strictly immutable, an LSM-Tree never modifies an existing file in place:

  • Updates & Deletions (Tombstones): Updating key $K$ simply appends a newer version (K, v_new, seqNo) to the MemTable. Deleting key $K$ appends a special Tombstone marker (K, TOMBSTONE, seqNo). The obsolete older record is only physically reclaimed later when a background compaction merges the SSTable containing the tombstone with the older SSTable at the bottommost level!
  • Point Lookups (MemTable $\to L_0 \to L_1 \dots \to L_k$): To read key $K$, we search the active MemTable, then immutable MemTables awaiting flush, then every overlapping $L_0$ SSTable (because $L_0$ files are raw MemTable flushes whose key ranges overlap!), and finally at most one SSTable per level $L_1 \dots L_k$ (since levels $\ge 1$ are partitioned into non-overlapping key ranges). To avoid reading SSTable blocks from disk for keys that are not present, every SSTable embeds an in-memory Bloom Filter (or Ribbon Filter) and fence-pointer sparse index!
Storage Metric B+ Tree (In-Place Page Updates) LSM: Size-Tiered Compaction (STCS — Cassandra) LSM: Leveled Compaction (LCS — RocksDB)
Write Amplification (WA) High on random small writes (PageSize / RecordSize) Low: O(log_T N) (≈ 4–8×) Moderate: O(T · log_T N) (≈ 10–30×)
Read Amplification (RA) Optimal for Range & Point (1 tree path) Higher (Check multiple tiers) ≈ 1 Disk Read (with 10 bits/key Bloom filters)
Space Amplification (SA) ~30–45% fragmentation + internal slack High: up to 50% temporary disk needed for largest merge! Very Low: ≤ 10–11% stale data overhead!

The RUM Conjecture (Athanassoulis et al., Harvard DASlab 2016) & Leveled WA Math

Define the three fundamental amplification factors of any index structure:

$$\text{WA} = \frac{\text{Bytes Written to Storage}}{\text{Logical Bytes Inserted}}, \quad \text{RA} = \frac{\text{Bytes Read from Storage}}{\text{Logical Bytes Queried}}, \quad \text{SA} = \frac{\text{Total Storage Footprint}}{\text{Logical Dataset Size}}$$

The RUM Conjecture: No access method can simultaneously optimize Read overhead, Update (Write) overhead, and Memory (Space) overhead—minimizing any two necessarily degrades the third! Specifically, for an LSM-Tree with level size multiplier $T$ (typically $T = 10$, so $L_k = 10^k \cdot L_0$) and $L = \log_T(N / M_{\text{mem}})$ levels:

  • In Size-Tiered Compaction (STCS), $T$ runs of equal size are merged together only when all $T$ accumulate: each byte is rewritten once per level, giving $\text{WA} = O(\log_T N)$, but up to $T$ overlapping runs exist per level ($\text{SA} = O(T)$).
  • In Leveled Compaction (LCS), each level $L_i$ ($i \ge 1$) is kept strictly sorted with non-overlapping SSTables. Merging a file from $L_{i-1}$ into $L_i$ rewrites up to $T$ overlapping files in $L_i$, yielding $\text{WA} = O(T \log_T N)$, while bounding stale duplicate space to $\sum_{i=1}^{L-1} T^{-i} \approx \frac{1}{T - 1} \approx 11.1\%$ for $T=10$!
B+ Tree Page Split, Leaf Sibling Scan & LSM-Tree SSTable Compaction
O(log_B N) I/Os vs LSM Sequential Writes

Trace how a full B+ Tree leaf page splits and copies its separator key up to the parent directory, or switch to LSM-Tree mode to inspect MemTable flushes, L0→L1 k-way merge compaction, and Bloom filter skips.

Step 1 / 9
Custom Input:
Timeline

Invariant & State

Leaf P1 has 3/3 keys [10, 15, 20]. Inserting key = 25 triggers page overflow (4 > max 3).
🌳 Live 2D Hierarchical Tree Topology — B+ Tree Page Split, Leaf Sibling Scan & LSM-Tree SSTable Compaction Visited/Active Pivot/Root Resolved
root 30 [0] 10 [1] 15 [2] 20 [3] 30 [4] 40 [5] 50 [6]
Root Directory Page: [ 30 ]
Leaf P1 (FULL 3/3): [ 10 | 15 | 20 ]
→ Leaf P2: [ 30 | 40 | 50 ]
Tree Topology
1 Root + 2 Leaves
Disk Page I/O
2 Page Reads
Occupancy Invariant
⌈M/2⌉ ≤ keys ≤ M−1
[Step 1] Leaf P1 has 3/3 keys [10, 15, 20]. Inserting key = 25 triggers page overflow (4 > max 3).
RAM: MemTable (SkipList) + Fence Pointers & Bloom Filters MemTable (64MB SkipList) RAM Bloom Filters (10b/key) Disk L0 (Overlapping Key Ranges — Flushed directly from MemTable): SST [A .. Z] SST [C .. W] SST [B .. M] Disk L1 & L2 (Leveled Compaction: Strictly Non-Overlapping Key Ranges, 10× Growth): [A .. F] [G .. M] [N .. T] [U .. Z] RUM Tradeoff Triangle: • B+ Tree: Fast Reads, High Random WA • LSM STCS: Min WA, High Space Amp • LSM LCS: Min Space Amp (≤11%), Fast Point Reads via Bloom Filters Note: Range scans must merge-join iterators across ALL levels (no Bloom)!
flowchart LR Write["put(K, V) / del(K)"] --> WAL["1. Append to Disk WAL (Sequential O(1) I/O)"] WAL --> Mem["2. Insert into RAM MemTable (SkipList O(log N))"] Mem -- "MemTable Full (64MB)" --> Flush["3. Flush to L0 SSTable + Build Bloom Filter & Index"] Flush -- "L0 Count >= 4" --> Compact["4. K-Way Merge Compaction into L1 → L2 → ... → Lk (Purge Tombstones at bottom level)"]

Hardware Reality: Why Range Queries Bypass Standard Bloom Filters (and How Prefix/SuRF Filters Help)

A standard Bloom Filter hashes an exact key $h(K)$, so it can answer get("user:42") by skipping 99% of SSTables, reducing a point lookup to $\le 1$ disk read. However, for a short range scan (SELECT * FROM events WHERE key BETWEEN "user:42:00" AND "user:42:99"), a standard Bloom Filter is completely useless because you cannot hash every possible string in a continuous interval! Consequently, an LSM range iterator must seek into every $L_0$ SSTable and one SSTable at every level $L_1 \dots L_k$. RocksDB mitigates this with Prefix Bloom Filters (hashing a fixed key prefix like "user:42") or Zhang et al.'s SuRF (Succinct Range Filter, SIGMOD 2018) based on Fast Succinct Tries.

Implementation: LSM Compaction Stream Merger with MVCC Sequence & Tombstone Elision

// C++20: LSM Compaction K-Way Deduplicator & Bottommost-Level Tombstone Elision
#include <vector>
#include <string>

struct LSMRecord {
    std::string key;
    std::string value;
    uint64_t seqNo;     // Monotonically increasing MVCC transaction timestamp
    bool isTombstone;   // True if this record is a DELETE marker
};

// Given sorted records from K merged SSTable runs (ordered by key ASC, then seqNo DESC),
// deduplicates shadowed versions and drops tombstones if compacting into the bottommost level.
std::vector<LSMRecord> compactSortedStream(
    const std::vector<LSMRecord>& mergedStream,
    uint64_t oldestActiveSnapshotSeq,
    bool isBottommostLevel
) {
    std::vector<LSMRecord> output;
    for (size_t i = 0; i < mergedStream.size(); ++i) {
        const auto& rec = mergedStream[i];
        // Skip older versions of the same key if shadowed by a version <= oldestActiveSnapshotSeq
        if (!output.empty() && output.back().key == rec.key &&
            output.back().seqNo <= oldestActiveSnapshotSeq) {
            continue;
        }
        // Physical deletion: only drop a tombstone when it reaches the bottommost level
        // AND no older snapshot can see past it!
        if (rec.isTombstone && isBottommostLevel && rec.seqNo <= oldestActiveSnapshotSeq) {
            continue;
        }
        output.push_back(rec);
    }
    return output;
}

Failure Modes & Adversarial Pitfalls

  • Dropping Tombstones Above the Bottommost Level Resurrects Deleted Data ("Zombie Keys"): If Compaction merges $L_1$ and $L_2$ and discards a Tombstone for key $K$ while an older version of $K$ still sits in $L_3$, subsequent reads will fall through $L_1$ and $L_2$ and return the deleted value from $L_3$! A tombstone can only be physically dropped when it reaches the bottommost level (or when Bloom/range metadata proves no SSTable at any deeper level contains $K$).
  • RocksDB Tombstone Scan Degradation ("Delete-Heavy Queue Anti-Pattern"): Using an LSM-Tree as a FIFO queue (insert at tail, delete at head, then seekToFirst()) accumulates millions of consecutive tombstones at the start of the keyspace before compaction catches up—causing seekToFirst() to scan past millions of dead tombstones in $O(N)$ time!
Check yourself Question 1 of 4
Concept Check The Write Path of an LSM-Tree Storage Engine recall

When a client executes `Put(key, value)` in an LSM-tree engine such as RocksDB, LevelDB, or Cassandra, what happens on the synchronous critical path before returning success?

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

Identify the exact conditions required before a tombstone can be permanently dropped during compaction:

Key Takeaways

LSM-Trees convert random writes, updates, and deletes into sequential appends via an in-memory MemTable + WAL that flushes immutable SSTables to disk.
Deletions write Tombstone markers; a tombstone cannot be garbage-collected until compaction reaches the bottommost level, otherwise older versions in deeper levels resurrect as zombie keys.
Leveled Compaction (RocksDB) minimizes Space Amplification (≤ 11% stale overhead) at the cost of higher Write Amplification O(T log_T N), whereas Size-Tiered Compaction (Cassandra) minimizes Write Amplification O(log_T N) at the cost of higher temporary Space Amplification.
Bloom Filters reduce LSM point-lookup Read Amplification to ~1 disk read, but standard Bloom Filters cannot prune arbitrary range scans unless paired with Prefix Bloom Filters or Succinct Range Filters (SuRF).
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue