MySQL 慢查询先查哪条:performance_schema digest Top-20 只读脚本与判读
一条只读 SQL 从 events_statements_summary_by_digest 取出按累计耗时排序的前 20 条,附 MySQL 8.0.46 实测输出、加索引前后 EXPLAIN ANALYZE 对比。
先按 SUM_TIMER_WAIT 总耗时排序,不要只盯单次最慢;用 no_index_used 和 avg_rows_examined 判断要不要动索引,再用 EXPLAIN ANALYZE 核实。
适用对象
- 排序看 total_s(累计耗时),不看 mean:一条 23ms 但跑了几十次的语句,往往比一条偶发的 2 秒报表更值得先处理。
- no_index_used / avg_rows_examined 比“感觉慢”更早提醒你缺索引;pct_total_time 告诉你优化上限。
- 脚本只读,不 TRUNCATE 摘要表,也不改消费者或参数。
- DIGEST_TEXT 已被规范化并截成 80 字符;外发前仍要自己检查表名是否敏感。
先确认摘要表在采集
执行 `SHOW VARIABLES LIKE 'performance_schema';`,必须是 ON。再看 `SELECT COUNT(*) FROM performance_schema.events_statements_summary_by_digest;`——如果长期是 0,多半是消费者没开,或者实例刚启动还没有任何语句。
普通已认证账号一般就能读这张摘要表;读不到时不要擅自 `UPDATE performance_schema.setup_consumers`,那是生产变更,交给负责人。
摘要是累计值,不是“最近五分钟”。我这次测试库刚建完,所以 Top-1 居然是建数用的 INSERT——生产里对应批处理/导入,要先和业务确认是不是预期负载,再决定是否进优化清单。
Top-20 输出每一列怎么用
脚本不包含任何 SET/DDL。客户端侧请自己设超时(例如 mysql 会话 `max_execution_time`,或连接工具的 query timeout)。用 `mysql -N -B shop < mysql-slow-query-topn-digest-readonly-v1.0.sql > topn.tsv` 可直接落盘。
| 列 | 含义 | 我怎么用 |
|---|---|---|
| calls / total_s / mean_s / max_s | 次数、累计秒、平均秒、单次最大秒 | 先看 total_s 排名,再分“次数多”还是“单次慢” |
| rows_examined / avg_rows_examined | 扫描行数合计与均值 | 均值接近表行数且 no_index_used>0,优先怀疑缺索引 |
| pct_total_time | 占本结果集总耗时的百分比 | 优化上限:占 5% 的语句优化到 0 也只省 5% |
| no_index_used / no_good_index_used | 未用索引 / 未用合适索引的次数 | 和 EXPLAIN 对照,不要只看名字吓人 |
| digest_head / digest | 规范化语句前 80 字符 + digest 哈希 | 定位代码;外发前检查表名 |
一次测试输出,以及加索引前后的 EXPLAIN ANALYZE
我故意让 `SELECT COUNT(*) FROM orders WHERE uid=?` 在没有 uid 索引的情况下跑 40 次。脚本里它排第二:calls=40,total_s=0.913,mean_s=0.0228,占 38.4% 总时间,avg_rows_examined=200000,no_index_used=40。排第一的是建数 INSERT(total_s=1.263),我把它标成“预期负载”跳过。
接着用 `EXPLAIN ANALYZE SELECT COUNT(*) FROM orders WHERE uid=7;`:执行计划是 Table scan on orders,actual time=29.8 ms,扫描 200000 行。加上 `INDEX idx_orders_uid (uid)` 后再测同一条:变成 Covering index lookup,actual time=0.075 ms。测试机、自造数据,只说明方向,不是生产 SLA。
| 指标 | 之前 | 之后 |
|---|---|---|
| 访问方式 | Table scan on orders | Covering index lookup (idx_orders_uid) |
| 扫描行数 | 200000 | 200 |
| actual time | 29.8 ms | 0.075 ms |
我踩过的两个坑
第一,Digest 文本里的关键字会被规范化成大写,例如 `status` 变成 `STATUS`。对照应用代码时按语义搜,不要精确字符串匹配。
第二,摘要表不会按“最近 N 分钟”自动切窗。实例跑了三个月,排第一的可能是早已下线的接口。生产上可以看 FIRST_SEEN / LAST_SEEN,必要时在低峰由负责人决定是否 `TRUNCATE` 摘要表——脚本本身不会做这件事。
从 Top-20 到改动
- 01
01
剔除预期负载
导入、备份、一次性 DDL,先和业务确认。
- 02
02
分清次数和单次
calls 大优先查应用侧重复请求;mean/max 大再看计划。
- 03
03
看 no_index_used
次数高且 avg_rows_examined 接近表大小时,准备 EXPLAIN ANALYZE。
- 04
04
在从库或低峰核实计划
EXPLAIN ANALYZE 会真正执行语句;写语句要包在事务里回滚。
- 05
05
再动索引
评估写入放大与磁盘;大表用 Online DDL,并准备回滚方案。
不要在生产上为了“看最新数据”随手 TRUNCATE events_statements_summary_by_digest,那会清掉所有人的历史。
脚本与测试样例
SQL 和本次测试输出的 CSV 都可以直接下载。样例 CSV 就是上面测试库的原始 Top-20,可用来对照列含义。
本节判断
- 适用:MySQL 8.0 / 8.4(已在 8.0.46 测试);5.7 的摘要列名不同,未验证。
- 权限:可读 performance_schema.events_statements_summary_by_digest。
- 边界:只读、最多 20 行;不截断摘要表、不改消费者。
参考依据
以下来源用于确认市场趋势、政策背景和术语边界;具体落地方案仍以客户的数据范围、权限和交付目标为准。
常见问题
和站内旧的“指纹摘要”脚本有什么区别?
旧卡故意不输出 DIGEST_TEXT,只给 digest 哈希,适合外发受限的场景。这篇的 Top-20 带截断后的 digest_head,方便本地对照代码;外发前请自行再脱敏。
云上 RDS / PolarDB 能用吗?
大多数 MySQL 8 兼容实例默认开启 performance_schema,账号需要有对应 SELECT。具体以云厂商文档和账号权限为准;本脚本未在各云分支逐一验证。
排名靠前的都是 mean 很小的语句怎么办?
说明压力来自调用量。优先在应用侧查 N+1、缺缓存和重复轮询;数据库侧能做的有限。