Cellular Anatomy of PostgreSQL: MVCC & Tuple Visibility Algorithms¶
1. Introduction: The Quantum Horizon of Database Visibility¶
In quantum mechanics, an observation collapses a wave function into a specific state. In PostgreSQL Multi-Version Concurrency Control (MVCC), a Snapshot freezes the observable state of the entire universe of transactions at a single microsecond in time.
To understand MVCC like a medical pathologist, one must abandon the concept that rows exist in a binary state of "present or absent." A tuple exists in physical reality on an 8KB disk page with an insertion timestamp (xmin) and a deletion timestamp (xmax). Its visibility is a mathematical function computed by the C function HeapTupleSatisfiesMVCC() (src/backend/access/heap/heapam_visibility.c).
2. Anatomical Blueprint: SnapshotData¶
Located in src/include/utils/snapshot.h. When a transaction executes a query, the engine captures a SnapshotData struct:
typedef struct SnapshotData
{
SnapshotType snapshot_type; /* SNAPSHOT_MVCC, SNAPSHOT_SELF, etc. */
TransactionId xmin; /* All XIDs < xmin are KNOWN to be completed */
TransactionId xmax; /* All XIDs >= xmax are in the future (INVISIBLE) */
TransactionId *xip; /* Array of active uncommitted transactions */
uint32 xcnt; /* Number of active transactions in xip array */
TransactionId *subxip; /* Array of active sub-transactions */
int32 subxcnt; /* Number of active sub-transactions */
CommandId curcid; /* Current Command ID within our transaction */
uint32 active_count; /* Refcount on this snapshot */
uint32 regd_count; /* Registration count */
bool copied; /* Is this a copy? */
} SnapshotData;
Visualizing the Snapshot Window:¶
0 ------------ [ xmin: 100 ] ---------------------- [ xmax: 150 ] ------------> Infinity
Past Transactions: | | Future:
All XIDs < 100 are | Active Transactions in flight: | All XIDs >= 150
GUARANTEED committed or aborted. | xip array = [102, 108, 145] | started AFTER this
Always visible (if committed). | If tuple has xmin in xip array, | snapshot was created.
| it is INVISIBLE! | Always INVISIBLE.
3. The Commit Log: pg_xact (CLOG) Cellular Bit-Packing¶
Where does Postgres record whether transaction 105 committed or aborted?
It does not write to the table! It writes to the Commit Log ($PGDATA/pg_xact/).
The 2-Bit Cellular Packing:¶
Every transaction ID in PostgreSQL requires exactly 2 bits of storage:
[Bit 1] [Bit 0] : Meaning
0 0 : TRANSACTION_STATUS_IN_PROGRESS (Currently running)
0 1 : TRANSACTION_STATUS_COMMITTED (Committed successfully)
1 0 : TRANSACTION_STATUS_ABORTED (Rolled back / Crashed)
1 1 : TRANSACTION_STATUS_SUB_COMMITTED
- Storage Math:
- 1 byte holds 4 transactions.
- 1 standard 8KB CLOG page holds 32,768 transactions (\(8,192 \times 4\)).
- A cluster executing 100 million transactions uses only ~24 MB of CLOG space on disk!
The CLOG Lookup Bottleneck:¶
If Postgres had to read the CLOG for every single row returned in a 1,000,000-row SELECT scan, performance would collapse under disk read contention.
* The Cellular Solution (Hint Bits): The very first query that inspects a tuple performs the CLOG lookup, determines that xmin is committed, and atomically modifies the tuple header directly on the data page by flipping the bit HEAP_XMIN_COMMITTED in t_infomask.
* Every subsequent query sees HEAP_XMIN_COMMITTED == 1 and bypasses the CLOG entirely!
4. The Exact Visibility Algorithm in C (HeapTupleSatisfiesMVCC)¶
When a query evaluates whether a tuple on an 8KB page is visible, it executes this exact logic branch tree (src/backend/access/heap/heapam_visibility.c):
bool
HeapTupleSatisfiesMVCC(HeapTuple htup, Snapshot snapshot, Buffer buffer)
{
HeapTupleHeader tuple = htup->t_data;
/* -------------------------------------------------------------
* STAGE 1: Check Insertion Transaction (t_xmin)
* ------------------------------------------------------------- */
if (!HeapTupleHeaderXminCommitted(tuple))
{
if (HeapTupleHeaderXminInvalid(tuple))
return false; /* Inserting transaction aborted -> INVISIBLE */
if (TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetRawXmin(tuple)))
{
/* Inserted by OUR OWN transaction */
if (HeapTupleHeaderGetCmin(tuple) >= snapshot->curcid)
return false; /* Inserted by a later command in our transaction -> INVISIBLE */
}
else if (XidInMVCCSnapshot(HeapTupleHeaderGetRawXmin(tuple), snapshot))
{
/* Inserting transaction was STILL ACTIVE when snapshot was taken */
return false; /* INVISIBLE */
}
else
{
/* Check CLOG if not already hinted */
if (!TransactionIdDidCommit(HeapTupleHeaderGetRawXmin(tuple)))
return false; /* Aborted in CLOG -> INVISIBLE */
/* Set Hint Bit for future queries! */
SetHintBits(tuple, buffer, HEAP_XMIN_COMMITTED, HeapTupleHeaderGetRawXmin(tuple));
}
}
else
{
/* Hint bit HEAP_XMIN_COMMITTED is already set! */
/* But wait: Is xmin >= snapshot->xmax? */
if (HeapTupleHeaderGetRawXmin(tuple) >= snapshot->xmax)
return false; /* Committed AFTER snapshot was taken -> INVISIBLE */
/* Is xmin in our snapshot active list (xip)? */
if (XidInMVCCSnapshot(HeapTupleHeaderGetRawXmin(tuple), snapshot))
return false; /* Was still running when snapshot taken -> INVISIBLE */
}
/* -------------------------------------------------------------
* STAGE 2: Check Deletion/Update Transaction (t_xmax)
* ------------------------------------------------------------- */
if (tuple->t_infomask & HEAP_XMAX_INVALID)
{
/* xmax is invalid -> Row was NEVER deleted or updated! */
return true; /* VISIBLE! */
}
if (!HeapTupleHeaderXmaxCommitted(tuple))
{
if (TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetRawXmax(tuple)))
{
/* Deleted by OUR OWN transaction */
if (HeapTupleHeaderGetCmax(tuple) >= snapshot->curcid)
return true; /* Deleted by a LATER command in our transaction -> Still VISIBLE */
return false; /* Deleted by an EARLIER command in our transaction -> INVISIBLE */
}
if (XidInMVCCSnapshot(HeapTupleHeaderGetRawXmax(tuple), snapshot))
{
/* Deleting transaction is STILL RUNNING -> Row is not yet officially deleted! */
return true; /* VISIBLE! */
}
if (!TransactionIdDidCommit(HeapTupleHeaderGetRawXmax(tuple)))
{
/* Deleting transaction aborted/crashed! Delete did not succeed! */
SetHintBits(tuple, buffer, HEAP_XMAX_INVALID, InvalidTransactionId);
return true; /* VISIBLE! */
}
/* Deleting transaction committed! */
SetHintBits(tuple, buffer, HEAP_XMAX_COMMITTED, HeapTupleHeaderGetRawXmax(tuple));
}
else
{
/* Hint bit HEAP_XMAX_COMMITTED is set! */
if (HeapTupleHeaderGetRawXmax(tuple) >= snapshot->xmax)
return true; /* Deleted AFTER snapshot was taken -> Still VISIBLE to us! */
if (XidInMVCCSnapshot(HeapTupleHeaderGetRawXmax(tuple), snapshot))
return true; /* Deleting TX was still active at snapshot time -> Still VISIBLE! */
}
/* Deleting transaction committed BEFORE our snapshot! */
return false; /* INVISIBLE (Row is dead to this snapshot) */
}
5. The Cellular Pathology: Read Phenomena & Isolation Levels¶
PostgreSQL implements 3 ANSI SQL Isolation Levels on top of this visibility engine:
1. READ COMMITTED (Default)¶
- Mechanics: Every single SQL statement in the transaction captures a brand new Snapshot!
- Consequence: If Query 1 runs at \(t_0\) and Query 2 runs at \(t_1\), any transaction committed between \(t_0\) and \(t_1\) becomes visible in Query 2.
- Vulnerable to: Non-repeatable reads and Phantom reads.
2. REPEATABLE READ¶
- Mechanics: The transaction captures a single Snapshot at the start of the first query and preserves that exact same
SnapshotDatastruct untilCOMMIT. - Consequence: All queries see the exact same view of historical data.
- First-Committer-Wins Rule: If two transactions update the same row concurrently, the second transaction throws an error:
ERROR: could not serialize access due to concurrent update.
3. SERIALIZABLE (SSI: Serializable Snapshot Isolation)¶
- Mechanics: Uses SIREAD Predicate Locks in shared memory (
src/backend/storage/lmgr/predicate.c). - Predicate locks do not block reads or writes. They track data dependency overlaps (rw-antidependencies).
- If a cycle is detected in the dependency graph (indicating that serial execution ordering is impossible), the engine aborts one of the transactions with:
ERROR: could not serialize access due to read/write dependencies among transactions.