4.4.18 · D2Databases

Visual walkthrough — Concurrency anomalies — dirty read, non-repeatable read, phantom read

1,896 words9 min readBack to topic

We build everything from the ground up. No word is used before it is drawn.


Step 1 — The only two things a transaction can do to shared data

WHAT. Before anomalies, we need the alphabet. A transaction is just a to-do list of steps a database runs as one unit (see ACID Properties for why "one unit" matters). On any single piece of data — call it a row — a transaction does exactly one of two things:

  • Read it (look at the value, change nothing), written = "read row ".
  • Write it (change the value), written = "write row ".

And at the end it either commits (writes become permanent, symbol ) or rolls back (writes are thrown away, symbol for "abort").

WHY these symbols. We need a compact language so two transactions can be drawn side by side on the same clock. That side-by-side clock is where every anomaly lives — an anomaly is never about one transaction alone, it's about interleaving.

PICTURE. Time flows left → right. Two lanes: on top, below. Each tick is one step.

Figure — Concurrency anomalies — dirty read, non-repeatable read, phantom read

Step 2 — The single root cause (draw it once, reuse three times)

WHAT. Every one of the three anomalies is the same sentence:

made a decision using data that was not stable — either not committed, or changed by between two of 's looks.

WHY this matters. If we can draw "unstable data" as a picture, we don't memorize three separate bugs — we recognize one shape and ask two yes/no questions to name which flavor it is.

PICTURE. A value line for row over time. Green = committed & stable. Red = provisional or shifting. An anomaly is whenever 's read arrow lands on red.

Figure — Concurrency anomalies — dirty read, non-repeatable read, phantom read

Now we walk each branch of that tree as its own timeline.


Step 3 — Branch A: Dirty Read (the read arrow lands on RED)

WHAT. writes row but has not committed. reads now, while it's still red. Then aborts () — the value is erased.

WHY this step exists. This is the "Q1 = No" branch: trusted a value the database never promised to keep. The write was pencil; the abort was the eraser.

PICTURE. Watch the red write at , the read arrow at pulling that red value up into , then the abort at deleting it — leaving holding a ghost.

Figure — Concurrency anomalies — dirty read, non-repeatable read, phantom read
  • sets balance to , but no yet, so it's provisional (red).
  • 's read arrow lands on red; now believes .
  • — the rollback. The value read never officially existed.

Step 4 — Branch B: Non-Repeatable Read (green, but the value MOVED)

WHAT. reads and gets a committed (green) value. then does an UPDATE on that same existing row and commits. reads again — different green value.

WHY it's a different bug from dirty. Q1 is now Yes (both reads see committed data — nothing red). The problem is that the green value itself slid between 's two looks. is internally inconsistent: it saw two truths about one row.

PICTURE. Two read arrows from hitting the same row line at different heights, with 's committed update in the gap between them.

Figure — Concurrency anomalies — dirty read, non-repeatable read, phantom read
  • — first look, a legal committed value.
  • changes the same row and commits (green, not red).
  • — same query, same row, new answer. Not repeatable.

Step 5 — Branch C: Phantom Read (the SET of rows moved)

WHAT. asks a range question — WHERE dept='Sales' — and counts rows. INSERTs a new matching row and commits. asks the same question and now counts .

WHY it's a third bug. Q1 = Yes again (all committed, all green). But Q2's answer flips: no existing row changed value. A brand-new row walked into the range. Locking the rows you read does nothing — the intruder wasn't among them.

PICTURE. A shaded "range box" (the dept='Sales' predicate). First scan: dots inside. drops an th dot into the box. Second scan: dots — a phantom appeared.

Figure — Concurrency anomalies — dirty read, non-repeatable read, phantom read
  • — a predicate read, not a single-row read. It scans whichever rows match.
  • — a row that didn't exist is created inside the predicate and committed.
  • — the membership of the result set changed.

Step 6 — The degenerate & boundary cases (never leave a gap)

WHAT. We must check the corners, or the reader hits an unshown scenario.

PICTURE. Four mini-timelines, one per corner.

Figure — Concurrency anomalies — dirty read, non-repeatable read, phantom read
  1. commits instead of aborting in Step 3. Then 's read of was... still a dirty read at the moment it happened — the anomaly is defined by reading uncommitted data, not by the eventual outcome. Dirtiness is about when, not what happens later.
  2. reads a row exactly once. Non-repeatable read is impossible — you need two reads to compare. One look can't disagree with itself.
  3. Empty range in Step 5 (WHERE dept='Sales' matches rows first). A single insert still makes : phantom reads include the case of rows appearing from nothing and (by symmetry, via DELETE) rows vanishing to nothing.
  4. touches a different row than reads. No anomaly at all — the read arrow never lands on the changed data. Anomalies require overlap on the same row or the same predicate.

The one-picture summary

All three anomalies are one timeline template with two switches flipped:

Figure — Concurrency anomalies — dirty read, non-repeatable read, phantom read
Recall Feynman retelling of the whole walkthrough

Draw two runners on the same track (that's the schedule). Runner keeps glancing at a scoreboard while running.

  • If reads a score the other runner scribbled in pencil and then rubbed out — dirty read.
  • If reads the score, looks away, looks back, and the same box now shows a different (but officially-written-in-pen) number — non-repeatable read.
  • If counts "5 scoreboards", looks back, and a brand-new sixth scoreboard has been bolted to the wall — phantom read. Same picture, one question each: was it in pen? (no → dirty), and did an old box change or a new box appear? (change → non-repeatable, appear → phantom). The database can freeze less or more of the wall — freezing more kills more glitches but slows everyone down. Choose the least freezing that stops the glitch your logic cares about.