Open-Source Wikis

/

PostgreSQL

/

Systems

/

MVCC and transactions

postgres/postgres

MVCC and transactions

PostgreSQL implements multi-version concurrency control (MVCC): writers create new row versions instead of overwriting, and readers see the version visible at the start of their snapshot. The combination of MVCC and a separate WAL system is the project's defining architectural choice. This page covers transaction state, snapshot computation, visibility rules, and the supporting catalogs.

Directory layout

src/backend/access/transam/
├── clog.c          # commit log: per-XID commit/abort status (SLRU)
├── commit_ts.c     # per-XID commit timestamp (SLRU, optional)
├── multixact.c     # XID groupings for shared row locks (SLRU)
├── parallel.c      # parallel-worker transaction state setup
├── slru.c          # simple LRU page cache used by clog/multixact/etc.
├── subtrans.c      # subtransaction parent pointers (SLRU)
├── timeline.c      # WAL timelines (used by recovery)
├── transam.c       # XID-status helpers (TransactionIdDidCommit, ...)
├── twophase.c      # PREPARE TRANSACTION
├── varsup.c        # XID and OID counters; wraparound
├── xact.c          # top-level transaction state machine
├── xlog.c          # WAL infrastructure
├── xloginsert.c    # XLogInsert API
└── xlogreader.c    # WAL parser used by recovery

The visibility logic itself lives next to the heap: src/backend/access/heap/heapam_visibility.c.

The proc array — the runtime state of which XIDs are in progress — lives at src/backend/storage/ipc/procarray.c.

XIDs

A transaction ID (XID) is a 32-bit counter. Every transaction that writes gets one, allocated lazily (read-only transactions get a virtual XID instead — BackendId, LocalTransactionId). Source: src/backend/access/transam/varsup.c::GetNewTransactionId.

XIDs wrap every ~4 billion. Wraparound is a real problem because a row's xmin of "transaction 5" is supposed to predate "transaction 4 billion + 5." The mitigation is freezing: VACUUM rewrites old rows to mark them with the special XID FrozenTransactionId (= 2), declaring "this is unconditionally visible to everyone." Once a row is frozen, its actual XID can be safely recycled. The autovacuum launcher monitors pg_class.relfrozenxid and forces an emergency vacuum if a relation's age approaches the wraparound limit.

64-bit XIDs have been a long-discussed feature; the project has continued to refine the freezing path instead.

Transaction state machine

Source: src/backend/access/transam/xact.c. Each backend maintains a TransactionState stack:

TopTransactionState (per top-level transaction)
└── CurrentTransactionState (top, plus a stack frame per active subtransaction)

Top-level entry points:

Function Role
StartTransaction Begin a top-level transaction. Allocates TopTransactionContext, sets state.
StartTransactionCommand Called at the start of every protocol command; opens an implicit transaction if none exists.
CommitTransactionCommand At the end of a command; commits if it was an implicit transaction.
BeginInternalSubTransaction Open a subtransaction (used for EXCEPTION blocks in PL/pgSQL).
CommitTransaction End-of-COMMIT. Releases locks, runs deferred triggers, writes the commit record to WAL, marks the XID committed in CLOG.
AbortTransaction End-of-ROLLBACK or post-error. Releases resources via the resource owner, runs aborts, writes abort record to WAL.
PrepareTransaction PREPARE TRANSACTION; durably records the transaction state to a 2PC file.

The state machine values themselves (TBLOCK_DEFAULT, TBLOCK_STARTED, TBLOCK_INPROGRESS, TBLOCK_END, TBLOCK_ABORT, TBLOCK_SUBINPROGRESS, ...) are enumerated in xact.c and handled in giant switches; touching them requires care.

Snapshots

A snapshot is the set of XIDs visible to a transaction. PostgreSQL's snapshot is essentially:

typedef struct SnapshotData
{
    SnapshotType snapshot_type;
    TransactionId xmin;       // oldest XID still potentially visible
    TransactionId xmax;       // first XID not yet visible
    TransactionId *xip;       // in-progress XIDs in [xmin, xmax)
    uint32        xcnt;
    /* ... */
} SnapshotData;

Visibility rule: a transaction with XID t is visible iff:

  • t < snapshot.xmin (committed before this snapshot was taken), OR
  • t >= snapshot.xmin && t < snapshot.xmax && t not in xip (committed before the snapshot).

Sources:

  • GetSnapshotData in src/backend/storage/ipc/procarray.c — builds a snapshot by walking the proc array under ProcArrayLock.
  • src/backend/utils/time/snapmgr.c — snapshot manager, snapshot stack, "active snapshot" vs "transaction snapshot."

There are several specialized snapshot types: MVCC (default), SelfMVCC (same-tx changes are visible), Dirty (catalog scans), NonVacuumable (used during VACUUM), Historic (logical decoding).

Tuple visibility

The actual test is HeapTupleSatisfiesVisibility(tuple, snapshot, buffer) in src/backend/access/heap/heapam_visibility.c. The function dispatches on the snapshot type. For an MVCC snapshot, the rough algorithm is:

graph TD
    Start[heap tuple] --> CheckXmin{Was xmin committed and visible to snapshot?}
    CheckXmin -- "no" --> Invisible
    CheckXmin -- "yes" --> CheckXmax{Is xmax set?}
    CheckXmax -- "no" --> Visible
    CheckXmax -- "yes" --> CheckXmaxState{Was xmax committed and visible?}
    CheckXmaxState -- "yes" --> Invisible[invisible<br/>(deleted row)]
    CheckXmaxState -- "no" --> Visible

In practice the function is a careful sequence of t_infomask flag checks, CLOG lookups (via TransactionIdDidCommit / TransactionIdDidAbort), and proc-array consultation — each step caching the result back into t_infomask hints to avoid future repeated lookups.

HeapTupleSatisfiesVacuum is the parallel function used by VACUUM to decide if a tuple can be removed.

CLOG

The commit log stores 2 bits per XID: in progress (00), committed (01), aborted (10), sub-committed (11). Backed by SLRU. Source: src/backend/access/transam/clog.c. Truncated periodically as XIDs are frozen.

TransactionIdDidCommit(xid) reads CLOG, with hint-bit caching in the proc array to amortize cost.

Multixact

When multiple transactions hold a shared lock on a row, the heap tuple's xmax slot can hold only one XID — so PostgreSQL invents a MultiXactId. A multixact is an ordered list of XIDs and their lock modes, stored in another SLRU (pg_multixact/offsets, pg_multixact/members). Source: src/backend/access/transam/multixact.c.

Multixacts have their own freeze counter (relminmxid), tracked alongside relfrozenxid to ensure they get cleaned up.

Subtransactions

Each SAVEPOINT (or PL/pgSQL EXCEPTION block, internally) opens a subtransaction. Subtransactions get their own XID; on commit, the parent inherits all of their effects. On rollback, the subtransaction's XID is marked aborted in CLOG.

A subtransaction's parent is recorded in pg_subtrans, an SLRU-backed lookup table (src/backend/access/transam/subtrans.c). The visibility code consults it when determining whether a sub-XID is "really" visible.

Two-phase commit

PREPARE TRANSACTION 'gid' durably records a transaction's state to disk in pg_twophase/<xid> and detaches it from the issuing backend. A later COMMIT PREPARED 'gid' (potentially by a different backend) finishes it. Used for distributed transactions coordinated by an external transaction manager. Source: src/backend/access/transam/twophase.c.

Isolation levels

PostgreSQL implements:

  • Read Committed (default) — each query sees its own fresh snapshot. Writes can race; row-level locking and SELECT FOR UPDATE provide additional ordering when needed.
  • Repeatable Read — one snapshot per transaction; serialization anomalies prevented at the row-version level by xmin/xmax + the proc array. Implements PostgreSQL's "snapshot isolation."
  • Serializable — adds Serializable Snapshot Isolation (SSI) on top: a runtime predicate-locking system that detects dangerous read-write dependency cycles and aborts the offending transaction. Source: src/backend/storage/lmgr/predicate.c.

The choice of level affects which Snapshot is "active": Read Committed re-fetches GetTransactionSnapshot each statement; Repeatable Read freezes one for the whole transaction; Serializable adds SSI tracking on top of the RR snapshot.

VACUUM

Source: src/backend/access/heap/vacuumlazy.c, src/backend/commands/vacuum.c. Reclaims dead row versions by:

  1. Heap scan: identify tuples whose xmax is committed and older than the global horizon (OldestXmin).
  2. Index vacuum: for each affected page, remove index entries pointing to dead tuples.
  3. Heap second pass: mark line pointers LP_DEAD/LP_UNUSED.
  4. Possibly truncate trailing all-dead pages.

Autovacuum (src/backend/postmaster/autovacuum.c) reads pg_stat_all_tables (now in shared memory), uses thresholds (autovacuum_vacuum_threshold, _scale_factor, _max_workers) to decide which tables need vacuum/analyze, and dispatches per-table workers.

OldestXmin is computed from the proc array — the smallest xmin of any active backend, replication slot, or prepared transaction. Long-running transactions and unused replication slots are common causes of "VACUUM can't clean up dead rows."

Entry points for modification

  • New isolation semantic: usually starts in xact.c (transaction state) and snapmgr.c (snapshot acquisition); SSI tracking lives in predicate.c.
  • New visibility check: heapam_visibility.c::HeapTupleSatisfiesVisibility is the dispatch site; specialized functions live in the same file.
  • New transaction-end side effect: xact.c::CommitTransaction and AbortTransaction are the choke points. Many subsystems already register here (AtCommit_*, AtAbort_*).
  • Tracking a new per-XID datum: there is precedent in commit_ts.c for adding an SLRU-backed XID-keyed array.

For the WAL and recovery side of transactions, see WAL and recovery. For the proc array internals, see Storage.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

MVCC and transactions – PostgreSQL wiki | Factory