学完这篇,你能独立完成一次 MySQL 慢查询排查:从打开慢查询日志找到问题 SQL,用 EXPLAIN 读懂执行计划,再按索引原理改对索引并验证效果。
下面以 Clara BBS 这类 PHP 论坛系统(PHP 7.4–8.5 + MySQL 5.7+)为背景讲,示例表名用 thread、reply 这类通用结构,换成你自己的表名同样适用。
第一步:先把慢查询日志打开
优化必须有数据支撑,不能靠猜。慢查询日志是 MySQL 自带功能,改配置即可(宝塔面板在「数据库 → 性能调整」里也能直接开)。
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = 1
前三行分别是:开启、日志路径、超过多少秒算慢(单位秒,可写 0.5)。最后一行把没走索引的 SQL 也记下来,调优初期很有用。改完重启 MySQL,或者在命令行临时生效:
SET GLOBAL slow_query_log = 1;
SET GLOBAL long_query_time = 1;
跑一天业务后看日志,重点找 Query_time 大、Rows_examined(扫描行数)远大于 Rows_sent(返回行数)的语句。
注意:long_query_time 设为 1 秒是通用起点,但论坛的帖子列表页要求毫秒级,可以临时降到 0.1 秒抓一批看看。
第二步:用 EXPLAIN 看执行计划
把可疑 SQL 前面加 EXPLAIN 直接跑:
EXPLAIN SELECT id, title, created_at
FROM thread
WHERE board_id = 12 AND status = 1
ORDER BY created_at DESC
LIMIT 20;
只盯四个字段就够入门:
type:访问类型。const > eq_ref > ref > range > index > ALL。看到 ALL 就是全表扫描,必须处理。
key:实际用到的索引。为 NULL 说明没走索引。
rows:预估扫描行数。这个数越接近最终返回条数越好。
Extra:出现 Using filesort(额外排序)或 Using temporary(临时表)都要警惕。
注意:rows 是优化器的估算值,不是精确值。统计信息过期时会严重失真,可以执行 ANALYZE TABLE thread; 刷新后再看。
第三步:理解索引为什么能加速
InnoDB 的索引是 B+ 树:非叶子节点只存键值,叶子节点存数据且用链表相连。所以等值查询是几次页读取就到,范围查询顺着叶子链表扫,排序也能直接复用索引顺序——这就是索引能同时干掉「查找」和「filesort」的原因。
三个必须记住的规则:
- 最左前缀:联合索引
(board_id, status, created_at) 能支持 board_id、board_id+status、board_id+status+created_at 三种查询,但单独用 status 用不上。
- 回表:二级索引叶子节点存的是主键,查其他列要再回主键索引取一次。如果查询列全在索引里(覆盖索引),就不用回表。
- 索引失效:列上用函数、隐式类型转换(字符串列传数字)、前导模糊
LIKE '%xx'、OR 连接非索引列,都会让索引白建。
把第二步那条 SQL 的索引设计成:
ALTER TABLE thread ADD INDEX idx_board_status_time (board_id, status, created_at);
顺序不能随意调:等值条件列放前面,排序/范围列放最后。这样 type 会变成 ref,Extra 里的 Using filesort 消失。
注意:索引不是越多越好。每个索引都增加写入成本和磁盘占用,论坛这种读多写少的场景也要控制在每表 5 个以内,定期用 SHOW INDEX FROM thread; 清理重复索引。
第四步:改写 SQL 而不仅是加索引
有些问题加索引也解决不了,要改语句:
- 分页深翻:
LIMIT 100000, 20 会扫 10 万行。改成 WHERE id > 上次最大id ORDER BY id LIMIT 20(游标分页),或先只查主键再关联回表。
COUNT(*) 统计:帖子总数这类可以走缓存(Clara BBS 内置 Cache::remember),不必每次实时统计。
- 一次查多表:能拆成两条走索引的简单查询,常常比一条 JOIN 更快。
第五步:验证,别靠感觉
改完再跑一次 EXPLAIN,对比 type、rows、Extra 三项的前后变化;然后把慢查询日志清空,观察一段时间确认该 SQL 不再出现。线上变更索引建议用 ALGORITHM=INPLACE, LOCK=NONE(MySQL 5.7+ 加普通索引默认就支持在线),避免锁表。
注意:任何 DDL 变更前先备份,低峰期执行。大表加索引虽然不锁写,但会消耗大量 IO,可能拖慢同时段的业务查询。
小结
- 优化顺序是「先测后改」:开慢查询日志 → 找候选 SQL → EXPLAIN → 改索引或改 SQL → 复测。
- 看执行计划只盯
type、key、rows、Extra,ALL 和 Using filesort 是首要信号。
- 联合索引遵守最左前缀,等值列在前、排序范围列在后。
- 列上套函数、隐式转换、前导模糊是索引失效三大元凶。
- 深翻页和实时统计,往往靠改写法或加缓存解决,加索引无效。