“我已经给这个字段加索引了,怎么还是慢?”——这句话我在团队里听过不止一次。加索引就像给一本书做目录,但很多人只做了目录,却没确认自己翻的是不是目录对应的那一页。数据库优化里最反直觉的一点是:加了索引不等于用上了索引,而用上了索引也不等于一定快。这篇文章想聊清楚三件事:索引到底在什么情况下会“失效”,怎么把执行计划读懂,以及在真实项目里我会怎么一步步把一条慢查询按下去。

一、索引失效:那些看起来加了、其实没走的查询
先说结论:索引不是数据库的“魔法加速器”,它只是一个按特定顺序排好的数据结构。只要你的查询条件无法利用这个顺序,优化器就会干脆放弃它,退化成全表扫描。
最常见的几种“失效”场景,我按踩坑频率排一下:
第一种,对索引列做了函数运算或隐式类型转换。比如 WHERE DATE(created_at) = '2026-09-11',一旦字段被函数包住,B+ 树的有序性就被人为破坏了,优化器只能逐行计算。正确写法是改造成范围查询:WHERE created_at >= '2026-09-11 00:00:00' AND created_at < '2026-09-12 00:00:00'。隐式转换同样隐蔽——当 user_id 是字符串类型,你却写了 WHERE user_id = 12345,MySQL 会对整列做类型转换,索引直接作废。这类问题最烦人的地方在于它不报错,只是悄悄变慢。
第二种,违反最左前缀原则。联合索引 (a, b, c) 就像按“先姓、再名、再生日”排好的名册,你跳过姓氏直接查名字,名册的顺序帮不上忙。所以 WHERE b = 1 用不上这个索引,WHERE a = 1 AND c = 3 也只能用到 a 这一段。设计联合索引时,第一件事不是想“哪些字段常用”,而是想“查询条件里哪个字段一定存在”——把那个字段放到最左边。
第三种,范围查询切断了后续列。同样是 (a, b, c),如果写 WHERE a = 1 AND b > 10 AND c = 3,那么 c 是用不上的——因为 b 是范围,后面已经无序了。很多人以为“只要写上就都能用”,其实索引列一旦遇到范围条件,后续列就退出加速队列。
还有一种容易被忽略的:选择性太低的列不值得建索引。比如“性别”“是否删除”这种只有两三个取值的字段,即使走了索引,优化器也可能发现扫描索引再回表还不如直接全表扫,于是主动放弃。索引的价值来自区分度,而不是来自“有没有建”。

二、读懂执行计划:从“猜”到“看”
经验告诉我,调优最忌讳凭感觉猜。真正该做的是让数据库自己招供——EXPLAIN 就是那份口供。
拿到 EXPLAIN 的结果,我通常只盯四个字段:
type 表示访问类型,从好到坏大致是 system > const > eq_ref > ref > range > index > ALL。看到 ALL 就意味着全表扫描,这是我最先要消灭的目标。看到 index 也别高兴太早——它表示扫了整个索引树,虽然比全表扫好一点,但仍然不是精确定位。
key 是优化器实际选择的索引。这一栏如果显示 NULL,说明压根没用索引,后面几栏就都不用看了;如果显示的是你没预期的另一个索引,那说明优化器的成本估算和你的直觉不一致,值得深挖。
rows 是预估扫描行数。这一栏是最有信息量的:如果它给出几十万,而你期望的是几十,那要么索引没被正确使用,要么统计信息过期了。我遇到过一个典型案例——线上一条查询 rows 预估值和实际值差了三个数量级,最后发现是统计信息长期没更新,ANALYZE TABLE 之后优化器立刻换了正确的执行路径。
Extra 是最容易藏雷的一栏。看到 Using filesort 说明排序没法走索引,数据量大时会成为瓶颈;Using temporary 意味着建了临时表,通常出现在 GROUP BY 或 DISTINCT 里;而 Using index 是好消息,表示覆盖索引命中,连回表都省了。
把 EXPLAIN 当成习惯之后,我发现很多争论会立刻消失——大家不再争论“应该能走索引吧”,而是直接看 key 那一栏。这种把主观猜测替换成客观证据的转变,本身就是工程能力的提升。
三、从一条慢查询到一套方法
讲完原理,说说我实际的调优顺序,它基本是固定的四步。
第一步,找到真正慢的那条。不要凭印象优化,先开慢查询日志(slow_query_log + long_query_time),或者看数据库自带的性能视图。优化一条每天跑一次的报表查询,和优化一条每秒跑一千次的接口查询,收益完全是两个量级。先定位高频高耗时的“头部查询”,投入产出比最高。
第二步,拿真实参数跑 EXPLAIN。很多慢查询问题是参数相关的:数据分布不均时,同一个 SQL 在 A 用户下很快、在 B 用户下极慢。所以一定要用出问题的那组真实参数,而不是随便填个 1 去试。
第三步,从代价最低的改动开始。优先考虑调整 SQL 写法(去掉函数、补上最左列、拆分大查询),其次是补索引或建覆盖索引,最后才考虑改表结构。改 SQL 是零风险的,加索引要评估写入开销,改表结构往往意味着停服和数据迁移——顺序错了,会把简单问题做复杂。
第四步,验证并留下记录。优化完必须验证 rows 与 type 是否改善、接口耗时是否下降。更重要的是把这次优化的原因写进提交记录或文档里——否则半年后有人“顺手清理”掉一个看起来冗余的索引,性能问题就会悄悄回归。
还要提醒一个常被低估的成本:索引本身是要付出代价的。每个索引都会占用磁盘,并且每次 INSERT/UPDATE/DELETE 都要维护所有相关索引。我在一个写入密集的日志表上见过 9 个索引,写入性能被拖得一塌糊涂,删掉 4 个长期没被用到的索引后,写入吞吐提升了近一倍。索引管理不只是“加”,更是定期做减法。

回头看,“加了索引还是慢”这个问题之所以常见,是因为它混淆了两件不同的事:建立索引是数据结构的准备,而用上索引是查询与结构的一次精确匹配。前者是一次性的动作,后者需要持续的理解与验证。真正把慢查询按下去的,从来不是记住几条“索引失效规则”,而是养成“看执行计划、找证据、小步验证”的习惯。数据库不会骗人,它只是从不主动开口——而 EXPLAIN 就是你把话问出来的方式。