在 MySQL、PostgreSQL、Oracle 等关系型数据库中,联合索引(Composite Index)是在一个索引结构里同时包含多个列。判断联合索引是否被查询利用,核心就是最左前缀原则(Leftmost Prefix Principle)。这个原则不是数据库优化器随意规定的,而是由 B+ 树索引键值的排序方式直接决定。了解索引按哪些列排序、查询条件如何与排序结果对齐,就能解释为什么有时建了索引却仍全表扫描。

联合索引可以看作按声明顺序生成的一个多级有序结构。下面从存储层面、执行计划和设计策略三个角度展开,把联合索引顺序和最左前缀原则一次说清。
一、联合索引在 B+ 树中是如何排序的
以 MySQL InnoDB 为例,假设有订单表 orders,包含用户ID、状态、创建时间等字段,并创建如下联合索引:
CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, status TINYINT NOT NULL, create_time DATETIME NOT NULL, amount DECIMAL(10,2), KEY idx_user_status_time (user_id, status, create_time) );
索引 idx_user_status_time 的键值并不是把三个列拼成一个字符串,而是先按 user_id 升序排列,当 user_id 相同时再按 status 升序排列,若 status 也相同,再按 create_time 升序排列。数据库中所有数据行的对应索引记录会按照这个规则组织成一棵 B+ 树。
为了更直观,可以把叶子节点中的键值逻辑顺序看成下面这样:
(1001, 1, '2024-01-01 10:00:00') (1001, 1, '2024-01-03 08:30:00') (1001, 2, '2024-01-02 12:00:00') (1002, 1, '2024-01-01 09:00:00') (1002, 2, '2024-01-04 15:20:00')
从上面顺序可以看出,user_id 是第一排序键,因此只有以 user_id 为条件才能快速定位起始范围;status 只在 user_id 相同的前提下才有序;create_time 则只在 user_id 和 status 都相同时才有序。这就是最左前缀原则的存储基础:越靠左的列,越能独立提供有序性;越靠右的列,越依赖前面列的等值条件。
二、最左前缀原则的匹配规则与执行计划
最左前缀原则可以概括为三点:查询条件必须从索引的最左列开始;中间列不能被跳过;范围条件会截断后续列的有序使用。也就是说,如果联合索引是 (A, B, C),那么 WHERE A = ?、WHERE A = ? AND B = ?、WHERE A = ? AND B = ? AND C = ? 都能正常使用索引;而 WHERE B = ? 则无法使用这个索引。
下面用一组查询对照说明索引使用情况。
| 查询条件 | 索引使用情况 | 说明 |
|---|---|---|
| user_id = ? AND status = ? AND create_time >= ? | 三列都能使用 | 完整匹配最左三列 |
| status = ? AND create_time >= ? | 无法使用 | 缺少最左列 user_id |
| user_id = ? AND create_time >= ? | 只能使用 user_id | 跳过 status,第三列无法生效 |
| user_id >= ? AND status = ? AND create_time >= ? | 只能使用 user_id | 第一列就是范围,后续列无法保持有序 |
实际判断时可以通过 EXPLAIN 的 key_len 观察到底用到了几个列。假设 user_id 是 BIGINT,status 是 TINYINT,create_time 是 DATETIME,key_len 会随着使用列的增加而变大。查询 user_id = ? AND status = ? 与查询 user_id = ? 的 key_len 不同,前者多使用了 status 列。
EXPLAIN SELECT id, user_id, status, create_time FROM orders WHERE user_id = 1001 AND status = 2 AND create_time >= '2024-01-01 00:00:00';
这个查询中,user_id 和 status 都是等值条件,优化器可以把它们作为精确匹配;create_time 是范围条件,也能在相同前缀内利用 B+ 树有序性缩小扫描区间。需要注意,如果查询条件是 user_id = ? AND create_time >= ?,则 create_time 无法使用,因为这个索引中 status 相同的条件下才会按 create_time 排序,跳过 status 后 create_time 不是全局有序的。
三、联合索引列顺序的设计原则
设计联合索引顺序时,首先要列出高频查询的过滤条件。通常把等值查询多、过滤性好的列放在前面,把范围查询列放在后面。例如订单系统中,用户频繁执行的是根据 userId 查某个状态的订单并按时间排序,那么较优索引是 (user_id, status, create_time),而不是 (create_time, status, user_id)。
SELECT id, order_no, amount FROM orders WHERE user_id = ? AND status = ? ORDER BY create_time DESC LIMIT 20;
上述 SQL 在索引 (user_id, status, create_time) 上执行时,可以先利用前两列等值定位到连续区间,再按第三列排序,天然满足 ORDER BY create_time DESC,不需要额外 filesort。若把 create_time 放到索引最左,由于查询总是先按 user_id 过滤,优化器很难利用该索引,而且排序成本也更高。
另一个常见设计点是覆盖索引。如果查询只需要 user_id、status、create_time 三个字段,那么索引 (user_id, status, create_time) 本身就是覆盖索引,查询可以只扫描索引,不回表。此时即使 status 被跳过,索引仍然可能被选择用于 user_id 过滤和覆盖扫描,只是扫描范围会变大。因此,判断联合索引是否生效,除了是否回表,还要看扫描行数和过滤效果。
还需要注意区分度与查询模式的平衡。选择性高的列放在前面能更快缩小范围,但如果绝大多数查询都先以某列作为等值条件,即使该列区分度不是最高,也应将它放在联合索引最左。例如查询用户的行为日志,通常以 user_id 开头,那么索引最左应是 user_id,而不是高基数的 event_id。
四、常见误区与排查方法
误区一:认为联合索引中每一列都能独立使用。实际上,联合索引只对最左连续前缀生效。单独的 status 索引、create_time 索引不会因为联合索引存在而自动获得。若业务上经常单独按 status 查询,需要额外创建单列索引或调整联合索引顺序。
误区二:将 LIKE '%keyword' 与最左前缀混为一谈。B+ 树索引可以利用字符串的前缀有序性,因此 LIKE 'keyword%' 通常能走索引,但 LIKE '%keyword' 或 LIKE '%keyword%' 无法利用有序结构。类似地,对索引列使用函数、进行隐式类型转换,也会破坏索引键值与查询值之间的直接比较,导致无法使用索引。
EXPLAIN SELECT * FROM orders WHERE user_id = 1001 AND DATE(create_time) = '2024-01-01';
上面的查询中,create_time 被 DATE() 函数包裹,索引中的原始时间值无法直接与函数结果匹配,优化器通常只使用 user_id 部分。更优写法是改成范围条件:
SELECT * FROM orders WHERE user_id = 1001 AND create_time >= '2024-01-01 00:00:00' AND create_time < '2024-01-02 00:00:00';
排查索引问题时,首先看 EXPLAIN 结果中的 type、key、key_len 和 rows。type=ALL 表示全表扫描;key 为空表示没有选择索引;key_len 可以判断实际用到了几列;rows 反映预估扫描行数。对于复杂查询,还可以通过 OPTIMIZER TRACE 查看优化器是否因为回表成本过高而放弃索引。联合索引顺序的调整,最终要结合实际查询比例、数据分布和响应时间要求来验证。
总结起来,联合索引的顺序不是简单按选择性高低排列,而是要根据实际查询模式,把等值过滤列放在前面,把范围列和排序列放在后面。最左前缀原则的实质是 B+ 树多列排序规则,理解了存储结构,设计联合索引时就能更准确地预判执行计划。