B+ 树联合索引为什么遵循最左前缀原则?
简化版
B+ 树联合索引会按多个列从左到右拼成复合 key 排序,例如 (a,b,c) 先按 a 排,再按 b 排,最后按 c 排。因此查询只有从最左列开始约束,才能在树上定位到连续范围;跳过 a 直接查 b,全局上并不是有序的。
详细版
联合索引 (a,b,c) 在 B+ 树里不是三棵树,而是一棵按复合 key 排序的树。排序规则类似字典序:
- 先比较
a; a相同再比较b;b相同再比较c。
所以:
where a=1可以用;where a=1 and b=2可以用;where a=1 and b=2 and c=3可以用;where b=2通常不能高效利用(a,b,c)的有序定位;where a=1 and c=3可以先用a定位范围,但c不能像连续前缀那样缩小树搜索范围。
范围条件也会截断后续列的有序利用,例如 a=1 and b>10 and c=3,通常能用到 a,b 定位范围,但 c 很难继续用于精确缩小 B+ 树扫描区间。
完整版教学
一、联合索引在 B+ 树里到底长什么样
联合索引不是给每个列各建一层树,而是把多个列组合成一个复合 key。对于 (a,b,c),叶子页中的索引项按 (a,b,c) 的字典序排列。这个排序规则决定了查询能不能变成一段连续扫描。
例如索引项可能是:
(1,1,5)
(1,2,1)
(1,2,9)
(2,1,1)
(2,2,3)
(3,1,8)
你会发现所有 a=1 的项连续,所有 a=1,b=2 的项也连续;但所有 b=2 的项不连续,它们被不同 a 分散隔开。这就是最左前缀原则的直观来源。
记忆钩子:联合索引像英文字典,先按第 1 个字母排;不知道第 1 个字母时,直接按第 2 个字母找就会散落全书。
二、为什么跳过最左列会失去连续范围
B+ 树查找依赖 key 的全局有序性。若查询 where b=2,在 (a,b,c) 索引中,b=2 的记录会分布在 a=1、a=2、a=3 等多个区域。树没有办法通过一次从根到叶的定位找到一段完整连续范围,只能扫描大量索引项再过滤。
用一个小表看得更清楚:
| 查询条件 | 在 (a,b,c) 中是否连续 | 能否高效定位 |
|---|---|---|
a=1 | 连续 | 可以 |
a=1 and b=2 | 连续 | 可以 |
b=2 | 不连续 | 通常不行 |
a=1 and c=3 | a=1 连续,c 在范围内过滤 | 部分可用 |
所以最左前缀不是数据库随便定的规则,而是复合 key 排序方式自然带来的限制。
三、范围条件为什么会截断后续列
如果条件是 a=1 and b>10 and c=3,B+ 树可以定位到 a=1,b>10 的起点,然后沿叶子链表扫描。但在 b>10 这个范围内,c 的值不是全局连续排列的,因为排序优先级仍然是先 b 后 c。不同 b 值下面各自有 c 的局部顺序,整体不能直接按 c=3 再缩成一段。
(1,11,1)
(1,11,3)
(1,12,2)
(1,12,3)
(1,13,1)
(1,13,3)
c=3 分散在多个 b 组里,不能形成单个连续区间。因此范围列之后的列通常只能用于索引条件下推或过滤,不能继续缩小树定位范围。
四、最左前缀和索引下推不是一回事
有些数据库可以在索引扫描过程中做 Index Condition Pushdown,把后续列条件尽早在存储引擎层过滤。比如 (a,b,c) 上 a=1 and b>10 and c=3,c=3 可能不能继续缩小扫描范围,但如果 c 在索引里,扫描索引项时可以先过滤 c,减少回表。
| 概念 | 作用 | 是否改变扫描范围 |
|---|---|---|
| 最左前缀 | 决定能定位到哪段索引范围 | 是 |
| 索引下推 | 在扫描过程中提前过滤 | 通常否 |
| 覆盖索引 | 查询列都在索引里,避免回表 | 否,但减少 I/O |
面试时把这三件事分开讲,会比只背口诀清楚得多。
五、联合索引列顺序怎么设计
联合索引列顺序要服务查询模式。常见原则是把高频等值条件放前面,把范围条件放后面,把排序和分组需要的列纳入连续前缀。但选择性不是唯一指标,如果某列虽然选择性高却不常作为前缀查询,也未必适合放最前。
例如常见查询是:
where tenant_id = ? and status = ? and created_at between ? and ?
order by created_at
索引 (tenant_id, status, created_at) 通常比 (created_at, tenant_id, status) 更符合定位和排序需求,因为前两个等值条件先缩小范围,第三列还能支持范围和顺序扫描。
六、常见例外和优化器选择
最左前缀是理解 B+ 树联合索引的核心规则,但实际优化器可能有额外策略,如 skip scan、索引合并、统计信息驱动的成本选择。某些数据库在最左列基数很低时,可能尝试枚举最左列不同值来利用后续列,但这不是通用默认答案。
因此面试回答最好分两层:原理上,复合 key 字典序决定最左前缀;工程上,优化器可能做额外成本优化,但不能把例外当基本规则。
七、常见误区与追问
- 误区:联合索引
(a,b,c)等于同时拥有a、b、c三个单列索引。 它是一棵按复合 key 排序的 B+ 树,不是三棵独立树。 - 追问:
where a=1 and c=3能不能用索引? 可以用a=1定位范围,c=3多数情况下作为过滤条件。 - 误区:范围条件之后的列完全没价值。 它可能不能缩小定位范围,但可用于索引下推或覆盖索引。
- 追问:为什么
b=2不连续? 因为排序先按 a,不同 a 下的 b=2 分散在多个位置。 - 误区:列选择性最高就一定放最前。 列顺序要结合等值、范围、排序、分组和真实查询频率。
八、加强记忆
最左前缀的根源是复合 key 的字典序。(a,b,c) 先按 a 排,a 相同才按 b,b 相同才按 c;所以从左连续约束越长,B+ 树越能定位到更窄的连续范围。跳过左列会散,范围条件会让后续列难以继续形成单段范围。记住「定位范围」和「扫描中过滤」是两件事,很多索引题就不会混。