MySQL 索引最左前缀:索引是本字典——Java 工兵连第 6 篇
「Java 工兵连」第 6 篇,暂时离开 JVM,讲数据库——因为 Java 工程师生产的慢查询,比慢代码多得多。最左前缀(Leftmost Prefix)是联合索引的核心性质,网上教程张口就来「(a,b,c) 只能 a、ab、abc 使用」,但很少讲为什么。这篇的目标:让你以后不用背规则,看一眼排序结构就能推出来。
一、先建直觉:电话簿
联合索引 (姓, 名) 就是一本按(姓, 名)排序的电话簿:先按姓排,姓相同的再按名排。对着它:
- 查「所有姓张的」✓——姓是整本书的第一序,直接翻到张姓区;
- 查「张三」✓——张姓区内名是有序的,二分即达;
- 查「所有名叫三的」✗——「名」的有序只在同姓内部成立,跨姓的全局顺序里名是乱的。想找全?逐页翻。
这就是最左前缀的全部秘密:它不是 MySQL 的规定,是排序结构的性质。B+ 树里的联合索引 (a,b,c) 先按 a 排、a 相同按 b、b 相同按 c——任何不从 a 开始的查找,都等于在不全局有序的序列里找东西。
二、五种查询模式,一张判定表
对着 hero 图右半,五种经典模式过一遍:
| WHERE 条件 | 索引使用 | 原因 |
|---|---|---|
a = 1 |
✓ 用 a | 从第一序开始 |
a = 1 AND b = 2 |
✓ 用 a,b | 连续前缀 |
a=1 AND b=2 AND c=3 |
✓ 三列全用 | 完整匹配 |
b = 2 AND c = 3 |
✗ 全表扫 | 缺最左列 a,序从中间开始 |
a = 1 AND c = 3 |
◐ a 定位 + c 过滤 | c 够不着有序定位,但能用 a 圈定的范围里逐条过滤(5.6+ 的索引下推 ICP 把这步过滤挪进了存储引擎层,减少回表,但本质仍是「半用」) |
最值钱的一条:范围之后全失效。 a = 1 AND b > 5 AND c = 3 里,b 一旦用了范围(>、<、between、like 'x%'),c 的有序就被打碎——b 落在一个区间内的行,c 是乱序的,没法继续二分。由此得出写 SQL 和建索引的黄金原则:等值条件在前,范围条件殿后;三个列里谁要 >,谁排最后。
三、三大自杀写法:对索引列做「变形」
以下写法的共同本质:等号左边不再是列本身,而是列的函数——对变形后的值来说,索引不再有序。
1. 函数。 DATE(create_time) = '2026-11-03' ——对列套函数,索引白建。改写成范围:
-- ✗ 索引失效
WHERE DATE(create_time) = '2026-11-03'
-- ✓ 前闭后开范围,索引满速
WHERE create_time >= '2026-11-03 00:00:00'
AND create_time < '2026-11-04 00:00:00'
2. 运算。 age + 1 = 18 失效;把运算挪到常量侧 age = 17 满血。同理 salary * 12 > 300000 改 salary > 25000。
3. 隐式类型转换。 最阴的一个。phone 是 varchar,查询写 WHERE phone = 13800138000(数字)——MySQL 的规则是字符串和数字比较时,把字符串转成数字,等价于 CAST(phone AS ...) = 13800138000,又是对列做变形,索引失效,还往往一条慢查询警告都没有。修法:列什么类型,条件就写什么类型('13800138000')。反过来(数字列 vs 字符串常量)转换发生在常量上,不伤索引。
顺带第四条:前导模糊。LIKE '%三' 和 LIKE '%三%' 的前导 % 等于「从中间任意位置开始找」,字典帮不了你;LIKE '张%' 是前缀范围,能用索引。
四、EXPLAIN 判读:key_len 是成绩单
规则背一百遍,不如看一次优化器的实话:
-- 假设 a INT、b INT、c INT,均 NOT NULL,联合索引 idx_abc(a,b,c)
EXPLAIN SELECT * FROM t WHERE a = 1 AND b = 2 AND c = 3;
-- key = idx_abc, key_len = 12 → 三列全用上(每列 4 字节)
EXPLAIN SELECT * FROM t WHERE a = 1 AND c = 3;
-- key = idx_abc, key_len = 4 → 只用了 a;Extra: Using index condition(ICP 下推)
EXPLAIN SELECT * FROM t WHERE a = 1 AND b > 5 AND c = 3;
-- key_len = 8 → a、b 用上,c 被 b 的范围断掉
key_len 直接告诉你用了几列:4 的倍数往上跳,一列 4 字节(INT);可空列每列再加 1。再看 type:ref(等值定位)优于 range(范围扫描),index(扫全索引)和 ALL(全表)就该警惕了。上线前的 SQL,EXPLAIN 一眼是肌肉记忆。
还有个白捡的优化:覆盖索引。如果 SELECT 的列全在索引里(比如 SELECT a,b,c ... WHERE a=1 AND b=2),连回表都省了,Extra 显示 Using index——把高频查询的返回列塞进联合索引尾部,是「用空间换时间」最划算的一档。
五、建索引的次序原则
联合索引的列序怎么定?三条原则按优先级:
- 等值在前,范围殿后(第二节的推论);
- 区分度高的在前:
count(distinct col)/count(*)越接近 1 越排前——性别列(区分度≈0.5)放第一位等于把索引切两半就废掉; - 前缀不重复建:有了
(a,b,c),a的查询天然被服务,单独再建(a)纯属浪费(还拖慢写入)。索引是写入时每次都要维护的 B+ 树,不是越多越好。
老兵点评
- 慢查询的第一嫌疑人永远是对列的变形。函数、运算、隐式转换、前导模糊——四宗罪占了线上索引失效的绝大多数。Code Review 看到 WHERE 里列上有「动作」(套函数、做运算、类型对不上),当场打回,成本最低。
- 别把建索引当礼物乱送。每个索引都是一张要同步维护的排序表:写入变慢、优化器选择变复杂(有时还选错)。先 EXPLAIN,缺什么补什么;冗余前缀的、长年没被查询命中的,定期清理(
sys.schema_unused_indexes)。 - 最左前缀是「结构推规则」的范本。背下来的规则会忘,从排序结构推出来的规则不会——这套思路后面讲 B+ 树聚簇索引、讲分库分表的路由键,全部通用。
带走三句话
- 索引是字典:先按 a 排,a 相同按 b——最左前缀是排序的性质,不是 MySQL 的规定;范围之后全失效。
- 四宗罪:函数、运算、隐式转换、前导模糊——对列做变形,等于废掉排序;varchar 列的条件永远写字符串。
- 建索引:等值在前、范围殿后、区分度高在前;前缀重复的索引等于白建——上线前 EXPLAIN 看 key_len。
本篇是「Java 工兵连」第 6 篇。→ 上一篇:第 5 篇 · Spring 事务失效的八个瞬间 | 继续阅读:第 7 篇 · 分布式锁的三重门 | 浏览整个专栏 →。
❦ 终 ❦