Storage Engine Deep Dive
This module provides the internals knowledge required to work on storage engines at companies building database infrastructure. We cover PostgreSQL’s MVCC, vacuum, WAL, and recovery systems in production-level depth.Target Audience: Storage engine and infrastructure engineers
Prerequisites: Data Structures, Query Processing modules
Source Directories:
Interview Relevance: Staff+ database infrastructure roles
Prerequisites: Data Structures, Query Processing modules
Source Directories:
src/backend/storage/, src/backend/access/Interview Relevance: Staff+ database infrastructure roles
Part 1: MVCC Implementation
1.1 Transaction ID System
Real-world analogy: Transaction IDs are like the ticket numbers at a bakery. Every customer gets the next number in sequence. The critical difference is that PostgreSQL’s counter is only 32 bits — meaning it can only count to about 4 billion before it wraps around to zero. When it wraps, suddenly “new” tickets look like they came before “old” tickets, breaking visibility rules. This is the transaction ID wraparound problem, and it is why VACUUM’s “freezing” operation is not optional — it is an existential requirement that prevents the database from becoming unusable.1.2 Tuple Visibility Rules
1.3 Hint Bits
Real-world analogy: Hint bits are like sticky notes on a file folder. The first time someone needs to know whether a transaction committed, they have to walk to the filing cabinet (CLOG) and look it up. Then they stick a note on the folder saying “committed” or “aborted.” Every subsequent reader just looks at the sticky note instead of walking to the cabinet. The tradeoff: sticking the note makes the folder “dirty” (it has been modified), so it needs to be written back to disk eventually. Common pitfall: Hint-bit setting is the reason you sometimes see unexpected write I/O on a read-only replica. The first read of a tuple after a checkpoint may set hint bits, dirtying the page. This is normal behavior, not corruption, but it can be surprising when monitoring shows write activity on a server that should be read-only.1.4 CLOG (Commit Log)
The CLOG (now calledpg_xact/ on disk, though the internal terminology persists) is a remarkably compact data structure. It stores the status of every transaction that has ever run using just 2 bits per transaction. This means 4 billion transaction statuses fit in about 1GB of disk space — and only a small portion is cached in shared memory at any given time.
Practical tip: If you see high SLRUReadPage wait events in pg_stat_activity, backends are contending on CLOG buffer pages. This typically happens when many transactions are performing visibility checks on tuples whose hint bits have not been set yet (common after a crash recovery or after restoring from a backup). Running VACUUM on affected tables will set hint bits, eliminating the CLOG lookups.
Part 2: Physical Storage Layout
Understanding how bits are laid out on disk is essential for high-performance database engineering.2.1 The 8KB Page Structure
PostgreSQL data is stored in fixed-size Pages (default 8KB).- Item Identifiers (Linp): Array of 4-byte pointers to the actual tuples.
- Upper and Lower Pointers: Free space grows from the header down and the tuples grow from the bottom up. When they meet, the page is full.
2.2 Heap File Segments
Tables are stored in Relation Forks in thebase/<db_oid>/<rel_oid> directory.
- MAIN Fork: The actual data. Files are limited to 1GB segments (e.g.,
16384,16384.1,16384.2). - FSM (Free Space Map):
_fsmsuffix. Tracks free space per page using a tree structure for lookup of “a page with X bytes free”. - VM (Visibility Map):
_vmsuffix. Tracks page visibility for Vacuum skipping and Index-Only Scans.
2.3 The Buffer Manager & Dirty Pages
When a page is modified:- It is loaded into Shared Buffers.
- Modified in memory (“Dirtied”).
- WAL record is written to ensure durability.
- Checkpointer eventually flushes the dirty page to disk.
Part 3: Vacuum Deep Dive
2.1 Why Vacuum Exists
2.2 Vacuum Algorithm
2.3 Visibility Map
2.4 XID Wraparound Prevention
2.5 Autovacuum Tuning
Part 3: WAL (Write-Ahead Log) Deep Dive
3.1 WAL Architecture
3.2 WAL Record Format
3.3 Full Page Writes
3.4 Checkpoints
3.5 Checkpoint Tuning
Part 4: Recovery Process
4.1 Crash Recovery
4.2 LSN Tracking
4.3 PITR (Point-In-Time Recovery)
Part 5: Logical Decoding
5.1 Logical Replication Architecture
5.2 Output Plugins
5.3 Change Data Capture (CDC)
Part 6: Interview Questions
Storage Engine Deep Dive
Q: Explain how PostgreSQL prevents torn pages during writes
Q: Explain how PostgreSQL prevents torn pages during writes
Answer:PostgreSQL uses Full Page Writes (FPW) to prevent torn page corruption:
-
The Problem
- PostgreSQL pages are 8KB
- Disk sectors are 512B or 4KB
- If crash during write: some sectors written, some not
- Page becomes corrupted (torn)
-
The Solution
- First modification to a page after checkpoint: write entire 8KB page to WAL
- This is called a Full Page Image (FPI)
- Subsequent changes to same page: only write delta
-
Recovery Process
- Find last checkpoint
- If page is torn: restore from FPI in WAL
- Then apply subsequent WAL records
-
Trade-offs
- FPW increases WAL volume significantly
- Especially after checkpoint (many FPIs)
- Can be disabled with
full_page_writes=offif hardware/filesystem guarantees atomic 8KB writes - Some use checksums + ZFS for detection without FPW
Q: Why does PostgreSQL need VACUUM? How would you redesign it?
Q: Why does PostgreSQL need VACUUM? How would you redesign it?
Answer:Why VACUUM exists:
- MVCC creates multiple tuple versions
- Old versions must be preserved until no transaction needs them
- Dead tuples accumulate, causing bloat
- XID wraparound must be prevented (32-bit counter)
- Table bloat grows between vacuums
- Can cause significant I/O
- Must track all dead tuples in memory (TidStore)
- Index vacuuming can be expensive
-
Undo logs (Oracle, MySQL InnoDB)
- Store old versions in separate undo space
- Main table always has latest version
- No need to vacuum table (just undo log)
- Trade-off: Longer transactions = longer undo retention
-
Append-only with compaction (RocksDB, Cassandra)
- Never update in place
- Background compaction merges and removes old versions
- Trade-off: Write amplification
-
Garbage collection (CockroachDB)
- Similar to VACUUM but distributed
- Each range tracks its own garbage
- GC based on TTL, not transaction visibility
- Inline microvacuum on UPDATE (like HOT but for dead tuples)
- Incremental index vacuum (don’t scan whole index)
- Better parallelization of vacuum
Q: How does PostgreSQL ensure durability without syncing every commit?
Q: How does PostgreSQL ensure durability without syncing every commit?
Answer:Group Commit optimization:
-
Basic durability requirement:
- Transaction durability: once COMMIT returns, data survives crash
- Requires: WAL on persistent storage before ACK
-
Naive approach (slow):
- Each commit: write WAL, fsync, return
- Fsync is expensive (~1-10ms)
- Limits to ~100-1000 commits/sec
-
Group commit (what PostgreSQL does):
- Multiple backends write to WAL buffer
- One backend does fsync
- Fsync covers ALL pending commits
- All waiting backends released together
-
Configuration:
- Wait briefly for more transactions to batch
-
Async commit option:
- Very fast (no fsync wait)
- Risk: Lose last few milliseconds of commits on crash
- Acceptable for many workloads (analytics, logging)
Next Steps
Distributed Systems
Replication, consensus, and distributed transactions
Contributing to PostgreSQL
Submit your first patch to PostgreSQL
Interview Deep-Dive
Explain the WAL protocol in PostgreSQL. Why does writing to WAL first improve both durability and performance?
Explain the WAL protocol in PostgreSQL. Why does writing to WAL first improve both durability and performance?
Strong Answer:
- The WAL protocol guarantees that no data page modification is written to disk before the corresponding WAL record is flushed. On COMMIT, PostgreSQL flushes the WAL (fsync) but does NOT immediately flush the modified data pages. Dirty pages remain in shared_buffers and are written lazily by the background writer or checkpointer.
- Durability: if the server crashes after COMMIT, dirty data pages may be lost, but the WAL on disk contains enough information to reconstruct those pages during recovery by replaying from the last checkpoint.
- Performance: WAL writes are sequential appends to a single file. Data page writes are random I/O scattered across hundreds of files. Sequential I/O is 10-100x faster on spinning disks and 2-5x faster on SSDs. A transaction modifying 50 pages only needs one sequential WAL flush instead of 50 random page writes.
- The checkpoint mechanism periodically flushes all dirty pages to disk, limiting recovery time.
checkpoint_completion_target = 0.9spreads checkpoint I/O over 90% of the interval to smooth the impact.
A 100GB table contains only 20GB of live data. How did this happen and how do you fix it without downtime?
A 100GB table contains only 20GB of live data. How did this happen and how do you fix it without downtime?
Strong Answer:
- This is classic table bloat. Common causes: autovacuum throttled too aggressively and could not keep up with dead tuple creation, a long-running transaction prevented VACUUM from removing tuples visible to that snapshot, a massive UPDATE created millions of dead tuples faster than autovacuum could process them, or hot_standby_feedback on a replica fed back an old xmin.
- Online fix: use pg_repack. It creates a new copy of the table with only live tuples, builds new indexes, and swaps atomically with a brief exclusive lock. The table remains readable and writable throughout. You need roughly 100GB of free disk temporarily.
- If pg_repack is unavailable, VACUUM FULL rewrites the table but holds an ACCESS EXCLUSIVE lock for the entire duration (potentially hours for 100GB). Never run this during business hours.
- Prevention: tune autovacuum per-table for high-churn tables (
autovacuum_vacuum_scale_factor = 0.01), monitorn_dead_tup / n_live_tupratio, and alert when it exceeds 20%. Kill transactions older than 1 hour.
Describe the page layout of a PostgreSQL heap page and explain how understanding it helps debug production issues.
Describe the page layout of a PostgreSQL heap page and explain how understanding it helps debug production issues.
Strong Answer:
- Every data file is divided into 8KB pages. Each page structure: page header (24 bytes) containing LSN of last WAL modification, flags, and free space boundaries. Then a line pointer array (4-byte entries growing forward) where each pointer contains the offset and length of a tuple. Then free space in the middle. Then tuples stored from the end of the page backward, each with a header (23 bytes minimum) containing xmin, xmax, infomask flags, null bitmap, and actual column data.
- Line pointer indirection is key: tuples are never accessed directly, always through their line pointer. This enables HOT updates (redirect a line pointer without touching indexes) and makes VACUUM’s dead tuple cleanup possible without index updates.
- Practical debugging: the
pageinspectextension lets you examine raw page contents.SELECT * FROM heap_page_items(get_raw_page('users', 0))shows every tuple on page 0, including dead tuples, xmin/xmax, and infomask flags. This is invaluable for diagnosing visibility issues, corruption, or unexpected bloat patterns.
SELECT * is discouraged — you may trigger TOAST decompression for columns you do not need.