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 COMMITTEDpermittedhappensproofhappensproof
REPEATABLE READpreventedprevented — proofprevented 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.10 and PostgreSQL 18.4 at bd6f201, and re-proven through psycopg and PyMySQL.