SQLMentor // learn postgresql

Transactions & Isolation

A transaction groups multiple SQL statements into a single atomic unit. Either all statements succeed and commit, or all are rolled back as if they never happened. PostgreSQL has excellent transaction support, with multiple isolation levels and MVCC enabling high concurrency.

Basic Transaction Commands

BEGIN;              -- start a transaction
COMMIT;             -- save all changes permanently
ROLLBACK;           -- undo all changes since BEGIN

-- Also valid
START TRANSACTION;  -- same as BEGIN
END;                -- same as COMMIT
ABORT;              -- same as ROLLBACK

Simple Transaction Example

-- Transfer $500 from account 1 to account 2
BEGIN;

UPDATE accounts SET balance = balance - 500 WHERE id = 1;
UPDATE accounts SET balance = balance + 500 WHERE id = 2;

-- Check for problems before committing
SELECT id, balance FROM accounts WHERE id IN (1, 2);

COMMIT;  -- makes both changes permanent atomically

If the server crashes between the two UPDATEs, PostgreSQL's WAL (Write-Ahead Log) ensures that on recovery, neither update is applied — the transaction is rolled back automatically.

SAVEPOINT — Nested Rollback Points

SAVEPOINTs let you roll back part of a transaction without losing everything:

BEGIN;

INSERT INTO orders (customer_id, total) VALUES (1, 150.00);

SAVEPOINT after_order;  -- mark this point

INSERT INTO order_items (order_id, product_id) VALUES (LASTVAL(), 999);
-- Suppose product 999 doesn't exist — this might fail

ROLLBACK TO SAVEPOINT after_order;  -- undo only the order_items insert

-- The order itself is still there
INSERT INTO order_items (order_id, product_id) VALUES (LASTVAL(), 5);  -- valid product

RELEASE SAVEPOINT after_order;  -- optional cleanup

COMMIT;
In PostgreSQL, once any statement in a transaction causes an error, the entire transaction is in an aborted state. You must ROLLBACK (or ROLLBACK TO SAVEPOINT) before you can do anything else. This is why applications use SAVEPOINTs to handle recoverable errors within a transaction.

ACID Properties

Property Meaning PostgreSQL implementation
Atomic All or nothing Transactions with WAL
Consistent Constraints are maintained Constraints checked at commit
Isolated Concurrent transactions don't interfere MVCC + isolation levels
Durable Committed data survives crashes WAL (Write-Ahead Log)

Transaction Isolation Levels

Isolation levels control what a transaction can see from concurrent transactions. PostgreSQL supports all four SQL-standard isolation levels.

-- Set isolation level for the current transaction
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- or
BEGIN ISOLATION LEVEL SERIALIZABLE;

The Concurrency Anomalies

Anomaly Description
Dirty Read Reading uncommitted data from another transaction
Non-repeatable Read Reading the same row twice gets different values (another transaction committed between reads)
Phantom Read Re-executing a query returns different rows (another transaction inserted/deleted between queries)
Serialization Failure The combined effect of concurrent transactions is not achievable serially

Isolation Level Comparison

Level Dirty Read Non-Repeatable Read Phantom Read Serialization Failure
READ UNCOMMITTED Possible* Possible Possible Possible
READ COMMITTED Not possible Possible Possible Possible
REPEATABLE READ Not possible Not possible Not possible** Possible
SERIALIZABLE Not possible Not possible Not possible Not possible

*PostgreSQL treats READ UNCOMMITTED the same as READ COMMITTED — it never allows dirty reads. **PostgreSQL's MVCC prevents phantom reads even at REPEATABLE READ.

READ COMMITTED (Default)

Each statement sees only data committed before that statement began. Most common for web applications:

-- Session A
BEGIN;
SELECT balance FROM accounts WHERE id = 1;  -- sees 1000

-- Session B commits: UPDATE accounts SET balance = 900 WHERE id = 1; COMMIT;

SELECT balance FROM accounts WHERE id = 1;  -- NOW sees 900 (committed between statements!)
COMMIT;

This is fine for most operations but can surprise you in multi-statement transactions.

REPEATABLE READ

All statements in the transaction see a consistent snapshot from when the transaction began:

BEGIN ISOLATION LEVEL REPEATABLE READ;

SELECT balance FROM accounts WHERE id = 1;  -- sees 1000

-- Even if another transaction commits balance = 900, this transaction still sees 1000

SELECT balance FROM accounts WHERE id = 1;  -- still sees 1000
COMMIT;

Use REPEATABLE READ for:

  • Generating consistent reports across multiple queries
  • Batch reads where you need a point-in-time snapshot

SERIALIZABLE

The highest isolation level. Transactions behave as if they ran one at a time, even though they run concurrently. PostgreSQL uses Serializable Snapshot Isolation (SSI), which is more sophisticated than two-phase locking:

BEGIN ISOLATION LEVEL SERIALIZABLE;

SELECT SUM(amount) FROM payments WHERE user_id = 1;
-- ... make decisions based on sum ...
INSERT INTO payments (user_id, amount) VALUES (1, 100);

COMMIT;  -- may fail with: ERROR: could not serialize access due to concurrent update

If PostgreSQL detects that the combined effect of two concurrent transactions cannot be achieved by any serial execution, one of them is aborted with a serialization error. The application must retry.

SERIALIZABLE transactions can fail with a serialization error even if neither transaction actually conflicts. Your application code must always be prepared to retry SERIALIZABLE transactions. Use REPEATABLE READ if you only need snapshot consistency — it doesn't produce serialization failures.

MVCC — How PostgreSQL Implements Isolation

PostgreSQL uses Multi-Version Concurrency Control (MVCC). Key points:

  • Every row has hidden xmin (created by transaction ID) and xmax (deleted/updated by transaction ID) system columns
  • Writers create new versions of rows rather than overwriting
  • Readers see rows based on their transaction's snapshot
  • Readers never block writers; writers never block readers
-- See the system columns (usually hidden)
SELECT xmin, xmax, id, name FROM products LIMIT 3;
xmin xmax id name
12345 0 1 Widget
12346 12400 2 Gadget
12347 0 3 Doohick

xmax = 0 means the row is current (not deleted). xmax = 12400 means transaction 12400 deleted or updated it.

Locking — SELECT FOR UPDATE

For explicit row-level locking (to prevent another transaction from modifying a row you're working with):

-- Lock the row for update (other transactions that try to lock or update this row will wait)
BEGIN;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
-- Now update safely — no other transaction can modify this row until we commit
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;

NOWAIT — Fail Instead of Wait

-- Fail immediately if the row is locked, rather than waiting
SELECT * FROM inventory WHERE product_id = 5 FOR UPDATE NOWAIT;
-- If locked: ERROR: could not obtain lock on row in relation "inventory"

SKIP LOCKED — Skip Already-Locked Rows

SKIP LOCKED is perfect for implementing job queues:

-- Worker picks up a job without waiting for locked jobs
BEGIN;
SELECT *
FROM job_queue
WHERE status = 'pending'
ORDER BY priority DESC, created_at ASC
LIMIT 1
FOR UPDATE SKIP LOCKED;

-- Process the job, then:
UPDATE job_queue SET status = 'done' WHERE id = :job_id;
COMMIT;

Multiple workers can run this query simultaneously. Each picks a different unlocked job — no collisions, no waits, no duplicate processing.

Autocommit

By default, PostgreSQL runs in autocommit mode when you're outside of an explicit transaction. Every statement is its own transaction:

-- This DELETE commits immediately — no rollback possible!
DELETE FROM orders WHERE order_date < '2020-01-01';

-- Safer: wrap in explicit transaction so you can check before committing
BEGIN;
DELETE FROM orders WHERE order_date < '2020-01-01';
SELECT COUNT(*) FROM orders;  -- sanity check
COMMIT;  -- only after you're sure
In psql, always run dangerous operations (DELETE without WHERE, DROP TABLE, large UPDATEs) inside an explicit BEGIN ... COMMIT block. If something looks wrong after seeing the query result, you can ROLLBACK instead of committing.