先看清一条慢 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;