Backend / Databases / SQL / 08_transactions_isolation.md

Transactions and isolation levels

Updated 3 min read source
On this page7
  1. ACID
  2. The four standard isolation levels
  3. The three phenomena
  4. Setting the level
  5. Snapshot isolation vs SERIALIZABLE
  6. The retry pattern
  7. Interview angle

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.

sql
-- 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.

sql
-- 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.

sql
-- 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

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

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

In SQLAlchemy:

python
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.

sql
-- 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:

python
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 4

  • 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 SQL Transactions for transaction basics, CAP, ACID, BASE, PACELC for the distributed angle.