| Bug #120901 | SELECT ... LOCK IN SHARE MODE returns different row sets depending on index choice under RC/RU | ||
|---|---|---|---|
| Submitted: | 13 Jul 8:53 | Modified: | 14 Jul 13:12 |
| Reporter: | Aaditya Dubey | Email Updates: | |
| Status: | Verified | Impact on me: | |
| Category: | MySQL Server: InnoDB storage engine | Severity: | S3 (Non-critical) |
| Version: | 8.4.9, 8.4.10 | OS: | Any |
| Assigned to: | CPU Architecture: | Any | |
[13 Jul 8:53]
Aaditya Dubey
[14 Jul 13:12]
Chaithra Marsur Gopala Reddy
Hi Aaditya Dubey, Thank you for the test case. Verified as described.
[30 Jul 9:49]
David Tan
For tracking and attribution purposes, this issue was reported earlier as Bug #120799 on June 28, 2026. Bug #120799 contains the same title, description, reproduction procedure, test case, observed result, and expected result. The issue was later confirmed by Percona and was submitted again here without knowledge of the earlier MySQL report. Could the MySQL Verification Team please cross-reference Bug #120799 and Bug #120901 and review whether the earlier Bug #120799 should be marked as Verified and treated as the original report?
[9 Sep 15:17]
Jakub Lopuszanski
Hello, and thank you for the report. This appears to be expected behavior for a locking read under READ COMMITTED and READ UNCOMMITTED. Under READ COMMITTED, InnoDB locks index records encountered by a locking read, but does not lock the intervening gaps. Concurrent transactions may therefore insert new records into those gaps, and phantom rows are permitted. The manual documents this behavior in [Transaction Isolation Levels](https://dev.mysql.com/doc/refman/8.4/en/innodb-transaction-isolation-levels.html). A locking read scans its chosen index incrementally. If the scan waits for a record lock, records inserted and committed later behind its current index position are not subsequently revisited, whereas records inserted ahead of that position may still be encountered. In this test, the covering c0 secondary-index scan can pass the original c0 entries because Transaction 1’s earlier update changes c1, not c0 (so the is no implicit or explicit lock on the record in the c0 index). It first waits on the uncommitted c0=1.68135 entry. While it waits, Transaction 1 inserts 1.25715, 1.32932, and 1.38422, all of which sort before 1.68135 These are therefore behind the scan position when it resumes. The later 1.93502 value sorts after that position and is returned. The PRIMARY path encounters Transaction 1’s clustered-record locks earlier, before those new keys would be scanned, and consequently observes them after the wait ends. This happens because locks on the records of the PRIMARY index are taken whenever such a record is modified, no matter which columns is modified. So even though c0 and PRIMARY have the same sort order, they differ in that PRIMARY contains all the columns, and if any of them, like c1, is modified by Transaction 1, it obtains X lock on it, which prevents Transaction 2 from looking at this record in PRIMARY index. This then means that even though the scan order is the same for PRIMARY and c0, the moment at which such locking SELECT gets blocked is different: the scan over PRIMARY will block on the first row which was modified at all, while the scan over c0 blocks only on a row which has new c0. The c1 path has a different index order and blocking point. READ UNCOMMITTED uses the READ COMMITTED behavior for locking selects, which is relevant here, so similar story. We are not aware of a specification guaranteeing access-path-independent results for locking reads in the presence of concurrent gap insertions at these isolation levels. Preventing these phantoms requires an isolation/locking strategy that protects the scanned gaps, such as the next-key locking used by locking reads under REPEATABLE READ. Unless there is a contrary documented guarantee that we have overlooked, we therefore intend to classify this report as “Not a Bug.”
