给字段建了索引,查询却还是很慢,是很多人都会遇到的困惑。明明 WHERE user_id = 123 上就有索引,可 EXPLAIN 一看,type 是 ALL,也就是全表扫描,索引没被用上。这种情况我们通常说"索引失效"。严格来说并不是索引消失了,而是优化器认为这条 SQL 走索引不划算,或者写法的限制让它根本用不了索引。
这篇文章把索引失效的常见原因梳理一遍,从"索引列上做运算"到"统计信息过期",每一类都配上可复现的例子、EXPLAIN 的关键列怎么读,以及修复方法。看懂之后,遇到慢查询可以先自己判断到底是哪种问题,而不是一味加索引。文中的执行计划输出是基于常见 MySQL 5.7 与 8.0 版本的示意,实际列名和取值以自己环境为准。
先学会看 EXPLAIN
在排查之前,得先知道怎么看执行计划。最常用的就是 EXPLAIN,MySQL 8.0 还提供了 EXPLAIN ANALYZE 可以看到实际执行耗时。
# 基本用法,只需要在 SQL 前面加 EXPLAIN
mysql -uroot -p -e "EXPLAIN SELECT * FROM orders WHERE user_id = 123\G"
# MySQL 8.0 可以看真实执行信息(会真正执行语句)
mysql -uroot -p -e "EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 123\G"
# 查看表上的索引
mysql -uroot -p -e "SHOW INDEX FROM orders\G"
EXPLAIN 里最需要关注的几列含义如下。
| 列名 | 含义 | 需要警惕的取值 |
|---|---|---|
| type | 访问类型,反映效率高低 | ALL(全表扫描)、index(全索引扫描) |
| possible_keys | 理论上可用的索引 | 为空说明没有合适索引 |
| key | 优化器实际选择的索引 | 为 NULL 说明没走索引 |
| key_len | 使用的索引长度 | 偏短可能只用了联合索引的前几列 |
| rows | 预估扫描行数 | 数值越大越差 |
| filtered | 过滤后剩余行占比 | 过低说明条件选择性差 |
| Extra | 额外信息 | Using filesort、Using temporary |
type 的效率从好到差大致是:system > const > eq_ref > ref > range > index > ALL。看到 ALL 基本可以确定是全表扫描,看到 index 表示扫描了整个索引树。
索引列上做了函数或运算
这是最常见的失效原因。只要在 WHERE 的索引列上套了函数、做了运算,索引通常就用不了。
-- 失效:对索引列 created_at 做了 DATE() 函数
SELECT * FROM orders WHERE DATE(created_at) = '2026-03-01';
-- 改写:改成范围查询,索引可用
SELECT * FROM orders
WHERE created_at >= '2026-03-01 00:00:00'
AND created_at < '2026-03-02 00:00:00';
-- 失效:索引列参与运算
SELECT * FROM orders WHERE total_price + 10 > 200;
-- 改写:把运算移到右边
SELECT * FROM orders WHERE total_price > 190;
原理很简单:B+ 树索引里存的是列的原值,一旦对列做函数变换,每个值都会变形,索引的有序性就不成立了,优化器无法用它定位。修复办法是保持列"干净",把函数或运算挪到常量一侧。
隐式类型转换
字段类型和查询值的类型不一致时,MySQL 会尝试转换,一旦把索引列转成其他类型,索引同样失效。
-- 假设 phone 是 VARCHAR(20)
-- 失效:传入数字,MySQL 把 phone 转成数字再比较
SELECT * FROM users WHERE phone = 13800000000;
-- 正确:用字符串去匹配
SELECT * FROM users WHERE phone = '13800000000';
这类问题在字符串字段存数字时特别容易发生。整数转字符串通常不会破坏索引(因为是常量转换),而字符串列遇到数字常量时,往往变成"列转数字",索引就废了。排查时可以用 EXPLAIN 的 key 为 NULL 来发现,配合 SHOW WARNINGS 看优化器改写后的语句:
mysql -uroot -p -e "EXPLAIN SELECT * FROM users WHERE phone = 13800000000\G"
mysql -uroot -p -e "SHOW WARNINGS\G"
以百分号开头的 LIKE
LIKE 的前置通配符会让索引无法定位,只能全表扫描。
-- 失效:以 % 开头
SELECT * FROM products WHERE name LIKE '%手机%';
-- 可用:前缀匹配
SELECT * FROM products WHERE name LIKE '手机%';
如果业务确实需要模糊的中缀或后缀匹配,可以考虑全文索引(FULLTEXT)、外部搜索引擎,或者专门做一张倒排/分词表。不要指望普通 B+ 树索引能解决中缀匹配。
OR 连接的字段没有索引
OR 两侧的条件如果有一侧没有索引,整个查询就可能退化为全表扫描。
-- 假设 user_id 有索引,status 没有索引
-- 可能失效
SELECT * FROM orders WHERE user_id = 123 OR status = 1;
-- 改写一:给 status 也建索引后,用 union 明确分开
SELECT * FROM orders WHERE user_id = 123
UNION
SELECT * FROM orders WHERE status = 1;
-- 改写二:如果确实要合并,给两个列都建索引让优化器用 index merge
另一种情况是"能走索引却不划算"。如果 OR 两侧命中的行占总行数比例过高(经验上超过百分之二三十),优化器会认为走索引再回表还不如直接全表扫描,于是主动放弃索引。
不满足最左前缀原则
联合索引 (a, b, c) 的匹配必须从最左列开始连续匹配。
-- 索引:(user_id, status, created_at)
-- 可用:从最左列开始
SELECT * FROM orders WHERE user_id = 1;
SELECT * FROM orders WHERE user_id = 1 AND status = 2;
SELECT * FROM orders WHERE user_id = 1 AND status = 2 AND created_at > '2026-01-01';
-- 部分可用:跳过中间列,只能用上 user_id
SELECT * FROM orders WHERE user_id = 1 AND created_at > '2026-01-01';
-- 失效:没有从最左列开始
SELECT * FROM orders WHERE status = 2;
SELECT * FROM orders WHERE created_at > '2026-01-01';
还有一种容易忽略的情况:范围查询会中断后续列的索引使用。在 (a, b, c) 上,WHERE a = 1 AND b > 5 AND c = 3 里,c 用不上索引,因为 b 是范围。这时可以考虑调整索引顺序,或改写为等值条件。
范围条件和不等号
!=、<>、NOT IN 这类否定条件通常只能走范围扫描,效率不高,甚至直接全表扫描。
-- 效率低:否定条件,选择性差的概率大
SELECT * FROM orders WHERE status != 2;
-- 如果"非 2"的行数很少,可以枚举有效值
SELECT * FROM orders WHERE status IN (0, 1, 3, 4);
IS NULL 和 IS NOT NULL 也常被误解。实际上 MySQL 的索引是可以存 NULL 值的,IS NULL 可以使用索引;真正的问题在于当 NULL 行占比很高时,优化器觉得用索引不划算,于是选择全表扫描。要判断是不是这个原因,可以先用 EXPLAIN 看 rows 的预估行数,如果它接近表的总行数,说明条件几乎没有过滤能力,走不走索引都慢,此时更该考虑的是业务上能不能改用其他条件,或者降低 NULL 值的比例。
同一个道理适用于所有"选择性差"的条件。索引真正有价值的前提是条件能把行数大幅缩小,比如从一百万行筛到几百行。如果一个条件筛完之后还剩几十万行,就算走了索引,回表读几十万次的开销也可能超过全表扫描,优化器放弃索引其实是理性的。所以看到索引失效,先别急着加索引或 FORCE INDEX,而是想想这个条件的区分度是不是本来就不够。
统计信息过期
优化器是根据统计信息来估算"走索引要读多少行、全表扫描要读多少行"的,统计信息不准,就可能选错执行计划。
# 查看统计信息
mysql -uroot -p -e "SHOW INDEX FROM orders\G"
# 手动更新(MySQL 8.0)
mysql -uroot -p -e "ANALYZE TABLE orders;"
# 查看优化器开关,定位是否被某些参数影响
mysql -uroot -p -e "SELECT @@optimizer_switch\G"
当表发生大量增删改后,统计信息可能滞后,导致优化器误判。对写入频繁的大表,可以在业务低峰期定期执行 ANALYZE TABLE。需要注意的是,ANALYZE TABLE 会短暂加锁,在线大表要评估影响,尽量避开高峰。
索引列参与比较但类型不一致
除了前面说的隐式类型转换,还有一种情况是两个表的连接列类型不同,或者列与列直接比较。
-- 连接条件类型不一致,可能无法高效用索引
SELECT * FROM orders o
JOIN users u ON o.user_id = u.user_code; -- 一个 INT 一个 VARCHAR
-- 列与列比较,索引通常帮不上忙
SELECT * FROM t WHERE col_a > col_b;
修正方法是统一表结构设计,让关联列类型、字符集、排序规则都一致。
常见失效原因速查表
| 原因 | 典型写法 | 修复方向 |
|---|---|---|
| 列上做函数/运算 | WHERE DATE(t)=... |
改写为范围查询 |
| 隐式类型转换 | 字符串列传数字 | 传对应类型常量 |
| 前置百分号 LIKE | LIKE '%x%' |
改前缀匹配或用全文索引 |
| OR 中有一列无索引 | a=1 OR b=2 |
建索引或改 UNION |
| 不满足最左前缀 | 跳列、缺最左列 | 调整查询或索引顺序 |
| 否定条件 | !=、<>、NOT IN |
改枚举或不依赖索引 |
| 统计信息过期 | 无固定写法 | 定期 ANALYZE TABLE |
| 关联列类型不一致 | 不同类型 JOIN | 统一表结构 |
常见问题
建了索引就一定会走索引吗?
不一定。优化器会根据成本估算选择执行计划,如果走索引要回表读的行太多,它可能直接全表扫描。选择性低的列即使建了索引也可能不被使用。
为什么同一条 SQL 昨天走索引今天不走了?
常见原因是数据量变化导致统计信息改变,或者优化器对成本估算发生了变化。可以对比两次的 EXPLAIN,并对表执行 ANALYZE TABLE 后观察是否恢复。
force index 能解决问题吗?
FORCE INDEX 能强制指定索引,但它是一种有副作用的兜底手段。数据分布变化后强制索引可能反而更慢,优先还是找出失效的根本原因并修正写法或索引。
联合索引的列顺序怎么定?
一般把等值条件中区分度高的列放在前面,范围条件列放在最后。同时要考虑哪些查询最频繁,让最常用的查询能完整命中索引前缀。
加了索引反而更慢是怎么回事?
写入时需要维护索引,索引越多,插入、更新和删除的开销越大。另外索引占用缓冲池空间,可能挤掉数据页缓存。所以索引要按查询需求精简,不是越多越好。
总结
- 索引失效的本质是"用不了"或"用得不划算",前者是写法问题,后者是优化器成本选择。
- 在索引列上套函数、做运算、隐式类型转换、前置百分号 LIKE 是最常见的几类失效原因。
- 联合索引要遵守最左前缀,范围条件会中断后续列的使用,索引列顺序要按查询模式设计。
EXPLAIN的type、key、rows、Extra是关键信号,type=ALL、key=NULL基本可判定没走索引。- 统计信息过期会让优化器选错执行计划,对写入频繁的大表要定期
ANALYZE TABLE。 - 修复顺序是:先改写法,再调索引,
FORCE INDEX只作为最后手段,并持续用监控验证效果。
企业QQ咨询




