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?
answer
- snapshot = which row versions you may see
- RC: new snapshot per statement → fresh, unstable
- RR: one snapshot per transaction → stable, stale
- snapshot taken at first statement, not BEGIN
- long RR transaction = version retention / bloat + retries on write
basics
~20 sREAD 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.
solid answer
~50 sA snapshot is the set of transactions whose effects are visible; the engine uses it to pick which stored version of each row a read should see. - **READ COMMITTED**: a fresh snapshot per statement. Statement two sees everything committed before *it* started, including commits that landed after statement one. Reads are as fresh as possible, and re-reading a row can return a different value — the non-repeatable read. - **REPEATABLE READ / snapshot isolation**: one snapshot, taken at the transaction's first statement (not at `BEGIN`), used by every read until commit. Re-reads are stable, and the whole transaction sees one consistent instant. The trade is freshness versus stability. The transaction-wide snapshot buys internal consistency at the cost of reading older data, forcing the engine to retain old row versions for as long as the transaction lives, and making writes conflict-prone: a write against a row that changed since the snapshot cannot silently re-read, so it aborts with a serialization failure you must retry.
code
text · 6 linesREAD COMMITTED REPEATABLE READ
BEGIN BEGIN
SELECT price -> 100 SELECT price -> 100 (snapshot taken here)
[other txn sets 120, commits]
SELECT price -> 120 SELECT price -> 100
COMMIT COMMITgo deeper
Say that READ COMMITTED refreshes the view for every statement while REPEATABLE READ keeps one view for the whole transaction.
Add that the snapshot is taken at the first statement, that a single statement is always consistent, and that the trade is freshness versus stability.
Cover the operational consequences: serialization failures needing retries, version retention and bloat from long snapshots, and collapsing reads into one statement as an alternative.
Decide per workload which transactions need a transaction-wide view, where long snapshots are permitted to run, and what the retry and observability policy is.
## What a snapshot is In a multiversion engine, an update does not overwrite a row — it stores a **new version** and leaves the old one until nothing can still need it. Each version is stamped with the transaction that created it (and the one that removed it). A **snapshot** is the visibility rule the reader carries: roughly, the set of transactions considered already committed. Given a snapshot, the engine walks the versions of a row and returns the one that was committed and current as of that snapshot. The interesting question is therefore not *how* versions are chosen but **how often a new snapshot is taken**. That single decision is what separates the two isolation levels people ask about. ## Per-statement snapshots — READ COMMITTED At READ COMMITTED, the engine takes a **new snapshot at the beginning of each statement**. Consequences: - Each statement sees everything committed before that statement started. A transaction that runs for a minute sees the world move under it, one statement at a time. - A single statement is still internally consistent: it uses one snapshot from start to finish, so a long-running `SELECT` never sees half of a concurrent transaction's work. - Re-reading a row in a later statement can return a different value: **non-repeatable read permitted**. - Writes are easy: when an `UPDATE` blocks on another transaction's row lock and that transaction commits, the waiting statement can simply take a newer snapshot for that row, re-evaluate its `WHERE` clause, and proceed. This is why write conflicts rarely surface as errors at this level. - Old row versions become collectable quickly, because no long-lived snapshot pins them. ## Per-transaction snapshots — REPEATABLE READ / snapshot isolation Here the engine takes **one snapshot** and uses it for every read until the transaction ends. Two important details: 1. The snapshot is normally taken at the **first statement**, not at `BEGIN`. A transaction that opens and then sits idle for a minute still sees a view from when it first read something. (Engines usually offer a way to fix the snapshot at transaction start explicitly, which matters for coordinating a consistent dump across sessions.) 2. The transaction **can see its own writes**, which sit outside the snapshot rule — you always read your own uncommitted changes. Consequences: - Re-reading a row always returns the same value: **non-repeatable reads prevented**. Every query in the transaction reflects the same instant, so multi-query reports tie out. - Reads are **stale** by design. A transaction running for a minute reports data as of a minute ago. For a report this is usually right; for a check-then-act decision it can be dangerously wrong, because you are validating against a world that has moved on. - Reads still **never block writers**, and writers never block readers. The concurrency benefit of multiversioning is preserved at both levels. - **Writes get harder.** If your transaction updates a row that has changed since your snapshot, the engine cannot let you silently re-read the newer version — that would contradict everything else you have seen. It aborts your transaction with a **serialization failure**, and the application must retry the transaction from the beginning. Code running at this level needs a retry loop; code at READ COMMITTED usually does not. - **Version retention cost.** The engine must keep every row version that your snapshot might still need. A long-running transaction at this level therefore holds back cleanup of old versions across the whole database, causing table/undo growth and, in some engines, errors when the retained history is exceeded. Long analytical transactions at REPEATABLE READ are a classic cause of storage bloat. ## Same name, different mechanism Be careful with the level *names*. In a lock-based engine, REPEATABLE READ means "shared read locks are held until commit" — readers block writers, and the data you see is always the latest committed rather than a snapshot. The observable guarantee about re-reads is the same; the performance profile is the opposite. When someone asks "does REPEATABLE READ prevent it", the complete answer names the mechanism, because that determines whether you should expect blocking or staleness plus retries. Also note that engines differ in what their REPEATABLE READ additionally forbids beyond the standard's minimum — some snapshot implementations also stop repeated range queries from changing, which the standard does not require. Do not assume; that behaviour is engine-specific. ## Choosing Use per-statement (READ COMMITTED) when statements are independent, freshness matters, and the transaction acts on what it just read. It is the right default for typical transactional workloads and keeps version churn low. Use a transaction-wide snapshot when several reads must be mutually consistent — a multi-query report, an export, a consistency check, a computation that reads the same data twice — and accept the staleness, the retention cost, and the retry requirement for any writes. And remember the third option: collapse the reads into **one statement**. A single statement gets one snapshot at any isolation level, so a join or a common table expression that answers the whole question at once is often the cheapest way to get consistency without holding a long snapshot at all.
- At REPEATABLE READ, can a transaction see its own uncommitted changes even though they are not in its snapshot?Yes. A transaction always sees the effects of its own statements; the snapshot rule governs other transactions' changes only. So an UPDATE followed by a SELECT in the same transaction returns the new value, while a concurrent transaction's committed change made after the snapshot remains invisible.
- Why does a long-running REPEATABLE READ transaction cause storage growth?Its snapshot may still need any row version that was current when it started, so the engine cannot reclaim superseded versions anywhere in the database while it runs. Old versions accumulate in the table or in undo storage, tables and indexes bloat, and scans get slower. This is why long analytical transactions are kept short, moved to a replica, or run against an exported snapshot.
- Why do writes at a transaction-wide snapshot need a retry loop?If the row you are updating changed after your snapshot, the engine cannot show you the newer version without contradicting your consistent view, so it aborts the transaction with a serialization failure. The correct response is to roll back and re-run the whole transaction, which re-reads the world at a fresh snapshot. At READ COMMITTED the statement can simply re-read the newer row instead, so this class of error is rare.
saying these in an interview costs you the question
- Believing the transaction-wide snapshot is taken at BEGIN rather than at the first statement
- Thinking a snapshot means the transaction cannot see its own writes
- Assuming REPEATABLE READ works the same way in a lock-based engine as in a multiversion one
- Treating a transaction-wide snapshot as free, ignoring staleness, version retention and write-conflict retries
- Claiming a single long-running statement can see a moving target at READ COMMITTED