Chapter 09 · Storage Engines: WAL, B-Trees, LSM Trees, Memtables, SSTables, and Compaction

LSM Trees: Memtables, Immutable Sorted Files, Write Amplification, and Read Amplification

Trace AtlasMart writes through a WAL, memtable, immutable sorted runs, point reads, flushes, and compaction while measuring simple read, write, and space amplification signals.

Intermediate100–120 minutesLSM flush/read/compaction labPython 3.13+ · standard libraryVendor-neutral mechanismsLast reviewed: August 2026

Learning outcomes

AtlasMart telemetry accepts bursts of writes faster than a single page-oriented update path can comfortably absorb. An LSM-style storage engine changes the shape of the work: append recovery information, update an ordered in-memory memtable, flush immutable sorted files, and later compact them. Fast foreground writes therefore create background obligations rather than eliminating storage cost.

01

Define LSM tree, memtable, immutable memtable, SSTable/sorted run, flush, level/tier, and compaction.

02

Trace a write through WAL and memtable into immutable sorted files.

03

Trace a point read across memtable and multiple files and identify read amplification.

04

Measure simple logical-versus-physical write counts to expose write amplification.

05

Explain why Bloom filters, indexes/summaries, caches, and compaction policy modify—but do not erase—the tradeoffs.

Tooling and version snapshot · checked 29 August 2026

The mandatory lab uses Python 3.13+ standard library only; it has no database server, driver, container, cloud, or paid-feature dependency. Optional implementation references were rechecked against PostgreSQL 18 documentation (PostgreSQL License), SQLite 3.53.4 (public domain), Apache Cassandra 5.0.9 (Apache License 2.0), and RocksDB 11.1.2 (Apache 2.0 or GPLv2 at the user's option). Product defaults are examples, not portable storage-engine guarantees.

1. The LSM write path converts random update pressure into append/batch work

A Log-Structured Merge (LSM) tree maintains recent writes in memory and periodically flushes them as immutable sorted runs. A common path is: append to a recovery log; update a memtable; acknowledge according to durability policy; freeze the memtable when a threshold is reached; write it as an immutable SSTable (sorted-string-table style file); then compact files in the background.

Because flushed files are immutable, updating sku-1 does not necessarily rewrite its older on-disk file immediately. Newer versions can coexist across runs until compaction. That improves foreground write locality but introduces version search and background merge work.

2. Reads must reconcile recency across several possible locations

A point read typically checks the mutable memtable, immutable memtables waiting to flush, and candidate SSTables from newest to oldest or according to level metadata. Index blocks/key ranges can rule out files; Bloom filters can cheaply say “definitely not here” for many absent keys; block caches avoid repeated device reads. Yet a miss can still probe multiple candidates, especially when many overlapping runs exist.

Read amplification is the extra physical lookup/read work required per logical read. It is not simply “number of SSTables,” because filters, indexes, cache hits, compression blocks, and storage layout matter.

3. Flushes and compaction create write and space amplification

Flushing writes logical data once into an SSTable; later compactions may rewrite it multiple times. Write amplification compares physical bytes written with logical bytes accepted. Space amplification compares physical footprint with live logical data, including stale versions, tombstones, temporary compaction outputs, indexes, filters, and metadata.

Leveled strategies generally constrain overlap to improve reads/space at the cost of more rewriting; tiered/universal strategies can reduce some rewrite pressure while retaining more overlapping runs. Exact behavior is product-specific and configuration-sensitive.

4. Wrong approach: disable compaction because it “steals I/O”

Turning off or starving compaction can briefly raise foreground write throughput, but the debt accumulates: more files overlap, misses inspect more candidates, stale versions consume space, deletion reclamation stalls, and eventually the engine may throttle/stall writes to protect itself. The apparent optimization moves cost into the future and often worsens tail latency.

The safe response is to measure incoming write rate versus sustainable flush+compaction throughput, file counts by level/tier, compaction backlog, bytes rewritten, cache/filter effectiveness, and storage headroom. Rate-limit or scale background work before the debt becomes an incident.

5. AtlasMart lab — memtable flush, overlapping runs, and compaction

python · lsm_path.py
class ToyLSM:
    def __init__(self, flush_limit=3):
        self.flush_limit = flush_limit
        self.wal = []
        self.mem = {}
        self.sstables = []  # oldest -> newest
        self.flush_entries_written = 0
        self.compaction_entries_written = 0

    def put(self, key, value):
        self.wal.append((key, value))
        self.mem[key] = value
        if len(self.mem) >= self.flush_limit:
            self.flush()

    def flush(self):
        if not self.mem:
            return
        run = dict(sorted(self.mem.items()))
        self.sstables.append(run)
        self.flush_entries_written += len(run)
        self.mem.clear()

    def get(self, key):
        probes = 0
        if key in self.mem:
            return self.mem[key], probes, "memtable"
        for i, run in enumerate(reversed(self.sstables), start=1):
            probes += 1
            if key in run:
                return run[key], probes, f"sstable-newest-{i}"
        return None, probes, "absent"

    def compact_all(self):
        merged = {}
        for run in self.sstables:
            merged.update(run)
        self.compaction_entries_written += len(merged)
        self.sstables = [dict(sorted(merged.items()))]

lsm = ToyLSM(flush_limit=3)
for kv in [
    ("sku-1", 10), ("sku-2", 20), ("sku-3", 30),
    ("sku-1", 11), ("sku-4", 40), ("sku-5", 50),
    ("sku-1", 12), ("sku-2", 21), ("sku-6", 60),
]:
    lsm.put(*kv)
lsm.flush()

print("SSTables before compaction:")
for i, run in enumerate(lsm.sstables, 1):
    print(i, run)
print("read sku-1:", lsm.get("sku-1"))
print("read missing sku-99:", lsm.get("sku-99"))
print("flush entries written:", lsm.flush_entries_written)

lsm.compact_all()
print("SSTables after compaction:", lsm.sstables)
print("read missing after compaction:", lsm.get("sku-99"))
print("compaction entries rewritten:", lsm.compaction_entries_written)
print("logical puts:", len(lsm.wal))
print("simple write-amplification numerator:", lsm.flush_entries_written + lsm.compaction_entries_written)
text · expected output
SSTables before compaction:
1 {'sku-1': 10, 'sku-2': 20, 'sku-3': 30}
2 {'sku-1': 11, 'sku-4': 40, 'sku-5': 50}
3 {'sku-1': 12, 'sku-2': 21, 'sku-6': 60}
read sku-1: (12, 1, 'sstable-newest-1')
read missing sku-99: (None, 3, 'absent')
flush entries written: 9
SSTables after compaction: [{'sku-1': 12, 'sku-2': 21, 'sku-3': 30, 'sku-4': 40, 'sku-5': 50, 'sku-6': 60}]
read missing after compaction: (None, 1, 'absent')
compaction entries rewritten: 6
logical puts: 9
simple write-amplification numerator: 15

Interpretation

Before compaction, sku-1 has several historical versions across sorted runs, while an absent lookup checks all runs in this toy model. After compaction, the miss probes one run and only the newest version remains. The cost is the extra entries rewritten during compaction. A real LSM uses file-range metadata, Bloom filters, block indexes, snapshots, sequence numbers, and sophisticated compaction selection, so the simulator is a mechanism map—not a performance model.

Check your understanding

  1. Why can an LSM acknowledge quickly without rewriting the old on-disk value immediately?
  2. What causes read amplification?
  3. Why is compaction necessary even if data is never deleted?
  4. What is the danger of sustained write rate above compaction capacity?
  5. Does “LSM” imply one compaction algorithm?
Review the answers

1. The new version is represented durably in the WAL and in the current memtable path, while immutable files are reconciled later.

2. A logical read may need metadata/filter/cache work and multiple candidate runs/levels before finding the newest value or proving absence.

3. It consolidates overlapping runs/versions and controls read/space behavior; implementations such as RocksDB explicitly use compaction for read efficiency.

4. File/backlog growth, more read/space amplification, eventual throttling/stalls, and tail-latency spikes.

5. No. Leveled, size-tiered/universal, time-window, FIFO, and custom policies make different tradeoffs.

6. Production judgment

LSM families are strong candidates for high write rates, append-heavy workloads, and designs where background compaction can be provisioned explicitly. They are not automatically superior for every point read, range scan, delete-heavy workload, or latency SLO. Evaluate amplification under a long-running steady state with realistic key skew and retention—not an empty-database ingest test.

Production dimension Questions to record before adopting the mechanism
Correctness / invariants Which writes may be lost, reordered, replayed, or observed through stale storage state? Which invariant is protected above the storage engine?
Durability / recovery What acknowledgement point is durable on the required failure domain? How are WAL replay, checkpoints, backups, replicas, and restore tests verified?
Latency / tails What happens to p95/p99 latency during fsync, page split, flush, compaction, cache miss, or device saturation rather than only during warm steady state?
Amplification / capacity Measure logical versus physical reads/writes and temporary disk headroom. Include compaction debt, tombstones, index/filter memory, and replication overhead.
Device / topology Account for SSD endurance, IOPS, bandwidth, fsync semantics, controller caches, filesystem behavior, replication placement, and failure domains.
Security / tenancy Protect WAL, temporary files, caches, backups, and storage metrics; noisy tenants can create compaction or cache-pressure side channels and denial of service.
Operations / rollback Define telemetry, failure injection, migration, version compatibility, format upgrades, rollback limits, and the skill needed to diagnose storage stalls.
Cost Price storage headroom, SSD replacement/endurance, extra replicas, cache memory, background I/O, backup retention, and engineering time—not only raw GB/month.

Bridge to Lesson 4: Compaction policy, tombstone lifetime, Bloom-filter behavior, and caches decide much of the real LSM operating envelope. We now make those components—and their failure modes—observable.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.