Build Your Own PostgreSQL
MVCC and reading the past
What You Will Learn
- Use a snapshot and transaction status to select one visible tuple version
- Explain how readers and writers overlap without claiming conflicts disappear
- Connect long-lived snapshots to dead versions, index rechecks, and vacuum delay
Not stopping the world
A bookstore clerk corrects an order address while a report is already reading orders. With one mutable copy, either the report waits or it risks mixing old and new facts. PostgreSQL keeps multiple tuple versions instead. The report's snapshot selects the version belonging to its database view while the writer can create a newer version for later views.
A snapshot describes transaction visibility. It records enough information to distinguish transactions that committed before the snapshot, transactions still in progress, and transactions that are too new to be visible. When a scan finds a tuple version, it checks the creating transaction, the deleting or superseding transaction, and the snapshot. The result answers one question with a lot of machinery behind it: should this version exist for this reader?
Time lives in tuple headers
Tuple headers make MVCC physical. A version has an inserting transaction identifier and may have a deleting transaction identifier. An update is commonly represented as a delete of the old version plus an insert of a new version, linked by metadata. The old version remains for snapshots that should still see it. The new version is visible only to snapshots where its creating transaction qualifies and the old version no longer does.
Visibility map
Trace two readers across one update
Reader A takes a snapshot before Writer W commits an address update. The old tuple version was created by an earlier committed transaction and has been superseded by W; for A, W is still in progress or too new, so the old version remains visible and the new one does not. Reader B takes a later snapshot after W commits. Its checks reject the old version and accept the new one.
Both readers visit the same physical history and derive different valid answers. The invariant is snapshot consistency, not 'always return the newest bytes.' Visibility combines tuple metadata, transaction outcomes, and the snapshot horizon. That same rule must be applied whether a sequential scan finds the tuple directly or an index supplies a candidate location.
The locking alternative
Strict read-write locking takes a more literal route. In a lock-heavy design, a writer updating a row may block readers until it commits, and readers may block writers to preserve their view. This is easier to conceptualize, but it damages concurrency for read-heavy workloads. MVCC lets readers and writers pass each other more often because they are not always competing for the same single version.
MVCC is not free. Each update creates another physical version. Indexes may point to old and new entries. Pages accumulate dead tuples. Visibility checks add per-row work. Vacuum becomes necessary to reclaim space. PostgreSQL trades storage and cleanup complexity for better concurrency and stable snapshots.
How snapshots shape what you see
Different isolation levels use snapshots differently. Under read committed, each statement can get a fresh snapshot, so a transaction may see changes committed by others between statements. Under repeatable read, a transaction keeps a stable snapshot, so repeated queries see the same committed world even as others continue writing. Serializable isolation adds stronger rules to prevent dangerous interleavings that would not match any serial order. MVCC supplies the versions, and isolation policy decides how snapshots are chosen and validated.
Writers still conflict with writers. If two transactions try to update the same row, the database must coordinate. One may wait for the other. If the first commits, the second may need to recheck whether its update still makes sense under its isolation level. MVCC reduces reader-writer blocking, but it does not make all conflicts disappear. Physically there is still one current update chain for a row.
Visibility also affects indexes. An index entry may lead to a heap tuple that is not visible to the current snapshot. That is why many index scans must recheck the heap. The index can quickly find candidate locations, but MVCC truth lives in the heap tuple header and transaction status. Index-only scans need extra visibility information to avoid heap visits safely.
Long-running transactions are expensive. If a snapshot remains open for a long time, vacuum may be unable to remove tuple versions that snapshot could still see. Space grows, indexes carry obsolete entries, and transaction ID cleanup is delayed. The database supports old readers, but the whole system pays the cost. This is why operational guidance often warns against idle transactions left open.
A visibility checklist
To explain any returned version, name the reader's snapshot, the creator's outcome, and the superseding transaction's outcome. To explain retained history, name the oldest snapshot that could still accept it. These checks replace the misleading question 'which row is current?' with the precise question 'which physical version is visible to this reader?'
Toward coordination
MVCC gives the database a layered sense of time. Physical pages hold many versions. Transaction status says which writers succeeded. Snapshots define each reader's horizon. Vacuum eventually removes history that no valid reader can need. The result feels natural to users: they read a consistent past while others write the future.
The next problem is coordination that MVCC alone cannot solve. Some conflicts are about row versions, but others are about schema changes, relation access, and internal data races over buffer structures. The database needs locks and latches to manage those boundaries without turning concurrency into chaos.
That distinction keeps MVCC honest. It is a visibility system, not a universal substitute for coordination. The database still needs explicit rules for objects, for memory structures, and for the moments when only one process can safely reshape shared state.
Visibility answers what a reader may believe. Coordination answers who may change the machinery underneath that belief. Both answers have to stay compatible.