Skip to content

Non-repeatable read

A non-repeatable read is reading the same row twice inside one transaction and getting different data, because another transaction committed a change in between. Nothing you read was uncommitted (every value was real), yet your transaction can no longer trust its own earlier reads.

Session A
Session B
BEGIN
SELECT balance→ 100
UPDATE accounts SET balance = 200
COMMIT
SELECT balance→ 200 ← same query, different answer
COMMIT

Formally Adya's P2. It is the flagship anomaly of READ COMMITTED, where every statement gets a fresh snapshot: each statement is internally consistent, but two statements may disagree with each other.

Read skew

Non-repeatable reads have a nastier multi-row cousin, read skew (formally Adya's G-single): read row 1, let someone move money to row 2, read row 2. Every row you saw was committed and correct, yet the combination existed at no point in time:

Session A
Session B
SELECT balance WHERE id = 1→ 500
moves 200 from account 1 to account 2
COMMIT
SELECT balance WHERE id = 2→ 500 ← total reads 1000; it was never 1000

A backup taken this way is corrupt; a report computed this way is wrong, even though no single read misbehaved.

Who prevents it

LevelSQL standardPostgreSQLMySQL (InnoDB)
READ COMMITTEDpermittedhappens (proof)happens (proof)
REPEATABLE READpreventedprevented (proof)prevented for plain SELECTs (proof); UPDATE/DELETE are current reads that bypass the snapshot
SERIALIZABLEpreventedpreventedprevented

The cure is a per-transaction snapshot: at REPEATABLE READ, both engines give your SELECTs one frozen view for the whole transaction. MySQL's asterisk matters, though: its writes and locking reads see the current data regardless of the snapshot, which is exactly how lost updates survive there.

  • Phantom read, the same effect on a set of rows: the rows you saw don't change, but new ones appear.
  • Lost update: what happens when you write back through a stale read instead of merely looking at it.

See it happen

MIT Licensed · Every transcript on this site was generated by a real database run against MySQL 8.4.11 and PostgreSQL 18.4 at c52e5c9, and re-proven through psycopg and PyMySQL.