AnyLearn
All lessons
Programmingadvanced

Storage Engines and Page Layout

Disk geometry dictates database design. Learn why sequential I/O beats random I/O by 100x, how fixed-size pages and slotted page headers work, why row-stores and column-stores suit different workloads, how the buffer pool keeps hot pages in RAM, and how the write-ahead log makes durability cheap.

Not signed in โ€” your progress and quiz score won't be saved.
Lesson progress1 / 9

Why I/O access patterns dominate everything

A modern NVMe SSD delivers ~500K random 4 KB reads per second but can sustain >3 GB/s sequential throughput. HDDs are worse: a 7200-RPM disk costs ~5 ms of seek time per random access โ€” 200 random reads per second vs 100+ MB/s sequential. That 1000x gap is why database engineers obsess over access patterns before algorithm complexity.

The core rule: minimize random I/O; maximize sequential I/O. Every structural choice โ€” B-trees, LSM-trees, column storage, buffer pools โ€” is downstream of this physics. A "fast" algorithm that causes random disk access will lose to a "slow" algorithm that reads sequentially.

Full lesson text

All 9 steps on one page โ€” for reading, reference, and search.

Show

1. Why I/O access patterns dominate everything

A modern NVMe SSD delivers ~500K random 4 KB reads per second but can sustain >3 GB/s sequential throughput. HDDs are worse: a 7200-RPM disk costs ~5 ms of seek time per random access โ€” 200 random reads per second vs 100+ MB/s sequential. That 1000x gap is why database engineers obsess over access patterns before algorithm complexity.

The core rule: minimize random I/O; maximize sequential I/O. Every structural choice โ€” B-trees, LSM-trees, column storage, buffer pools โ€” is downstream of this physics. A "fast" algorithm that causes random disk access will lose to a "slow" algorithm that reads sequentially.

2. Fixed-size pages: the universal currency

Databases split storage into fixed-size pages (typically 4 KB, 8 KB, or 16 KB โ€” matching or being a multiple of the OS page size). Every read and write is an integer number of pages. Benefits:

  • Predictable addressing: byte_offset = page_id * page_size โ€” no scanning to find page nn.
  • Buffer pool simplicity: the pool allocates uniform frames; no fragmentation bookkeeping.
  • Atomic replacement: OS guarantees single-sector writes are atomic; databases extend this to a page with checksums and WAL.

Postgres defaults to 8 KB; SQLite to 4 KB; InnoDB to 16 KB. Going larger helps sequential scans; going smaller helps small random point lookups โ€” there is no universally correct answer.

3. Heap files and slotted pages

The simplest on-disk table layout is a heap file: an unordered collection of pages. Inside each page, a slotted page design handles variable-length rows without external fragmentation.

ZoneDirectionContains
Page headerfixed startpage LSN, checksum, free-space pointer
Slot arraygrows right(offset, length) pairs for each row
Row datagrows leftactual row bytes packed from the end

The slot array is stable โ€” external indexes store (page_id, slot_id), not byte offsets. When a row is updated in place and needs more space, its data moves but the slot pointer is patched; indexes are unaffected. Deletion zeroes the slot; VACUUM reclaims space lazily.

4. Row-oriented vs column-oriented storage

Row stores serialize an entire row contiguously โ€” ideal for OLTP where a query fetches one row (SELECT * WHERE id = 42). Column stores serialize a single column across all rows โ€” ideal for OLAP aggregations that touch one column of millions of rows.

PropertyRow store (Postgres, MySQL)Column store (DuckDB, Redshift)
Write (INSERT)1 page writeN column writes
Full row fetch1 page readN column reads
Aggregate over 1 colread all row dataread 1 column only
Compression ratiolow (mixed types)high (same type, sorted)
Vectorized executionawkwardnatural โ€” SIMD over arrays

Column stores also enable vectorized execution: a tight loop over an array of int64s is SIMD-friendly in a way that a loop over heterogeneous row structs is not.

5. The buffer pool: your database's page cache

The buffer pool keeps recently-used pages in RAM, serving most reads with zero disk I/O. It is typically the single most important configuration knob (shared_buffers in Postgres, innodb_buffer_pool_size in MySQL โ€” commonly 25% of RAM on a dedicated server).

Key mechanics:

  • Page table: hash map from (file_id, page_id) to a buffer frame pointer โ€” O(1) lookup.
  • Eviction: most engines use LRU-K (tracks last K accesses) to avoid large sequential scans evicting hot OLTP pages.
  • Dirty pages: modified in memory but not yet flushed. Before eviction, a dirty page must be written to disk โ€” but only after WAL has recorded the change.
  • Pinning: a page in active use is pinned; the eviction thread cannot steal it mid-query.

6. The write-ahead log (WAL) and durability

Flushing every dirty page on every commit would be agonizingly slow. WAL makes this fast and safe simultaneously with one rule: log the change before applying it to the page.

On commit, only the log records need to be fsynced; data pages can be flushed lazily by a background writer. On crash, the engine replays the log from the last checkpoint to reconstruct any page not yet written to disk.

-- Postgres: inspect recent WAL records
SELECT lsn, xid, resource_manager, record_length
FROM pg_walinspect(
  pg_current_wal_lsn() - '1MB'::pg_lsn,
  pg_current_wal_lsn()
);

WAL wins because it converts random data-page writes into sequential log appends โ€” the fastest possible disk access pattern.

7. Write path through WAL and buffer pool

A transaction write touches the log before touching the page:

flowchart TD
  T["Transaction: UPDATE row"]
  WB["Generate WAL record (LSN assigned)"]
  WL["Append WAL record to log buffer"]
  BP["Modify page in buffer pool (dirty bit set)"]
  FC["COMMIT: fsync log buffer to disk"]
  BG["Background writer flushes dirty pages to disk"]
  CK["Checkpoint: advance redo point in WAL"]
  T --> WB
  WB --> WL
  WL --> BP
  BP --> FC
  FC --> BG
  BG --> CK

8. Checkpoints and crash recovery

If the WAL grows forever, crash recovery replays forever. Checkpoints bound recovery time: flush all dirty buffer-pool pages to disk, write a checkpoint LSN to the WAL. On restart, REDO only replays records after the last checkpoint.

Postgres triggers a checkpoint every checkpoint_timeout seconds (default 5 min) or when max_wal_size is reached. The flush is spread over checkpoint_completion_target (default 0.9) of the interval to avoid I/O spikes.

Gotchas:

  • Too-frequent checkpoints spike I/O and hurt write throughput.
  • Too-infrequent checkpoints make crash recovery slow and let WAL files accumulate on disk.
  • Watch pg_stat_bgwriter โ€” if checkpoints_req (triggered by WAL size) exceeds checkpoints_timed, increase max_wal_size.

9. Putting it together: read vs write paths

Read path (SELECT * FROM orders WHERE id = 99):

  1. Hash (file, page_id) โ†’ check buffer pool. On hit, return immediately โ€” no disk I/O.
  2. On miss, pick a victim frame (evict dirty frame if needed), read page from disk, insert into pool, return.

Write path (UPDATE orders SET status = 'shipped' WHERE id = 99):

  1. Generate WAL record with before/after-image.
  2. Append to in-memory WAL buffer.
  3. Apply change to buffered page (mark dirty).
  4. On COMMIT: fsync WAL buffer. Caller gets confirmation. Done.
  5. Background: dirty page eventually flushed by bgwriter or checkpoint.

The asymmetry is intentional: reads can tolerate stale pages from RAM; writes must serialize through the log first to guarantee durability.

Check your understanding

The lesson ends with a 5-question quiz. Take it in the player above to see your score.

  1. Why do databases use fixed-size pages rather than variable-size records?
    • Fixed pages eliminate the need for a buffer pool entirely.
    • Fixed pages enable predictable addressing and simpler buffer-pool frame management.
    • Fixed pages reduce the total storage size of tables.
    • Fixed pages are required by the OS for memory-mapped file access.
  2. In a slotted page, external indexes store which identifier for a row?
    • The byte offset of the row within the page.
    • The row's primary key value.
    • A (page_id, slot_id) pair.
    • The offset of the row from the end of the page.
  3. A query runs `SELECT AVG(revenue) FROM sales` over 500 million rows. Which storage layout finishes fastest?
    • Row store, because all row data is co-located on disk.
    • Column store, because only the revenue column pages are read.
    • Row store, because it avoids the overhead of column reconstruction.
    • Both perform identically; CPU is always the bottleneck.
  4. The WAL rule states that a log record must be fsynced BEFORE the corresponding data page is evicted. What does this enable?
    • It allows the buffer pool to be eliminated on modern SSDs.
    • It allows data pages to be flushed lazily while still guaranteeing crash recovery via log replay.
    • It allows checkpoints to be skipped indefinitely.
    • It guarantees that transactions never need to be rolled back.
  5. What is the primary performance cost of making checkpoints too frequent?
    • Crash recovery time increases proportionally.
    • The WAL grows without bound and fills the disk.
    • Bursty I/O spikes as all dirty pages are flushed to disk repeatedly.
    • Sequential read throughput drops because page IDs become non-contiguous.

Related lessons