3.3.3 组合索引

组合索引(Composite Index,又称多列索引)是 MySQL 中最强大的查询优化手段之一。理解其执行流程,尤其是最左前缀原则范围截断,是编写高性能 SQL 的根本。


🧱 一、组合索引的 B+ 树结构

假设在 orders 表上创建了组合索引 idx_user_status_time (user_id, status, create_time)

  • 这是一个二级索引(非聚簇索引),单独一棵 B+ 树。
  • 排序规则:索引键按 (user_id, status, create_time) 的顺序依次排序。
    • 首先按 user_id 排序。
    • user_id 相同则按 status 排序。
    • status 相同再按 create_time 排序。
  • 叶子节点存储:(user_id, status, create_time) + 对应的 主键值(假设主键是 id)。
  • 内部节点存储索引键值及子页指针,用于导航。

这种排序方式决定了查询必须从最左列开始才能高效利用索引。


🎯 二、最左前缀原则的执行流程

最左前缀原则:只有当查询条件包含索引最左列(或多列前缀)时,该索引才能被用来进行快速查找。

1. 等值查询匹配全部列
SELECT * FROM orders 
WHERE user_id = 10 AND status = 'paid' AND create_time = '2025-01-01';
  • 执行步骤

    1. idx_user_status_time 的根页出发,进行 B+ 树导航。
    2. 定位时,比较顺序是:先比较 user_id,再比较 status,最后比较 create_time
    3. 由于三个列都是等值条件,且顺序完全匹配索引定义,能精确定位到唯一符合条件的叶子记录(或范围起始点)。
    4. 然后向前扫描,直到键值发生变化。
    5. 通过叶子记录获取主键 id,再到聚簇索引回表获取整行数据。
  • key_len 显示索引被全部使用。

2. 仅匹配最左列
SELECT * FROM orders WHERE user_id = 10;
  • 虽然查询只提供了 user_id,但索引最左列就是 user_id,因此可以定位到 user_id=10 的起始位置
  • 然后顺序扫描所有 user_id=10 的叶子记录,再回表。
  • 此时 key_len 只显示 user_id 列的长度。
3. 匹配最左两列
SELECT * FROM orders WHERE user_id = 10 AND status = 'paid';
  • 可以精确定位到 (10, 'paid') 的起始点,然后顺序扫描。
  • key_len 包含 user_idstatus 两列的长度。
4. 跳过最左列 —— 索引失效
SELECT * FROM orders WHERE status = 'paid';
  • 因为索引是先按 user_id 排序的,status 在整个索引中是无序的(仅当 user_id 相同时才有序)。
  • 优化器无法利用该索引进行定位,只能全表扫描或选择其他索引。
  • 如果强制使用此索引,则会退化为全索引扫描type: index),效率仍然低下。

🚧 三、范围条件截断后续列

对于组合索引 (A, B, C),如果某列使用了范围条件(>, <, BETWEEN, LIKE 'prefix%' 等),则该列之后的列无法再用于精确定位,但仍可用于 ICP 过滤或覆盖索引。

SELECT * FROM orders 
WHERE user_id = 10 AND status = 'paid' AND create_time > '2025-01-01';
  • user_idstatus 是等值,可以精确定位到 (10, 'paid') 的起始点。
  • create_time > '2025-01-01' 是范围条件,在 B+ 树中,紧接在等值部分之后,索引按 create_time 排序,因此可以直接跳到 create_time 大于 ‘2025-01-01’ 的位置,然后顺序扫描。
  • 截断效应create_time 之后的列(如果有)就无序了,无法用于缩小扫描范围。但本例中 create_time 是最后一列,无影响。

执行过程:

  1. 定位到第一个满足 (user_id=10, status='paid', create_time > '2025-01-01') 的记录。
  2. 沿着叶子链表顺序扫描,直到 (user_id, status) 发生变化(即 user_id 不再是 10 或 status 不再是 ‘paid’)。
  3. 扫描过程中,每个符合条件的叶子记录都会获取主键,进行回表。

key_len 包含 user_idstatus 的长度,create_time 部分不会体现在 key_len 中,但能在 Extra 看到 Using index condition


⚡ 四、索引条件下推(ICP)在组合索引中的应用

WHERE 条件无法完全用来缩小扫描范围时,ICP 可以在索引层就进行过滤,减少回表。

SELECT * FROM orders 
WHERE user_id = 10 AND create_time > '2025-01-01';

索引是 (user_id, status, create_time)。由于跳过了 statuscreate_time 在索引中是无序的(相对于 user_id 相同时并不直接有序,因为中间缺了 status)。此时:

  • 只能利用 user_id = 10 定位,然后扫描所有 user_id=10 的叶子记录(范围较大)。
  • 没有 ICP:每一条 user_id=10 的记录都会回表,取出整行后再检查 create_time > '2025-01-01'
  • 有 ICP(默认):在扫描叶子记录时,直接利用索引中的 create_time 值过滤,只有满足条件的才回表。这就是 Using index condition

执行流程

  1. 定位到 user_id=10 的起始叶子节点。
  2. 顺序遍历这些叶子记录,检查 create_time > '2025-01-01'
  3. 符合条件的记录获取主键,回表得到完整行;不符合的直接跳过。

✨ 五、覆盖索引:组合索引的终极优化

如果查询列完全包含在组合索引中,则可避免回表,直接从索引返回结果。

SELECT user_id, status, create_time 
FROM orders 
WHERE user_id = 10 AND status = 'paid';

索引 (user_id, status, create_time) 已经包含了这三个列,查询不需要表中的其他列,因此:

  • 定位到 (10, 'paid') 后,直接顺序扫描叶子节点,读取键值并返回。
  • EXPLAIN 显示 Using index,无回表,性能极高。

🔄 六、组合索引与 ORDER BY 和 GROUP BY

组合索引的顺序还可以优化排序和分组,避免 filesort。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码农颜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值