优化器坚持走全表扫描不走复合索引

【TiDB 使用环境】
生产集群,3 PD / 3 TiDB / 6 TiKV,TiDB 与 PD 混合部署在同一批机器上,TiKV 独占,NVMe 盘。
【TiDB 版本】
v7.5.1(TiDB 7.5 LTS)
【部署方式】
使用 TiUP 离线部署,3 台物理机,每台 64C / 256G。
【问题描述】
orders 表目前约 2.3 亿行,数据量 130GB 左右。表结构里除了自增主键 id,还建了复合索引 idx_user_time(user_id, created_at) 和单列索引 idx_status(status)。
业务高频查询长这样:
SELECT order_id, user_id, status, amount, created_at
FROM orders
WHERE user_id = 881423 AND status = ‘PAID’ AND created_at >= ‘2026-10-01’
ORDER BY created_at DESC LIMIT 50;
EXPLAIN 一打出来是 TableFullScan + Sort,rows 估算 2.3 亿。explain analyze 实测 scan 了全表,耗时 8.2s。按理说 user_id 基数很高(接近 9000 万),点查应该走索引,结果优化器认定全表更便宜。
【已尝试】

  1. 跑了 ANALYZE TABLE orders; 之后 SHOW STATS_HEALTHY 显示该表 healthy 是 100,但执行计划没变,还是全表。
  2. 用 SELECT *+ USE_INDEX(orders, idx_user_time) * … 强制走索引,耗时降到 120ms,确认索引本身没问题、数据分布也正常。
  3. 调小 tidb_auto_analyze_ratio 想让它更积极收集统计信息,没效果。
  4. 试过把 idx_status 删掉,计划也没变,说明不是选错单列索引的问题,是压根没考虑索引检索这条路。
    【问题】
  5. user_id 区分度这么高,优化器为什么还觉得全表扫描代价更低?是不是统计信息里 user_id 的 NDV 被低估了,或者多个等值+范围条件的组合让它算偏了?有没有办法直接看它对这个查询算出来的各路径 cost?
  6. 现在靠 hint 能跑快 70 倍,但 hint 写死在业务 SQL 里维护起来很烦。不改 SQL 的话,用 SQL Plan Management(CREATE GLOBAL BINDING)绑执行计划靠谱吗?绑定后统计信息再变化会不会失效?

这个计划不是优化器看不见索引,是它把你这三个条件估成了几乎全部命中——那行 rows 直接等于表行数 2.3 亿,选择性被算成 1,全表扫描当然显得更便宜。而且你这个 ORDER BY created_at DESC 跟 idx_user_time 的后缀正好对得上,索引路径连 Sort 都不用,按每个 user_id 两三行的估算它根本没理由输,反过来也说明是估算坏了。

最便宜的一个可能性先排掉,就是 created_at 的类型。它要是 int、存的时间戳,'2026-10-01' 这种字符串会走隐式转换,范围条件可能退化成恒真,表现正好就是选择性 1、est rows 等于表行数。SHOW CREATE TABLE orders 看一眼类型,或者把条件换成 CAST('2026-10-01' AS DATETIME) 或者对应的时间戳常量再 EXPLAIN 一次,计划立刻变索引的话那就是它。

类型没问题再查统计。SHOW STATS_HEALTHY 是表级的,只反映"表改了百分之多少、该不该重收集",不代表 idx_user_time 这个索引的直方图还在——索引统计要用 SHOW STATS_HISTOGRAMS WHERE Table_name='orders' AND Is_index=1 去看,重点看有没有 idx_user_time 这一行、Distinct_count 和 Update_time 是什么,顺便把 user_id 那列的 Distinct_count 跟你 SELECT COUNT(DISTINCT user_id) FROM orders 对一下。索引行压根不在,或者 NDV 被压得很低,问题就在这儿。

判断档位也很快:全表扫那步 est rows 是 2.3 亿,属于"条件没生效",往上查类型和列统计;如果是两千万这种量级,那就是 NDV 被低估,加 hint 那份 EXPLAIN 的 est rows 一定明显更小,两份摆一起就看得出来。

想让我们直接定位,用 PLAN REPLAYER DUMP EXPLAIN SELECT ... 把那条 SQL 的计划、统计、变量打成一个 zip 发我,我拉优化的同学一起看,比自己猜快得多。

SQL Plan Management 是能用的,CREATE GLOBAL BINDING 绑的是 SQL 指纹加当时那条计划用到的 hint 组合,统计信息后面再变不会让绑定自己失效,比你写死在业务 SQL 里干净。但它有两个前提要知道,SQL 文本一改就可能算另一个指纹(换绑定变量写法、加减一个注释甚至空格),匹配不上就走回默认计划;索引被删或者改名也会失效。所以我更建议先开 tidb_capture_plan_baselines 让它自己抓几天,确认这条语句一直拿到好计划,再决定要不要手动绑。反正 hint 能快 70 倍已经说明索引路径本身是划算的,多半是统计这块坏了,统计修对大概率就不用绑。

1 个赞

想看 cost 用 EXPLAIN FORMAT=‘cost_trace’,多出 estCost、costFormula

created_at 和user_id是什么类型,有没有隐式转换。
不想加hint用binding方式简单很多,一条命令就好了
CREATE [GLOBAL|SESSION] BINDING | TiDB 社区版