MySQL 慢查询怎么查?从 slow log 到索引优化的实战步骤(新手可照抄)
MySQL 慢查询怎么查?从 slow log 到索引优化的实战步骤(新手可照抄)
如果你现在的症状是:
- 首页/列表页偶尔卡 3~10 秒
- 后台“看起来没报错”,但用户就是觉得慢
- 服务器 CPU 不一定很高,IO 也不一定爆,就是“说不清的慢”
我一般不建议第一时间“加机器/升配置”。MySQL 慢查询这事儿,很多时候是某几条 SQL 在偷偷拖后腿:一次慢 2 秒还好,关键是它可能每分钟跑几百次。
这篇文章我按我常用的排查顺序写:先抓证据 → 再锁定 Top SQL → 看执行计划 → 对症下药(索引/改写)→ 验证。你照着做,基本能把“慢在哪”说清楚,并且把它修掉。
默认场景:MySQL 5.7/8.0 + InnoDB。命令以 Linux 为例。
0)先确认:到底是不是 MySQL 在慢?(别一上来就开大招)
很多“数据库慢”其实是:反代超时、应用线程池堵住、磁盘满了写不进去、DNS 卡住……所以我会先花 2 分钟做个快速分流。
0.1 看 MySQL 有没有“堆积”的请求
mysql -e "SHOW FULL PROCESSLIST\G" | sed -n '1,120p'
你重点看:
Time很高的行是不是重复出现State是否在Sending data/Sorting result/Copying to tmp table- 有没有一堆
Locked/Waiting for ... lock
0.2 看机器是否被 IO 或内存拖住
top -c
free -h
# 没装 iostat 的话先装 sysstat
iostat -x 1 5
iostat里如果%util长时间接近 100%,那是 IO 瓶颈free如果 swap 在狂用,那优化 SQL 也救不了太多(先把内存/缓存策略理顺)
如果你确实看到“某几条 SQL 反复出现 + 时间很长”,就可以进入正题:开慢日志。
1)开启慢查询日志(slow log):先抓证据
慢日志不是为了“炫技”,它就是为了回答一句话:到底哪条 SQL 在拖慢网站。
1.1 临时开启(适合你现在就要定位)
-- 开启慢日志
SET GLOBAL slow_query_log = 1;
-- 慢阈值:先从 1 秒开始,站点很忙的话可以先 0.5
SET GLOBAL long_query_time = 1;
-- 不建议一上来就开,噪音太大
SET GLOBAL log_queries_not_using_indexes = 0;
-- 可选:只记录扫描行数足够多的查询(减少噪音)
-- SET GLOBAL min_examined_row_limit = 1000;
检查是否生效:
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
1.2 生产建议:配置文件里长期启用 + 做日志轮转
慢日志很容易把磁盘写满(尤其你把 long_query_time 调得很低的时候)。所以我的经验是:
- 慢日志文件单独放到可控目录
- 配合 logrotate,别等磁盘满了再哭
常见配置(示意,路径按你发行版调整):
[mysqld]
slow_query_log=1
slow_query_log_file=/var/log/mysql/slow.log
long_query_time=1
log_queries_not_using_indexes=0
如果你的站已经出现“磁盘占满”的问题,可以先看我站内这篇(排查思路类似):
- 服务器磁盘占满怎么办?Linux 一键定位大文件与日志清理思路(含命令)
2)读慢日志:别只盯“最慢的一条”,要看“最耗时的一类”
慢日志抓到以后,很多人第一反应是找 Query_time 最大的那条。这个思路容易跑偏。
我更关注两个指标:
- 总耗时最高(出现次数 * 单次耗时)
- P95/P99(95%/99% 的请求耗时)
2.1 快速看个大概:mysqldumpslow
mysqldumpslow -s t -t 20 /var/log/mysql/slow.log
mysqldumpslow -s at -t 20 /var/log/mysql/slow.log
-s t:按单次时间排序-s at:按平均时间排序
2.2 更推荐:pt-query-digest(真的省时间)
如果你能装 percona-toolkit,优先用它:
pt-query-digest /var/log/mysql/slow.log > /tmp/slow.report.txt
你看报告时先盯:
# Query 1: 38.2%这种占比高的Calls很多的(出现频率高)Rows_examined很高的(多半在扫表/扫索引)
3)看执行计划:EXPLAIN / EXPLAIN ANALYZE 怎么读
定位到某条 SQL 后,下一步就是 EXPLAIN。
3.1 我最常看的 5 个字段
EXPLAIN SELECT ...;
type:越靠近const/ref/range越好;ALL基本就是全表扫key:实际用到的索引是什么(为空就危险)rows:估算要扫多少行(越大越危险)Extra:Using filesort/Using temporary往往是性能雷区possible_keys:有索引但没用上,说明索引不匹配/写法不对/统计信息不准
3.2 MySQL 8.0 可以直接 EXPLAIN ANALYZE(更直观)
EXPLAIN ANALYZE SELECT ...;
它会给你“实际耗时”和“每步执行次数”,比纯 EXPLAIN 更接近真实。
4)最常见的 3 类慢 SQL(以及我通常怎么改)
下面这三类,我在建站/后台系统里见得最多。
4.1 列表页:WHERE + ORDER BY + LIMIT 但没合适索引
典型 SQL:
SELECT id, title, created_at
FROM posts
WHERE status = 1 AND category_id = 3
ORDER BY created_at DESC
LIMIT 20;
这种 SQL 想跑得快,核心是:过滤条件 + 排序字段要能被同一条索引覆盖。
通常我会这么建索引:
CREATE INDEX idx_posts_cat_status_created
ON posts(category_id, status, created_at DESC);
注意:索引顺序不是随便排的。这里先 category_id/status 是为了过滤,再 created_at 是为了排序。
4.2 大分页:LIMIT offset 很大,越翻页越慢
典型 SQL:
SELECT id, title
FROM posts
ORDER BY id DESC
LIMIT 10000, 20;
这种写法到后面就是“数据库替你数 10000 行再丢掉”。体验一定差。
我更常用 游标分页(keyset pagination):
-- 假设上一页最后一条 id=12345
SELECT id, title
FROM posts
WHERE id < 12345
ORDER BY id DESC
LIMIT 20;
前端/接口只要记住“上一页最后一条 id”,性能会稳很多。
4.3 过滤条件写法“挡住了索引”
比如你为了按天查,把字段包了一层函数:
-- 不推荐:函数会让索引失效
WHERE DATE(created_at) = '2026-03-12'
正确姿势是改成范围:
WHERE created_at >= '2026-03-12 00:00:00'
AND created_at < '2026-03-13 00:00:00'
5)加索引不是越多越好:我一般会做这 3 个检查
- 这条索引是否真的服务于高频查询?(看慢日志/调用次数)
- 有没有重复索引?(字段顺序不同但效果一样的)
- 写入压力会不会明显变大?(索引越多写入越慢)
很多站点“索引越加越慢”,不是幻觉:写入、更新、维护统计信息都有成本。
6)最后一步:验证你真的变快了(别凭感觉)
我通常会这样验证:
- 用同一组参数跑优化前后的 SQL,对比耗时
- 再看慢日志里这类 SQL 的占比是否下降
- 观察接口 P95 是否下降(如果你有监控)
MySQL 8.0 的话,EXPLAIN ANALYZE 的“实际耗时”也能给你很直观的对比。
7)如果你只想抄一个最小清单(我给你一句话版本)
1) 开 slow log(先 1s)→ 2) 用 pt-query-digest 排序(看总耗时)→ 3) EXPLAIN 看 type/key/rows/Extra → 4) 先解决列表页索引和大分页 → 5) 验证 P95
