慢查询分析与索引优化
数据库慢查询,几乎是每个后端工程师都会遇到的噩梦。我记得刚带团队那会儿,线上一个报表接口突然从 200ms 飙到 8 秒,用户投诉电话直接打到了 CTO 那里。后来一查,就是一条没走索引的 SQL 在作祟。
说白了,慢查询分析就是给数据库做「体检」。你得先知道哪里慢,才能对症下药。这一节,我就带你走一遍完整的排查链路。
开启慢查询日志
很多同学上来就问索引怎么建,其实第一步应该是把慢查询日志打开。没有数据,你连问题在哪都不知道。
MySQL 的慢查询日志配置很简单,修改 my.cnf 或在运行时动态设置都行:
-- 查看当前配置
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;
这里有个坑——long_query_time 的单位是秒,支持小数。我一般设 0.5 或 1,线上业务压力大的时候设 0.3。另外,log_queries_not_using_indexes 这个参数建议也打开,它能帮你抓到那些没走索引的「隐形炸弹」。
日志文件长什么样?大概是这样:
# Time: 2025-01-15T10:23:45.123456Z
# User@Host: app_user[app] @ [10.0.1.100]
# Query_time: 2.345678 Lock_time: 0.000123 Rows_sent: 100 Rows_examined: 50000
SET timestamp=1736929425;
SELECT * FROM orders WHERE status = 'PENDING' ORDER BY created_at DESC;
看到 Rows_examined: 50000 和 Rows_sent: 100 的差距了吗?扫描了 5 万行只返回 100 行,这明显有问题。
使用 EXPLAIN 分析执行计划
拿到慢查询 SQL 之后,下一步就是用 EXPLAIN 看看 MySQL 到底是怎么执行的。这就像医生看 CT 片子,一眼就能找到病灶。
EXPLAIN SELECT * FROM orders WHERE status = 'PENDING' ORDER BY created_at DESC\G
输出结果里,我最关注这几个字段:
| 字段 | 重点关注 | 我的判断标准 |
|---|---|---|
| type | 访问类型 | ALL 是灾难,index 是次优,range/ref 算及格,const/eq_ref 是理想 |
| key | 实际使用的索引 | NULL 说明没走索引,必须优化 |
| rows | 预估扫描行数 | 这个数字越大,查询越慢 |
| Extra | 额外信息 | 看到 Using filesort、Using temporary 就要警惕 |
实战经验:有一次我排查一个分页接口,EXPLAIN 显示 type 为 ALL,rows 显示 80 万。加上合适的索引后,rows 降到了 200,接口响应时间从 3 秒降到了 30 毫秒。你想想看,这差距有多大。
常见的 Extra 信息解读:
- Using where:正常,表示使用了 WHERE 条件过滤
- Using index:好现象,覆盖索引,不需要回表
- Using filesort:需要优化,ORDER BY 没走索引
- Using temporary:需要优化,用了临时表,通常是 GROUP BY 或 DISTINCT 的问题
- Using index condition:索引下推,MySQL 5.6+ 的优化特性
复合索引设计与最左前缀原则
很多同学以为索引就是给每个查询字段单独建一个,其实这是误区。我见过最夸张的案例,一张表建了 20 多个单列索引,写入慢得要命,查询也没快多少。
正确的做法是设计复合索引。但复合索引有个核心规则——最左前缀原则。
什么叫最左前缀?说白了,MySQL 只能从复合索引的最左边开始匹配。比如你建了一个索引 (a, b, c),那么:
WHERE a = 1✅ 走索引WHERE a = 1 AND b = 2✅ 走索引WHERE a = 1 AND b = 2 AND c = 3✅ 走索引WHERE b = 2❌ 不走索引(跳过了 a)WHERE a = 1 AND c = 3⚠️ 只用到 a,c 用不到
复合索引设计的原则,我总结了几条:
- 区分度高的放前面——比如性别只有男女,区分度低;订单号几乎唯一,区分度高
- 等值条件放前面,范围条件放后面——
=的条件能精确定位,>、<、BETWEEN会中断后续匹配 - 覆盖索引优先——如果查询的字段都在索引里,就不用回表,性能翻倍
- 避免冗余索引——有了 (a, b),就不需要单独建 (a)
来看一个实际案例。假设有张订单表,查询模式是:
SELECT id, order_no, amount, status
FROM orders
WHERE status = 'PAID'
AND created_at > '2025-01-01'
ORDER BY created_at DESC
LIMIT 20;
索引怎么设计?
- 等值条件
status放前面 - 范围条件
created_at放后面 - 查询字段
id, order_no, amount也放进索引,实现覆盖索引
最终索引:(status, created_at, id, order_no, amount)
这样设计,EXPLAIN 会显示 Using index,连回表都省了。
SHOW INDEX FROM table 查看现有索引,避免重复。我习惯在 ER 图上标注每个表的索引设计,评审时一目了然。
知识体系总览
下面这张图,把慢查询分析的完整链路串起来了:
从发现慢查询开始,到开启日志、EXPLAIN 分析,再到复合索引设计,最后落到最左前缀原则——这是一条完整的优化链路。每一步都有对应的工具和方法,缺一不可。
核心总结:慢查询优化不是玄学,是一套可复用的方法论。先抓日志,再 EXPLAIN,最后设计索引。按这个流程走,90% 的慢查询都能在半小时内解决。