MySQL EXPLAIN 怎么用:不要一味创建索引,而要根据查询增加和删除索引

4 阅读13分钟

MySQL EXPLAIN 怎么用:不要一味创建索引,而要根据查询增加和删除索引

很多团队优化 SQL 时,第一反应都是“加索引”。
但真正成熟的做法不是“索引越多越好”,而是:先看查询,再看执行计划,再决定索引该加、该改,还是该删。


1. 先说结论:EXPLAIN 不是看个 type=ref 就结束了

很多人学习 MySQL 优化时,第一步是学会 EXPLAIN
这当然没问题,但常见误区是把 EXPLAIN 简化成几条机械规则:

  • type=ALL 不好
  • type=ref 不错
  • Using index 很好
  • 没走索引就加一个

这些判断不能说错,但如果只停留在这个层面,优化很容易做偏。

真正有用的结论应该是:

EXPLAIN 不是用来证明“用了索引”,而是用来理解“数据库为什么会这样查”。

也就是说,重点不在于“有没有索引”,而在于:

  • 当前 SQL 的访问路径是什么
  • 扫描代价大不大
  • 回表代价大不大
  • 排序和分页代价大不大
  • 这个索引是否真的服务于高频查询
  • 现有索引是不是已经冗余甚至拖累写入

所以这篇文章想讲清楚两件事:

  1. EXPLAIN 到底该怎么看
  2. 为什么索引要根据查询生命周期来增删,而不是越多越好

2. 为什么“有问题就加索引”是危险习惯

在很多业务项目里,索引膨胀是一个非常常见的问题。

典型路径是这样的:

  1. 某个查询慢了
  2. 大家看一眼 SQL
  3. 给某个字段加个索引
  4. 查询暂时快了
  5. 以后又来了新查询,再加一个
  6. 几个月后,一张表挂了十几个索引

表面上看,这像是在持续优化。
实际上,很多时候是在持续堆积负担。

因为索引不是免费的,它会带来明确成本:

  • 插入更慢
  • 更新更慢
  • 删除更慢
  • 索引页占空间
  • Buffer Pool 压力更大
  • 优化器选择路径更复杂
  • 冗余索引更难维护

也就是说:

每增加一个索引,都是在用写入成本、存储成本和维护成本,换取某些查询的读取收益。

如果这个交换没有基于真实查询做判断,那大概率不是优化,而是在制造未来的技术债。


3. EXPLAIN 的核心价值是什么

EXPLAIN 的作用不是让你背字段,而是让你看到 MySQL 打算怎么执行这条 SQL。

最基本的用法:

EXPLAIN
SELECT id, user_id, status
FROM orders
WHERE user_id = 1001
  AND status = 1
ORDER BY id DESC
LIMIT 20;

在 MySQL 8 中,通常还会用:

EXPLAIN ANALYZE
SELECT id, user_id, status
FROM orders
WHERE user_id = 1001
  AND status = 1
ORDER BY id DESC
LIMIT 20;

两者差异很关键:

  • EXPLAIN 看的是优化器预估路径
  • EXPLAIN ANALYZE 看的是实际执行过程和真实耗时

如果数据库版本允许,后者通常更有价值。
因为很多性能误判,恰恰就出在“预估看起来合理,但真实执行并不理想”。


4. 看 EXPLAIN,重点不是字段全背下来,而是抓住这几个核心

4.1 type:访问方式粗略好坏

type 是很多人最先看的字段,它确实重要。
常见值从差到好,大致可以粗略理解为:

  • ALL:全表扫描
  • index:全索引扫描
  • range:范围扫描
  • ref:普通索引等值匹配
  • eq_ref:唯一索引等值匹配
  • const:常量级命中

但不要犯一个常见错误:

type 好,不代表查询就一定快。

例如:

  • range 可能扫很多行
  • ref 后面仍然可能大量回表
  • index 虽然比 ALL 好一点,但仍可能非常重

所以 type 只能给你一个初步印象,不能单独下结论。

4.2 key:用了哪个索引

key 表示实际选择了哪个索引。
很多人看到 key 不为空就放心了,这也不够。

你还要继续问:

  • 用的是不是预期中的索引
  • 有没有选错索引
  • 这个索引是主过滤条件还是凑合能用
  • 为什么不用另一个索引

“用了索引”不等于“用对了索引”。

4.3 rows:预计要扫描多少行

这是一个非常重要的指标。

因为数据库性能很多时候不是败在“有没有索引”,而是败在:

虽然用了索引,但要扫的记录还是太多。

例如一个查询:

  • type=ref
  • key=idx_status
  • rows=500000

这看起来用了索引,但如果 status 只有几个离散值,扫描 50 万行依然可能很慢。

也就是说,索引字段的区分度不够,虽然“命中索引”,但收益有限。

4.4 filtered:过滤后大概剩多少

这个字段能帮助你判断:

  • 扫描进来的行有多少真正符合条件
  • 条件是不是在索引层就过滤掉了
  • 还是要回表后再继续过滤

它不是绝对精确值,但能给出一个信号:
当前访问路径是不是足够“精准”。

4.5 Extra:这里往往藏着最关键的信息

Extra 很容易被忽略,但很多问题就藏在这里。

常见值包括:

  • Using where
  • Using index
  • Using temporary
  • Using filesort
  • Using index condition

其中最需要警惕的通常是:

  • Using temporary
  • Using filesort

它们不一定绝对有问题,但在大结果集、分页、分组、排序场景中,往往意味着额外成本。

同样要注意:

Using index 也不一定就是“完美查询”。

它往往表示覆盖索引读取,但如果扫描范围很大,一样可能重。


5. 真正实用的看法:把 EXPLAIN 当成“查询路径诊断工具”

一个更实用的阅读顺序通常是:

  1. 先看 SQL 是查什么场景
  2. 再看 key 选了谁
  3. 再看 type 大概访问方式
  4. 再看 rows 扫描规模
  5. 再看 Extra 有没有排序、临时表、回表风险
  6. 最后再判断索引是否应该新增、调整或删除

也就是说,EXPLAIN 不是孤立看的。
它必须跟这几个上下文一起看:

  • 真实业务场景
  • 数据量
  • 查询频率
  • 分页方式
  • 排序方式
  • 是否写多读少

脱离查询场景讨论索引,结论通常不可靠。


6. 一个常见误区:只根据 WHERE 字段建索引

很多人建索引时,思路是:

  • SQL 里用了哪些字段
  • 就给这些字段建索引

这不完整。

因为一个查询是否高效,往往不只取决于 WHERE,还取决于:

  • ORDER BY
  • GROUP BY
  • LIMIT
  • 回表字段
  • 联合索引顺序

例如这个 SQL:

SELECT id, user_id, created_at
FROM orders
WHERE user_id = 1001
  AND status = 1
ORDER BY created_at DESC
LIMIT 20;

很多人可能会想到:

  • user_id 建索引
  • status 建索引

但更合理的问题应该是:

这个查询的最佳访问路径,是不是应该由一个联合索引统一支撑?

例如:

(user_id, status, created_at)

这样 MySQL 才更有机会同时利用:

  • 过滤条件
  • 排序条件

而不是先筛一批数据,再额外排序。

所以索引设计不能只看字段出现与否,而要看整个查询形态。


7. 为什么索引应该“根据查询增加”,而不是按字段随手创建

真正成熟的索引策略通常遵循一句话:

索引不是围着字段建,而是围着高频查询模式建。

这句话很重要。

因为业务系统真正需要优化的,从来不是“字段”,而是“访问模式”:

  • 根据用户 ID 查订单列表
  • 根据状态和时间范围筛选任务
  • 根据租户和更新时间拉取数据
  • 根据业务主键精确查一条记录

也就是说,一个索引存在的理由应该是:

  • 服务于某个高频或关键查询
  • 能明显降低扫描成本
  • 能改善排序 / 分页 / 覆盖能力
  • 能稳定查询路径

如果一个索引没有明确服务对象,它就很可疑。


8. 更重要的一半:索引也应该根据查询删除

这是很多团队几乎不做,但实际上非常重要的一件事。

8.1 为什么索引要删除

因为业务会变。

今天的高频查询,半年后可能已经下线。
今天的筛选条件,未来可能已经不用。
今天为了某个报表建的索引,未来可能再也没有流量命中。

如果索引只增不减,就会出现:

  • 冗余索引越来越多
  • 写入链路越来越重
  • 优化器选择变复杂
  • 表结构越来越难维护

所以索引不是“建完就永远有价值”,它和代码一样,也需要做生命周期管理。

8.2 什么样的索引值得怀疑

下面几类索引应该定期审视:

  • 几乎没有查询命中
  • 完全被更左前缀的联合索引覆盖
  • 只为历史功能服务,功能已下线
  • 跟当前核心查询路径已经不匹配
  • 写入代价明显,但收益不明确

举个简单例子:

如果你已经有:

KEY idx_user_status_created (user_id, status, created_at)

那下面这些索引就要重新评估价值:

KEY idx_user_id (user_id)
KEY idx_user_status (user_id, status)

它们未必都要立刻删,但至少应该先问一句:

这些索引是否真的还在承载独立查询价值,还是已经变成冗余负担?


9. 一套更靠谱的索引工作流

如果你的目标是把索引管理从“拍脑袋”变成“按查询演进”,一个更务实的流程通常是这样的。

9.1 第一步:先找高频和高成本查询

不要先看表结构,先看查询。

优先关注:

  • 慢 SQL 日志
  • 高频 API 对应 SQL
  • 排名靠前的读查询
  • 排名靠前的写查询

索引优化应该围绕这些真实查询展开,而不是在表设计阶段无限预判。

9.2 第二步:对关键 SQL 看 EXPLAIN / EXPLAIN ANALYZE

重点看:

  • 扫描行数是不是过大
  • 排序是不是额外成本
  • 是否发生临时表
  • 是否索引选择不理想
  • 是否存在回表过重

这一步不是为了“证明该加索引”,而是为了先找到真正的瓶颈位置。

9.3 第三步:决定是改 SQL,还是改索引,还是两者都改

很多慢查询并不是索引缺失,而是 SQL 形态本身不合理,例如:

  • 范围条件和排序不兼容
  • 分页过深
  • 一条 SQL 承担过多职责
  • 查询字段过多
  • 条件写法导致无法有效利用索引

这时候正确动作可能是:

  • 改查询方式
  • 改分页方式
  • 收缩返回字段
  • 调整联合索引顺序

而不是简单“再加一个索引”。

9.4 第四步:验证收益,而不是只看理论

索引加完以后,不要只看 EXPLAIN 变漂亮。

还要看:

  • 真实耗时是否下降
  • 高并发下是否稳定
  • 写入是否受影响
  • 其他查询是否被干扰

很多索引在单条 SQL 上有效,但从全局看可能不划算。

9.5 第五步:定期清理低价值索引

索引治理不是一次性动作。
它应该像代码重构一样,定期做。

一个成熟团队至少要形成这样的意识:

  • 索引可以新增
  • 索引也可以合并
  • 索引还可以删除

10. 三个很典型的错误索引思路

10.1 错误一:每个筛选字段都单独建索引

例如有这样的查询:

WHERE tenant_id = ?
  AND status = ?
  AND created_at >= ?
ORDER BY id DESC
LIMIT 50;

有些人会建:

  • idx_tenant_id
  • idx_status
  • idx_created_at

这看起来“都有索引了”,但很可能不如一个更贴近查询模式的联合索引。

10.2 错误二:看到慢 SQL 就立即补索引,不看写入代价

一张高写入表如果挂了很多索引,读是局部变快了,写可能整体变慢了。

特别是在这些表上要谨慎:

  • 订单表
  • 消息表
  • 日志表
  • 任务表
  • 高并发流水表

这些表的索引策略,必须读写一起看。

10.3 错误三:旧索引永远不删

这会导致典型的“历史包袱型表结构”:

  • 表里索引越来越多
  • 但没人知道每个索引是谁建的
  • 也没人知道哪些还在用

这类表最终很容易变成性能治理黑洞。


11. 一个更成熟的判断标准:索引是否值得存在

一个索引值不值得保留,我通常会看四件事:

11.1 它是否服务于高频或关键查询

如果不是,就要谨慎。

11.2 它是否明显降低扫描成本或排序成本

如果只是“理论上可用”,但收益不明显,也要谨慎。

11.3 它是否与现有联合索引重复

如果重复,就应该考虑合并或删除。

11.4 它带来的写入成本是否值得

如果一张表写很多、更新很多,那索引一定要克制。

也就是说,索引存在的依据不该是:

这个字段看起来以后可能会查到。

而应该是:

这个索引正在稳定地为重要查询提供收益,且这个收益值得它的成本。


12. 关于 EXPLAIN,一个更务实的使用姿势

如果你只是想把它真正用起来,而不是背概念,我建议按下面这个方式落地。

每次看 EXPLAIN 都问自己这几个问题

  1. 这条 SQL 真正要解决的业务场景是什么?
  2. 现在走的是哪个索引?是不是预期路径?
  3. 扫描行数大不大?
  4. 排序、临时表、回表成本大不大?
  5. 慢的根因是缺索引,还是查询设计本身有问题?
  6. 如果加索引,它服务的是长期高频查询,还是一次性需求?
  7. 如果已有多个相近索引,能不能合并或删除?

只要持续按这套问题去看,EXPLAIN 就会从“会用命令”变成“会做判断”。


13. 最后的结论

MySQL 优化的关键,不是会不会背 EXPLAIN 字段,也不是会不会不停加索引。

真正重要的是建立一个更成熟的思路:

  • 先看真实查询
  • 再看执行计划
  • 再决定改 SQL 还是改索引
  • 索引围绕查询模式设计
  • 索引要有生命周期管理
  • 新增索引,也要敢于删除索引

如果要把全文浓缩成一句话,可以这样说:

EXPLAIN 是用来理解查询路径的,索引是用来服务真实查询的;不要一味创建索引,而要根据查询增加、调整和删除索引。