SQL Isolation Levels Lab

Lab
Easy
Medium
Hard

Non-repeatable readEasy

B reads the same row twice and gets two different answers. Rule: Within one transaction, reading the same row twice should give the same value.

Isolation level

READ COMMITTED

Both sessions use this level. Switch it and replay to see which anomaly appears or disappears.

Which anomalies can happen?

AnomalyRURCRRSER
Dirty read✗✓✓✓
Non-repeatable read✗✗✓✓
Phantom read✗✗✓✓
Lost update✗✗✓✓
Write skew✗✗✗✓

✓ prevented · ✗ possible (PostgreSQL / InnoDB behavior; the SQL standard alone allows phantoms at RR).

1 / 8
Setup
Two sessions (A and B) will run these statements at the same moment, both under READ COMMITTED.

Two sessions, one database

time flows downward
Committed data · table “accounts”
idnamebalance
1Alice100
2Bob100
Uncommitted in A
nothing
Uncommitted in B
nothing
Session A
Session B
BEGIN;
SELECT balance FROM accounts WHERE id = 1;
BEGIN;
UPDATE accounts SET balance = 50 WHERE id = 1;
COMMIT;
SELECT balance FROM accounts WHERE id = 1; -- again
COMMIT;
✗ Non-repeatable read HAPPENED under READ COMMITTED

The second read in B returned something different from the first.

Try a stricter level above to see how the database stops it.

Learn the theory

SQL Isolation Levels Lab: how it works

5 min read · Updated

Databases serve many clients at once, and those clients' transactions overlap in time. Isolation — the “I” in ACID — defines how much one transaction is allowed to see of another transaction's in-flight work. Stronger isolation means fewer surprising results but more waiting or more aborted transactions, so every database lets you pick a level. This lab runs two real interleavings side by side so you can watch each anomaly happen, and watch the right level prevent it.

In short

  • Isolation is a trade-off: stricter levels prevent more anomalies but cost concurrency (locks, retries).
  • The classic anomalies are dirty read, non-repeatable read, phantom read, lost update and write skew.
  • READ COMMITTED is the default in PostgreSQL, Oracle and SQL Server; REPEATABLE READ is MySQL InnoDB's default.
  • Snapshot-based REPEATABLE READ still allows write skew; only SERIALIZABLE stops it — by aborting a transaction.
  • With stricter levels, your application must be ready to retry failed transactions.

What isolation means

A transaction is a group of statements that should behave as one unit. Atomicity makes it all-or-nothing and durability makes commits permanent. Isolation answers a different question: while my transaction is running, what can I see of everyone else's?

The ideal is serializability: the result is the same as if the transactions had run one after another. That is the safest model, but enforcing it for every workload is expensive, so databases offer weaker levels that allow specific, well-defined anomalies in exchange for speed.

The anomalies

AnomalyWhat happensExample
Dirty readYou read data another transaction hasn't committed (and may roll back).A sets balance = 0 but rolls back; B already saw 0.
Non-repeatable readReading the same row twice in one transaction gives different values.B reads balance = 100; A commits 50; B reads 50.
Phantom readRe-running a range query returns a different set of rows.B counts accounts ≥ 100 → 2; A inserts one; B counts → 3.
Lost updateTwo read-modify-write cycles overwrite each other.Both read 100; A writes 70, B writes 50; A's change vanishes.
Write skewTwo transactions read overlapping data, then write different rows, breaking a rule neither alone would break.Two on-call doctors each check “someone else is on call” and both leave.

The four standard levels

LevelDirty readNon-repeatablePhantom*Lost updateWrite skew
READ UNCOMMITTEDpossiblepossiblepossiblepossiblepossible
READ COMMITTEDpreventedpossiblepossiblepossiblepossible
REPEATABLE READpreventedpreventedprevented†prevented†possible
SERIALIZABLEpreventedpreventedpreventedpreventedprevented

*The SQL standard permits phantoms at REPEATABLE READ. †PostgreSQL and MySQL InnoDB use snapshots, which hide phantoms from plain SELECTs and (in PostgreSQL) abort conflicting updates. Always check your engine's documentation: level names are shared, behavior isn't.

How databases implement it

There are two main strategies, often mixed:

  • Locking. Readers take shared locks and writers take exclusive locks; conflicting transactions wait. Simple to reason about, but readers and writers block each other.
  • MVCC (multi-version concurrency control). Every write creates a new version of the row. Each transaction reads the version that was current at its snapshot. Readers never block writers. PostgreSQL, Oracle and InnoDB work this way.
EngineDefault levelNotes
PostgreSQLREAD COMMITTEDREAD UNCOMMITTED behaves as READ COMMITTED. SERIALIZABLE uses Serializable Snapshot Isolation (SSI).
MySQL (InnoDB)REPEATABLE READUses MVCC plus next-key locks, which also block many phantoms.
SQL ServerREAD COMMITTEDOptional row-versioning (RCSI) and SNAPSHOT isolation.
OracleREAD COMMITTEDNo true READ UNCOMMITTED; SERIALIZABLE is snapshot-based.

Write skew and SERIALIZABLE

Write skew is the sneaky one. Each transaction sees a consistent snapshot, writes a different row, and no row-level conflict is detected — yet together they break an invariant (“at least one doctor on call”). REPEATABLE READ cannot see the problem because the two writes never touch the same row.

SERIALIZABLE closes the gap. PostgreSQL tracks read/write dependencies between concurrent transactions and aborts one when no serial order could explain the outcome. You'll see an error such as could not serialize access due to read/write dependencies (SQLSTATE 40001). That is not a bug: it is the database protecting your data and asking you to retry.

Choosing a level in practice

  • Start with your database's default (READ COMMITTED or REPEATABLE READ) and think about which invariants span multiple rows.
  • For read-modify-write on a single row, lock it explicitly with SELECT … FOR UPDATE, or do the arithmetic in SQL (SET balance = balance - 30).
  • For cross-row rules (like on-call), use SERIALIZABLE, an explicit lock on a parent row, or a constraint the database can enforce.
  • Whatever you choose, wrap transactions in a retry loop for serialization failures and deadlocks.
BEGIN ISOLATION LEVEL SERIALIZABLE;

SELECT COUNT(*) FROM doctors WHERE on_call = 1;   -- must stay >= 1
UPDATE doctors SET on_call = 0 WHERE id = 2;

COMMIT;  -- may fail with SQLSTATE 40001: retry the whole transaction

Frequently asked questions

What are the four SQL isolation levels?
READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ and SERIALIZABLE, ordered from weakest to strongest. Each prevents more anomalies than the one before it.
What is a dirty read?
Reading data written by a transaction that hasn't committed yet. If that transaction rolls back, you acted on data that never officially existed. Only READ UNCOMMITTED allows it.
What is the difference between a non-repeatable read and a phantom read?
A non-repeatable read means an existing row you read earlier now has a different value. A phantom read means the set of rows matching your query changed because rows were inserted or deleted.
What is write skew?
An anomaly where two concurrent transactions read overlapping data and then update different rows, violating an invariant that each transaction alone would have preserved. Snapshot isolation allows it; SERIALIZABLE prevents it.
Is REPEATABLE READ enough to prevent lost updates?
In PostgreSQL, yes: a transaction that tries to update a row changed after its snapshot is aborted. In MySQL InnoDB, plain updates read the latest committed row, so use SELECT … FOR UPDATE or atomic updates.
Why does SERIALIZABLE throw errors?
To guarantee serializable outcomes without locking everything, the database lets transactions run concurrently and aborts one if their combined effect couldn't have happened in any serial order. Your code should retry the transaction.

Keep exploring

More interactive labs

All labs

HTTP Caching Explained

Cache-Control, ETag and 304s, one request at a time.

Networking12 min · Intermediate

JS Event Loop Visualizer

See the call stack, microtasks and timers run, frame by frame.

JavaScript10 min · Intermediate

CORS & Preflight Simulator

Edit the request and the server headers, then see exactly why the browser blocks it.

Networking12 min · Intermediate

What Happens When You Type a URL

DNS, TCP, TLS and HTTP — with a stopwatch.

Networking10 min · Beginner

How Database Indexes Work (B-Tree)

See a B-tree lookup beat a full table scan, and why some queries ignore your index.

Databases12 min · Intermediate

What Happens When 100 Reads Hit the Database

Connection pools, CPU workers, caches and replicas, as 100 requests arrive at once.

Databases12 min · Intermediate

Sharding & Replication

Split data across servers, copy it for reads, and see what each choice costs.

Databases15 min · Advanced

Redis Commands & Data Structures

Strings, lists, hashes, sets and sorted sets, one command at a time.

Redis15 min · Beginner

Redis Caching: Cache-Aside, TTL & Eviction

Hit or miss? Watch LRU, LFU, TTLs and a cache stampede decide your database load.

Redis12 min · Intermediate

Webhooks: Delivery, Retries & Signatures

Why you get duplicate deliveries, lost events and forged requests — and how to stop them.

Queues & Jobs12 min · Intermediate

Cron Jobs: Schedules, Overlaps & Missed Runs

Write a cron expression and watch what really happens when jobs overlap or the scheduler is down.

Queues & Jobs10 min · Beginner

Job Queues with BullMQ (Redis)

Watch jobs move through waiting, active, delayed, completed and failed — and see what BullMQ stores in Redis.

Queues & Jobs14 min · Intermediate

RabbitMQ: Exchanges, Queues, Acks & Dead Letters

Direct, topic and fanout routing, competing consumers, prefetch, redelivery and dead-letter queues, drawn.

Queues & Jobs14 min · Intermediate

Git Commands, HEAD & Refs (Visual)

Every core git command drawn with arrows: three areas, commits, branches, HEAD and remotes.

Git15 min · Beginner

Git Merge vs Rebase

Watch the commit graph change under merge, rebase and squash.

Git10 min · Beginner