The four anomalies, and what each level costs
Dirty read, non-repeatable read, phantom, and write skew. Postgres never permits the first, permits the next two at READ COMMITTED, and refuses all four at SERIALIZABLE.
16 min · runs against the shop dataset
The isolation levels are defined by which anomalies they allow. Every one of these can be produced by hand in the isolation lab, in two sessions, in under a minute — and the difference between reading about a phantom and making one happen is most of the learning.
The dirty read, which cannot happen
Reading a row another transaction has written and not committed. The standard permits it at READ UNCOMMITTED; Postgres never produces one, because MVCC has no mechanism for showing one transaction's unfinished work to another. Ask for READ UNCOMMITTED and you quietly get READ COMMITTED — which is what this engine does too, and the lab's first scenario is exactly this.
The non-repeatable read
The same query, twice, in one transaction, giving two different answers — because another transaction committed a change in between. Permitted at READ COMMITTED, where every statement takes a fresh snapshot. Prevented at REPEATABLE READ, where the whole transaction shares the one it started with.
The phantom
The same range query returning a different set of rows, because another transaction inserted one that matches. Prevented at REPEATABLE READ in Postgres — the standard only requires SERIALIZABLE to prevent it, and Postgres's snapshot happens to be strong enough that it does so anyway.
Write skew, and why it needs SERIALIZABLE
Two transactions each read a set of rows, each check a condition that still holds, and each write a different row. Neither one wrote what the other read, so no row-level conflict is detected — and the two results together violate a rule that each of them individually preserved. Two doctors both going off call because the other is still on.
REPEATABLE READ permits this. Only SERIALIZABLE refuses it, and it does so by tracking read-write dependencies between transactions and aborting one when they form a cycle. The cost is that some perfectly fine transactions get aborted too, which is why every serializable writer needs a retry loop.