The SQL standard names three read anomalies. A dirty read sees another transaction's uncommitted change; a non-repeatable read gets a different value when it reads a row again; a phantom is a row that appears in a repeated range query. Four isolation levels trade them against concurrency; SET SESSION TRANSACTION ISOLATION LEVEL ... picks one, and @@transaction_isolation shows it.
In this run Session B restocks one Databases title and adds another inside a transaction, while Session A sums the category three times:
Session A> SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
Session A> START TRANSACTION;
Session A> SELECT COUNT(*), SUM(stock) FROM products WHERE category_id = 3\G
Session A: *************************** 1. row ***************************
Session A: COUNT(*): 2
Session A: SUM(stock): 12
Session B> START TRANSACTION;
Session B> UPDATE products SET stock = 20 WHERE id = 2;
Session B: Query OK, 1 row affected
Session B> INSERT INTO products VALUES (9, 'BK-SQL-03', 'Query Tuning', 3, 38.00, 5, NULL);
Session B: Query OK, 1 row affected
Session A> SELECT COUNT(*), SUM(stock) FROM products WHERE category_id = 3\G
Session A: *************************** 1. row ***************************
Session A: COUNT(*): 2
Session A: SUM(stock): 12
Session B> COMMIT;
Session A> SELECT COUNT(*), SUM(stock) FROM products WHERE category_id = 3\G
Session A: *************************** 1. row ***************************
Session A: COUNT(*): 3
Session A: SUM(stock): 25
Session A> COMMIT;A's second read ignored B's uncommitted work; its third saw B's commit, so the sum changed (a non-repeatable read) and a row appeared (a phantom). The same file at the other levels gave:
| Level | Second read (B uncommitted) | Third read (B committed) | Session B |
|---|---|---|---|
| READ UNCOMMITTED | 3 rows, 25: dirty read | 3 rows, 25 | Not blocked |
| READ COMMITTED | 2 rows, 12 | 3 rows, 25 | Not blocked |
| REPEATABLE READ (default) | 2 rows, 12 | 2 rows, 12 | Not blocked |
| SERIALIZABLE | 2 rows, 12 | 2 rows, 12 | Waited for A's COMMIT |
The standard permits phantoms under REPEATABLE READ, but InnoDB's snapshot hides them from plain reads, and gap locks keep them out of locking reads (Row and Gap Locks). SERIALIZABLE turns each plain SELECT inside a transaction into SELECT ... FOR SHARE, which made B wait. READ COMMITTED, the usual alternative, skips gap locks and so suffers fewer lock waits and deadlocks.