Notes
Write-ahead log
Before either tree, one shared trick. Every write is first appended to a write-ahead log and flushed to disk. Only then does the engine touch its real data structure. If the process dies halfway, it replays the log on restart. Appending to a file is the fastest thing a disk can do, so the WAL makes writes both durable and cheap.
- append the change to a log file
- fsync it: now it is durable
- then update the real structure
- crash in between? replay the log
- appending is the fastest thing
- a disk can do
B-tree
Option one: the B-tree, the engine under PostgreSQL, MySQL's InnoDB and most relational databases. Data lives in fixed-size pages, usually eight kilobytes. The root page points to branch pages, which point to leaf pages holding the rows. Each page holds hundreds of keys, so a billion rows is only four levels deep. Finding a row is four page reads, and three of those are almost always cached in RAM.
disk = fixed pages (8 KB) root → branch → leaf each page: sorted keys + pointers to child pages fan-out ~500 per page 1 billion rows = 4 levels lookup = 4 page reads
B-tree writes
Writing is where the B-tree pays. To change a row you find its leaf page and rewrite that whole page, eight kilobytes for a hundred-byte change. If the page is full, it splits in two and the parent is updated. And because rows land wherever their key says, writes are scattered randomly across the disk. Reads: great. Heavy random writes: the weak spot.
- find the leaf, change it in place
- rewrite the whole 8 KB page
- even for a 100-byte row
- page full? split into two,
- update the parent
- random writes, all over the disk
LSM tree
Option two: the log-structured merge tree. A write goes to the WAL and into a memtable, a sorted structure in RAM. That's it; the write is done in a microsecond. When the memtable fills, say sixty-four megabytes, the engine writes it out as one sorted file, an SSTable, in a single sequential pass. That file is never modified again. Every disk write is sequential, which is why Cassandra can take a hundred thousand writes a second on one node.
write → WAL + memtable (RAM, sorted, e.g. a skip list) memtable full (64 MB)? write it to disk as one sorted file: an SSTable never modified again writes are sequential, always
LSM reads
Now the LSM pays. A key could be in the memtable or in any SSTable, so a read checks the memtable and then every file from newest to oldest; the newest copy is the current value. Twenty files could mean twenty disk reads. Two fixes. Each SSTable carries a Bloom filter, a tiny bitmap that says 'this key is definitely not in here', so most files are skipped without a read. And compaction keeps the file count down.
- check the memtable
- then each SSTable, newest first
- (newest wins: it is the latest
- version of that key)
- many files = many reads
- fixes: Bloom filter per file
- ("key definitely not here")
- + compaction (next)
Compaction
Compaction is a background job that merges several SSTables into one. Since every file is sorted, merging is a single sequential pass. Old versions of a key and deleted keys, marked with tombstones, are dropped on the way. Files are organised into levels, each ten times bigger than the last, so a read touches at most one file per level. The cost: compaction burns disk bandwidth and CPU, and can stall writes when it falls behind.
- background job: merge several
- SSTables into one bigger one
- (both sorted: a merge is a
- single sequential pass)
- drops overwritten versions
- and deleted keys (tombstones)
- levels: L0 10 MB → L1 100 MB
- → L2 1 GB …
The trade
Engineers compare engines by amplification. Write amplification: how many bytes hit the disk per byte you wrote. The B-tree rewrites a whole page per row. The LSM rewrites each row once per compaction level. Read amplification: how many disk reads per lookup. The B-tree needs about one. The LSM needs one per level, minus whatever Bloom filters skip. Neither wins; they trade.
write amplification: bytes written to disk per byte the app wrote B-tree: rewrite 8 KB per row LSM: rewrite on each compaction read amplification: disk reads per lookup B-tree: ~1 (rest cached) LSM: 1 per level, minus Bloom
In the wild
Who uses what. B-trees: PostgreSQL, MySQL, SQLite, Oracle, SQL Server. LSM trees: Cassandra, HBase, LevelDB, and RocksDB, which sits under CockroachDB, TiDB and Kafka Streams. The pattern: relational databases that expect mixed reads and writes with transactions chose B-trees; write-heavy distributed stores chose LSM.
- B-tree:
- PostgreSQL, MySQL (InnoDB),
- SQLite, Oracle, SQL Server
- LSM tree:
- Cassandra, HBase, RocksDB
- (→ CockroachDB, TiDB, Kafka
- Streams), LevelDB, ScyllaDB
- MongoDB WiredTiger: B-tree
- with LSM option
Decision
How to choose. Read-heavy with point lookups, range scans and transactions: B-tree. A firehose of writes, logs, events, metrics, time series: LSM. In practice you rarely pick the engine directly; you pick it by picking the database. Choose PostgreSQL and you get a B-tree. Choose Cassandra and you get an LSM tree. Knowing which one you got explains how it behaves under load.
- read-heavy, point lookups,
- range scans, transactions
- → B-tree
- write-heavy: logs, events,
- metrics, time series, IoT
- → LSM tree
- most apps: B-tree is fine;
- you pick LSM by picking Cassandra
Say it like this
The interview answer. Both engines log every write first. The B-tree then updates pages in place, random I/O, but a lookup is one page read. The LSM appends to a memtable and flushes sorted files sequentially, so writes are fast, but a read may touch several files, which Bloom filters and compaction keep in check. Say 'write amplification' and 'read amplification' and the interviewer knows you've seen the inside.
- "Why is Cassandra fast at writes
- but PostgreSQL faster at reads?"
- 1. WAL makes any write durable
- 2. B-tree: in-place page writes,
- random I/O, one read per lookup
- 3. LSM: sequential flushes,
- many files per read,
- Bloom filters + compaction