SQL聚合函数是处理数据汇总统计的核心工具,常用函数包括COUNT、SUM、AVG、MAX、MIN等,在实际业务的数据统计、报表生成场景中应用十分广泛。不过很多开发者在使用这些函数时,没有注重优化逻辑,导致查询性能跟不上数据量增长,下面我们通过实践案例来讲解优化方法。

一、聚合函数统计查询的常见性能问题
先来看一个未优化的统计查询案例,假设我们有一张订单表order_info,包含订单ID、用户ID、订单金额、订单状态、创建时间等字段,现在需要统计2024年已支付订单的总金额和订单数量,常见的写法如下:
SELECT
COUNT(order_id) AS paid_order_count,
SUM(order_amount) AS total_paid_amount
FROM order_info
WHERE order_status = 'paid'
AND create_time >= '2024-01-01 00:00:00'
AND create_time < '2025-01-01 00:00:00';如果这张表的数据量达到千万级,且上面没有合适的索引,这条查询会触发全表扫描,逐行判断条件后再计算聚合结果,耗时可能达到数十秒甚至几分钟。常见的问题包括:没有命中索引导致全表扫描、聚合前未过滤无效数据、重复计算相同的聚合逻辑等。
二、聚合函数统计查询的优化方法
1. 合理创建索引减少扫描范围
针对上面的统计需求,我们可以创建组合索引idx_status_time(order_status, create_time),这样查询时可以先通过order_status过滤出已支付订单,再通过create_time范围过滤出2024年的订单,只需要扫描符合条件的数据行,不需要全表扫描。创建索引的语句如下:
-- 创建组合索引,注意字段顺序和查询条件顺序匹配 CREATE INDEX idx_status_time ON order_info(order_status, create_time);
2. 先过滤再聚合,减少计算量
一定要把数据过滤条件放在WHERE子句中,而不是先聚合再过滤。很多开发者会写成先GROUP BY再HAVING过滤,这样会先计算所有分组的聚合结果,再过滤掉不需要的分组,徒增计算量。比如要统计每个用户的已支付订单总金额,正确的写法应该是:
-- 先过滤再聚合,效率更高
SELECT
user_id,
SUM(order_amount) AS user_total_paid
FROM order_info
WHERE order_status = 'paid'
GROUP BY user_id;
-- 错误写法:先聚合再过滤,会计算所有状态的订单聚合结果
SELECT
user_id,
SUM(order_amount) AS user_total_paid
FROM order_info
GROUP BY user_id
HAVING order_status = 'paid'; -- 这种写法本身语法错误,仅为示例错误逻辑3. 避免不必要的DISTINCT使用
COUNT函数使用时要区分COUNT(*)和COUNT(字段)的区别,COUNT(*)会统计所有行数,包括NULL值,而COUNT(字段)会忽略该字段为NULL的行。如果不是需要去重统计,不要随意加DISTINCT,因为DISTINCT会触发排序操作,增加额外开销。比如统计已支付订单的不同用户数量,才需要用COUNT(DISTINCT user_id),否则直接用COUNT(user_id)即可。
三、数据汇总中的高级实践技巧
1. 使用GROUP BY的ROLLUP扩展实现多层级汇总
如果需要同时统计各用户的订单金额、所有用户的总订单金额,不需要写多条查询分别统计,可以用ROLLUP扩展一次性得到结果:
SELECT
COALESCE(user_id, 'ALL_USER') AS user_id,
SUM(order_amount) AS total_paid
FROM order_info
WHERE order_status = 'paid'
GROUP BY ROLLUP(user_id);执行结果会包含每个用户的汇总数据,以及一行所有用户的总汇总数据,user_id为ALL_USER的就是全局汇总结果,减少多次查询的开销。
2. 结合窗口函数实现聚合与明细共存
如果需要在展示订单明细的同时,展示该用户的总订单金额、平均订单金额,不需要子查询多次聚合,可以用窗口函数配合聚合函数使用:
SELECT
order_id,
user_id,
order_amount,
SUM(order_amount) OVER(PARTITION BY user_id) AS user_total_paid,
AVG(order_amount) OVER(PARTITION BY user_id) AS user_avg_paid
FROM order_info
WHERE order_status = 'paid'
AND create_time >= '2024-01-01 00:00:00'
AND create_time < '2025-01-01 00:00:00';这种写法可以在一次查询中同时得到明细和聚合统计结果,避免多次扫描表数据。
3. 超大表统计的分区优化
如果订单表数据量达到亿级,可以按创建时间做分区,比如按季度分区,统计2024年数据时,只需要扫描2024年对应的几个分区,不需要扫描全表。创建分区表的示例如下:
-- 按创建时间季度分区示例(以MySQL为例)
CREATE TABLE order_info (
order_id BIGINT PRIMARY KEY,
user_id BIGINT,
order_amount DECIMAL(10,2),
order_status VARCHAR(20),
create_time DATETIME
) PARTITION BY RANGE (TO_DAYS(create_time)) (
PARTITION p2024q1 VALUES LESS THAN (TO_DAYS('2024-04-01')),
PARTITION p2024q2 VALUES LESS THAN (TO_DAYS('2024-07-01')),
PARTITION p2024q3 VALUES LESS THAN (TO_DAYS('2024-10-01')),
PARTITION p2024q4 VALUES LESS THAN (TO_DAYS('2025-01-01')),
PARTITION p_other VALUES LESS THAN MAXVALUE
);四、聚合查询的注意事项
首先要注意聚合函数的NULL值处理,SUM、AVG函数在计算时会忽略NULL值,而COUNT(*)不会,所以统计时如果字段可能存在NULL,要明确统计逻辑。其次,尽量避免在聚合函数内部做复杂计算,比如SUM(order_amount * 0.9)这种写法,可以先过滤出需要的数据,或者提前在表中冗余计算后的字段,减少运行时的计算开销。最后,定期分析表的统计信息,让数据库优化器能选择最优的执行计划,保证聚合查询的效率。