SQL SQL In Depth
0%
lesson lost-update write-skew concurrency-anomalies

Write Anomalies: Dirty Writes, Lost Updates, Read Skew & Write Skew

7 min read Module 5.1: ACID Transactions, Concurrency Anomalies & Isolation Levels Updated Oct 26, 2026
📄 Cheat Sheet
Write Anomalies: Dirty Writes, Lost Updates, Read Skew & Write Skew — Database Systems & SQL Architecture Lecture
Goto Conferences (Martin Kleppmann) Write Anomalies: Dirty Writes, Lost Updates, Read Skew & Write Skew — Database Systems & SQL Architecture Lecture Watch on YouTube ↗

Builds first-principles mastery of Write Anomalies: Dirty Writes, Lost Updates, Read Skew & Write Skew with concrete query execution trees, storage page layouts, and production tuning trade-offs.

0:00 / 5:17

1. First-Principles Mental Model & Relational Mechanics

Every SQL statement in Write Anomalies: Dirty Writes, Lost Updates, Read Skew & Write Skew compiles down to a deterministic tree of relational algebra operators acting on physical pages in the buffer pool. At the logical layer, Why preventing standard SQL-92 read phenomena is not enough: how concurrent read-modify-write cycles lose updates and how Write Skew breaks multi-row invariants. Rather than mutating global variables imperatively, declarative SQL specifies the mathematical properties of the desired output relation while delegating physical access path selection to the cost-based query optimizer.

When the database engine processes write-anomalies-lost-update-write-skew within ACID Transactions, Concurrency Anomalies & Isolation Levels, it enforces strict invariants over tuple domains, attribute visibility, and I/O page budgets. Understanding the boundary between logical relational semantics (what rows qualify) and physical execution (whether the engine performs a sequential heap scan, a B+Tree range seek, or an in-memory hash probe) is the defining skill of a Staff Database Engineer.

Formal Relational & Cost Invariant — Write Anomalies: Dirty Writes, Lost Updates, Read Skew & Write Skew

Let relation RR contain NN tuples stored across BB physical 8KB pages (B≈⌈N/tuples_per_page⌉B \approx \lceil N / \text{tuples\_per\_page} \rceil). Evaluating Write Anomalies: Dirty Writes, Lost Updates, Read Skew & Write Skew maintains the relational closure and I/O cost bound:

CostI/O(R)=Bseq⋅cseq+log⁡F(N)⋅crandom,σθ(πA(R))⊆Dom(A)\text{Cost}_{\text{I/O}}(R) = B_{\text{seq}} \cdot c_{\text{seq}} + \log_{F}(N) \cdot c_{\text{random}}, \quad \sigma_{\theta}(\pi_{A}(R)) \subseteq \text{Dom}(A)

Where F≈250–500F \approx 250\text{--}500 is the B+Tree page fanout, σθ\sigma_{\theta} denotes Three-Valued Logic predicate selection over lost-update, and πA\pi_{A} projects the target attribute schema without violating domain constraints.

2. Database Table Schema, Execution Pipeline & Visual Architecture

Tracing how Write Anomalies: Dirty Writes, Lost Updates, Read Skew & Write Skew flows through the parser, catalog binder, cost-based optimizer, and physical executor reveals exactly where latency and cardinality bugs originate:

  • Stage 1 — Lexical Parsing & Catalog Binding (lost-update): The SQL parser builds an Abstract Syntax Tree (AST) and resolves every relation, column identifier, and data type against system catalog tables (such as pg_class, pg_attribute, or sqlite_master).
  • Stage 2 — Logical Rewrite & Algebraic Simplification (write-skew): The planner normalizes predicates, pushes selective filters as close to the storage scan as possible, and verifies whether Three-Valued NULL semantics permit subquery unnesting or join elimination.
  • Stage 3 — Cost Estimation & Physical Operator Selection (concurrency-anomalies): Using histogram boundaries, Most Common Values (MCV), and Number of Distinct Values (NDV), the optimizer selects the minimum-cost physical plan and streams tuples through the execution pipeline.
Write Anomalies: Dirty Writes, Lost Updates, Read S... — 8KB Slotted Page & MVCC Tuple Versioning Module M13 · PageHeader (pd_lsn) · Line Pointer Array (ItemId) · MVCC Tuple (xmin / xmax) 8192-BYTE SLOTTED PAGE · pd_lsn=0/1A8F40 · pd_lower=44 · pd_upper=7820 lp[1] -> 8096 lp[2] -> 7960 lp[3] -> 7820 ──► pd_lower UNALLOCATED FREE SPACE (7776 bytes) Tuple v1 (ctid 0,1): xmin=501 xmax=509 [DEAD] | acct=101 | bal=475000 Tuple v2 (ctid 0,2): xmin=509 xmax=0 [LIVE] | acct=101 | bal=415000 MVCC SNAPSHOT & WAL INVARIANT • Snapshot xmin=505, xmax=512 • Readers never block Writers • WAL Rule: Flush LSN >= pd_lsn before dirty page eviction • VACUUM reclaims dead v1 tuple when OldestXmin > 509
sequenceDiagram autonumber participant App as Client / Txn participant Exec as SQL Executor (lost-update) participant Buf as Buffer Pool & MVCC participant WAL as Write-Ahead Log (fsync) App->>Exec: BEGIN; Statement (write-skew) Exec->>Buf: Acquire Snapshot (xmin/xmax) & Pin 8KB Page Buf->>WAL: Append Redo Record (LSN) WAL-->>Buf: fdatasync(WAL) Confirmed Buf-->>Exec: Commit Tuple & Release Locks (concurrency-anomalies) Exec-->>App: Return Verified Result Set

3. Interactive Table-Level & Storage Engine Simulator

Use the interactive simulator below to step through Write Anomalies: Dirty Writes, Lost Updates, Read Skew & Write Skew at the tuple and page level. Observe how input rows, intermediate filter/join states, NULL badges, and physical page reads evolve at each step:

ACID Isolation Timeline: Dirty Read, Lost Update, Phantom & Write Skew (SSI)
RC / RR / SSI

Interleave concurrent Transactions T1 and T2 step by step. Trigger Lost Updates and Write Skew under Snapshot Isolation, then resolve them with FOR UPDATE or Serializable SSI.

Step 1 / 6
Custom Input:
Timeline

Invariant & State

Relation initialized with 4 tuples across schema (clock_tick, tx_t1_action, tx_t2_action, db_state, isolation_verdict) — preset: Write Skew Anomaly (Disjoint Writes Violating Global Invariant).
📊 Live Magnitude & Pointer Stage — ACID Isolation Timeline: Dirty Read, Lost Update, Phantom & Write Skew (SSI) Active Pivot Match/Sorted
Tuple # t = 1:… [0] t = 2:… [1] t = 3:… [2] t = 4:… [3]
📋 ACID Isolation Timeline: Dirty Read, Lost Update, Phantom & Write Skew (SSI) — Initial Relation State 4 row(s)
clock_ticktx_t1_actiontx_t2_actiondb_stateisolation_verdictState
t = 1BEGIN ISOLATION LEVEL REPEATABLE READBEGIN ISOLATION LEVEL REPEATABLE READon_call: Alice=true, Bob=trueSnapshots taken @ LSN 100CURSOR START
t = 2SELECT COUNT(*) WHERE on_call → 2SELECT COUNT(*) WHERE on_call → 2Both see 2 doctors on callrw-antidependency formed
t = 3UPDATE doctors SET on_call=false WHERE name=AliceUPDATE doctors SET on_call=false WHERE name=BobDisjoint rows: no row lock conflict!Dangerous structure T1 --rw--> T2 --rw--> T1
t = 4COMMIT ✓COMMIT (Under SI: ✓ / Under SSI: ERROR 40001!)SI: 0 doctors on call! SSI: Aborted T2SSI prevents Write Skew ✓
[0]t = 1:BEGIN ISOLATION LEVEL REPEATABLE READTuple #0
[1]t = 2:SELECT COUNT(*) WHERE on_call → 2
[2]t = 3:UPDATE doctors SET on_call=false WHERE name=Bob
[3]t = 4:COMMIT (Under SI: ✓ / Under SSI: ERROR 40001!)
Input Tuples
4
Columns
5
Preset Mode
writeskew
Pipeline Stage
1. Initial Scan
[Step 1] Initialized ACID Isolation Timeline: Dirty Read, Lost Update, Phantom & Write Skew (SSI) (Write Skew Anomaly (Disjoint Writes Violating Global Invariant)) with 4 base rows.

4. Live WASM SQL Lab, Clause-by-Clause Visualizer & Automated Test Checker

Execute, edit, and test the production SQL query below directly in your browser using the embedded WebAssembly SQLite 3 engine. Click ▶ Run Query to execute, 🔍 Step Clauses to inspect the live intermediate table after FROM/JOIN, WHERE, GROUP BY, HAVING, WINDOW, SELECT, and ORDER BY, or ✓ Check SQL Tests to run automated relational assertions against the reference result:

SQL · WASM14 lines
-- Dataset: fintech_ledger | Double-Entry ACID Invariant & Ledger Conservation Audit
SELECT
  t.transfer_id,
  t.idempotency_key,
  t.status,
  COUNT(je.entry_id) AS journal_legs,
  SUM(CASE WHEN je.direction = 'debit' THEN je.amount_cents ELSE 0 END) AS total_debit_cents,
  SUM(CASE WHEN je.direction = 'credit' THEN je.amount_cents ELSE 0 END) AS total_credit_cents,
  SUM(CASE WHEN je.direction = 'credit' THEN je.amount_cents ELSE -je.amount_cents END) AS net_balance_cents
FROM transfers AS t
INNER JOIN journal_entries AS je
  ON je.transfer_id = t.transfer_id
GROUP BY t.transfer_id, t.idempotency_key, t.status
ORDER BY t.transfer_id ASC;

5. Cross-Engine Dialect & Cost Comparison Matrix

While the SQL ISO standard defines the logical behavior of Write Anomalies: Dirty Writes, Lost Updates, Read Skew & Write Skew, production storage engines differ in how they execute, lock, and index the underlying tuples:

Database Engine Primary Storage & Access Mechanism I/O & CPU Complexity Production Caveat for Write Anomalies: Dirty Writes, Lost Updates, Read Skew & Write Skew
PostgreSQL 17 Heap Tuple Store (ctid) + B-Tree / GIN / BRIN + MVCC (xmin/xmax) O(log⁡BN)O(\log_B N) seek / O(N)O(N) scan Requires ANALYZE statistics and Visibility Map bits for Index-Only Scans.
SQLite 3.46 (WASM) B+Tree Table (rowid clustered) + Covering Secondary B-Trees O(log⁡BN)O(\log_B N) B+Tree traversal Runs in-process with zero network RTT; inspect EXPLAIN QUERY PLAN for USE TEMP B-TREE.
MySQL 8.4 (InnoDB) Clustered Primary Key B+Tree + Undo Log Rollback Segments O(log⁡BN)O(\log_B N) PK / $2 \times O(\log_B N)$ Secondary Secondary indexes store the Primary Key value; wide PKs amplify secondary index footprint.
DuckDB / ClickHouse Columnar Vectorized Chunks + Zone Maps (Min/Max Skipping) O(N/SIMD_Width)O(N / \text{SIMD\_Width}) vectorized scan Excels at wide analytical scans and aggregations; avoid single-row OLTP point mutations.

6. Live Verification, Catalog Introspection & Execution Plan Lab (SQL)

To verify how a production database engine executes Write Anomalies: Dirty Writes, Lost Updates, Read Skew & Write Skew across edge-case rows, run or modify the second live SQL verification query below. Inspect how GROUP BY buckets, window partitions, and Three-Valued NULL guards behave in real time:

SQL · WASM15 lines
-- Dataset: fintech_ledger | Double-Entry Conservation & Account Audit (write-anomalies-lost-update-write-skew)
SELECT
  a.account_id,
  a.owner_name,
  a.account_type,
  COUNT(je.entry_id) AS journal_postings,
  SUM(CASE WHEN je.direction = 'credit' THEN je.amount_cents ELSE 0 END) AS credit_cents,
  SUM(CASE WHEN je.direction = 'debit' THEN je.amount_cents ELSE 0 END) AS debit_cents,
  SUM(CASE WHEN je.direction = 'credit' THEN je.amount_cents ELSE -je.amount_cents END) AS computed_balance_cents,
  a.posted_balance_cents
FROM accounts AS a
INNER JOIN journal_entries AS je
  ON je.account_id = a.account_id
GROUP BY a.account_id, a.owner_name, a.account_type, a.posted_balance_cents
ORDER BY a.account_id ASC;

7. Production Failure Modes, Anti-Patterns & Staff Tuning Checklist

Production Pitfall & SEV-1 Outage Pattern

The most common production failure in Write Anomalies: Dirty Writes, Lost Updates, Read Skew & Write Skew occurs when a query passes functional unit tests on a 50-row local fixture but degrades into an O(N2)O(N^2) loop, an unindexed full-table scan, or a silent NULL data-drop once table cardinality crosses 10710^7 rows. Always verify both NULL boundary rows and physical buffer reads before deploying to production.

  • Verify Execution Plans Early: Never guess what the planner will do—run EXPLAIN QUERY PLAN (SQLite) or EXPLAIN (ANALYZE, BUFFERS) (PostgreSQL) and confirm that estimated row counts match actual row counts within 2×2\times.
  • Guard Against Three-Valued NULL Traps: Audit every NOT IN, outer-join WHERE filter, and aggregate AVG/COUNT(col) expression for nullable columns; prefer NOT EXISTS and explicit COALESCE defaults.
  • Keep Predicates SARGable: Never wrap an indexed column in a function, arithmetic expression, or implicit type cast on the left-hand side of a comparison operator.
  • Bound Memory & Lock Scope: Ensure sorting, hashing, and batch mutations operate within bounded memory budgets (work_mem) and hold row/table locks for the shortest possible duration.
Check yourself Question 1 of 4
Concept Check Write Anomalies: Dirty Writes, Lost Updates, Read Skew & Write Skew — Core Relational Invariant recall

Which statement accurately characterizes the primary mechanism and guarantee of Write Anomalies: Dirty Writes, Lost Updates, Read Skew & Write Skew?

Interactive 3-Step Guided SQL Lab & Step-by-Step Table Visualizer (PostgreSQL 18 WASM & SQLite 3 WASM) 3 Guided Steps + Progressive Hints

Write Anomalies: Dirty Writes, Lost Updates, Read Skew & Write Skew — Interactive Guided SQL Lab

Why preventing standard SQL-92 read phenomena is not enough: how concurrent read-modify-write cycles lose updates and how Write Skew breaks multi-row invariants.

⏱ Target Time: O(N) scan / O(log_B N) indexed 💾 Auxiliary Space: O(1) streaming tuple state 🧭 Pattern: ACID Transactions, Concurrency Anomalies & Isolation Levels 🗄️ Dataset: fintech_ledger
01 Phase 1 · Base Relation & SARGable Predicate
Base Relation Scan & SARGable Filter (fintech_ledger)
Establish the base relation scan in fintech_ledger for Write Anomalies: Dirty Writes, Lost Updates, Read Skew & Write Skew with a SARGable predicate and explicit Three-Valued Logic NULL handling.
Run or edit query.sql to inspect the base table tuples, verify SARGable index filtering in the WHERE clause, and step through the live Table Visualizer.
💡 Progressive SQL Hint (Step 1)
SELECT transfer_id, idempotency_key, from_account_id, to_account_id, ROUND(amount_cents / 100.0, 2) AS amount_usd, status, created_at FROM transfers WHERE status = 'posted' AND amount_cents >= 25000 ORDER BY transfer_id ASC LIMIT 10;
02 Phase 2 · Relational Join, Aggregation & 3VL Guard
Relational Join, Grouping & NULL-Safe Aggregation
Combine normalized tables via explicit JOIN conditions and fold group buckets or window partitions without losing NULL boundary rows.
Run or refine the Step 2 relational join and aggregation query in query.sql, then click 🔍 Step Clauses to inspect each intermediate table.
💡 Progressive SQL Hint (Step 2)
-- Dataset: fintech_ledger | Double-Entry Conservation & Account Audit (write-anomalies-lost-update-write-skew)
SELECT
  a.account_id,
  a.owner_name,
  a.account_type,
  COUNT(je.entry_id) AS journal_postings,
  SUM(CASE WHEN je.direction = 'credit' THEN je.amount_cents ELSE 0 END) AS credit_cents,
  SUM(CASE WHEN je.direction = 'debit' THEN je.amount_cents ELSE 0 END) AS debit_cents,
  SUM(CASE WHEN je.direction = 'credit' THEN je.amount_cents ELSE -je.amount_cents END) AS computed_balance_cents,
  a.posted_balance_cents
FROM accounts AS a
INNER JOIN journal_entries AS je
  ON je.account_id = a.account_id
GROUP BY a.account_id, a.owner_name, a.account_type, a.posted_balance_cents
ORDER BY a.account_id ASC;
03 Phase 3 · Production Query & Plan Verification
Production Query & Execution Plan Verification (Write Anomalies: Dirty Writes, Lost Updates, Read Skew & Write Skew)
Verify final column projections, ordering, and physical B+Tree access paths via EXPLAIN QUERY PLAN and automated relational assertions.
Execute the complete production query in query.sql and click ✓ Check SQL Assertions to verify schema alignment, row cardinality, and tuple equivalence.
💡 Progressive SQL Hint (Step 3)
-- Dataset: fintech_ledger | Double-Entry ACID Invariant & Ledger Conservation Audit
SELECT
  t.transfer_id,
  t.idempotency_key,
  t.status,
  COUNT(je.entry_id) AS journal_legs,
  SUM(CASE WHEN je.direction = 'debit' THEN je.amount_cents ELSE 0 END) AS total_debit_cents,
  SUM(CASE WHEN je.direction = 'credit' THEN je.amount_cents ELSE 0 END) AS total_credit_cents,
  SUM(CASE WHEN je.direction = 'credit' THEN je.amount_cents ELSE -je.amount_cents END) AS net_balance_cents
FROM transfers AS t
INNER JOIN journal_entries AS je
  ON je.transfer_id = t.transfer_id
GROUP BY t.transfer_id, t.idempotency_key, t.status
ORDER BY t.transfer_id ASC;

Key Takeaways

Write Anomalies: Dirty Writes, Lost Updates, Read Skew & Write Skew (write-anomalies-lost-update-write-skew): Why preventing standard SQL-92 read phenomena is not enough: how concurrent read-modify-write cycles lose updates and how Write Skew breaks multi-row invariants.
Always separate logical SQL semantics (FROM -> WHERE -> GROUP BY -> HAVING -> WINDOW -> SELECT -> ORDER BY -> LIMIT) from the physical execution plan chosen by the cost-based optimizer.
Audit every predicate and join condition for Three-Valued Logic (NULL -> UNKNOWN) edge cases and verify SARGability so B+Tree indexes can perform O(log_B N) range seeks.
Use EXPLAIN QUERY PLAN / EXPLAIN (ANALYZE, BUFFERS) to inspect physical scan operators, join algorithms, and temporary sort/hash spills before shipping queries to production.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue