| |

SQL 71 🛢️ Transaction Isolation Levels and Concurrency Anomalies

⏱️ Estimated read: 22 min | |

A database serves many clients at once. Two customers buy the last item in stock. A report runs while an order is being processed. An analyst sums account balances while a transfer moves money between them. These concurrent operations are the normal state of any production database, and without careful control they produce results that no single user ever intended. Transaction isolation levels exist to manage this tension. They define exactly how much one transaction can see of another’s uncommitted work, and what anomalies the database will prevent versus allow. The choice of isolation level is one of the highest-leverage decisions a database developer makes, because it directly trades correctness against concurrency, and the wrong choice produces bugs that appear only under load, only in production, and only intermittently.

Key point: Isolation levels are not a binary choice between “safe” and “fast.” They are a spectrum of specific guarantees, each preventing a precisely defined class of anomaly, and the correct level is the lowest one that your application’s correctness actually requires.


Why isolation levels exist

The concurrent access problem. A database is a shared resource. Multiple transactions read and write the same rows, often within milliseconds of each other. Without isolation rules, one transaction could read another’s half-finished changes, make a decision based on them, and produce a result that never logically existed. The classic example: two customers both see one item in stock, both place an order, and the warehouse has negative inventory . Isolation levels are the mechanism that prevents this.

The performance problem. The simplest way to prevent all concurrency anomalies is to serialize everything: run one transaction at a time, fully complete it, then start the next. This is perfectly correct and completely unusable for any real system. Isolation levels exist so that most transactions can run concurrently, and only the specific conflicts that would cause anomalies are blocked or detected. The lower the isolation level, the more concurrency the system allows, and the more anomalies the application must tolerate or handle itself .

The application logic problem. Many transactions are not single statements. They read a value, compute something in application code, then write a result back. These multi-step transactions are where isolation matters most. A single-statement UPDATE inventory SET qty = qty - 1 WHERE item_id = 42 is atomic and safe by itself. But SELECT qty FROM inventory WHERE item_id = 42 followed by application logic followed by an UPDATE creates a window where another transaction can change the data between the read and the write . Isolation levels determine how wide that window is.

The consistency-per-operation trade-off. A stronger isolation level gives each transaction a more consistent view of the database, but at the cost of more blocking, more retries, or more overhead. Read Uncommitted gives no consistency but maximum concurrency. Serializable gives full consistency but can force transactions to wait or abort. Every intermediate level is a specific point on this curve, and no single level is correct for all workloads .

The different implementations problem. The SQL standard defines four isolation levels by the anomalies they prevent, but it explicitly allows databases to implement stronger guarantees than the minimum. PostgreSQL’s Repeatable Read, for example, prevents phantom reads even though the standard permits them at that level . MySQL’s default Repeatable Read also prevents phantoms in many cases through gap locking . This means the same named isolation level can behave differently on different databases, and developers must understand their specific database’s behavior, not just the standard’s definitions .

The detection-vs-prevention trade-off. There are two fundamentally different strategies for handling concurrency conflicts. Pessimistic approaches prevent conflicts by locking data before it is used, forcing other transactions to wait. Optimistic approaches let transactions proceed without blocking, then detect at commit time whether a conflict occurred, aborting one transaction if necessary. Different isolation levels on different databases use different combinations of these strategies, and the choice affects deadlock rates, retry logic, and throughput characteristics .


a. The four SQL standard isolation levels

The SQL:1992 standard defines four isolation levels, each specified by which of three concurrency anomalies it must prevent. In increasing order of strictness: Read Uncommitted, Read Committed, Repeatable Read, and Serializable .

Read Uncommitted

The lowest level. A transaction can read data written by another transaction that has not yet committed. This is called a dirty read. If the writing transaction rolls back, the reading transaction has acted on data that never officially existed .

-- Transaction A (not yet committed)
UPDATE accounts SET balance = 0 WHERE account_id = 1;

-- Transaction B at READ UNCOMMITTED
SELECT balance FROM accounts WHERE account_id = 1;
-- Returns 0, even though A may still roll back.
-- If A rolls back, B made a decision on phantom data.

Read Uncommitted is rarely appropriate for transactional workloads. Its legitimate use is approximate reporting: a dashboard showing rough order counts where being off by a few is acceptable, and where blocking the production write path for exact consistency is not .

Read Committed

The most common default (SQL Server, Oracle, PostgreSQL). A transaction only sees data committed before the statement began. Dirty reads are impossible. However, successive reads within the same transaction can see different data if another transaction commits between them, producing non-repeatable reads and phantom reads .

-- Transaction B at READ COMMITTED
SELECT balance FROM accounts WHERE account_id = 1;  -- returns 1000

-- Transaction A commits: UPDATE accounts SET balance = 500 WHERE account_id = 1;

SELECT balance FROM accounts WHERE account_id = 1;  -- returns 500
-- Same transaction, same query, different result. Non-repeatable read.

Read Committed is the workhorse level. It prevents the worst anomaly (dirty reads) while allowing high concurrency. Most applications that do not perform multi-step read-then-write logic are safe here.

Repeatable Read

Rows read by a transaction cannot be changed by other transactions until the reading transaction completes. Non-repeatable reads are prevented. The transaction sees a stable snapshot of the rows it has read. However, new rows that match the query filter can still appear, so phantom reads remain possible in the standard definition .

-- Transaction B at REPEATABLE READ
SELECT balance FROM accounts WHERE account_id = 1;  -- returns 1000

-- Transaction A attempts: UPDATE accounts SET balance = 500 WHERE account_id = 1;
-- A blocks (or is aborted at commit, depending on implementation).

SELECT balance FROM accounts WHERE account_id = 1;  -- still returns 1000

On PostgreSQL, Repeatable Read uses snapshot isolation: the entire transaction sees the database as of the transaction’s start, and a concurrent update that commits after the transaction began causes the transaction to abort with a serialization error rather than silently losing the update .

Serializable

The strictest level. The result of running transactions concurrently must be equivalent to running them in some serial order, one after another. All three anomalies are prevented. In lock-based implementations, range locks prevent inserts into queried ranges. In snapshot-based implementations like PostgreSQL, predicate locks track dependencies and abort transactions when a serialization anomaly would occur .

-- PostgreSQL SERIALIZABLE example
-- Transaction A: SELECT SUM(value) FROM mytab WHERE class = 1;  -- result 30
-- Transaction A: INSERT INTO mytab (class, value) VALUES (2, 30);

-- Transaction B: SELECT SUM(value) FROM mytab WHERE class = 2;  -- result 300
-- Transaction B: INSERT INTO mytab (class, value) VALUES (1, 300);

-- Both commit. No serial order produces both results.
-- PostgreSQL aborts one:
-- ERROR: could not serialize access due to read/write dependencies

The cost is real: range locks cause blocking and deadlocks, while predicate-lock implementations add monitoring overhead and force application retry logic .


b. The three concurrency anomalies

The anomalies are the concrete failures that isolation levels exist to prevent. Each is a specific pattern of concurrent access that produces a result inconsistent with any serial execution.

Dirty read

A transaction reads data written by another transaction that has not committed. If the writer rolls back, the reader has seen data that never existed. This is the most severe anomaly because it can produce decisions based on entirely fictional state .

-- T1: UPDATE products SET price = 0 WHERE id = 1;  -- not committed
-- T2 at READ UNCOMMITTED: SELECT price FROM products WHERE id = 1;  -- reads 0
-- T1: ROLLBACK;  -- price is still the original value
-- T2 made pricing decisions based on a price that was never real.

Non-repeatable read

A transaction reads the same row twice and gets different values because another transaction committed a change between the reads. The reading transaction’s view of the row is not stable across its own lifetime .

-- T1: SELECT balance FROM accounts WHERE id = 1;  -- 1000
-- T2: UPDATE accounts SET balance = 500 WHERE id = 1; COMMIT;
-- T1: SELECT balance FROM accounts WHERE id = 1;  -- 500
-- T1 computed a budget based on 1000, then saw 500. Inconsistent.

Phantom read

A transaction re-executes a query and sees new rows that were not present before, because another transaction inserted rows matching the query’s filter. The new rows are “phantoms” — they appeared between two executions of the same query within one transaction .

-- T1: SELECT COUNT(*) FROM orders WHERE status = 'pending';  -- 10
-- T2: INSERT INTO orders (status) VALUES ('pending'); COMMIT;
-- T1: SELECT COUNT(*) FROM orders WHERE status = 'pending';  -- 11
-- T1's count changed without T1 doing anything.

Phantom reads are harder to prevent than non-repeatable reads because they require locking a range of values (or detecting equivalent read-write dependencies), not just the rows that were actually read .

Serialization anomaly

A serialization anomaly is not one of the three SQL-standard anomalies but the general condition that Serializable prevents: the concurrent execution of a set of transactions produces a result that no serial (one-at-a-time) execution of the same transactions could produce. The PostgreSQL example above (each transaction computing a sum and inserting based on it) is a serialization anomaly that Read Committed and even Repeatable Read would allow .


c. Implementation strategies: locking vs. versioning

The SQL standard defines behavior. Databases implement it differently. The two dominant strategies are lock-based and version-based concurrency control, and understanding which one your database uses is essential for predicting performance and failure modes.

Lock-based isolation

The database acquires shared locks on rows that a transaction reads and exclusive locks on rows it writes. Shared locks allow other readers but block writers. Exclusive locks block both. The isolation level determines how long read locks are held .

-- READ COMMITTED locking behavior (SQL Server default, before RCSI)
-- T1: SELECT * FROM products WHERE id = 1;
--     Takes shared lock, reads row, releases lock immediately.
--     Another transaction can update the row before T1 reads it again.

-- REPEATABLE READ locking behavior
-- T1: SELECT * FROM products WHERE id = 1;
--     Takes shared lock and holds it until T1 commits or rolls back.
--     Other transactions cannot update this row.

Lock-based systems are predictable but prone to blocking and deadlocks. A long-running transaction at Repeatable Read can block writers for the entire duration of the transaction .

Row-versioning (MVCC) isolation

The database maintains multiple versions of each row. Readers see the version that was current when their statement (or transaction) started. Writers create new versions. Readers never block writers, and writers never block readers .

-- PostgreSQL MVCC at READ COMMITTED
-- T1: SELECT * FROM products WHERE id = 1;
--     Sees the version committed before the statement started.

-- T2: UPDATE products SET price = 99 WHERE id = 1; COMMIT;
--     Creates a new row version. Old version remains.

-- T1: SELECT * FROM products WHERE id = 1;
--     Sees the new version (Read Committed takes fresh snapshot per statement).

PostgreSQL’s Repeatable Read and Serializable use MVCC. MySQL’s InnoDB uses MVCC for consistent reads at all isolation levels, with locking for writes . SQL Server offers Snapshot isolation as a versioning alternative to its default lock-based Read Committed .

The trade-off is storage and cleanup. Old row versions must be retained until no transaction needs them. Under long-running transactions or high write rates, version storage grows, and vacuum processes must clean up obsolete versions .

Hybrid approaches

MySQL’s InnoDB default is Repeatable Read with MVCC for plain SELECT statements and gap/next-key locks for locking reads and writes. A plain SELECT reads a snapshot; a SELECT ... FOR UPDATE locks index records and gaps to prevent phantom inserts . This hybrid gives consistent reads without blocking, while still preventing phantoms for locking operations.

SQL Server’s Read Committed Snapshot Isolation (RCSI) changes the default Read Committed to use row versioning. Every statement reads a snapshot as of the statement’s start, without taking shared locks. Writers still take exclusive locks. This eliminates reader-writer blocking while keeping the Read Committed semantics .


Complete Example Session

-- ============================================
-- PART 1: SETUP — THE SCENARIO
-- ============================================

-- A simple inventory table.
CREATE TABLE inventory (
    item_id     INT PRIMARY KEY,
    item_name   VARCHAR(100),
    quantity    INT NOT NULL CHECK (quantity >= 0)
);

INSERT INTO inventory VALUES (1, 'Widget', 10);
INSERT INTO inventory VALUES (2, 'Gadget', 5);

-- The business rule: quantity must never go below zero.
-- Two concurrent transactions will test this rule.


-- ============================================
-- PART 2: THE NORMAL CASE — NO CONCURRENCY
-- ============================================

-- Transaction A: buy 3 widgets. Safe.
BEGIN;
SELECT quantity FROM inventory WHERE item_id = 1;  -- 10
-- Application logic: 10 >= 3, proceed.
UPDATE inventory SET quantity = quantity - 3 WHERE item_id = 1;
-- quantity is now 7.
COMMIT;

-- Transaction B: buy 8 widgets. Safe, single transaction.
BEGIN;
SELECT quantity FROM inventory WHERE item_id = 1;  -- 7
-- Application logic: 7 < 8, reject.
ROLLBACK;
-- No change. Correct behavior.


-- ============================================
-- PART 3: THE RACE CONDITION — READ COMMITTED
-- ============================================

-- Transaction A and Transaction B both start.
-- Both read the same initial quantity.

-- T-A:
BEGIN;
SELECT quantity FROM inventory WHERE item_id = 1;  -- 10

-- T-B (concurrent):
BEGIN;
SELECT quantity FROM inventory WHERE item_id = 1;  -- 10 (same value!)

-- T-A: Application logic: 10 >= 8, proceed.
UPDATE inventory SET quantity = quantity - 8 WHERE item_id = 1;
COMMIT;
-- quantity is now 2.

-- T-B: Application logic: 10 >= 8, proceed.
UPDATE inventory SET quantity = quantity - 8 WHERE item_id = 1;
-- This UPDATE runs against the current value (2):
-- 2 - 8 = -6, which violates the CHECK constraint.
-- The CHECK constraint saves us here.

-- Without the CHECK constraint:
-- quantity would be -6. Oversold by 6 units.
-- This is the classic lost update / overselling problem.


-- ============================================
-- PART 4: PREVENTING THE RACE — REPEATABLE READ
-- ============================================

-- PostgreSQL Repeatable Read (snapshot isolation)

-- T-A:
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT quantity FROM inventory WHERE item_id = 1;  -- 10

-- T-B (concurrent):
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT quantity FROM inventory WHERE item_id = 1;  -- 10

-- T-A: Application logic: 10 >= 8, proceed.
UPDATE inventory SET quantity = quantity - 8 WHERE item_id = 1;
COMMIT;
-- quantity is now 2.

-- T-B: Application logic: 10 >= 8, proceed.
UPDATE inventory SET quantity = quantity - 8 WHERE item_id = 1;
-- PostgreSQL detects that T-A committed after T-B's snapshot.
-- ERROR: could not serialize access due to concurrent update
-- T-B must ROLLBACK and retry.
-- On retry, T-B reads quantity = 2, rejects the order.
-- No overselling. Correct behavior.


-- ============================================
-- PART 5: PREVENTING THE RACE — SERIALIZABLE
-- ============================================

-- PostgreSQL Serializable

-- T-A:
BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT quantity FROM inventory WHERE item_id = 1;  -- 10

-- T-B:
BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT quantity FROM inventory WHERE item_id = 1;  -- 10

-- T-A:
UPDATE inventory SET quantity = quantity - 8 WHERE item_id = 1;
COMMIT;
-- quantity is now 2.

-- T-B:
UPDATE inventory SET quantity = quantity - 8 WHERE item_id = 1;
-- ERROR: could not serialize access due to read/write dependencies
-- T-B must ROLLBACK and retry.
-- Same outcome as Repeatable Read in this case, but Serializable
-- also protects against phantom-based anomalies that Repeatable Read
-- would allow.


-- ============================================
-- PART 6: USING EXPLICIT LOCKS INSTEAD
-- ============================================

-- Sometimes the right answer is not a higher isolation level
-- but an explicit lock at the application's chokepoint.

-- T-A:
BEGIN;
SELECT quantity FROM inventory WHERE item_id = 1 FOR UPDATE;
-- Row is locked. T-B will block here.
-- quantity = 10. Proceed.
UPDATE inventory SET quantity = quantity - 8 WHERE item_id = 1;
COMMIT;
-- Lock released. quantity = 2.

-- T-B (was blocking on FOR UPDATE):
SELECT quantity FROM inventory WHERE item_id = 1 FOR UPDATE;
-- Now sees quantity = 2. Application logic: 2 < 8, reject.
ROLLBACK;

-- FOR UPDATE serializes access to this row without raising
-- the entire transaction's isolation level. This is often the
-- more surgical solution.


-- ============================================
-- PART 7: THE PHANTOM PROBLEM
-- ============================================

-- A different scenario where row locks are insufficient.

-- A reporting transaction sums pending orders.
-- T-Report at REPEATABLE READ:
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT SUM(amount) FROM orders WHERE status = 'pending';
-- Result: 5000

-- T-Insert commits a new pending order:
INSERT INTO orders (status, amount) VALUES ('pending', 1000);
COMMIT;

-- T-Report:
SELECT SUM(amount) FROM orders WHERE status = 'pending';
-- Result: 6000 in standard Repeatable Read (phantom row).
-- Result: still 5000 in PostgreSQL Repeatable Read (snapshot).
-- Result: still 5000 in MySQL Repeatable Read for plain SELECT (MVCC snapshot).


-- ============================================
-- PART 8: THE SERIALIZABLE PHANTOM GUARANTEE
-- ============================================

-- PostgreSQL Serializable

-- T-Report:
BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT SUM(amount) FROM orders WHERE status = 'pending';
-- Result: 5000. Predicate lock tracks this read.

-- T-Insert:
BEGIN ISOLATION LEVEL SERIALIZABLE;
INSERT INTO orders (status, amount) VALUES ('pending', 1000);
-- Insert proceeds (predicate locks don't block).

-- T-Insert: COMMIT;
-- T-Report: SELECT SUM(amount) FROM orders WHERE status = 'pending';
-- Result: still 5000 (snapshot).
-- T-Report: COMMIT;
-- ERROR: could not serialize access due to read/write dependencies
-- T-Report must retry. On retry, it sees the new order.
-- The serial order (T-Report before T-Insert) is not possible
-- because T-Insert's row was not visible to T-Report's sum.


-- ============================================
-- PART 9: SETTING ISOLATION LEVELS
-- ============================================

-- PostgreSQL: per-transaction
BEGIN ISOLATION LEVEL READ COMMITTED;
-- or
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

-- MySQL: session or global
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;

-- SQL Server: per-connection
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;

-- Note: MySQL does not allow SET TRANSACTION ISOLATION LEVEL
-- in the middle of an active transaction .


-- ============================================
-- PART 10: THE RETRY PATTERN
-- ============================================

-- Serializable transactions may abort. Applications must retry.

-- Pseudocode pattern:
--
-- while true:
--     BEGIN ISOLATION LEVEL SERIALIZABLE;
--     try:
--         ... application logic ...
--         COMMIT;
--         break;  -- success
--     catch SerializationFailure:
--         ROLLBACK;
--         continue;  -- retry with fresh snapshot
--
-- The retry must re-read all data from the beginning.
-- The second attempt sees the committed state that caused
-- the first attempt to fail, so it can proceed correctly.
-- Read-only transactions never need retry in PostgreSQL .

The ten parts built from a simple inventory scenario through the three anomalies, three isolation levels, explicit locking, phantom reads, and the retry pattern that Serializable requires. The same database table and business rule thread through every part, showing how each isolation level changes the outcome of the identical concurrent operations.


Quick Reference

The Four Standard Isolation Levels

LevelDirty ReadNon-Repeatable ReadPhantom Read
Read UncommittedPossiblePossiblePossible
Read CommittedNot possiblePossiblePossible
Repeatable ReadNot possibleNot possiblePossible*
SerializableNot possibleNot possibleNot possible

*PostgreSQL and MySQL (MVCC) prevent phantoms at Repeatable Read for plain SELECT.

Default Isolation Levels by Database

DatabaseDefaultRow Versioning Option
PostgreSQLRead CommittedRepeatable Read = Snapshot Isolation
MySQL (InnoDB)Repeatable ReadMVCC for consistent reads
SQL ServerRead CommittedSnapshot, RCSI
OracleRead CommittedSerializable (snapshot-based)

Lock Duration by Level

LevelRead Lock HeldWrite Lock Held
Read UncommittedNot takenUntil end of transaction
Read CommittedReleased immediatelyUntil end of transaction
Repeatable ReadUntil end of transactionUntil end of transaction
SerializableUntil end of transaction (range)Until end of transaction

Anomaly Summary

AnomalyDefinitionPrevented By
Dirty readReading uncommitted dataRead Committed+
Non-repeatable readRow changes between readsRepeatable Read+
Phantom readNew rows appear in re-querySerializable (standard)
Serialization anomalyNon-serializable outcomeSerializable

Best Practices

✅ Do This:

-- Use the lowest isolation level that satisfies correctness.
BEGIN ISOLATION LEVEL READ COMMITTED;  -- ✅ Default for most work

-- Use explicit FOR UPDATE for specific chokepoints.
SELECT * FROM inventory WHERE item_id = 1 FOR UPDATE;  -- ✅

-- Implement retry logic for Serializable.
while (serialization_failure) retry_transaction();  -- ✅

-- Set isolation level explicitly in code.
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;  -- ✅

-- Keep transactions short to minimize lock duration.
BEGIN; UPDATE ...; COMMIT;  -- ✅

-- Test concurrency behavior under load.
-- Write integration tests with parallel transactions.  -- ✅

❌ Don’t Do This:

-- Don't use Serializable everywhere.
BEGIN ISOLATION LEVEL SERIALIZABLE;  -- ❌ Unnecessary blocking

-- Don't read-then-write without protection.
SELECT qty; /* compute */ UPDATE qty;  -- ❌ Race condition

-- Don't hold transactions open for user input.
BEGIN; /* wait for HTTP request */ COMMIT;  -- ❌ Locks held for seconds

-- Don't ignore serialization failures.
catch (SerializationFailure) { /* log and continue */ }  -- ❌ Must retry

-- Don't assume isolation level names mean the same everywhere.
-- PostgreSQL Repeatable Read ≠ MySQL Repeatable Read  -- ❌ Verify behavior

-- Don't mix locking and non-locking reads carelessly.
SELECT ...; SELECT ... FOR UPDATE;  -- ❌ Inconsistent snapshots

Common Pitfalls

PitfallWhy It HappensFix
Overselling inventoryRead-then-write without lockingUse FOR UPDATE or Serializable
Lost updateTwo transactions update same row from stale readRepeatable Read or explicit lock
Phantom rows in reportsRe-query sees new rowsSerializable or snapshot isolation
Deadlocks from range locksSerializable locks large rangesRetry logic; shorter transactions
Version store bloatLong transactions hold old versionsKeep transactions short
Unexpected abort stormsSerializable detects dependenciesRetry with backoff; consider lower level
Isolation level ignoredSET inside transaction (MySQL)Set before BEGIN
Cross-database portabilitySame name, different behaviorTest on target database

Real-World Examples

1. E-Commerce Inventory Reservation

BEGIN;
SELECT quantity FROM inventory WHERE item_id = 42 FOR UPDATE;
-- If quantity >= requested, UPDATE and COMMIT.
-- If not, ROLLBACK.

2. Financial Transfer with Serializable

BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT balance FROM accounts WHERE id = 1;
-- Compute after deducting amount.
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;  -- May abort; retry on serialization failure.

3. Reporting Snapshot

BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT region, SUM(sales) FROM orders
WHERE created_at >= '2026-01-01' GROUP BY region;
-- All queries in this transaction see the same snapshot.
COMMIT;

4. MySQL Session Isolation

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- Applies to all subsequent transactions in this connection.

5. SQL Server Snapshot Isolation

ALTER DATABASE MyDb SET ALLOW_SNAPSHOT_ISOLATION ON;
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
-- Readers never block writers.

6. PostgreSQL Retry Loop

-- Application-level pseudocode
for attempt in range(max_retries):
    try:
        cursor.execute("BEGIN ISOLATION LEVEL SERIALIZABLE")
        # ... work ...
        cursor.execute("COMMIT")
        break
    except SerializationFailure:
        cursor.execute("ROLLBACK")

7. Batch Insert with Gap Locking

BEGIN;
SELECT COUNT(*) FROM orders WHERE status = 'pending' FOR UPDATE;
-- Range locked. No new pending orders can be inserted.
-- Prevents phantom in the count.
COMMIT;

8. Deadlock from Range Locks

-- T1: SELECT * FROM t WHERE id BETWEEN 1 AND 10 FOR UPDATE;
-- T2: SELECT * FROM t WHERE id BETWEEN 5 AND 15 FOR UPDATE;
-- Both block. Deadlock detected. One rolls back.

9. Read Uncommitted Dashboard

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT COUNT(*) FROM orders WHERE status = 'processing';
-- Approximate count. No locks. May be slightly off.

10. Detecting Isolation Level in PostgreSQL

SHOW transaction_isolation;
-- current_setting('transaction_isolation')

Visual

The Anomaly Spectrum

┌──────────────────────────────────────────────────────────────┐
│  ANOMALY SPECTRUM                                            │
│                                                              │
│  READ UNCOMMITTED                                            │
│  ├── Dirty read        ✓ allowed                            │
│  ├── Non-repeatable    ✓ allowed                            │
│  └── Phantom           ✓ allowed                            │
│                                                              │
│  READ COMMITTED                                              │
│  ├── Dirty read        ✗ prevented                          │
│  ├── Non-repeatable    ✓ allowed                            │
│  └── Phantom           ✓ allowed                            │
│                                                              │
│  REPEATABLE READ                                             │
│  ├── Dirty read        ✗ prevented                          │
│  ├── Non-repeatable    ✗ prevented                          │
│  └── Phantom           ✓ allowed (standard)                 │
│                                                              │
│  SERIALIZABLE                                                │
│  └── All anomalies     ✗ prevented                          │
│                                                              │
└──────────────────────────────────────────────────────────────┘

The Overselling Race

┌──────────────────────────────────────────────────────────────┐
│  THE OVERSELLING RACE                                        │
│                                                              │
│  Transaction A              Transaction B                    │
│  ───────────────            ───────────────                  │
│                                                              │
│  SELECT qty = 10                                            │
│                             SELECT qty = 10                 │
│                             (same snapshot)                 │
│                                                              │
│  UPDATE qty = 10 - 8 = 2                                   │
│  COMMIT                                                     │
│                             UPDATE qty = 10 - 8 = 2         │
│                             (against current value)         │
│                             COMMIT                          │
│                                                              │
│  RESULT: qty = -6? NO — CHECK constraint saves us.         │
│  WITHOUT CONSTRAINT: Oversold by 6 units.                   │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Locking vs. Versioning

┌──────────────────────────────────────────────────────────────┐
│  LOCKING vs. VERSIONING                                      │
│                                                              │
│  LOCK-BASED (Pessimistic)                                   │
│  ┌────────┐    ┌────────┐    ┌────────┐                    │
│  │ Reader │───▶│  Lock  │───▶│ Writer │                    │
│  │        │    │ (hold) │    │ waits  │                    │
│  └────────┘    └────────┘    └────────┘                    │
│  Reader blocks writer. Predictable. Deadlock-prone.         │
│                                                              │
│  VERSION-BASED (Optimistic)                                  │
│  ┌────────┐    ┌─────────────────┐    ┌────────┐           │
│  │ Reader │───▶│ Version Store   │    │ Writer │           │
│  │        │    │ (old versions)  │◀───│ creates│           │
│  └────────┘    └─────────────────┘    │ new ver│           │
│  Reader never blocks. Writer never blocks reader.           │
│  Storage overhead. Cleanup needed.                           │
│                                                              │
└──────────────────────────────────────────────────────────────┘

The Retry Pattern

┌──────────────────────────────────────────────────────────────┐
│  SERIALIZABLE RETRY PATTERN                                  │
│                                                              │
│  ┌─────────┐                                                │
│  │  BEGIN  │                                                │
│  │SERIALIZ-│                                                │
│  │  ABLE   │                                                │
│  └────┬────┘                                                │
│       │                                                     │
│       ▼                                                     │
│  ┌─────────┐    ┌──────────────┐    ┌─────────┐           │
│  │  Read   │───▶│  Application │───▶│  Write  │           │
│  │  Data   │    │    Logic     │    │  Result │           │
│  └─────────┘    └──────────────┘    └────┬────┘           │
│                                           │                 │
│                                           ▼                 │
│                                      ┌─────────┐           │
│                                      │ COMMIT  │           │
│                                      └────┬────┘           │
│                                           │                 │
│              ┌────────────────────────────┴────┐            │
│              │                                 │            │
│              ▼                                 ▼            │
│         ┌─────────┐                      ┌──────────┐      │
│         │ SUCCESS │                      │SERIALIZ- │      │
│         │         │                      │ ATION    │      │
│         └─────────┘                      │ FAILURE  │      │
│                                          └────┬─────┘      │
│                                               │             │
│                                               ▼             │
│                                          ┌─────────┐       │
│                                          │ROLLBACK │       │
│                                          │ & RETRY │───────┐│
│                                          └─────────┘       ││
│                                                            ││
│◀───────────────────────────────────────────────────────────┘│
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
IsolationThe “I” in ACID; controls visibility between concurrent transactions
Dirty readReading uncommitted data; prevented by Read Committed+
Non-repeatable readRow changes between reads; prevented by Repeatable Read+
Phantom readNew rows appear on re-query; prevented by Serializable (standard)
Read UncommittedLowest level; allows all anomalies; approximate reporting only
Read CommittedDefault for most databases; prevents dirty reads
Repeatable ReadStable snapshot of read rows; implementation varies by database
SerializableFull serial equivalence; retry required on conflicts
MVCCMulti-version concurrency control; readers don’t block writers
FOR UPDATEExplicit row lock; surgical alternative to higher isolation
Retry patternRequired for Serializable transactions; must re-read all data

Key takeaways:

  • Isolation levels are a trade-off spectrum. Higher levels prevent more anomalies but cost concurrency through blocking, aborts, or overhead. The correct level is the lowest one that satisfies your application’s correctness requirements.
  • The three standard anomalies are precise. Dirty read (uncommitted data), non-repeatable read (changed row), phantom read (new row). Each isolation level prevents a specific subset.
  • Implementation varies by database. PostgreSQL Repeatable Read prevents phantoms; standard does not require this. MySQL and SQL Server have different default levels and versioning options.
  • Locking and versioning are different strategies. Locking blocks conflicts; versioning detects them. Each has performance characteristics that matter under load.
  • Serializable requires retry logic. Transactions can abort with serialization failures and must be re-executed from the beginning. Applications must be designed for this.
  • Explicit locks are often the better tool. SELECT ... FOR UPDATE at the specific chokepoint is more surgical than raising the entire transaction’s isolation level.
  • Phantom prevention requires range awareness. Row locks cannot prevent new rows. Serializable uses range locks or predicate locks to detect insert conflicts.
  • The default is usually right. Read Committed (or the database’s default) is correct for most single-statement and simple multi-statement transactions. Raise isolation only when you have a specific anomaly to prevent.

Remember: Transaction isolation is the discipline of choosing what concurrent operations may observe of each other. The SQL standard gives four named levels, but each database implements them with different strategies and sometimes stronger guarantees than required. The practical question is not “which level is best” but “what is the lowest level at which my application produces correct results under concurrency.” Answer that by understanding the specific anomalies that threaten your business logic, then selecting the level that prevents them, adding explicit locks where the cost of a higher level is not justified, and building retry logic wherever the database may abort a transaction to preserve correctness.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!