pingcap / pingcap/tidb

TiDB TTL always use wrong index

Open
#69,967 7 comments 1 reaction 0 assignees View on GitHub
component/ddl contribution first-time-contributor severity/moderate sig/planner sig/sql-infra type/bug
Dominant language
Go
Stars
40.5k
Forks
6.2k
PR merge metrics
PR metrics pending

Description

# TiDB TTL 扫描 SQL 选择 `create_time` 索引,导致清理任务效率低且 Hint Plan Binding 无法创建

## 背景

表 `lego_leju_pic.leju_record` 配置了基于 `create_time` 的 TTL。TTL job 会按主键 `id` 区间分片扫描,每批扫描 500 行,实际执行的扫描 SQL 形态如下:

```sql
SELECT LOW_PRIORITY `id`
FROM `lego_leju_pic`.`leju_record`
WHERE `id` > 2865434175
AND `id` < 2943154302
AND `create_time` < FROM_UNIXTIME(1779170146)
ORDER BY `id` ASC
LIMIT 500;
```

其中 `id` 为主键,`create_time` 存在二级索引。

## 问题描述

优化器会为 TTL 扫描选择 `create_time` 二级索引,而该 SQL 同时具备:

- 主键 `id` 的范围条件;
- `ORDER BY id ASC`;
- 基于主键顺序推进的分页扫描语义。

选择 `create_time` 索引后,需要额外处理 `id` 范围和排序,TTL 扫描性能明显变差。希望 TTL 任务在该 SQL 形态下优先/强制走 `PRIMARY`。

尝试通过 optimizer hint 创建 Global Plan Binding:

```sql
CREATE GLOBAL BINDING FOR
SELECT LOW_PRIORITY `id`
FROM `lego_leju_pic`.`leju_record`
WHERE `id` > 2865434175
AND `id` < 2943154302
AND `create_time` < FROM_UNIXTIME(1779170146)
ORDER BY `id` ASC
LIMIT 500
USING
SELECT /*+ USE_INDEX(`leju_record`, PRIMARY) */ LOW_PRIORITY `id`
FROM `lego_leju_pic`.`leju_record`
WHERE `id` > 2865434175
AND `id` < 2943154302
AND `create_time` < FROM_UNIXTIME(1779170146)
ORDER BY `id` ASC
LIMIT 500;
```

创建失败,报错:

```text
ERROR 8066 (HY000): Optimizer hint can only be followed by certain keywords like SELECT, INSERT, etc.
```

原因是 TTL SQL 带有 `LOW_PRIORITY`,而 optimizer hint 与该 SELECT modifier 的解析/Plan Binding 组合不兼容。

## 期望行为

1. TTL 内部扫描 SQL 在存在 `id` 范围和 `ORDER BY id ASC` 时,能够正确评估主键扫描的成本,不应不合理地选择 `create_time` 索引。
2. 对带 `LOW_PRIORITY` 的 TTL 内部 SQL,能够使用 optimizer hint 创建并命中 Plan Binding;或者官方提供受支持的 TTL 扫描索引指定方式。

## 实际行为

1. TTL 扫描选择 `create_time` 二级索引,清理效率下降。
2. 使用 `SELECT /*+ USE_INDEX(...) */ LOW_PRIORITY ...` 创建 binding 时触发 Error 8066。
3. 去除 `LOW_PRIORITY` 可以创建 binding,但该 binding 无法匹配 TTL 的内部 SQL,因此不能影响 TTL 任务。

## 实测性能对比

在相同 TTL SQL 形态、相同 `LIMIT 500` 下,观察到两种访问路径的执行时间存在数量级差异:

| 访问路径 | 单次实际耗时 | 扫描特征 |
| --- | ---: | --- |
| `PRIMARY`(按 `id` 范围扫描) | 约 **1.1 秒** | 执行计划中仅处理约 992 个 key,`TableRangeScan` 后可直接按 `id` 顺序返回结果。 |
| `idx_create_time(create_time)` 二级索引 | 约 **29.8 分钟** | 执行计划显示 `IndexRangeScan` 实际扫描约 **3,629,819,011** 个 key,随后还需处理 `id` 范围过滤和 `ORDER BY id LIMIT 500`。 |

从慢查询记录可见,同类 TTL SQL 同时存在约 1.1 秒和约 29.8 分钟的执行样本,和执行计划中主键路径、`create_time` 二级索引路径的差异一致。

这表明该问题不是轻微的成本估计偏差:二级索引路径产生了极大的扫描放大,导致 TTL job 在调度窗口内难以及时完成清理。

## 可复现步骤

1. 创建带主键 `id`、TTL 时间列 `create_time` 和 `create_time` 二级索引的表。
2. 为 `create_time` 配置 TTL,并确保表内有足够的数据。
3. 观察 TTL task 的扫描 SQL / 执行计划。
4. 尝试上述带 optimizer hint 的 `CREATE GLOBAL BINDING`。

## 当前可行的规避方案

使用表级 `USE INDEX(PRIMARY)`,而不是 optimizer hint。该写法可保留 `LOW_PRIORITY`,且能与 TTL SQL 的规范化文本匹配:

```sql
CREATE GLOBAL BINDING FOR
SELECT LOW_PRIORITY `id`
FROM `lego_leju_pic`.`leju_record`
WHERE `id` > 2865434175
AND `id` < 2943154302
AND `create_time` < FROM_UNIXTIME(1779170146)
ORDER BY `id` ASC
LIMIT 500
USING
SELECT LOW_PRIORITY `id`
FROM `lego_leju_pic`.`leju_record` USE INDEX(PRIMARY)
WHERE `id` > 2865434175
AND `id` < 2943154302
AND `create_time` < FROM_UNIXTIME(1779170146)
ORDER BY `id` ASC
LIMIT 500;
```

创建后可使用以下语句检查 binding:

```sql
SHOW GLOBAL BINDINGS;
```

> 注意:该方案属于执行计划绑定规避措施。建议同时确认表统计信息是否过期,并评估主键扫描在过期数据稀疏时的实际扫描放大情况。

## 补充信息

- TiDB 版本:`v7.1.5`
- 表结构(`SHOW CREATE TABLE`):`<请补充>`
- `EXPLAIN` / `EXPLAIN ANALYZE` 输出:`<请补充>`
- TTL job/task 记录:`<请补充>`
- `ANALYZE TABLE` 最近执行时间及统计信息健康度:`<请补充>`

Contributor guide

Open the contributing guide

Research direction

Start by reproducing the provided TTL scan SQL and CREATE GLOBAL BINDING statements on TiDB v7.1.5, then inspect the TTL job/task path and optimizer hint and Plan Binding handling. Compare EXPLAIN ANALYZE for PRIMARY and idx_create_time with current statistics. Done means the supported behavior is documented or the TTL scan and binding path consistently use the intended index.

Written by the indexing model from the issue text.

Assessment

Tech stack
go, sql
Domain
databases, performance
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.