Skip to main content

Fixed: A MySQL SELECT in a Transaction Returns an Old Value

· 5 min read
Jagdish Kumawat
Founder @ Dewiride

Another session changed a row and committed, and I could see that it worked. In my transaction, the same SELECT kept returning the old number. Nothing's broken: under MySQL's default isolation level, a transaction keeps reading from the snapshot it took at its first read.

What I Was Doing​

I had a transaction open, reading a product's quantity on MySQL 9.7.4 in Docker:

mysql: terminal 1
START TRANSACTION;
SELECT Id, Sku, QuantityOnHand FROM products WHERE Id = 3;
Output (the SELECT)
+----+-------------+----------------+
| Id | Sku | QuantityOnHand |
+----+-------------+----------------+
| 3 | CBL-HDMI-2M | 50 |
+----+-------------+----------------+
1 row in set (0.001 sec)

Meanwhile, another session changed it:

mysql: terminal 2
UPDATE products SET QuantityOnHand = 40 WHERE Id = 3;
SELECT Id, Sku, QuantityOnHand FROM products WHERE Id = 3;
Output (the SELECT)
+----+-------------+----------------+
| Id | Sku | QuantityOnHand |
+----+-------------+----------------+
| 3 | CBL-HDMI-2M | 40 |
+----+-------------+----------------+
1 row in set (0.001 sec)

What I Expected​

The first terminal to show 40 the next time it asked.

What I Got​

mysql: terminal 1
SELECT Id, Sku, QuantityOnHand FROM products WHERE Id = 3;
Output
+----+-------------+----------------+
| Id | Sku | QuantityOnHand |
+----+-------------+----------------+
| 3 | CBL-HDMI-2M | 50 |
+----+-------------+----------------+
1 row in set (0.001 sec)

Terminal 1's SELECT still returning a quantity of 50 inside its transaction.

What I Checked​

A locking read, in the same transaction.

mysql: terminal 1
SELECT Id, Sku, QuantityOnHand FROM products WHERE Id = 3 FOR UPDATE;
Output
+----+-------------+----------------+
| Id | Sku | QuantityOnHand |
+----+-------------+----------------+
| 3 | CBL-HDMI-2M | 40 |
+----+-------------+----------------+
1 row in set (0.001 sec)

So 40 is there, and the plain SELECT just isn't looking at it.

The isolation level. SELECT @@transaction_isolation; said REPEATABLE-READ, the default.

Why It Happens​

MySQL's isolation levels page says of this level: "Consistent reads within the same transaction read the snapshot established by the first read." That's the "repeatable" in the name: read a row twice, and you get the same answer, so a report's numbers add up. A plain SELECT is a consistent read, and a locking read isn't, so it sees the newest row.

The Fix​

Choose by what you need the value for:

You only want the newest data: end the transaction, and read again.

mysql: terminal 1
COMMIT;
SELECT Id, Sku, QuantityOnHand FROM products WHERE Id = 3;

The consistent read page says: "You can advance your timepoint by committing your transaction and then doing another SELECT or START TRANSACTION WITH CONSISTENT SNAPSHOT."

You're going to change it based on what you read: use FOR UPDATE, as above. That's the newest value, and it's locked until you finish.

You want every read to be fresh: use READ COMMITTED for the session:

mysql: terminal 1
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT Id, Sku, QuantityOnHand FROM products WHERE Id = 3;

After the Fix​

With READ COMMITTED, the same story gives the new value on the second read:

Output (the first read)
+----+-------------+----------------+
| Id | Sku | QuantityOnHand |
+----+-------------+----------------+
| 3 | CBL-HDMI-2M | 50 |
+----+-------------+----------------+
1 row in set (0.000 sec)
Output (the second read, after terminal 2's change)
+----+-------------+----------------+
| Id | Sku | QuantityOnHand |
+----+-------------+----------------+
| 3 | CBL-HDMI-2M | 40 |
+----+-------------+----------------+
1 row in set (0.000 sec)

Under READ COMMITTED, the first SELECT returns 50 and the second returns 40 inside the same transaction.

Common Mistakes​

MistakeWhat happensDo this instead
Reading a value in a transaction, and writing a number computed from itAnother session's change is lostCompute in the UPDATE, or use FOR UPDATE
Assuming an UPDATE uses the old snapshot tooIt reads the newest row: here, 40 - 1 = 39READ COMMITTED vs REPEATABLE READ shows it
Leaving a transaction open across a long pauseEverything it reads is out of dateKeep transactions short

Troubleshooting​

SymptomLikely causeFix
An app sees old data until it reconnectsA transaction stayed open on that connectionCommit after each unit of work
autocommit = 0 in the sessionEvery read is in one long transactionCommit, or turn autocommit on
A plain SELECT and a FOR UPDATE disagreeThat's the difference between themUse the one you need

Conclusion​

Under REPEATABLE READ, a transaction's plain SELECTs keep showing the snapshot from its first read. Commit and read again for fresh data, use FOR UPDATE when you're going to act on the value, or use READ COMMITTED if every read should be fresh.

Additional Resources​

Stay Updated

Subscribe to our newsletter for the latest tutorials, tech insights, and developer news.

By subscribing, you agree to our privacy policy. Unsubscribe at any time.