组合索引(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';
-
执行步骤:
- 从
idx_user_status_time的根页出发,进行 B+ 树导航。 - 定位时,比较顺序是:先比较
user_id,再比较status,最后比较create_time。 - 由于三个列都是等值条件,且顺序完全匹配索引定义,能精确定位到唯一符合条件的叶子记录(或范围起始点)。
- 然后向前扫描,直到键值发生变化。
- 通过叶子记录获取主键
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_id和status两列的长度。
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_id和status是等值,可以精确定位到(10, 'paid')的起始点。create_time > '2025-01-01'是范围条件,在 B+ 树中,紧接在等值部分之后,索引按create_time排序,因此可以直接跳到create_time大于 ‘2025-01-01’ 的位置,然后顺序扫描。- 截断效应:
create_time之后的列(如果有)就无序了,无法用于缩小扫描范围。但本例中create_time是最后一列,无影响。
执行过程:
- 定位到第一个满足
(user_id=10, status='paid', create_time > '2025-01-01')的记录。 - 沿着叶子链表顺序扫描,直到
(user_id, status)发生变化(即user_id不再是 10 或status不再是 ‘paid’)。 - 扫描过程中,每个符合条件的叶子记录都会获取主键,进行回表。
key_len 包含 user_id 和 status 的长度,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)。由于跳过了 status,create_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。
执行流程:
- 定位到
user_id=10的起始叶子节点。 - 顺序遍历这些叶子记录,检查
create_time > '2025-01-01'。 - 符合条件的记录获取主键,回表得到完整行;不符合的直接跳过。
✨ 五、覆盖索引:组合索引的终极优化
如果查询列完全包含在组合索引中,则可避免回表,直接从索引返回结果。
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。

953

被折叠的 条评论
为什么被折叠?



