为什么索引会失效

MySQL 索引是提升查询性能的关键,但很多时候我们建了索引却发现并没有被使用,查询依旧很慢。这就是典型的”索引失效”。下面梳理几种最常见的失效原因,并给出对应的解决方法。

一、违反最左前缀原则

对于联合索引 (a, b, c),查询必须从最左边的列开始使用,否则部分索引会失效。

-- 假设有联合索引 idx_abc (a, b, c)

-- ✅ 能用到索引
SELECT * FROM t WHERE a = 1;                     -- 使用 a
SELECT * FROM t WHERE a = 1 AND b = 2;           -- 使用 a, b
SELECT * FROM t WHERE a = 1 AND b = 2 AND c = 3; -- 使用 a, b, c

-- ❌ 无法使用索引(跳过了 a)
SELECT * FROM t WHERE b = 2 AND c = 3;

-- ❌ 部分失效(跳过了 b,c 用不上)
SELECT * FROM t WHERE a = 1 AND c = 3;

解决方法:根据查询模式合理设计联合索引的字段顺序,把最常用作过滤条件的列放在最左边。

二、在索引列上使用函数或运算

对索引列做运算或调用函数,会让优化器无法直接使用索引。

-- 假设 create_time 上有索引

-- ❌ 使用了函数,索引失效
SELECT * FROM t WHERE YEAR(create_time) = 2024;

-- ✅ 改为范围查询
SELECT * FROM t
WHERE create_time >= '2024-01-01'
  AND create_time <  '2025-01-01';

-- ❌ 列上做运算
SELECT * FROM t WHERE id + 1 = 100;

-- ✅ 把运算移到右侧
SELECT * FROM t WHERE id = 99;

解决方法:避免在索引列上使用函数或表达式,改为等价的常量条件。

三、使用 OR 导致全表扫描

当 OR 两端有一个条件列没有索引时,整个查询会放弃索引走全表扫描。

-- 假设 a 有索引,b 没有索引

-- ❌ b 没有索引,整个查询全表扫描
SELECT * FROM t WHERE a = 1 OR b = 2;

-- ✅ 方案1:给 b 也建索引
CREATE INDEX idx_b ON t(b);

-- ✅ 方案2:改写为 UNION ALL
SELECT * FROM t WHERE a = 1
UNION ALL
SELECT * FROM t WHERE b = 2;

解决方法:确保 OR 关联的列都有索引,或改写为 UNION

四、LIKE 以 % 开头

LIKE '%abc' 这种以通配符开头的模糊匹配无法利用 B+Tree 索引。

-- ❌ 以 % 开头,索引失效
SELECT * FROM t WHERE name LIKE '%张';

-- ❌ 两边都加 %,索引失效
SELECT * FROM t WHERE name LIKE '%张%';

-- ✅ 前缀匹配可以使用索引
SELECT * FROM t WHERE name LIKE '张%';

解决方法: - 尽量使用前缀匹配 LIKE 'abc%' - 真的需要中间匹配时,考虑全文索引 FULLTEXT 或外置搜索引擎(如 Elasticsearch)

五、隐式类型转换

当查询条件的类型与列类型不一致时,MySQL 会做隐式转换,通常会导致索引失效。

-- 假设 phone 字段是 VARCHAR 类型

-- ❌ 传入数字,发生隐式转换,索引失效
SELECT * FROM t WHERE phone = 13800138000;

-- ✅ 传入字符串,类型一致
SELECT * FROM t WHERE phone = '13800138000';

解决方法:查询条件的类型要和列定义保持一致,字符串字段就用引号包裹。

六、NOT IN / NOT EXISTS

NOT INNOT EXISTS 在很多场景下优化器会选择全表扫描。

-- ❌ NOT IN 容易走全表扫描
SELECT * FROM t WHERE id NOT IN (1, 2, 3);

-- ✅ 改写为 LEFT JOIN ... IS NULL
SELECT t.* FROM t
LEFT JOIN exclude_t e ON t.id = e.id
WHERE e.id IS NULL;

解决方法:尽量用 LEFT JOIN ... IS NULL 等方式替代 NOT IN

七、索引选择性差

如果一列的取值很少(比如性别只有”男/女”),即使建了索引,优化器也可能认为走全表扫描更划算,从而放弃索引。

-- ❌ 性别列基数太低,索引可能不被使用
SELECT * FROM t WHERE gender = '男';

-- ✅ 选择性高的列更适合建索引
CREATE INDEX idx_create_time ON t(create_time);

解决方法: - 选择基数高(区分度大)的列建索引 - 低基数列可以考虑与其他列组成联合索引

如何用 EXPLAIN 分析索引使用

EXPLAIN 是排查索引失效最有力的工具。在 SQL 前加上 EXPLAIN 即可查看执行计划。

EXPLAIN SELECT * FROM t WHERE name = '张三';

重点关注以下字段:

字段 含义
type 访问类型,从好到差依次是 system > const > eq_ref > ref > range > index > ALL。出现 ALL 表示全表扫描
key 实际使用的索引名,NULL 表示没用索引
key_len 使用的索引长度,可判断联合索引用了几列
rows 估算扫描行数,越小越好
Extra 额外信息。Using index 表示覆盖索引,Using filesort / Using temporary 需要优化
-- 示例:分析一条查询
EXPLAIN SELECT id, name FROM users WHERE age > 18 AND city = '北京';
-- 如果 type 为 ALL、key 为 NULL,说明索引没生效,需要调整索引或查询写法

总结

索引失效的常见原因可以归纳为”优化器认为走索引不划算”或”写法让索引无法被利用”。排查时记住三步: 1. 用 EXPLAIN 看执行计划 2. 检查查询是否满足索引使用规则 3. 调整 SQL 写法或重建合适的索引

养成用 EXPLAIN 的习惯,索引问题就能事半功倍。