【计算基础|MySQL09】索引(下):失效与设计

【计算基础|MySQL09】索引(下):失效与设计

上一篇把 B+树索引的结构、聚簇与非聚簇、回表讲透了。那篇留了两个坑:为什么建了索引查询还是全表扫?联合索引 (a,b,c) 到底哪几列能用上?

这一篇就干两件事:把索引失效的所有常见姿势踩一遍并用 EXPLAIN 验证,再讲联合索引该怎么设计。所有结论都在 MySQL 8.0 上实测,EXPLAIN 输出原样贴出,不背口诀。

环境:Docker 起 MySQL 8.0.46(InnoDB,默认隔离级别 REPEATABLE-READ),表 t_abc 10 万行,建了联合索引 idx_abc(a,b,c) 以及单列索引 idx_ct(create_time)、idx_name(name)、idx_phone(phone)。


一、最左前缀原则:联合索引的 B+树是怎么排序的

结论先给:联合索引 (a,b,c) 在 B+树里是先按 a 排序,a 相同再按 b 排,b 相同再按 c 排。所以查询条件必须"从最左列开始、连续不断"地用,中间断了,后面的列就用不上索引。

你看这棵树:整体先按 a 排(1,1,1,1,2,2,3),a 相同的内部才按 b 排,b 相同才按 c 排。这意味着------没有 a,你根本不知道 b 在哪一片区间。

我在 10 万行的 t_abc 上实测五种查询,重点看 key_len(索引用了多少字节,能反推用了几列):

查询 用到的列 type key_len Extra
WHERE a=1 AND b=2 AND c=3 a,b,c 全用 ref 15 NULL
WHERE a=1 AND b=2 a,b ref 10 NULL
WHERE a=1 a ref 5 NULL
WHERE b=2 AND c=3(缺 a) 不用 ALL NULL Using where
WHERE a=1 AND c=3(跳 b) 只用 a ref 5 Using index condition

key_len 为什么是 5、10、15?因为 a/b/c 都是可空 INT:4 字节数据 + 1 字节 NULL 标记 = 5 字节/列。三列全用就是 15。这条是面试手写题 ------给你 key_len 反推索引用了几列。

🔴 重点 :最后一行 WHERE a=1 AND c=3,b 断了,所以 c 用不上索引(key_len 只有 5 = 1 列),但优化器没全表扫------它在 a 的索引上把 a=1 的行都捞出来,再在引擎层用 c 过滤,这就是 Using index condition(索引下推,第三节讲)。

⚠️ 避坑 :WHERE b=2 AND c=3 在网上很多文章里说"优化器会自动补 a",实测 8.0.46 直接 type=ALL, key=NULL 全表扫描,别迷信。


二、索引失效大全:每个都配反例和 EXPLAIN

下面每一条我都建好了对应索引,先让它失效给你看,再给改法。

2.1 对索引列用函数或运算

索引存的是列本身的有序值。你在列外面包一层函数,B+树就没法按原值定位了------等于要求"对索引里每个键都先套一遍函数再比较",只能全表扫。

sql 复制代码
-- 失效:对 create_time 套 YEAR()
EXPLAIN SELECT * FROM t_abc WHERE YEAR(create_time)=2024;
-- type=ALL  key=NULL  rows=99875
sql 复制代码
-- 改法:把函数挪到右边,改成范围
EXPLAIN SELECT * FROM t_abc
WHERE create_time>='2024-01-01' AND create_time<'2024-01-02';
-- type=range  key=idx_ct  rows=1

同样的还有:

sql 复制代码
WHERE id + 1 = 2        -- 失效,type=ALL(MySQL 不会帮你化简成 id=1)
WHERE id = 1            -- 改后 type=const,走主键

WHERE LEFT(name,3)='use'  -- 失效,type=ALL

✅ 改法口诀:索引列"裸奔",运算/函数都挪到等号右边。

2.2 隐式类型转换:字符串列用数字查

这是生产环境最隐蔽的坑。phone 是 VARCHAR 且建了索引,你却传了个数字进去:

sql 复制代码
-- 失效:phone 是 VARCHAR,却用数字 13800000000 查
EXPLAIN SELECT * FROM t_abc WHERE phone=13800000000;
-- type=ALL  key=NULL  rows=99875(possible_keys 里其实有 idx_phone,但 key=NULL)

-- 正确:传字符串
EXPLAIN SELECT * FROM t_abc WHERE phone='13800000000';
-- type=ref  key=idx_phone  key_len=83  rows=1

🔴 为什么 :MySQL 的类型转换规则是"把字符串转成数字"而不是"把数字转成字符串",等价于 WHERE CAST(phone AS UNSIGNED)=138...------又在索引列上套函数了,索引直接报废。key_len=83 也对得上:VARCHAR(20) utf8mb4 最多 20×4=80 字节 + 2 字节长度 + 1 字节 NULL 标记 = 83。

⚠️ 关联查询同理 :两张表关联字段字符集不一致(一个 utf8mb4 一个 utf8),会发生隐式字符集转换,被转的那一侧索引失效。建表时全库统一 utf8mb4 能省掉一堆事故。

2.3 LIKE 左模糊

sql 复制代码
-- 左模糊:索引彻底用不了
EXPLAIN SELECT * FROM t_abc WHERE name LIKE '%user%';
-- type=ALL  possible_keys=NULL  key=NULL

-- 右模糊:前缀固定,索引能用(但还要看选择性)
EXPLAIN SELECT * FROM t_abc WHERE name='user_55555';
-- type=ref  key=idx_phone  rows=1

⚠️ 一个容易被忽略的真相 :右模糊 LIKE 'xxx%' 只是"有资格"走索引,走不走还得优化器算成本。我实测 name LIKE 'user_55%' 时 possible_keys 里列出了 idx_name,但 key=NULL------因为这批数据里 user_55% 命中了大约一半的行,优化器觉得"回表比全表扫还贵",干脆放弃索引。左模糊是"连候选都进不了"(possible_keys=NULL),右模糊是"候选里有但优化器嫌它贵",两者性质不同。

2.4 OR 连接了非索引列

sql 复制代码
-- a 有索引、status 没索引,OR 一下整个废掉
EXPLAIN SELECT * FROM t_abc WHERE a=1 OR status=2;
-- type=ALL  key=NULL  rows=99875

OR 两边只要有一个列没索引,优化器就不敢用索引(因为另一个分支必须全表扫,索性一起扫)。改法是拆成 UNION:

sql 复制代码
EXPLAIN SELECT * FROM t_abc WHERE a=1
UNION
SELECT * FROM t_abc WHERE status=2;
-- 第一行 type=ref 用 idx_abc,第二行 status 仍全表扫(它没索引)

2.5 NOT IN / != / <>

sql 复制代码
EXPLAIN SELECT * FROM t_abc WHERE a != 1;
-- type=ALL  key=NULL  filtered=50.00

EXPLAIN SELECT * FROM t_abc WHERE a NOT IN (1,2,3);
-- type=ALL  key=NULL  filtered=50.00

不等于这类条件,索引通常帮不上忙------因为"不等于 1"意味着要扫除了 1 以外的几乎所有行,优化器一算还不如全表扫。IS NOT NULL 同理,走不走取决于选择性。这类写法本身就说明你在查一大半数据,业务上往往该换个设计。

2.6 失效场景速查表

失效姿势 反例 改法
违反最左前缀 WHERE b=2(缺 a) 调整索引列顺序或补条件
索引列套函数 WHERE YEAR(t)=2024 改范围 t>=.. AND t<..
索引列运算 WHERE id+1=2 改 id=1
隐式类型转换 WHERE varchar_col=123 传字符串 '123'
隐式字符集转换 关联表字符集不一致 全库统一 utf8mb4
LIKE 左模糊 WHERE name LIKE '%ab' 右模糊 'ab%',或全文索引
OR 非索引列 a=1 OR b=2(b 无索引) 改 UNION 或给 b 加索引
NOT IN / != WHERE a != 1 评估业务,必要时换思路

三、索引下推 ICP:把过滤尽量往存储引擎推

Using index condition 不是摆设,它背后是 Index Condition Pushdown(索引下推,MySQL 5.6 引入)。

没有 ICP 时:存储引擎按联合索引最左列把记录捞出来、回表拿到整行,再交给 Server 层逐行过滤剩余条件。有 ICP 时:能在索引里判断的条件,就在索引层先过滤掉,不满足的根本不回表------省掉的是昂贵的随机回表 I/O。

实测对比(联合索引 idx_abc(a,b,c),查 a=1 AND b>50):

sql 复制代码
SET optimizer_switch='index_condition_pushdown=off';
EXPLAIN SELECT * FROM t_abc WHERE a=1 AND b>50;
-- Extra: Using where        ← 关了ICP

SET optimizer_switch='index_condition_pushdown=on';
EXPLAIN SELECT * FROM t_abc WHERE a=1 AND b>50;
-- Extra: Using index condition  ← 开了ICP

🔴 怎么认 :EXPLAIN 的 Extra 列出现 Using index condition 就是 ICP 生效了。这是个 5.6 之后默认开启的优化,你不用配,但看到它要知道它在帮你省回表。


四、联合索引到底怎么设计

失效讲完了,正面问题来了:一个表要建哪些联合索引?五条原则,按优先级排。

4.1 等值在前,范围在后

联合索引里,范围查询列(>、<、BETWEEN、LIKE)之后的列用不上索引。所以:

  • 经常 WHERE a=? AND b=? AND c>? → 索引建 (a,b,c),等值的 a、b 在前,范围的 c 在最后。
  • 别建 (a,c,b) 然后指望 b 能用上------c 是范围,后面的 b 直接断档。

4.2 区分度高的列往前放

SHOW INDEX 里有个 Cardinality(基数),是索引里不同值的估算个数。Cardinality / 总行数 = 选择性,越接近 1 越好 。我实测 t_abc:

索引列 Cardinality 含义
PRIMARY(id) 99875 接近总行数,选择性极高
idx_phone 97557 手机号几乎唯一,好
idx_name 63531 用户名较高
idx_abc(a) 101 a 只有 ~100 个值,选择性差
idx_ct(create_time) 355 一天一个值,低

性别、状态这种只有两三个取值的列,单独建索引基本没用(优化器嫌它选择性差,宁可靠全表扫),但放进联合索引当末位列是有价值的------前面的高选择性列已经把范围缩得很小了。

⚠️ Cardinality 是估算值 ,由采样统计得来,数据大改后可能不准。跑一句 ANALYZE TABLE t_abc; 让它重新采样。

4.3 高频查询列优先覆盖

把最常出现在 WHERE、ORDER BY、GROUP BY 里的列挑出来。别给很少查的列单独建索引。

4.4 尽量用覆盖索引

如果一个查询需要的列全都在某个索引里 ,就不用回表了,EXPLAIN 的 Extra 会显示 Using index:

sql 复制代码
EXPLAIN SELECT phone FROM t_abc WHERE phone='13800000000';
-- type=ref  key=idx_phone  Extra=Using index  ← 不回表

因为要查的 phone 本身就在 idx_phone 索引叶子节点上,直接从索引拿,连主键都不用回。设计联合索引时,把高频 SELECT 的列也"塞"进索引末尾,就是常见的覆盖索引技巧。

4.5 单表索引别贪多

每多一个索引,写操作(INSERT/UPDATE/DELETE)都要多维护一棵 B+树,占用磁盘和 buffer pool。经验值:单表索引不超过 5 个 。上线后用 sys.schema_unused_indexes 找从来没用过的索引定期删。


五、几个被传歪了的结论

5.1 "单表不能超过 2000 万行"

这是谣言的讹传。真实依据是 B+树的高度:InnoDB 一个数据页 16KB,若单行不大,3 层 B+树大约能支撑 2000 万行左右,4 层能到几十亿 。但"2000 万"不是红线,它取决于单行大小和查询模式------单行越窄,一个页能塞的键越多,树越矮。千万级单表加好索引在生产里非常常见,真正要考虑分表的是到了亿级、或者写入压力大的时候。分分两种:垂直拆分 (按列把宽表拆开)和水平拆分(按行哈希/范围打散到多表)。

5.2 索引与排序:filesort 不一定真在"排文件"

ORDER BY 的列如果是索引列、且顺序和索引一致,就能直接用索引的有序性,不额外排序:

sql 复制代码
-- create_time 有 idx_ct
EXPLAIN SELECT * FROM t_abc ORDER BY create_time LIMIT 10;
-- type=index  key=idx_ct  Extra=NULL  ← 没 filesort

-- status 没索引
EXPLAIN SELECT * FROM t_abc ORDER BY status LIMIT 10;
-- type=ALL  Extra=Using filesort

⚠️ Using filesort 听着像"磁盘文件排序",其实多数情况是在内存里用快排/归并排的,数据量大才会落磁盘。它不是洪水猛兽,但能靠索引消除就消除。

🔴 MySQL 8.0 的坑要记 :ORDER BY a ASC, b DESC 这种升降混合,在 8.0 之前用不上联合索引(因为索引默认全是 ASC);8.0 开始支持降序索引 ,建索引时写 KEY idx(a ASC, b DESC) 就能匹配混合排序。

5.3 索引与 GROUP BY

GROUP BY 的列有索引时,能直接利用索引有序性分组,避免 Using temporary;没索引就会建临时表,数据量大时很贵。这条和排序同理。

5.4 强制/忽略索引

sql 复制代码
SELECT * FROM t_abc FORCE INDEX(idx_phone) WHERE ...;  -- 强行用某索引
SELECT * FROM t_abc IGNORE INDEX(idx_ct) WHERE ...;      -- 避开某索引

⚠️ 不到万不得已别用。优化器基于成本的判断通常比你拍脑袋准,FORCE INDEX 是在跟未来的数据分布赌命------数据量一变,你强推的索引可能就变成最差选择了。

5.5 索引监控

sql 复制代码
-- 从来没用过的索引(MySQL 8 的 sys 库)
SELECT * FROM sys.schema_unused_indexes;
-- 索引使用统计(读多还是写多、是否真被命中)
SELECT * FROM sys.schema_index_statistics;

上线后定期跑这两个,把"建了等于没建"的索引删掉,给写入减负。


六、面试怎么考

面试题 一句话答
最左前缀原则 联合索引按 a,b,c 排序,查询必须从最左列连续匹配,断了后面用不上
key_len 怎么算 反推用了几列;INT 可空=5,VARCHAR(n) utf8mb4=4n+2+1
索引失效场景 函数/运算、隐式类型转换、左模糊、OR 非索引列、违反最左前缀、!=/NOT IN
什么是 ICP 5.6+ 把索引能判断的条件下推到引擎层,减少回表;Extra=Using index condition
联合索引设计 等值在前范围在后、高选择性在前、覆盖索引、单表不超 5 个
filesort 是文件排序吗 不是,多为内存排序;能靠索引消除最好
单表 2000 万行 讹传,取决于行宽和查询,千万级加好索引没问题

总结:你真正需要记住的 8 件事

  1. 联合索引 (a,b,c) 先按 a 排、再按 b、再按 c,查询必须从最左列连续匹配。
  2. key_len 是手测"索引用了几列"的利器:可空 INT=5,三列全用=15。
  3. 索引列套函数、做运算、被隐式类型转换,都会让索引报废------列要"裸奔",变换挪到右边。
  4. 字符串列别用数字查;关联表字符集要统一 utf8mb4。
  5. 左模糊 %xx 用不了索引,右模糊 xx% 有资格但优化器看选择性决定。
  6. Using index condition = ICP 在帮你省回表,Using index = 覆盖索引不回表。
  7. 联合索引:等值在前范围在后、高选择性在前、尽量覆盖高频列、单表别超 5 个。
  8. 单表 2000 万行是讹传;filesort 多半是内存排序;FORCE INDEX 不到万不得已别用。

验证清单

  • 建一张 (a,b,c) 联合索引表,亲手跑出 key_len = 15/10/5 的三种组合
  • 制造一次 WHERE b=2(缺 a)确认 type=ALL
  • 用 WHERE phone=138...(数字)和 '138...'(字符串)对比 key 的有无
  • 开关 optimizer_switch 观察 Using index condition 的出现与消失
  • 跑一次 SHOW INDEX,看懂 Cardinality 和选择性
  • 给一个 ORDER BY 无索引列观察 Using filesort,加索引后消失
  • 查一次 sys.schema_unused_indexes 认识这张表

参考资源


下篇预告:《【工程实战】explain 与慢查询优化》------理论背得再熟,上了生产还是得靠 EXPLAIN 和慢查询日志抓现行。下一篇我会在 Docker 里灌 100 万行订单数据,开慢日志,把"全表扫描→加索引前后耗时对比""函数导致失效""深分页"这七个真实优化案例逐个跑给你看。

相关推荐
张宇Joaquin1 小时前
Linux系统下磁盘扇区512n、512e和4kn的区别
linux·运维·数据库
南丘山1 小时前
接口自动化测试从零搭:31 条用例跑 4 秒,抓出 6 个权限缺陷
后端
用户6279947182621 小时前
南大通用 GBase 8c 锁等待与死锁排查:从阻塞链到全局事务号
数据库
道虽迩,不行不至1 小时前
数据库自动连库执行增量脚本-Linux
linux·数据库
掘金小豆1 小时前
Spring AI 实战:从一次可运行的模型调用到 RAG 体系
后端·程序员·ai编程
GeW1 小时前
新质生产力发展路线图来了,Linux和数据库人该怎么接
linux·数据库
前端小万1 小时前
探索副业五个月,最终以放弃告终
前端·后端·面试
用户87359865635871 小时前
Bias-Variance Tradeoff
后端
今天早睡1 小时前
TypedDict 还是 Pydantic?
后端