ANALYZE TABLE (v2) panics "index out of range [-1]" when an index references a column absent from the analyzed column set — incl. clustered PK on a plain table
- Dominant language
- Go
- Stars
- 40.5k
- Forks
- 6.2k
- PR merge metrics
- PR metrics pending
Description
## Bug Report
Please answer these questions before submitting your issue. Thanks!
### 1. Minimal reproduce step (Required)
Repro A — index on generated columns (filtered out of the analyzed set):
```
CREATE TABLE t_gen (
col_pk varchar(100) NOT NULL,
payload json NOT NULL,
gen_a varchar(36) GENERATED ALWAYS AS (json_unquote(json_extract(payload,'$.a'))) STORED,
gen_b bigint GENERATED ALWAYS AS (cast(json_unquote(json_extract(payload,'$.b')) as unsigned)) STORED,
KEY idx_gen (gen_a, gen_b),
PRIMARY KEY (col_pk) /*T![clustered_index] CLUSTERED */
);
SET SESSION tidb_analyze_version = 2;
ANALYZE TABLE t_gen; -- panics: index out of range [-1]
```
filterSkipColumnTypes drops gen_a/gen_b (JSON-derived) from execColsInfo, but idx_gen still references them.
### 2. What did you expect to see? (Required)
ANALYZE completes and collects statistics.
### 3. What did you see instead (Required)
`ERROR 1105 (HY000): runtime error: index out of range [-1]`
The analyze worker panics (server logs analyze worker panicked); auto-analyze then retries the same table and re-panics, causing a crash loop.
```
executor.(*AnalyzeExec).analyzeWorker.func1 analyze.go:500 (recover)
runtime.goPanicIndex panic.go:115
executor.(*AnalyzeColumnsExecV2).analyzeColumnsPushDownV2 analyze_col_v2.go:79 <-- panic
executor.analyzeColumnsPushDownEntry analyze_col.go:69
executor.(*AnalyzeExec).analyzeWorker analyze.go:527
executor.(*AnalyzeExec).Next.func1 analyze.go:124
```
tidb_analyze_column_options = 'ALL' does not reliably avoid it: allColumns is a length-equality check (len(TableInfo.Columns) == len(execColsInfo)), and filterSkipColumnTypes can still trim columns.
### 4. What is your TiDB version? (Required)
8.5.5
### 5 RCA
Crash site — pkg/executor/analyze_col_v2.go, analyzeColumnsPushDownV2:
```
for i, idx := range e.indexes {
for _, col := range idx.Columns {
colInfo := e.colsInfo[col.Offset] // col.Offset == -1 -> panic
col.Offset becomes -1 upstream in pkg/planner/core/planbuilder.go:
```
`filterSkipColumnTypes` drops the generated columns gen_col_a / gen_col_b (they depend on the json column payload) from the analyzed set execColsInfo.
allColumns := len(tbl.TableInfo.Columns) == len(execColsInfo) ` → false (columns were filtered).
In getModifiedIndexesInfoForAnalyze, the non-allColumns branch remaps offsets: idx.Columns[i].Offset = getColOffsetForAnalyze(execColsInfo, colID), and getColOffsetForAnalyze returns -1 when the column isn't in execColsInfo. idx_gen references the filtered generated columns → -1 → consumed unguarded at analyze_col_v2.go:79.
Notes:
Columns are STORED (IsVirtualGenerated() == false), so this is not #61606 / PR #62333 (first-column virtual column, decode path). That PR does not touch analyze_col_v2.go:79 or the offset remap, so it does not fix this.
Column position is irrelevant; trigger is "a secondary index references a generated column filtered out of the analyzed set."
tidb_analyze_column_options = 'ALL' does not avoid it: allColumns is a length-equality check and filterSkipColumnTypes still trims the generated columns. Reproduces under both PREDICATE and ALL.
### 6 suggest fix
In getModifiedIndexesInfoForAnalyze (planbuilder.go), when getColOffsetForAnalyze returns -1 for any index column, do not assign a -1 offset — route that index to the independent/special (NDV-only) handling instead of the sampling path. Optionally add a defensive col.Offset < 0 guard at the consumers in analyze_col_v2.go (analyzeColumnsPushDownV2 and the subBuildWorker index sample-collect loop).
### 7 Workaround
`
SET SESSION tidb_analyze_version = 1;
ANALYZE TABLE sample_table;
`
Contributor guide
Research direction
Reproduce the failure with the provided generated-column schema and ANALYZE commands, then read getModifiedIndexesInfoForAnalyze in pkg/planner/core/planbuilder.go and analyzeColumnsPushDownV2 in pkg/executor/analyze_col_v2.go. Trace how filtered generated columns produce an offset of -1; done means ANALYZE completes without a panic for both PREDICATE and ALL options.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- databases
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100