前言
要理解Loose Index Scan(松散索引扫描) 如何加速 GROUP BY,核心是抓住「索引有序性」和「跳过无效数据、只取分组核心值」这两个关键点——它彻底避开了“遍历所有数据→临时表分组”的低效路径,直接从有序索引中“精准提取”分组结果。
先看「常规 GROUP BY」的低效逻辑(对比更易理解)
假设表 t1 有索引 idx(c1,c2),数据如下(索引是有序存储的,所以实际索引中 c1 的值是连续的):
| c1 | c2 |
|---|---|
| 1 | 10 |
| 1 | 20 |
| 2 | 30 |
| 2 | 40 |
| 3 | 50 |
如果执行 SELECT c1, MIN(c2) FROM t1 GROUP BY c1;,常规方式的执行步骤是:
- 扫描全表(或全索引),读取所有 5 行数据;
- 创建临时表,把
c1=1的两行、c1=2的两行、c1=3的一行分别归集; - 对每个分组计算
MIN(c2); - 返回结果。
这个过程要遍历所有数据,还需要临时表,数据量越大效率越低。
再看「Loose Index Scan」的加速逻辑
因为索引 idx(c1,c2) 是 有序的(BTREE 索引特性),c1 相同的记录在索引中是连续排列的。Loose Index Scan 利用这一点,只读取每个分组的“关键值”,跳过同分组的其他数据:
步骤拆解(还是上面的查询 SELECT c1, MIN(c2) FROM t1 GROUP BY c1;):
- 定位索引中第一个
c1=1的记录,其c2=10——因为索引有序,同组后续的c1=1记录的c2一定≥10,所以MIN(c2)直接确定为 10,无需读取c1=1的下一条记录(c2=20); - 跳过所有
c1=1的剩余记录,直接定位到第一个c1=2的记录,其c2=30——同理,MIN(c2)=30,跳过c1=2的下一条记录(c2=40); - 跳过所有
c1=2的剩余记录,定位到第一个c1=3的记录,其c2=50——MIN(c2)=50; - 直接返回结果,全程只读取了 3 条记录(每个分组 1 条),且无需创建临时表。
核心加速点总结:
| 维度 | 常规 GROUP BY | Loose Index Scan |
|---|---|---|
| 数据读取量 | 遍历所有符合条件的记录 | 仅读取每个分组的“首条关键记录” |
| 临时表 | 必须创建(归集分组) | 无需创建 |
| 计算逻辑 | 先归集所有数据,再算聚合 | 直接从索引首条记录推导聚合结果 |
| 时间复杂度 | O(N)(N 为总记录数) | O(M)(M 为分组数,M << N) |
再举一个带范围条件的例子,更显“松散”的优势
如果查询是 SELECT c1, MIN(c2) FROM t1 WHERE c1 < 3 GROUP BY c1;:
- 常规方式:读取
c1<3的所有 4 条记录(c1=1 的 2 条 + c1=2 的 2 条),临时表分组后计算; - Loose Index Scan:仅读取
c1=1的第一条、c1=2的第一条,共 2 条记录,直接得出结果,跳过了同组的其他 2 条记录。
为什么叫“松散”?
“松散”的核心是——它不“紧密”遍历索引的每一个键值,而是跳过同分组的所有冗余记录,只抓每个分组的“锚点”(首条记录),就像从有序的数组中“跳着找”分组,而非逐个遍历。
关键前提:为什么能“跳着找”?
必须满足「GROUP BY 列是索引最左前缀」+「聚合函数仅 MIN/MAX 且列紧跟分组列」,本质是:
- 索引有序性保证了“同分组的记录连续”,所以首条记录就能确定 MIN/MAX;
- 索引最左前缀保证了“分组列在索引中是最优先的排序维度”,能直接按分组列跳着定位。
如果不满足这些条件(比如用 SUM、GROUP BY 列不是最左前缀),就无法“跳着找”,只能退化为 Tight Index Scan 或常规方式。
简单总结:Loose Index Scan 把 GROUP BY 的执行成本,从「依赖数据总量」降到了「依赖分组数量」,分组数远小于数据量时,加速效果极其显著。
官方文档
https://dev.mysql.com/doc/refman/8.0/en/group-by-optimization.html
956

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



