skip to content

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%

answer

  1. Read-only, approximate, disposable — all three
  2. Not just rollback: missed and double-counted rows
  3. Unlocked scan has no stable position
  4. Torn multi-row states, no error raised
  5. Prefer replica, catalog estimate, or better index

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.

solid answer

~60 s

The narrow legitimate case: a **read-only, approximate, disposable** query on a lock-based engine where taking shared read locks would either block writers for the duration of a scan or make the query wait behind a long write transaction. Rough row counts for capacity planning, an operator inspecting a table mid-batch, a progress indicator — results nobody stores, compares, bills, or displays as authoritative. The hazards go beyond dirty reads: - **Rolled-back data counted as real** — the obvious one. - **Torn multi-row states** — half a transfer, an order without its lines. - **Rows read twice or missed entirely.** Without a read lock, a scan holds no stable position against concurrent structural change: a row updated so that it relocates within an index can be visited before and after the move, or moved past the scan's cursor and never seen. So the error is not bounded by in-flight write volume. - **Silence.** No error, no warning, no flag on the result. So the rule is: use it where being wrong is free and being wrong is expected. Anything else wants a replica, a snapshot-based read path, or an approximation the engine maintains itself.

go deeper

for a junior

Recognise it as approximate-only and unsafe for anything that matters; do not attempt to justify it in production paths.

for a middle

Name the read-only, approximate, disposable criteria and explain that it exists to avoid shared read locks on lock-based engines.

for a senior

Add the unbounded double-count and missed-row failure mode from unlocked scans, torn multi-row states, and the alternatives you would try first.

for a principal

Frame it as an architecture smell — analytics competing with OLTP on one instance — and drive toward replicas, engine-maintained estimates and access-path work rather than trading correctness for latency.

## Why the question is engine-family specific The level only *does* anything where isolation is implemented with read locks. In a lock-based engine, a `SELECT` at a stronger level takes shared locks on the rows or pages it reads. Two costs follow: the reader can wait behind an exclusive lock held by a long write transaction, and the reader's own shared locks can block writers for the duration of a large scan. READ UNCOMMITTED removes both by taking no shared read locks at all. In a multi-version engine neither cost exists — reads are already served from committed versions without waiting — so the level buys nothing there and the discussion below simply does not apply. ## The narrow zone where it is defensible Every legitimate use shares three properties. The query is: 1. **Read-only.** Its result never becomes an input to a write, directly or indirectly. 2. **Approximate by design.** The consumer's contract already says "about this many", and a wrong answer produces no wrong decision. 3. **Disposable.** The number is not persisted, reconciled against another number, billed, or shown as authoritative. Examples that pass all three: a rough row-count or size estimate for capacity planning; an operator inspecting a table in the middle of a hours-long batch load to see whether it is progressing; a progress or heartbeat indicator on an ETL job; a quick diagnostic peek at what a stuck job has written so far. Examples that fail: any figure shown to a customer; anything compared against another query's result; anything feeding a decision, a threshold, an alert people act on, or a downstream write. "It's only a dashboard" fails as soon as someone makes a decision from the dashboard, which is what dashboards are for. ## The failure mode people forget Most candidates stop at "you might read data that gets rolled back" and conclude the error is bounded by how much uncommitted work is in flight. It is not. A scan without read locks holds **no stable position** with respect to concurrent structural change. Consider a scan walking an index in key order. A concurrent transaction updates a row so that its indexed key changes: - If the row moves from a location the scan has **not yet reached** to one it has **already passed**, the scan never sees it — a **missed row**. - If the row moves from a location the scan has **already passed** to one it has **not yet reached**, the scan sees it a second time — a **double-counted row**. The same class of problem arises around page splits and row relocations during maintenance operations. Nothing about this is proportional to the amount of uncommitted data; it is proportional to concurrent write and reorganisation activity. A count taken this way can be off in either direction by an amount you cannot bound in advance, and no error is raised to tell you. On top of that: - **Torn multi-row states.** A transaction is precisely the mechanism that hides intermediate states. A dirty reader sees them: a debit without its matching credit, an order header without its lines, a parent row whose children have not been written yet. Aggregates computed across such a moment are internally inconsistent even before any rollback. - **States that violate declared constraints.** Constraints are enforced at write or commit; the reader can observe a state that will be rejected. - **Total silence.** There is no flag on the result set saying "this contains provisional data", no counter to alarm on, and no way for a downstream consumer to tell a dirty result from a clean one. The blast radius is set by who reuses the number, not by who wrote the query. ## The better alternatives, in order Before reaching for the level, exhaust the options that do not give up correctness: 1. **A read replica or a dedicated reporting endpoint.** Moves the scan off the contended instance entirely and keeps normal isolation. This solves the real problem — analytics competing with OLTP — rather than papering over it. 2. **An engine-maintained approximation.** Row-count and size estimates from the engine's own catalog or statistics are usually the number the person actually wanted, cost nothing, and carry no dirty-data risk. 3. **A multi-version read path**, where the engine offers one, giving non-blocking reads of committed data. 4. **Fixing the query.** Reads that must scan huge portions of a table because of a missing index hold locks proportional to what they touch; narrowing the access path shrinks the contention that motivated the whole idea. 5. **Shortening the write transactions** that the reader is waiting behind, which usually helps far more things than this one query. ## If you use it anyway Put a comment on the statement stating that the result is approximate and why. Keep it out of any code path that writes. Do not let the value be stored where a later reader will assume it is exact. And revisit it if the platform ever moves to a multi-version engine, where the setting becomes noise that misleads the next reader about the query's intent. ## The stance to take in an interview Name the narrow zone honestly, then show you know the unbounded-error failure mode rather than only the rollback one, then say what you would do instead. "I know when it is defensible, and I would still reach for a replica or the engine's own estimate first" is the answer that reads as production judgement.

  • Why can a row be counted twice or missed entirely by an unlocked scan, even with no rollbacks at all?
    Because the scan holds no lock fixing its position relative to concurrent structural change. If a concurrent update changes a row's indexed key so the row relocates, it can move from ahead of the scan to behind it and never be visited, or from behind to ahead and be visited twice. The error therefore scales with concurrent write activity, not with the volume of uncommitted data.
  • What would you propose instead when someone wants a fast approximate row count on a busy table?
    First the engine's own catalog or statistics estimate, which is maintained anyway, costs nothing, and carries no dirty-data risk. Failing that, a read replica or reporting endpoint so the scan does not compete with the transactional workload. Lowering the isolation level is the last resort and the least honest about its error bars.
  • A colleague adds READ UNCOMMITTED to a dashboard query to make it faster. What is your response?
    Ask what the number is used for: dashboards exist so people make decisions, and a silently wrong figure with unbounded error is worse than a slow correct one. Then attack the actual cost — access path, transaction length, or placement of the query — and offer a replica or an engine-maintained estimate. If the engine is multi-version, point out that the setting will not speed anything up at all.

saying these in an interview costs you the question

  • Believing the only risk is reading data that later rolls back
  • Assuming the error is bounded by the amount of uncommitted work in flight
  • Using it on dashboards or reports that drive decisions
  • Adding it as a general performance setting on a multi-version engine, where it does nothing
  • Not reaching first for a replica, an engine-maintained estimate, or a better access path

context