Franck PachotDatabase Developer Advocate Field guide · SQL concurrency All articles

A practical minibook

SQL Isolation Levels

From anomalies to application design

Isolation is not a ladder from “unsafe” to “safe.” It is a contract between concurrent transactions, the database engine, and your application. This guide builds that contract from observable histories, then maps it to SQL levels, MVCC implementations, locks, and data models.

Franck Pachot ~35 minute read PostgreSQL · Oracle · YugabyteDB · MongoDB

Correctness lives in business invariants, not in an isolation-level name.

This minibook was AI-generated from Franck Pachot's archived blog posts. Links to the original articles are included for source context and verification.

01

Start with a history, not a label

A transaction reads a state, makes a decision, and attempts to publish a new state. Isolation asks whether concurrent decisions can be ordered as if they happened one after another.

The final value may be 7, silently erasing A's update. Or the second writer may wait, restart, or fail. All are implementation choices. The application contract must say which outcome is acceptable.

AtomicityAll or nothing

The transaction does not expose half of its work.

ConsistencyInvariants survive

The schema and application define what “valid” means.

IsolationConcurrent histories

Readers and writers observe a constrained ordering.

DurabilityCommit persists

A successful commit survives the promised failures.

02

Name the anomaly before choosing the remedy

The classic SQL phenomena are useful vocabulary, but production failures are easier to reason about as dependency cycles: what did each transaction read, and which facts did it invalidate?

PhenomenonWhat happensTypical defense
Dirty readA transaction reads data another transaction has not committed.Read Committed or MVCC visibility.
Dirty writeOne uncommitted write overwrites another uncommitted write.Write locks; virtually every transactional SQL engine prevents it.
Non-repeatable readThe same row returns a different committed value later in one transaction.Transaction snapshot or explicit lock.
PhantomA repeated predicate finds a different set of rows.Predicate/range protection or serialization detection.
Lost updateA read-modify-write cycle erases a concurrent update.Atomic DML, row lock, version check, or abort/retry.
Write skewTransactions update different rows after reading a shared invariant.Serializable, explicit invariant lock, assertion, or remodel.
Read skewA query or transaction combines values from incompatible moments.Statement or transaction snapshot, depending on the invariant.
-- Fragile read-modify-write
SELECT balance FROM account WHERE id = 42;
-- application computes a new value
UPDATE account SET balance = :new_balance WHERE id = 42;

-- Prefer one atomic statement when the rule is local to the row
UPDATE account
SET balance = balance - :amount
WHERE id = 42
  AND balance >= :amount;

The second statement turns the predicate and mutation into one database operation. It narrows the isolation problem instead of asking a broad isolation level to repair an avoidable race.

03

The isolation spectrum is not one-dimensional

SQL names describe intent, but engines reach that intent through different mechanisms. The most important distinction is often not the name; it is whether the snapshot is refreshed per statement or held for the transaction, and whether dangerous histories are blocked or detected.

00

Non-transactional writes

Atomicity may stop at one operation. Cross-record invariants belong to the application or data model.

01

Read Uncommitted

The SQL standard permits dirty reads, though MVCC engines may implement it as Read Committed.

02

Read Committed

Only committed versions are visible. A new statement can see a newer world than the previous statement.

03

Repeatable Read

Often a transaction-level snapshot. The name does not guarantee full serializability or identical behavior across products.

04

Snapshot Isolation

Readers use a stable snapshot; write-write conflicts are controlled. Write skew can remain.

05

Serializable

Committed transactions are equivalent to some serial order. Waiting or aborts are part of the contract.

Statement snapshot versus transaction snapshot

“Serializable” is a semantic guarantee. “Snapshot” is a visibility mechanism. One does not imply the other.

04

MVCC removes read/write blocking, not coordination

Multi-Version Concurrency Control lets readers find a visible committed version while writers create a new one. This is excellent for concurrency, but old versions cannot enforce a future business invariant.

Locks and MVCC solve different parts of the problem:

  • MVCC visibility chooses which committed version a reader can see.
  • Row or key locks coordinate writers that touch the same protected object.
  • Predicate protection covers facts such as “no row exists in this range.”
  • Serialization detection observes dependency patterns and aborts a transaction before an impossible history commits.
BEGIN;
SELECT balance
FROM account
WHERE id = 42
FOR UPDATE;

-- The row now represents the coordination point.
UPDATE account SET balance = balance - 50 WHERE id = 42;
COMMIT;

SELECT FOR UPDATE expresses an intention to modify the selected rows. It cannot lock a row that does not exist, and it does not automatically protect an arbitrary aggregate or predicate.

05

Write skew: when every row is valid and the database is wrong

The doctor's on-call example is the canonical demonstration. The invariant says at least one doctor must remain on call. Alice and Bob each see two doctors, then update different rows. There is no write-write conflict, yet both can leave.

Four ways to preserve the invariant

  1. Serializable plus retry.Let the engine detect the dangerous dependency cycle. Treat serialization failure as a normal control-flow result.
  2. Lock a shared invariant row.Make both transactions contend on one shift record before changing doctor assignments.
  3. Encode the rule in the model.Normalization, a unique structure, or an assertion can move correctness closer to declarative data integrity.
  4. Use one atomic statement.When possible, combine the checked predicate and mutation so they share one statement snapshot and write operation.

06

Same SQL name, different operational contract

Portable SQL syntax does not make concurrency behavior portable. Defaults, snapshots, lock timing, retry errors, and even the meaning behind a level name vary.

SystemCommon defaultStable transaction snapshotSerializable mechanism
PostgreSQLRead CommittedRepeatable ReadSSI detects dangerous structures; applications retry serialization failures.
Oracle DatabaseRead CommittedSERIALIZABLE uses snapshot semanticsIts historical naming needs care: write conflicts can raise ORA-08177, while the guarantee is not identical to PostgreSQL SSI.
YugabyteDBVaries by API/version; Serializable is centralDistributed hybrid-time snapshotsConflict detection and transparent internal restarts where safe; clients must still handle retryable failures.
MongoDBOperation-level atomicity; configurable concernsSnapshot read concern in transactionsTransactions, data modeling, unique indexes, and assertions-like coordination solve different invariant scopes.

Why Oracle “serializable” needs qualification

Oracle's level historically called SERIALIZABLE gives a transaction-consistent snapshot and rejects some conflicting updates with ORA-08177. Snapshot isolation is stronger than ordinary Read Committed, but a stable snapshot alone is not the general definition of serializability. The distinction matters for write skew and predicate-based invariants.

Why distributed SQL makes retries visible

A distributed database coordinates keys across nodes and clocks. Waiting forever would preserve correctness but destroy availability and latency. Optimistic conflict detection, transaction restarts, and explicit retry errors are therefore not defects; they are part of the isolation API.

07

Design patterns that make isolation explicit

Pattern 01

Atomic conditional DML

Put the precondition in the WHERE clause and verify the affected-row count.

UPDATE inventory
SET quantity = quantity - :requested
WHERE sku = :sku
  AND quantity >= :requested;
Pattern 02

Optimistic version check

Reject a stale decision without holding a lock during application work.

UPDATE document
SET body = :body, version = version + 1
WHERE id = :id
  AND version = :version_read;
Pattern 03

Pessimistic coordination

Lock the row that truly represents the invariant, in a stable order, and keep the transaction short.

SELECT id FROM shift
WHERE id = :shift_id
FOR UPDATE;
Pattern 04

Serializable retry loop

Retry the entire transaction with bounded exponential backoff and idempotent external effects.

for attempt in retry_budget:
    try:
        begin_serializable()
        apply_business_transaction()
        commit()
        break
    except SerializationFailure:
        rollback()
        backoff(attempt)

The retry boundary is the transaction boundary

Do not retry only the failed statement: its inputs may have come from a now-invalid snapshot. Re-run every read and decision in the transaction. Keep emails, payments, and messages idempotent or publish them through an outbox committed with the data.

08

A decision guide for real applications

1

Can the invariant be expressed by a unique, exclusion, check, or foreign-key constraint?

Use the constraint first.
2

Can the check and change be one conditional SQL statement?

Prefer atomic DML.
3

Does every conflicting transaction touch one known row or key?

Lock that coordination point.
4

Does the invariant span an open predicate, aggregate, or changing set?

Use Serializable and retry, or remodel the invariant.
5

Are retries expensive because transactions include network calls?

Shorten the transaction; use idempotency and an outbox.

Test the history, not just the happy path

  • Open two independent sessions and pause them at controlled points.
  • Record values read, locks waited on, errors returned, and final committed state.
  • Test the configured default and every level the application explicitly requests.
  • Repeat after database upgrades and driver or connection-pool changes.
  • Monitor serialization failures and deadlocks as expected concurrency signals, not generic server faults.

The compact rulebook

  1. Write the invariant in one sentence.
  2. Identify all rows and missing rows that influence it.
  3. Prefer declarative constraints and atomic SQL.
  4. Choose the snapshot and coordination mechanism deliberately.
  5. Assume aborts can happen; design a correct retry boundary.
  6. Verify behavior on the actual engine and version.

09

Presentation, videos, and source articles

This AI-generated minibook synthesizes the following original material from Franck Pachot's blog archive. The links remain part of the text so each claim can be followed back to its experiment and discussion.

The original 13-part series

  1. IntroductionWhy isolation is an application concern.
  2. Characteristics and use casesHow guarantees map to workloads.
  3. Main conceptsVocabulary for histories and concurrency.
  4. SerializableThe semantic target.
  5. Read OnlyConsistent reporting and snapshots.
  6. Snapshot IsolationStable visibility and its limits.
  7. Repeatable ReadNames versus implementations.
  8. Cursor StabilityLock-oriented history.
  9. Read CommittedStatement snapshots and practical defaults.
  10. Non-Transactional WritesWhen atomic scope is smaller.
  11. Read UncommittedThe weakest standard level.
  12. To go furtherModels beyond the standard phenomena.
  13. Explicit locking with SELECT FOR UPDATEMaking write intention visible.

Applied studies

Video channel

Recordings and short technical explanations are collected on Franck Pachot's YouTube channel. The presentation link above is the canonical visual companion currently verified for this minibook.