DB2的SUM、AVG、COUNT是日常统计查询中最常用的三个聚合函数,但很多高级场景下它们的行为与基础用法存在明显差异。如果忽视NULL值处理、DISTINCT去重规则、窗口分区以及分组扩展,统计结果可能会与业务预期产生偏差,甚至导致性能问题。本文围绕这三个函数展开,结合真实SQL示例,说明如何在DB2中实现条件汇总、累计求和、移动平均以及多层次小计。

一、NULL值与DISTINCT:聚合结果为什么总是不对
聚合函数最容易产生误解的地方在于NULL值处理。DB2中,COUNT(*)统计所有行,包括整行全为NULL的记录;COUNT(列名)只统计该列非NULL的行数。SUM和AVG会直接忽略NULL值,但它们返回的结果可能因为忽略NULL而造成平均值基数变小。例如一个销售明细表里,quantity列存在NULL,SUM(quantity)得到的是有效数量的总和,而COUNT(*)减去COUNT(quantity)就是缺失数量记录的条数。如果不区分这两类计数,计算平均数量时用SUM(quantity)/COUNT(*)就会得到偏小的结果。推荐始终明确使用AVG(quantity)而不是手动计算,避免NULL处理遗漏。
DISTINCT用在聚合函数内部时,会先对目标列去重再计算。COUNT(DISTINCT product_id)可以统计出现过多少种产品,SUM(DISTINCT product_id)在逻辑上很少使用,因为产品ID求和通常没有业务含义,但某些场景下需要统计不同数值的唯一和。AVG(DISTINCT price)只对不同的价格做平均,忽略重复价格,这与AVG(price)的含义完全不同。DB2还支持COUNT_BIG函数,返回DECIMAL(31,0)类型,适合超大表行数统计,避免普通INT计数溢出。
-- 创建示例表
CREATE TABLE sales (
sale_id INT NOT NULL,
product_id INT,
quantity INT,
price DECIMAL(10,2)
);
INSERT INTO sales VALUES
(1, 101, 5, 20.00),
(2, 102, NULL, 30.00),
(3, 101, 3, 20.00),
(4, NULL, 2, 15.00);
-- 基础聚合与NULL处理
SELECT
COUNT(*) AS total_rows,
COUNT(quantity) AS non_null_quantity,
SUM(quantity) AS total_quantity,
AVG(quantity) AS avg_quantity,
SUM(price) AS total_price
FROM sales;
-- DISTINCT 去重聚合
SELECT
COUNT(DISTINCT product_id) AS distinct_products,
SUM(DISTINCT product_id) AS sum_distinct_product_id,
AVG(DISTINCT price) AS avg_distinct_price
FROM sales;上述SQL中,SUM(quantity)返回8,因为NULL行被忽略,COUNT(quantity)为3,而COUNT(*)为4。DISTINCT示例中,COUNT(DISTINCT product_id)结果为2,因为101重复出现,NULL product_id不参与计数。SUM(DISTINCT product_id)是101+102=203,AVG(DISTINCT price)只对20.00、30.00、15.00三个不同价格求平均。理解这些细节后,再遇到统计值与直觉不符的问题,可以快速定位是不是NULL或重复值在起作用。
二、窗口函数:让SUM和AVG具备行级计算能力
传统GROUP BY会把结果压缩到分组粒度,丢失明细行信息。DB2支持OLAP窗口函数,可以在保留每一行数据的同时,对分区内的行进行聚合。语法为聚合函数 OVER (PARTITION BY 分组列 ORDER BY 排序列 ROWS/RANGE 窗口范围)。SUM函数配合ORDER BY和ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,可以实现分区内累计求和。例如按部门对员工工资排序后,计算截至当前员工的累计工资,不需要自连接或游标。
AVG的窗口用法非常实用:通过ROWS BETWEEN 2 PRECEDING AND CURRENT ROW可以计算当前行及前两行的移动平均,适合做趋势平滑。COUNT(*) OVER (PARTITION BY dept_id)能直接给出每个部门的员工总数,并出现在每一行上,方便后续计算占比。窗口函数与普通GROUP BY的最大区别在于:窗口函数不减少结果集行数,聚合结果作为新列附加到原有行上。它也不会导致分组内多行合并,因此常用于排名、累计、环比等分析。
SELECT
emp_id,
dept_id,
salary,
SUM(salary) OVER (PARTITION BY dept_id ORDER BY emp_id
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_salary,
AVG(salary) OVER (PARTITION BY dept_id ORDER BY emp_id
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS moving_avg_3,
COUNT(*) OVER (PARTITION BY dept_id) AS dept_emp_count
FROM employee
ORDER BY dept_id, emp_id;在上例中,running_salary按emp_id顺序累计,每一行都能看到当前累计值;moving_avg_3是最近三行的工资均值;dept_emp_count则显示部门总人数。需要注意窗口范围子句如果省略,默认是RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,在有重复排序键时可能与ROWS行为不同。建议显式写出ROWS范围,确保结果可预测。
三、条件聚合与多层级小计:减少扫描次数的实战技巧
条件聚合是指将CASE WHEN表达式作为聚合函数的参数,从而在一次扫描中同时得到多个条件下的统计值。例如SUM(CASE WHEN salary > 10000 THEN 1 ELSE 0 END)可以统计高薪员工数量,AVG(CASE WHEN salary > 10000 THEN salary END)只对高薪员工求平均。CASE WHEN返回NULL时,SUM、AVG、COUNT会自动忽略,因此不需要额外WHERE过滤。这种写法能避免针对每个条件执行多次查询,大幅减少I/O。
在报表场景中,往往需要同时输出明细汇总、部门小计和总计。如果使用多次GROUP BY再UNION ALL,代码冗长且需要重复扫描。DB2提供GROUPING SETS、ROLLUP和CUBE三种分组扩展语法。GROUPING SETS可以指定多个分组组合,一次查询返回多个层级的结果。ROLLUP会产生从最细粒度到总计的逐级汇总,CUBE则生成所有维度组合的笛卡尔积。配合GROUPING函数可以判断当前行是否属于小计行,便于前端标识。
-- 条件聚合
SELECT
dept_id,
COUNT(*) AS total_emp,
SUM(CASE WHEN salary > 10000 THEN 1 ELSE 0 END) AS high_salary_count,
AVG(CASE WHEN salary > 10000 THEN salary END) AS avg_high_salary,
SUM(CASE WHEN job_id = 'MANAGER' THEN salary END) AS manager_salary_total
FROM employee
GROUP BY dept_id;
-- GROUPING SETS 多层级小计
SELECT
dept_id,
job_id,
SUM(salary) AS total_salary,
COUNT(*) AS emp_count,
GROUPING(dept_id) AS grp_dept,
GROUPING(job_id) AS grp_job
FROM employee
GROUP BY GROUPING SETS (
(dept_id, job_id),
(dept_id),
()
)
ORDER BY dept_id, job_id;条件聚合SQL中,高薪员工数量和平均工资仅对salary大于10000的记录计算,其余行被忽略但保留在其他列中。GROUPING SETS示例返回部门与岗位的明细行、部门小计行以及总计行,GROUPING(dept_id)返回1表示该行不是按dept_id分组,而是汇总行。与UNION ALL相比,这种写法让优化器只扫描一次employee表,并通过排序和哈希聚合完成多层级统计,执行计划更加高效。不过ROLLUP和CUBE生成的组合数量可能很大,需要评估维度数量和结果集膨胀。
还需要注意,窗口函数与GROUP BY分组扩展可以结合使用,但执行顺序不同。DB2会先执行FROM、WHERE、GROUP BY和HAVING,之后才计算窗口函数。因此窗口聚合不能直接引用SELECT别名,也不应该在WHERE中过滤窗口函数结果。如果需要按累计值过滤,应当将窗口查询作为子查询或CTE,再在外层使用WHERE条件。理解了这些原理后,就能在复杂统计场景中灵活组合SUM、AVG、COUNT,写出高效且可维护的DB2查询。