MySQL 索引失效原理剖析:别让 “加速键” 变成 “绊脚石”

在 MySQL 优化中,索引是公认的 "性能加速器"------ 一个合理的索引能让查询效率提升 10 倍甚至 100 倍。但实际开发中,很多同学会遇到 "明明建了索引,查询却依旧很慢" 的情况,这很可能是索引失效在搞鬼。

本文就从 "索引生效的底层逻辑" 入手,拆解 10 种常见的索引失效场景,结合代码案例讲清 "为什么失效""如何避免",帮你让索引真正发挥作用。

一、先搞懂:索引为什么能 "加速查询"?

要理解 "失效",得先明白 "生效" 的原理。MySQL 中最常用的索引是B + 树索引(InnoDB 默认),它的结构像一棵 "倒挂的树":

  • 底层是 "叶子节点",按索引键值有序排列,且存储了数据地址(聚簇索引直接存数据);

  • 上层是 "非叶子节点",类似 "目录",用于快速定位叶子节点。

当执行SELECT * FROM user WHERE id=10时(id 是主键索引),MySQL 会通过 B + 树的 "目录" 逐层定位,无需扫描全表,这就是索引加速的核心。

但如果查询条件破坏了 B + 树的 "有序性" 或 "可定位性",MySQL 就会放弃索引,转而执行 "全表扫描"(full table scan),这就是 "索引失效"。

MySQL 事务隔离级别:从理论到实战,这篇讲透了(附案例 + 避坑)

二、10 种常见索引失效场景:案例 + 原理拆解

场景 1:索引列参与 "计算 / 函数操作"

案例:

假设有表user,age字段建了普通索引,执行以下查询:

复制代码
-- 索引失效:age参与了计算SELECT * FROM user WHERE age + 1 = 25;-- 索引失效:age被函数处理SELECT * FROM user WHERE SUBSTR(name, 1, 1) = '张';

原理:

B + 树索引的 "目录" 是按原始索引值排序的。当索引列被计算(如age+1)或函数处理(如SUBSTR(age))后,查询条件中的 "键值"(如 25、' 张 ')与索引中存储的原始值不匹配,MySQL 无法通过索引定位,只能全表扫描。

解决办法:

避免在索引列上做操作,将条件 "反推" 到常量端。比如:

复制代码
-- 改为:条件移到常量端,不操作索引列SELECT * FROM user WHERE age = 25 - 1;-- 若需用函数,可考虑"函数索引"(MySQL 8.0+支持)CREATE INDEX idx_substr_name ON user (SUBSTR(name, 1, 1));

场景 2:索引列使用 "不等于"(!=/<>)或 "NOT IN"

案例:

age字段有索引,执行以下查询:

复制代码
-- 索引失效(大概率)SELECT * FROM user WHERE age != 25;SELECT * FROM user WHERE age NOT IN (20, 25);

原理:

B + 树适合 "定位符合条件的记录",但 "不等于""NOT IN" 需要 "排除不符合的记录",这类条件往往会命中 "大部分数据"(比如表中 90% 的 age 都不等于 25)。此时 MySQL 会判断:"全表扫描比走索引更高效"(因为走索引需要先定位再回表,成本可能更高),于是放弃索引。

注意:

如果 "不等于" 的结果集很小(比如表中只有 10 条数据,age!=25 的只有 2 条),MySQL 可能会走索引 ------ 最终是否走索引,由 MySQL 的 "优化器" 根据数据分布决定。

解决办法:

  • 若业务允许,用 "等于" 的反面替代(如用age < 25 OR age > 25,但需注意是否等价);

  • 确保表中 "不等于" 的记录占比低(可通过数据清洗、分表等方式优化)。

场景 3:索引列使用 "IS NOT NULL"

案例:

email字段有索引(允许为 NULL),执行:​​​​​​​

复制代码
-- 索引失效SELECT * FROM user WHERE email IS NOT NULL;

原理:

B + 树索引会存储 "NULL 值"(按 "最小值" 排序,通常放在最前面),但 "IS NOT NULL" 需要查询 "除 NULL 外的所有记录"。若表中 NULL 值少(比如大部分 email 都非 NULL),结果集很大,MySQL 会放弃索引;反之若 NULL 值多(比如 90% 的 email 是 NULL),可能会走索引。

对比:

IS NULL通常会走索引(因为 NULL 值集中存储,容易定位):​​​​​​​

复制代码
-- 索引生效(大概率)SELECT * FROM user WHERE email IS NULL;

场景 4:字符串索引不加引号,导致 "隐式类型转换"

案例:

phone字段是varchar类型(存手机号),建了索引,执行:​​​​​​​

复制代码
-- 索引失效:phone是字符串,查询用了数字SELECT * FROM user WHERE phone = 13800138000;

原理:

MySQL 会自动进行 "隐式类型转换"------ 因为phone是字符串,而条件是数字,会将phone转为数字再比较,相当于执行:​​​​​​​

复制代码
-- 等价于"索引列被函数操作",索引失效SELECT * FROM user WHERE CAST(phone AS UNSIGNED) = 13800138000;

解决办法:

字符串索引查询时,务必加引号(单引号 / 双引号):​​​​​​​

复制代码
-- 索引生效SELECT * FROM user WHERE phone = '13800138000';

场景 5:用 "OR" 连接非索引列和索引列

案例:

age有索引,name无索引,执行:​​​​​​​

复制代码
-- 索引失效:OR两边有非索引列SELECT * FROM user WHERE age = 25 OR name = '张三';

原理:

"OR" 的逻辑是 "满足任意一个条件即可"。如果age有索引但name无索引,MySQL 无法通过索引定位 "name=' 张三 '" 的记录,只能全表扫描所有记录,逐一判断 "age 是否 = 25" 或 "name 是否 = 张三"------ 此时索引失去意义,会被放弃。

解决办法:

  • 给 "OR" 连接的所有列都建索引(如给name也建索引);

  • 若某列不适合建索引,用 "UNION" 拆分查询(需确保结果等价):

    -- 拆分后,age的条件走索引,name的条件全表扫描,再合并结果SELECT * FROM user WHERE age = 25UNIONSELECT * FROM user WHERE name = '张三';

场景 6:联合索引 "不满足最左前缀原则"

案例:

建联合索引idx_age_name(age, name),执行以下查询:​​​​​​​

复制代码
-- 1. 索引生效(用了age,符合最左前缀)SELECT * FROM user WHERE age = 25;-- 2. 索引生效(用了age+name,符合最左前缀)SELECT * FROM user WHERE age = 25 AND name = '张三';-- 3. 索引失效(没用到age,不符合最左前缀)SELECT * FROM user WHERE name = '张三';

原理:

联合索引的 B + 树是按 "第一列(age)排序,第一列相同则按第二列(name)排序" 的。如果查询时不指定第一列(age),直接查第二列(name),B + 树的 "目录" 无法定位,只能全表扫描。

扩展:

"最左前缀" 不仅指 "开头的列",还包括 "连续的前缀"。比如联合索引(a, b, c):

  • a=1→生效;

  • a=1 AND b=2→生效;

  • a=1 AND c=3→生效(但只用到 a 的索引部分,c 没用到);

  • b=2 AND c=3→失效(没用到 a)。

场景 7:联合索引中,中间列用了 "范围条件"(>/>=/< 等)

案例:

联合索引idx_age_name(age, name),执行:​​​​​​​

复制代码
-- 索引部分失效:age生效,name失效SELECT * FROM user WHERE age > 25 AND name = '张三';

原理:

联合索引中,若第一列(age)用了范围条件(如age>25),则第一列的 "后续记录是无序的"(因为 B + 树中 age>25 的部分是连续的,但 name 在这部分中没有排序规则)。此时 MySQL 只能用索引定位 "age>25" 的记录,无法再通过索引筛选 "name=' 张三 '",name 的索引部分失效。

解决办法:

  • 若范围条件的结果集小(如age BETWEEN 25 AND 26,结果少),可接受(即使 name 失效,整体效率也不低);

  • 若范围结果集大,可考虑调整索引顺序(如把常用的列放前面),或拆分索引。

场景 8:用 "LIKE" 时,前缀是 "%"(模糊匹配开头)

案例:

name字段有索引,执行:​​​​​​​

复制代码
-- 1. 索引失效:%在开头SELECT * FROM user WHERE name LIKE '%张三';-- 2. 索引生效:%在结尾(前缀明确)SELECT * FROM user WHERE name LIKE '张三%';

原理:

"LIKE ' 张三 %'" 是 "前缀匹配",B + 树中 name 是按字符顺序排序的(如 "张三 1""张三 2" 连续存储),可通过索引快速定位 "以张三开头" 的记录;但 "LIKE '% 张三 '" 是 "后缀匹配",需要匹配 "任意字符 + 张三",这类记录在 B + 树中是分散的,无法通过索引定位,只能全表扫描。

解决办法:

  • 若需 "后缀匹配"(如查 "手机号以 138 结尾"),可反转字段内容并建索引。比如:

    -- 新增反转后的字段ALTER TABLE user ADD COLUMN phone_reverse VARCHAR(20);-- 存储反转后的手机号(如13800138000→000803100831)UPDATE user SET phone_reverse = REVERSE(phone);-- 建索引CREATE INDEX idx_phone_reverse ON user (phone_reverse);-- 查询时用反转条件(查"以138结尾"→反转后"以831开头")SELECT * FROM user WHERE phone_reverse LIKE CONCAT(REVERSE('138'), '%');

  • 用 "全文索引"(适合长文本,如文章内容):

    -- 建全文索引CREATE FULLTEXT INDEX idx_name_ft ON user (name);-- 用MATCH...AGAINST查询SELECT * FROM user WHERE MATCH(name) AGAINST('张三');

场景 9:查询条件用 "!=" 或 "<>" 判断 NULL 值

案例:

email字段允许为 NULL,建了索引,执行:​​​​​​​

复制代码
-- 索引失效:应该用IS NOT NULLSELECT * FROM user WHERE email != NULL;

原理:

在 MySQL 中,NULL是 "未知值",不能用 "!=""=" 判断,需用IS NULL/IS NOT NULL。用email != NULL时,MySQL 会将条件视为 "永远为假"(或无法正确解析),直接放弃索引,甚至返回错误结果。

正确写法:

复制代码
SELECT * FROM user WHERE email IS NOT NULL;

场景 10:表数据太少,MySQL 直接 "放弃索引"

案例:

user表只有 10 条数据,age有索引,执行:​​​​​​​

复制代码
-- 可能索引失效:表数据太少,全表扫描更快SELECT * FROM user WHERE age = 25;

原理:

索引的 "定位 + 回表" 有一定成本(比如从索引树找到地址,再去表中取数据)。若表数据少(比如少于 1000 行),全表扫描(直接逐行读数据)的成本可能更低,MySQL 优化器会选择 "不走索引"。

注意:

这是 "正常现象",无需优化 ------ 数据量小时,全表扫描的效率差异可忽略不计。

三、如何快速判断 "索引是否失效"?用 EXPLAIN!

遇到索引疑似失效时,用EXPLAIN命令分析查询计划,重点看type和key字段:

  • type:表示访问类型,ref/range/const/eq_ref说明走了索引;ALL说明全表扫描(索引失效)。

  • key:显示实际使用的索引,若为NULL,说明未使用索引。

示例:​​​​​​​

复制代码
-- 分析查询计划EXPLAIN SELECT * FROM user WHERE age = 25;

若type是ALL、key是NULL,则索引失效,需对照前文场景排查原因。

四、总结:避免索引失效的 3 个核心原则

  1. 不破坏索引的 "有序性":不在索引列上做计算、函数操作,避免隐式类型转换;

  2. 符合索引的 "定位逻辑":联合索引遵循最左前缀原则,避免中间列用范围条件;

  3. 让优化器 "愿意走索引":避免结果集过大的查询条件(如 "不等于""% 开头的 LIKE"),确保索引列的数据分布合理。

索引不是 "建了就万事大吉",只有避开失效场景,才能让它真正成为查询的 "加速键"。如果觉得有用,欢迎转发给团队小伙伴,一起避开 MySQL 优化的 "坑"!

相关推荐
奥尔特星云大使2 小时前
mysql重置管理员密码
linux·运维·数据库·mysql·centos
我好饿12 小时前
MySQL 主主复制 + keepalived + HAProxy
mysql·负载均衡
奥尔特星云大使3 小时前
MySQL多实例管理
linux·运维·数据库·mysql·dba·mysql多实例
多多*3 小时前
2025最新centos7安装mysql8 相关 服务器配置 纯命令行操作 保姆级教程
java·运维·服务器·mysql·spring·adb
ヾChen3 小时前
初识MySQL
数据库·物联网·学习·mysql
牛奶咖啡133 小时前
高可用MySQL的整体解决方案、体系化原理和指导思路
数据库·mysql·主从复制·高可用mysql解决方案·双主复制·mha高可用·全同步和半同步复制
lang201509283 小时前
MySQL专用服务器自动调优指南
服务器·mysql
计算机学姐3 小时前
基于微信小程序的智能在线预约挂号系统【2026最新】
java·vue.js·spring boot·mysql·微信小程序·小程序·tomcat
wow_DG3 小时前
【MySQL✨】MySQL 入门之旅 · 第十一篇:常见错误排查与解决方案
数据库·mysql·adb