skip to content

Isolation Levels

The ANSI ladder from READ UNCOMMITTED to SERIALIZABLE, plus the snapshot-isolation level real engines actually ship, each defined by which anomalies it forbids. Interviewers walk this ladder constantly: I should name the default in common engines and know that identically named levels behave differently across them.

part ofRelational database conceptsoverview, primer and where to startread it →
on this pageshow

questions

21

What does the READ COMMITTED transaction isolation level guarantee, and which concurrency anomalies can still occur under it?

level: juniorimportance: must knowfreq 78%

answer

  1. No dirty reads — and nothing more
  2. Committed as of statement start, not transaction start
  3. Non-repeatable + phantom + read skew still legal
  4. Lost update is the application's problem
  5. Default level in most engines

basics

~20 s

It guarantees one thing: you never read data written by a transaction that has not committed — no dirty reads. Everything weaker still happens: non-repeatable reads, phantoms, read skew across statements, and lost updates in read-modify-write code.

solid answer

~50 s

READ COMMITTED forbids **dirty reads**: every value a statement returns was committed at the time that statement started. It also forbids dirty writes — you cannot overwrite another transaction's uncommitted row; your write blocks until that transaction ends. That is the whole guarantee. Within one transaction, reads are **not stable**: re-running the same query can return a changed value (non-repeatable read) or a different set of rows (phantom), because each statement sees a fresh view of committed data. Two queries against related tables can therefore observe mutually inconsistent states (read skew) — for example a transfer counted in the destination table but not yet in the source read. It also gives you no protection for read-modify-write logic in the application: two transactions can both read balance 100, both write 90, and one update is silently lost. So READ COMMITTED buys cheap, low-blocking reads, and leaves every multi-statement invariant to you — atomic single statements, explicit row locks, version columns, or a higher isolation level.

go deeper

for a junior

Be able to state the guarantee (no dirty reads) and name at least non-repeatable read and phantom read as still possible.

for a middle

Explain the per-statement view that produces the guarantee, and give a concrete read-skew or lost-update scenario from application code.

for a senior

Discuss the practical mitigations — single-statement writes, explicit row locks, version columns, declarative constraints — and when you would escalate the level instead.

for a principal

Frame it as the throughput/complexity default: cheap non-blocking reads with invariant enforcement pushed to schema constraints and targeted locking, escalating isolation only on the few paths that genuinely need it.

## What isolation levels actually specify When many transactions run concurrently, the engine may interleave their reads and writes in ways that produce results no serial (strictly one-at-a-time) execution could produce. The SQL standard does not describe *how* an engine prevents that; it describes which **anomalies** each named level is forbidden to exhibit. The classic anomalies are dirty read, non-repeatable read, and phantom read, on top of dirty write, which every level forbids. ## The one guarantee READ COMMITTED forbids exactly one anomaly: the **dirty read**. A dirty read is observing a row version written by a transaction that has not yet committed — data that may be rolled back and thus never have logically existed. Under READ COMMITTED, every value your statement returns was committed by somebody at the instant your statement began. It also forbids the **dirty write**: you cannot replace a row version that an uncommitted transaction has already modified. Your write blocks until the other transaction commits or rolls back. That rule is universal across levels, but it matters here because it is the only place READ COMMITTED makes you wait. ## How engines deliver it Two implementation families: - **MVCC engines** keep multiple versions of each row. Each *statement* takes a fresh snapshot of committed state when it starts, and reads the newest version that was committed before that instant. Readers never block writers and writers never block readers. - **Lock-based engines** take a shared read lock on each row as they read it and release it immediately after, instead of holding it to end-of-transaction. Holding it only for the read is enough to keep you off uncommitted data, and not enough to keep the data stable afterwards. Both give you "committed data as of now". Neither gives you "the same data as a moment ago". ## What is still legal, with concrete shapes - **Non-repeatable read** — you `SELECT price FROM item WHERE id=7` and get 100; another transaction updates it to 120 and commits; you re-run the same query in the same transaction and get 120. - **Phantom read** — you count orders `WHERE status='NEW'` and get 5; a concurrent insert commits; you count again and get 6. - **Read skew (inconsistent multi-statement view)** — you read account A (balance 100), a transfer of 50 from A to B commits, then you read account B. You see the money in B and also still in A, so your computed total is wrong. No single statement lied; the pair of statements straddled a commit. - **Lost update** — two transactions each read `qty = 10`, each compute `10 - 1` in application code, each write 9. One decrement vanishes. Nothing in READ COMMITTED forbids this, and in most engines nothing even reports an error. - **Write skew** — two transactions each check "at least one doctor is still on call", each see two, each take themselves off call. Both checks were true when made; the combined result violates the invariant. ## Why anyone chooses it Because it is cheap and it rarely blocks. Reads take no lasting locks and produce no serialization failures, so throughput is high and application code does not need retry loops for isolation conflicts. The exposure is real but bounded: any *single* statement is internally consistent, so a single aggregate query, a single multi-row `UPDATE ... WHERE`, or a single `INSERT ... SELECT` still sees one coherent state. This is why READ COMMITTED is the shipped default in most relational engines. ## Living with it The practical discipline is: **push each invariant into one statement, or lock explicitly, or move up a level.** - Express read-modify-write as a single statement (`SET qty = qty - 1 WHERE qty > 0`) so the engine, not your process, does the arithmetic on the current row. - When you must round-trip through the application, take an explicit row lock on read (`SELECT ... FOR UPDATE`) or use an optimistic version column and re-check the version in the `WHERE` of the update, retrying on zero rows affected. - Rely on declarative constraints — unique keys, foreign keys, check constraints — which are enforced by the engine at write time regardless of isolation level, and therefore catch races that your reads missed. - Escalate the isolation level only where a genuine multi-statement invariant exists, and accept the retry/blocking cost there rather than globally. ## The mental model to keep READ COMMITTED means *"nothing I read was a lie at the moment I read it."* It does not mean *"the world stood still while my transaction ran."* Every bug people blame on READ COMMITTED comes from assuming the second sentence.

  • If READ COMMITTED allows non-repeatable reads, is a single aggregate query such as SUM over a whole table still trustworthy?
    Yes, within itself. A single statement runs against one consistent view of committed data, so the SUM reflects a state the database really was in at that statement's start. What you cannot do is compare that SUM to a second query's result and assume both describe the same instant — the second statement gets its own view.
  • Does READ COMMITTED prevent lost updates?
    No. Two transactions can each read a value, compute a new one in application code, and write it back, with the second write silently overwriting the first; both writes were of committed data, so nothing in the level is violated. You prevent it with a single self-referencing UPDATE, an explicit SELECT ... FOR UPDATE, or an optimistic version check that fails the second writer.
  • How is READ COMMITTED different from just taking a snapshot at transaction start?
    A transaction-start snapshot would freeze the whole transaction's view, making reads repeatable and hiding all concurrent commits. READ COMMITTED deliberately re-snapshots per statement, so it sees newer commits as the transaction proceeds — that freshness is the point, and the instability is its direct cost.

It is like reading a live news site rather than a printed newspaper: nothing you read is a rumour, every headline was real when it was published — but refresh and the front page has changed, so two facts you noted a minute apart may never have been true at the same moment.

saying these in an interview costs you the question

  • Saying READ COMMITTED gives a consistent view for the whole transaction — the view is per statement
  • Claiming it prevents lost updates or write skew
  • Confusing 'no dirty reads' with 'no anomalies'
  • Assuming readers block writers or writers block readers under it in an MVCC engine
  • Believing two queries in the same READ COMMITTED transaction are guaranteed mutually consistent

context

open as a page

What is a dirty read, and what does the READ UNCOMMITTED transaction isolation level permit that stronger levels do not?

level: juniorimportance: must knowfreq 58%

basics

~20 s

A dirty read is seeing a row written by a transaction that has not committed — data that may be rolled back and never have existed. READ UNCOMMITTED is the only standard level that allows it; it is the weakest level and permits every other anomaly too.

open as a page

Inside one database transaction you run SELECT balance FROM accounts WHERE id = 7 twice and get two different values because another transaction committed a change in between. What is that anomaly called, and what does the REPEATABLE READ isolation level do about it?

level: juniorimportance: must knowfreq 58%

basics

~20 s

That is a non-repeatable read. REPEATABLE READ gives a transaction a stable view of rows it has read — a fixed snapshot, or read locks held until commit — so the second SELECT returns the same value as the first.

open as a page

Two concurrent transactions, both at the READ COMMITTED isolation level, each read an account's balance, compute a new value in application code, and write it back. Explain what goes wrong and what your options are to prevent it.

level: middleimportance: must knowfreq 62%

basics

~20 s

One update is silently lost: both read 100, both write their own result, the second overwrites the first and no error is raised. Fixes: do the arithmetic in one UPDATE statement, lock the row on read with SELECT ... FOR UPDATE, or use an optimistic version column and retry when zero rows change.

open as a page

Under the READ COMMITTED isolation level, two identical queries inside one transaction can return different results. Explain the visibility rule that causes this, and contrast it with taking one view for the whole transaction.

level: middleimportance: must knowfreq 68%

basics

~20 s

READ COMMITTED establishes a new view of committed data at the start of every statement, not once per transaction. So any transaction that commits between your two queries becomes visible to the second one. A transaction-wide view would hide those commits instead.

open as a page

The ANSI SQL standard defines REPEATABLE READ as permitting one specific anomaly. Which one is it, and why do many multiversion engines not actually exhibit it at that level?

level: middleimportance: must knowfreq 68%

basics

~20 s

ANSI REPEATABLE READ still permits phantoms: rows another transaction inserts or deletes can change the result of a repeated range query. Snapshot-based engines read everything from one fixed snapshot, so later inserts stay invisible and plain reads show no phantoms.

open as a page

What exactly does the SERIALIZABLE isolation level guarantee about a set of concurrent transactions, and why is the guarantee phrased as equivalent to some serial order rather than to the order in which they committed?

level: middleimportance: must knowfreq 66%

basics

~20 s

It guarantees the committed result is identical to running the transactions one at a time in some order, with no overlap. The engine may pick any such order, so it can run them concurrently and only has to produce an equivalent outcome, not a specific one.

open as a page

Code running at the SERIALIZABLE isolation level must expect transactions the database aborts with a serialization failure (SQLSTATE 40001). Describe the retry pattern you would implement around such transactions, and the mistakes that make a retry loop wrong.

level: middleimportance: must knowfreq 56%

basics

~20 s

Wrap the whole transaction in a loop: on a serialization failure, roll back and re-run everything including the reads, with a capped number of attempts and jittered backoff. Keep external side effects out of the retried block, and retry only serialization and deadlock errors.

open as a page

Snapshot isolation gives every transaction a consistent view of the database and never lets two concurrent transactions overwrite the same row, yet a workload run entirely under snapshot isolation can still end in a state that no serial order of those same transactions could produce. Explain how snapshot isolation works and why that gap exists.

level: middleimportance: must knowfreq 58%

basics

~20 s

Snapshot isolation reads a consistent snapshot fixed at transaction start and checks only write-write conflicts at commit. Two transactions that read overlapping data but write different rows never conflict, so read-write dependencies go unchecked. That gap allows write skew.

open as a page

At the READ COMMITTED isolation level, a statement such as UPDATE orders SET status='SHIPPED' WHERE status='PENDING' reaches a row that a concurrent, still-uncommitted transaction has already modified. Describe what your statement does while it waits and what happens after the other transaction commits or rolls back.

level: seniorimportance: must knowfreq 45%

basics

~20 s

Your statement blocks on that row's write lock. If the blocker rolls back, your original view stands and you update the row. If it commits, typical engines re-examine the newest committed version: the row is skipped if it was deleted or no longer matches your WHERE, and updated using that new version if it does.

open as a page

Two engines both advertise REPEATABLE READ: one implements it by holding shared locks on every row read until commit, the other by giving the transaction one fixed multiversion snapshot. How does observable behaviour differ — blocking, phantoms, and failure modes?

level: seniorimportance: must knowfreq 52%

basics

~20 s

Lock-based: readers block writers, phantoms still possible, failures appear as lock waits and deadlocks. Snapshot-based: reads never block, plain reads see no phantoms, but the view goes stale and conflicting writes fail with a serialization error the application must retry.

open as a page

A booking service checks under snapshot isolation that a meeting room has no overlapping reservation, then inserts one; occasionally two overlapping reservations both end up committed. Explain the anomaly and give the options for eliminating it, including ones that keep the service on snapshot isolation.

level: seniorimportance: must knowfreq 50%

basics

~20 s

That is write skew: each transaction verified the invariant on its own snapshot and then inserted a different row, so no write-write conflict was detected. Fix by switching to true serializability, locking or materializing the contended resource, or enforcing the rule with a database constraint.

open as a page

Some multi-version relational engines accept a request for the READ UNCOMMITTED isolation level but behave exactly as if READ COMMITTED had been requested. Why does that happen, and is it standard-conformant?

level: middleimportance: should knowfreq 40%

basics

~20 s

Multi-versioning already lets readers proceed without blocking on writers, so exposing uncommitted versions buys nothing and would require extra machinery. It is conformant: the standard says which anomalies a level must prevent, and preventing more than required is always allowed.

open as a page

Does running a transaction at the READ UNCOMMITTED isolation level change how its own INSERT, UPDATE and DELETE statements acquire locks, or let it overwrite a row another uncommitted transaction has already modified?

level: middleimportance: should knowfreq 32%

basics

~20 s

No to both. Isolation levels govern reads; write locks are unaffected. Your writes still take exclusive row locks held until commit, and you still block on rows an uncommitted transaction has modified. Dirty writes are forbidden at every level, including this one.

open as a page

Two concurrent transactions running under snapshot isolation both read the same inventory row and both issue an update to it. Describe exactly what the database does, what error the loser sees, and which classic anomaly this rule does and does not prevent.

level: middleimportance: should knowfreq 45%

basics

~20 s

Only one can commit. Under first-committer-wins the second commit is rejected; under first-updater-wins the second update blocks until the first ends and then aborts. The loser gets a serialization failure and must retry. This prevents lost update, not write skew.

open as a page

A transaction at REPEATABLE READ on a multiversion engine reads a row, computes with it, then updates it — and the engine aborts the transaction with a serialization failure (SQLSTATE 40001). Why is that abort necessary, and how should the application be written to cope?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Another transaction updated that row after your snapshot. Overwriting would lose their update; showing you their value would break read stability — so the engine aborts you. The fix is to catch the failure, roll back, and re-run the whole transaction, re-reading everything.

open as a page

SERIALIZABLE can be implemented by strict two-phase locking with predicate or next-key locks, or by Serializable Snapshot Isolation (SSI). Contrast how each achieves serial-equivalence, including how each stops a transaction from missing rows another transaction inserts, and what each costs at runtime.

level: seniorimportance: should knowfreq 46%

basics

~20 s

Locking prevents non-serializable schedules up front: locks held to commit, plus predicate or index-range locks so nobody can insert into a range you read — cost is blocking and deadlocks. SSI runs on snapshots, tracks read-write dependencies, and aborts a transaction when a dangerous pattern appears — cost is false-positive aborts.

open as a page

Most relational database engines ship READ COMMITTED as the default isolation level. Make the case for that default, and describe how you decide that a particular workload needs something stronger.

level: principalimportance: should knowfreq 40%

basics

~20 s

It is the cheapest level that removes the clearly unacceptable anomaly. Reads never block and never abort, version retention stays short, and most OLTP work is single-statement anyway. Escalate only where a real invariant spans statements and constraints or row locks cannot express it.

open as a page

You are deciding whether a high-throughput OLTP service should run all of its transactions at SERIALIZABLE. What throughput and abort-rate effects do you expect, and what alternatives would you weigh for the invariants that actually need protection?

level: principalimportance: should knowfreq 38%

basics

~20 s

Expect throughput to fall with contention, not uniformly: uncontended paths change little, hot rows and wide scans degrade sharply through waits or aborts, and abort rates rise superlinearly with transaction length. Weigh declarative constraints, single-statement updates, explicit locking and targeted use before adopting it globally.

open as a page

Serializable Snapshot Isolation keeps snapshot isolation's non-blocking reads but adds runtime detection of read-write anti-dependencies and aborts transactions that could produce a non-serializable result. Explain how that detection works, why it aborts transactions that would in fact have been fine, and how you would decide whether to run a high-throughput OLTP system on it.

level: principalimportance: should knowfreq 30%

basics

~20 s

SSI tracks which transactions read data that another later wrote (anti-dependencies) and aborts when a transaction has both an incoming and an outgoing such edge — the dangerous structure. Detection is conservative, so some aborts are false positives. Adopting it means budgeting for aborts, short transactions, and a universal retry path.

open as a page

On a lock-based database engine, when is reading at the READ UNCOMMITTED isolation level a defensible choice, and what failure modes beyond seeing rolled-back data does it expose?

level: seniorimportance: nice to knowfreq 30%

basics

~20 s

Defensible only for read-only, approximate, disposable results — rough counts, an operator peek during a long batch, progress indicators. Beyond rolled-back data, an unlocked scan can double-count or miss rows when concurrent writes move them, so even the approximation can be wrong in unbounded ways.

open as a page