One account with 50 in it. Transaction B is charging the customer’s card 40. Transaction A is deciding whether the same customer can pay for an order of 30. Nothing has run yet.
The first thing every isolation level stops. Postgres stops it so thoroughly that you can’t turn it off, and the reason why says a lot about how it stores data.
Scroll to advance. Steps 1–6 are the idea, steps 7–11 are how Postgres does it. You can switch the isolation level at any step.
{{ warmFb }}
One account with 50 in it. Transaction B is charging the customer’s card 40. Transaction A is deciding whether the same customer can pay for an order of 30. Nothing has run yet.
B subtracts 40. The balance is now 10, but only inside B. B hasn’t committed, because it’s still waiting to hear back from the card network, and that can take a few seconds.
{{ fb2 }}
A reads the balance. For a moment, picture a database with no isolation, where a read returns whatever is newest on disk, finished or not. A gets 10, which is less than 30, so it declines the order.
The card network says no, so B rolls back. The balance was never really 10. A turned a customer away because of a number that, as far as the database is concerned, never existed.
That’s a dirty read: reading data that another transaction hasn’t committed yet, and might never commit.
{{ fb4 }}
READ COMMITTED has one rule: you only ever see data that has been committed. B’s 10 wasn’t, so A reads 50 and approves the order.
Switch between the two levels above to compare.
Committed doesn’t mean consistent. Each statement sees what was committed when that statement began. Two statements in the same transaction can see two different states of the database.
That gap is where the next note starts.
You can stop here and you’ll know what a dirty read is. The next five steps are why Postgres can’t produce one, even if you ask it to.
The NO ISOLATION button above is imaginary. Postgres accepts SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED and then quietly runs READ COMMITTED. The SQL standard allows this, because a level is allowed to be stricter than requested.
For Postgres, preventing dirty reads costs nothing extra. The reason is in how an update is stored.
B’s update didn’t change the row. It added a second version with xmin 102, and stamped the first one with xmax 102.
When A reads, it checks 102 against its snapshot. 102 is still running, so the new version doesn’t count yet and the old one hasn’t been replaced. A gets 50 without waiting for anyone and without anyone waiting for A.
When B rolls back, Postgres doesn’t go back and repair the row. It changes one entry in its commit log, pg_xact, from in progress to aborted. From then on every transaction treats 102’s versions as if they had never been written.
The dead version stays on disk until vacuum removes it. That’s why a rollback in Postgres is fast however much the transaction wrote.
There’s a worse cousin: overwriting data someone else hasn’t committed. If A had tried to update the balance while B’s change was pending, A would have waited on B’s row lock until B finished.
No level in Postgres allows dirty writes. Without that rule, a rollback wouldn’t even know which value to go back to.
READ COMMITTED takes a new snapshot for every statement. That’s cheap, and a long transaction always sees recent data. It also means the transaction as a whole doesn’t belong to any single point in time.
Part 3 shows what goes wrong when a transaction adds up two reads.
Every idea in the series, and every step where it appears. To review, pick a term, jump to a step, and come back.
| Anomaly | Read committed | Repeatable read | Serializable | What stops it |
|---|---|---|---|---|
| Dirty readthis note | prevented | prevented | prevented | Uncommitted versions are invisible · part 2, step 8 |
| Read skew | allowed | prevented | prevented | One snapshot per transaction · part 3, step 7 |
| Lost update (read, then write) | allowed | one fails · 40001 | one fails · 40001 | Refusing to overwrite a newer row · part 4, step 7 |
| Write skew | allowed | allowed | one fails · 40001 | Tracking what was read (SSI) · part 5, step 6 |
| Phantom read | allowed | prevented | prevented | One snapshot per transaction · part 6, step 5 |
| Write skew on new rows | allowed | allowed | one fails · 40001 | Predicate locks · part 6, step 9 |
{{ q.q }}
{{ q.fb }}
{{ q.model }}