Beyond Code & Karma · Databases

Transaction Isolation

Two transactions touch the same row and each perceives its own version of reality. The isolation level decides how much one transaction's actions disturb another's perceived truth — from anything-goes Read Uncommitted to perfectly serial Serializable. Explained through anomalies, MVCC snapshots, and the discipline of not letting illusion leak between observers.

Interactive Deep Dive · Databases & Concurrency

Isolation is the I in ACID

A transaction groups several operations so they succeed or fail as one. Isolation is the promise that concurrent transactions don't corrupt each other's view of the data — ideally, each runs as though it were alone. Perfect isolation is expensive, so databases offer a dial: weaker levels allow more anomalies but more concurrency; stronger levels forbid anomalies but block, abort and retry more.

The whole subject in one sentence: an isolation level is just a contract about which concurrency anomalies are allowed to happen — and you trade truth for throughput by choosing where on the dial to sit.
Atomicity
All-or-nothing. A transaction's writes either all commit or all roll back — never half-applied.
Consistency
Every commit moves the database from one valid state to another, honouring constraints and invariants.
Isolation
The dial we tune here. How much one in-flight transaction's effects are hidden from another running at the same time.
Durability
Once committed, it survives crashes — the change is on stable storage, not just in memory.

Satya & Rita vs Maya — truth, order, and illusion

Hindu Philosophy Parallel

In Vedic thought, satya (truth) rests on rita — the cosmic order that keeps every observer's reality coherent. Against it stands maya, illusion: seeing something that isn't truly real. Serializable is rita — perfect order where every transaction sees one consistent truth. A dirty read is maya — seeing an uncommitted value that may vanish. Each weaker level lifts another veil, trading truth for speed.

Rita — Serializable
Absolute cosmic order. Every observer sees one consistent truth; no illusion leaks between transactions.
Maya — the anomalies
Veils of illusion. A dirty read shows a value that isn't yet real; a phantom shows rows that come and go.
The veil — isolation level
How much illusion you tolerate for throughput. Each notch down the dial lifts one more veil of maya.

"Hold to rita and every observer shares one truth; tolerate maya, and you trade certainty for speed."

The four isolation levels

The SQL standard names four levels. Climbing the ladder removes one class of anomaly at a time — at the cost of more blocking, aborts and retries, and therefore less concurrency.

Read Uncommitted

The floor. A transaction may read another's uncommitted writes. Permits dirty reads, non-repeatable reads and phantoms. Maximum concurrency, minimum truth.

Read Committed

You only ever see committed data — dirty reads gone. But each statement takes a fresh snapshot, so a re-read can still change. The common default (PostgreSQL, Oracle, SQL Server).

Repeatable Read

One snapshot for the whole transaction — re-read a row and it's unchanged. Non-repeatable reads gone. The standard still permits phantoms; snapshot engines like PostgreSQL prevent most. MySQL/InnoDB's default.

Serializable

The ceiling — rita. The result must equal some serial order of the transactions. The engine detects dangerous interleavings and aborts one with a serialization failure; your app retries. No anomalies, highest cost.

More isolation →
Climbing the ladder removes anomalies one by one — dirty, then non-repeatable, then phantom.
← Less concurrency
Each guarantee costs throughput: more snapshots held, more locks or conflict checks, more aborts to retry.
Pick per transaction
The level is set per transaction, not per database. Default low; raise it only on the transactions whose correctness needs it.

The three read anomalies (and a lost update)

Concurrency anomalies are the ways one transaction's view of the data gets corrupted by another running alongside it. Three are read anomalies, in increasing subtlety; a fourth corrupts a write.

Dirty Read
You read a value another transaction wrote but hasn't committed. If it rolls back, you acted on data that never officially existed — pure maya.
Non-repeatable Read
You read a row, another transaction updates and commits it, and you re-read the same row to a different value.
Phantom Read
You run a range or COUNT(*) query twice; another transaction commits an insert/delete in between, so new rows appear or vanish.
Lost Update
Two transactions read, modify and write the same row; one silently overwrites the other. Read Committed allows it; Repeatable Read+ blocks it.

See it in SQL — a non-repeatable read

Under Read Committed, each statement re-snapshots. T1 reads twice and gets two different answers because T2 committed in between:

non-repeatable-read.sql
-- Two sessions, Read Committed. Watch x change under T1's feet.
-- session T1                       session T2
BEGIN;
SELECT x FROM acct WHERE id = 1;   -- T1 reads x = 100
                                  BEGIN;
                                  UPDATE acct SET x = 150 WHERE id = 1;
                                  COMMIT;             -- T2 commits the change
SELECT x FROM acct WHERE id = 1;   -- T1 re-reads x = 150 → NON-REPEATABLE READ
COMMIT;

The worst veil — a dirty read

Only Read Uncommitted permits this. T2 reads a value that T1 later throws away:

dirty-read.sql
-- A dirty read: seeing a value that may never become real.
-- session T1                       session T2 (READ UNCOMMITTED)
BEGIN;
UPDATE acct SET x = 150 WHERE id = 1;
                                  SELECT x FROM acct WHERE id = 1;  -- reads 150, NOT committed
ROLLBACK;                          -- x snaps back to 100; the 150 never existed
-- T2 acted on maya — an illusion that dissolved.

Tuning the dial — SET TRANSACTION ISOLATION LEVEL

Raise the level on just the transaction that needs it. At Repeatable Read both reads share one snapshot; at Serializable a conflicting interleaving aborts instead:

set-isolation.sql
-- Raise the level only on the transaction that needs it.
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;

-- both reads now share ONE snapshot — no non-repeatable read
SELECT x FROM acct WHERE id = 1;
-- ... other transactions commit in between ...
SELECT x FROM acct WHERE id = 1;   -- identical value
COMMIT;

-- At SERIALIZABLE the engine may instead raise:
-- ERROR: could not serialize access due to concurrent update
-- ...which means: retry the transaction.

The cheat-sheet — levels × anomalies

What each level permits, per the ANSI SQL standard. "Allowed" means the level does not prevent it.

Isolation levelDirty readNon-repeatablePhantom
Read UncommittedAllowedAllowedAllowed
Read CommittedPreventedAllowedAllowed
Repeatable ReadPreventedPreventedAllowed*
SerializablePreventedPreventedPrevented

* The standard permits phantoms at Repeatable Read, but PostgreSQL's snapshot-based Repeatable Read prevents most of them — theory and practice diverge here. The visualizer follows the PostgreSQL snapshot model.

Watch a dirty read appear and vanish

A scripted two-transaction interleaving at Read Uncommitted. T2 reads a value T1 never commits — maya — then T1 rolls back and the value dissolves. Press Run, or step through it.

Dirty read · Read Uncommitted
⏸ Ready
T1
    T2
      Press Run to start…

      How the engine keeps the truth

      Beneath the levels sit two strategies for managing conflict — locking and versioning — and a lot of nuance senior engineers carry.

      Pessimistic (locking)
      Assume conflict. Take locks before touching data; others wait. Simple to reason about, but locks contend and can deadlock.
      Optimistic
      Assume no conflict. Proceed freely, then validate at commit; if someone else touched your data, abort and retry. Great under low contention.
      MVCC (versioning)
      Keep many versions per row. Readers walk to the version their snapshot can see — reads never block writes. PostgreSQL's core.

      Instead of overwriting a row in place, every update writes a new version and leaves the old one behind. Each version carries two transaction-id stamps:

      StampMeaning
      xminThe transaction that created this version.
      xmaxThe transaction that deleted or superseded it (empty while live).

      A statement takes a snapshot — the set of transactions whose effects it may see. A version is visible only if its xmin committed before the snapshot and its xmax hasn't. Readers walk to the newest visible version, so reads never block writes and writes never block reads. Repeatable Read takes one snapshot at transaction start and reuses it — that's why a re-read is stable.

      Keeping old versions isn't free. Every update and delete leaves a dead tuple — a version no live snapshot can see — and they pile up in the table and its indexes. PostgreSQL's autovacuum reclaims that space and advances the visibility horizon. If it falls behind you get table bloat, slower scans, and eventually transaction-id wraparound pressure. Long-running transactions are the usual culprit: they hold the horizon back, keeping dead tuples alive.

      Snapshot isolation (Repeatable Read) stops the classic anomalies, but not write skew: two transactions read an overlapping set, each checks an invariant that still holds, and each writes a different row — together breaking the invariant. Classic example: "at least one doctor must remain on call." Both doctors read "two on call", both decide it's safe to go off, both commit — now zero. Only Serializable detects the dangerous read/write dependency and aborts one.

      Rule of thumb: if an invariant spans multiple rows and check-then-write logic depends on it, Repeatable Read is not enough — reach for Serializable and handle the retry.

      PostgreSQL, Oracle and SQL Server (with read-committed snapshot) default to a flavour of Read Committed. MySQL's InnoDB defaults one notch higher to Repeatable Read. None default to Serializable — it's the most expensive, since it must detect and abort non-serializable interleavings and your app must retry. The practical rule: default to Read Committed and raise the level only on the specific transactions whose correctness depends on it.

      At Repeatable Read and Serializable, a transaction can fail at commit with a serialization failure ("could not serialize access due to concurrent update"). This is not a bug — it's the engine refusing to let an anomaly through. The correct response is to retry the whole transaction from the beginning, usually with a small backoff. Code that raises the isolation level without a retry loop is incomplete.

      The Karma of a snapshot

      The Karma Connection

      Many observers, many truths. As each soul perceives reality coloured by its own karma, each transaction reads the world as it was at the moment of its intention — its snapshot. A dirty read is an illusion leaking from one observer into another; isolation is the discipline of not letting your unfinished actions disturb another's perceived truth.

      karma.sql
      -- Each transaction reads the world as it was at its intention.
      BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;  -- perfect rita
      
      -- act as though you alone exist — the result stays consistent
      SELECT sum(balance) FROM acct;
      UPDATE acct SET balance = balance - 10 WHERE id = 1;
      COMMIT;  -- or retry, if another's karma conflicts with yours

      Serializable is perfect dharma: every transaction may act as though it alone exists, and the result is still consistent. MVCC keeps the old worlds intact a while, so no observer is forced into another's present — and lets them go, through vacuum, once no one is looking back.

      "Act on the truth that was real at your intention; do not be moved by another's unfinished act." — the Gita, interpreted for database engineers 🙏

      Isolation Trivia

      Three questions. No cheating. Your karma is watching.

      Question 1
      A transaction reads a value another transaction wrote but has NOT committed. Which anomaly is this?
      Question 2
      Under PostgreSQL's snapshot model, which is the LOWEST level that prevents both dirty and non-repeatable reads?
      Question 3 — 🌶️ Spicy
      'At least one doctor must stay on call.' Two transactions each read 'two on call' and each take one off, breaking the rule. Which level prevents this?

      The Six Truths of Transaction Isolation

      A Level Is a Contract
      An isolation level is just a promise about which anomalies are allowed. Nothing more, nothing less.
      Three Read Anomalies
      Dirty, non-repeatable, phantom — removed in that order as you climb from Read Uncommitted to Serializable.
      Snapshots Are Truth
      Each transaction reads the world as of its snapshot. Repeatable Read freezes one; Read Committed re-takes per statement.
      MVCC Means No Read Locks
      Many versions per row, stamped xmin/xmax. Readers never block writers; the cost is dead tuples and vacuum.
      Serializable Stops Write Skew
      Snapshots alone don't catch multi-row invariant breaks. Only Serializable detects them — and you must retry.
      Rita Over Maya
      Buy only the truth you need. Default low, raise per transaction, and let go of versions no one is looking back at.

      कर्म करो, फल की चिंता मत करो