Hook: A phantom read is like checking the same shelf twice and finding a new book that was not there a minute ago — interviewers love it because it proves you understand how transactions protect a moving target.
Question: What is a phantom read in SQL transactions?
Answer: A phantom read happens when a transaction runs the same WHERE query twice and gets a different set of rows the second time because another committed transaction inserted, deleted, or changed rows so they now match the filter. It is not about one row changing value; it is about the result set itself changing. This usually shows up at weaker isolation levels such as READ COMMITTED.
Interview-Ready Answer: I would say a phantom read is when I repeat the same predicate query inside one transaction and the returned row set changes because another transaction committed rows that now match that predicate. For example, I might count all open orders, and on the second read I see extra orders appear. To prevent it, I use stronger isolation, especially SERIALIZABLE; and in some databases like PostgreSQL, REPEATABLE READ also gives a stable snapshot for repeated reads.
A phantom read is a range anomaly. A range anomaly means the problem is not one specific row; it is the set of rows that match a predicate such as status = 'OPEN' or amount > 100. You read the same condition twice inside one transaction, and the second read sees extra rows, missing rows, or both.
Think of a WHERE clause as a fence around a field. A phantom is a sheep that slips into the fenced area after your first count.
| Anomaly | What changes | Simple example |
|---|---|---|
| Dirty read | Uncommitted data | Read rows another tx may roll back |
| Non-repeatable read | Same row value | Row 7 changes from 10 to 12 |
| Phantom read | Matching row set | Count of open orders goes 5 to 6 |
There are two common defenses. The first is snapshot isolation, which means a transaction reads a consistent picture of the database taken at the start of the transaction. The second is range locking, such as gap locks or next-key locks, where the database protects not just rows you read, but the space between them so new matching rows cannot appear.
Here is the important interview detail: behavior depends on the engine. PostgreSQL default isolation is READ COMMITTED; MySQL InnoDB default is REPEATABLE READ; SQL Server default is READ COMMITTED. In PostgreSQL, REPEATABLE READ gives a stable snapshot for repeated reads, while SQL Server needs SERIALIZABLE to block phantoms. MySQL InnoDB often uses next-key locks to protect indexed ranges.
Normal indexed range reads are still about O(log n + k), where k is the number of matched rows. The extra cost comes from coordination: stronger isolation can add lock waits, deadlocks, or retries under contention. In a busy checkout table, one hot range can turn a fast read into a blocked read for tens or hundreds of milliseconds, and sometimes longer if many sessions keep touching the same key range.
So the practical rule is simple: use the weakest isolation that is safe, but move to SERIALIZABLE or explicit locking when business rules depend on the exact set of rows, not just the values inside them.
A row that was updated so it now matches your filter also counts as a phantom. It is not only about brand-new inserts. If your transaction first sees no rows with balance < 0, and another transaction changes a row so it becomes negative, that new match is still a phantom from your point of view.
Real-World Example: Imagine a checkout service for a concert ticket app. One transaction checks how many seats are still available in section A, then reserves a seat if the count is above zero. If two buyers do this at the same time under weak isolation, both can see the same available count before either commit is visible to the other. One of them creates the phantom: a new matching row appears in the seat-reservation range, and the system accidentally oversells the last seat.
What goes wrong: the user sees a success message, then support gets a wave of complaints because two confirmations were sent for one seat. Logs often show odd flips like available_seats=1 on two different requests followed by duplicate reservation inserts. In the worst case, downstream systems such as payment capture or seat assignment disagree, and the team has to refund one order and manually fix inventory.
Why this matters: phantom reads are one of those bugs that look rare in testing but show up under load, exactly when money and trust are on the line.
-- Phantom read demo: same predicate, different result after a concurrent insert.
-- This script is valid SQL and sets up the example data.
-- To see the true phantom read, run the "Session B" insert in a second connection
-- between the two SELECT statements in Session A.
DROP TABLE IF EXISTS orders;
CREATE TABLE orders (
order_id INT PRIMARY KEY,
customer_id INT NOT NULL,
status VARCHAR(20) NOT NULL
);
CREATE INDEX idx_orders_status ON orders (status);
INSERT INTO orders (order_id, customer_id, status) VALUES
(1, 10, 'OPEN'),
(2, 11, 'OPEN'),
(3, 12, 'CLOSED');
-- Session A: begin a transaction and read a range.
BEGIN TRANSACTION;
SELECT COUNT(*) AS open_orders_before
FROM orders
WHERE status = 'OPEN';
-- Session B, in another connection, would run this and commit:
-- INSERT INTO orders (order_id, customer_id, status)
-- VALUES (4, 13, 'OPEN');
-- COMMIT;
-- Session A repeats the exact same query.
-- Under READ COMMITTED, the second read can see the new row.
SELECT COUNT(*) AS open_orders_after
FROM orders
WHERE status = 'OPEN';
-- Edge case / failure path:
-- If Session A uses SERIALIZABLE and Session B touches the same range,
-- one transaction may block or fail with a serialization error on COMMIT.
-- That retry is the database protecting the "same predicate, same result" rule.
ROLLBACK;Follow-up & Tricky Questions:
SERIALIZABLE is the safest general answer. Some engines also make REPEATABLE READ behave like a stable snapshot for repeated reads, but that is database-specific.SELECT ... FOR UPDATE always stop phantoms? No. It locks the rows you actually found, but it does not automatically protect every possible new row that could appear in the predicate range. Some databases need range locks or serializable isolation for that.REPEATABLE READ always prevent phantoms? No. That is true in some engines and not in others. In an interview, always mention the database engine if you bring up isolation guarantees.Common Mistakes:
REPEATABLE READ the same way. Correction: always name the engine, because PostgreSQL, MySQL, and SQL Server differ.Memory Hook: Same fence, new sheep. The WHERE clause is the fence; a phantom is a row that sneaks into the fenced area between two looks.
Cheat Sheet:
SERIALIZABLE is the universal prevention answer.READ COMMITTED, MySQL InnoDB default REPEATABLE READ, SQL Server default READ COMMITTED.Practice Tasks:
bookings table and run the same count query twice while inserting a matching row in another session.READ COMMITTED and then at SERIALIZABLE; note the different behavior.