人人都会AI编程

12.2 联合索引设计方法与列顺序选择

更新时间:2026-07-11

联合索引是日常开发中最常用、也最容易用错的索引类型。用好它,一条 SQL 从几秒降到几毫秒是常有的事;用错了,索引建了一大堆,查询却依然慢得离谱。这一节的目标是让你能根据实际的查询需求,设计出真正生效的联合索引,而不是凭感觉随意排列字段。

12.2.1 什么是联合索引

联合索引也叫多列索引,是在一张表的多个列上共同创建的索引。比如:

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

这里创建的并不是三个独立的索引,而是一个包含三列的 B+ 树索引。在这个索引中,数据首先按 user_id 排序,user_id 相同的行再按 status 排序,status 也相同的再按 create_time 排序。索引的叶子节点存放的是对应的主键值,查询时如果需要回表,就拿着主键值回到主键索引获取完整行数据。

联合索引能一次覆盖多个查询条件,最关键的依据就是最左前缀原则

12.2.2 最左前缀原则:联合索引生效的核心规则

最左前缀原则是说:MySQL 的查询优化器在利用联合索引时,只会从索引的最左边列开始,按照索引列的顺序,连续匹配查询条件。 一旦条件中出现了范围查询(>, <, BETWEEN, LIKE 'xxx%' 等),范围查询列右侧的列就无法再利用索引继续精确过滤。

用前面的 idx_a_b_c 举例:

  • WHERE user_id = 101 AND status = 'paid' — 可以完美利用索引的两列;
  • WHERE user_id = 101 — 只用索引第一列,也生效;
  • WHERE status = 'paid' — 索引第一列 user_id 没出现,索引完全失效,变成全表扫描;
  • WHERE user_id = 101 AND create_time > '2024-01-01'user_id 用上了,create_time 虽然也在索引里,但中间的 status 被跳过了,所以 create_time 这部分只能做部分过滤(索引条件下推),但不会像前缀那样精准缩小范围。

范围查询的那一列本身是能用上索引的(定位到范围的起点,然后向后扫描),但它后面的列就没办法再缩小扫描范围了。例如:

SELECT * FROM orders
WHERE user_id = 101 AND status > 'paid' AND create_time > '2024-01-01';

这里,user_id 精确匹配,status 是范围查询,所以索引可以快速定位到 user_id = 101 AND status > 'paid' 的起点,然后顺序扫描。但是对于 create_time 的条件,只能在扫描过程中过滤掉不符合的行,而不能缩小扫描范围。

12.2.3 联合索引列顺序的三大选择原则

知道了最左前缀原则,下一个关键问题是:多个列到底应该怎么排序? 列顺序选反了,索引的使用价值会大打折扣。

原则一:等值查询列放最前面,范围查询列放最后

如果一个查询既有 = 条件又有 >< 条件,把等值查询的列放在联合索引左侧,范围查询的列放在右侧。这样可以在索引中先用等值条件缩小到一个很小的范围,然后再对这个范围内的数据进行范围扫描。

比如订单表经常查询:

SELECT * FROM orders
WHERE user_id = ? AND status = ? AND create_time > ?;

这里 user_idstatus 都是等值查询,create_time 是范围查询。索引设计应该为 (user_id, status, create_time),把范围查询列放在最后。如果把 create_time 放在中间,那么它后面的 status 就无法被索引有效利用。

原则二:区分度高的列放前面,区分度低的可后置或省略

区分度是指某一列上不同值的数量与总行数的比值。比如 user_id 的区分度极高(每个用户都是唯一的),gender 区分度极低(只有男、女、未知)。在联合索引中,将高区分度的列放在前面,可以让索引在最初几步就过滤掉大量数据,快速定位到目标行。

例如在用户画像查询中:

SELECT * FROM users WHERE city = ? AND gender = ?;

city 的区分度可能远高于 gender,因此 (city, gender)(gender, city) 的效果要好得多。事实上,如果 gender 区分度太低,单独为它建索引几乎没有意义,放在联合索引的靠后位置反而可以借助 city 进一步过滤。

原则三:基于高频查询模式,让一个索引服务多个查询

实际应用中,我们往往希望一个联合索引能够覆盖表上的多个高频查询,从而减少索引数量(索引太多会影响写入性能)。这就需要根据最常见的查询模式来设计列顺序。

比如对订单表,常见的查询有:

  • 查询某用户的所有订单:WHERE user_id = ?
  • 查询某用户某状态的订单:WHERE user_id = ? AND status = ?
  • 查询某用户某状态的订单并按时间排序:WHERE user_id = ? AND status = ? ORDER BY create_time DESC

这三个查询都可以被 (user_id, status, create_time) 这一个联合索引覆盖。查询一和查询二分别使用了索引的前 1 列和前 2 列,查询三则既能利用前两列过滤,又能利用第三列直接完成排序,避免文件排序(filesort)。

如果业务中同时有大量只查 status 的请求(不传 user_id),那这个联合索引就帮不上忙了,需要额外给 status 单独建索引,或者根据业务调整设计。

12.2.4 排序与分组对索引列顺序的要求

如果你的查询经常需要 ORDER BY 某个字段,联合索引也可以用来直接完成排序,避免额外开销。

当 SQL 中的 ORDER BY 子句的顺序正好与联合索引的列顺序一致,且全部为升序(或全部为降序)时,MySQL 可以直接利用索引返回有序数据,无需额外排序。但如果排序方向与索引方向交叉(有的升序有的降序),在 MySQL 8.0 之前无法直接利用索引排序(8.0 引入了降序索引,可以部分解决)。

比如索引为 (user_id, status, create_time),查询为:

SELECT * FROM orders
WHERE user_id = 101
ORDER BY status, create_time;

此时索引可以直接用来排序。但如果写成 ORDER BY status, create_time DESC,并且索引是默认的升序索引,MySQL 就可能需要额外排序步骤(filesort),除非你创建降序索引 (user_id, status, create_time DESC)

同样,GROUP BY 列的顺序也应尽量与联合索引前缀匹配,这样可以避免生成临时表,直接用索引完成分组。

12.2.5 实战中的典型误区与避坑指南

误区一:把等值查询列放在范围查询之后

错误示例:索引 (create_time, user_id, status),查询 WHERE user_id = ? AND create_time > ?。此时 user_id 被放在了范围列后面,索引只能用到 create_time 的范围扫描,user_id 只做索引下推过滤,效果大打折扣。

误区二:按字段出现顺序随便排列

很多开发者直接把建表时的字段顺序或者 WHERE 条件里的字段顺序拿过来建联合索引,这是没有意义的。列顺序必须基于查询模式精心设计,而不是机械照抄。

误区三:始终追求“建一个超大联合索引”

试图用一个联合索引覆盖所有查询是危险的。一个包含五六列的联合索引,不仅写入维护成本高,而且实际查询时往往只有前一两列被用到,后面的列成了无效负载。应当有针对性地为高频核心查询设计联合索引,其他低频率查询可以通过单独索引或复合方式解决。

误区四:认为联合索引的每一列都可以独立查询

很多人觉得建了 (a, b, c) 索引,那么查询 WHERE b = ? 也能用上索引。实际上这违反了最左前缀原则,除非查询本身能触发“索引跳跃扫描”(MySQL 8.0 引入的优化,但限制很多且不如图里独),否则基本不会走这个联合索引。

12.2.6 设计流程建议

面对一张需要优化的表,可以按下面的步骤来确定联合索引:

  1. 列出这张表上所有核心查询 SQL(WHERE、ORDER BY、GROUP BY 的组合)。
  2. 识别出最通用、最频繁的查询模式,找出必须的等值列和范围列。
  3. 根据“等值在前,范围在后”和区分度高低,拟定 1~2 个候选联合索引方案。
  4. EXPLAIN 验证这些索引是否能覆盖到你的主要查询(观察 key_len 看用到了几列)。
  5. 权衡写入性能,避免建立过多冗余索引(比如已经有了 (a, b) 索引,再建 (a) 索引基本是冗余的)。

联合索引的设计,本质上是用最少的索引满足最多的查询,并且让每个查询都能用上尽可能多的前缀列。结合 EXPLAIN 反复验证,你就能建立稳定可靠的设计直觉。