MySQL 慢查询治理实战:从开启 slow_query_log 到用 EXPLAIN 和联合索引把接口从 3 秒压到 80 毫秒
上个月我给一个做企业官网的朋友救火。他的 VPS 是 2 核 4G 的便宜货,跑 PHP + MySQL 5.7,白天访问还不慢,一到晚上七八点就卡得打不开,PHP-FPM 日志里全是「MySQL server has gone away」和几百毫秒起步的查询。他第一反应是加钱升配置,我拦住了他——十有八九不是 CPU 不够,而是某条 SQL 没有走索引,在几万行数据上做了全表扫描,一台机器被一条烂 SQL 拖垮。这篇文章就把我这次完整排查过程写出来,从如何开启慢查询日志,到用 EXPLAIN 读懂执行计划,再到设计联合索引把接口从 3 秒优化到 80 毫秒。
第一步:先把慢查询日志打开,让数据说话
很多人优化 SQL 靠猜,这是最要命的。MySQL 自带了慢查询日志,能精确记录超过阈值的查询。先在命令行确认当前状态:
mysql -uroot -p -e "SHOW VARIABLES LIKE 'slow_query%';"
mysql -uroot -p -e "SHOW VARIABLES LIKE 'long_query_time';"如果 slow_query_log 是 OFF,就需要打开。生产环境要写进配置文件,因为 SET GLOBAL 重启后会丢失。编辑 /etc/mysql/mysql.conf.d/mysqld.cnf:
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = 1
log_slow_admin_statements = 1这里的参数值得逐个说明。long_query_time = 1 表示超过 1 秒的查询会被记录。对于大多数个人站,1 秒已经是灾难级别的慢,一开始可以设 1 秒,等系统整体健康后再收紧到 0.5 秒甚至 0.1 秒,用来发现潜在问题。log_queries_not_using_indexes 会额外记录所有没走索引的查询,这个开关非常有价值,但它也容易把日志写爆——因为哪怕是 SELECT * FROM 小表 也会被记录。所以在数据量大的站上,建议同时配合下面的参数来限流:
min_examined_row_limit = 100这一行的意思是:扫描行数少于 100 的查询即使没走索引、即使超过阈值,也不记录。这能过滤掉大量小表的噪音,让日志只保留真正有问题的查询。改完配置执行 systemctl restart mysql 生效。注意重启 MySQL 会短暂中断服务,低峰期操作。
第二步:用 pt-query-digest 把日志变成排名榜
直接 cat 慢日志你会被淹死,一堆重复的 SQL 刷屏。正确做法是用 Percona Toolkit 里的 pt-query-digest 做聚合分析,它会按「总耗时」排序,告诉你哪几条 SQL 吃掉了最多的时间。安装:
apt-get install -y percona-toolkit然后分析:
pt-query-digest /var/log/mysql/slow.log > /root/slow_report.txt打开报告,最上面会有这样的汇总:
# Profile
# Rank Query ID Response time Calls R/Call Query
# ==== ================== ============== ====== ======= ========
# 1 0x8A1B... 1200.0000 40.0% 8520 140.8ms SELECT posts
# 2 0x3C9D... 600.0000 20.0% 1200 500.0ms SELECT users这里的关键信息是 Response time(总耗时占比)和 R/Call(平均单次耗时)。我那个朋友的站,排名第一的就是一条查文章的 SQL,8520 次调用、平均 140 毫秒、占总耗时 40%。而更值得警惕的是排名第二那条,虽然只调用 1200 次,但单次 500 毫秒——这就是晚上卡顿的元凶之一。优化的优先级应该是:总耗时占比最高的先动刀,单次耗时特别高的其次。
第三步:EXPLAIN 读懂执行计划,找到扫描行数
拿到慢 SQL 后,不要急着加索引。先 EXPLAIN 看它到底怎么执行的:
EXPLAIN SELECT id,title,created_at FROM posts
WHERE category_id = 12 AND status = 1
ORDER BY created_at DESC LIMIT 20;输出里最需要盯的是这几列:
- type:访问类型。从好到坏大致是
const>eq_ref>ref>range>index>ALL。看到ALL基本就是全表扫描,必优化;index表示扫了整个索引树,也不理想。 - key:实际用到的索引。如果是
NULL,说明根本没走索引。 - rows:预估要扫描的行数。这个数字越大越危险。几万行的表里出现 rows=50000,说明它在翻整张表。
- Extra:额外信息。
Using filesort说明排序没走索引,需要在内存或磁盘里重新排;Using temporary说明建了临时表;Using where表示在存储引擎返回后再过滤。
我朋友那条 SQL 的 EXPLAIN 结果里,type 是 ALL,rows 是 62000,Extra 赫然写着 Using where; Using filesort。这就全明白了:它把六万两千行文章全部扫了一遍,筛出符合条件的,再对结果做文件排序,最后取前 20 条。每来一次请求都这么干一遍,一天几千次,机器当然扛不住。
第四步:设计联合索引,让 type 从 ALL 变成 ref
问题清楚了,就是缺一个能同时覆盖「筛选 + 排序」的联合索引。联合索引有一个核心原则叫最左前缀:索引 (a, b, c) 能被用来查询 a、a+b、a+b+c,但不能被单独用来查 b 或 c。顺序很有讲究。
对于上面的 SQL,WHERE 条件是 category_id = 12 AND status = 1,等值条件,两个都是等值匹配;ORDER BY 是 created_at DESC。索引设计的第一原则是:等值条件列放前面,排序列放最后。因为等值条件下,只要所有等值列都固定了,后面的排序列天然就是有序的,无需 filesort。
ALTER TABLE posts ADD INDEX idx_cat_status_time (category_id, status, created_at);加完索引再 EXPLAIN 一次,结果应该变成 type=ref,key=idx_cat_status_time,rows 降到几十,Extra 里的 Using filesort 消失。实测这条 SQL 从平均 140 毫秒降到几毫秒。注意 ORDER BY created_at DESC 能不能用上索引,取决于索引里 created_at 的排序方向。MySQL 5.7 及以前,降序查询可以反向扫描索引,通常没问题;但如果是多列排序方向不一致(比如 ORDER BY a ASC, b DESC),5.7 的联合索引就用不上,这种情况要么建 (a,b) 反向扫描能凑合,要么升级到 MySQL 8.0——8.0 支持真正的降序索引 INDEX (a ASC, b DESC)。
第五步:几个极易踩的优化陷阱
陷阱一:函数和运算破坏索引。 下面这两种写法会导致索引失效:
-- 错误:对列做函数运算,索引失效
SELECT * FROM posts WHERE DATE(created_at) = '2026-10-01';
-- 正确:改成范围查询
SELECT * FROM posts WHERE created_at >= '2026-10-01 00:00:00'
AND created_at < '2026-10-02 00:00:00';
-- 错误:隐式类型转换,字符串列用数字查
SELECT * FROM users WHERE phone = 13800138000;
-- 正确:加引号
SELECT * FROM users WHERE phone = '13800138000';第一条把列包在函数里,MySQL 无法用索引定位,只能逐行算函数值。第二条是字段类型不匹配引发的隐式转换,varchar 列用 int 查会触发全表扫描,这个坑极其隐蔽,很多人查了半天都不知道为什么索引没生效。
陷阱二:SELECT * 带来回表。 索引分为聚簇索引(主键)和二级索引。SELECT * 时,二级索引只能定位到主键,还要拿着主键回聚簇索引取整行数据,这叫回表。如果只需要几个字段,尽量让索引「覆盖」这些字段(覆盖索引),避免回表。比如查标题和时间,建 INDEX (category_id, status, created_at, title),这样查询完全在索引里完成,Extra 会显示 Using index。
陷阱三:索引不是越多越好。 每多一个索引,写入、更新、删除时都要多维护一棵 B+ 树,磁盘和内存都吃紧,而且优化器在选索引时可能选错。个人站常见表,一张表 3 到 5 个索引足够了。定期用 SHOW INDEX FROM posts 检查,删掉从未被使用的冗余索引——可以从 sys.schema_unused_indexes 里查。
总结与持续监控
这次的优化链路总结成一句话:先开慢日志拿到数据,用 pt-query-digest 排序找重点,用 EXPLAIN 定位扫描行数与 filesort,再用「等值在前、排序在后」的联合索引对症下药,最后避开函数运算和隐式转换的坑。那位朋友的接口从 3 秒降到了 80 毫秒,一分钱配置没加。
优化不是一锤子买卖。建议把慢日志分析做成例行工作,比如每周跑一次 pt-query-digest;同时打开 performance_schema,用 sys.statement_analysis 视图看全局 Top SQL。个人站长没有 DBA 团队,靠的就是这套「日志 + 执行计划 + 索引」的自查闭环,把它变成肌肉记忆,你的站就能一直轻快下去。