Dirty reads · Aleksei Zhynguel
Interactive noteTransaction isolation · part 2 of 7Sep 2026 · 13 minPostgres 16

Dirty reads

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.

{{ outShort }}
{{ aName }} t {{ bName }}
{{ e.seq }}
{{ e.t }}{{ e.s }}
Database · committed
{{ r.name }}{{ r.val }}
{{ v.t }}{{ v.s }}
{{ pnote }}
{{ outLabel }} {{ outVal }}
{{ warmFrom }} {{ warmQ }}

{{ warmFb }}

01 · The setup

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.

02 · B writes, but doesn’t commit

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.

03 · A reads
Predict first · the diagram shows ? {{ aq2 }}
The explanation opens when you answer.

{{ 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.

04 · B rolls back

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.

05 · READ COMMITTED
Predict first · the diagram shows ? {{ aq4 }}
The explanation opens when you answer.

{{ 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.

06 · What it doesn’t promise

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.

Deeper

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.

07 · You can’t turn it off

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.

08 · Two versions, side by side

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.

09 · Rollback doesn’t undo anything

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.

10 · Dirty writes

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.

11 · The price of a fresh view

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.

Connections

Every idea in the series, and every step where it appears. To review, pick a term, jump to a step, and come back.

{{ kCount }}
{{ ocTerm }}

{{ ocDef }}

Related
  1. {{ r.where }}{{ r.title }}
The whole series in one table
AnomalyRead committedRepeatable readSerializableWhat stops it
Dirty readthis notepreventedpreventedpreventedUncommitted versions are invisible · part 2, step 8
Read skewallowedpreventedpreventedOne snapshot per transaction · part 3, step 7
Lost update (read, then write)allowedone fails · 40001one fails · 40001Refusing to overwrite a newer row · part 4, step 7
Write skewallowedallowedone fails · 40001Tracking what was read (SSI) · part 5, step 6
Phantom readallowedpreventedpreventedOne snapshot per transaction · part 6, step 5
Write skew on new rowsallowedallowedone fails · 40001Predicate locks · part 6, step 9
Postgres 16 behavior, as described in chapter 13.2 of its documentation. The SQL standard allows more than this at each level.
Check yourself{{ score }}
{{ q.n }}

{{ q.q }}

{{ q.fb }}

{{ q.model }}

Three things to keep
COMMITTEDREAD COMMITTED means you only see committed data. It says nothing about when it was committed.
FREEIn Postgres, avoiding dirty reads costs nothing: the old version is still there to read.
ROLLBACKA rollback is one entry in the commit log. The dead versions wait for vacuum.
← What isolation is for Found a mistake? Tell me and I'll fix it. Next: Understanding read skew →