最后更新:2026-10-31
适用环境:MySQL 8.0.21 及以上的 8.0 版本、MySQL 8.4,InnoDB;示例使用 mysql 客户端
接口开始超时,日志里 SQL 看起来也很慢,先别马上加索引。时间可能花在等连接、等锁、扫描和排序,也可能花在结果传输或应用处理上。先把慢的那一段找到,再决定该改 SQL、事务还是资源配置。
文中的 app_db、orders 和参数都是示例,执行前换成实际库表与有代表性的参数。查询其他会话、系统视图和日志需要相应权限;权限或采集配置不足时,空结果不能作为“没有问题”的证据。
先确认:是数据库执行慢,还是接口整体慢
先对齐故障时间、请求标识、实际数据库实例、SQL 与绑定参数。读写分离的项目尤其要确认:应用请求落到的是哪个主库或副本,排查连接是否也连到了那里。
在目标实例记录版本和时间,再查看正在运行的会话:
SELECT NOW() AS db_time,
VERSION() AS mysql_version,
DATABASE() AS current_schema;
SHOW FULL PROCESSLIST;
SHOW FULL PROCESSLIST 是当时的快照,不是历史慢 SQL 列表。关注执行时长、状态、库和完整语句,再结合业务调用链定位;不能仅凭某条连接时间长就判断它正在执行慢查询。
| 观察到的现象 | 先查哪里 |
|---|---|
| 接口等待很久,但数据库语句执行时间不长 | 连接获取、网络、结果传输、应用处理 |
| 少数语句持续等待,其他查询基本正常 | 锁等待、长事务与 DDL |
| 同类查询扫描量增加或执行计划变化 | 参数、数据分布、索引和统计信息 |
| 多种查询同时变慢 | 实例 CPU、磁盘 I/O、活跃会话、并发变化与后台任务 |
应用中的“SQL 耗时”覆盖哪些阶段,取决于埋点位置。有的日志没有包含获取连接的等待,有的则包含结果读取。若连接池已经排队,可先对照 Spring Boot 数据库连接池耗尽,不要把连接获取慢直接归因于某条 SQL 的索引。
先排锁等待,避免优化错方向
同一条 SQL 平时很快,偶尔突然等很久,应先看它是否在等待其他事务。sys 库可用且相关采集已开启时,可以查看 InnoDB 等待链:
SELECT waiting_pid,
blocking_pid,
wait_age_secs,
waiting_query,
blocking_query
FROM sys.innodb_lock_waits
ORDER BY wait_age_secs DESC
LIMIT 10;
SELECT trx_mysql_thread_id,
trx_started,
trx_state,
trx_query
FROM information_schema.innodb_trx
ORDER BY trx_started;
waiting_pid 和 blocking_pid 是 MySQL 会话的 processlist ID,不是操作系统 PID。把等待方、阻塞方和业务事务对应起来,再看事务从什么时候开始、为什么迟迟没有结束。innodb_lock_waits 官方说明列出了这些字段。
阻塞连接可能已经进入空闲状态,blocking_query 或 trx_query 也可能为空,但事务仍未提交。此时只看正在执行的 SQL 容易漏掉持锁方,应结合连接所属服务、事务开始时间和应用日志追踪。
这个视图主要帮助查 InnoDB 数据锁等待,不代表所有锁。若状态是等待表的元数据锁,应继续检查 DDL 与未结束事务,不能因为这里没有记录就排除锁问题。
先确认事务归属和业务影响,再决定提交、回滚或中止会话;不要把查询输出中的会话 ID 直接拼成批量 KILL。后续修复通常落在事务范围、执行顺序、批量大小或未及时结束的连接上。若实际报错为死锁,可继续看 MySQL 死锁怎么定位,与单纯等待锁区分处理。
从语句摘要找出值得优先处理的查询
故障过去后,processlist 可能已经没有现场。Performance Schema 的语句摘要可以按同类 SQL 查看执行次数和累计耗时。下面需要 Performance Schema 及相应的语句、摘要采集已启用,并具有读取权限:
SELECT SCHEMA_NAME,
DIGEST_TEXT,
COUNT_STAR,
ROUND(SUM_TIMER_WAIT / 1000000000000, 2) AS total_seconds,
ROUND(AVG_TIMER_WAIT / 1000000000, 2) AS avg_ms,
ROUND(MAX_TIMER_WAIT / 1000000000, 2) AS max_ms,
SUM_ROWS_EXAMINED,
SUM_ROWS_SENT
FROM performance_schema.events_statements_summary_by_digest
WHERE SCHEMA_NAME = 'app_db'
AND DIGEST IS NOT NULL
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;
原始计时值按皮秒展示,因此除以 10^12 得到秒,除以 10^9 得到毫秒。语句摘要表和 事件计时说明解释了字段与单位。
看结果时,把几组数据放在一起:
- 总耗时高:可能是单次很慢,也可能是执行次数过多。
- 平均耗时低但次数很高:检查循环查库、重复请求和批处理调用方式。
- 扫描行数很多、返回行数很少:继续核对过滤条件、索引和执行计划。
- 最大耗时很高:回到对应时间范围查锁等待、资源争用和具体参数,不能靠平均值掩盖尖峰。
这些通常是采集开始或上次重置后的累计值,不等于最近一分钟的统计。需要观察某个窗口时,保存同一实例两次快照和时间范围,按相同 SCHEMA_NAME + DIGEST 比较增量;不要为方便观察而清空生产统计表。
DIGEST_TEXT 是归一化文本,不保留每次执行的原始参数。若业务连接没有设置默认库,SCHEMA_NAME 还可能为空,需调整过滤条件。采集关闭、摘要容量受限或统计被重置时,也不能据此追溯完整历史。
慢查询日志:先核对开关、阈值和输出位置
慢日志已经开启时,可按故障时间提取 SQL、Query_time、Lock_time、Rows_examined 和 Rows_sent,与语句摘要交叉定位。先查看当前设置:
SHOW GLOBAL VARIABLES
WHERE Variable_name IN (
'slow_query_log',
'slow_query_log_file',
'log_output',
'long_query_time',
'min_examined_row_limit',
'log_queries_not_using_indexes'
);
SELECT @@GLOBAL.long_query_time AS global_threshold_seconds,
@@SESSION.long_query_time AS current_session_threshold_seconds;
几个容易影响判断的地方要先确认:
- 慢日志开关打开,不代表一定写入文件;还要看
log_output。输出到TABLE时,应从对应日志表或平台入口查询。 - 普通慢日志记录受
long_query_time、min_examined_row_limit等规则影响;没有被记录不等于执行快。 - 全局阈值改变后,已有连接的会话阈值不会自动一起改变。新开的管理连接与长期复用的业务连接,可能使用不同值。
- 开启“未使用索引”的额外日志,会纳入很多未必慢的语句,不能把这类记录直接当成必须加索引的清单。
全局与会话变量的关系见 SET 官方说明。需要补采慢日志时,由具备权限的人员按实际负载设置有限观察窗口,先记录原配置,确认输出、磁盘容量与恢复方式;不建议排障开始就把全实例阈值长期设成零。
慢日志在语句执行完成后才写入,尚未完成的慢 SQL 可能还没有出现,日志顺序也不一定等于执行开始顺序。日志与应用的时区、耗时口径还要对齐,不能只拿一个 Query_time 就解释全部接口等待。慢查询日志手册说明了记录条件与时间范围。
拿到具体 SQL 后,再看执行计划
先核对表结构与现有索引。下面假设订单表包含 id、tenant_id、status、created_at,查询某租户最近的已支付订单:
SHOW CREATE TABLE app_db.orders;
SHOW INDEX FROM app_db.orders;
EXPLAIN FORMAT=TRADITIONAL
SELECT id, created_at
FROM app_db.orders
WHERE tenant_id = 42
AND status = 'PAID'
ORDER BY created_at DESC
LIMIT 50;
这里显式选择表格格式,避免实例的默认 EXPLAIN 格式不同。重点看访问方式、选中的 key、预计扫描的 rows、filtered,以及排序、临时表和联接顺序,并对照 EXPLAIN 输出说明。
rows 是估算,不是实际扫描结果;key 非空也不能单独证明执行高效。小表全扫描可能合理,索引扫描也可能读取大量数据。Using filesort 表示需要额外排序,不代表一定落盘;Using temporary 同样不能单独证明使用了磁盘临时表。
对这条示例,可以评估 (tenant_id, status, created_at) 这样的联合索引是否与过滤、排序和返回字段匹配,但还要结合现有索引、数据分布、其他查询和写入成本。不要仅按 WHERE 中出现的字段机械地新建索引。
如果相同 SQL 只在部分参数下很慢,要使用有代表性的租户、时间范围和数据规模比较计划。拿一个只有几条数据的测试租户代替大租户,可能完全看不到问题。
EXPLAIN ANALYZE 会执行查询,要安排在合适环境
估算明显偏离现象时,可以进一步看真实执行信息。EXPLAIN ANALYZE 会实际运行语句,不是单纯展示计划。 先在具有代表性数据、允许执行该查询的测试环境或合适的副本评估负载与影响,再决定是否用于生产排查。
下面仍是 SELECT 示例,显式使用 TREE 格式,适用于本文列出的版本:
EXPLAIN ANALYZE FORMAT=TREE
SELECT id, created_at
FROM app_db.orders
WHERE tenant_id = 42
AND status = 'PAID'
ORDER BY created_at DESC
LIMIT 50;
重点比较预估行数、实际返回行数、循环次数与耗时:是否有一层读取了大量行,是否内层联接被反复执行,是否大量数据最终被过滤掉。算子耗时包含子算子的部分,不能把每行时间简单相加;多次循环时还要注意输出中的平均含义。MySQL 8.0 的 EXPLAIN ANALYZE 说明列出了这些计时信息。
若估算与实际差距很大,继续核对数据倾斜和统计信息。是否执行 ANALYZE TABLE 更新统计,应结合版本、表规模、业务窗口和可能影响评估,不把它当成每次排障的第一步。测试环境或副本的数据、索引和资源与主库不同,也要在结论里注明。
修改后,确认是哪一段真正变快
每次修改都要对应前面的证据:锁等待就缩小事务与占锁范围;扫描和排序问题就调整查询或索引;调用次数过多就检查批量与缓存设计;整实例资源紧张则结合并发与后台任务处理。
对照修改前后的同类参数、数据量、并发和观察区间,比较数据库执行耗时、扫描量、调用次数以及接口端到端耗时,同时确认结果、排序和分页语义保持正确。新增索引还要观察写入与维护成本,不能只验证一次查询返回快了。
记录实际做了什么、观察了多久、还有什么没有证明。如果只是重启后短暂恢复、缓存命中或临时扩容,应继续追踪原来的慢点。连接与事务相关问题,可回到 后端排障专题 查找对应入口。
参考资料
- MySQL 8.4:慢查询日志
- MySQL 8.4:语句摘要表
- MySQL 8.4:Performance Schema 计时单位
- MySQL 8.4:InnoDB 锁等待视图
- MySQL 8.0:InnoDB 锁等待视图
- MySQL 8.4:EXPLAIN 输出
- MySQL 8.0:EXPLAIN 与 EXPLAIN ANALYZE
- MySQL 8.4:全局与会话变量
更新记录
- 2026-10-31:整理查询变慢的定位顺序、锁等待、语句摘要、慢日志与执行计划;示例按 MySQL 官方资料核对,未在本站生产数据库运行诊断、改配置或验证性能结果。
MySQL 查询突然变慢怎么查
https://java.li/archives/mysql-slow-query-troubleshooting
评论