做 MySQL 优化这行,我见过太多人看见慢查询就无脑加索引。加完索引发现不生效,又开始怀疑 MySQL 版本有问题、数据量太大了、服务器磁盘不行了。其实绝大多数时候,问题都不在这些地方,而是你根本没搞清楚查询到底是怎么执行的。
我自己早期做过一个订单列表功能,越查越慢。我当时咔咔给表加了六个索引,结果慢的问题没解决,INSERT 反而慢了不少。后来老老实实用 EXPLAIN 一分析,发现该用的索引压根没被选上,建的一堆索引里真正被查询用到的只有两个。从那以后我给自己定了个规矩:任何索引的增删,都必须先跑一遍 EXPLAIN,让查询执行计划给我“签字画押”。
这篇文章不聊大而全的优化理论,就讲我怎么用 EXPLAIN 来判断一条查询该加什么索引、不该留什么索引。整个过程会带几个真实 SQL 案例,每个都有前后对比和数据佐证,可以直接搬到你的业务里参考。
1. EXPLAIN到底在告诉你什么:一张表读懂查询执行计划
1.1 EXPLAIN是一条命令,不是一个“建议”
先摆个基本认知:EXPLAIN 不是 MySQL 给你的优化建议,而是 MySQL 优化器基于当前表结构和统计信息算出来的“执行计划”预览。你在执行 SELECT 之前,优化器已经把表访问顺序、索引选择、连接方式都定好了,EXPLAIN 只是把这个计划打印出来给你看。
用法就一句话:
EXPLAIN SELECT user_id, status, created_at FROM orders WHERE user_id = 123;在 SELECT 前面加 EXPLAIN,MySQL 不会真的去拉取结果集,而是返回一张表,告诉你它打算怎么查。你需要做的,是在脑子里把这张表翻译成一句话:“它是准备翻全表,还是走索引,还是先在索引里定位再回表拿数据”。这句话想清楚了,优化方向也就清楚了。
1.2 输出列逐个拆解:别被十几个字段吓到
EXPLAIN 的输出列不算少,不同 MySQL 版本还略有差异。很多人拿着输出问我“这一大堆是什么意思”,其实真正需要反复看的,只有五列。先把全貌列出来:
| 列名 | 含义 | 是否需要重点看 |
|---|---|---|
| id | 每个 SELECT 子句的序号 | 单表查询不用太在意 |
| select_type | SELECT 类型,SIMPLE、PRIMARY、SUBQUERY、DERIVED 等 | 子查询/多表时看执行顺序 |
| table | 访问哪张表 | 多表连接对照 id 看 |
| partitions | 命中的分区 | 分区表才用到 |
| type | 访问类型,从 system 到 ALL | 重点,索引好坏的直接体现 |
| possible_keys | 优化器认为可能用到的索引 | 和 key 对比看最有价值 |
| key | 优化器最终选中的索引 | 重点 |
| key_len | 用到的索引前缀字节数 | 判断联合索引实际用了哪几列 |
| ref | 索引等值匹配时参照的列或常量 | 和 key 配合看 |
| rows | 优化器估算需要扫描的行数 | 重点,但只是估算值 |
| filtered | 过滤后剩余行数的百分比 | 配合行数判断过滤效果 |
| Extra | 额外的执行信息 | 重点,filesort 和临时表在这里暴露 |
多数时候,你只需要盯住 type、key、key_len、rows、Extra 这五列,就足够判断一条查询的健康度了。
1.3 真正要关注的五件事
先说 type。type 如果出现 ALL,代表全表扫描,大表里的 ALL 基本都是慢查询的元凶。但要注意,如果表只有几百行,全表扫描并不慢,反而可能比走索引还快,因为索引回表有额外开销。优化永远是看场景,不是看单个指标。
再说 key。key 是优化器最终选中的索引,如果 key 是 NULL,说明 MySQL 没选任何索引。即使 possible_keys 罗列了一堆候选索引也没用,这时候要么是没索引可用,要么是索引被查询条件写失效了。
key_len 很容易被忽略,但它信息量很大。联合索引 (a, b, c) 到底用了哪几列的有序性,看 key_len 就知道。比如 a、b、c 都是 int,每个 4 字节,如果 key_len 是 8,说明 a+b 两列被用于定位,c 没有参与。这就是判断索引设计是否“用足”的关键证据。
rows 是优化器估算的扫描行数,虽然是估计值,但量级通常是可信的。如果 rows 到了几十万,即使 key 不是 NULL,也说明索引的选择性不好,或者查询条件本身就没法有效过滤数据。
Extra 里的标志最直观。出现 Using filesort 说明查询要额外排序,Using temporary 说明用了临时表,这两个都是危险信号。反过来,Using index 表示覆盖索引,Using index condition 表示二级索引条件下推,都是值得高兴的事。
把这五列养成条件反射,一张 EXPLAIN 表三秒钟就能读完,比看完整份输出高效得多。
2. 读懂type:索引用得好不好,全看这一列
2.1 type 从快到慢的完整序列
type 列的值不是随便枚举的,它是一条完整的“速度阶梯”。从快到慢大致是:
system > const > eq_ref > ref > range > index > ALL
我给每个层级配一个直观解释:
- system:表里只有一行,极致情况。
- const:主键或唯一索引匹配到一行,一次命中。比如 WHERE id = 100。
- eq_ref:多表连接时,被驱动表通过主键或唯一索引等值匹配,每一行只匹配一条。连接查询最理想的内层访问方式。
- ref:使用普通索引做等值匹配,可能匹配到多行。比如 WHERE status = 0,status 不是唯一索引。
- range:索引范围扫描,常见于 <、>、BETWEEN、LIKE 'abc%' 这类条件。
- index:扫描整个索引树,比全表扫描快一点,因为索引树比数据页小,但本质上还是“全扫”。
- ALL:全表扫描,从头到尾翻数据页,最糟糕的访问方式。
2.2 用坐电梯来理解这套速度层级
很多人第一次接触 type 觉得很抽象,我用一个生活化的类比:等电梯。ALL 相当于从 1 楼走楼梯上 30 楼,每一层都经过;index 相当于坐了一趟每层都停、但不让你出电梯的“慢梯”,比走楼梯快,但依然费时间;range 相当于从 10 层坐到 20 层,只停一段;ref 相当于你按了目的地楼层,中间不耽误;const 像专属电梯,按一下就到位。这么一想,EXPLAIN 里的 type 不再是英文字母,而是每天都可能遇到的问题。
2.3 实战判断标准:什么样的 type 算合格
这不是死标准,得由表大小和查询特征共同决定。我自己有一套判断习惯:
- 单表等值查询,能到 ref 以上就算合格,const、eq_ref 更优。
- 等值加范围混合的查询,能到 range 就接受。
- 如果出现 index 或 ALL,但估算扫描行数只有几百行,可以先不强求;一旦 rows 过万,就该认真优化。
- 连接查询里,被驱动表的访问类型最低也要 range,理想是 ref 或 eq_ref。
记住一句话:优化不是非要把 type 改成 const 才叫成功,而是让 type 和 rows 的组合符合查询特征,把无效的大范围扫描干掉。
3. 实操案例一:慢查询“加了索引还是慢”,问题出在哪
3.1 真实场景:订单查询越查越慢
假设有一张订单表 orders,表结构大致如下:
CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, status TINYINT NOT NULL DEFAULT 0, pay_type TINYINT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL, amount DECIMAL(10,2) NOT NULL, INDEX idx_user (user_id), INDEX idx_status (status) ) ENGINE=InnoDB;业务上经常跑这样一条查询:取出某个用户、状态为待支付、支付方式为线上的订单列表,用于后台自动催付。
SELECT * FROM orders WHERE user_id = 10086 AND status = 0 AND pay_type = 1 ORDER BY created_at DESC LIMIT 20;业务方反馈这条 SQL 越来越慢,问我是不是该把 pay_type 也加到索引里。我听到这句话就明白了,典型的“还没看执行计划就急着加索引”。先跑 EXPLAIN,让数据说话。
3.2 第一次EXPLAIN:问题全部暴露
执行:
EXPLAIN SELECT * FROM orders WHERE user_id = 10086 AND status = 0 AND pay_type = 1 ORDER BY created_at DESC LIMIT 20;关键输出如下:
| type | possible_keys | key | key_len | rows | Extra |
|---|---|---|---|---|---|
| ref | idx_user, idx_status | idx_user | 8 | 45600 | Using where; Using filesort |
看到没有,possible_keys 里有两个索引,最终 key 只选了 idx_user。为什么?因为 user_id 是等值条件,大概率过滤性比 status 好。但 rows 显示要扫描 45600 行,说明这个用户的历史订单量很大,MySQL 在 idx_user 上定位到一批订单后,还得继续过滤 status 和 pay_type 两个条件,最后还要对 created_at 排序。
Extra 里有两个关键信号:Using where 说明其他过滤条件是在回表后执行的;Using filesort 说明排序没走索引,要额外做一次文件排序。这两个加在一起,就是这条查询慢的根源。
3.3 设计联合索引:等值条件在前,排序字段在后
基于上面的执行计划,优化目标非常清晰:让 user_id 定位之后,能直接利用索引继续过滤 status 和 pay_type,再让 created_at 的有序性承担排序,省掉 filesort。
很多人天真地以为把 WHERE 里的字段都塞进索引就行。但列顺序是有讲究的:等值条件放前面,范围条件放中间,排序字段放最后。这里 user_id、status、pay_type 都是等值匹配,created_at 负责排序,所以合理的设计是:
ALTER TABLE orders ADD INDEX idx_user_status_pay_created (user_id, status, pay_type, created_at);为什么要让 created_at 放最后?因为联合索引本身就是一棵按列顺序排序的 B+ 树。前面的列都是等值匹配时,后面的 created_at 列才能保持全局有序,MySQL 才能通过索引直接反向读取来满足 ORDER BY created_at DESC。如果把 created_at 放在中间,前面一旦有等值条件,它的排序性就被“截断”了,排序依然要 filesort。
另外 pay_type 和 status 之间的顺序,实际影响不大,因为它们区分度都低。经验法则:等值字段按选择性从高到低排,区分度高的放前面,这样才能让每层索引树分支尽快收窄。
3.4 第二次EXPLAIN:索引增加后的前后对比
索引建好后再跑一次 EXPLAIN:
EXPLAIN SELECT * FROM orders WHERE user_id = 10086 AND status = 0 AND pay_type = 1 ORDER BY created_at DESC LIMIT 20;优化后的输出:
| type | key | key_len | rows | Extra |
|---|---|---|---|---|
| ref | idx_user_status_pay_created | 10 | 128 | Using index condition; Backward index scan |
rows 从 45600 降到 128,下降了两个数量级。key_len 是 10,正好是 user_id(BIGINT 8 字节)+ status(TINYINT 1 字节)+ pay_type(TINYINT 1 字节)这三列等值条件占用的字节数。created_at 没有计入 key_len,因为它不参与定位,而是通过索引反向扫描来满足排序。Extra 里的 Backward index scan 是 MySQL 8.0 的新能力,表示它反向扫描索引来支持 DESC 排序,不再需要 filesort。
如果业务上只需要查订单号的某些列,还能把查询列都收进索引做覆盖索引,连回表都省掉。但覆盖索引会进一步加大索引体积,属于费用权衡,不是无脑追求。
3.5 这个案例告诉我们什么
这个案例完全印证了标题那句话:不要一味创建索引。建索引之前,表上已经有两个单列索引,但它们各管各的,没法形成合力。真正解决问题的是根据查询重新设计联合索引。
更值得注意的是,新索引建立之后,旧的 idx_user 看起来就有点冗余了。因为 idx_user 只有 user_id 一列,而新索引以 user_id 开头并且覆盖了更多列,任何能用 idx_user 的查询都能改走新索引。这意味着 idx_user 可以考虑删除。这就是“根据查询增加和删除索引”的完整闭环,我在下一个案例详细展开。
4. 实操案例二:不删无用的索引,优化等于白做
4.1 索引的代价:你加的每个索引都会反噬
索引从来不是免费的午餐。每个二级索引都会在写入时同步维护,INSERT、UPDATE、DELETE 的代价会随索引数量上升;每个索引还占用磁盘空间和 InnoDB 缓冲池的内存。一张 1000 万行的表,每多一个二级索引,额外占用的磁盘空间经常以 GB 计算。
更重要的是,索引维护是实时的。你给高频写入的表增加一个索引,等于让每一次 INSERT 都多写一棵 B+ 树。所以“索引越多查询越快”这种认知,在写多读少的业务里会变成灾难。
尤其是为了某个查询新加了联合索引之后,之前专为旧查询建立的单列索引很大概率变成冗余。这时不删掉,就是在持续给写入流程上枷锁。
4.2 怎么发现冗余索引:用信息模式查索引关系
我在案例一里建了 idx_user_status_pay_created,它是以 user_id 开头的四列联合索引。此时旧的 idx_user(user_id) 就完全冗余,因为任何查询如果用 idx_user,都可以改走新联合索引,而且新索引能过滤更多条件。
除了人工分析,更系统的方法是直接查系统元数据。看表上所有索引:
SELECT INDEX_NAME, GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) AS columns FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = 'mydb' AND TABLE_NAME = 'orders' GROUP BY INDEX_NAME;输出结果大概是这样的:
| INDEX_NAME | columns |
|---|---|
| PRIMARY | id |
| idx_user | user_id |
| idx_status | status |
| idx_user_status_pay_created | user_id,status,pay_type,created_at |
一眼就能看出,idx_user 的列组合是 idx_user_status_pay_created 的最左前缀,属于重复索引。判断规则很简单:如果索引 A 的列组合是索引 B 的列组合的连续前缀,并且排序方向一致,索引 A 就是冗余的。这里 idx_user 只是 user_id,正是联合索引的最左前缀,可以安全删除。
然后执行删除:
ALTER TABLE orders DROP INDEX idx_user;删除后要回归线上验证。稳妥的做法是,先跑一遍所有涉及该索引的业务 SQL 的 EXPLAIN,确认 key 列会落到联合索引或者有等价执行路径,再在低峰期执行 DDL。
4.3 慢查询日志加实际使用度排查:用数据找“僵尸索引”
如果表上索引组合不直观,没法一眼判断谁冗余,可以用慢查询日志和 performance_schema 做统计。先把慢查询日志打开:
SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 1;然后跑一段时间,捞出来哪些 SQL 慢。对每条慢 SQL 执行 EXPLAIN,记录它实际使用的 key。一段时间之后你会发现,某些索引在 EXPLAIN 里从未被选为 key。这种“永远没出场”的索引,要么是设计时拍脑袋建的,要么是它对应的查询早已下线,可以纳入删除候选。
但这里有一个重要提醒:EXPLAIN 没走某个索引,不代表业务就一定没用到它。优化器在小表上可能故意选择全表扫描,因为走索引回表反而更慢。判断时一定要结合表大小和查询特征,千万别只凭一次 EXPLAIN 就草率删索引。
我的习惯是:先把要删的索引定义备份到版本控制,删完之后观察线上写入性能和慢查询曲线,出了问题马上从备份脚本里捞回定义重建。删索引可以很快,重建索引在大表上可能要锁表很久,所以预案必须做在前面。
4.4 删除索引后的效果:写入更轻了
我在一张约 600 万行的业务表上做过类似操作,删掉两个冗余单列索引后,这条业务链路的写入平均响应时间降了 15% 左右。原因很简单,少了两个二级索引,每次 INSERT 插入索引树的节点操作明显减少,同时 InnoDB 缓冲池里也少了两棵索引树占用的内存。查询端因为有覆盖场景更合理的联合索引,不但没有变慢,部分 SQL 反而更快了。这就是“加减结合”的甜头。
5. 索引失效的七个坑,EXPLAIN一查一个准
5.1 对索引列做函数操作
最常见的坑。假设 orders 表有 idx_created(created_at),查询:
SELECT * FROM orders WHERE DATE(created_at) = '2024-03-18';EXPLAIN 通常显示 type=ALL,key=NULL。原因很简单:索引里存的是原始日期值,不是 DATE(created_at) 的结果。对列做函数运算后,索引顺序对不上这个条件了。改成范围查询就正常了:
SELECT * FROM orders WHERE created_at >= '2024-03-18' AND created_at < '2024-03-19';改完 type 会变成 range,rows 大幅下降。这也是业务里常见的错误写法,排查时看到 DATE()、MONTH()、YEAR() 这类函数,直接想都不想要改写。
5.2 隐式类型转换的坑
假如 user_id 列是 varchar(32),但业务代码里传了数字:
SELECT * FROM users WHERE user_id = 10086;MySQL 会把字符串列转成数字再比较,等于对 user_id 做了隐式函数操作。EXPLAIN 里同样会发现索引失效。解决办法就是让查询参数的类型和列类型保持一致,代码里不要用数字接字符串列。这个坑在 ORM 框架里尤其常见,因为 ORM 生成的参数类型有时候会跟表结构对不上。
5.3 前导模糊查询
SELECT * FROM orders WHERE remark LIKE '%加急%';前缀不确定,索引无法定位起点,只能全表扫。反过来:
SELECT * FROM orders WHERE remark LIKE '加急%';这就是 range 查询,可以用索引。业务上如果非要做包含匹配,考虑全文索引或者专门检索系统,别死磕单表 SQL。
5.4 OR 条件把好牌打烂
SELECT * FROM orders WHERE user_id = 123 OR status = 0;如果 user_id 和 status 各有索引,优化器可能不选择“分别走索引再合并”,而是直接全表扫描。因为两个条件跨列,MySQL 要算出两个集合再求并集,代价往往比全表扫描还高。稳妥的写法是用 UNION ALL 拆分:
SELECT * FROM orders WHERE user_id = 123 UNION ALL SELECT * FROM orders WHERE status = 0;重写后两条子查询各自都能用索引。不过这种改写要先确认业务语义是否允许两个条件取并集。
5.5 NOT IN 和 NOT LIKE
理论上 MySQL 判断不等于条件时,可以先走索引取全集再排除,但大多数优化器会直接选全表扫描,因为“不等于”的分支太多,选择性太差。比如 status <> 0。如果一个列只有两三个取值,你反而可以用 IN 改写,让查询变成等值匹配。这种改写要小心业务语义,但方向上是对的。
5.6 联合索引不满足最左前缀
联合索引 (a, b, c) 只能从 a 开始用。查询里如果直接只写 b = 1 和 c = 2,EXPLAIN 往往显示 type=index 甚至 ALL。虽然索引文件存在,但没法用前缀定位。这也是为什么设计联合索引前,一定要把高频查询条件的列顺序想清楚。
5.7 排序方向和索引顺序不匹配
索引 (created_at) 默认升序存储,你 ORDER BY created_at DESC,MySQL 8.0 可以反向扫描索引,Extra 会显示 Backward index scan,这不算失效。但如果你在联合索引里有多个排序字段,比如 ORDER BY a ASC, b DESC,索引按 (a ASC, b ASC) 存储,此时两个字段排序方向不一致,就会触发 Using filesort。如果必须有这种混排,MySQL 8.0 之后可以给 b 列建降序索引来贴合。
5.8 失效场景速查表
| 场景 | 失效原因 | EXPLAIN 常见表现 | 推荐改法 |
|---|---|---|---|
| 函数运算 | 索引列被计算 | ALL / key=NULL | 改写为范围查询 |
| 隐式类型转换 | 列类型和参数类型不一致 | ALL / key=NULL | 统一类型 |
| LIKE '%xx' 前导模糊 | 无固定起点 | ALL | 改为后缀匹配或全文索引 |
| OR 跨列条件 | 合并成本高 | ALL 或索引合并不稳定 | UNION ALL 拆分 |
| NOT IN / <> | 选择性差 | ALL | 语义允许时改用 IN |
| 最左前缀违反 | 联合索引从中间列开始用 | index / ALL | 调整查询或改索引列顺序 |
| 排序方向不匹配 | 索引序与需求相反 | Using filesort | 单独索引或降序索引 |
排查这些坑的通用方法只有一个:对每一条可疑 SQL,都带着 EXPLAIN 去看,盯着 type 和 Extra 判断方案优劣,而不是靠猜。
6. EXPLAIN 进阶:JSON 格式和 EXPLAIN ANALYZE 怎么用
6.1 FORMAT=JSON:看优化器算的成本
普通表格输出不够用时,可以用 JSON 格式:
EXPLAIN FORMAT=JSON SELECT * FROM orders WHERE user_id = 10086 AND status = 0 AND pay_type = 1 ORDER BY created_at DESC LIMIT 20;JSON 输出里会给出每个访问路径的 cost 值,最关键的是会在“attached_condition”里显示优化器对每个可走索引的评估。当 possible_keys 里有多个候选索引时,JSON 能告诉你优化器为什么选了这个、放弃了另一个。看 cost 不是追求绝对准确,而是理解优化器的决策逻辑,避免你再建一个它根本不会选的新索引。
6.2 EXPLAIN ANALYZE:真实执行,给你实际行数和耗时
MySQL 8.0.18 之后,有个更狠的命令:EXPLAIN ANALYZE。它不只是给执行计划,而是真的执行这条 SQL,返回每一步的实际行数、实际耗时和循环次数。
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 10086 AND status = 0 AND pay_type = 1 ORDER BY created_at DESC LIMIT 20;输出类似下面这样:
-> Limit: 20 rows (actual time=0.4..0.4 rows=20 loops=1) -> Index lookup on orders using idx_user_status_pay_created (user_id=10086, status=0, pay_type=1) (cost=2.3 rows=128) (actual time=0.3..0.3 rows=20 loops=1)注意看 cost 后面的记录是优化器估算的 rows=128,而 actual rows 是 20。当 estimated 和 actual 差距巨大时,往往是统计信息过期了,要跑 ANALYZE TABLE 重新收集统计信息。这是普通 EXPLAIN 永远发现不了的问题。
6.3 我的使用经验:什么时候用 EXPLAIN ANALYZE
线上高并发环境不要随便跑 EXPLAIN ANALYZE,因为它真的会执行 SQL,一条大扫表查询可能直接打爆数据库。我的习惯是在测试库跑 EXPLAIN ANALYZE,线上只跑普通 EXPLAIN 或者 FORMAT=JSON。如果你非要在线上看真实执行时间,建议加 LIMIT 并且挑低峰期,比如限制行数的查询,影响可控。
另外,EXPLAIN ANALYZE 的输出非常长,不要只看最后一行,要顺着执行计划树一层层看。慢的节点往往藏在中间层的“actual time”里,而不是最外层。这点和看普通 EXPLAIN 完全不同。
7. 建立索引管理的闭环流程:从建到删都不拍脑袋
7.1 我现在的索引增删五步法
这套方法是我踩了不少坑之后固定下来的,简单可复制:
- 抓慢 SQL。打开慢查询日志,或者直接用 performance_schema 查事件统计,把 TOP N 慢查询捞出来。
- 逐个 EXPLAIN。对每条慢 SQL 跑 EXPLAIN,记录 type、key、rows、Extra,重点搞清楚慢点在哪:是全表扫、回表过多、filesort,还是临时表。
- 逆推索引设计。根据 WHERE 的等值、范围、排序字段设计联合索引,列顺序遵循“等值前置、范围中置、排序后置”。如果索引已经存在但没被用上,先查失效原因,而不是急着再建一个。
- 前后对比验证。建完索引再跑 EXPLAIN 和真实执行时间,确认 rows 下降、type 提升、filesort 消失。不能只看 EXPLAIN 满意就完事,要在测试库压一遍真实数据量和并发。
- 清理冗余。建立每张表的索引清单和用途清单,删除冗余索引和长期未被使用的索引。这一步必须做,否则索引会越堆越多。
7.2 日常巡检:把索引管理变成常规动作
我习惯每周跑一次索引健康巡检,主要用三条 SQL:
-- 1. 列出所有索引及对应列 SELECT TABLE_NAME, INDEX_NAME, GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) AS columns FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = 'mydb' GROUP BY TABLE_NAME, INDEX_NAME; -- 2. 对比慢查询日志,找出 EXPLAIN 里从未使用过的索引 -- 3. 查看索引占用空间 SELECT TABLE_NAME, INDEX_NAME, ROUND(STAT_VALUE/1024/1024, 2) AS size_mb FROM performance_schema.table_io_waits_statistics_by_table WHERE TABLE_SCHEMA = 'mydb';这套巡检做下来,我基本能保证每张表上的每个索引都有“存在的理由”:要么是高频查询的执行路径,要么是唯一性约束,要么是外键约束需要的辅助索引。没有理由的索引,一律进入删除候选。
回归业务场景再看一遍,我处理过的那些慢查询,几乎都不需要你疯狂建索引。真正需要的,其实是先看懂查询的执行计划,再决定一个联合索引怎么建、两个冗余索引怎么删。EXPLAIN 就是把“增加索引”和“删除索引”这两件事变成有据可依的核心工具。我现在写任何优化方案,第一页永远是 EXPLAIN 输出对比,第二页才是索引变更脚本。这个习惯坚持下来,慢查询和索引炸弹都会离你远一点。