How it works
The explanation, with examples you can run.
The anomalies
- Dirty read: seeing another transaction's uncommitted changes. Never happens in PostgreSQL.
- Non-repeatable read: reading a row twice and getting different values, because another transaction committed a change in between.
- Phantom read: running the same query twice and getting a different set of rows.
- Lost update: two transactions read a value, both write a new value based on it, and one write silently replaces the other.
- Write skew / serialization anomaly: each transaction is fine alone, but together they break a rule neither could see being broken.
Read committed
Each statement gets a fresh snapshot. If an UPDATE finds a row that another transaction is changing, it waits; when that commits, it re-evaluates its condition against the new version and applies the change to it. So SET balance_cents = balance_cents - 100 is safe: both decrements land. The lost update happens when the new value is computed in the application from an earlier read:
-- Transaction A (read committed)BEGIN;SELECT balance_cents FROM accounts WHERE id = 1; -- 10000-- ... meanwhile B commits: UPDATE accounts SET balance_cents = balance_cents - 500 WHERE id = 1;UPDATE accounts SET balance_cents = 9900 WHERE id = 1; -- computed from the stale 10000COMMIT;-- final balance: 9900, B's -500 is lostFixes: do the arithmetic in SQL, lock the row when reading it with SELECT ... FOR UPDATE, or use a stronger level.
Repeatable read
The transaction sees one snapshot, taken at its first statement. Running the same lost-update script under BEGIN ISOLATION LEVEL REPEATABLE READ, A's update no longer overwrites B:
ERROR: could not serialize access due to concurrent updateThe application must retry the transaction from the start, reading the new balance. Repeatable read in PostgreSQL also prevents phantoms. What it does not prevent is write skew. In the on-call example, both transactions count two doctors on duty and each takes a different doctor off; there is no conflicting write, so both commit and nobody is on call.
Serializable
Serializable uses Serializable Snapshot Isolation: transactions run on snapshots as in repeatable read, while PostgreSQL tracks which transactions read data that others wrote. If the pattern could produce a result no serial order could, one transaction is aborted. The on-call example:
ERROR: could not serialize access due to read/write dependencies among transactionsDETAIL: Reason code: Canceled on identification as a pivot, during commit attempt.HINT: The transaction might succeed if retried.All serialization failures use SQLSTATE 40001. Code that uses repeatable read or serializable needs a retry loop around the whole transaction, including the reads, and should keep transactions short, because the longer they run the more they conflict. Declaring read-only transactions as READ ONLY helps the serializable checks.
The next topic shows the machinery that makes all of this possible without readers blocking writers: row versions.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.