← Back to skills
extension
Category: Development & EngineeringAPI key requirement unconfirmed

SQL 执行计划医生

读 EXPLAIN、定位全表扫描与索引失效,输出可直接验证的索引方案与 SQL 改写。

personAuthor: Castorice123hubOpenAPI

SQL 执行计划医生

把“这条 SQL 为什么慢”从经验猜测变成一条可复现的诊断流水线:收齐输入 → 读计划 → 定量归因 → 开处方 → 给验证。 适用于 MySQL 8 / OceanBase / 兼容 MySQL 协议的分析型与 OLTP 查询。

一、先收齐输入,缺什么就问什么

| 输入 | 为什么必须有 | 缺失时怎么办 | | --- | --- | --- | | SQL 原文(含参数占位符) | 一切分析的起点 | 必须先要到 | | 建表 DDL(含全部索引) | 判断“有没有索引”“索引列序对不对” | 缺 DDL 不要给建索引建议,先要 SHOW CREATE TABLE | | EXPLAIN 输出 | 定位瓶颈算子 | 给出采集命令让用户补 | | 表行数量级 + 关键列区分度 | 判断 10 万行全表扫和 1 亿行全表扫不是一回事 | 用 COUNT(*) 或统计信息估算 | | 实际耗时 / 返回行数 | 建立基线,验证优化是否有效 | 从慢日志取 |

采集命令:

SHOW CREATE TABLE orders\G
EXPLAIN SELECT ...;                       -- MySQL
EXPLAIN EXTENDED SELECT ...;              -- OceanBase,带 cost
EXPLAIN FORMAT=JSON SELECT ...;           -- 需要更细的估算时
SHOW TABLE STATUS LIKE 'orders';          -- 行数级、平均行长

二、五步工作流

Step 1 · 建立基线。 记录:耗时、扫描行数、返回行数、调用频次。先明确目标——是降单次耗时,还是降扫描量(影响 CPU/IO 总量)。

Step 2 · 读计划,找扇出点。 从最内层算子往外读,对每个算子算 rows × filtered% 得到估算输出行数。瓶颈通常不是最慢的算子,而是输出行数最多的那个——它决定了上层所有算子的工作量。

Step 3 · 归因。 对照下表落到具体一类,不要停在“索引没走”这种描述上。

Step 4 · 开处方。 索引优先于改写,改写优先于改表结构。每条处方必须写清:预期收益、副作用(写放大 / 索引膨胀 / 语义风险)、实施成本。

Step 5 · 验证。 给出 before/after 的采集命令与判定标准(扫描行数下降多少、是否消除 filesort),以及回滚语句。

三、根因 → 处方对照表

| 症状(计划里的证据) | 常见根因 | 首选处方 | | --- | --- | --- | | type=ALL,rows 与表行数同量级 | 无可用索引 / 索引列被函数或运算包裹 | 建组合索引;去函数包裹改成范围条件 | | possible_keys 有值但 key=NULL | 优化器算出来走索引更贵(选择率低 / 回表代价高) | 覆盖索引消除回表;或强制索引验证 | | key_len 明显小于索引定义长度 | 组合索引只用到前几列(最左前缀断了) | 调整索引列序:等值列在前,范围列在后 | | filtered 很低而 rows 很大 | 索引只做了粗过滤,剩大量回表再过滤 | 把过滤列并入索引;覆盖索引 | | Using filesort + rows 大 | 排序字段不在索引顺序里 | 组合索引把 ORDER BY 列放最后 | | Using temporary | GROUP BY / DISTINCT / UNION 需要物化 | 覆盖索引让分组有序;提前聚合缩小结果集 | | Using join buffer (hash join) | 被驱动表关联列无索引 | 给被驱动表关联列建索引,让小表驱动 | | rows 估算与实际差一个数量级 | 统计信息过期 / 数据倾斜 | ANALYZE TABLE;对倾斜值单独处理 | | 深分页 LIMIT 100000,20 慢 | 扫描并丢弃前 10 万行 | 延迟关联 / 游标分页(WHERE id > ?) |

四、固定输出格式

回答必须按这个结构,便于对照和复制:

### 1. 结论
一句话:瓶颈在哪个表、哪个算子、由什么造成。

### 2. 证据
引用计划中的具体算子与数字(rows / filtered / key_len / Extra),不要转述。

### 3. 处方(按收益 ÷ 风险排序)
1. [索引] ALTER TABLE ... ADD INDEX ...  —— 预期:扫描行数 987万 → 3千;风险:写放大
2. [改写] ...                              —— 预期:消除 filesort;风险:无

### 4. 验证方案
采集命令 + 判定标准 + 回滚语句。

### 5. 不确定项
缺哪些信息、哪些结论需要实测才能确认。

五、红线

  • 不改变业务语义。 NULL 语义、排序稳定性、去重、事务隔离级别,改一条都要显式说明。
  • 不臆造。 没看到 DDL 就不写“表里应该有 index”;没看到数据量就不写“全表扫描很慢”。
  • 索引建议必须给列序理由,不能只写“加个索引就好了”。
  • DDL 只生成不执行,并加注释提示在低峰期用 online DDL。
  • 单表索引不超过 5 个,新增前先检查是否与既有索引前缀重复。
  • 计划里的 rows 是估算值;当估算与实际差一个数量级时,先怀疑统计信息,再怀疑索引。

六、配合脚本

# 把 EXPLAIN 输出丢进去,得到红旗清单 + 索引候选
python scripts/plan_check.py examples/explain-before.txt \
    --ddl examples/schema.sql --sql examples/query.sql

脚本做的是机械劳动(解析、比对阈值、拼索引候选),归因和取舍仍然按上面的流程人工判断。脚本输出的候选索引要回到 Step 4 复核列序与选择性。

七、参考资料

| 文件 | 内容 | | --- | --- | | references/explain-reading.md | 表格型 EXPLAIN 各列语义、type 等级、key_len 反推、OceanBase 计划树算子 | | references/rewrite-playbook.md | 12 个“症状 → 处方”改写模式,含错误写法与正确写法 | | examples/case-01-missing-index.md | 完整案例:1000 万行订单表按状态 + 时间范围查询,2.4s → 38ms | | examples/case-02-index-not-used.md | 完整案例:varchar 列被隐式转成数字,索引静默失效 |

<!-- OB-S:JW2_C4vUjx7_Zy1DPd1ZZA -->