在大数据量的业务表中执行分组聚合统计查询时,很多开发者会遇到查询耗时过长的问题,这往往和索引设计不合理有直接关系。分组聚合操作需要先对数据进行分组,再对每组数据做聚合计算,如果缺少合适的索引,数据库会先全表扫描数据,再创建临时表存储分组结果,最后进行排序和聚合计算,整个过程的资源消耗会随数据量增长快速上升。

分组聚合查询的性能瓶颈分析
要优化分组聚合的索引,首先需要明确这类查询的执行逻辑。以常见的统计订单表中各地区的订单总金额为例,基础查询语句如下:
-- 统计各地区订单总金额 SELECT region, SUM(order_amount) AS total_amount FROM order_table WHERE create_time >= '2024-01-01' GROUP BY region ORDER BY total_amount DESC;
如果order_table表没有针对region、create_time、order_amount的合适索引,数据库执行该查询的流程通常是:
- 全表扫描所有订单记录,过滤出
create_time符合条件的行 - 将过滤后的结果按
region字段分组,生成临时表存储分组数据 - 对临时表中的每组数据计算
order_amount的总和 - 最后对聚合结果按
total_amount排序返回
当表数据量达到千万级甚至更高时,全表扫描和临时表的创建排序会成为主要的性能瓶颈,此时合理的索引设计可以直接跳过全表扫描,甚至避免临时表的生成。
分组聚合索引优化的核心方法
1. 设计联合索引匹配查询条件
联合索引的顺序需要遵循“最左前缀原则”,同时优先把过滤性强的字段放在前面,分组字段紧跟其后,最后包含聚合计算需要的字段,形成覆盖索引,避免回表操作。
针对上面的订单统计查询,合适的联合索引应该包含create_time(过滤条件)、region(分组字段)、order_amount(聚合字段),创建索引的语句如下:
-- 创建联合覆盖索引 CREATE INDEX idx_order_stat ON order_table(create_time, region, order_amount);
这个索引的叶子节点已经按create_time排序,相同create_time下按region排序,同时存储了order_amount的值。执行查询时,数据库可以直接通过索引快速定位到create_time符合条件的索引记录,由于索引本身已经按region有序,不需要额外创建临时表分组,直接按索引顺序遍历就可以完成分组聚合,大幅减少IO和计算开销。
2. 评估索引的过滤性
索引的过滤性指的是索引能够过滤掉多少不符合条件的记录,过滤性越强,索引的效果越好。如果create_time的过滤性很差,比如查询条件覆盖了表中90%的数据,那么即使有索引,数据库也可能选择全表扫描,因为回表的成本比全表扫描更高。
可以通过下面的语句评估字段的过滤性:
-- 评估create_time的过滤性,计算符合条件的记录占总记录的比例
SELECT
COUNT(CASE WHEN create_time >= '2024-01-01' THEN 1 END) AS match_count,
COUNT(*) AS total_count,
COUNT(CASE WHEN create_time >= '2024-01-01' THEN 1 END) / COUNT(*) AS filter_ratio
FROM order_table;
如果filter_ratio大于0.3,说明过滤性较差,此时可以考虑调整查询条件,或者结合其他过滤性强的字段一起创建联合索引。
3. 避免索引失效的场景
即使创建了合适的索引,一些写法也会导致索引无法被使用,常见的有:
- 对索引字段做函数处理,比如
WHERE DATE(create_time) = '2024-01-01',会导致create_time上的索引失效 - 查询条件中使用
OR连接非索引字段,比如WHERE create_time >= '2024-01-01' OR status = 1,如果status没有索引,整个查询可能不会走索引 - 联合索引没有遵循最左前缀原则,比如索引是
(create_time, region, order_amount),查询条件中只用了region和order_amount,无法使用这个索引
优化前后的效果对比
我们在1000万条数据的订单表上测试上面的统计查询,优化前后的执行数据如下:
| 场景 | 执行时间 | 扫描行数 | 是否使用临时表 |
|---|---|---|---|
| 无合适索引 | 12.3秒 | 1000万行 | 是 |
| 创建联合覆盖索引后 | 0.2秒 | 120万行 | 否 |
可以看到优化后查询效率提升了60倍以上,同时避免了临时表的创建,减少了内存和CPU的消耗。
注意事项
索引并不是越多越好,每个索引都会占用额外的存储空间,并且在插入、更新、删除数据时都需要维护索引,会影响写操作的性能。因此分组聚合的索引只需要在高频执行的统计查询上创建,低频的查询可以通过调整查询时间范围、分批统计等方式优化。
另外如果分组字段的基数很高,比如要按用户ID分组统计千万级用户的订单数据,即使有索引,分组操作的成本依然会比较高,此时可以考虑预聚合的方式,比如创建定时任务每天预计算各用户的订单统计数据,存储到汇总表中,查询时直接查汇总表,避免实时分组聚合。