MySQL慢查询日志分析与EXPLAIN执行计划调优实战

MySQL 线上性能问题的排查入口,九成从慢查询开始。慢查询日志记录执行时间超阈值的 SQL,EXPLAIN 展示执行计划,两者配合能定位绝大多数查询性能问题。本文按日志开启、采集、分析、索引优化、改写 SQL 的完整链路给出可执行的方案,命令在 MySQL 5.7 与 8.0 通用。

开启慢查询日志与阈值设置

生产环境建议持久化开启,避免每次重启丢失配置:

-- 查看当前状态
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';

-- 动态开启(重启失效)
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;
SET GLOBAL log_queries_not_using_indexes = ON;

-- 持久化:写入 my.cnf [mysqld] 段
slow_query_log = ON
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = 1
log_queries_not_using_indexes = ON

long_query_time 从业务角度取值:核心链路 1 秒即可视为慢,报表类查询可放宽到 3 秒。注意 long_query_time 为 0 会记录全部 SQL,日志量爆炸,生产别这么配。

慢查询日志分析:mysqldumpslow与pt-query-digest

日志文件按时间累积,直接看是灾难,用工具聚合才有价值:

-- 按平均耗时排序,取前20
mysqldumpslow -s at -t 20 /var/log/mysql/mysql-slow.log

-- 按执行次数排序,定位高频慢SQL
mysqldumpslow -s c -t 20 /var/log/mysql/mysql-slow.log

-- pt-query-digest 生成完整报告
pt-query-digest /var/log/mysql/mysql-slow.log > slow_report.txt

mysqldumpslow 会把数字参数归一化为 N,便于聚合同形态 SQL。percona-toolkit 的 pt-query-digest 给出更详细的分位数耗时分布,适合定期周报分析。

EXPLAIN执行计划核心字段解读

拿到具体慢 SQL 后,用 EXPLAIN 看执行计划,重点关注 type、key、rows、Extra 四列:

EXPLAIN SELECT o.id, o.amount, u.name
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.created_at > '2026-09-01' AND o.status = 'paid';

+----+-------------+-------+------+---------------+---------+---------+-------+-------+---------------------------------+
| id | select_type | table | type | possible_keys | key     | key_len | rows  | Extra                           |
+----+-------------+-------+------+---------------+---------+---------+-------+-------+---------------------------------+
|  1 | SIMPLE      | o     | range| idx_created   | idx_created | 4     | 5000 | Using index condition           |
|  1 | SIMPLE      | u     | ref  | PRIMARY       | PRIMARY | 4       | 1     | Using where                     |
+----+-------------+-------+------+---------------+---------+---------+-------+-------+---------------------------------+

type 从好到差:system > const > eq_ref > ref > range > index > ALL。ALL 表示全表扫描,是慢查询的重灾区;rows 是预估扫描行数,与实际情况对比能发现统计信息不准;Extra 出现 Using filesort、Using temporary 时优先处理排序与临时表。

索引优化:联合索引、覆盖索引与索引失效

多条件查询靠联合索引。建索引时遵循最左前缀,把等值条件放前面、范围条件放后面:

-- 最优:等值在前,范围在后
ALTER TABLE orders ADD INDEX idx_user_status_time (user_id, status, created_at);

-- 覆盖索引:查询列全在索引内,避免回表
ALTER TABLE orders ADD INDEX idx_user_amount (user_id, amount);

-- 常见失效场景,逐个排查
EXPLAIN SELECT * FROM orders WHERE DATE(created_at) = '2026-09-08';
-- 函数包裹索引列导致索引失效,改写为范围:
EXPLAIN SELECT * FROM orders WHERE created_at >= '2026-09-08' AND created_at < '2026-09-09';

索引列上做函数运算、隐式类型转换、% 前缀模糊查询、OR 连接未全部命中索引,都会导致优化器放弃索引。加索引后一定要用 EXPLAIN 验证 type 从 ALL 变为 range/ref 才算生效。

分页深翻页与排序优化

LIMIT 1000000, 20 这类深分页,MySQL 要先扫描 100 万行再丢弃,性能极差。改造方案:

-- 慢:深分页全量扫描
SELECT id, amount FROM orders ORDER BY id LIMIT 1000000, 20;

-- 快:延迟关联,先取主键再回表
SELECT o.id, o.amount FROM orders o
JOIN (SELECT id FROM orders ORDER BY id LIMIT 1000000, 20) t
ON o.id = t.id;

-- 更快:游标方式,记住上一页最后一条 id
SELECT id, amount FROM orders WHERE id > 1000020 ORDER BY id LIMIT 20;

游标分页在数据持续增长时最稳定,代价是无法跳页。业务上支持“加载更多”的场景,直接改用游标模式。

从慢查询到性能验证的闭环

优化完成后用 EXPLAIN 对比执行计划,再用真实数据量压测确认:

-- 复查目标SQL是否仍出现在慢查询汇总中
SELECT DIGEST_TEXT, COUNT(*) AS cnt, SUM(SUM_TIMER_WAIT)/1e12 AS total_s
FROM performance_schema.events_statements_summary_by_digest
GROUP BY DIGEST_TEXT ORDER BY total_s DESC LIMIT 10;

performance_schema 的 digest 汇总能看全局 SQL 耗时分布,适合优化效果的定期跟踪。调优不是一次性工作,索引随数据分布和查询模式变化,建议每季度对 TOP 慢 SQL 做一次复盘。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/mysql-man-cha-xun-ri-zhi-fen-xi-yu-explain-zhi-xing-ji-hua/

赞 (0)
小编小编
上一篇 2026年9月8日
下一篇 2026年9月8日

相关推荐

MySQL慢查询日志分析与EXPLAIN执行计划调优实战

慢查询是MySQL性能问题的首要排查方向。通过慢查询日志定位执行时间超过阈值的SQL语句,再用EXPLAIN分析执行计划,找到全表扫描、索引失效、临时表排序等性能瓶颈,针对性优化索引和SQL写法。

慢查询日志配置与采集

开启慢查询日志并设置阈值:

-- 查看当前配置
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';

-- 动态开启(运行时生效,重启失效)
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;          -- 超过1秒的查询记录
SET GLOBAL log_queries_not_using_indexes = ON;  -- 记录未使用索引的查询
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';

-- 永久配置(my.cnf)
-- [mysqld]
-- slow_query_log = ON
-- long_query_time = 1
-- log_queries_not_using_indexes = ON
-- slow_query_log_file = /var/log/mysql/slow.log

使用mysqldumpslow分析慢查询日志:

# 按平均查询时间排序,显示Top 10
mysqldumpslow -s at -t 10 /var/log/mysql/slow.log

# 按总锁定时间排序
mysqldumpslow -s al -t 10 /var/log/mysql/slow.log

# 按返回行数排序(可能扫描大量行但返回少,典型低效查询)
mysqldumpslow -s ar -t 10 /var/log/mysql/slow.log

# 按出现次数排序(高频慢查询)
mysqldumpslow -s c -t 10 /var/log/mysql/slow.log

输出结果中Count表示该SQL执行次数,Time为总耗时和平均耗时,Rows为扫描行数和返回行数。高频且耗时的SQL优先优化。

EXPLAIN执行计划关键字段解读

EXPLAIN SELECT o.order_id, o.user_id, u.username, u.email
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 2 AND o.created_at > '2024-01-01'
ORDER BY o.created_at DESC
LIMIT 20;

EXPLAIN输出关键字段:

字段 说明 重点关注
type 访问类型 ALL(全表扫描)最差,ref/range较好,const/eq_ref最优
key 实际使用的索引 NULL表示未使用索引
rows 预估扫描行数 越小越好,与返回行数差距大说明扫描冗余
Extra 附加信息 Using filesort(文件排序)、Using temporary(临时表)需要优化
key_len 索引使用长度 判断复合索引是否被完整使用

访问类型从优到劣排序:

system > const > eq_ref > ref > range > index > ALL
  • const:主键或唯一索引等值查询,最多匹配一行
  • eq_ref:JOIN时被驱动表使用主键或唯一索引
  • ref:非唯一索引等值查询
  • range:索引范围扫描(BETWEEN、>、<、IN)
  • index:扫描整个索引树,不回表但遍历所有索引行
  • ALL:全表扫描,必须优化

索引优化策略与覆盖索引

复合索引遵循最左前缀原则,索引列顺序影响查询是否命中索引:

-- 创建复合索引
CREATE INDEX idx_status_created ON orders(status, created_at);

-- 命中索引(使用status + created_at)
EXPLAIN SELECT * FROM orders WHERE status = 2 AND created_at > '2024-01-01';

-- 命中索引(仅使用status部分)
EXPLAIN SELECT * FROM orders WHERE status = 2;

-- 未命中索引(跳过status,违反最左前缀)
EXPLAIN SELECT * FROM orders WHERE created_at > '2024-01-01';

-- 覆盖索引:查询字段全部包含在索引中,无需回表
CREATE INDEX idx_covering ON orders(status, created_at, order_id, user_id);

EXPLAIN SELECT status, created_at, order_id, user_id
FROM orders
WHERE status = 2;

-- Extra列显示 "Using index" 表示覆盖索引生效
-- 避免了通过主键回表查询聚簇索引,减少大量随机IO

复杂查询改写与优化案例

案例一:子查询改JOIN

-- 优化前:相关子查询,每行执行一次子查询
SELECT * FROM orders
WHERE user_id IN (SELECT id FROM users WHERE status = 1);

-- 优化后:JOIN改写,MySQL 8.0优化器可自动转换
SELECT o.* FROM orders o
INNER JOIN users u ON o.user_id = u.id
WHERE u.status = 1;

-- 或使用EXISTS(适合users表大的场景)
SELECT * FROM orders o
WHERE EXISTS (SELECT 1 FROM users u WHERE u.id = o.user_id AND u.status = 1);

案例二:分页查询优化

-- 优化前:OFFSET越大性能越差,需要扫描前面所有行
SELECT * FROM orders ORDER BY created_at DESC LIMIT 100000, 20;

-- 优化后:使用游标分页(延迟关联),先通过索引查出主键再关联
SELECT o.* FROM orders o
INNER JOIN (
    SELECT id FROM orders ORDER BY created_at DESC LIMIT 100000, 20
) t ON o.id = t.id;

-- 或使用WHERE条件替代OFFSET(记住上一页最后一条记录的值)
SELECT * FROM orders
WHERE created_at < '2024-06-01 12:00:00'
ORDER BY created_at DESC LIMIT 20;

案例三:避免索引失效的常见写法

-- 函数操作导致索引失效
-- 错误:对索引列使用函数
SELECT * FROM orders WHERE DATE(created_at) = '2024-06-01';
-- 正确:改写为范围查询
SELECT * FROM orders
WHERE created_at >= '2024-06-01' AND created_at < '2024-06-02';

-- 隐式类型转换导致索引失效
-- 错误:status是int类型,传入字符串
SELECT * FROM orders WHERE status = '2';
-- 正确:使用正确类型
SELECT * FROM orders WHERE status = 2;

-- LIKE前导通配符导致索引失效
-- 错误:无法使用索引
SELECT * FROM users WHERE username LIKE '%zhang%';
-- 正确:后导通配符可使用索引
SELECT * FROM users WHERE username LIKE 'zhang%';

MySQL 8.0的EXPLAIN ANALYZE提供实际执行统计:

EXPLAIN ANALYZE
SELECT o.order_id, u.username FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 2;

-- 输出实际行数和耗时,对比预估值判断统计信息是否准确
-- 如果实际行数远大于预估行数,执行ANALYZE TABLE更新统计信息
ANALYZE TABLE orders;

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/mysql-man-cha-xun-ri-zhi-fen-xi-yu-explain-zhi-xing-ji-hua/

赞 (0)
小编小编
上一篇 2026年9月7日
下一篇 2026年9月7日

相关推荐