← Back to skills
extension
Category: Productivity & OfficeAPI key requirement unconfirmed

sql-explain-analyzer

分析 SQL 执行计划(EXPLAIN / EXPLAIN ANALYZE 文本),诊断全表扫描、索引失效、回表过多、filesort、临时表、隐式转换等性能问题,并给出可落地的索引与改写建议。当用户贴出 EXPLAIN 结果、说某条 SQL 很慢、问索引该怎么加、或想知道某条查询为什么走不到索引时使用。

personAuthor: zhaogoudanhubModelScope

SQL 执行计划分析

这个技能解决什么

把一段 EXPLAIN 输出翻译成「问题清单 + 优化动作」,而不是让用户自己去比对十来个列。 重点是给出可执行的下一步:改哪一行 SQL、建哪个索引、改完怎么验证。

覆盖三种输入格式,自动识别:

  1. 表格型 —— MySQL / OceanBase MySQL 模式 / MariaDB 的 select_type / type / key / rows / Extra
  2. 算子树型 —— OceanBase 的 |ID|OPERATOR|NAME|EST.ROWS|EST.TIME|
  3. TREE 型 —— MySQL 8 的 EXPLAIN FORMAT=TREE,即以 -> 开头的树

何时使用

  • 用户贴出 EXPLAIN / EXPLAIN ANALYZE 结果,问「为什么慢」「怎么优化」
  • 用户描述某条 SQL 慢,但没有执行计划 → 先请他提供计划,不要凭空猜
  • 用户问「这个查询能不能走索引」「该建什么索引」「联合索引列顺序怎么排」
  • 用户在做 SQL 调优、慢查询治理、上线前评审

何时不用

  • 纯粹问 SQL 语法、函数用法 → 不需要本技能
  • 库表设计、分库分表选型 → 属于架构决策,本技能只覆盖单条查询的执行路径
  • 没有执行计划且无法获取 → 先说明为什么必须要计划(优化器的选择无法靠肉眼推断)

前置条件

  • 一段真实的 EXPLAIN 输出(文本即可,粘贴进对话或存成文件)
  • 尽量同时提供原始 SQL:没有 SQL 就无法判断过滤条件、排序键,只能给出方向性建议
  • 表的大致数据量级(万级和亿级的结论完全不同)

工作流程

Step 1 · 收集并确认输入

  1. 确认拿到的是 EXPLAIN(估算)还是 EXPLAIN ANALYZE(实测)。

  2. 若只有 SQL 没有计划,请用户先执行:

    EXPLAIN <原始SQL>;
    -- 需要实测行数时
    EXPLAIN ANALYZE <原始SQL>;
    
  3. 若用户只给了结论(「这条很慢」),追问三件事:执行计划、表行数、实际耗时。

-> 告知用户:已确认输入格式(表格 / 算子树 / 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 文本推断,必须在近似生产的数据量上复核后再上线。
  • 不处理权限、备份、容灾等非性能话题。
<!-- OB-S:k1gteGh3jASrCUU4GNRmWg -->