VOL.001 · 创刊号Token 天团 API

Token 天团

—— 一份关于代码与生活的个人刊物 ——

技术Java教程

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——把高频查询的返回列塞进联合索引尾部,是「用空间换时间」最划算的一档。

五、建索引的次序原则

联合索引的列序怎么定?三条原则按优先级:

  1. 等值在前,范围殿后(第二节的推论);
  2. 区分度高的在前:count(distinct col)/count(*) 越接近 1 越排前——性别列(区分度≈0.5)放第一位等于把索引切两半就废掉;
  3. 前缀不重复建:有了 (a,b,c),a 的查询天然被服务,单独再建 (a) 纯属浪费(还拖慢写入)。索引是写入时每次都要维护的 B+ 树,不是越多越好。

老兵点评

  1. 慢查询的第一嫌疑人永远是对列的变形。函数、运算、隐式转换、前导模糊——四宗罪占了线上索引失效的绝大多数。Code Review 看到 WHERE 里列上有「动作」(套函数、做运算、类型对不上),当场打回,成本最低。
  2. 别把建索引当礼物乱送。每个索引都是一张要同步维护的排序表:写入变慢、优化器选择变复杂(有时还选错)。先 EXPLAIN,缺什么补什么;冗余前缀的、长年没被查询命中的,定期清理(sys.schema_unused_indexes)。
  3. 最左前缀是「结构推规则」的范本。背下来的规则会忘,从排序结构推出来的规则不会——这套思路后面讲 B+ 树聚簇索引、讲分库分表的路由键,全部通用。

带走三句话

  1. 索引是字典:先按 a 排,a 相同按 b——最左前缀是排序的性质,不是 MySQL 的规定;范围之后全失效。
  2. 四宗罪:函数、运算、隐式转换、前导模糊——对列做变形,等于废掉排序;varchar 列的条件永远写字符串。
  3. 建索引:等值在前、范围殿后、区分度高在前;前缀重复的索引等于白建——上线前 EXPLAIN 看 key_len。

本篇是「Java 工兵连」第 6 篇。→ 上一篇:第 5 篇 · Spring 事务失效的八个瞬间 | 继续阅读:第 7 篇 · 分布式锁的三重门 | 浏览整个专栏 →。

❦ 终 ❦

← 回到首页全部归档 →