索引建了却不走?7 个失效原因逐层排查

6 阅读6分钟

EXPLAIN 里 key 是 NULL,多半不是索引没建好,而是写法逼优化器放弃了它:函数运算、隐式转换、最左前缀、前置百分号、OR 不对等、回表成本过高——逐个排查,优先改写 SQL。

一条 SQL 的现场

WHERE 条件的列上明明有索引,EXPLAIN 出来 type=ALL、key=NULL、rows 七位数。第一反应通常是"索引失效了,重建一个"。

重建完还是不走。

索引失效从来不是单一原因,它是一串因果链:要么 SQL 让索引根本用不上,要么优化器算了一下觉得用索引更亏。 这两类问题的修法完全不同——前者改 SQL,后者改索引,或者干脆接受全表扫描。

possible_keys 才是分水岭

下面这张图是我实际用的排查路径,从上往下走,每一步都能排除掉一批可能。

flowchart TD
 A[EXPLAIN 看执行计划] --> B{key 是否为 NULL}
 B -->|不是| Z[已经在走索引]
 B -->|是| C[先看 possible_keys]
 C -->|有候选索引| D[检查 SQL 写法]
 C -->|没有候选索引| E[检查索引定义与列类型]
 D --> D1[索引列上有函数或运算]
 D --> D2[隐式类型转换]
 D --> D3[最左前缀被跳过]
 D --> D4[LIKE 以百分号开头]
 D --> D5[OR 一侧没有索引]
 E --> E1[重建单列或联合索引]
 D1 --> F[改写 SQL 后重新 EXPLAIN]
 D2 --> F
 D3 --> F
 D4 --> F
 D5 --> F
 E1 --> F
 F --> G[ANALYZE TABLE 更新统计信息]
 G --> H{返回行数是否超过全表 20%}
 H -->|是| I[考虑覆盖索引或接受全表扫描]
 H -->|否| J[索引或统计信息仍有问题]

possible_keys 为空,说明索引根本没资格参与;它有值但 key 为空,才轮到写法或成本估算背锅。先把这两拨问题分开,方向就不会跑偏。

索引压根没建对

最常见也最冤枉的一种:条件里用的是 B、C 两列,手上却是一个单列索引,或者字段顺序不匹配的联合索引。

修法只有一句——重新建一条单独的索引,或者按查询形态重建联合索引。 没有别的技巧。

函数一包,索引就废

-- 索引失效:create_time 被函数包住了
SELECT * FROM t_order WHERE DATE(create_time) = '2024-05-01';

-- 改成范围查询,索引可用
SELECT * FROM t_order
WHERE create_time >= '2024-05-01' AND create_time < '2024-05-02';

B+ 树里存的是 create_time 的原始值,不是 DATE(create_time) 的结果。函数一包,索引列就退化成表达式,MySQL 只能逐行算。 通用解法是把运算挪到常量那一侧。

隐式类型转换,Java 工程师的重灾区

-- phone 是 varchar,传数字会触发隐式转换
SELECT * FROM t_user WHERE phone = 13800000000;   -- 全表扫描
SELECT * FROM t_user WHERE phone = '13800000000'; -- 走索引

数据库会自动把字段侧转成数字来比较,等价于对索引列做了一次函数运算。两边数据类型保持一致,问题就消失了。

这类坑在 Java 里特别容易踩,因为 Long 参数和 varchar 列看起来"都是数字"。等号两边类型不一致,优化器不跟你讲情面。

联合索引被跳过了最左列

CREATE INDEX idx_a_b_c ON t_order (a, b, c);

SELECT * FROM t_order WHERE a = 1 AND b = 2;      -- 命中
SELECT * FROM t_order WHERE b = 2 AND c = 3;      -- 跳过 a,不命中

联合索引按 A、B、C 的顺序层层排序,跳过 A 直接查 B,等于在一本没按 B 排序的目录里找 B。建索引时按业务查询频率排字段顺序,高频等值列放最左边。

前置百分号,MySQL 的硬边界

SELECT * FROM t_user WHERE name LIKE '%小明';  -- 前置模糊,索引失效
SELECT * FROM t_user WHERE name LIKE '小明%';  -- 后置模糊,正常走索引

前缀不确定,B+ 树就定位不到起点,只能全扫。后置百分号可以用上索引,前置的不行。 真需要前置模糊,那是全文检索的活儿,该上 ES 就上 ES,别在 MySQL 上硬扛。

OR 两侧不对等,一起完蛋

-- age 有索引,city 没有
SELECT * FROM t_user WHERE age = 18 OR city = '杭州';

-- 拆成 UNION,让有索引的一侧走索引
SELECT * FROM t_user WHERE age = 18
UNION
SELECT * FROM t_user WHERE city = '杭州';

OR 的语义是"两边都要查",只要有一侧无索引,整体就只能全表扫描才有正确结果。要么两侧都建索引,要么拆成 UNION 改写。

优化器主动放弃:不报错,也不违规

这一条最容易被误判成"索引失效",其实索引好好的,是优化器不想用。

筛选之后返回的数据量占整张表超过 20% 时,优化器会判断:走二级索引要不停回表,随机 IO 的开销已经高于直接顺序扫全表。于是它主动放弃索引。

flowchart LR
 A[WHERE status = 1] --> B[二级索引定位主键]
 B --> C[估算需要回表的行数]
 C --> D{占全表比例}
 D -->|低于 20%| E[回表取行 走索引]
 D -->|高于 20%| F[直接全表扫描]

注意这里是估算,不是精确值。所以统计信息一偏,执行计划就可能翻车——这也是排查步骤里必须有一句 ANALYZE TABLE 的原因。

应对方式是构建覆盖索引,让查询需要的列全在索引里,把回表这个动作干掉:

CREATE INDEX idx_status_time_user ON t_order (status, create_time, user_id);

EXPLAIN SELECT status, create_time, user_id FROM t_order WHERE status = 1;

四步收口

  1. EXPLAIN 看执行计划,重点看 type、possible_keys、key、rows
  2. 对比 possible_keys 和实际的 key,判断是"没资格用"还是"能用但不用"
  3. ANALYZE TABLE t_order; 更新统计信息,排除统计偏差导致的误判
  4. 评估实际返回的数据量占全表比例,判断优化器的选择是否合理
EXPLAIN SELECT * FROM t_order WHERE status = 1;

+----+-------+---------+------+---------------+------+---------+------+---------+-------------+
| id | type  | table   | type | possible_keys | key  | key_len | ref  | rows    | Extra       |
+----+-------+---------+------+---------------+------+---------+------+---------+-------------+
|  1 | SIMPLE| t_order | ALL  | idx_status    | NULL | NULL    | NULL | 1200000 | Using where |
+----+-------+---------+------+---------------+------+---------+------+---------+-------------+

速查表

现象根因修法
索引列被函数包住索引存的是原值运算挪到常量侧
varchar 列传数字隐式类型转换参数加引号,类型对齐
联合索引只用后几列违反最左前缀调整索引字段顺序
LIKE '%x'无法定位前缀改后置,或上 ES
OR 一侧无索引整体放弃索引两侧建索引或拆 UNION
走全表比走索引便宜回表成本高于全扫建覆盖索引

只会重建索引的人,和会逐层排查的人

菜鸟遇到索引失效,只会两招:重建索引、FORCE INDEX 强制干预执行计划。

高手会从函数运算、隐式类型转换、最左前缀、模糊查询、优化器决策这几个层面逐层排查,优先改写 SQL,而不是粗暴地干预执行计划。

区别在于:FORCE INDEX 只是把问题按下去,数据量一涨、统计信息一变,坑还在那里;改写 SQL 是让优化器自己愿意选索引,这才稳。

写在最后

这套排查顺序里,我个人觉得最容易翻车的是第 7 条——因为它不报错、不违背任何语法规则,只是优化器算了一笔账。你们线上有没有遇到过那种"昨天还走索引,今天就 ALL 了"的查询?最后是靠覆盖索引解决的,还是干脆改了分页策略?评论区聊聊。

有用的话点个赞。