Chris Collins

Tools · distributed

Storage engines: LSM-tree vs B-tree, simulated

A B+tree and an LSM-tree side by side in your browser, fed the same operations: page splits and merges, the WAL and buffer pool, memtable flushes, leveled, size-tiered and lazy-leveling compaction, Bloom filters, fence pointers and tombstones — with write, read and space amplification counted as you watch, guided scenarios and the textbook cost models.

Added

Works with

How to use it

Two storage engines in miniature, running entirely in this page. A B+tree keeps every key in one sorted tree of fixed-size pages and updates pages in place; an LSM-tree appends every write to a log and a sorted in-memory table, writes immutable sorted files (SSTables) and merges them in the background. They take the same stream of inserts, updates, deletes, lookups and scans, and every byte written, every page or block read and every byte on disk is counted — so the trade-offs show up as numbers: write amplification (bytes the engine writes per byte you write), read amplification (disk reads per lookup or scan) and space amplification (bytes on disk per byte of live data). Nothing here talks to a real database.

How to use it

Pick a scenario and press Start. Each step sends some operations and plays for a few simulated seconds, then pauses with an explanation. Next step (or →) moves on, Back rewinds (the simulator is deterministic, so it replays the earlier steps), ⏭ jumps to the end of the step and Step ⇥ advances to the next single thing that happens. Space plays and pauses. Speed scales the clock; Hop time is how long each drawn operation stays on screen at 1× (the simulated clock slows while one is in flight — it never changes what happens); Sim clock turns that off.

⛶ Full screen (or F) makes the diagram fill the window — the browser’s full screen where it is allowed, a full-window layer otherwise (iPhone) — and Esc leaves. ☰ Narration hides the narration, speed row, legend and inspector so the diagram can use the whole height.

  • The diagram. The B+tree on the left (on a phone: on top): its pages by level, root at the top, each page’s fill shaded, its keys written in when they fit (otherwise used/capacity), the leaf chain dashed. A dot on a page means it is in the buffer pool (orange: dirty, not yet on disk). Splits, borrows and merges flash on the pages involved. Above the tree: the WAL, the buffer pool and the disk counters. The LSM-tree on the right: the WAL, the memtable (its keys, sorted) and the immutable memtable being flushed, then one band per level (L0, L1, …; one band for size-tiered). Each SSTable is drawn over the key range it covers, so overlap is visible; one row per sorted run. Ticks on a file are its fence pointers (one per block), the dark part is the share of tombstones, stripes mean it is being compacted. Operations travel as labelled packets: a write goes to the WAL and memtable; a lookup visits the memtable and then each file it has to read (a ✕ marks a file whose Bloom filter said "not here", skipped without I/O); a B+tree operation walks from the root to a leaf. Flushes and compactions move blocks from their input files to the level they write. Tap a page, a file, a level, the memtable, the WAL or the buffer pool to inspect it and act on it (look up a key, delete a page’s keys, compact a file down, flush, checkpoint).

  • Counters, per engine: write amplification, reads per lookup that finds its key, per lookup of a missing key, and per range scan, space amplification, splits and merges, buffer-pool hit ratio, Bloom-filter negatives and false positives, compactions, tombstones and write stalls.

  • Amplification over the run: write, space and read amplification against the number of operations, for both engines.

  • Do something: send 1–1,000 inserts, updates, deletes, lookups (of existing or missing keys) or 20-key range scans, with random, ascending, descending or Zipf-skewed keys; flush the memtable, run a full compaction, toggle Bloom filters, checkpoint the B+tree, or empty both engines.

  • Event log: splits, merges, flushes, compactions, stalls and checkpoints.

  • Engine settings rebuild the engines (listed below).

The Cost models tab sets the textbook estimates side by side for any N, entry size, page size, memtable size, size ratio, Bloom bits per key and fanout, and can add the current simulation’s measured numbers as rows.

The scenarios

  • B+tree: pages split as keys arrive — ascending keys leave half-full pages with 50/50 splits; the rightmost-split optimisation fills them; random keys settle near 69% (ln 2).

  • B+tree: deletes, borrowing and merging — under-full pages borrow from a sibling or merge, and the root collapses.

  • LSM: WAL, memtable and flush to L0 — the write path, overlapping L0 files and newest-first reads.

  • Leveled compaction: the cascade — L0 → L1 → L2 with targets T times apart, and what it costs in writes.

  • Size-tiered compaction and its space spike — Cassandra’s STCS buckets, overwrites piling up, and a major compaction that briefly needs the old and the new data on disk.

  • Tombstones: deletes are writes — deletes add data, scans wade through tombstones, compaction drops them at the bottom.

  • Bloom filters save lookups of missing keys — a block read per run without filters, almost none with them.

  • Range scans favour the B+tree — one descent and a leaf walk against a block from every run.

  • Update-heavy: write amplification compared — a page rewritten per update against append and compact, and what a buffer pool does to the B+tree.

  • Playground — both engines, empty.

What it models

  • B+tree (Comer 1979): leaves hold entries and are chained; inner pages hold separators. An insert into a full leaf splits it 50/50 (or, with the rightmost-split option, leaves an appended-to last leaf full, as PostgreSQL and InnoDB do for ascending keys) and pushes a separator up, splitting inner pages and growing a new root as needed. A delete that leaves a page under half full borrows from a sibling or merges with it; a root with one child is removed. Page size is derived: 64 bytes of header plus the entries. Every page access goes through an optional LRU buffer pool (write-back: dirty pages reach the disk at eviction or checkpoint); every change appends a WAL record, and with full-page writes the first change to a page after a checkpoint puts the whole page in the WAL, as PostgreSQL’s full_page_writes does.

  • LSM-tree (O’Neil et al. 1996): a write appends to the WAL and goes into the memtable; a full memtable becomes immutable and is flushed to L0 as an SSTable while a new one takes writes (if the previous flush has not finished, or L0 has reached its stop trigger, writes stall). Each SSTable has a Bloom filter with k = round(bits per key × ln 2) and fence pointers, one per block, both in memory: a point lookup asks each candidate file’s filter and reads one block on a "maybe"; a range scan reads the blocks covering the range in every overlapping file.

  • Leveled compaction (RocksDB): L0 files overlap; at l0Trigger files they are merged with the overlapping L1 files. Level 1’s target is l0Trigger × memtable entries and each level below is T times larger; a level over target merges one file (round-robin by key) with its overlap below. Files in L1+ never overlap. Size-tiered (Cassandra STCS): runs are bucketed by size (0.5–1.5× the bucket average, small ones together) and a bucket with min_threshold runs is merged, up to max_threshold. Lazy leveling (Dostoevsky): levels above the last accumulate up to T runs and merge them into the next level; the last level is a single run that incoming runs merge into.

  • Merging keeps the newest version of each key. A tombstone is dropped only when no file outside the merge at that level or below (for STCS: no other SSTable) covers its key.

  • Amplification: write = (WAL + page writes × page size) ÷ user bytes for the B+tree, (WAL
    flushes + compactions) ÷ user bytes for the LSM-tree; read = disk page or block reads per operation; space = (pages × page size + WAL since checkpoint) or (SSTables + live WAL + a running compaction’s output) ÷ live bytes.

Settings

Setting Meaning

Engines

Both, B+tree only, LSM-tree only

Key space, key bytes, value bytes

Keys are integers 0 … n−1; entry size for byte accounting

ms between operations, hop latency, seed

Workload pacing, how long a drawn hop takes, the random seed

Entries per leaf, children per inner page

B+tree page capacities

Leaf split

50/50, or rightmost (sequential-insert optimisation)

Buffer pool pages

LRU pages cached (0 = every access is I/O)

Operations per checkpoint, full-page writes

Checkpoint interval; PostgreSQL-style page images in the WAL

Compaction

Leveled, size-tiered (STCS) or lazy leveling

Memtable entries, size ratio T, levels

LSM shape

L0 compaction trigger, L0 stop-writes

RocksDB’s level0_file_num_compaction_trigger and level0_stop_writes_trigger

Entries per SSTable, entries per block

File size in a leveled run; fence-pointer granularity

Bloom filters, bits per key

Filters on new files (toggling rebuilds them on all files)

min_threshold, max_threshold

STCS bucket merge limits

Flush / compaction ms per entry

How long background work takes

Limits

The sizes are tiny on purpose — pages of a few entries, memtables of a few dozen — so that a few hundred operations show every mechanism; real pages hold hundreds of entries and real memtables millions. Ratios carry over, absolute numbers do not. Entries are fixed-size, keys are integers, there are no snapshots, no compression, no block cache on the LSM side (its filters and fence pointers are always in memory), one compaction at a time, no sub-compactions, no partitioned filters, no trivial moves and no TTL. The B+tree has no free-space map, no fill factor setting and no concurrency. Write stalls stop LSM writes but not the B+tree’s. Treat the numbers as an explanation of where amplification comes from, not as a benchmark: measure your own workload with your own engine.

Sources: P. O’Neil, E. Cheng, D. Gawlick, E. O’Neil, The Log-Structured Merge-Tree (LSM-Tree) (Acta Informatica, 1996); D. Comer, The Ubiquitous B-Tree (ACM Computing Surveys, 1979); the RocksDB wiki: Leveled Compaction, Universal Compaction and the Tuning Guide; Cassandra’s Size Tiered Compaction Strategy; N. Dayan, M. Athanassoulis, S. Idreos, Monkey: Optimal Navigable Key-Value Store (SIGMOD 2017); N. Dayan, S. Idreos, Dostoevsky: Better Space-Time Trade-Offs for LSM-Tree Based Key-Value Stores (SIGMOD 2018); M. Athanassoulis et al., Designing Access Methods: The RUM Conjecture (EDBT 2016); M. Kleppmann, Designing Data-Intensive Applications, chapter 3; the PostgreSQL B-tree implementation notes and WAL configuration.

Related: Postgres playground (real B-tree indexes and the WAL), probabilistic data structures (Bloom filters in depth), cache simulator (buffer pools and block caches are caches), Kafka, simulated (log segments and log compaction), Elasticsearch, simulated (Lucene segments and merges), latency numbers (why a disk read costs what it does), Redis client.

Linking to an example

Share link copies a URL that opens the simulator exactly as configured. Hand-written links work too: ?example=<scenario> opens a scenario with its own settings, and any setting written beside it overrides the scenario’s, by the names below.

Parameter Meaning Example

example

A scenario (see the list below)

example=bloom

scenario

The same, without resetting other settings

scenario=leveled

engine

both, btree or lsm

engine=lsm

keySpace, keySize, valueSize

Key space and entry bytes

valueSize=1000

every, latency, seed

ms between operations, hop latency, random seed

seed=7

leafCap, fanout, split

B+tree page capacities; half or rightmost

leafCap=8&split=rightmost

buffer, checkpoint, fpw

Buffer pool pages, operations per checkpoint, full-page writes

buffer=0&fpw=true

compaction

leveled, tiered or lazy

compaction=tiered

memtable, T, levels, l0Trigger, l0Stop

LSM shape and L0 triggers

T=10

fileEntries, blockEntries

Entries per SSTable and per block

blockEntries=8

bloom, bitsPerKey

Bloom filters on or off, bits per key

bloom=true&bitsPerKey=5

minThreshold, maxThreshold

STCS merge thresholds

minThreshold=2

flushMs, compactMs

Background work speed

compactMs=1

speed, hop

Playback speed, hop time in seconds or clock

speed=4&hop=clock

example= Opens

btree-splits

B+tree splits, sequential vs random (open)

btree-deletes

Deletes, borrowing and merging (open)

lsm-flush

WAL, memtable and flush to L0 (open)

leveled

The leveled compaction cascade (open)

size-tiered

STCS and its space-amplification spike (open)

tombstones

Deletes as writes, and tombstone GC (open)

bloom

Bloom filters and lookups of missing keys (open)

range-scans

Range scans: B+tree against LSM-tree (open)

update-heavy

Write amplification under skewed updates (open)

playground

Both engines, empty (open)

For example ?example=leveled&T=10 runs the leveled cascade with a size ratio of 10, and ?example=bloom&bitsPerKey=4 shows a weaker filter.

Runs entirely in your browser; nothing you type leaves this page.