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
- PostgreSQL 18 transaction isolation.
predicate.c— SSI predicate locks and conflict detection.
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) andpredicate.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, withinConflicts/outConflictslists ofRWConflictDataedges, itspredicateLocks,prepareSeqNo/commitSeqNoandflags(SXACT_FLAG_DOOMED,SXACT_FLAG_RO_SAFE…). It outlives COMMIT until no overlapping serializable transaction remains (finishedBeforevsPredXact->SxactGlobalXmin).PREDICATELOCKTARGET, keyed by aPREDICATELOCKTARGETTAGbuilt withSET_PREDICATELOCKTARGETTAG_RELATION/PAGE/TUPLE, andPREDICATELOCK= (target, transaction). Both live in partitioned shared hash tables.- When space runs short,
SummarizeOldestCommittedSxact()folds old committed transactions into a dummyOldCommittedSxactand thepg_serialSLRU (sized byserializable_buffers).
Call flow
- Snapshot:
GetTransactionSnapshot()callsGetSerializableTransactionSnapshot(), which registers theSERIALIZABLEXACT. - Reads:
heap_beginscan()callsPredicateLockRelation()for a sequential scan,_bt_readpage()callsPredicateLockPage()on the B-tree leaf, and returned heap tuples getPredicateLockTID(). Index AMs withampredlocks = true(B-tree, hash, GiST, GIN) lock pages; for others the whole index relation is locked. Too many fine locks are promoted byCheckAndPromotePredicateLockRequest(), using the thresholds fromMaxPredicateChildLocks(). - A read that meets a version written by a concurrent transaction:
HeapCheckForSerializableConflictOut()→CheckForSerializableConflictOut(). - Writes:
heap_insert/update/delete()and B-tree inserts callCheckForSerializableConflictIn()→CheckTargetForConflictsIn()(tuple, page and relation targets) →FlagRWConflict()→OnConflict_CheckForSerializationFailure(), which either raises the error itself ("during write") or setsSXACT_FLAG_DOOMEDon 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