SwadhyAI Notes

All System Design notes

System Design · Data Foundations

Storage Engines: Pages, Logs, and Compaction

A database can acknowledge a write before its table file changes, spend more disk bandwidth rewriting old values than storing new ones, and become slow while its CPU is mostly idle. Those behaviors make sense once you follow the : the part that turns reads and writes into recoverable bytes.

This lesson stays on one machine before replication. You will connect memory, disk layout, crash recovery, , and , then use those mechanisms to explain real latency and capacity limits.

Before this lesson, review Database Indexing for access paths and Transactions & Isolation for what readers may observe. Here we explain the storage work underneath.

One write, one machine

1

Accept

The engine places the change in memory and records recovery evidence.

2

Make durable

A WAL flush, or an LSM flush path, decides when “committed” is safe.

3

Organize later

Dirty pages write back, or compaction merges sorted files.

4

Recover

After a crash, replay committed work the data files still lack.

1. Start with the contract, then follow the bytes

The query layer decides which records a request needs. The storage engine decides how to locate those records, make updates durable, and recover a consistent state after interruption. “The write succeeded” needs a boundary: process memory, stable local storage, or a required set of replicas? Those are different promises.

Disk devices transfer data in blocks. Engines organize records into pages or sorted files. A keeps recently used data in memory. A modified page is a because memory and the data file disagree. Dirty means “needs eventual writeback,” not “corrupt.”

Request path through one machine

The client never talks to pages directly. Durability and recovery live in the engine’s log and file layout.

What the API promised

  • Which key range or rows were read or updated
  • Whether the transaction committed
  • What isolation a concurrent reader may observe

What the engine actually did

  • Which pages or files were touched
  • When the log became durable
  • What recovery would replay after a crash

2. B+ trees turn sorted keys into a small number of page visits

A keeps routing keys in internal pages and indexed entries in leaf pages. Each internal page points to many children, so a large index can stay shallow. For an illustrative tree with 200 child pointers per internal page, three branching steps can address up to 200³ = 8 million leaf pages when full. Real occupancy is lower; the point is high fanout keeps the path short.

B+ tree shape for key 42

Lookup walks the highlighted path. A range scan then follows the dashed leaf links instead of restarting from the root.

An insertion changes a leaf. When space runs out, a split redistributes entries and adds a separator to the parent; that can propagate upward. Updating an indexed column may also change secondary index entries, so “one row write” is not necessarily one storage operation. See the PostgreSQL B-tree chapter for one mature implementation.

3. Write-ahead logging separates durable commit from page writeback

Suppose a transfer must debit one account page and credit another. If the engine wrote the first page to the data file and then crashed before writing the second, recovery would see a half-applied transfer unless it had independent evidence of the intended change. A is that evidence. The engine appends redo information describing both page changes, then forces the relevant log — including the commit record for a synchronous durable commit — onto stable storage before it is allowed to consider the transaction durable. Only after that ordering rule is satisfied may the dirty data pages be written back, and they need not be written back before the client hears success.

That design is why commit latency is dominated by sequential log durability rather than by random page I/O for every touched row. It is also why a buffer pool full of dirty pages is normal rather than alarming: dirty means “memory is ahead of the data file,” not “the database is corrupt.” The trade you accept is that crash recovery must replay committed log records the data files still lack, and that an incomplete transaction with no durable commit record must never become visible as committed.

Durable commit without waiting for every data page

Steal / no-force style path

1

Begin transfer

Memory only

Pages A and B become dirty in the buffer pool. The data files still hold the old images.

2

Append WAL

Sequential append

Redo for A and B is written into the log, usually with a rising log sequence number so recovery can order work.

3

Force the commit record

Durability gate

An fsync, often shared across several transactions as group commit, makes the commit durable on stable storage.

4

Acknowledge the client

Client-visible

Success can return now. A crash after this point must still recover the committed transfer from the log.

5

Write pages back later

Optional delay

Background writeback and checkpoints move dirty pages to the data files and advance the safe recovery start.

Why this is fast

  • One sequential log flush replaces many random page writes at commit time.
  • Group commit shares that fsync across several waiters, so commit latency is paid once for a batch.
  • Dirty pages can linger in memory until writeback pressure or a checkpoint needs them on disk.

What recovery trusts

  • A durable commit record is evidence the transaction finished.
  • A current data-page image is helpful, not sufficient by itself after a crash.
  • Checkpoints shrink how far recovery must walk; they do not redefine what “committed” meant.

Caller trap

A timeout after step 3 can still be a committed transfer. Retry the business action only with an idempotency key or an operation identity that can discover the earlier result.

batches several commit records behind one durability flush so many waiters share the cost of a single fsync. That raises throughput and can add a small batching delay; it does not weaken the rule that a returned success already has a durable commit record. persist enough dirty data and checkpoint metadata to give recovery a later starting point, which bounds restart time without forcing every later commit to wait for every dirty page. Replica shipping, backups, and archival retention can still require older log segments after local crash recovery no longer does. PostgreSQL's WAL introduction is a concrete walkthrough of one mature instance of this pattern.

Crash explorer: increment a durable counter from 10 to 11

Assume one synchronous, durable transaction on a healthy local storage device. This illustrates a transactional WAL engine; exact recovery algorithms differ. Select when the process crashes.

Durable evidence. The log contains the update to 11 and its durable commit record. The data page still contains 10.

After recovery. 11: recovery replays the committed change even though the data page had not been written.

What the client knows. The caller saw no success response, although the operation committed. Retrying an increment without deduplication could produce 12.

A durable log is enough for recovery; a current data page is not required at this point.

Simple rule of thumb

A timeout tells you what the caller knows, not what the database decided. If the commit record may already be durable, retry the business action only through an idempotent operation identity that can discover the earlier result instead of applying the transfer twice.

Try it: follow one write through the engine

Pick B-tree + WAL or LSM, then step a write through memory, durable log, flush or writeback, crash recovery, and lookup. The highlighted panel is the stage that just moved.

Acknowledged
0
Waiting on WAL
No
Dirty pages
0
Engine state
Accepting

Buffer pool

memory
rootclean

keys: 5, 20, 40

Write-ahead log

durable evidence

Empty. Writes append here first.

Data pages on disk

after writeback

leaf-a

keys: 5

leaf-b

keys: 20

leaf-c

keys: 40

B-tree + WAL mode. Write a key, force a durable commit, then optionally crash before page writeback.

B-tree path. Write, flush WAL, then crash before writeback. Recovery still restores the committed key because the log carried the evidence. Write back pages first and the same crash has less redo.

LSM path. Fill the memtable until it freezes, flush to level 0 twice, then compact. If you keep writing while an immutable table is waiting, the lab stalls on purpose.

Model scope: one process, tiny fanout, no replicas, and simplified page/file layouts. Real engines add checksums, group commit batching, Bloom filters, and richer compaction pickers — the stages here are the ones those details hang on.

4. An LSM tree makes writes sequential, then organizes them later

An takes the same appetite for sequential durability and pushes it further into the data layout itself. Incoming updates first land in an ordered in-memory structure called a . A WAL can make those memory updates recoverable under the acknowledgment policy you chose. When the mutable memtable fills, the engine freezes it into an immutable memtable, opens a fresh mutable one so writers can continue, and later flushes the frozen contents into an immutable sorted file, commonly called an . The important consequence is deferred organization: updating key K does not immediately seek out and overwrite every older K on disk.

LSM write path: sequential now, organized later

Memory → L0 → compacted levels

In memory

Mutable memtable

10 · 17 · 24

Newest writes land here, usually in a skiplist or similar ordered structure.

Immutable memtable

3 · 11 · 19

Read-only copy waiting for a background flush while a fresh mutable table accepts new writes.

On disk

Level 0

ranges may overlap

sst-1

keys 1–40

sst-2

keys 20–70

Level 1+

tighter ranges

sst-3

Non-overlapping slices after merge

WAL beside the memtable. Memory updates become crash-safe only after the matching log records are durable under your acknowledgment policy.
Flush ≠ compaction. Flush moves one frozen memtable to a file. Compaction rewrites existing files to drop obsolete versions and tighten ranges.
Reads climb from the top. Newest evidence wins, which is why a backlog of overlapping L0 files hurts every lookup.

Newer and older versions therefore coexist until merges sorted files, preserves versions still required by readers or snapshots, and drops obsolete data when it is safe to do so. A flush and a compaction are different jobs. Flush moves one frozen memory table onto disk. Compaction rewrites existing files, which is where much of the steady-state write amplification comes from. If you confuse the two, you will misread both capacity charts and latency incidents.

What you buy

  • Sequential appends for ingest instead of a random page update on every write
  • High sustained write throughput while background compaction stays ahead of debt
  • A natural fit for write-heavy and time-ordered workloads that tolerate deferred reorganization

What you owe later

  • Background rewrite bandwidth that must fit inside the device budget
  • Reads that may inspect several versions and overlapping files before they settle
  • Temporary space amplification until obsolete values and tombstones are dropped

Making a new file visible also requires a recoverable metadata transition that names which files belong to the current database version. If the process creates a new SST and crashes before that metadata commits, recovery must not lose live data or expose the wrong file set. Copying arbitrary SST files out of a live directory is therefore not a consistent backup. RocksDB documents this role through its MANIFEST metadata.

5. Reads reconcile versions; filters only avoid unnecessary work

A point lookup in an LSM engine is a newest-first search across evidence, not a single file open. The engine checks the mutable memtable, then any immutable memtables still waiting to flush, then candidate files from newer levels toward older ones, using sequence numbers or equivalent versioning to decide which value is visible for the reader's snapshot. Freshly flushed level-zero files often overlap in key range, so several of them may be relevant. Later levels, after leveled compaction, usually arrange files so a key maps to at most one file per level. A backlog of overlapping level-zero files therefore raises read amplification on every lookup until compaction catches up.

Point lookup walks newest evidence first

get(K) decision path

Caller asks get(K)

The engine must return the newest visible version for this snapshot, including a tombstone that means “deleted,” not the newest physical file by accident.

Memory first

Mutable memtable

Hit here and the search stops.

Immutable memtable

Checked next if the mutable table missed.

Block cache can satisfy a later file read without touching the device, but it does not replace the newest-first version rule. A cached older block still loses to a newer memtable or L0 version.

Level 0 · overlapping files

Bloom says no

Skip the file. An inserted key is not falsely reported absent.

Bloom says maybe

Open the index and check. False positives are allowed.

Because L0 ranges can overlap, several files may need this check. That is why an L0 backlog shows up as read amplification.

Lower levels

After leveled compaction, a key usually maps to at most one file per level. The search continues only while newer layers missed, then returns the visible value or “not found.”

A answers a cheaper question first: could this candidate file contain K at all? A negative answer lets the engine skip the file. A positive answer is only “maybe,” so the engine still has to consult the file's index and, on a real candidate, read the block. Under correct construction and the supported lookup semantics, an inserted key is not falsely reported absent; false positives are the price you pay for a compact filter. Filters consume memory and help a lot when many files cannot contain the key. They cannot shrink a scan that genuinely returns a million rows.

Range merge hides obsolete versions

Stream A

  • 40:v3
  • 45:v1

Stream B

  • 42:v2
  • 45:tomb

Stream C

  • 41:v1
  • 50:v1

Visible for this snapshot

40 · 41 · 42 · 50

45 disappears because the newer tombstone wins

A range scan makes the version problem even more visible. Imagine three sorted streams that together cover keys 40 through 50. The engine merges those streams by key and version, suppresses obsolete values and deletions according to the snapshot, and emits only the visible result. In the diagram above, key 45 disappears because a newer tombstone wins for that snapshot, while an older snapshot might still need the previous live value. “Keep only the newest value everywhere” is safe only relative to the visibility rules of every retained reader. See RocksDB's filter documentation for one concrete filter implementation and its memory trade-offs.

6. A deletion marker protects you from resurrecting old data

Suppose an older file contains user:7 = active, while a newer file contains a for that key. The marker tells reads to suppress the old value. If compaction removes the marker but leaves the older file eligible for future reads, the deleted value can reappear.

Tombstone hides the older live value

Older SST

user:7 = active

Newer SST

user:7 = TOMBSTONE

Reader

not found

Drop the tombstone only when no retained reader still needs it to hide the older file. Logical delete is immediate; physical reclaim waits.

A replicated store adds another boundary: a lagging replica might later send back an older live value. Repair and rejoining rules must prevent that resurrection too. Cassandra's tombstone documentation is a concrete example of why local deletion and distributed deletion have different safety conditions.

7. Compaction policies move cost between writes, reads, and space

Compaction has no free universal setting. Merging more aggressively can reduce the files a read must inspect and remove stale versions sooner, but it rewrites bytes now. Keeping more sorted runs avoids some immediate rewriting while leaving more candidates for future reads.

Why two LSM engines can behave differently under the same API
PolicyOrganizationCost to examine
LeveledMaintain tighter key-range organization as files move downward.Overlapping ranges may be rewritten repeatedly; check write bandwidth under sustained updates.
Tiered / universal familyRetain several sorted runs and merge selected runs together.Reads may inspect more runs, and large merges can need considerable temporary space.
Workload-specific policyExploit a rule such as time-ordered data with bounded retention.Late updates or exceptions can undermine the rule that made cheap reclamation possible.

8. Calculate amplification before declaring the disk fast enough

compares bytes written below a chosen layer with logical bytes accepted above it. Read amplification describes extra reads for a logical read. Space amplification compares stored bytes with live logical data. Always name the boundary.

Write amplification as a bandwidth budget
Logical payload19.5 MiB/s
Engine writes ×10195 MiB/s
Device budget250 MiB/s

About 55 MiB/s remains for surprises on a 250 MiB/s mixed device budget. The sequential brochure number is not this path.

The advertised sequential benchmark is not a safe substitute for this mixed write path. Three replicas accepting the same stream multiply aggregate engine writes again.

Large values deserve separate attention. Rewriting a large value through multiple compactions can dominate bandwidth even when keys are small. Separating values from the sorted key structure can reduce that rewriting, but introduces another garbage-collection problem. The WiscKey paper explores that design.

9. Compaction debt becomes a latency incident

If writes create compaction work faster than background workers can finish it, debt accumulates. Larger memory buffers may absorb a burst; they do not fix a permanent throughput deficit. More frozen memtables or level-zero files eventually force a so the engine can protect bounded resources. RocksDB documents this as write stalls.

Compaction debt turns into a write stall

Writers

Sustained ingest

Mem / L0

Pending merges grow

Compaction

Falls behind device budget

Admit path

Stall until debt drains

Rising pending-compaction bytes plus growing file counts usually beat “CPU looks fine” as an explanation.

Good default: benchmark long enough for file structure and background work to reach a representative state. Include steady writes, a restart, and a period of degraded storage throughput. Measure tail read latency during compaction, write stalls, recovery duration, and temporary disk space together.

10. Use the mechanism to defend a storage decision

Lean B+ tree

  • Ordered scans and point lookups dominate
  • Small transactional updates
  • You can explain cache hits and page writes

Lean LSM

  • Sustained ingest is the main load
  • Compaction bandwidth and space headroom exist
  • You can explain read amplification under that policy

Either family can work well. The useful explanation connects the workload to the exact work the engine performs, not to a brand slogan.

Practice: throughput halves after two hours, but a restart temporarily helps. What would you investigate?

Treat this as a hypothesis exercise, not proof of an LSM bug. Compare the initial and degraded periods for compaction backlog, immutable memtables, level-zero file counts, stalls, and device latency. Check whether a restart merely empties queues or changes cache and workload behavior. Durable backlog usually does not disappear just because the process restarts.

Estimate how much background work the incoming payload generates and compare it with measured spare storage throughput. Then test a sustained lower admission rate. If debt drains and tail latency recovers, capacity or compaction scheduling is a stronger explanation. If the issue remains, inspect durability flushes, long snapshots, locks, and access-pattern changes before increasing worker counts blindly.

Practice and further reading

Use these exercises to connect the lesson to a design decision. State your assumptions, trace what happens to one request, and explain the rule that must remain true even during a failure. That rule is the design's correctness invariant.

Focus for this lesson: Trace a write from memory to the log and data files, then explain how crash recovery and compaction affect latency, durability, and storage capacity.

Explain the decision

How can a database safely acknowledge a transaction before its table page reaches disk?

Read the reasoning

For a synchronous durable transaction, the engine first makes the recovery information and commit record durable. The data page can then be written later because recovery can reconstruct the committed update from the log. The write-ahead rule also requires the relevant log to be durable before a changed page is persisted. This promise depends on the storage stack honoring durability requests; it does not, by itself, survive loss of the entire device. Replication adds a separate acknowledgement contract.

Apply it to a production scenario

A delete-heavy LSM workload uses more disk even though the visible row count falls. What could explain it?

Read one approach

Deleting a key usually writes a tombstone that hides older versions; it does not immediately erase every older file. Compaction must establish that dropping both the marker and the obsolete versions is safe. Retained snapshots, background compaction debt, and replicated repair requirements can delay reclamation. Inspect live versus total bytes, snapshot age, pending compaction work, and replica health. Shortening retention without understanding these readers and replicas can make deleted data reappear.

Work through the numbers

A replica accepts 20,000 one-KiB writes each second. Measured total engine write amplification is 10. What disk bandwidth is implied?

Read the worked calculation

The logical payload rate is 20,000 / 1,024 = 19.53 MiB/s. Multiplying by the measured factor of ten gives about 195.3 MiB/s of engine writes on that replica. Three equally loaded replicas produce about 585.9 MiB/s in aggregate, while each still needs its own 195.3 MiB/s. State whether the factor includes WAL, indexes, and compaction so you do not count those twice. Device-internal amplification and mixed-workload headroom require separate measurements.

Reason through a failure

The client times out after the commit log is durable but before receiving a reply. Is repeating an increment safe?

Read the recovery reasoning

The transaction may already have committed, so blindly repeating the increment can apply it twice. Recovery and client certainty are separate questions: the engine can recover the committed value while the caller still lacks a response. Use a stable operation identifier and a transactional deduplication record, or query the durable operation status. A retry must preserve the identity of the original operation; generating a new identifier defeats deduplication.