backend / databases / sql / 08_transactions_isolation.md

Transactions and isolation levels

3 min read source

Transactions and isolation levels

ACID is the contract a transaction makes. Isolation levels control how strictly one transaction is shielded from concurrent ones.

ACID

  • Atomicity — all statements in a transaction commit together or none do.
  • Consistency — the transaction moves the DB from one valid state to another (constraints hold).
  • Isolation — concurrent transactions don’t see each other’s intermediate state.
  • Durability — once committed, changes survive crashes (typically via WAL — write-ahead log).

The four standard isolation levels

Level Dirty read Non-repeatable read Phantom read Default in
READ UNCOMMITTED possible possible possible (rare)
READ COMMITTED prevented possible possible PostgreSQL, Oracle, SQL Server
REPEATABLE READ prevented prevented possible (SQL standard) / prevented (PG, MySQL InnoDB) MySQL
SERIALIZABLE prevented prevented prevented

Note: PostgreSQL’s REPEATABLE READ uses snapshot isolation, which actually prevents phantoms too. The standard doesn’t require this.

The three phenomena

Dirty read — read uncommitted data that may roll back.

-- T1: UPDATE accounts SET balance = 0 WHERE id = 1;  -- not committed yet
-- T2: SELECT balance FROM accounts WHERE id = 1;     -- sees 0
-- T1: ROLLBACK;                                       -- T2 saw a value that never existed

Non-repeatable read — same row read twice in one transaction returns different values.

-- T1: SELECT balance FROM accounts WHERE id = 1;     -- 100
-- T2: UPDATE accounts SET balance = 50 WHERE id = 1; COMMIT;
-- T1: SELECT balance FROM accounts WHERE id = 1;     -- 50  ← changed mid-transaction

Phantom read — same query returns different sets of rows.

-- T1: SELECT COUNT(*) FROM orders WHERE user_id = 7; -- 3
-- T2: INSERT INTO orders (user_id, ...) VALUES (7, ...); COMMIT;
-- T1: SELECT COUNT(*) FROM orders WHERE user_id = 7; -- 4  ← new row appeared

Setting the level

-- Per transaction
BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- ...
COMMIT;

-- Session-wide (PG)
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL REPEATABLE READ;

In SQLAlchemy:

from sqlalchemy import create_engine
engine = create_engine(url, isolation_level="SERIALIZABLE")
# or per-connection:
with engine.connect().execution_options(isolation_level="REPEATABLE READ") as conn:
    ...

Snapshot isolation vs SERIALIZABLE

PG’s REPEATABLE READ gives snapshot isolation: every read in the transaction sees the DB as of transaction start. Fast, but suffers from write skew — two transactions read disjoint rows, both decide to update based on a constraint, and the constraint is violated after both commit.

-- Doctor on-call constraint: at least one doctor must be on call.
-- Two doctors run "if total > 1, set me off-call" simultaneously under REPEATABLE READ.
-- Both see total = 2, both update, constraint broken.

PG’s SERIALIZABLE detects this via SSI (serializable snapshot isolation) and aborts one transaction with 40001 serialization_failure. Application must retry.

The retry pattern

Under SERIALIZABLE (or in any optimistic-concurrency scheme), assume any write transaction can fail with a serialization error and code retries:

from time import sleep
import random

for attempt in range(5):
    try:
        with session.begin():
            # work
            ...
        break
    except OperationalError as e:
        if "40001" not in str(e.orig):
            raise
        sleep(0.05 * (2 ** attempt) + random.random() * 0.01)

Interview angle

  • Q: “What are the four isolation levels and what does each prevent?”
  • Q: “Default isolation in Postgres vs MySQL?” — RC vs RR (InnoDB).
  • Follow-up: “When would you raise the level?” — financial calculations, on-call rotations, anything with cross-row invariants.
  • Follow-up: “What’s write skew and how do you prevent it?” — SERIALIZABLE detects it; or take row locks (SELECT ... FOR UPDATE); or use a unique constraint that captures the invariant.

See 04_sql_transactions.md for transaction basics, 15_cap_theorem_acid_base.md for the distributed angle.