Dirty Reads, Non-Repeatable Reads, Phantom Reads and Serialization Anomalies
coreintermediateA dirty read sees another transaction's uncommitted change. A non-repeatable read sees the same row return different values on a second SELECT within one transaction, because another transaction committed a change to it in between. A phantom read sees a repeated query return a different set of rows (not just different values), because another transaction inserted or deleted matching rows in between. A serialization anomaly is the most subtle: the combined result of several committed, concurrent transactions could not have happened under ANY serial (one-at-a-time) ordering of them.
Think of it as
These four phenomena form an escalating ladder of "how much can concurrent activity disturb what I observe." A dirty read is the most obviously dangerous — building on data that might not even exist a moment later. A non-repeatable read is subtler: every value you read was genuinely committed, just not stable across your own transaction. A phantom read is the same instability applied to a set of rows rather than one row's values. A serialization anomaly is the subtlest of all — every individual read was fine, but the transactions collectively produced an outcome that is provably impossible under any actual one-at-a-time execution, which is exactly the class of bug that isolation levels beyond Read Committed exist to prevent.
What we're doing: Demonstrate a phantom read under Read Committed: the same WHERE condition matches a different set of rows on a second SELECT, because another transaction inserted a new matching row in between.
- 3
- First read of the matching set — 5 rows, inside Session A's still-open transaction.
- 5–6
- Session B inserts and commits a new row that matches the same WHERE condition, entirely independently of Session A.
- 8
- The identical query, run again in the SAME transaction, now sees a different set of matching rows — the defining feature of a phantom read.
count
-------
5
count
-------
6Why this works: Under Read Committed, each SELECT takes its own fresh snapshot, so a newly-committed row that matches the WHERE clause is legitimately visible to the second query — this is expected, documented behavior at this level, not a bug, and it is exactly the instability Repeatable Read exists to eliminate for transactions that need their own view of the matching set to stay fixed.
Counting on a repeated aggregate query to return the same answer within one Read Committed transaction
Wrong
Better
What you see: Logic that reads a count early in a transaction and later assumes the same set of rows still matches produces subtly wrong results under concurrent write load — off-by-one allocations, a worker that never gets assigned the "phantom" row, or double-counted totals.
Why: Read Committed intentionally gives every statement the freshest possible snapshot, which means a count taken early in a transaction is not a promise about what a later query in the SAME transaction will see — any logic that needs the matching row set to stay fixed for the whole transaction needs Repeatable Read (or Serializable) to actually get that guarantee.
- Transaction A → accounts table: SELECT balance → 1000
- Transaction B → accounts table: UPDATE balance = 900; COMMIT
- Transaction A → accounts table: SELECT balance → 900 (different!)
The four phenomena
Remember: Dirty read (uncommitted data — never happens in PostgreSQL), non-repeatable read (a row's values change between two reads), phantom read (the matching row SET changes between two reads), serialization anomaly (the combined outcome is impossible under any serial ordering). Repeatable Read prevents the first three; only Serializable prevents all four.
See also: transaction isolation levels · postgresqls behavior under its isolation levels

