数据库

MySQL 索引设计与 EXPLAIN 分析:从小余量到慢查询清零

表上索引越加越多,慢查询却不见减少,多半是索引没设计到位。本文从实际业务 SQL 出发,讲复合索引顺序、覆盖索引、索引失效场景,并用 EXPLAIN 逐段解析,把执行计划读明白。

作者:巧匠团队·8 分钟阅读·更新于 2026-09-06

先看清一条慢 SQL 的执行计划长什么样

改索引之前,先把慢查询日志打开,定位到具体某条 SQL,然后对这条 SQL 单独跑 EXPLAIN。不要凭对数据量的印象去猜,执行计划会直接告诉你全表扫描、走了哪条索引、扫描了多少行、有没有回表。

以一条订单查询为例:EXPLAIN 结果里 type 字段是核心。出现 ALL 就是全表扫描,出现 ref 或 eq_ref 代表走普通索引或唯一索引,出现 index 说明覆盖了索引但只是扫描索引树。rows 是估算扫描行数,Extra 里的 Using filesort 或 Using temporary 则是要重点优化的信号。

EXPLAIN 只能看估算,MySQL 8.0 以下没给出精确成本,但足够做相对比较:优化前后 rows 是否明显下降、type 是否从 ALL 提升到 range/ref,就能判断改动是否有效。

SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;   # 超过 1 秒记录
EXPLAIN SELECT order_id, user_id, amount, status
  FROM orders
  WHERE user_id = 10086 AND status = 'UNPAID'
  ORDER BY created_at DESC;

复合索引的顺序别乱排:先等值、再范围、后排序

复合索引 (a, b, c) 只有在最左前缀匹配时才能生效,而列的顺序直接影响能否用到范围条件和能否省掉排序。经验法则:把等值筛选的列放前面,范围筛选放中间,用于排序的列放最后,因为一旦某个列做了范围比较,其后的列就基本用不上索引了。

比如查询条件是 user_id 等值 + status 等值 + ORDER BY created_at,那么建 (user_id, status, created_at) 就既能走索引过滤,又能让 MySQL 直接按索引顺序取出结果而免去 filesort。如果你建成了 (status, user_id, created_at),等值列被拆开,联合索引的很多优势就浪费了。

-- 推荐:等值(user_id,status) + 排序列放最后
CREATE INDEX idx_usr_status_created ON orders(user_id, status, created_at);
-- 结果:Extra 不再出现 Using filesort

覆盖索引是"少一次回表"的白送福利

当 SELECT 的所有列都在索引里,MySQL 就能只扫索引而不回表,这就是覆盖索引。它能把大表查询提速明显,尤其适合高频只读的小字段列表页。

这要求你选择需要的列而不是 SELECT *。比如列表页只需要 id、title、updated_at,就建一个覆盖 (category_id, updated_at, title) 的索引。EXPLAIN 里 Extra 出现 Using index 就说明命中了覆盖索引,这时 type 即使偏低性能也通常可接受。代价是每个列都占存储,索引太多会拖慢写入,所以在读多写少的列表场景用覆盖索引最划算。

EXPLAIN SELECT id, title, updated_at FROM articles WHERE category_id = 5;
-- Extra: Using index   <- 命中覆盖索引,未回表

一张表列几个常见的"索引失效"陷阱

索引不是建了就会用。最常见的三类失效:对索引列用了函数或表达式,比如 WHERE DATE(created_at) = '2026-09-06',函数让索引失效;对索引列做隐式类型转换,比如字符串列用数字比较;以及 LIKE 前缀通配,WHERE name LIKE '%keyword%' 无法走索引。

这些都是可以通过改写 SQL 修复的:把函数移到等号右侧或换成范围写法,比如 created_at BETWEEN '...' AND '...';保证参数类型和列类型一致;需要模糊搜索时改用全文索引或第三方搜索引擎。核对方式很简单,改完再跑一次 EXPLAIN 看 type 是否仍是 ref/range。

-- 陷阱写法(DATE 函数使 created_at 索引失效)
SELECT * FROM orders WHERE DATE(created_at) = '2026-09-06';
-- 修复写法(范围扫描可走索引)
SELECT * FROM orders WHERE created_at >= '2026-09-06 00:00:00' AND created_at < '2026-09-07';

用线上慢日志验证优化是否清零

优化收尾的标准不是"EXPLAIN 好看",而是线上真实慢查询降下来。打开慢查询日志后持续观察一段时间,看这一条 SQL 是否还出现在 TOP 里,同时对比优化前后的平均耗时和 rows 扫描数。

建议记录优化前的基线(执行一条 SQL 的耗时、rows、type),优化后在生产一次性脚本里按同参数重跑对比。如果 EXTRA 里的 Using filesort / Using temporary 消失了,且 EXPLAIN 的 type 提升到 range/ref,就基本可以认定为有效;若仍慢,再用 optimizer trace 检查是不是优化器没选中你想要的索引,必要时强制索引 cue 来验证猜想。

SHOW VARIABLES LIKE 'long_query_time';
# 查看某条 SQL 的优化器选择
SET optimizer_trace = 'enabled=on';
EXPLAIN SELECT ... ;
SELECT * FROM INFORMATION_SCHEMA.OPTIMIZER_TRACE;

官方参考来源

下方为命令对应的官方权威文档,供你核对最新用法与深入查阅。