planner: binary literals and prepared parameters can return extra rows with a case-insensitive composite index
- Dominant language
- Go
- Stars
- 40.5k
- Forks
- 6.2k
- PR merge metrics
- PR metrics pending
Description
## Bug Report
### 1. Minimal reproduce step (Required)
A raw binary literal or binary prepared parameter can make TiDB return rows that fail an explicit binary equality predicate. `FORCE INDEX` is used only to make the access path deterministic.
```sql
DROP TABLE IF EXISTS t;
CREATE TABLE t (
a BIGINT PRIMARY KEY,
b BIGINT NOT NULL,
c VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
KEY idx_bc (b, c)
);
INSERT INTO t VALUES
(1, 42, 'Alice'),
(2, 42, 'alice');
-- Non-prepared binary literal.
SELECT a, c
FROM t FORCE INDEX (idx_bc)
WHERE b = 42
AND c = _binary'Alice'
AND CAST(c AS BINARY) = CAST(_binary'Alice' AS BINARY)
ORDER BY a;
```
The same wrong result occurs with `X'416C696365'`, `0x416C696365`, and a binary prepared parameter:
Prepared parameter variant
```sql
SET @p1 = 42;
SET @p2 = CAST(_utf8mb4'Alice' AS BINARY);
SELECT CHARSET(@p2), COLLATION(@p2); -- binary, binary
PREPARE stmt FROM '
SELECT a, c
FROM t FORCE INDEX (idx_bc)
WHERE b = ?
AND c = ?
AND CAST(c AS BINARY) = CAST(? AS BINARY)
ORDER BY a';
EXECUTE stmt USING @p1, @p2, @p2;
```
### 2. What did you expect to see? (Required)
Only the byte-exact match should be returned:
```text
a c
1 Alice
```
MySQL 8.4.10 returns only this row for `_binary`, `X'...'`, `0x...`, and binary prepared parameters. Its plan retains the binary filter above the case-insensitive index lookup:
```text
Filter: (cast(t.c as char charset binary) = 'Alice')
-> Covering index lookup on t using idx_bc (b=42, c=_binary'Alice')
```
The prepared case was also verified through the native protocol with the value bound as `MYSQL_TYPE_BLOB`.
### 3. What did you see instead (Required)
TiDB returns both rows for these binary value forms:
| Value form | Execution | Returned IDs |
| --- | --- | --- |
| `_binary'Alice'` | direct SQL | `1,2` |
| `X'416C696365'` | direct SQL | `1,2` |
| `0x416C696365` | direct SQL | `1,2` |
| binary parameter | prepared statement | `1,2` |
The plan builds a range on `(b, c)`, but contains no residual `Selection` for the binary predicate:
```text
IndexReader
└─Projection
└─IndexRangeScan index:idx_bc(b, c)
range:[42 "\\x00A\\x00L\\x00I\\x00C\\x00E",
42 "\\x00A\\x00L\\x00I\\x00C\\x00E"]
```
As controls, direct `CAST('Alice' AS BINARY)`, `BINARY 'Alice'`, and a binary user variable return only row `1` and retain a `Selection`. Disabling `tidb_enable_prepared_plan_cache` does not change the prepared result.
### 4. What is your TiDB version? (Required)
Reproduced on TiDB v8.5.7:
```text
Release Version: v8.5.7
Edition: Community
Git Commit Hash: 202b7f47286a1109b5c957401d34c9358d130ae0
Git Branch: HEAD
UTC Build Time: 2026-07-07 08:10:56
GoVersion: go1.25.10
Race Enabled: false
Check Table Before Drop: false
Store: tikv
```
Also reproduced on:
- v7.5.7
- nightly `v9.0.0-beta.2.pre-1982-g2964713e26`
### Additional context
The fixes in [#67899](https://github.com/pingcap/tidb/issues/67899), [#67898](https://github.com/pingcap/tidb/pull/67898), and [#68498](https://github.com/pingcap/tidb/pull/68498) preserve residual filters for binary-cast literals, but do not cover raw binary literals or binary prepared parameters in this query shape.
Contributor guide
Research direction
Start by running the SQL reproduction, including the direct and prepared binary-value variants, and inspect the planner output for IndexReader and IndexRangeScan on idx_bc. Trace why the binary equality predicate is not retained as a residual Selection; done means every binary form returns only the byte-exact Alice row while case-insensitive index lookup remains usable.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go, sql
- Domain
- databases
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100