4.4.18 · D5Databases

Question bank — Concurrency anomalies — dirty read, non-repeatable read, phantom read

1,774 words8 min readBack to topic

Before the traps, the two anchors that resolve 90% of confusion:


True or false — justify

Every item here is a statement. Decide true/false, then say why — the justification is the whole point.

A dirty read requires the writing transaction to eventually roll back.
False — the danger is realised on rollback, but the dirty read itself is defined the moment reads 's uncommitted value. Even if later commits, the read was still "dirty" because at read time the data was not stable.
A non-repeatable read can occur even if every value reads is committed.
True — that is exactly its nature. Both reads see committed data; the problem is the committed value changed between them, so sees two truths about one row.
If a database has only one transaction running at a time, concurrency anomalies are impossible.
True — every anomaly requires a second transaction () to modify data during 's lifetime. With no concurrency there is no "other" work to leak in.
Read Committed prevents dirty reads by never reading data that any transaction has touched.
False — it only refuses to read uncommitted data. It happily reads committed data, including values that a concurrent transaction commits and re-commits repeatedly.
Repeatable Read guarantees the entire result of your transaction is as if it ran alone.
False — it freezes the rows you read, killing non-repeatable reads, but new rows matching a range can still appear (phantoms). Only Serializable gives the "ran alone" guarantee.
A phantom read involves at least one row whose value changed.
False — no existing row's value need change. A brand-new row (INSERT) or a vanished row (DELETE) alters which rows match your condition; the surviving rows can be untouched.
Higher isolation levels are strictly safer, so Serializable is always the correct default.
False on the "always" — Serializable removes all three anomalies but throttles concurrency, raising contention, deadlocks, and aborts. Pick the lowest level that forbids the anomalies your logic actually cares about.
Every anomaly is caused by 's write, never by 's read.
True — reads never mutate data, so a lone read by cannot make 's data shift. All three anomalies trace back to a write (uncommitted for dirty; committed UPDATE for non-repeatable; committed INSERT/DELETE for phantom).
If two of 's reads return the same value, no anomaly occurred on that row.
False — the value could have been changed to and back to by committed writes between the reads, or the equality could be luck. "Same value observed" is weaker than "guaranteed stable". Stability is a property the isolation level provides, not something you infer from one lucky match.

Spot the error

Each line contains a plausible but wrong claim. State what's wrong.

" reads 100, commits an UPDATE to 150, reads 150 — that's a phantom read."
Wrong label — an existing row's value changed (UPDATE), so this is a non-repeatable read. Phantoms are about the set of matching rows changing via INSERT/DELETE.
" INSERTs a matching row and commits, 's COUNT rises — that's a non-repeatable read."
Wrong label — the membership of the result set changed, not any existing row's value, so this is a phantom read. Non-repeatable is about a row you already read moving under you.
"We set Read Committed, so re-reading the same row twice always gives the same answer."
Wrong guarantee — Read Committed only makes each individual read see committed data. Cross-read stability of one row requires Repeatable Read.
"Row-level locks on the rows read will prevent phantom reads."
Wrong tool — row locks protect rows that already exist. A phantom is a new row entering the range, which no existing-row lock covers. You need range/predicate (next-key) locks or serialization.
"A rolled-back transaction leaves no trace, so it can never cause an anomaly in another transaction."
Wrong — before it rolls back, its uncommitted write can be read by (dirty read). The rollback is precisely what makes that read a lie; the trace is the wrong decision already made.
"The SQL standard says Repeatable Read forbids phantoms."
Wrong — the SQL standard allows phantoms at Repeatable Read. Some engines (e.g. InnoDB) go beyond the standard and block them via next-key locks, but that is engine-specific, not the standard.
"Serializable means the database literally runs transactions one after another with no overlap."
Wrong — it means the observable result is equivalent to some serial order. Transactions may still overlap physically; the engine enforces serializability via locking or conflict detection, not by forcing single-file execution.

Why questions

Why does classifying by "what changed and when" cleanly separate the three anomalies?
Because the two questions are independent: "when" splits uncommitted (dirty) from committed (the other two), and "what" splits value-of-a-row (non-repeatable) from membership-of-a-set (phantom). Two binary axes give exactly the distinct cases we need.
Why does a dirty read not require re-reading, while non-repeatable and phantom reads do?
A dirty read is defined by reading uncommitted data — a single read of the wrong kind of value is enough. The other two are defined by inconsistency between two reads, so they need a first read to be contradicted by a second.
Why does preventing non-repeatable reads still leave phantoms possible?
Preventing non-repeatable reads means locking/snapshotting the rows you have already read so they can't change. A phantom is a row you haven't read yet because it didn't exist when you looked — there's nothing to lock until you also lock the condition it would match.
Why is more isolation not free?
More isolation means holding locks longer or over wider ranges (or aborting more conflicting transactions). That reduces how many transactions can proceed simultaneously, increasing waiting, deadlock risk, and abort/retry cost — throughput drops.
Why is "which anomalies do I care about?" the right question rather than "which level is safest?"
Because the correct level is the lowest one that forbids the anomalies your business logic can't tolerate. Over-isolating pays a performance tax for safety you don't need; under-isolating admits a bug. The anomaly set is the actual requirement.
Why can a snapshot-based engine (MVCC) prevent non-repeatable reads without blocking writers?
MVCC — Multiversion Concurrency Control gives a consistent point-in-time snapshot, so its re-reads see the same version it first saw. writes a new version without waiting, so readers and writers don't block each other — the re-read is stable by construction, not by locking. See also Isolation Levels.

Edge cases

reads a row, DELETEs that exact row and commits, reads it again and finds it gone. Which anomaly?
This straddles the boundary: the specific row you read disappearing is often treated as a non-repeatable read (the row you held changed — to non-existent), but because it also changes range membership many texts call it a phantom. The clean rule: value-change of a still-present row = non-repeatable; change in which rows match a predicate = phantom. A DELETE of a matched row is a membership change → phantom under the strict "set" definition.
Read Uncommitted is chosen but never rolls back — only commits. Can a dirty read still "happen"?
The dirty read still occurs (you read uncommitted data), but the harmful consequence doesn't materialise this time because the value was later confirmed. It's a latent bug: correct by luck, wrong whenever a rollback eventually does occur.
runs a range query, INSERTs a row that does not match the condition, commits. Phantom?
No — a phantom requires the matching set to change. An insert outside the predicate leaves 's result identical, so no anomaly is observed for that query.
Zero rows match 's condition on both reads, but inserted-then-deleted a matching row (both committed) between them. Phantom?
For 's two observations the result set is unchanged (empty both times), so observes no phantom. Anomalies are defined by what a transaction observes, not by unrelated churn it never sees.
Both and only ever read (no writes at all). Can any of the three anomalies occur?
No — all three anomalies are triggered by a write from the other transaction. Two pure readers cannot disturb each other, regardless of isolation level.
is at Serializable; is it now immune to seeing any stale-feeling data?
It is immune to the three standard anomalies, so its result equals some serial order. But "serial order" may place logically before recent commits, so data can still feel "old" — that's correct, not an anomaly, because a consistent serial position is exactly the guarantee. This is why Serializable can force aborts under Two-Phase Locking (2PL) and raises Deadlocks risk.
A single transaction reads a row twice and sees different values, with no other transaction running. Anomaly?
Not a concurrency anomaly — with no concurrent writer, the change came from 's own write between the reads, which is intended behaviour. Anomalies require another transaction's interference.