AcademySQL
Transactions and isolation levels
What ACID actually promises, which anomalies each isolation level still allows, and why the read-then-write you wrote is a race unless you said otherwise.
3 min readUpdated 3 exercises
A transaction is all of it or none of it
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;If anything fails between BEGIN and COMMIT — a constraint, a crash, a lost connection — neither update survives. That is the A in ACID, and it is the easy part. The four letters promise different things, and only two of them are usually what a bug is about:
| Promise | |
|---|---|
| Atomicity | all statements or none |
| Consistency | constraints hold before and after |
| Isolation | concurrent transactions do not see each other's mess |
| Durability | a committed write survives a crash |
Isolation is the one with a dial on it, and the one people are surprised by.
The anomalies the levels are named after
| Level | Dirty read | Non-repeatable read | Phantom read |
|---|---|---|---|
| Read uncommitted | possible | possible | possible |
| Read committed | no | possible | possible |
| Repeatable read | no | no | possible* |
| Serializable | no | no | no |
- Dirty read — you see a change another transaction has not committed. Practically no engine you will meet allows it by default.
- Non-repeatable read — you read a row twice in one transaction and get two different values, because someone committed in between.
- Phantom read — you run the same
WHEREtwice and the second run returns a row that was not there before.
* PostgreSQL's repeatable read uses snapshot isolation, which happens to prevent phantoms too. This is the kind of detail that makes "what does repeatable read do" a question with a per-engine answer. The defaults differ as well: read committed in PostgreSQL and Oracle, repeatable read in MySQL's InnoDB.
The bug this causes in real code
BEGIN;
SELECT stock FROM products WHERE id = 1; -- 1 left
-- application decides: enough stock, proceed
UPDATE products SET stock = stock - 1 WHERE id = 1;
COMMIT;Two requests run that at once and both read 1. Both decide there is enough. Stock ends at -1. Nothing errored, no isolation level below serializable prevented it, and the transaction did its job — the read and the write were simply not atomic together.
Three ways out, in increasing cost:
-- 1. Do the check in the write itself. No read, no race.
UPDATE products SET stock = stock - 1
WHERE id = 1 AND stock > 0;
-- then check how many rows were affected
-- 2. Lock the row for the duration of the transaction.
SELECT stock FROM products WHERE id = 1 FOR UPDATE;
-- 3. Let the database refuse the conflict.
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;The first is the one to reach for: a conditional update is one statement, and one statement is atomic by definition. Serializable is correct but pushes the problem onto you — transactions can now fail with a serialization error, and the application must retry them.
Deadlocks are a lock-ordering problem
Two transactions, each holding what the other wants:
T1: UPDATE accounts id=1 T2: UPDATE accounts id=2
T1: UPDATE accounts id=2 T2: UPDATE accounts id=1
both wait, foreverThe engine detects the cycle and kills one with a deadlock error. The fix is not a retry loop, it is ordering: if every transaction touches rows in the same order — by primary key, say — the cycle cannot form. Keep a retry as a backstop, not as the strategy.
Keep them short
A transaction holds locks and a snapshot until it ends. Two habits cause most production incidents here: doing an HTTP call inside a transaction, and opening one at the start of a request handler and committing at the end. Both hold a database resource for as long as something unrelated takes. Open late, commit early, and never wait on a network inside one.
Now practice it
3 exercisesReading this page is the cheap half. These are the exercises that make you use it.
- Transactions & Concurrency · ACID & Basic TransactionsACID and the basic shape of a transaction, which is what the harder questions are all built on.QuizTake · EasyQuizTake · Easy
- Transactions & Concurrency · Isolation LevelsIsolation levels and the anomalies each one still permits — including the ones your default allows.QuizTake · MediumQuizTake · Medium
- Transactions & Concurrency · Locking & DeadlocksLocking and deadlocks, where the answer is usually about the order two transactions took their locks.QuizTake · HardQuizTake · Hard
Read next
Indexes: what a B-tree cannot do
Why column order in a composite index decides which queries it serves, what makes a condition unindexable, and the reason an index sometimes makes things slower.
3 min
JOINs, and the rows you did not expect
What a join really computes, the difference INNER and LEFT actually make, and why filtering a LEFT JOIN in the WHERE clause silently turns it back into an INNER one.
3 min