Internals · LSMwrite amplificationread amplificationspace amplificationrum conjectureleveled

Write, Read and Space Amplification

Every storage engine pays for a logical operation with more physical work than the operation itself: extra bytes written, extra pages read, extra bytes stored. Naming the three amplifications precisely — and seeing that no design minimises all of them — is the vocabulary for comparing B+ trees, leveled LSMs and tiered LSMs honestly.

▶ InteractiveInterview question
Progress

Why this exists

The mechanism as the answer to a problem — read this before the name.

  1. Problem

    Two engines both "write 100 bytes" for an insert, yet one wears out an SSD in a year and the other in five; one answers a point read from one page, the other from twelve. We need units that expose the physical cost behind the logical operation.

  2. Naive solution

    Benchmark operations per second and pick the higher number.

  3. Why it breaks

    Throughput hides where the cost went. An LSM store can accept a million writes per second into its memtable and then spend the next hour rewriting them; a B+ tree can look fast while its buffer pool absorbs dirty pages that will all hit the disk at the checkpoint.

  4. Better idea

    Count bytes and pages at the storage boundary and divide by what the application asked for: physical bytes written per logical byte written, physical reads per logical read, physical bytes stored per live byte.

  5. Internal mechanism

    Write amplification = bytes written to storage ÷ bytes written by the application (WAL + flush + every compaction rewrite, or WAL + page writes). Read amplification = pages or blocks read per logical read (files consulted, minus those bloom filters skip). Space amplification = bytes on disk ÷ bytes of live data (stale versions, tombstones, unfilled pages, dead tuples).

  6. Trade-offs

    The three pull against each other: leveled compaction minimises read and space at the cost of write; tiered minimises write at the cost of read and space; a B+ tree minimises read amplification and has no compaction but pays page-granular writes. The RUM conjecture states that no design can minimise all three.

  7. Real database

    RocksDB reports all three in its statistics (compaction.write-amp, rocksdb.bytes.read); Cassandra exposes SSTables-per-read histograms; PostgreSQL's bloat and checkpoint_completion_target are its space and write amplification knobs.

Choose your depth

The same mechanism at four altitudes. Start where you are; come back deeper.

Three hidden multipliers

When you write a row the disk sees more than a row; when you read a row the disk serves more than a row; the file is bigger than the data. Write, read and space amplification are those three ratios. They are the honest way to say what a storage engine costs.

Every design lowers one or two of them by raising the third. The choice of engine is the choice of which multiplier your workload can afford.

Write amplification

Definition: WA = bytes physically written to storage ÷ bytes logically written by the application, measured over a long enough window to include the deferred work. The deferred work is the point: an LSM tree's write path writes each byte once to the WAL and once at flush, and *then* compaction rewrites it once per level it passes through. With leveled compaction and size ratio T = 10, merging one file from level N into level N+1 rewrites on average (T+1)/2 ≈ 5.5 bytes of level N+1 per byte moved, so a byte that ends in level 4 has cost roughly 2 + 3 × 5.5 ≈ 18.5 bytes of writes. Tiered compaction merges equals with equals — about one rewrite per tier — for 2 + (L−1) ≈ 5.

A B+ tree has no compaction, but its unit of writing is the page. A 100-byte row that dirties an 8 KB page will, at the next checkpoint, cause an 8 KB write plus the WAL record — 83× if that row was the only change on the page. If sixteen rows on the page changed first, the page write is shared and the amplification is ≈ 6×. This is why B+ trees love sequential inserts (every insert lands on the same rightmost leaf) and hate random updates. It is also why "write amplification" appears in the practical Why Is This Query Slow? Indexes lesson: each secondary index is another tree, another page per row.

Where the bytes go for one 100 B row, modelled
B+ tree, random update, 1 row/page/checkpoint     WAL 120 B + page 8,192 B          ≈ 83×
B+ tree, 16 rows/page/checkpoint                   WAL 120 B + 8,192 B / 16          ≈  6×
LSM leveled  (T=10, 4 levels)                      WAL + flush + 3 × 5.5 rewrites     ≈ 18×
LSM tiered   (4 tiers)                             WAL + flush + 3 × 1 rewrite        ≈  5×
+ SSD flash translation layer                      × 1.5–3 on top of all of the above

Read amplification

Definition: RA = physical pages or blocks read ÷ logical reads, distinguished by read type. A point read in a B+ tree is height + 1 pages, but the root and branch pages of any hot index live permanently in the buffer pool, so the uncached cost is ≈ 1 leaf page (plus the heap page in PostgreSQL). In an LSM tree a point read consults the memtable, every L0 file and one file per deeper level; each consultation is a bloom-filter check, and only a "maybe" costs a block read. With F candidate files and false-positive rate FP the expected block reads are ≈ 1 + (F − 1) × FP for an existing key and ≈ F × FP for an absent one — the filter is what makes the LSM read path competitive.

A range read gets no help from bloom filters. The B+ tree walks the leaf chain: one sorted run, sequential I/O. The LSM tree must open every run whose key range intersects the query and merge them, skipping tombstones: L0 + (L − 1) runs for leveled, up to (T − 1) × L for tiered. This asymmetry is the single most important line in the Storage Engine Comparison: B+ Tree vs LSM Tree comparison.

Space amplification

Definition: SA = bytes on disk ÷ bytes of live data. In an LSM tree the excess is stale versions and tombstones waiting for compaction. Leveled compaction bounds it tightly: the newest version of every key in level N shadows at most one copy in level N+1, and level N is 1/T of N+1, so SA ≈ 1 + 1/T ≈ 1.1. Tiered compaction lets the same key live in every run of a tier until the tier is merged, so with an update-heavy workload SA approaches the tier ratio — RocksDB's universal compaction is usually configured with a cap around 2×.

A B+ tree's excess is empty space in pages: splits leave two half-full pages, and a tree under random inserts settles near 70% fill, SA ≈ 1/0.7 ≈ 1.4. Under MVCC (UPDATE, DELETE and Dead Tuples) dead tuples add to that until VACUUM reclaims them — PostgreSQL bloat is space amplification with a local name. Sorted, compressed SSTable blocks typically shrink data 3–5×, which is why MyRocks stores the same dataset in a third of InnoDB's space; compression is the LSM's counterweight to its amplifications.

How the workload moves the effective cost

Amplification factors are constants of a design; what a workload pays is Σ (share of operation type × its amplification). At 90% point reads, a B+ tree's ≈ 1 page and an LSM's ≈ 1.06 blocks are equivalent and the B+ tree's page-granular writes are a rounding error. At 95% random-key ingest, the B+ tree pays a page write per few rows while the LSM pays a sequential append — a 10–20× difference in disk bandwidth. At 50% range scans the LSM's 7–36 runs per scan dominate everything else.

The interactive lets you move the mix and watch the ranking flip. That flip, not any single number, is the lesson: the question "which engine is faster?" has no answer until the workload is on the table, and the amplification triangle is how you reason about it before you have a benchmark.

Modelled amplification (T = 10, 4 levels, 4 L0 files, 1% bloom FP, 8 KB pages, 70% fill)
Write ampPoint read ampRange read amp (runs)Space amp
B+ tree, random updates≈ 6–80× (page-granular)≈ 1 page1 run≈ 1.4× + dead tuples
B+ tree, sequential inserts≈ 2×≈ 1 page1 run≈ 1.0× (packed leaves)
LSM leveled≈ 18×≈ 1.06 blocks7 runs≈ 1.1×
LSM tiered≈ 5×≈ 1.35 blocks≈ 36 runs≈ 2×

The RUM conjecture

Athanassoulis et al. (2016) named the triangle: Read overhead, Update overhead, Memory (space) overhead — an access method can minimise two of the three at the expense of the third, never all three. A B+ tree is read-optimal with update overhead; a log is update-optimal with read and space overhead; leveled LSMs trade towards read and space; tiered LSMs trade towards update. Every tuning knob — level ratio, bloom bits, compaction strategy, fill factor — is a slider along one edge of that triangle.

It also explains hybrids. Fractal trees and Bε-trees buffer updates inside B+ tree nodes to lower write amplification while keeping one sorted structure. WiredTiger offers both a B+ tree and an LSM layout in one engine. None escapes the triangle; each picks a point on it.

Key points

  • Write amplification = physical bytes written ÷ logical bytes written, including deferred compaction or checkpoint work.
  • Read amplification = physical reads per logical read; point and range reads must be counted separately.
  • Space amplification = bytes on disk ÷ live bytes: stale versions and tombstones (LSM) or unfilled pages and dead tuples (B+ tree).
  • Leveled: WA high, RA and SA low. Tiered: WA low, RA and SA high. B+ tree: RA lowest, WA page-granular, no compaction.
  • Workload share × amplification is the cost that matters; the ranking of engines flips with the mix.
  • The RUM conjecture: no design minimises read, update and space overhead at once.

Read / write / space amplification

Write, read and space amplification
Set the workload mix, pick a storage design, and read the three amplification factors with the formula behind each. No design wins all three.
Workload mix
Weighted I/O units per operation
LSM · leveled14.2
LSM · size-tiered17.8
B+ tree6.2
= writes·WA + point·RA_point + range·RA_range·4 blocks. Move the sliders: the ranking flips.
LSM · leveled
Write amplification — bytes written to storage ÷ bytes the application wrote18.5×
WAL 1 + flush 1 + (L−1)·(T+1)/2 = 2 + 3·5.5
Every byte is rewritten about 5.5× per level as it trickles down 3 levels — the price of keeping each level one sorted run.
Read amplification (point) — blocks/pages read per lookup1.06
1 block + (files − 1)·FP = 1 + 6·0.01 (7 files consulted, bloom skips most)
Read amplification (range) — sorted runs that must be merged7
L0 files + (L−1) = 4 + 3 (every run must be merged; bloom cannot help)
A point read consults 7 files but the bloom filters turn that into ≈ 1.06 block reads. Ranges cannot use bloom filters and must merge 7 runs.
Space amplification — bytes on disk ÷ live bytes1.1×
1 + 1/T = 1 + 1/10 (only the last level can hold stale copies of the level above)
Tight: a level is at most 10× bigger than the one above, so overwritten data lingers in at most 1/10 of the store.
Leveled buys low space and read amplification with high write amplification. Fits read-mostly key-value stores where disk is the constraint.
Modelled numbers: T = 10, L = 4 levels, 4 L0 files, bloom FP 1%, 8 KB pages, 100 B rows, fill 0.7.

When to use — and when not

Use it when
  • This vocabulary fits any storage decision: it is how to compare engines, compaction strategies and index counts without a benchmark.
  • It fits capacity planning — SSD endurance is write amplification × logical write rate.
Avoid it when
  • Amplification factors do not replace a benchmark of the actual workload; they predict its shape, not its numbers.
  • They do not fit workloads small enough to sit entirely in memory, where none of the three costs is on the critical path.

Failure modes

  • Sizing SSD endurance from logical write rate and wearing out drives in a year.
  • Picking leveled compaction for an ingest workload and saturating the disk with rewrites.
  • Picking tiered compaction for a space-constrained store and running out of disk at 2× the data size.
  • Comparing engines on point-read throughput while the workload is range scans.

Where you meet this

Back up to the practical layer, and across to the rest of Engineer Atlas.