MySQL EXPLAIN 怎么用:不要一味创建索引,而要根据查询增加和删除索引
很多团队优化 SQL 时,第一反应都是“加索引”。
但真正成熟的做法不是“索引越多越好”,而是:先看查询,再看执行计划,再决定索引该加、该改,还是该删。
1. 先说结论:EXPLAIN 不是看个 type=ref 就结束了
很多人学习 MySQL 优化时,第一步是学会 EXPLAIN。
这当然没问题,但常见误区是把 EXPLAIN 简化成几条机械规则:
type=ALL不好type=ref不错Using index很好- 没走索引就加一个
这些判断不能说错,但如果只停留在这个层面,优化很容易做偏。
真正有用的结论应该是:
EXPLAIN 不是用来证明“用了索引”,而是用来理解“数据库为什么会这样查”。
也就是说,重点不在于“有没有索引”,而在于:
- 当前 SQL 的访问路径是什么
- 扫描代价大不大
- 回表代价大不大
- 排序和分页代价大不大
- 这个索引是否真的服务于高频查询
- 现有索引是不是已经冗余甚至拖累写入
所以这篇文章想讲清楚两件事:
EXPLAIN到底该怎么看- 为什么索引要根据查询生命周期来增删,而不是越多越好
2. 为什么“有问题就加索引”是危险习惯
在很多业务项目里,索引膨胀是一个非常常见的问题。
典型路径是这样的:
- 某个查询慢了
- 大家看一眼 SQL
- 给某个字段加个索引
- 查询暂时快了
- 以后又来了新查询,再加一个
- 几个月后,一张表挂了十几个索引
表面上看,这像是在持续优化。
实际上,很多时候是在持续堆积负担。
因为索引不是免费的,它会带来明确成本:
- 插入更慢
- 更新更慢
- 删除更慢
- 索引页占空间
- 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=refkey=idx_statusrows=500000
这看起来用了索引,但如果 status 只有几个离散值,扫描 50 万行依然可能很慢。
也就是说,索引字段的区分度不够,虽然“命中索引”,但收益有限。
4.4 filtered:过滤后大概剩多少
这个字段能帮助你判断:
- 扫描进来的行有多少真正符合条件
- 条件是不是在索引层就过滤掉了
- 还是要回表后再继续过滤
它不是绝对精确值,但能给出一个信号:
当前访问路径是不是足够“精准”。
4.5 Extra:这里往往藏着最关键的信息
Extra 很容易被忽略,但很多问题就藏在这里。
常见值包括:
Using whereUsing indexUsing temporaryUsing filesortUsing index condition
其中最需要警惕的通常是:
Using temporaryUsing filesort
它们不一定绝对有问题,但在大结果集、分页、分组、排序场景中,往往意味着额外成本。
同样要注意:
Using index也不一定就是“完美查询”。
它往往表示覆盖索引读取,但如果扫描范围很大,一样可能重。
5. 真正实用的看法:把 EXPLAIN 当成“查询路径诊断工具”
一个更实用的阅读顺序通常是:
- 先看 SQL 是查什么场景
- 再看
key选了谁 - 再看
type大概访问方式 - 再看
rows扫描规模 - 再看
Extra有没有排序、临时表、回表风险 - 最后再判断索引是否应该新增、调整或删除
也就是说,EXPLAIN 不是孤立看的。
它必须跟这几个上下文一起看:
- 真实业务场景
- 数据量
- 查询频率
- 分页方式
- 排序方式
- 是否写多读少
脱离查询场景讨论索引,结论通常不可靠。
6. 一个常见误区:只根据 WHERE 字段建索引
很多人建索引时,思路是:
- SQL 里用了哪些字段
- 就给这些字段建索引
这不完整。
因为一个查询是否高效,往往不只取决于 WHERE,还取决于:
ORDER BYGROUP BYLIMIT- 回表字段
- 联合索引顺序
例如这个 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_ididx_statusidx_created_at
这看起来“都有索引了”,但很可能不如一个更贴近查询模式的联合索引。
10.2 错误二:看到慢 SQL 就立即补索引,不看写入代价
一张高写入表如果挂了很多索引,读是局部变快了,写可能整体变慢了。
特别是在这些表上要谨慎:
- 订单表
- 消息表
- 日志表
- 任务表
- 高并发流水表
这些表的索引策略,必须读写一起看。
10.3 错误三:旧索引永远不删
这会导致典型的“历史包袱型表结构”:
- 表里索引越来越多
- 但没人知道每个索引是谁建的
- 也没人知道哪些还在用
这类表最终很容易变成性能治理黑洞。
11. 一个更成熟的判断标准:索引是否值得存在
一个索引值不值得保留,我通常会看四件事:
11.1 它是否服务于高频或关键查询
如果不是,就要谨慎。
11.2 它是否明显降低扫描成本或排序成本
如果只是“理论上可用”,但收益不明显,也要谨慎。
11.3 它是否与现有联合索引重复
如果重复,就应该考虑合并或删除。
11.4 它带来的写入成本是否值得
如果一张表写很多、更新很多,那索引一定要克制。
也就是说,索引存在的依据不该是:
这个字段看起来以后可能会查到。
而应该是:
这个索引正在稳定地为重要查询提供收益,且这个收益值得它的成本。
12. 关于 EXPLAIN,一个更务实的使用姿势
如果你只是想把它真正用起来,而不是背概念,我建议按下面这个方式落地。
每次看 EXPLAIN 都问自己这几个问题
- 这条 SQL 真正要解决的业务场景是什么?
- 现在走的是哪个索引?是不是预期路径?
- 扫描行数大不大?
- 排序、临时表、回表成本大不大?
- 慢的根因是缺索引,还是查询设计本身有问题?
- 如果加索引,它服务的是长期高频查询,还是一次性需求?
- 如果已有多个相近索引,能不能合并或删除?
只要持续按这套问题去看,EXPLAIN 就会从“会用命令”变成“会做判断”。
13. 最后的结论
MySQL 优化的关键,不是会不会背 EXPLAIN 字段,也不是会不会不停加索引。
真正重要的是建立一个更成熟的思路:
- 先看真实查询
- 再看执行计划
- 再决定改 SQL 还是改索引
- 索引围绕查询模式设计
- 索引要有生命周期管理
- 新增索引,也要敢于删除索引
如果要把全文浓缩成一句话,可以这样说:
EXPLAIN 是用来理解查询路径的,索引是用来服务真实查询的;不要一味创建索引,而要根据查询增加、调整和删除索引。