Skip to main content
Database Isolation Levelslesson 1 of 4 · 2 min read

The Anomalies and the Ladder

The anomalies, named

Isolation levels are defined by which anomalies they allow, so start with the anomalies themselves.

Start with the dirty read: reading what another transaction wrote before it committed. If that transaction rolls back, you acted on data which officially never existed.

A non-repeatable read means reading the same row twice inside one transaction and getting different values, because somebody committed an update in between. A phantom read means running the same query twice and getting different rows, because somebody inserted or deleted matching ones.

Add the lost update yourself, because the standard leaves it out and you will meet it most. Two transactions read a value, both compute a new one, and the second write silently erases the first.

Climb the ladder one rung at a time, because each closes one more anomaly. Read Uncommitted allows dirty reads and almost nobody uses it. Read Committed promises you only ever see committed data. Repeatable Read promises that rows you have read stay stable for as long as your transaction runs. Serializable promises the whole interleaving matches some one-at-a-time ordering.

Expect every step up to cost something: more locking, more version tracking, or more of your transactions aborted and retried. That is why nobody runs Serializable everywhere by reflex.

Carry two facts into the rest of this chapter. Defaults differ. Postgres, a relational database, and Oracle start you at Read Committed, while MySQL starts you at Repeatable Read. The same code ships different bugs depending on where it runs.

And vendors implement the same names differently, because the standard was written with locking in mind and modern engines use snapshots. The name on the box tells you less than the anomaly list in the manual.

the shape of it
Read Uncommitteddirty reads possibleRead CommittedPostgres defaultRepeatable ReadMySQL defaultSerializableno anomaliesno dirty readsstable rowsno phantoms
step 1 of 3
Each rung of the ladder closes one more anomaly and charges more concurrency for it.

Worked example

Kenji migrates a Django app from MySQL to Postgres and a subtle bug appears in a nightly reconciliation job. The job opens a transaction, reads an account row, does 30 seconds of processing, then reads the row again expecting the same value. On MySQL, whose InnoDB default is Repeatable Read, the second read always matched the first, and the code quietly relied on that. On Postgres, defaulting to Read Committed, a customer payment landing mid-job changes the second read, and the reconciliation report shows a 240 dollar discrepancy that nobody can reproduce in the morning. Nothing in the code changed; the ladder rung underneath it did. One line fixes it: SET TRANSACTION ISOLATION LEVEL REPEATABLE READ at the top of the job.