Skip to content
About
Main content

Read Committed and the lost update

Why does Read Committed allow lost updates, and how much does it matter where the read-modify-write lives?

Level by level, each derived from the mechanism that produced it

Read Uncommitted: a fossil

(RU) forbids only . Note why dirty write is forbidden even at this level. It has nothing to do with isolation. Undo-based recovery (rolling back by restoring old values) needs a transaction's writes to be the last writes to those items. Otherwise a would erase another transaction's work. So the floor of the isolation hierarchy is set by the recovery mechanism, not by an isolation requirement.

Under the level buys nothing, because there is no cheaper path to an uncommitted version than to a committed one. PostgreSQL silently maps RU to Read Committed. RU made sense only in lock-based engines, where reading meant taking a shared and RU meant skipping it.

Read Committed: cheap, and the home of lost updates

Read Committed (RC) is the cheapest level with a defensible story. Its defining property is not what it forbids but the scope of its : per statement, not per transaction. Each statement sees the data committed when that statement started. Two statements in one transaction can see two different worlds.

By Little's rule ( count ≈ conflict rate × ), RC makes the window W small while still excluding garbage such as uncommitted values.

Two details carry weight.

  1. In PostgreSQL at RC, if an UPDATE has to wait for a lock held by another transaction, then after the lock is released it re-evaluates the row against the new version. One statement is therefore evaluated against a mixture of two snapshots. It can produce a result that no serial order allows.
  2. This is the detail that actually corrupts production data: RC permits , and the window is not a database window.

The window argument, with numbers

Look at the bonus job:

-- Application code (two round trips, plus code between them)
BEGIN;
SELECT balance FROM accounts WHERE id = 7;        -- returns 100
-- application computes 100 + 100 = 200 ...
UPDATE accounts SET balance = 200 WHERE id = 7;   -- overwrites
COMMIT;

If a transfer adds 50 between the SELECT and the UPDATE, the balance in the database is 150 when the job writes 200. The 50 is gone. The anomaly needs two operations to fall inside the same window.

Suppose 200 conflicting operations per second touch the same hot accounts (an illustration).

Where the read-modify-write livesWindow WOperations in the window (λ × W)
Application code with network 50 ms = 0.05 s200 × 0.05 = 10
One SQL statement inside the engine50 µs = 0.00005 s200 × 0.00005 = 0.01

The ratio is 10 ÷ 0.01 = 1,000. The typical windows in practice are 1–100 ms for application code and 10–100 µs for a single statement. Taking the ends of those ranges gives ratios from 10× to 10,000×, so "three to four orders of magnitude" describes a common case, not a guarantee. (This is an expected count of overlaps, used as a rough probability measure. At small values it approximates the chance that at least one collision happens.)

The point:

The three fixes, in ascending cost

These are three escapes that recur throughout database design.

  1. Restate the operation so that it commutes. UPDATE accounts SET balance = balance - 100 WHERE id = 7; Two such updates give the same result in either order, and the engine applies each to the latest value. Free. The becomes irrelevant. This is the .
  2. Check a version on write. UPDATE accounts SET balance = 200, version = 8 WHERE id = 7 AND version = 7; If zero rows are updated, someone else changed the row, so the code re-reads and retries. This is compare-and-swap (). It costs a retry loop and needs an owner for that loop.
  3. Take the lock. SELECT balance FROM accounts WHERE id = 7 FOR UPDATE; The row is locked until , so no other writer can slip in. This is called : a level between RC and that the standard lacks. Historically it was the answer to lost update, and SELECT ... FOR UPDATE is the fix most application code needs. It costs blocking and a risk of deadlock (two transactions each waiting for a lock the other holds).