MySQL 索引失效的六个真实场景

“我明明加了索引,为什么还是全表扫描?”

这个问题几乎每个月都会遇到一次。索引失效不是数据库的 bug,绝大多数情况下是查询写法让优化器判断”用索引反而更慢”。

下面六个场景都来自真实项目,附 EXPLAIN 对照。

一、对索引列做了函数运算

-- 失效
SELECT * FROM orders WHERE DATE(created_at) = '2026-10-01';

-- 有效
SELECT * FROM orders
WHERE created_at >= '2026-10-01 00:00:00'
  AND created_at <  '2026-10-02 00:00:00';

DATE(created_at) 让索引列变成了一个表达式,B+ 树是有序的,但函数值的顺序和原值顺序不一致,只能逐行计算。

同理还有 SUBSTRING()、CONCAT()、YEAR()。

二、隐式类型转换

字段是 varchar,查询传了数字:

-- phone 是 varchar,这里会失效
SELECT * FROM users WHERE phone = 13800138000;

MySQL 会把字符串列转换成数字再比较(不是反过来),等价于对每一行执行 CAST(phone AS DOUBLE),索引直接废掉。

这类问题在 Java 项目里特别常见——Long 类型的参数直接拼进了 SQL。

三、模糊查询以通配符开头

SELECT * FROM articles WHERE title LIKE '%架构%';   -- 全表扫描
SELECT * FROM articles WHERE title LIKE '架构%';    -- 走索引

前缀不确定,B+ 树无法定位起点。真的要做全文模糊搜索,用全文索引或者 ES,别硬扛。

四、联合索引不符合最左前缀

建了 (a, b, c) 的联合索引:

WHERE a = 1 AND b = 2 AND c = 3   -- 全用上
WHERE a = 1 AND c = 3             -- 只用到 a
WHERE b = 2 AND c = 3             -- 完全用不上

顺序不是必须和定义一致(优化器会调整),但跳过的列之后的索引部分就断了。

有个例外:a = 1 AND c = 3 在 MySQL 8 里能用上 ICP(索引条件下推),但 c 不是用来定位范围,只是回表前过滤。

五、OR 连接了没有索引的列

SELECT * FROM users WHERE name = 'leo' OR age = 30;

只要 age 没索引,整个查询就退化成全表扫描。改成 UNION:

SELECT * FROM users WHERE name = 'leo'
UNION
SELECT * FROM users WHERE age = 30;

六、区分度太低

性别、状态、是否删除这类字段,全表只有几个值。当某个值占比超过 20%~30%,优化器会认为”走索引再回表”比”顺序扫全表”更贵,直接放弃索引。

-- status 只有 0/1,且 95% 是 1
SELECT * FROM tasks WHERE status = 1;

这种情况不该加索引,该考虑的是改变查询模型——比如把”待处理”单独拆表,或者用覆盖索引把回表开销消掉。

怎么确认

不要猜,看执行计划:

EXPLAIN SELECT ...;

-- 关注三列
-- type:  ALL 表示全表扫描,range/ref 才是走索引
-- key:   实际使用的索引,NULL 就是没用
-- rows:  预估扫描行数

MySQL 8 还有 EXPLAIN ANALYZE,会真正执行并给出实际耗时,比 EXPLAIN 的预估值准得多。

小结

索引失效的本质只有一句话:优化器认为不用索引更快。

所以排查时不要问”为什么没用索引”,要问”它为什么判断不用更快”——是函数让索引列失去有序性,是类型转换引入了逐行计算,还是回表成本超过了顺序扫描。