SQL (PostgreSQL, MySQL, SQLite) Module 11 – Transactions, isolation and locking
Isolation levels and read anomalies
Dirty reads, non-repeatable reads, phantoms, lost updates and write skew, the isolation levels of PostgreSQL, MySQL and SQLite, and how to pick one.
What you will learn
- Describe dirty reads, non-repeatable reads, phantoms, lost updates and write skew
- Map the isolation levels and defaults of PostgreSQL, MySQL and SQLite
- Set an isolation level for one transaction or a session in each engine
- Pick an isolation level for a given workload
Before you start
What you’ll learn · Before you start
What you will learn
- Describe dirty reads, non-repeatable reads, phantoms, lost updates and write skew
- Map the isolation levels and defaults of PostgreSQL, MySQL and SQLite
- Set an isolation level for one transaction or a session in each engine
- Pick an isolation level for a given workload
Before you start
On this page
An isolation level decides how much a transaction may see of other transactions that run at the same time. Weaker levels let more read anomalies through: a dirty read sees data that may never be committed, a non-repeatable read gets two answers to one question, a phantom finds new rows on a second search, a lost update overwrites a change it never saw, and write skew breaks a rule that two transactions each checked. The SQL standard names four isolation levels: READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ and SERIALIZABLE. PostgreSQL starts every transaction at Read Committed and MySQL’s InnoDB at Repeatable Read. SQLite has no level to choose: it admits a single writer at a time, and that alone makes its transactions serializable. Choose the level by the anomalies your code cannot live with, and set it for the transactions that need it.
Five read anomalies, one sentence each
- Dirty read: a transaction reads a change that another transaction has not committed and may still roll back.
- Non-repeatable read: reading one row twice gives two committed values, because another transaction changed it in between.
- Phantom: running one search twice returns a different set of rows, because another transaction inserted or deleted a match.
- Lost update: two transactions read a value and each writes a new value computed from it, so one write silently replaces the other.
- Write skew: two transactions check a rule that spans several rows, then change different rows, and together break the rule that each one checked.
The standard defines its levels by the first three. Lost updates and write skew start with a read that goes stale before the write, which is why they count among the read anomalies here, but the standard’s table does not mention them, and each engine handles them its own way.
The four levels, and what each engine does
| Criterion | Dirty read | Non-repeatable read | Phantom |
|---|---|---|---|
| READ UNCOMMITTED | Possible | Possible | Possible |
| READ COMMITTED | Not possible | Possible | Possible |
| REPEATABLE READ | Not possible | Not possible | Possible |
| SERIALIZABLE | Not possible | Not possible | Not possible |
The standard sets the least each level must prevent, and engines may prevent more. The isolation levels share their names across engines, but not their machinery:
- PostgreSQL runs Read Uncommitted as Read Committed, so it never shows a dirty read. Its Repeatable Read keeps one snapshot for the whole transaction, which also hides phantoms. Serializable adds checks and fails one transaction when the result could differ from every one-at-a-time order.
- MySQL with InnoDB really allows dirty reads at Read Uncommitted. At its default, Repeatable Read, plain
SELECTs read a snapshot taken at the transaction’s first read, whileUPDATE,DELETEand locking reads work on the newest committed rows. - SQLite has nothing to set. Its transactions are serializable because writers take turns, and in WAL mode a reader keeps a stable snapshot while a writer commits.
You choose the level per transaction. In PostgreSQL, write BEGIN ISOLATION LEVEL REPEATABLE READ, or SET TRANSACTION ISOLATION LEVEL REPEATABLE READ as the first statement after BEGIN. In MySQL, SET TRANSACTION ISOLATION LEVEL READ COMMITTED comes before START TRANSACTION and applies to the next transaction only, while SET SESSION TRANSACTION ISOLATION LEVEL changes it for the rest of the connection.
A non-repeatable read on a real database
The same second read, two isolation levels
Text description of the diagram
The figure is a timeline with three columns: session A, which reads the stock row twice; the row's qty column; and session B, which changes it. Time runs downwards.
- A begins a transaction and reads qty: 10.
- B sets qty to 7 and commits.
- A reads qty again, in the same transaction.
- At Read Committed the second read returns 7, the newly committed value: a non-repeatable read. At Repeatable Read it returns 10 again, because the whole transaction reads from one snapshot.
The browser runs one session at a time, so this program opens two connections to one SQLite file in WAL mode and runs them step by step. The reader reads the stock of a kettle twice, once with each read in a transaction of its own and once with both reads inside one transaction:
"""When does a read see another session's commit? Two connections to one SQLite file.
SQLite in WAL mode gives each read transaction a snapshot. A read outside BEGIN is a
transaction of its own; reads inside BEGIN ... COMMIT share one snapshot.
"""
import os
import sqlite3
DB = "shop.db"
for suffix in ("", "-wal", "-shm"):
if os.path.exists(DB + suffix):
os.remove(DB + suffix)
def connect():
return sqlite3.connect(DB, autocommit=True, timeout=0)
setup = connect()
setup.execute("PRAGMA journal_mode = WAL")
setup.execute("CREATE TABLE stock (id INTEGER PRIMARY KEY, item TEXT NOT NULL, qty INTEGER NOT NULL)")
setup.execute("INSERT INTO stock VALUES (1, 'kettle', 10)")
reader, writer = connect(), connect()
QTY = "SELECT qty FROM stock WHERE id = 1"
def read(label):
print(f" reader sees {reader.execute(QTY).fetchone()[0]}{label}")
print("1. Two reads, each in a transaction of its own")
read("")
writer.execute("UPDATE stock SET qty = 7 WHERE id = 1")
print(" writer sets qty to 7 and commits")
read(" <- a non-repeatable read")
print("2. Two reads inside one transaction")
reader.execute("BEGIN")
read("")
writer.execute("UPDATE stock SET qty = 4 WHERE id = 1")
print(" writer sets qty to 4 and commits")
read(" <- the same snapshot as before")
reader.execute("COMMIT")
read(" after its own COMMIT") Output
1. Two reads, each in a transaction of its own reader sees 10 writer sets qty to 7 and commits reader sees 7 <- a non-repeatable read 2. Two reads inside one transaction reader sees 7 writer sets qty to 4 and commits reader sees 7 <- the same snapshot as before reader sees 4 after its own COMMIT
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 snapshots.py
In the first run, each read is its own transaction and sees the latest commit, so the reader gets 10 and then 7.
That is exactly how every statement behaves at PostgreSQL’s Read Committed: each one takes a fresh snapshot. In the
second run, both reads share one snapshot and return 7, although the writer committed 4 in between, which is the
promise of Repeatable Read. Only after its own COMMIT does the reader see 4.
Check yourself
Check yourself
4 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Every anomaly on PostgreSQL, MySQL and SQLite, replayed Pro: opens with a Pro, Premium or Ultimate pass
The standard’s table says what a level must prevent; this part shows what each engine actually does. It replays five two-session timelines at every level of PostgreSQL 18 and MySQL 8.4, explains each outcome, and runs the hardest case, write skew, on SQLite.
In this part:
- Five timelines on PostgreSQL 18
- The same timelines on MySQL 8.4
- SQLite: the write skew attempt refused, reproduced
1 diagram · 1 runnable example (Python) · about 4 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Choosing and setting a level, with worked cases Pro: opens with a Pro, Premium or Ultimate pass
A level is a trade between anomalies and failures: stronger levels refuse more interleavings, so more transactions wait or fail and must be retried. This part sets levels without the common traps, then works through five workloads and the cost of Serializable in retries.
In this part:
- Setting the level without the three common traps
- Five workloads, worked
- What Serializable costs, in retries
- A level for any transaction, in four questions
about 4 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
-
Fresher to mid · Warm-up
Which isolation level do PostgreSQL, MySQL and SQLite use by default, and which read anomalies can each show at it?
PostgreSQL uses Read Committed: every statement sees the data committed when it started, so there are no dirty reads, but two reads in one transaction can disagree (a non-repeatable read or a phantom), and a read followed by a write can lose an update. MySQL with InnoDB uses Repeatable Read: plain
SELECTs read one snapshot for the whole transaction, butUPDATE,DELETEand locking reads work on the newest rows, so lost updates and write skew are still possible. SQLite lets one writer in at a time, so its transactions are serializable, and in WAL mode a reader keeps its snapshot while a writer commits. -
Mid to senior
A month-end report on PostgreSQL runs 40 queries over 15 minutes at the default level while transfers keep running, and its assets and liabilities no longer balance. Why, and what do you change without blocking the transfers?
-
Mid to senior
A coupon may be redeemed 3 times. The code counts the coupon's redemptions and inserts a new one when the count is below 3, at Repeatable Read on PostgreSQL. During a sale it was redeemed 5 times. Explain, and fix it on PostgreSQL and on MySQL.
-
Senior to staff
On MySQL 8.4 at the default level, a transaction reads qty 10 with a plain SELECT, then UPDATE stock SET qty = qty - 1 WHERE id = 1 AND qty > 0 changes 0 rows, and a second plain SELECT still shows 10. Explain each step and what the code should do.
Key takeaways
- An isolation level decides which read anomalies a transaction may meet: dirty reads, non-repeatable reads, phantoms, lost updates and write skew.
- Defaults differ: PostgreSQL Read Committed, MySQL Repeatable Read, SQLite serializable with one writer at a time.
- Each statement at Read Committed sees the latest commits; a Repeatable Read transaction keeps one snapshot.
- Set the level per transaction, at its start.
References
- PostgreSQL documentation: Transaction Isolation (The PostgreSQL Global Development Group)
- PostgreSQL documentation: SET TRANSACTION (The PostgreSQL Global Development Group)
- PostgreSQL documentation: BEGIN (The PostgreSQL Global Development Group)
- PostgreSQL documentation: Client Connection Defaults (default_transaction_isolation) (The PostgreSQL Global Development Group)
- PostgreSQL documentation: PostgreSQL Error Codes (The PostgreSQL Global Development Group)
- MySQL 8.4 Reference Manual: Transaction Isolation Levels (Oracle Corporation)
- MySQL 8.4 Reference Manual: Consistent Nonlocking Reads (Oracle Corporation)
- MySQL 8.4 Reference Manual: SET TRANSACTION Statement (Oracle Corporation)
- MySQL 8.4 Reference Manual: Phantom Rows (Oracle Corporation)
- MySQL 8.4 Reference Manual: Locking Reads (Oracle Corporation)
- Isolation In SQLite (SQLite)
- SQLite: Write-Ahead Logging (SQLite)
- H. Berenson, P. Bernstein, J. Gray, J. Melton, E. O’Neil and P. O’Neil: A Critique of ANSI SQL Isolation Levels (ACM SIGMOD)
- sqlite3: DB-API 2.0 interface for SQLite databases (Python Software Foundation)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress