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.
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.
The transaction does not expose half of its work.
The schema and application define what “valid” means.
Readers and writers observe a constrained ordering.
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?
-- 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.
Non-transactional writes
Atomicity may stop at one operation. Cross-record invariants belong to the application or data model.
Read Uncommitted
The SQL standard permits dirty reads, though MVCC engines may implement it as Read Committed.
Read Committed
Only committed versions are visible. A new statement can see a newer world than the previous statement.
Repeatable Read
Often a transaction-level snapshot. The name does not guarantee full serializability or identical behavior across products.
Snapshot Isolation
Readers use a stable snapshot; write-write conflicts are controlled. Write skew can remain.
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
- Serializable plus retry.Let the engine detect the dangerous dependency cycle. Treat serialization failure as a normal control-flow result.
- Lock a shared invariant row.Make both transactions contend on one shift record before changing doctor assignments.
- Encode the rule in the model.Normalization, a unique structure, or an assertion can move correctness closer to declarative data integrity.
- 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.
SERIALIZABLE uses snapshot semanticsIts historical naming needs care: write conflicts can raise ORA-08177, while the guarantee is not identical to PostgreSQL SSI.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
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;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;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;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
Can the invariant be expressed by a unique, exclusion, check, or foreign-key constraint?
Use the constraint first.Can the check and change be one conditional SQL statement?
Prefer atomic DML.Does every conflicting transaction touch one known row or key?
Lock that coordination point.Does the invariant span an open predicate, aggregate, or changing set?
Use Serializable and retry, or remodel the invariant.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
- Write the invariant in one sentence.
- Identify all rows and missing rows that influence it.
- Prefer declarative constraints and atomic SQL.
- Choose the snapshot and coordination mechanism deliberately.
- Assume aborts can happen; design a correct retry boundary.
- 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.
Isolation Levels and MVCC in SQL Databases: A Technical Comparative Study
The visual companion to this guide, covering the comparative model and product behavior.
View presentation →The original 13-part series
- IntroductionWhy isolation is an application concern.
- Characteristics and use casesHow guarantees map to workloads.
- Main conceptsVocabulary for histories and concurrency.
- SerializableThe semantic target.
- Read OnlyConsistent reporting and snapshots.
- Snapshot IsolationStable visibility and its limits.
- Repeatable ReadNames versus implementations.
- Cursor StabilityLock-oriented history.
- Read CommittedStatement snapshots and practical defaults.
- Non-Transactional WritesWhen atomic scope is smaller.
- Read UncommittedThe weakest standard level.
- To go furtherModels beyond the standard phenomena.
- 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.