什么是最左前缀匹配?为什么要遵守?

#中间件#面试八股

什么是最左前缀匹配?为什么要遵守?

结论:联合索引 (a, b, c) 在 B+Tree 里是按 a、再 b、再 c 的字典序排列的。只有先确定前缀,后续列才有序、才能被二分定位。所以查询条件必须从最左列开始连续使用,跳过 a 直接查 b 就用不上这条索引。

一、索引里的顺序长什么样

联合索引 idx_abc (a, b, c) 的叶子顺序(先按 a,再按 b,再按 c):

a=1,b=1,c=1
a=1,b=1,c=5
a=1,b=2,c=3     ← a=1 且 b=2 的区间内,c 有序,可二分
a=2,b=1,c=2
a=2,b=1,c=7
a=3,b=1,c=1

WHERE a = 1 AND b = 2 AND c = 3
  → 先定位 a=1 的区间,再在其中定位 b=2,再定位 c=3 ✅

WHERE b = 2 AND c = 3
  → b 在整个索引里并不全局有序(a=1/2/3 下都有 b=2)
  → 无法二分,只能扫全索引或全表 ❌

一句话:索引的有序性是「前缀有序」,不是「每列各自有序」。

二、命中矩阵

设索引为 idx_abc (a, b, c):

查询条件用到的索引列key_len 示例说明
a = 1a4只用最左列
a = 1 AND b = 2a, b4+203连续前缀
a = 1 AND b = 2 AND c = 3a, b, c4+203+5全部命中
a = 1 AND c = 3a4跳过 b,c 只能回表过滤
b = 2无—全索引扫描或全表
a > 1 AND b = 2a4范围列之后的列无法继续定位
a = 1 AND b > 2 AND c = 3a, b4+203c 仅能靠 ICP 过滤,不能定位
a = 1 AND b LIKE 'x%'a, b4+203前缀匹配可用
a = 1 AND b LIKE '%x'a4前导通配符失效

(key_len 仅为示意:int NOT NULL 4 字节,varchar(50) NULL utf8mb4 为 50×4+2+1=203 字节。)

三、用 key_len 反推用了几列

CREATE TABLE t_demo (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  a  INT NOT NULL,
  b  VARCHAR(50) DEFAULT NULL,
  c  INT DEFAULT NULL,
  PRIMARY KEY (id),
  KEY idx_abc (a, b, c)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 只用到 a:key_len = 4
EXPLAIN SELECT * FROM t_demo WHERE a = 1\G

-- 用到 a、b:key_len = 4 + 203 = 207
EXPLAIN SELECT * FROM t_demo WHERE a = 1 AND b = 'x'\G

-- 跳过 b:key_len 仍是 4,c 只能作为过滤条件
EXPLAIN SELECT * FROM t_demo WHERE a = 1 AND c = 3\G

-- 范围列之后的 c 不再贡献 key_len
EXPLAIN SELECT * FROM t_demo WHERE a = 1 AND b > 'm' AND c = 3\G
-- Extra: Using index condition(ICP 在引擎层过滤 c,减少回表)

key_len 是判断「最左前缀到底用了几列」最直接的手段,比看 ref 列更精确。

四、为什么范围条件会截断

B+Tree 能二分的前提是「当前列在前缀固定时全局有序」。a > 1 只是定位到一段连续区间,区间内按 (b, c) 有序,但 c 的有序性依赖 b 的取值,而 b 在查询里是范围而非定值,所以 c 无法继续二分。

因此列的排列顺序应该是:等值条件列 → 范围条件列 → 排序列。

-- 查询:WHERE tenant_id = ? AND ctime >= ? AND status = ? ORDER BY ctime
-- 推荐索引:
ALTER TABLE t_order ADD KEY idx_tenant_status_ctime (tenant_id, status, ctime);

五、两个 8.0 的例外

  1. 索引跳跃扫描(Skip Scan,8.0.13+):当最左列取值很少时,优化器可以枚举它的值逐个探测,等价于「跳过最左列」。
  2. 索引条件下推(ICP):范围列之后的过滤条件下推到引擎层,减少回表次数,但不改变「无法用于定位」的事实。
EXPLAIN SELECT * FROM t_demo WHERE b = 'x';
-- Extra: Using index for skip scan(仅当 a 的基数很小时可能出现)

Skip Scan 是补救,不是设计依据;把最左列换成查询常用的列才是根本解。

常见追问

  • (a, b) 和 (b, a) 有区别吗? 有,前者服务 a 与 a+b,后者服务 b 与 b+a,完全不能互相替代。
  • a = 1 AND c = 3 能不能建 (a, c)? 能,如果这条查询高频且 a 之外只关心 c,单独建 (a, c) 更合适。
  • ORDER BY 也要遵守最左前缀吗? 要,排序的列顺序必须是索引的前缀,且方向一致(8.0 支持降序索引后更灵活)。
  • IN 算等值还是范围? 对最左前缀而言等价于多个等值,后续列仍可继续使用索引。
  • 为什么 LIKE 'x%' 能用、LIKE '%x' 不能用? 前者是有序前缀,可二分;后者没有确定起点,只能全扫。
  • 联合索引最多几列? 上限 16 列,实际 3~5 列足够,再多就要怀疑是不是该拆表了。

小结

最左前缀不是 MySQL 的怪癖,而是 B+Tree 字典序的必然结果:前缀定了,后面才有区间可二分。设计时按「等值 → 范围 → 排序」排列列序,验证时看 key_len 用了几列,遇到跳列就补索引或调整列序,而不是指望 Skip Scan。

笔记加载中…