3. 最左前缀法则:联合索引的工作原理

联合索引,说白了就是多个字段拼在一起组成的索引。很多同学觉得它很简单,不就是把几个字段塞进一个 B+ 树嘛。但实际用起来,坑特别多。

我记得刚带团队那会儿,有个同事跑过来问我:“我明明建了联合索引,为什么查询还是全表扫描?” 我一看他的 SQL,嗯,问题出在查询条件的顺序上。这就是我们今天要聊的核心——最左前缀法则。

3.1 联合索引到底长什么样?

先看一个例子。假设我们有张订单表,建了这样一个联合索引:

CREATE INDEX idx_uid_status_time ON orders(user_id, status, create_time);

这个索引在 B+ 树里是怎么存的?

你可以把它想象成一本三层目录的电话本。第一层按 user_id 排序,第二层在相同 user_id 里按 status 排序,第三层再按 create_time 排序。

说白了,MySQL 不是把三个字段分别存三份,而是把它们拼接成一个复合键,整体排序。这就决定了:你跳过了左边的字段,右边的字段就没法用。

核心结论:联合索引的排序规则是——先按第一个字段排,相同再按第二个,以此类推。查询时必须从最左边开始匹配,不能跳过中间的字段。

下面这张图能帮你直观理解:

联合索引 B+ 树结构示意图 根节点(按 user_id 范围) user_id=1001 user_id=1002 user_id=1003 叶子页(按 status 排序) status=0 → status=1 → status=2 相同 status 再按 create_time 排序 叶子页(按 status 排序) status=0 → status=1 → status=2 相同 status 再按 create_time 排序 💡 查询时:必须从最左字段开始匹配,不能跳过中间的字段 例如:WHERE user_id=1002 AND status=1 可以命中;WHERE status=1 无法命中

3.2 最左前缀法则的三种场景

我们拿刚才的 idx_uid_status_time 索引来测试。假设表里有 100 万行数据,看看不同查询条件的效果:

查询条件 是否命中索引 原因
WHERE user_id=1002 ✅ 完全命中 从最左字段开始匹配
WHERE user_id=1002 AND status=1 ✅ 完全命中 连续匹配前两个字段
WHERE user_id=1002 AND status=1 AND create_time>'2024-01-01' ✅ 完全命中 三个字段连续匹配
WHERE status=1 ❌ 无法命中 跳过了最左字段 user_id
WHERE user_id=1002 AND create_time>'2024-01-01' ⚠️ 部分命中 只用到了 user_id,跳过了 status
WHERE status=1 AND create_time>'2024-01-01' ❌ 无法命中 跳过了最左字段 user_id

我的经验:很多人在写 SQL 时,习惯把选择性高的字段放前面。但联合索引的字段顺序,应该优先考虑查询频率,而不是选择性。我在项目中见过太多人把性别字段放最左边,结果查询时根本用不上——因为几乎所有查询都带 user_id 而不是性别。

3.3 为什么查询条件顺序会影响索引命中?

你可能会问:“我把条件换个顺序,比如 WHERE status=1 AND user_id=1002,MySQL 会怎么处理?”

答案是:MySQL 的查询优化器会自动调整条件顺序。所以只要条件里包含了最左字段,不管你怎么写顺序,优化器都能识别。

但这里有个陷阱——查询条件的顺序和索引字段的顺序是两码事。真正影响索引命中的,是你写了哪些字段,而不是你按什么顺序写。

举个例子:

-- 这两个 SQL 效果完全一样,都能命中索引
SELECT * FROM orders WHERE status=1 AND user_id=1002;
SELECT * FROM orders WHERE user_id=1002 AND status=1;

但是:

-- 这个 SQL 只能用到 user_id 字段,status 用不上
SELECT * FROM orders WHERE user_id=1002 AND create_time > '2024-01-01';

为什么会这样?因为联合索引的排序规则决定了:只有连续的前缀字段才能参与索引定位。你跳过了 status,直接查 create_time,MySQL 只能通过 user_id 定位到一批数据,然后在这批数据里做回表过滤。

⚠️ 注意:“回表过滤”不是索引扫描,而是先通过索引找到一批主键,再回表查完整数据,最后用 create_time 条件过滤。如果这批数据量很大,性能会急剧下降。

3.4 实战中的常见误区

我带过不少团队,发现大家对联合索引的误解主要集中在下面几个地方:

误区一:建了联合索引就不用建单列索引

这个说法不完全对。联合索引确实能覆盖多个查询,但如果你有一个高频查询只查 status 字段,联合索引是帮不上忙的。我曾经在一个电商项目里,发现有个接口响应时间超过 5 秒,排查后发现就是这个问题——查询只用了 status 字段,但索引是从 user_id 开始的。

我的建议是:单独评估每个高频查询的索引需求,不要一刀切。

误区二:字段顺序按选择性排就一定好

选择性高的字段(比如手机号)放前面,确实能快速缩小范围。但如果你 90% 的查询都带 user_id,只有 10% 的查询带手机号,那 user_id 放前面才是更优解。

你想想看,索引的最终目的是减少扫描行数。如果最左字段能过滤掉 99% 的数据,后面的字段选择性再低也无所谓。

误区三:范围查询后面的字段也能用索引

这是一个非常经典的坑。看这个例子:

-- 假设索引是 (user_id, status, create_time)
SELECT * FROM orders 
WHERE user_id = 1002 
  AND status > 0 
  AND create_time > '2024-01-01';

这里 status > 0 是范围查询。一旦遇到范围查询,它后面的字段(create_time)就无法使用索引了。MySQL 只能通过 user_id 和 status 定位到一批数据,然后回表过滤 create_time。

解决方案:把范围查询的字段尽量放在联合索引的最后面。如果范围查询的字段必须放中间,考虑拆成两个索引,或者用 IN 列表代替范围查询。

误区四:OR 条件也能走索引

我曾经在代码评审时看到这样的 SQL:

SELECT * FROM orders 
WHERE user_id = 1002 OR status = 1;

这个查询大概率不会走索引。因为 OR 条件意味着 MySQL 需要分别查找两个条件的结果,然后做合并。除非两个条件各自都有独立的索引,否则优化器通常会选择全表扫描。

我的建议是:尽量避免 OR,改用 UNION 或者拆成两个查询。

3.5 如何验证索引是否命中?

说了这么多理论,怎么在实际中验证?很简单,用 EXPLAIN:

EXPLAIN SELECT * FROM orders WHERE user_id = 1002 AND status = 1\G

*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: orders
   partitions: NULL
         type: ref
possible_keys: idx_uid_status_time
          key: idx_uid_status_time
      key_len: 8
          ref: const,const
         rows: 15
     filtered: 100.00
        Extra: Using index condition

重点关注几个字段:

  • type:如果是 ref 或 range,说明走了索引。如果是 ALL,就是全表扫描。
  • key:显示实际使用的索引名。
  • key_len:这个很关键。它表示索引使用的字节数。如果联合索引的 key_len 比预期短,说明后面的字段没用到。
  • Extra:出现 Using index condition 说明用了索引下推(ICP),这是 MySQL 5.6 后的优化特性。

一个小技巧:通过 key_len 可以判断联合索引到底用到了几个字段。比如 user_id 是 INT(4 字节),status 是 TINYINT(1 字节),如果 key_len 是 5,说明两个字段都用上了;如果只有 4,说明只用了 user_id。

3.6 总结一下

最左前缀法则其实就一句话:联合索引的查询必须从最左边的字段开始,连续匹配,不能跳过中间的字段。

记住几个关键点:

  • 查询条件的书写顺序不重要,优化器会自动调整
  • 查询条件包含哪些字段才重要
  • 范围查询后面的字段用不上索引
  • OR 条件大概率不走索引
  • 用 EXPLAIN 和 key_len 验证索引使用情况