PhysicalIndexMergeReader.ResolveIndices references _tidb_rowid on table with CLUSTERED integer primary key
- Dominant language
- Go
- Stars
- 40.5k
- Forks
- 6.2k
- PR merge metrics
- PR metrics pending
Description
# Bug Report: PhysicalIndexMergeReader references _tidb_rowid on CLUSTERED primary key table
## 1. Minimal reproduce step (Required)
```sql
CREATE TABLE `devices_custom_params` (
`id` int NOT NULL AUTO_INCREMENT,
`tenant_id` int DEFAULT NULL,
`user_id` int DEFAULT NULL,
`device_id` int DEFAULT NULL,
`custom_param_id` int DEFAULT NULL,
`value` text COLLATE utf8_general_ci DEFAULT NULL,
`customer_id` int NOT NULL DEFAULT '1',
PRIMARY KEY (`id`) /*T![clustered_index] CLUSTERED */,
UNIQUE KEY `dcp_tenant_uniq` (`tenant_id`, `custom_param_id`),
UNIQUE KEY `dcp_user_uniq` (`user_id`, `custom_param_id`),
UNIQUE KEY `dcp_device_uniq` (`device_id`, `custom_param_id`),
KEY `user_id` (`user_id`),
KEY `device_id` (`device_id`),
KEY `custom_param_id` (`custom_param_id`),
KEY `dcp_customer_id` (`customer_id`),
KEY `dcp_tenant_id_fk` (`tenant_id`, `customer_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
CREATE TABLE `devices_custom_params_def` (
`id` int NOT NULL AUTO_INCREMENT,
`tenant_id` int NOT NULL,
`param_id` varchar(255) COLLATE utf8_general_ci DEFAULT NULL,
`param_name` varchar(255) COLLATE utf8_general_ci DEFAULT NULL,
`is_multiline` tinyint(1) DEFAULT NULL,
`is_password` tinyint(1) DEFAULT NULL,
`is_in_reports` tinyint(1) DEFAULT NULL,
`customer_id` int NOT NULL DEFAULT '1',
PRIMARY KEY (`id`) /*T![clustered_index] CLUSTERED */,
KEY `param_id` (`param_id`),
KEY `tenant_id` (`tenant_id`),
KEY `dcpd_customer_id` (`customer_id`),
KEY `dcpd_tenant_id_fk` (`tenant_id`, `customer_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
```
Trigger query (fails during compilation/optimization):
```sql
SELECT dcp.tenant_id, dcp.user_id, dcp.device_id, dcp.value, dcpd.is_password
FROM devices_custom_params dcp, devices_custom_params_def dcpd
WHERE
(
dcp.tenant_id = 0
OR dcp.tenant_id = 1
OR dcp.user_id = 3251
OR dcp.device_id = 61329
)
AND dcp.custom_param_id = dcpd.id
AND dcpd.param_id = 'department'
AND (dcpd.tenant_id = 1 OR dcpd.tenant_id = 0 OR dcpd.tenant_id = 1)
AND dcp.customer_id = 1
AND dcpd.customer_id = 1;
```
**Note:** The optimizer must choose a `PhysicalIndexMergeReader` plan for the `dcp` table due to the OR conditions across multiple indexed columns (`tenant_id`, `user_id`, `device_id`). Having sufficient data in the tables and up-to-date statistics may be required to trigger the Index Merge plan selection. The error occurs repeatedly in production with varying parameter values.
## 2. What did you expect to see? (Required)
The query should compile and execute successfully.
## 3. What did you see instead? (Required)
```
ERROR 1105 (HY000): Can't find column .devices_custom_params._tidb_rowid in schema Column:
[.devices_custom_params.tenant_id,
.devices_custom_params.user_id,
.devices_custom_params.device_id,
.devices_custom_params.custom_param_id,
.devices_custom_params.value,
.devices_custom_params.customer_id,
.devices_custom_params.id]
Unique key: []
```
The table has `PRIMARY KEY (id) CLUSTERED` with `int AUTO_INCREMENT`. The `_tidb_rowid` pseudo-column should not exist for this table, yet `PhysicalIndexMergeReader` constructs an `IntHandleCols` referencing `_tidb_rowid`, which fails during `ResolveIndices`.
## 4. Stack trace
```
expression.(*Column).resolveIndices column.go:686
expression.(*Column).ResolveIndices column.go:679
planner/util.(*IntHandleCols).ResolveIndices handle_cols.go:295
planner/core.(*PhysicalIndexMergeReader).ResolveIndices resolve_indices.go:479
planner/core/operator/physicalop.(*BasePhysicalPlan).ResolveIndices base_physical_plan.go:175
planner/core.(*physicalSchemaProducer).ResolveIndices resolve_indices.go:908
planner/core.(*PhysicalProjection).ResolveIndices resolve_indices.go:44
planner/core/operator/physicalop.(*BasePhysicalPlan).ResolveIndices base_physical_plan.go:175
planner/core.(*physicalSchemaProducer).ResolveIndices resolve_indices.go:908
planner/core.(*PhysicalMergeJoin).ResolveIndices resolve_indices.go:188
[...]
planner/core.physicalOptimize optimizer.go:1094
planner/core.doOptimize optimizer.go:291
planner.optimize optimize.go:540
planner.Optimize optimize.go:367
planner/core.generateNewPlan plan_cache.go:304
planner/core.GetPlanFromPlanCache plan_cache.go:241
planner.getPlanFromNonPreparedPlanCache optimize.go:123
planner.Optimize optimize.go:288
executor.(*Compiler).Compile compiler.go:102
session.(*session).ExecuteStmt session.go:2112
```
## 5. Analysis
The optimizer selects this plan:
```
PhysicalProjection
└── PhysicalMergeJoin
├── PhysicalProjection
│ └── PhysicalIndexMergeReader ← failure here
│ (scanning dcp via multiple indexes for the OR conditions)
└── (scan on dcpd)
```
`PhysicalIndexMergeReader` merges rows from partial index scans and constructs `IntHandleCols` to identify rows by handle. Instead of using the clustered PK column (`id`), it references `_tidb_rowid`. Since `_tidb_rowid` does not exist for `CLUSTERED` integer PK tables, `ResolveIndices` fails.
Key conditions:
- Table with `CLUSTERED` integer PK (`_tidb_rowid` should not exist)
- OR conditions across multiple separately-indexed columns, triggering Index Merge plan
- Cross-table join (`PhysicalMergeJoin`) wrapping the `IndexMergeReader` in projections
- Code path passes through `getPlanFromNonPreparedPlanCache`
## 6. Workaround
```sql
SET GLOBAL tidb_enable_index_merge = OFF;
```
Or per-query:
```sql
SELECT /*+ NO_INDEX_MERGE() */ ...
```
## 7. Frequency
Multiple times per day in production since at least 2026-02-13, across different connections and varying parameter values. Not a one-off event.
## 8. Version
```
Release Version: v8.5.5
Edition: Community
Git Commit Hash: 1fa258b
Git Branch: HEAD
UTC Build Time: 2026-01-14 22:20:57
GoVersion: go1.25.5
Race Enabled: false
Check Table Before Drop: false
Store: tikv
```
Contributor guide
Assessment
This issue has not been assessed yet.