AnyLearn
All lessons
Programmingadvanced

LSM-Trees and Write-Optimized Storage

B-trees rule read-heavy OLTP; LSM-trees rule write-heavy workloads. Learn how the memtable and immutable SSTable pipeline converts random writes into sequential I/O, why compaction is unavoidable, how bloom filters slash unnecessary reads, and when to pick LSM over B-tree.

Not signed in โ€” your progress and quiz score won't be saved.
Lesson progress1 / 8

The problem B-trees have with heavy writes

Every insert into a B-tree is a random write: find the right leaf page (random read), modify it (random write), update WAL (sequential write). Under high write load, that random leaf access becomes the bottleneck. Worse, page splits propagate upward, dirtying multiple pages per insert.

Write amplification measures how many bytes hit disk per byte of user data. A B-tree under heavy random-insert load can exhibit 10โ€“30x write amplification just from splits and WAL. On flash storage, write amplification directly shortens SSD lifespan and saturates the write bandwidth.

Log-Structured Merge-trees (LSM-trees) eliminate the random-write problem by converting all writes into sequential appends โ€” at the cost of more complex reads and background compaction.

Full lesson text

All 8 steps on one page โ€” for reading, reference, and search.

Show

1. The problem B-trees have with heavy writes

Every insert into a B-tree is a random write: find the right leaf page (random read), modify it (random write), update WAL (sequential write). Under high write load, that random leaf access becomes the bottleneck. Worse, page splits propagate upward, dirtying multiple pages per insert.

Write amplification measures how many bytes hit disk per byte of user data. A B-tree under heavy random-insert load can exhibit 10โ€“30x write amplification just from splits and WAL. On flash storage, write amplification directly shortens SSD lifespan and saturates the write bandwidth.

Log-Structured Merge-trees (LSM-trees) eliminate the random-write problem by converting all writes into sequential appends โ€” at the cost of more complex reads and background compaction.

2. The memtable: writes land in RAM first

An LSM-tree never writes a user record directly to disk. Every write goes into the memtable โ€” an in-memory sorted data structure (typically a red-black tree or skip list) that accepts random insertions in O(logโกn)O(\log n) time while maintaining sort order.

The memtable is also backed by a WAL on disk (append-only), so writes are durable immediately: the client gets a confirmation after the WAL append, not after the memtable is flushed. If the process crashes, the WAL replays into a fresh memtable on startup.

Write path:
  client write
    -> append to WAL (sequential disk write, fast)
    -> insert into memtable (in-memory, O(log n))
    -> ACK to client

The memtable grows until it reaches a size threshold (RocksDB default: 64 MB), then it is frozen and a new, empty memtable takes over.

3. SSTables: immutable sorted files on disk

When the memtable is full, it is flushed to disk as an SSTable (Sorted String Table) โ€” a file of key-value pairs in sorted key order, written sequentially in a single pass. SSTables are immutable once written; updates and deletes are represented as new entries (tombstones for deletes).

An SSTable on disk has two sections:

  • Data blocks: compressed key-value pairs, typically 4โ€“64 KB each.
  • Index block: sparse index mapping every Nth key to its block offset โ€” allows binary search within the SSTable.
# RocksDB-style SSTable read (pseudocode)
def lookup(key, sstable):
    block_offset = sstable.index.search(key)  # binary search in index
    block = sstable.read_block(block_offset)   # one sequential disk read
    return block.search(key)                   # binary search within block

Immutability is key: concurrent reads need no locks, and the OS page cache naturally caches hot SSTables.

4. Compaction: keeping reads manageable

Without compaction, the number of SSTables grows without bound, and a point lookup must search every SSTable from newest to oldest โ€” O(N)O(N) disk reads. Compaction merges SSTables: read two (or more) SSTables, merge-sort their entries (resolving overwrites and tombstones), write one new SSTable, delete the old ones.

Two dominant strategies:

Size-tiered compaction: accumulate N SSTables of similar size, then merge them into one larger SSTable. Writes are cheap; reads and space amplification can be high (multiple copies of a key live in different tiers simultaneously).

Leveled compaction (RocksDB default): SSTables are organized into levels L0โ€“L6. Each level is 10x larger than the previous. Key ranges in L1+ are non-overlapping within a level. Reads only need to check 1 SSTable per level after L0. Space amplification ~1.1x; write amplification ~10โ€“30x.

5. Bloom filters: skipping SSTables cheaply

Even with leveled compaction, a point lookup for a non-existent key would have to check every level. Bloom filters prevent this: each SSTable carries a compact probabilistic filter (typically 10 bits per key) that answers "is key kk definitely NOT in this SSTable?" with zero false negatives.

# Bloom filter sketch
import mmh3, bitarray

class BloomFilter:
    def __init__(self, size, num_hashes):
        self.bits = bitarray.bitarray(size)
        self.k = num_hashes
    def add(self, key):
        for i in range(self.k):
            self.bits[mmh3.hash(key, i) % len(self.bits)] = 1
    def maybe_contains(self, key):
        return all(self.bits[mmh3.hash(key, i) % len(self.bits)] for i in range(self.k))

With a 1% false positive rate, a lookup for a missing key reads zero SSTable blocks in 99% of cases. RocksDB stores bloom filters in block cache; they're typically sub-millisecond to check.

6. Read, write, and space amplification

The three competing metrics every LSM-tree design trades off:

Write amplification (WA): bytes written to disk / bytes of user data. Compaction re-writes data repeatedly. Leveled compaction: WA ~10โ€“30x. Size-tiered: WA ~5โ€“10x but with higher space amplification.

Read amplification (RA): disk reads per point lookup. Leveled: RA ~level count (6โ€“7) plus bloom filter checks. Size-tiered: RA can be O(N)O(N) without bloom filters.

Space amplification (SA): disk bytes used / bytes of live data. Size-tiered: up to 2x during compaction (old + new SSTable coexist). Leveled: ~1.1x steady-state.

RocksDB exposes all three via db.GetProperty('rocksdb.stats'). Tuning compaction style, level multiplier, and bloom filter bits per key is the primary lever for balancing these.

7. B-tree vs LSM-tree: choosing the right engine

Neither is universally superior. Pick based on your workload:

PropertyB-tree (Postgres/InnoDB)LSM-tree (RocksDB/Cassandra)
Write throughputModerate (random I/O)Very high (sequential appends)
Point read latencyVery low (3โ€“4 page reads)Low to moderate (bloom + leveled)
Range scan performanceExcellent (linked leaves)Good with leveled, poor with size-tiered
Space amplification~2x (dead tuples before VACUUM)~1.1x leveled / ~2x size-tiered
Write amplification10โ€“30x10โ€“30x leveled (similar!)
Update-in-placeYesNo (always appends new version)
Best fitOLTP, mixed read/writewrite-heavy, time-series, event logs

Surprise: at high write rates, B-tree and leveled LSM have similar write amplification โ€” but LSM's amplification is sequential, while B-tree's is random, making LSM far more SSD-friendly.

8. LSM-trees in the wild

LSM-trees power most of the write-intensive systems you use:

  • RocksDB (Meta): the reference LSM implementation, embedded in MySQL (MyRocks), TiKV, CockroachDB.
  • Cassandra/ScyllaDB: wide-column store with size-tiered compaction, tunable bloom filter FP rate.
  • LevelDB (Google): original open-source LSM; basis for RocksDB.
  • ClickHouse MergeTree: LSM-inspired, with parts (SSTables) merged in the background.

Common production gotchas:

  • Compaction falling behind: if write rate exceeds compaction throughput, L0 SSTable count grows, reads degrade, and eventually writes are throttled (L0_slowdown_writes_trigger). Monitor rocksdb.num-files-at-level0.
  • Tombstone accumulation: deleted keys linger until compaction reaches their level. A delete-heavy workload on a rarely-compacted level can waste significant disk space.

Check your understanding

The lesson ends with a 5-question quiz. Take it in the player above to see your score.

  1. Why does an LSM-tree offer higher write throughput than a B-tree for random key inserts?
    • LSM-trees use larger pages, reducing the number of writes per insert.
    • LSM-trees buffer all writes in a sorted memtable and flush to disk sequentially as SSTables, avoiding random I/O.
    • LSM-trees skip the WAL entirely, reducing the number of disk fsyncs.
    • LSM-trees pre-sort incoming keys in a background thread before writing.
  2. Why are SSTables immutable once written to disk?
    • Immutability reduces WAL size because changes do not need to be logged.
    • Immutability allows lock-free concurrent reads and makes the OS page cache naturally effective.
    • Immutability prevents tombstones from accumulating during compaction.
    • Immutability is required because SSDs cannot overwrite individual blocks.
  3. What is the main advantage of leveled compaction over size-tiered compaction?
    • Leveled compaction produces lower write amplification.
    • Leveled compaction keeps key ranges non-overlapping within each level, limiting point lookups to one SSTable per level.
    • Leveled compaction eliminates the need for bloom filters.
    • Leveled compaction stores more data per level, reducing total disk usage.
  4. A bloom filter reports that key `k` is NOT in an SSTable. What can you conclude?
    • Key k is definitely absent from that SSTable.
    • Key k might be present, but you need to read the SSTable to confirm.
    • Key k is present in the next older SSTable.
    • The SSTable should be compacted because it contains stale data.
  5. In production, `rocksdb.num-files-at-level0` is growing steadily. What is the most likely cause and consequence?
    • Bloom filter false positive rate is too high, causing unnecessary reads that slow compaction.
    • The write rate exceeds compaction throughput; L0 files accumulate, degrading read performance until writes are throttled.
    • The memtable is too large, causing infrequent flushes that let L0 grow.
    • Leveled compaction is not configured, so all SSTables accumulate at L0 permanently.

Related lessons