skip to content

Concurrency Anomalies

The zoo of things that go wrong when transactions interleave: dirty reads, non-repeatable reads, phantoms, lost updates, and write skew. Interviewers love these because each anomaly is a concrete two-session story I should be able to tell on a whiteboard and map to the isolation level that stops it.

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

questions

22

What is a dirty read in a relational database, and what can go wrong for the transaction that performed one if the writing transaction later rolls back?

level: juniorimportance: must knowfreq 70%

answer

  1. Read of an uncommitted row version
  2. Writer rolls back → value never existed
  3. Reader's derived writes stay committed — one-way contamination
  4. Can also see half of a multi-row change
  5. Only at READ UNCOMMITTED

basics

~20 s

A dirty read is reading a row version written by a transaction that has not committed. If that writer rolls back, the value you read never existed in any committed state, so any decision, calculation or write you based on it is derived from data the database itself disowned.

solid answer

~50 s

A **dirty read** happens when transaction A reads data that transaction B has written but not yet committed. Two bad outcomes follow. If B **rolls back**, the value A saw never existed in any committed state of the database — A acted on a fact the database subsequently denied. If A then *wrote* something derived from it (an invoice total, an approval flag, a message published downstream), that garbage is now committed and durable, and no rollback of B can undo it. Even if B commits, A's read was still unsound in general: B may have been mid-way through a multi-row update, so A could see half of a transfer — the debit without the matching credit — and compute a total that violates an invariant nobody ever intended to exist. Dirty reads are permitted only at READ UNCOMMITTED; every stronger level forbids them, and multi-version engines never produce them because readers only ever see committed row versions.

code

text · 7 lines
text
T1: BEGIN
T1: UPDATE account SET balance = 0 WHERE id = 1;   -- uncommitted
T2: BEGIN (READ UNCOMMITTED)
T2: SELECT balance FROM account WHERE id = 1;      -- reads 0  <-- dirty
T2: INSERT INTO alerts(kind) VALUES ('ZERO_BALANCE');
T2: COMMIT                                          -- alert is now durable
T1: ROLLBACK                                        -- balance is 100 and always was

go deeper

for a junior

Define it as reading uncommitted data, and give the rollback consequence: the value never existed in any committed state.

for a middle

Add the partial-state hazard even when the writer commits, and name READ UNCOMMITTED as the only level that permits it.

for a senior

Emphasize that the reader's derived writes stay committed after the writer aborts, making the corruption one-directional and unrepairable, and treat any READ UNCOMMITTED usage as a review finding.

for a principal

Frame it as an architectural boundary: uncommitted data must never cross into durable state or into downstream systems, and set a policy that explicit dirty-read hints require justification tied to a non-authoritative consumer.

## The definition A transaction is a unit of work that is not visible to others until it commits. A **dirty read** is the violation of that rule: transaction A reads a row version produced by transaction B while B is still in flight. The data is 'dirty' because it is uncommitted — provisional, and legally revocable by B at any moment. Concretely: 1. B starts and updates `account.balance` from 100 to 0 as part of a withdrawal. 2. A reads `account.balance` and sees 0. 3. B hits an error and rolls back. The row is 100 again, and always was, as far as committed history is concerned. 4. A is now holding the number 0, which corresponds to no committed state the database ever had. ## Why it is harmful **Rollback makes the value fictional.** The most cited hazard: A acted on data that was never real. If A only displayed it, you have shown a user something wrong. If A branched on it — declined a purchase, skipped a top-up, triggered an alert — you have taken an irreversible action on fiction. **Contamination is permanent and one-directional.** A rollback of B undoes B's changes. It cannot undo anything A wrote. So if A read 0 and inserted an 'insufficient funds' record, or published an event, or wrote a derived total, that data is committed and durable while its input has evaporated. This is the reason dirty reads are considered a correctness bug rather than a staleness annoyance: the corruption outlives its cause. **Partial states violate invariants.** Even when B eventually commits, at the moment A reads, B may have applied only some of its writes. A transfer that debits one account and credits another is atomic to everyone else — but a dirty reader can observe the interval between the two writes and see money that has left one account and not yet arrived at the other. Any aggregate A computes in that window is wrong even though every individual value it read will eventually be committed. **It breaks recoverability in theory.** A schedule where one transaction reads another's uncommitted data is not *recoverable* unless the reader is forced to wait to commit until the writer commits. If the reader commits first and the writer then aborts, the database has committed work derived from an aborted transaction — a state no correct recovery procedure can repair. Systems that allow dirty reads therefore do not attempt this repair at all; they push the risk onto the application. ## When can it happen Only at the **READ UNCOMMITTED** isolation level. The SQL standard's table permits dirty reads there and forbids them at READ COMMITTED, REPEATABLE READ and SERIALIZABLE. So in the overwhelming majority of systems — where READ COMMITTED is the default — dirty reads simply do not occur. It is also worth knowing that the level is a *ceiling*: an engine may forbid dirty reads even when you ask for READ UNCOMMITTED, and multi-version engines do exactly that, because their readers are structurally incapable of seeing an uncommitted version. ## What a dirty read is not - It is **not** stale data. Reading a value that was committed but has since changed is normal and, at most, a non-repeatable read. - It is **not** a lost update. Losing a concurrent write is a different anomaly with a different cause. - It is **not** the same as reading your own uncommitted writes. Every isolation level lets a transaction see its own changes; that is required, not an anomaly. - It is **not** caused by missing indexes, replication lag, or caching — those produce staleness, not uncommitted visibility. ## How you avoid it Do nothing unusual: run at READ COMMITTED or above, which is the default nearly everywhere. The only way to encounter a dirty read is to explicitly request READ UNCOMMITTED on a lock-based engine, or to use an engine-specific hint that has the same effect. If you find such a hint in a codebase, treat it as a finding: ask what the query feeds, and whether anything downstream writes or decides based on the result. A dirty read is defensible for a rough, human-eyeballed progress count; it is indefensible anywhere the number is stored, compared against a threshold, or shown as authoritative.

  • If the writing transaction ends up committing anyway, was the dirty read harmless?
    Not necessarily. At the instant of the read the writer may have applied only part of its changes, so the reader can observe a state that violates an invariant the writer restores before commit — for example a debit without its matching credit. The reader's computation is wrong even though every value it saw was eventually committed.
  • Is reading a value that was committed a moment ago but has since been changed a dirty read?
    No. That is ordinary staleness, and if it happens twice within one transaction it is a non-repeatable read. A dirty read specifically requires that the version you observed had not been committed at the time you observed it, which is what makes rollback able to erase it.
  • Which isolation levels forbid dirty reads?
    READ COMMITTED, REPEATABLE READ and SERIALIZABLE all forbid them; only READ UNCOMMITTED permits them. Because the standard's levels are ceilings on permitted phenomena, an engine may also refuse to produce dirty reads at READ UNCOMMITTED, which is what multi-version implementations do.

It is like quoting a colleague's unsaved draft in your published article: they can hit undo, but your article is already printed with their retracted sentence in it.

saying these in an interview costs you the question

  • Calling any stale or out-of-date read a dirty read
  • Believing that rolling back the writer also cleans up whatever the reader wrote
  • Thinking a dirty read is harmless as long as the writer eventually commits
  • Confusing it with reading your own uncommitted changes, which every level allows
  • Assuming dirty reads can occur at READ COMMITTED under heavy load

context

open as a page

What is the lost update anomaly in a database, and what sequence of operations produces it?

level: juniorimportance: must knowfreq 62%

basics

~20 s

Two transactions read the same row, each computes a new value from what it read, then both write. The second write overwrites the first, so one committed change silently disappears. No error is raised — only the final value is wrong.

open as a page

What is a non-repeatable read, and what sequence of events inside a transaction produces one?

level: juniorimportance: must knowfreq 66%

basics

~20 s

A transaction reads a row, another transaction updates that row and commits, and the first transaction reads the same row again and sees a different value. The re-read is not repeatable — the data changed underneath a transaction that is still running.

open as a page

What is a phantom read in a database transaction, and how is it different from a non-repeatable read?

level: juniorimportance: must knowfreq 68%

basics

~20 s

A phantom read is when a transaction re-runs the same search condition and the set of matching rows changes, because another transaction committed inserts or deletes. A non-repeatable read is when a row you already read comes back with different values.

open as a page

Why does an in-place update such as `UPDATE accounts SET balance = balance - 10 WHERE id = 42` avoid a lost update, while reading the balance into application code and writing back the computed number does not?

level: middleimportance: must knowfreq 66%

basics

~20 s

The in-place form reads and writes inside one statement while holding the row's write lock, so no other transaction can slip in between. The application version leaves a gap between the read and the write in which another transaction can commit a change you never see.

open as a page

In a multiversion engine, what is the difference between taking a read snapshot once per statement and taking one per transaction, and how does that map to READ COMMITTED versus REPEATABLE READ?

level: middleimportance: must knowfreq 58%

basics

~20 s

READ COMMITTED takes a new snapshot at the start of every statement, so each query sees the newest committed data and a re-read can change. REPEATABLE READ takes one snapshot at the transaction's first statement and answers every read from it, so re-reads are stable but possibly stale.

open as a page

Your transaction takes a lock on every row a range query returns, yet re-running that same range query still returns extra rows. Explain why row-level locking cannot prevent phantoms, and what locking mechanisms do.

level: middleimportance: must knowfreq 52%

basics

~20 s

Row locks only cover rows that exist; a phantom is a row inserted into the gap between them. Prevention needs predicate locking, approximated in practice by index range locks — gap locks and next-key locks that lock the empty key space too.

open as a page

What is write skew in a database, and how can two transactions that never write the same row still leave the data violating a business rule?

level: middleimportance: must knowfreq 44%

basics

~20 s

Write skew: two transactions read an overlapping set of rows, each decides its write is safe based on that read, then each writes a different row. Individually valid, together they break an invariant that spans the rows — for example both on-call doctors going off call.

open as a page

Explain how an optimistic version column detects a lost update: what the UPDATE statement must look like, how a conflict is discovered, and what the application does next.

level: seniorimportance: must knowfreq 58%

basics

~20 s

Store a version number on the row. Read it with the data, then write with ... SET data = ?, version = version + 1 WHERE id = ? AND version = <the value you read>. If zero rows are affected, someone changed the row first — reload, reapply, and retry or tell the user.

open as a page

An application must guarantee an invariant that spans several rows — for example that at least one doctor stays on call for each shift. What are your options for making concurrent transactions respect that invariant, and what does each one cost?

level: seniorimportance: must knowfreq 38%

basics

~20 s

Four options: run SERIALIZABLE and retry on serialization failures; take locking reads on the rows the decision depends on; materialize the conflict as a single row every transaction writes; or express the rule as a database constraint. All work by making the reads that justify the write part of the conflict footprint.

open as a page

Why can a multi-version relational engine never expose a dirty read to a reader, even when the session explicitly requests the READ UNCOMMITTED isolation level?

level: middleimportance: should knowfreq 45%

basics

~20 s

Multi-version engines serve reads from committed row versions chosen by a visibility rule keyed on committed transaction state. An uncommitted version is simply not visible to anyone but its writer, so there is no mechanism to return one — READ UNCOMMITTED is accepted and behaves as READ COMMITTED.

open as a page

How does taking an explicit exclusive row lock when reading — `SELECT ... FOR UPDATE` — prevent a lost update, and what does that approach cost?

level: middleimportance: should knowfreq 55%

basics

~20 s

Reading with FOR UPDATE locks the rows exclusively for the rest of the transaction, so a second transaction wanting the same rows blocks until you commit. Your read-modify-write is serialized. The cost is blocking, deadlocks, and reduced throughput on hot rows.

open as a page

A transaction is guaranteed that any row it re-reads still shows the values it saw earlier. Why is that guarantee still not enough for a query that aggregates a range of rows, and what stronger guarantee would be needed?

level: middleimportance: should knowfreq 52%

basics

~20 s

The guarantee covers rows that already exist and that you already read. It says nothing about rows another transaction inserts into, or deletes from, the range — so a repeated range query can return a different set of rows even though every individual row is stable.

open as a page

Which transaction isolation levels actually prevent a lost update, and how do lock-based and snapshot-based engines differ in the way they stop one?

level: seniorimportance: should knowfreq 45%

basics

~20 s

READ UNCOMMITTED and READ COMMITTED do not prevent it. Lock-based REPEATABLE READ prevents it by holding read locks to commit, turning the race into a block or deadlock. Snapshot-based engines detect the concurrent write to the same row and abort one transaction with a serialization error. SERIALIZABLE always prevents it.

open as a page

How do a lock-based engine and a multiversion engine each stop a transaction from seeing a different value when it re-reads a row, and what does each approach cost in production?

level: seniorimportance: should knowfreq 46%

basics

~20 s

A lock-based engine holds the shared read lock until commit, so writers cannot change what you read — cost: blocking, deadlocks, lower concurrency. A multiversion engine answers all reads from one transaction-wide snapshot — cost: stale data, retained old row versions, and write conflicts that abort and must be retried.

open as a page

A nightly report runs a dozen separate SELECT statements inside one transaction at READ COMMITTED, and its section subtotals do not add up to its grand total. What is the likely cause, and what are your options for fixing it?

level: seniorimportance: should knowfreq 44%

basics

~20 s

At READ COMMITTED each statement takes a fresh view of the data, so every query in the report sees a different instant while writers keep committing. Fix it by giving the whole report one consistent view — a transaction-wide snapshot, one combined statement, or a point-in-time copy such as a replica or extract.

open as a page

The SQL standard lists phantom reads as permitted at REPEATABLE READ, yet on several multi-version (MVCC) engines a repeated range query inside a REPEATABLE READ transaction never shows newly committed rows. Explain the discrepancy and what it does and does not guarantee.

level: seniorimportance: should knowfreq 42%

basics

~20 s

The standard's levels were defined by which anomalies a lock-based implementation exhibits. MVCC engines serve a transaction's reads from one snapshot, so re-reads are stable and read phantoms never appear — but that only stabilizes what you see, it does not make writes based on that view safe.

open as a page

Snapshot isolation detects and rejects two transactions that write the same row after both read it. Explain why that same mechanism does not stop two transactions whose reads overlap but whose writes touch different rows.

level: seniorimportance: should knowfreq 36%

basics

~20 s

Snapshot isolation's conflict detector compares write sets. Two transactions writing different rows have an empty write-set intersection, so there is nothing to reject — even though each transaction's decision depended on rows the other changed. Reads are never part of the conflict footprint.

open as a page

You are designing the write path for a record that many users and jobs modify concurrently. How do you choose between an atomic in-place update, pessimistic row locking, and an optimistic version check — and what changes your answer as contention rises?

level: principalimportance: should knowfreq 40%

basics

~20 s

Match the tool to the write's shape: commutative arithmetic on one row goes in-place; a short server-side critical section with cross-row logic takes a row lock; a long or stateless edit uses a version check. As contention rises, optimistic retries waste work, so move toward locks, in-place arithmetic, or eliminate the hot row entirely.

open as a page

A team wants to run a large reporting query with reads of uncommitted data so it stops waiting behind write transactions. How do you evaluate that request, and what would you propose instead?

level: seniorimportance: nice to knowfreq 28%

basics

~20 s

Ask what consumes the result. Uncommitted reads are tolerable only for rough, human-eyeballed numbers that are never stored, compared to a threshold, or sent downstream. Otherwise propose a multi-version snapshot read, a read replica, or a precomputed aggregate instead of weakening correctness.

open as a page

A hot table takes thousands of writes per second, and one code path must be able to trust that no other row matches its search condition before it inserts. Compare the ways to eliminate the phantom in that check — SERIALIZABLE with retries, explicit range locking, and pushing the rule into an index constraint — and explain how you would choose.

level: principalimportance: nice to knowfreq 28%

basics

~20 s

Three options: SERIALIZABLE (no blocking, but aborts you must retry, and abort rate grows with contention), range/next-key locking (deterministic but serializes the hot range and invites deadlocks), or a unique/exclusion constraint that makes the index enforce the rule with no extra locking. Prefer the constraint when the rule fits an index.

open as a page

You have moved a contended workload to a serializable isolation level implemented with optimistic conflict detection, and production now shows a steady rate of serialization failures. How do you operate and tune such a system?

level: principalimportance: nice to knowfreq 24%

basics

~20 s

Treat aborts as normal: bounded retries with jittered backoff, idempotent transactions, abort rate as a first-class metric. Reduce aborts by shortening transactions, narrowing what they read, partitioning hot key space, and marking read-only transactions as such. Escalate individual hot paths to locking or constraints.

open as a page