数据库响应变慢时,慢查询日志是最直接的线索来源。很多团队只在出现严重超时后才临时打开日志,缺少持续收集和分析习惯,导致问题反复发生。要系统排查 MySQL 慢查询,应当先保证慢日志配置合理,再借助工具定位问题 SQL,最后结合执行计划与索引设计完成优化。下面围绕这条路径展开。

一、把慢查询日志配置到位
慢查询日志默认可能是关闭的,即便开启,默认的 long_query_time 也可能不符合业务预期。这个参数控制超过多少秒的查询会被记录,线上环境不建议设置为 0,否则所有 SQL 都会写入日志,磁盘和 I/O 很快会被拖垮。一般可以从 1 秒或 2 秒开始,结合业务可接受的延迟逐步收紧。另一个容易被忽略的参数是 log_queries_not_using_indexes,开启后未使用索引的查询也会被记录,但可能产生大量日志,需要配合 min_examined_row_limit 过滤扫描行数极少的查询。
在会话中临时调整可以使用 SET GLOBAL,这样不用重启数据库即可生效,但重启后会失效。持久化配置则需要写入 MySQL 配置文件。下面分别给出两种方式的示例。
SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 2; SET GLOBAL min_examined_row_limit = 100; SET GLOBAL log_queries_not_using_indexes = ON;
配置文件方式通常写入 [mysqld] 段,示例如下:
[mysqld] slow_query_log = 1 slow_query_log_file = /var/lib/mysql/mysql-slow.log long_query_time = 2 log_queries_not_using_indexes = 1 min_examined_row_limit = 100 log_output = FILE
日志输出方式可以选择 FILE 或 TABLE。写入文件更常见,便于后续用命令行工具分析;写入 mysql.slow_log 表则方便用 SQL 查询,但在高负载下可能增加额外开销。通常建议生产环境使用文件输出,并做好日志轮转,避免单个文件过大影响分析效率。
二、用工具从海量慢日志中提取有效信息
慢日志一大特点是重复度高,同一个模板的 SQL 可能记录成千上万条。直接逐行看文件效率很低,需要先做聚合处理。MySQL 自带 mysqldumpslow 工具,可以按执行次数、总耗时、锁等待时间等维度排序。例如 -s t 表示按平均查询时间排序,-s c 表示按出现次数排序,-t 10 表示只显示前十条。
mysqldumpslow -s t -t 10 /var/lib/mysql/mysql-slow.log
mysqldumpslow 适合快速查看日志,但对 SQL 指纹的归一化比较粗糙,复杂场景建议使用 Percona Toolkit 中的 pt-query-digest。它不仅能分析慢日志文件,还能解析通用日志、二进制日志和 processlist,输出结果包含请求占比、响应时间分布、执行计划示例等,信息量远大于自带工具。基本用法如下:
pt-query-digest /var/lib/mysql/mysql-slow.log
解读输出时,优先关注排名靠前的查询。重点看 Query_time 和 Lock_time,如果锁等待时间占比过高,说明可能不是 SQL 本身的执行慢,而是存在锁竞争。再比较 Rows_examined 与 Rows_sent 的比值,该值过大通常意味着扫描了大量行却只返回少量数据,这是索引缺失或执行计划不合理的典型表现。
三、通过 EXPLAIN 定位执行计划中的问题
找到候选慢 SQL 后,直接把 SQL 放到 EXPLAIN 前面执行。EXPLAIN 不会真正执行查询,只返回优化器选择的执行计划。核心字段包括 type、possible_keys、key、rows 和 Extra。其中 type 从好到差依次为 system、const、eq_ref、ref、range、index、ALL。如果 type 为 ALL,说明发生了全表扫描;key 为 NULL 表示没有实际使用索引;Extra 中出现 Using filesort 或 Using temporary 也提示排序或分组操作消耗了额外资源。
EXPLAIN SELECT id, user_id, order_amount FROM orders WHERE user_id = 100;
对应的输出可能如下:
+----+-------------+--------+------+---------------+-------------+---------+-------+------+-------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+------+---------------+-------------+---------+-------+------+-------+ | 1 | SIMPLE | orders | ref | idx_user_id | idx_user_id | 8 | const | 12 | NULL | +----+-------------+--------+------+---------------+-------------+---------+-------+------+-------+
这里 type=ref 表示使用非唯一索引查找,rows=12 表示估计只会扫描 12 行,Extra 为 NULL 说明没有临时表或文件排序。如果同样是这条 SQL,rows 变成几十万,或者 type=ALL,就需要继续优化。MySQL 8.0.18 及以上版本还可以使用 EXPLAIN ANALYZE,它会实际执行语句并输出每一步的真实耗时,比传统 EXPLAIN 更接近实际场景。
EXPLAIN ANALYZE SELECT id, user_id, order_amount FROM orders WHERE user_id = 100;
四、慢查询优化的几个实用方向
索引是慢查询优化的首选手段,但加索引不是越多越好。应根据 WHERE、ORDER BY、GROUP BY 中出现的列设计联合索引,并遵循最左前缀原则。例如查询条件为 user_id 且按 create_time 排序,可以创建联合索引 idx_user_time(user_id, create_time)。如果 SELECT 只查询索引包含的列,还能形成覆盖索引,避免回表。
ALTER TABLE orders ADD INDEX idx_user_time (user_id, create_time); SELECT user_id, create_time FROM orders WHERE user_id = 100 ORDER BY create_time DESC;
深分页是另一个常见问题。LIMIT 100000,20 会让 MySQL 先扫描大量行再丢弃前面的结果,数据量大了以后性能会很差。可以通过延迟关联的方式,先在子查询中只取主键,再回表获取完整记录,减少回表数据量。使用游标记录上一页最后一条 ID 也是更高效的做法。
SELECT o.id, o.user_id, o.order_amount
FROM orders o
INNER JOIN (
SELECT id FROM orders
WHERE user_id = 100
ORDER BY id DESC
LIMIT 100000, 20
) t ON o.id = t.id;
如果业务允许,还可以用游标方式替代大幅跳过:
SELECT id, user_id, order_amount FROM orders WHERE user_id = 100 AND id < 999999 ORDER BY id DESC LIMIT 20;
还要避免在索引列上使用函数或做隐式类型转换。例如 WHERE DATE(create_time) = '2025-01-01' 会导致索引失效,因为函数作用在列上后优化器无法直接利用索引值。应改写为范围查询:
-- 不推荐:函数包裹索引列 SELECT * FROM orders WHERE DATE(create_time) = '2025-01-01'; -- 推荐:使用范围条件 SELECT * FROM orders WHERE create_time >= '2025-01-01 00:00:00' AND create_time < '2025-01-02 00:00:00';
多表连接时,要确保连接列上有索引。优化器通常会选择小表驱动大表,但前提是有正确的统计信息。可以通过 ANALYZE TABLE 定期更新统计信息,避免优化器因统计信息过期做出错误选择。如果确实需要强制连接顺序,可以使用 STRAIGHT_JOIN,但不建议轻易使用,优先通过索引和统计信息让优化器自动判断。
五、形成可落地的慢查询排查闭环
排查慢查询不是一次性动作。线上环境应保持慢日志开启,并结合监控系统对慢查询数量、最大耗时设置报警。每次发版后可以统一分析慢日志,优先处理执行次数高且平均耗时长的 SQL。优化完成后需要对比 EXPLAIN 执行计划变化和实际响应时间,避免只降低扫描行数却增加回表次数。MySQL 8.0 的 sys 库提供了一些现成的语句摘要视图,适合日常巡检时快速查看全局慢语句。
SELECT query, exec_count, avg_latency, rows_sent_avg, rows_examined_avg FROM sys.statement_analysis WHERE db = 'orders' ORDER BY avg_latency DESC LIMIT 10;
sys.statement_analysis 的数据来自 performance_schema,它聚合了语句的执行次数、平均延迟、平均扫描行数等信息。相比直接翻慢日志,这个视图更适合用来做趋势观察和问题初筛。把慢日志记录、工具聚合、执行计划分析和索引优化串起来,就能形成从发现问题到验证效果的完整闭环,避免 SQL 性能问题反复出现。