Skip to content

Interactive · Runs in your browser · No signup

Watch two transactions quietly corrupt each other.

An interactive database simulator. Run two transactions side by side, one step at a time, and see exactly where the database gave a wrong answer — then change the engine or the isolation level and watch the same steps come out differently.

Opens the schedule below, running on PostgreSQL 16 at REPEATABLE READ.

01234567T1committedT2committedb1b2r1[P:1..2]{1,2}r2[P:1..2]{1,2}w1[1=0]w2[2=0]c1c2anomaly
Two transactions, eight steps, left to right. Both read the roster and see the same two doctors; each then writes a different row. Nothing collides, both commit, and the red mark is where it became unavoidable.

A failure you can hold in your head

A hospital rule says at least one doctor must stay on call. Two are on call right now. Both decide to go home at the same moment.

  1. Dr ADr A checks how many doctors are on call. The answer is two.
  2. Dr BDr B checks at the same instant. Also two.
  3. Dr AA sees that one other doctor remains, so A takes themselves off call.
  4. Dr BB sees that one other doctor remains, so B takes themselves off call.

Nobody is on call. Neither transaction touched the other’s row, nothing was locked, no error was raised, and both committed successfully. This is write skew, and most databases permit it at the level you are probably running right now.

In plain terms

Your database runs many transactions at the same time. To stay fast, it lets them see slightly stale or half-finished versions of each other’s work. Usually that is invisible. Sometimes two transactions overlap in just the wrong order and the result is an answer that is simply wrong — money counted twice, a rule enforced by nobody, a row that vanishes mid-read. The setting that decides how much overlap is allowed is called the isolation level. This site lets you cause those failures on purpose and watch them happen.

One run, all the way through

Here is the whole thing, step by step, with the real numbers — produced by running this schedule through the same simulator the rest of the site uses. Nothing here needs you to press anything. Watch for the moment where nothing at all goes wrong: every statement below succeeds.

PostgreSQL 16 · REPEATABLE READ

Key 1
Dr A
Key 2
Dr B
= 0
off call
= 1
on call
  1. 0b1begin
  2. 1b2begin
  3. 2r1[P:1..2]range read over keys 1..2{1=1, 2=1}

    part of the table before the run started

  4. 3r2[P:1..2]range read over keys 1..2{1=1, 2=1}

    part of the table before the run started

  5. 4w1[1=0]write key 1 = 0
  6. 5w2[2=0]write key 2 = 0

    T1 wrote key 1 at step 4 and T2 wrote key 2 at step 5. Each had already read the row the other wrote, neither saw the other's write, and no row was written twice — so nothing conflicted and both committed.

  7. 6c1commit
  8. 7c2commit

The table is left as 1=0 2=0

Neither transaction saw uncommitted data, neither wrote a row the other wrote, no lock was contended and no error was raised. Each read was correct as of the moment its snapshot was taken. The two snapshots were taken before either write, and that is the whole bug.

The same eight steps, one level higher

Nothing about the schedule changes — same statements, same order, same data. Only the isolation level is different, and the run below is produced the same way as the one above.

PostgreSQL 16 · SERIALIZABLE

  1. 0b1
  2. 1b2
  3. 2r1[P:1..2]
  4. 3r2[P:1..2]
  5. 4w1[1=0]
  6. 5w2[2=0]
  7. 6c1
  8. 7c240001 could not serialize access due to read/write dependencies among transactions

The table is left as 1=0 2=1

One doctor is still on call, because the database refused the second commit rather than allowing it.

How to use this

  1. 1

    Pick a failure

    Each scenario is a real, classic failure written out as two or three transactions with their statements interleaved in a specific order.

  2. 2

    Step through it

    Press Next to advance one statement at a time. The score shows who did what and when; the panels below show the state of the engine at that exact moment.

  3. 3

    Change one thing

    Swap the engine or raise the isolation level and the same steps re-run instantly. What changes — and what stubbornly does not — is the whole lesson.

Write skew is the point

Two transactions read the same data, each verify a constraint, each write a different row, and both commit. No shared row, no lock contention, no version clash — and the constraint is violated by the combination. It is absent from the ANSI list, permitted by snapshot isolation, and it is the anomaly most likely to hurt a real application.

The level names mean different things in different engines

PostgreSQL’s REPEATABLE READ is snapshot isolation: it prevents phantoms, which ANSI does not require, and permits write skew. PostgreSQL’s READ UNCOMMITTED silently behaves as READ COMMITTED. Oracle’s SERIALIZABLE is snapshot isolation, so it permits write skew despite the name. None of that is derivable from general MVCC knowledge.

Checked against real databases

Every schedule here has been executed against the real engine in a container, and what it did — values read, waits, error codes, which transaction was aborted, the final table — is committed as a fixture the model is tested against. When the model and the database disagree, the model is wrong.

The failures this site can name

The formula beside each name is the standard shorthand for a schedule: r is a read, w is a write, c a commit; the number is which transaction did it, and the letter in brackets is which row. So w1[x] r2[x] reads “transaction 1 writes row x, then transaction 2 reads row x”.

  • Dirty writeP0

    w1[x] ... w2[x] ... ((c1 or a1) and (c2 or a2) in any order)

    not in the ANSI SQL-92 list

  • Dirty readP1

    w1[x] ... r2[x] ... (c1 or a1)

    in the ANSI SQL-92 list

  • Lost updateP4

    r1[x] ... w2[x] ... w1[x] ... c1

    not in the ANSI SQL-92 list

  • Non-repeatable readP2

    r1[x] ... w2[x] ... c2 ... r1[x]

    in the ANSI SQL-92 list

  • Phantom readP3

    r1[P] ... w2[y in P] ... c2 ... r1[P]

    in the ANSI SQL-92 list

  • Read skewA5A

    r1[x] ... w2[x] ... w2[y] ... c2 ... r1[y]

    not in the ANSI SQL-92 list

  • Write skewA5B

    r1[x] ... r2[y] ... w1[y] ... w2[x] ... (c1 and c2)

    not in the ANSI SQL-92 list