LSM tree vs B-tree: amplification simulator

Every storage engine writes more than you give it, reads more than you ask for, or stores more than you keep. Run the same stream of random writes through an LSM tree and a B+ tree with your own settings and measure all three costs side by side.

LSM write amp
10.2×
B-tree write amp
30.2×
LSM compaction
Workload
LSM tree
B+ tree
Running…
Side by side
Write amplification
LSM10.2×
B+ tree30.2×
Space amplification
LSM1.03×
B+ tree1.44×
Point read, worst case
LSM3 files
B+ tree4 pages

Write amplification counts the write-ahead log too (1× for both). Point reads are the worst case with no Bloom filters and an empty cache.

LSM tree at the end
LevelFilesEntries
L000
L1109,753
L210299,305
L37777,000
Memtable flushes
200
Compactions
317
Live keys
181,314
B+ tree at the end
Leaf pages
4,091
Page splits
4,090
Pages written
91,250
Average leaf fill
69%

How to use the simulator

Set the number of writes and how many different keys they may hit; fewer keys means more updates of the same keys. Both engines receive exactly the same writes. The LSM tree flushes its memtable to sorted files and compacts them, leveled or size-tiered; the B+ tree inserts into fixed-size leaf pages, splitting full ones, and writes a dirty page to disk only when the buffer pool evicts it. Sizes are counted in entries, so an amplification of 10× means ten entries written to disk for every entry written by the application.

Two designs

A B-tree keeps every key in one place, a page of a balanced tree, and changes that page when the key changes. Reads follow one path from the root. A log-structured merge tree never updates in place: writes collect in an in-memory table, which is written out as a sorted, immutable file when it fills, and a background process merges files into larger ones, discarding overwritten versions. The LSM design was published by Patrick O’Neil and colleagues in 1996 and underlies RocksDB, LevelDB, Cassandra and many other stores; B-trees remain the default in most relational databases.

Write amplification

An LSM tree writes each entry to the log, once when its memtable is flushed, and again every time compaction merges it into a lower level. With leveled compaction each level is about T times bigger than the one above, and pushing a file down rewrites the overlapping part of the next level, so each level costs several rewrites; the RocksDB documentation notes leveled write amplification is often larger than 10. Size-tiered compaction merges runs of similar size together, so an entry is rewritten about once per tier. On 200,000 random writes over a million possible keys, with a 1,000-entry memtable:

EngineWrite ampSpace ampWorst point read
LSM, leveled, T = 1010.2×1.03×3 files
LSM, leveled, T = 49.6×1.05×4 files
LSM, size-tiered, T = 103.9×1.05×2 files
LSM, size-tiered, T = 44.9×1.07×5 files
B+ tree, 64 per page, 100 pages cached60.4×1.44×4 pages
B+ tree, 64 per page, 1,000 pages cached30.2×1.44×4 pages
B+ tree, 64 per page, 100,000 pages cached3.6×1.44×4 pages

A B-tree’s write cost depends mostly on its cache. Each random insert changes one entry in a page of 64, and if that page is evicted before another write lands on it, the whole page is written for that one entry. With little cache the B+ tree writes 60.4 times the data; with enough cache to hold the whole tree, pages are written roughly once each and the cost falls to 3.6×. This is why B-tree databases want their working set in memory, and why LSM trees suit write-heavy workloads on data much larger than memory.

Read amplification

A point lookup in a B-tree reads one page per level, and the upper levels are almost always cached, so in practice one or two disk reads. An LSM tree has to check the memtable, every level-0 file and then one file per level, newest first, and a key that doesn’t exist has to be looked for everywhere. LSM engines keep a Bloom filter for each file, which rules out most files without reading them, and cache the index blocks; range scans still have to merge every level. Size-tiered compaction leaves more runs to search than leveled.

Space amplification

B-tree pages are rarely full: after random inserts, leaves split in half and average about 69% full (the simulation measured 69%), so the tree uses about 1.45 times the space of its data. An LSM tree keeps dead versions of updated keys until compaction removes them. With mostly new keys that costs little, but when the same 20,000 keys are updated over and over, leveled compaction held 1.61 times the live data and size-tiered 4.85 times, since tiers hold several copies of the same keys until they merge.

The RUM trade-off

Athanassoulis and colleagues’ RUM conjecture (EDBT 2016) states that an access method can bound at most two of its read overhead, update overhead and memory or space overhead. The table above shows it: leveled compaction spends writes to keep reads and space low, tiered compaction saves writes at the cost of reads and space, and a B-tree reads cheaply but pays for random updates unless it has memory to spare. Choosing a storage engine, or tuning one, is choosing where on that triangle to sit.

Storage engine questions and answers

What is write amplification?

The ratio of bytes a storage engine writes to disk to the bytes the application asked it to write. Logging, compaction and rewriting whole pages all add to it. In this page’s run of 200,000 random writes, leveled compaction with a size ratio of 10 wrote about 10.2 times the data, size-tiered compaction 3.9 times, and a B+ tree with 64 entries per page and 1,000 cached pages 30.2 times.

Why are LSM trees faster for writes?

They turn random writes into sequential ones: writes go to an in-memory table, which is flushed as a sorted file, and files are merged in the background in large sequential passes. A B-tree updates pages in place, so each random write eventually rewrites a whole page for one changed entry, unless the page is still cached when the next write to it arrives.

Why are B-trees better for reads?

A key lives in exactly one place, found by walking one path from the root: a few pages, the top ones nearly always cached. An LSM tree may have to look in the memtable, several level-0 files and one file per level, which is why LSM engines keep a Bloom filter per file to skip the files that cannot contain the key.

What is the difference between leveled and tiered compaction?

Leveled compaction keeps each level as one sorted run and merges files down level by level; it rewrites data more often but keeps few runs and little garbage. Tiered (size-tiered or universal) compaction lets several runs pile up at each level and merges them together; it writes less but reads and stores more. With a workload of repeated updates to 20,000 keys, leveled compaction stored 1.61 times the live data and tiered 4.85 times.

What is the RUM conjecture?

The observation by Athanassoulis and colleagues that an access method can optimise for at most two of read overhead, update overhead and memory (space) overhead, at the expense of the third. B-trees, leveled LSM trees and tiered LSM trees sit at different points of that trade-off.

Further reading

Published Updated