什么是最左前缀匹配?为什么要遵守?
结论:联合索引
(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 = 1 | a | 4 | 只用最左列 |
a = 1 AND b = 2 | a, b | 4+203 | 连续前缀 |
a = 1 AND b = 2 AND c = 3 | a, b, c | 4+203+5 | 全部命中 |
a = 1 AND c = 3 | a | 4 | 跳过 b,c 只能回表过滤 |
b = 2 | 无 | — | 全索引扫描或全表 |
a > 1 AND b = 2 | a | 4 | 范围列之后的列无法继续定位 |
a = 1 AND b > 2 AND c = 3 | a, b | 4+203 | c 仅能靠 ICP 过滤,不能定位 |
a = 1 AND b LIKE 'x%' | a, b | 4+203 | 前缀匹配可用 |
a = 1 AND b LIKE '%x' | a | 4 | 前导通配符失效 |
(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 的例外
- 索引跳跃扫描(Skip Scan,8.0.13+):当最左列取值很少时,优化器可以枚举它的值逐个探测,等价于「跳过最左列」。
- 索引条件下推(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。