慢查询分析与索引优化

数据库慢查询,几乎是每个后端工程师都会遇到的噩梦。我记得刚带团队那会儿,线上一个报表接口突然从 200ms 飙到 8 秒,用户投诉电话直接打到了 CTO 那里。后来一查,就是一条没走索引的 SQL 在作祟。

说白了,慢查询分析就是给数据库做「体检」。你得先知道哪里慢,才能对症下药。这一节,我就带你走一遍完整的排查链路。

开启慢查询日志

很多同学上来就问索引怎么建,其实第一步应该是把慢查询日志打开。没有数据,你连问题在哪都不知道。

我的习惯:新项目上线前,我一定会在数据库配置里把慢查询日志打开。阈值先设 1 秒,跑几天再根据实际情况调整。

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 这个参数建议也打开,它能帮你抓到那些没走索引的「隐形炸弹」。

注意:慢查询日志会消耗磁盘 IO,高并发场景下不要长期全量开启。我通常只在排查期间打开,或者用 pt-query-digest 这类工具做采样分析。

日志文件长什么样?大概是这样:

# 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 用不到
我曾经踩过的坑:给订单表建了 (status, created_at) 的复合索引,结果业务方写了个查询 WHERE created_at > '2025-01-01',没带 status。索引完全没生效,全表扫描跑了 10 秒。后来加了 status 条件,瞬间降到 50ms。

复合索引设计的原则,我总结了几条:

  1. 区分度高的放前面——比如性别只有男女,区分度低;订单号几乎唯一,区分度高
  2. 等值条件放前面,范围条件放后面——= 的条件能精确定位,>、<、BETWEEN 会中断后续匹配
  3. 覆盖索引优先——如果查询的字段都在索引里,就不用回表,性能翻倍
  4. 避免冗余索引——有了 (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 分析执行计划 type key rows 复合索引设计与优化 最左前缀原则

从发现慢查询开始,到开启日志、EXPLAIN 分析,再到复合索引设计,最后落到最左前缀原则——这是一条完整的优化链路。每一步都有对应的工具和方法,缺一不可。

核心总结:慢查询优化不是玄学,是一套可复用的方法论。先抓日志,再 EXPLAIN,最后设计索引。按这个流程走,90% 的慢查询都能在半小时内解决。