为什么你建了索引,查询还是很慢?常见失效原因汇总

为什么你建了索引,查询还是很慢?常见失效原因汇总


一、先搞清楚一个基本认知

很多开发者以为:建了索引 = 查询变快

但现实往往是:

索引建了,EXPLAIN 也走了索引,结果查询还是慢得像蜗牛。

这不是数据库的锅,是你的索引 没被正确使用 ,或者 根本就没生效

今天我们系统梳理一下,索引失效的 12 个常见原因,以及背后的原理。


二、索引失效的核心原理:为什么优化器不走索引?

MySQL 优化器在选择执行计划时,会做一个 成本估算

  • 走索引的成本 vs 全表扫描的成本
  • 如果估算走索引比全表扫描还贵,它就直接放弃索引

所以索引失效的本质就两种:

类别 说明
语法层面失效 SQL 写法导致索引无法被匹配
统计信息层面失效 优化器认为全表扫描更优,主动不走索引

下面逐一拆解。


三、语法层面:SQL 写法导致索引失效

1. ❌ 对索引列做函数/运算

sql 复制代码
sql
-- 失效 ❌
SELECT * FROM user WHERE YEAR(create_time) = 2024;
SELECT * FROM user WHERE age + 1 = 25;

-- 走索引 ✅
SELECT * FROM user WHERE create_time >= '2024-01-01' 
                       AND create_time < '2025-01-01';
SELECT * FROM user WHERE age = 24;

原理:B+Tree 存储的是原始列值,对列做函数后,索引中找不到对应的值,只能全表扫描逐行计算。

📌 规则:索引列上不能有任何操作,包括函数、运算、类型转换。


2. ❌ 隐式类型转换

sql 复制代码
sql
-- phone 字段是 varchar 类型
-- 失效 ❌
SELECT * FROM user WHERE phone = 13800138000;

-- 走索引 ✅
SELECT * FROM user WHERE phone = '13800138000';

原理 :当字符串列与数字比较时,MySQL 会把字符串列隐式转换成数字 ,相当于对列做了 CAST() 函数,索引失效。

📌 排查技巧:用 EXPLAINkey_len,如果明显变短,很可能是类型转换导致只用了部分索引甚至没走索引。


3. ❌ LIKE 以通配符开头

sql 复制代码
sql
-- 失效 ❌(%在前,无法利用B+Tree有序性)
SELECT * FROM user WHERE name LIKE '%张';

-- 走索引 ✅(%在后,可以走范围扫描)
SELECT * FROM user WHERE name LIKE '张%';

-- 走索引 ✅(全模糊也能走,但只走到第一个%)
SELECT * FROM user WHERE name LIKE '张%三';

原理 :B+Tree 是按从左到右 顺序组织的。%张 意味着要匹配所有以"张"结尾的值,无法利用树的有序性,只能全表扫描。

📌 如果必须支持 %xxx% 全模糊,考虑走 全文索引(FULLTEXT)Elasticsearch


4. ❌ 违反最左前缀原则(联合索引)

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

ini 复制代码
sql
-- ✅ 走索引(最左匹配)
WHERE a = 1
WHERE a = 1 AND b = 2
WHERE a = 1 AND b = 2 AND c = 3

-- ❌ 不走索引(跳过最左列)
WHERE b = 2
WHERE c = 3
WHERE b = 2 AND c = 3

-- ⚠️ 部分走索引(最左连续中断)
WHERE a = 1 AND c = 3   -- 只有 a 走索引,c 不走

原理 :联合索引的 B+Tree 先按 a 排序,a 相同再按 b 排,再按 c 排。跳过 a 直接查 b,就像跳过目录直接翻书------树的结构用不上。

📌 建联合索引时,把最常作为查询条件、区分度最高的列放在最左边。


5. ❌ OR 条件中有一列没有索引

sql 复制代码
sql
-- 假设只有 name 有索引,age 没有索引
-- 失效 ❌(age 列无法走索引,优化器可能直接放弃)
SELECT * FROM user WHERE name = '张三' OR age = 25;

-- 走索引 ✅(两列都有索引)
SELECT * FROM user WHERE name = '张三' OR name = '李四';

原理 :OR 两边只要有一列没有索引,优化器往往会选择全表扫描而不是"索引合并"(index merge 效率通常不高)。

📌 解决方案:

  • 确保 OR 两边的列都有索引

  • 或改用 UNION ALL

    sql 复制代码
    sql
    SELECT * FROM user WHERE name = '张三'
    UNION ALL
    SELECT * FROM user WHERE age = 25 AND name != '张三';

6. ❌ 使用 != 或 <> 或 NOT IN

sql 复制代码
sql
-- 大概率失效 ❌
SELECT * FROM user WHERE status != 1;
SELECT * FROM user WHERE id NOT IN (1, 2, 3);

-- 走索引 ✅
SELECT * FROM user WHERE status = 0;
SELECT * FROM user WHERE id NOT IN (SELECT id FROM blacklist); 
-- 子查询结果少时,优化器可能走索引

原理!=NOT IN 需要扫描大部分数据,优化器判断"反正要扫那么多,不如直接全表扫描"。

📌 但这不是绝对的------如果 != 的条件过滤后数据量很小,优化器仍可能走索引。关键看选择性


7. ❌ IS NULL / IS NOT NULL

sql 复制代码
sql
-- 失效 ❌(取决于索引定义和数据分布)
SELECT * FROM user WHERE name IS NULL;

-- 如果 name 列允许 NULL,且建了普通索引,可能不走索引
-- 因为 NULL 值在索引中的存储方式特殊,优化器可能认为扫描代价大

原理 :InnoDB 的普通索引不存储 NULL 值(只存非 NULL),所以 IS NULL 条件无法直接通过索引定位。

📌 解决方案:给列设置默认值(如空字符串),然后查 name = '',或者用复合索引把 NULL 列放在后面。


四、统计信息层面:优化器主动不走索引

8. ❌ 数据区分度太低(选择性差)

sql 复制代码
sql
-- gender 只有 0/1 两个值,即使建了索引
SELECT * FROM user WHERE gender = 1;

原理 :如果 gender = 1 的数据占表的 50%,走索引需要回表 50% 的行,代价比全表扫描还大。优化器会直接选择全表扫描。

📌 区分度公式count(distinct col) / count(*),结果越接近 1 索引越有效,越接近 0 越可能失效。一般认为 < 0.1 就不适合单独建索引。


9. ❌ 表数据量太小

sql 复制代码
sql
-- 表只有 100 行,建了索引也不走
SELECT * FROM user WHERE id = 50;

原理:表太小时,全表扫描的 I/O 代价极低,优化器认为"没必要走索引"。索引的优势在数据量大时才体现。

📌 这种情况不算"失效",是优化器的正确选择。


10. ❌ 索引失效 + 优化器选错(统计信息过期)

sql 复制代码
sql
-- 以前走索引,现在不走了,数据没变但执行计划变了

原理 :MySQL 依赖统计信息 来估算成本。如果统计信息过时(比如大量数据删除后没跑 ANALYZE TABLE),优化器可能做出错误判断。

📌 解决方案:

sql 复制代码
sql
ANALYZE TABLE user;
-- 或
OPTIMIZE TABLE user;  -- 重建表+更新统计信息

五、索引设计层面:建了但不好用

11. ❌ 索引列太多 / 索引太宽

sql 复制代码
sql
-- 联合索引包含大量列
CREATE INDEX idx_wide ON user(a, b, c, d, e, f, g);

问题

  • 索引文件变大,占用更多磁盘和内存
  • B+Tree 层数增加,IO 次数变多
  • 维护成本高(INSERT/UPDATE/DELETE 都要维护索引)

📌 建议联合索引不超过 3-5 列,优先把高区分度、常用查询条件放前面。


12. ❌ 用了覆盖索引的反面:SELECT *

sql 复制代码
sql
-- 即使走了索引,也要回表
SELECT * FROM user WHERE name = '张三';  -- 索引只有 name,需要回表取所有列

-- 走覆盖索引,不回表 ✅
SELECT id, name FROM user WHERE name = '张三';

原理 :如果查询的列不在索引中,MySQL 需要回表(根据主键再查一次聚簇索引)。如果回表次数太多,优化器可能觉得不如全表扫描。

📌 覆盖索引优化:把常用查询的列都加到联合索引中,避免回表。

sql 复制代码
sql
CREATE INDEX idx_name_age ON user(name, age);
SELECT name, age FROM user WHERE name = '张三';  -- 纯索引扫描,不回表

六、一张表速查:索引失效全景图

序号 失效原因 关键词 解决方案
1 列上做函数/运算 YEAR(), +1, UPPER() 改写 SQL,把计算放到等号右边
2 隐式类型转换 varchar = int 统一类型,用引号包字符串
3 LIKE 左通配 %张 改成 张%,或用全文索引
4 违反最左前缀 跳过联合索引第一列 调整索引列顺序,或建多个索引
5 OR 一边无索引 a=1 OR b=2,b 无索引 两边都建索引,或改用 UNION
6 != / NOT IN 负向查询 改用正向查询,或 UNION
7 IS NULL 空值判断 设默认值,或复合索引
8 区分度太低 性别、状态等枚举值 不单独建索引,用复合索引
9 表太小 < 几百行 不需要索引,优化器会处理
10 统计信息过期 执行计划突然变了 ANALYZE TABLE
11 索引太宽 联合索引 > 5 列 精简索引列
12 SELECT * 导致回表 查询列不在索引中 用覆盖索引,只查需要的列

七、实战排查流程

遇到慢查询,按这个顺序排查:

sql 复制代码
1. EXPLAIN 看执行计划
   ├── type 是 ALL?→ 全表扫描,检查索引
   ├── key 是 NULL?→ 没走索引,往下排查
   ├── key_len 异常短?→ 可能类型转换或最左前缀断了
   └── Extra 有 Using index?→ 覆盖索引,很好

2. 检查 SQL 写法
   ├── 列上有函数吗?
   ├── 有隐式转换吗?
   ├── LIKE 是 %开头吗?
   └── OR 两边都有索引吗?

3. 检查索引设计
   ├── 区分度够吗?
   ├── 联合索引顺序对吗?
   └── 查询列在索引里吗?(覆盖索引)

4. 检查统计信息
   └── 跑一下 ANALYZE TABLE

八、最后说句大实话

索引不是银弹,建索引是技术,用好索引是艺术。

很多时候慢查询的根因不是"没建索引",而是:

  • 建了但设计不合理(列顺序错、太宽)
  • 建了但 SQL 写得太烂(函数、类型转换)
  • 建了但数据量太小/区分度太低,优化器根本不想用

先 EXPLAIN,再改 SQL,最后调索引------这个顺序反了,就是浪费时间。

相关推荐
长栎39 分钟前
写 for 循环写了十年,你却从没用过迭代器模式最狠的那一面
后端
LiaCode43 分钟前
Redis 在生产项目的使用
前端·后端
用户559822481221 小时前
Docker Compose Down 导致容器数据误删——ext4 日志恢复全记录
后端
LiaCode1 小时前
一天学完 redis 的爽翻版核心知识总结
前端·后端
大刚测试开发实战1 小时前
如何内网穿透访问本地私有化部署的TestHub
前端·后端·github
xiaodaoluanzha1 小时前
迄今為止,最簡單的編程語言 Nolang
前端·后端
Csvn1 小时前
Docker 容器管理入门 — 从镜像到容器编排
后端
用户762352425911 小时前
ShardingJDBC
后端
行者全栈架构师1 小时前
IDEA 中 Maven 项目的 15 个红色报错快速解决方法
java·后端
Colin草率地做慢慢地改1 小时前
关于QuickStore这个项目的重构(2)- 数据库建表文件
后端·面试·架构