DB Internals AtlasPostgreSQL 18.6 Home About

Transactions and MVCC

Serializable Snapshot Isolation

PostgreSQL Serializable isolation keeps MVCC reads nonblocking, records predicate-style SIREAD locks, tracks read/write dependencies between concurrent transactions, and aborts a transaction when the dependency pattern could produce a serialization anomaly.

In plain words

The SERIALIZABLE isolation level promises that transactions behave as if they ran one after another. PostgreSQL keeps that promise without making readers wait: reads still use normal snapshots.

Instead, each serializable read leaves a light marker (an SIREAD lock) that says "this transaction looked at this data". The markers never block anyone. When another serializable transaction later changes data that was read, PostgreSQL notes the dependency between the two.

If those dependencies form a pattern that no one-after-another order could produce, PostgreSQL cancels one of the transactions with error code 40001 ("could not serialize access"). The application must then retry that whole transaction, which is why apps that use SERIALIZABLE need retry logic.

For DBAs and operators

Why SIREAD locks are unusual

In predicate.c, SIREAD predicate locks exist specifically for SSI. They do not block and do not participate in ordinary deadlock detection. They may need to survive a successful commit until overlapping transactions finish, because a later writer can still complete the dependency pattern.

Conflict checks

Reads acquire predicate locks where needed. Writes call conflict checks such as CheckForSerializableConflictIn(); reads can discover conflicts through CheckForSerializableConflictOut(). Before commit, PreCommit_CheckForSerializationFailure() performs the final check. The implementation tracks serializable transactions and rw-antidependencies rather than trying to lock every predicate with a blocking range lock.

Memory and promotion

max_pred_locks_per_transaction sizes the predicate-lock pool. The page/relation settings control when many locks are represented at coarser granularity. Coarser locks are safe but can increase false-positive serialization failures.

Source trail

For developers: inside the source

Where it lives

  • predicate.c: all of SSI, from the SIREAD lock table to the rw-conflict graph and the failure checks. Its header comment lists the interface routines and the LWLock order.
  • predicate_internals.h (shared structs) and predicate.h (API used by the access methods).
  • README-SSI: the design document (Cahill's algorithm, dangerous structures, the PostgreSQL-specific optimizations).

Key data structures

  • SERIALIZABLEXACT: one per serializable transaction in shared memory, with inConflicts/outConflicts lists of RWConflictData edges, its predicateLocks, prepareSeqNo/commitSeqNo and flags (SXACT_FLAG_DOOMED, SXACT_FLAG_RO_SAFE…). It outlives COMMIT until no overlapping serializable transaction remains (finishedBefore vs PredXact->SxactGlobalXmin).
  • PREDICATELOCKTARGET, keyed by a PREDICATELOCKTARGETTAG built with SET_PREDICATELOCKTARGETTAG_RELATION/PAGE/TUPLE, and PREDICATELOCK = (target, transaction). Both live in partitioned shared hash tables.
  • When space runs short, SummarizeOldestCommittedSxact() folds old committed transactions into a dummy OldCommittedSxact and the pg_serial SLRU (sized by serializable_buffers).

Call flow

  • Snapshot: GetTransactionSnapshot() calls GetSerializableTransactionSnapshot(), which registers the SERIALIZABLEXACT.
  • Reads: heap_beginscan() calls PredicateLockRelation() for a sequential scan, _bt_readpage() calls PredicateLockPage() on the B-tree leaf, and returned heap tuples get PredicateLockTID(). Index AMs with ampredlocks = true (B-tree, hash, GiST, GIN) lock pages; for others the whole index relation is locked. Too many fine locks are promoted by CheckAndPromotePredicateLockRequest(), using the thresholds from MaxPredicateChildLocks().
  • A read that meets a version written by a concurrent transaction: HeapCheckForSerializableConflictOut() → CheckForSerializableConflictOut().
  • Writes: heap_insert/update/delete() and B-tree inserts call CheckForSerializableConflictIn() → CheckTargetForConflictsIn() (tuple, page and relation targets) → FlagRWConflict() → OnConflict_CheckForSerializationFailure(), which either raises the error itself ("during write") or sets SXACT_FLAG_DOOMED on another transaction.
  • Commit: CommitTransaction() → PreCommit_CheckForSerializationFailure() fails if this transaction was doomed, otherwise dooms the pivot of any structure it would complete. That is why, in the scene, T1's COMMIT succeeds and T2 fails at its own COMMIT. ReleasePredicateLocks() runs at resource-owner release.

Observe and test

SIREAD locks appear in pg_locks with mode SIReadLock (GetPredicateLockStatusData()). With \set VERBOSITY verbose the DETAIL reason code and LOCATION show which check fired, for example DETAIL: Reason code: Canceled on identification as a pivot, during commit attempt. The isolation suite (src/test/isolation/specs: simple-write-skew, receipt-report, read-only-anomaly*, predicate-*, serializable-parallel*) covers the edge cases; run make -C src/test/isolation check.

Parameters that shape this

default_transaction_isolation max_pred_locks_per_transaction max_pred_locks_per_relation max_pred_locks_per_page

Related topics

Isolation levels Snapshots and visibility Locking and deadlocks MVCC: row versions