SQL 执行计划分析
这个技能解决什么
把一段 EXPLAIN 输出翻译成「问题清单 + 优化动作」,而不是让用户自己去比对十来个列。
重点是给出可执行的下一步:改哪一行 SQL、建哪个索引、改完怎么验证。
覆盖三种输入格式,自动识别:
- 表格型 —— MySQL / OceanBase MySQL 模式 / MariaDB 的
select_type / type / key / rows / Extra - 算子树型 —— OceanBase 的
|ID|OPERATOR|NAME|EST.ROWS|EST.TIME| - TREE 型 —— MySQL 8 的
EXPLAIN FORMAT=TREE,即以->开头的树
何时使用
- 用户贴出
EXPLAIN/EXPLAIN ANALYZE结果,问「为什么慢」「怎么优化」 - 用户描述某条 SQL 慢,但没有执行计划 → 先请他提供计划,不要凭空猜
- 用户问「这个查询能不能走索引」「该建什么索引」「联合索引列顺序怎么排」
- 用户在做 SQL 调优、慢查询治理、上线前评审
何时不用
- 纯粹问 SQL 语法、函数用法 → 不需要本技能
- 库表设计、分库分表选型 → 属于架构决策,本技能只覆盖单条查询的执行路径
- 没有执行计划且无法获取 → 先说明为什么必须要计划(优化器的选择无法靠肉眼推断)
前置条件
- 一段真实的
EXPLAIN输出(文本即可,粘贴进对话或存成文件) - 尽量同时提供原始 SQL:没有 SQL 就无法判断过滤条件、排序键,只能给出方向性建议
- 表的大致数据量级(万级和亿级的结论完全不同)
工作流程
Step 1 · 收集并确认输入
-
确认拿到的是
EXPLAIN(估算)还是EXPLAIN ANALYZE(实测)。 -
若只有 SQL 没有计划,请用户先执行:
EXPLAIN <原始SQL>; -- 需要实测行数时 EXPLAIN ANALYZE <原始SQL>; -
若用户只给了结论(「这条很慢」),追问三件事:执行计划、表行数、实际耗时。
-> 告知用户:已确认输入格式(表格 / 算子树 / TREE),以及是否缺少原始 SQL。
Step 2 · 跑静态体检脚本
把计划文本交给脚本,先拿机器可判定的问题,避免人工漏看:
# 计划存文件
python3 scripts/explain_lint.py --file explain.txt
# 同时提供原始 SQL,脚本会额外给出候选索引
python3 scripts/explain_lint.py --file explain.txt --query query.sql
# 需要结构化结果时
python3 scripts/explain_lint.py --file explain.txt --json
# 也可以直接从标准输入读
pbpaste | python3 scripts/explain_lint.py
脚本做的是确定性规则匹配:全表扫描、候选索引未命中、filesort、临时表、join buffer、隐式转换、扫描行数异常。它不懂业务语义,所以脚本结论只是起点,不是最终答案。
-> 告知用户:脚本报了哪几项、分别是什么级别。
Step 3 · 人工复核脚本漏掉的语义问题
脚本看不到的东西,必须自己判断:
- 选择性:
filtered很低 +rows很大 = 索引区分度不够,而不是缺索引 - 索引顺序:等值列 → 范围列 → 排序列。范围列后面的等值条件基本用不上
- 回表代价:
SELECT *会放大回表;覆盖索引能省掉 - 连接顺序:被驱动表的关联列必须有索引,否则
join buffer出现 - 统计信息:
rows和实际差一个数量级时,先ANALYZE TABLE,别急着加索引
细节查 references/oceanbase-explain.md(列的语义、type 优劣序、Extra 信号、算子树读法)。
改写范式查 references/anti-patterns.md(12 类高频反模式,每条带改写前后对比)。
-> 告知用户:正在核对语义层面的问题,并说明依据。
Step 4 · 给出诊断报告
固定按下面的结构输出,避免漏项:
## 结论
一句话说清瓶颈在哪(如:orders 表全表扫描 + filesort 排序)。
## 问题清单
| 级别 | 对象 | 问题 | 依据 | 建议 |
| --- | --- | --- | --- | --- |
| 高 | orders | 全表扫描 type=ALL | 预估 98 万行 | ... |
| 中 | orders | 排序未走索引 | Using filesort | ... |
## 优化动作
1. 建索引(给出的 DDL 必须能直接执行)
2. SQL 改写(给出改写前后对照)
3. 其他(统计信息、参数、分页方式)
## 验证方法
改完再跑 EXPLAIN,预期看到:type 从 ALL 变 range;Extra 里 filesort 消失;rows 下降 N 倍。
## 风险提示
写入放大、索引磁盘占用、DDL 对大表的影响(在线 DDL 可行性)。
报告里每个「建议」都要能被验证,写清改之前看到什么、改之后应该看到什么。
-> 告知用户:报告已给出,重点看「结论」和「验证方法」两节。
Step 5 · 收尾检查
- 是否给了 DDL?会不会漏掉列顺序的理由?
- 是否说明了索引的代价(写放大、空间)?
- 是否提示在真实数据量级复测?
- 有没有把「估算」说成「实测」?
示例
示例 1:电商订单查询慢
用户输入:
+----+-------------+--------+------+---------------+------+---------+------+--------+----------+----------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------+---------------+------+---------+------+--------+----------+----------------------------------+
| 1 | SIMPLE | orders | ALL | idx_user | NULL | NULL | NULL | 982341 | 10.00 | Using where; Using filesort |
+----+-------------+--------+------+---------------+------+---------+------+--------+----------+----------------------------------+
SELECT * FROM orders WHERE user_id = 42 AND status = 'PAID'
AND created_at > '2026-01-01' ORDER BY created_at DESC LIMIT 20;
期望输出要点:
- 结论:
type=ALL全表扫描 98 万行,且Using filesort额外排序 - 动作:
CREATE INDEX idx_orders_user_id_status_created_at ON orders (user_id, status, created_at);等值user_id、status在前,范围/排序列created_at在后,一个索引同时解决过滤和排序 - 验证:
type应变range,Extra里的filesort消失,rows应从 98 万降到百级
示例 2:OceanBase 算子树里的全表扫描
|ID|OPERATOR |NAME |EST.ROWS|EST.TIME(us)|
|0 |SORT | |1000 |2500 |
|1 |└─TABLE SCAN|t_order|1000 |900 |
期望输出要点:指出根节点下的 SORT 依赖 TABLE SCAN 的输出,两者可以合并消除 —— 建 (status, created_at) 索引后,扫描与排序由一个索引有序性同时满足。
示例 3:索引有了却没用上
现象:possible_keys=idx_phone 但 key=NULL。
排查顺序:条件里是否对 phone 做了类型转换/函数调用 → 字符集是否与常量一致 → 是否用了 !=、LIKE '%x' 这类不可索引的形态。
故障排查
| 现象 | 原因 | 处理 |
| --- | --- | --- |
| 脚本报「输入格式 unknown」 | 粘贴时表格对齐被破坏 | 保留 | 分隔符原样粘贴,或用 EXPLAIN FORMAT=JSON 前的文本格式重跑 |
| 脚本没报问题但查询确实慢 | 慢在锁等待、网络传输、返回行数、或 SELECT 列过宽 | 这不是执行计划问题,转向 EXPLAIN ANALYZE 看实际耗时分布 |
| 按建议建了索引还是不走 | 统计信息过期、区分度太低、或优化器认为全表扫更便宜 | 先 ANALYZE TABLE;小表全表扫是合理的,不必强求索引 |
| 建索引后写入变慢 | 每次写入要维护 B+ 树 | 精简索引数量,合并冗余索引(前导列重复的可以合并) |
| 线上不敢加索引 | 大表 DDL 有锁风险 | 评估在线 DDL 能力与业务低峰窗口,先说明风险再动手 |
参考文件
references/oceanbase-explain.md—— 列语义、type优劣序、Extra信号表、OceanBase 算子树读法references/anti-patterns.md—— 12 类慢 SQL 反模式与改写范式,附复核清单scripts/explain_lint.py—— 静态体检脚本,支持表格 / 算子树 / TREE 三种格式,可输出 Markdown 或 JSON
使用边界
- 只做读取与分析,不连接数据库、不执行 DDL。所有变更由用户在自己的环境确认后执行。
- 索引建议基于执行计划和 SQL 文本推断,必须在近似生产的数据量上复核后再上线。
- 不处理权限、备份、容灾等非性能话题。
Scan to join WeChat group