PostgreSQL Transactions, MVCC & Isolation - Complete Notes

An in-depth and interview-focused guide to atomic work, snapshots, row versions, isolation anomalies, safe retries, savepoints, and the concurrency behavior behind PostgreSQL transactions.

00. The transaction and snapshot mental model

A transaction defines one logical unit of work. MVCC decides which row versions that work can see. The isolation level decides when its snapshot is chosen and which concurrent outcomes PostgreSQL must reject.

Imagine a table as a history of row versions rather than a collection of boxes that are overwritten in place. When one transaction updates a row, PostgreSQL normally keeps the old physical version and creates a new physical version. Each statement reads through a snapshot that answers: "Which transactions count as completed for this view?" The visibility rules then choose the correct version of every logical row.

Think of an online document with immutable revisions. A snapshot is a rule for choosing which revisions belong to your view. Two readers can open the same document at different logical times and correctly see different revisions. Publishing a new revision does not require erasing the revision an older reader still needs.

Layer Question it answers PostgreSQL mechanism
Transaction boundary Which statements succeed or fail as one unit? BEGIN, COMMIT, ROLLBACK
Physical history How can old and new states coexist? Tuple versions carrying transaction metadata
Snapshot Which transaction effects are visible to this statement? Completed and in-progress transaction information
Isolation level How stable must the view and final outcome be? Statement snapshots, transaction snapshots, or SSI checks
Cleanup When can obsolete versions be reused? VACUUM after no required snapshot can see them
Visibility is not the same as existence

A row version may physically exist on disk but be invisible to a snapshot. Likewise, a version marked by another transaction may remain visible until that transaction commits. Always reason from versions, transaction status, and the current snapshot, not only from what is physically stored.

01. ACID and choosing the unit of work

ACID describes the guarantees expected around a transaction. It does not automatically make a multi-step business workflow correct. The application still has to choose the right boundary, constraints, isolation level, and retry behavior.

Property Practical meaning What the developer must still do
Atomicity Database changes in the transaction commit together or are discarded together Place every database change for one invariant in the same transaction
Consistency A committed state satisfies enforced database rules Define constraints and write concurrency-safe business logic
Isolation Concurrent work is hidden or rejected according to the selected level Choose a sufficient level and handle permitted anomalies
Durability After a successful durable commit, database changes survive a crash Understand durability settings, backups, and external side effects
One invariant, one transaction
BEGIN;

UPDATE accounts
SET balance = balance - 100.00
WHERE account_id = 10
  AND balance >= 100.00;

-- The application must require exactly one updated source row.

UPDATE accounts
SET balance = balance + 100.00
WHERE account_id = 20;

INSERT INTO transfers (from_account, to_account, amount)
VALUES (10, 20, 100.00);

COMMIT;

Without one boundary, a crash after the debit but before the credit leaves incomplete work. Atomicity prevents that database state. It does not make an email, HTTP request, or message sent to an external system roll back. For cross-system work, patterns such as a transactional outbox are needed so the committed database state records what must be delivered.

COMMIT is a decision point

Do not commit and then decide whether the business operation was valid. Validate first, let constraints check the final state, and commit only when the whole unit is ready. If the client loses its connection during commit, the outcome can be uncertain from the client's point of view, so blindly repeating a non-idempotent operation can duplicate work.

02. BEGIN, COMMIT, ROLLBACK and autocommit

PostgreSQL always executes statements in transactions. An explicit transaction block groups several statements. Without one, a client normally uses autocommit, so each successful statement is committed as its own transaction.

Core transaction syntax
-- BEGIN is PostgreSQL's common spelling.
BEGIN;
-- statements
COMMIT;

-- SQL-standard spelling with identical transaction-start behavior.
START TRANSACTION;
-- statements
ROLLBACK;

-- Set characteristics while starting the transaction.
BEGIN TRANSACTION
  ISOLATION LEVEL REPEATABLE READ
  READ ONLY;

-- Finish and immediately start another transaction with the same characteristics.
COMMIT AND CHAIN;
ROLLBACK AND CHAIN;
Command Effect Important detail
BEGIN Starts an explicit transaction block A second BEGIN warns; it does not create a nested transaction
COMMIT Makes the transaction's database changes permanent and visible Locks are released when the transaction ends
ROLLBACK Discards the whole transaction's database changes Some non-transactional effects, especially sequence advancement, remain
AND CHAIN Ends one transaction and immediately starts another The new transaction retains transaction characteristics

In many drivers, "autocommit off" means the driver opens a transaction implicitly and leaves it open until the application commits or rolls back. Check the driver and framework behavior. A forgotten transaction can retain snapshots, locks, and database resources even if the code never typed BEGIN.

What happens after a statement error

A transaction stays failed until it is recovered or ended
BEGIN;

INSERT INTO users (email) VALUES ('duplicate@example.com');
-- ERROR: duplicate key value violates unique constraint

SELECT 1;
-- ERROR: current transaction is aborted, commands ignored...

ROLLBACK;
-- The session can now start new work.

PostgreSQL does not silently skip a failed statement and continue the same top-level transaction. That would make it too easy to commit a partially understood state. Use a savepoint when a particular failure is expected and recoverable. Otherwise roll back the whole transaction.

03. Savepoints and partial rollback

A savepoint is a named recovery point inside one transaction. It is useful for an optional step or an expected error, but it is not an independently durable nested transaction.

Keep earlier work while undoing a later step
BEGIN;

INSERT INTO orders (order_id, customer_id, status)
VALUES (501, 42, 'created');

SAVEPOINT optional_coupon;

UPDATE coupons
SET remaining_uses = remaining_uses - 1
WHERE code = 'WELCOME'
  AND remaining_uses > 0;

-- Suppose a later coupon operation fails.
ROLLBACK TO SAVEPOINT optional_coupon;

INSERT INTO audit_log (event_type, entity_id)
VALUES ('order_created_without_coupon', 501);

COMMIT;
  • SAVEPOINT name records a point inside the current transaction. It requires an explicit transaction block.
  • ROLLBACK TO SAVEPOINT name undoes work after the point and restores a usable transaction state. The named savepoint remains available.
  • Rolling back to a savepoint destroys savepoints created after it. Earlier savepoints remain.
  • RELEASE SAVEPOINT name removes that savepoint and later nested savepoints while keeping their successful effects in the enclosing transaction.
  • The outer COMMIT is still required. A released savepoint has not made anything independently durable.
Savepoints have a cost

Savepoints use PostgreSQL subtransaction machinery. A sensible number for error recovery is normal, but creating huge numbers in one transaction adds bookkeeping and can hurt performance. Some framework "nested transactions" are savepoints, so inspect what the abstraction really does.

04. MVCC: multiple physical versions of one logical row

Multi-Version Concurrency Control lets readers use an older committed version while a writer creates a newer version. Ordinary reads therefore do not block ordinary writes, and ordinary writes do not block ordinary reads.

Conceptual version chain after an update
Logical account 42

Version A: balance = 500, xmin = 100, xmax = 125
Version B: balance = 450, xmin = 125, xmax = 0

Transaction 125 performed:
UPDATE accounts SET balance = 450 WHERE account_id = 42;

Old snapshot: Version A may be visible
New snapshot after transaction 125 commits: Version B may be visible

An UPDATE is conceptually a delete of the old tuple version plus an insert of a new tuple version. The old version cannot be reclaimed while an active snapshot might still need it. A DELETE marks a version as deleted but does not immediately remove its bytes. Routine VACUUM later makes dead-version space reusable after it is safe.

Operation Version effect What concurrent snapshots can do
INSERT Creates a version whose creator is the inserting transaction Ignore it until its creator is visible
UPDATE Marks the old version and creates a replacement Choose the old or new committed version as the snapshot requires
DELETE Marks the current version as deleted Keep seeing it if the deleting transaction is not visible
VACUUM Reclaims versions no snapshot can need Does not change the logical result of a valid snapshot

MVCC reduces read/write contention, but it does not mean "no locks." Writers of the same row can wait for each other, schema changes can conflict with queries, and constraints can coordinate concurrent transactions. Explicit lock modes belong to the locking topic; the important point here is that snapshot visibility and blocking are different mechanisms.

05. Tuple xmin, xmax and visibility

Every table has system columns. For MVCC, xmin identifies the transaction that created a row version and xmax usually identifies the transaction that deleted, updated, or locked it.

Inspect system columns for learning and diagnostics
SELECT
  account_id,
  balance,
  xmin,
  xmax,
  ctid
FROM accounts
WHERE account_id = 42;

-- Use modern transaction and snapshot information functions.
SELECT pg_current_xact_id_if_assigned();
SELECT pg_current_snapshot();
Value Meaning Caution
Tuple xmin XID of the transaction that inserted this physical version It is not a creation timestamp or permanent business identifier
Tuple xmax XID-related deletion, update, or locking metadata Nonzero does not by itself prove the version is invisible
ctid Physical location of this tuple version It can change after update or table movement; never use it as a primary key
Snapshot xmin Lowest XID that was still active for that snapshot This is a snapshot boundary, not the tuple's creator field
Snapshot xmax One past the highest completed XID at snapshot time XIDs at or above it are invisible to that snapshot
Snapshot xip_list Transactions in progress between the snapshot boundaries Commit status is still part of visibility reasoning

A simplified visibility checklist

  1. Is the inserting transaction this transaction? If so, command ordering determines whether the version is visible to later commands in the same transaction.
  2. Otherwise, did the inserting transaction commit before the snapshot, and was it not in progress for that snapshot? If not, the version is not visible.
  3. Has a deleting or updating transaction marked the version? If that transaction is aborted or not visible to the snapshot, the older version can remain visible.
  4. If the deleting transaction committed and is visible to the snapshot, the old version is no longer visible.
Do not implement application logic from xmin or xmax

Real visibility also uses transaction status, tuple header flags, command IDs, subtransactions, multitransaction IDs, freezing, and wraparound-safe XID comparisons. Transaction IDs are finite internal identifiers and can wrap. Use normal SQL, constraints, explicit version columns, or supported concurrency controls for business behavior.

06. Snapshots: a consistent visibility decision

A snapshot is not a full copy of the database. It is compact transaction visibility information used to decide which stored row versions a statement may see.

Reading a conceptual pg_snapshot value
100:110:103,108

xmin = 100
xmax = 110
xip_list = 103,108

XID less than 100:
  completed before the snapshot horizon, then visibility depends on commit status

XID 103 or 108:
  in progress at snapshot time, so its new effects are invisible

XID from 100 through 109 but not in xip_list:
  completed by snapshot time, then commit or abort status decides

XID 110 or greater:
  had not completed as of the snapshot and is invisible

A transaction always sees its own earlier changes even though no other transaction can see them before commit. At READ COMMITTED, each command normally gets a fresh snapshot. At REPEATABLE READ and SERIALIZABLE, the transaction uses the snapshot established by its first query or data-changing statement.

BEGIN time is not always snapshot time

For transaction-snapshot levels, merely issuing BEGIN does not freeze the data view. The snapshot is acquired by the first non-transaction-control statement. This distinction matters when reproducing concurrency demonstrations.

07. Concurrency anomalies in plain language

Isolation levels are easiest to understand by the bad outcomes they permit or prevent. The SQL standard names dirty reads, nonrepeatable reads, phantom reads, and serialization anomalies.

Anomaly What happens Simple example
Dirty read A transaction reads another transaction's uncommitted write Read a balance that the writer later rolls back
Nonrepeatable read Reading one row twice gives different committed values A status changes between two queries in the same transaction
Phantom read Repeating a predicate query returns a different matching set A new paid order appears in a repeated count
Serialization anomaly The committed result cannot match any one-at-a-time transaction order Two doctors each go off call after each saw the other on call

Where does lost update fit?

"Lost update" is a widely used practical term, but its exact definition varies. The classic form is application-side read-modify-write:

Unsafe application-side calculation
-- Both transactions read 10.
SELECT quantity FROM inventory WHERE sku = 'A';

-- Each application calculates 10 - 1 and sends 9.
UPDATE inventory SET quantity = 9 WHERE sku = 'A';

-- Both can commit, but two sales reduced quantity only once.

Prefer one atomic SQL expression when possible:

Atomic relative update with invariant enforcement
UPDATE inventory
SET quantity = quantity - 1
WHERE sku = 'A'
  AND quantity > 0
RETURNING quantity;

Concurrent writers of this row serialize at the row update, and the second statement evaluates against the current row version under READ COMMITTED. For a disconnected workflow, use optimistic concurrency with an explicit version column:

Compare-and-set optimistic update
UPDATE documents
SET body = $1,
    version = version + 1
WHERE document_id = $2
  AND version = $3
RETURNING version;

-- Zero returned rows means another writer changed the document.

08. The four isolation levels in PostgreSQL

SQL names four levels, but PostgreSQL implements three distinct behaviors because READ UNCOMMITTED behaves exactly like READ COMMITTED.

Requested level Dirty read Nonrepeatable read Phantom read Serialization anomaly
READ UNCOMMITTED Prevented by PostgreSQL Possible Possible Possible
READ COMMITTED Prevented Possible Possible Possible
REPEATABLE READ Prevented Prevented Prevented in PostgreSQL Possible
SERIALIZABLE Prevented Prevented Prevented Prevented by aborting a conflicting transaction when needed

"Prevented" does not mean every transaction always commits. PostgreSQL may wait for a concurrent writer or abort a transaction to preserve the requested guarantee. Higher isolation trades some retry risk and overhead for stronger reasoning. It is not a substitute for constraints, correct transaction boundaries, or handling external side effects.

09. Read Committed: one snapshot per command

READ COMMITTED is PostgreSQL's default. A plain query sees rows committed before that query began plus earlier changes from its own transaction.

Nonrepeatable read with two sessions
-- Session A                            -- Session B
BEGIN ISOLATION LEVEL READ COMMITTED;
SELECT status FROM orders
WHERE order_id = 10; -- 'pending'

                                       BEGIN;
                                       UPDATE orders
                                       SET status = 'paid'
                                       WHERE order_id = 10;
                                       COMMIT;

SELECT status FROM orders
WHERE order_id = 10; -- 'paid'
COMMIT;

Each individual statement sees a consistent snapshot, so it does not see half of Session B's transaction. However, Session A's second statement starts later and gets a newer snapshot. Therefore a multi-query report can mix different committed moments.

Concurrent writes under Read Committed

An UPDATE, DELETE, MERGE, or row-locking command finds targets using its command snapshot. If a target was concurrently changed, the command can wait. After the other transaction commits, PostgreSQL can follow the updated version and re-evaluate the command's condition against it.

A safe conditional state transition
UPDATE jobs
SET status = 'running',
    started_at = clock_timestamp()
WHERE job_id = 900
  AND status = 'queued'
RETURNING *;

-- Exactly one contender changes queued to running.
-- Other contenders eventually return zero rows.
One command can observe a specialized mix

In READ COMMITTED, a modifying command can update a newer version of a row that was not visible in its original command snapshot after waiting and rechecking. This is useful for simple targeted updates, but complex search conditions can produce surprising results. If the business rule depends on a stable multi-row view, use stronger isolation or explicit coordination.

10. Read Uncommitted: an alias in PostgreSQL

PostgreSQL accepts the SQL-standard name READ UNCOMMITTED, but gives it READ COMMITTED behavior. Dirty reads are not available.

Accepted syntax, Read Committed semantics
BEGIN ISOLATION LEVEL READ UNCOMMITTED;

SHOW transaction_isolation;
-- PostgreSQL reports read uncommitted as the requested characteristic,
-- but visibility behavior is the same as read committed.

SELECT * FROM accounts;
COMMIT;

This is an important interview detail. Do not describe PostgreSQL as exposing another session's uncommitted tuple data through this level. PostgreSQL maps the level to its MVCC-compatible READ COMMITTED behavior.

11. Repeatable Read: one stable transaction snapshot

PostgreSQL REPEATABLE READ uses one transaction snapshot. Successive queries see the same committed database view plus the transaction's own changes.

A stable row and predicate result
-- Session A                            -- Session B
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT COUNT(*) FROM orders
WHERE status = 'paid'; -- 50

                                       INSERT INTO orders (order_id, status)
                                       VALUES (999, 'paid');
                                       COMMIT;

SELECT COUNT(*) FROM orders
WHERE status = 'paid'; -- still 50
COMMIT;

-- A transaction started after both commits can see 51.

The SQL standard permits phantoms at Repeatable Read, but PostgreSQL's snapshot isolation implementation prevents them. A repeated predicate sees the same row set from concurrent transactions. PostgreSQL's implementation is therefore stronger than the standard minimum on that dimension.

Updating a row changed after the snapshot

If a Repeatable Read transaction tries to modify or lock a row that another transaction changed after its snapshot, it can fail with could not serialize access due to concurrent update. The application must roll back and retry the complete unit of work.

A stable snapshot is not automatically serializable

Two transactions can each make a decision from a stable snapshot and write different rows. Both can commit even when their combined result violates a cross-row rule. This is write skew, a serialization anomaly that Repeatable Read can allow.

12. Serializable and Serializable Snapshot Isolation

SERIALIZABLE guarantees that committed transactions have an effect consistent with some serial, one-at-a-time order. PostgreSQL preserves MVCC concurrency and aborts a transaction when dangerous read/write dependencies make that guarantee impossible.

PostgreSQL implements this with Serializable Snapshot Isolation, commonly abbreviated SSI. A serializable transaction reads from a stable snapshot like Repeatable Read. PostgreSQL also tracks read/write dependency patterns, including predicate reads. If concurrent work forms a dangerous structure that could produce a non-serializable result, one transaction receives a serialization failure instead of all conflicting results being committed.

Write skew prevented by Serializable
-- Initially Alice and Bob are both on call.

-- Session A                            -- Session B
BEGIN ISOLATION LEVEL SERIALIZABLE;     BEGIN ISOLATION LEVEL SERIALIZABLE;

SELECT COUNT(*) FROM doctors            SELECT COUNT(*) FROM doctors
WHERE on_call; -- 2                     WHERE on_call; -- 2

UPDATE doctors                          UPDATE doctors
SET on_call = false                     SET on_call = false
WHERE name = 'Alice';                   WHERE name = 'Bob';

COMMIT;                                 COMMIT;

-- PostgreSQL cannot allow both effects to commit because no serial order
-- would let both transactions read 2 and leave nobody on call.
-- One transaction can fail with SQLSTATE 40001.
Serializable feature Meaning
Stable snapshot Reads have Repeatable Read snapshot behavior
Dependency tracking PostgreSQL detects read/write patterns that threaten a serial outcome
Predicate information Logical ranges that were read can participate in dependency detection
Serialization failure A transaction is canceled so the remaining committed history stays valid
Application retry The complete decision-making transaction must be run again

Serializable does not mean PostgreSQL runs every transaction one after another. Many serializable transactions execute concurrently and commit. It also does not promise the wall-clock order you imagined. It promises that some valid serial order explains the committed effects.

The guarantee requires a consistent policy

If a business rule depends on several writers, running only one of them at Serializable cannot make non-serializable writers participate in SSI dependency tracking. For a serializable guarantee over permanent data, all transactions that can affect the invariant should follow the required policy.

13. Worked isolation scenarios

The fastest way to master isolation is to predict two-session outcomes before running them.

Scenario A: dirty read attempt

PostgreSQL never exposes the uncommitted balance
-- Session A                            -- Session B
BEGIN;                                  BEGIN ISOLATION LEVEL READ UNCOMMITTED;
UPDATE accounts
SET balance = 0
WHERE account_id = 42;

                                       SELECT balance FROM accounts
                                       WHERE account_id = 42;
                                       -- Sees the previously committed balance.

ROLLBACK;                               COMMIT;

Scenario B: phantom at Read Committed

Each count uses a different command snapshot
-- Session A                            -- Session B
BEGIN;
SELECT COUNT(*) FROM bookings
WHERE room_id = 7
  AND booking_date = DATE '2026-08-01'; -- 3

                                       INSERT INTO bookings
                                         (room_id, booking_date)
                                       VALUES (7, DATE '2026-08-01');
                                       COMMIT;

SELECT COUNT(*) FROM bookings
WHERE room_id = 7
  AND booking_date = DATE '2026-08-01'; -- 4
COMMIT;

Scenario C: write skew at Repeatable Read

Different rows, shared invariant
-- Rule: at least one operator must remain active.
-- Both transactions use REPEATABLE READ and see two active operators.

-- Session A updates operator 1 to inactive.
-- Session B updates operator 2 to inactive.
-- They do not update the same row.
-- Both may commit, leaving zero active operators.

-- SERIALIZABLE detects the dangerous dependency pattern and aborts one.
-- A schema redesign or explicit coordination may also enforce the rule.

Scenario D: check-then-insert race

Let a unique constraint be the final authority
BEGIN;

SELECT 1 FROM users WHERE email = 'sam@example.com';
-- No row does not reserve this email.

INSERT INTO users (email) VALUES ('sam@example.com');
-- A concurrent transaction may insert it first.

COMMIT;

-- Define UNIQUE (email), then handle the conflict or use ON CONFLICT.

14. Setting transaction characteristics

Isolation, access mode, and deferrability can be set when a transaction starts or before its first query or data-changing statement.

Transaction-level and session-level forms
BEGIN;
SET TRANSACTION
  ISOLATION LEVEL SERIALIZABLE
  READ WRITE
  NOT DEFERRABLE;
-- The SET must happen before the first query or data-changing statement.

SHOW transaction_isolation;
SHOW transaction_read_only;
COMMIT;

-- Default for later transactions in this session.
SET SESSION CHARACTERISTICS AS TRANSACTION
  ISOLATION LEVEL REPEATABLE READ
  READ ONLY;

READ ONLY rejects many commands that change non-temporary tables or schema objects. It expresses intent and reduces accidental writes, but it is not a statement that the server will perform no internal disk writes.

Serializable Read Only Deferrable

A good mode for a long, consistent report
BEGIN TRANSACTION
  ISOLATION LEVEL SERIALIZABLE
  READ ONLY
  DEFERRABLE;

SELECT region, SUM(amount)
FROM payments
GROUP BY region;

COMMIT;

This transaction may wait while acquiring its initial snapshot. Once PostgreSQL finds a safe snapshot, the transaction can run without normal Serializable tracking overhead and without risk of a serialization cancellation caused by concurrent updates. DEFERRABLE has an effect only with SERIALIZABLE READ ONLY.

Exporting and importing a snapshot

Coordinate consistent reads across sessions
-- Exporting transaction
BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY;
SELECT pg_export_snapshot();
-- Example result: 00000003-0000001B-1

-- Importing transaction, before its first query
BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY;
SET TRANSACTION SNAPSHOT '00000003-0000001B-1';

-- The exporter must remain open while the snapshot is imported.

Snapshot export is useful for coordinated parallel reporting or backup tooling. It also prolongs the visibility horizon while the exporting and importing transactions remain open, so keep the operation bounded.

15. Correct transaction retry design

A serialization failure is not a server defect. It is PostgreSQL refusing a concurrent outcome that violates the chosen guarantee. The application must retry the complete transaction.

Language-neutral retry structure
for attempt from 1 to MAX_ATTEMPTS:
    begin transaction isolation level serializable
    try:
        read every value used to make the decision
        recompute the decision
        perform every database write
        commit
        return success
    catch SQLSTATE 40001:
        rollback
        if attempt == MAX_ATTEMPTS:
            report transient failure
        wait using bounded exponential backoff plus jitter
    catch any other error:
        rollback
        rethrow
  • Retry SQLSTATE 40001 from the beginning, including reads and application-side calculations. Retrying only the final UPDATE reuses a stale decision.
  • Deadlock SQLSTATE 40P01 is often retryable too, but it is a different failure and can reveal inconsistent lock ordering.
  • Bound retries. Use exponential backoff and jitter so many conflicting workers do not retry in lockstep.
  • Keep effects outside PostgreSQL idempotent or delay them until the database outcome is known. A retried transaction can otherwise send an email or charge a card twice.
  • Generate an idempotency key at the operation boundary and enforce it with a unique constraint when requests themselves may be repeated.
Commit outcome can be ambiguous to the client

A connection failure during COMMIT can leave the client unsure whether the server committed. A retry loop must distinguish a known serialization rollback from an unknown commit outcome. Idempotency records let the application safely discover or repeat an operation.

16. Performance and operational implications

Transactions are not only correctness boundaries. Their duration and contention shape vacuum progress, lock duration, memory use, latency, and retry rates.

Pattern Cost or risk Better approach
Long transaction with an old snapshot Can prevent cleanup of obsolete tuples and contribute to bloat Process bounded batches and commit between independent units
Idle in transaction session May retain a snapshot, locks, and backend resources Always commit or roll back promptly; configure suitable timeouts
Very large write transaction More WAL, longer recovery on abort, longer lock retention Batch only when batches preserve the business invariant
Many savepoints Subtransaction bookkeeping and cache pressure Use savepoints for real recovery boundaries, not every trivial statement
High Serializable contention More dependency tracking and transaction retries Shorten transactions, reduce hot conflicts, and retry with jitter
Transaction held during network calls Unpredictable duration and longer retained resources Move slow external work outside the transaction where correctness permits
One transaction per inserted row Repeated commit and WAL flush overhead Batch related writes without creating an unbounded transaction
Find sessions that may be holding old transaction state
SELECT
  pid,
  usename,
  state,
  xact_start,
  backend_xid,
  backend_xmin,
  query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start;

A long transaction is not automatically wrong. A consistent backup or report may require one. The operational lesson is to make its cost intentional and monitored. The separate VACUUM topic explains cleanup and transaction ID wraparound in depth.

17. PostgreSQL transaction edge cases

Most table changes are transactional, but several PostgreSQL behaviors surprise developers who assume rollback rewinds every observable effect.

Edge case Actual behavior Consequence
Sequences nextval values are not returned on rollback Sequence gaps are normal; sequences are not gapless invoice numbering
setval Its state changes are visible immediately and not undone by rollback Do not treat sequence state like ordinary table state
Transaction timestamps current_timestamp is fixed at transaction start Use statement_timestamp() or clock_timestamp() when needed
Transactional DDL Many schema changes can roll back Do not assume every database product has identical DDL behavior
MVCC-unsafe DDL TRUNCATE and table-rewriting ALTER TABLE have snapshot caveats Plan these changes carefully around concurrent snapshot transactions
Read-only mode Rejects many logical writes but does not imply zero internal disk writes Treat it as access intent, not storage-level immutability
Exported snapshot Valid only while its exporting transaction remains open Coordinate lifecycle and avoid retaining it longer than necessary
Sequence gaps survive rollback
BEGIN;
INSERT INTO orders (description) VALUES ('cancelled attempt')
RETURNING order_id; -- suppose this returns 100
ROLLBACK;

INSERT INTO orders (description) VALUES ('real order')
RETURNING order_id; -- commonly returns 101, not 100

18. Common mistakes and precise fixes

Mistake Why it fails Fix
Assuming autocommit groups several statements Each statement can commit independently Use an explicit transaction for one logical unit
Continuing after an error without rollback The transaction remains in an aborted state Rollback the transaction or recover to a prior savepoint
Calling savepoints nested commits Only the outer transaction can make their work durable Describe them as partial rollback points
Expecting Read Committed to provide a stable report Every statement can get a newer snapshot Use Repeatable Read or a suitable Serializable read-only mode
Expecting Repeatable Read to prevent write skew A stable snapshot can still produce a non-serializable combined result Use Serializable or redesign enforcement
Using Serializable without retries Correctness can require SQLSTATE 40001 cancellation Retry the complete transaction with bounded backoff
Retrying only the failed write The decision may depend on stale reads Repeat all reads, calculations, and writes
Reading then writing a literal value Concurrent changes can be overwritten Use atomic SQL, a constraint, or compare-and-set versioning
Treating nonzero xmax as deleted The marking transaction may be active, aborted, or only locking Let PostgreSQL's visibility machinery interpret tuple headers
Using xmin as a permanent version field It is internal, finite, and subject to freezing and wraparound concerns Store an explicit application version column
Keeping a transaction open during user input Duration, lock time, and vacuum impact become unbounded Collect input first, then open a short transaction
Expecting rollback to restore sequence numbers Sequence advancement is deliberately non-transactional Accept gaps or design a separately controlled numbering workflow

19. Choosing an isolation strategy

Start from the invariant and concurrent decision pattern, not from a belief that the strongest isolation is always the fastest or that the default is always enough.

Need Good starting point Reason
Independent request with atomic conditional update Read Committed plus constraints One statement can recheck the newest row and enforce a local condition
Consistent multi-query report Repeatable Read, Read Only Every query sees one stable snapshot
Long report that must be serially valid Serializable, Read Only, Deferrable It can wait for a safe snapshot and then run without serialization abort risk
Cross-row rule with concurrent readers and writers Serializable plus complete retry SSI rejects outcomes inconsistent with every serial order
Disconnected edit form Explicit version column and compare-and-set A database transaction should not remain open during human think time
Uniqueness or simple data invariant Database constraint at every isolation level Constraints remain the authoritative last line of defense
  1. Write the invariant in one sentence.
  2. List every row or predicate read to make the decision.
  3. List every concurrent transaction that can change those facts.
  4. Prefer constraints and atomic SQL for invariants they can express directly.
  5. Choose isolation for the remaining multi-statement or multi-row reasoning.
  6. Design retries and idempotency before enabling a retry-producing level.
  7. Test the interleavings using two real sessions, not only single-session unit tests.

20. Practice problems

Predict the visibility, blocking, and commit outcome before reading each answer.

1. A Read Committed transaction runs the same SELECT COUNT(*) twice. Another transaction inserts and commits a matching row between them. Can the counts differ?

Yes. Each statement receives a fresh snapshot, so the second count can include the newly committed row. This is a phantom read.

2. Would requesting Read Uncommitted let the first count see a concurrent uncommitted insert?

No. PostgreSQL treats Read Uncommitted as Read Committed and does not expose dirty tuple data.

3. A Repeatable Read transaction issues BEGIN, waits, and then performs its first SELECT. At which point is its data snapshot chosen?

At the first query or data-modification statement, not merely at BEGIN. Transaction control statements do not establish the data snapshot.

4. Two Repeatable Read transactions read a cross-row rule and update different rows. Must one abort?

Not necessarily. They may avoid a direct write conflict and both commit, producing write skew. Serializable is designed to detect dangerous dependency patterns of this kind.

5. Does RELEASE SAVEPOINT s commit that part of the work?

No. It removes the recovery point while retaining its changes inside the outer transaction. The outer transaction can still roll back everything.

6. A unique violation happens after a savepoint. How can the transaction continue safely?

Execute ROLLBACK TO SAVEPOINT for the earlier savepoint, then continue from the restored transaction state. Without that recovery, later commands are rejected.

7. A Serializable transaction fails with SQLSTATE 40001. Should only COMMIT be retried?

No. Roll back and repeat the entire transaction, including every read and application calculation. The original decision was made from a snapshot that can no longer commit safely.

8. An inserted row received sequence value 55, then its transaction rolled back. Must the next row receive 55?

No. Sequence values are not reclaimed on rollback. The next value will commonly be 56 or later, and gaps are correct behavior.

9. A row has nonzero xmax but is visible to your query. Is PostgreSQL corrupted?

No. The marking transaction might be uncommitted, aborted, or represented there for locking metadata. Visibility requires more than testing whether xmax is zero.

10. Why can an hour-long idle transaction cause table growth even when it performs no more work?

Its old snapshot or transaction horizon can keep obsolete row versions potentially visible, limiting cleanup. It may also retain locks and backend resources.

21. Interview Q&A

Q: What is MVCC in PostgreSQL?

MVCC is Multi-Version Concurrency Control. PostgreSQL keeps multiple physical tuple versions so each statement can see the version valid for its snapshot. This lets ordinary reads and writes proceed without blocking each other, while vacuum later reclaims obsolete versions.

Q: What do tuple xmin and xmax mean?

Tuple xmin is the transaction ID that created that row version. xmax stores transaction-related deletion, update, or locking metadata. Nonzero xmax alone does not prove invisibility because transaction status and snapshot rules also matter.

Q: What is the difference between tuple xmin and snapshot xmin?

Tuple xmin identifies a version's creator. Snapshot xmin is the lowest transaction ID still active for a snapshot and helps define its visibility horizon. They share a name but represent different things.

Q: Does PostgreSQL support dirty reads?

No. Even when Read Uncommitted is requested, PostgreSQL uses Read Committed visibility behavior. A query does not see another transaction's uncommitted tuple writes.

Q: Read Committed versus Repeatable Read?

Read Committed takes a new snapshot for each command, so repeated queries can see newer commits. Repeatable Read uses one snapshot established by the first query or data-changing statement, so repeated row and predicate reads remain stable.

Q: Does PostgreSQL Repeatable Read allow phantom reads?

No. Although the SQL standard permits phantoms at that level, PostgreSQL's snapshot isolation implementation prevents them. It can still allow serialization anomalies such as write skew.

Q: Repeatable Read versus Serializable?

Both use a stable transaction snapshot. Serializable additionally tracks read/write dependencies and cancels a transaction when needed to ensure committed effects match some serial execution. Repeatable Read does not provide that final serial-order guarantee.

Q: What is write skew?

Two transactions read a shared condition, update different rows, and both commit changes that jointly violate the condition. There may be no same-row write conflict. PostgreSQL Serializable can detect the dangerous dependency pattern and abort one transaction.

Q: Does Serializable mean transactions execute one at a time?

No. Transactions still run concurrently through MVCC. The committed result must be explainable as some serial order. PostgreSQL uses SSI tracking and aborts a transaction if necessary to preserve that property.

Q: How should a serialization failure be handled?

Detect SQLSTATE 40001, roll back, and rerun the complete transaction with fresh reads and calculations. Use bounded retry attempts, backoff, jitter, and idempotent handling of effects outside the database.

Q: What is a savepoint?

A savepoint is a named point inside one transaction. Rolling back to it discards later work and restores a usable transaction. Releasing it keeps the work but does not commit independently; only the outer commit makes the work durable.

Q: Why are long-running transactions harmful?

They can retain old snapshots, delay cleanup of dead tuples, hold locks and resources, increase bloat, and make operational changes wait. Transactions should normally be as short as correctness allows.

Q: How do you prevent an application-level lost update?

Prefer an atomic relative update such as SET quantity = quantity - 1 with a guarded predicate. For disconnected edits, include an explicit version in the WHERE clause and treat zero updated rows as a conflict.

Q: Are sequence values transactional?

Sequence advancement is deliberately not rolled back, so concurrent allocation stays efficient and values remain unique. Aborts and conflicts can create gaps. A sequence should not be used when gapless numbering is a legal or business requirement.

Q: When is a Serializable Read Only Deferrable transaction useful?

It is useful for long consistent reports or backups. PostgreSQL may delay its first snapshot until the snapshot is safe, after which it avoids normal Serializable tracking overhead and serialization cancellation risk from concurrent updates.

22. One-screen cheat sheet

  • Transaction: one atomic database unit ending in commit or rollback.
  • Autocommit: usually one implicit transaction per statement.
  • Savepoint: partial rollback point, never an independent commit.
  • MVCC: concurrent snapshots choose among physical tuple versions.
  • UPDATE: marks an old version and creates a replacement version.
  • Tuple xmin: XID that created a physical row version.
  • Tuple xmax: deletion, update, or locking metadata; nonzero is not enough.
  • Snapshot: transaction visibility information, not a database copy.
  • Read Uncommitted: behaves like Read Committed in PostgreSQL.
  • Read Committed: fresh snapshot per command; default level.
  • Repeatable Read: stable transaction snapshot; no PostgreSQL phantoms.
  • Repeatable Read risk: serialization anomalies such as write skew.
  • Serializable: serial outcome through SSI dependency checks and aborts.
  • 40001: retry the entire transaction from fresh reads.
  • Atomic update: compute from the current column in one SQL statement.
  • Optimistic edit: explicit version column plus compare-and-set.
  • Constraint: final authority for expressible data invariants.
  • Sequences: unique allocation can have gaps and does not rewind on rollback.
  • Operational rule: keep transactions short and never idle unintentionally.

23. Official reference map

Last reviewed · July 2026 · part of knowledge-base