财务报表中最常见的两个需求就是算总和、算平均,比如某个月的总收入、某个部门的平均费用。SQL 里的聚合函数 SUM() 和 AVG() 就是为这类场景设计的,但很多人写出来的查询结果和手工对账对不上,问题多半出在对 NULL 值的理解、分组条件遗漏或者浮点精度上。先理清这两个函数的行为,再结合实际表结构来写查询,可以少踩很多坑。

一、SUM() 和 AVG() 的基础行为与常见误区
SUM() 用于计算指定列中所有非 NULL 数值的总和,如果该列没有非 NULL 值,则返回 NULL。AVG() 计算的是非 NULL 数值的平均值,而不是把所有行数当作分母。这是最容易出错的地方:一张费用表里有 100 条记录,其中 5 条金额字段是 NULL,那么 AVG(amount) 只会对 95 条有效金额求平均,而不是除以 100。如果业务上需要把 NULL 当作 0 参与平均,必须先用 COALESCE(amount, 0) 做处理,否则平均值会被高估。
这两个函数的语法非常直接:SELECT SUM(amount) AS total_amount FROM transactions; 或 SELECT AVG(amount) AS avg_amount FROM transactions;。在没有 GROUP BY 的情况下,它们会把整张表压缩成一行汇总结果。配合 WHERE 条件可以限定统计范围,例如只统计已入账的记录:SELECT SUM(amount) FROM transactions WHERE status = 'posted';。需要注意的是,SUM() 和 AVG() 只能作用于数值类型列,如果字段是字符串类型但存了数字,需要先做类型转换。
另一个常见误区是混合不同粒度数据时直接求平均。比如一张表同时记录了每日小计和每月总计,直接对金额列求 AVG 会把小计和总计放在一起平均,结果毫无意义。正确的做法是先按粒度分开,或者只对明细行进行统计。财务报表里的总和与平均值通常都需要明确统计范围,不能盲目套用函数。
二、在财务报表中按科目、月份和部门分组统计
真实财务数据很少只用一次 SUM 就能满足需求,更多时候需要按维度拆分。GROUP BY 可以配合 SUM 和 AVG 生成按科目、月份、部门等维度的汇总报表。例如有一张 journal_entries 表,包含 account_code、entry_date、debit、credit 和 department 字段,要统计每个科目的借方发生额和贷方发生额,可以这样写:
SELECT account_code,
SUM(debit) AS total_debit,
SUM(credit) AS total_credit
FROM journal_entries
WHERE entry_date BETWEEN '2024-01-01' AND '2024-12-31'
GROUP BY account_code
ORDER BY account_code;
如果要进一步按月份汇总,可以在 GROUP BY 中加入月份表达式。不同数据库的日期函数略有差异,MySQL 中可以用 DATE_FORMAT(entry_date, '%Y-%m'),PostgreSQL 中可以用 to_char(entry_date, 'YYYY-MM')。以标准 SQL 风格为例,可以先在子查询中提取月份再分组:
SELECT account_code,
EXTRACT(YEAR FROM entry_date) AS year,
EXTRACT(MONTH FROM entry_date) AS month,
SUM(debit) AS total_debit,
AVG(debit) AS avg_monthly_debit
FROM journal_entries
WHERE entry_date BETWEEN '2024-01-01' AND '2024-12-31'
GROUP BY account_code,
EXTRACT(YEAR FROM entry_date),
EXTRACT(MONTH FROM entry_date)
ORDER BY account_code, year, month;
部门维度的统计也是同样思路。如果需要同时看每个部门每月的平均费用,GROUP BY 后面跟上部门、月份两列即可。AVG 在这里计算的是该分组内所有非 NULL 明细金额的平均值,而不是多个分组平均值的平均。如果有些月份没有数据,该月份不会出现在结果中,这一点在报表展示时需要提前用日期维度表补齐。
条件聚合在财务报表中也非常实用。比如要在一个查询中同时得到总收入、总支出和净额,不必写三个子查询,可以直接用 CASE WHEN 配合 SUM:
SELECT SUM(CASE WHEN entry_type = 'income' THEN amount ELSE 0 END) AS total_income,
SUM(CASE WHEN entry_type = 'expense' THEN amount ELSE 0 END) AS total_expense,
SUM(CASE WHEN entry_type = 'income' THEN amount ELSE 0 END)
- SUM(CASE WHEN entry_type = 'expense' THEN amount ELSE 0 END) AS net_profit
FROM transactions
WHERE entry_date BETWEEN '2024-01-01' AND '2024-12-31';
这种写法把过滤条件放进聚合函数内部,可以避免多次扫描表,也方便后续调整统计口径。需要注意的是,ELSE 0 是必需的,否则 SUM 会忽略不符合条件的行,结果可能变成 NULL。
三、用窗口函数计算移动平均和累计余额
除了常规的分组汇总,财务报表中经常需要计算累计余额和移动平均。例如银行流水表中,每笔交易后需要知道账户余额,余额就是前面所有交易金额的累计和。标准 SQL 中可以用窗口函数 SUM() OVER (ORDER BY ...) 实现:
SELECT transaction_id,
entry_date,
amount,
SUM(amount) OVER (ORDER BY entry_date, transaction_id) AS running_balance
FROM bank_transactions
WHERE account_id = 1001
ORDER BY entry_date, transaction_id;
移动平均则可以用 AVG() OVER (ORDER BY ... ROWS BETWEEN ...) 来指定窗口范围。比如要计算最近 5 笔交易的平均金额,可以写成 AVG(amount) OVER (ORDER BY entry_date ROWS BETWEEN 4 PRECEDING AND CURRENT ROW)。如果数据库不支持窗口函数,也可以用自连接或相关子查询实现,但性能通常不如窗口函数。
窗口函数与 GROUP BY 的关键区别在于,GROUP BY 会把每组压成一行,而窗口函数保留每一行明细,同时附加聚合结果。这在制作带有累计列、移动平均列的财务报表时非常有用,报表中往往需要同时看到单笔明细和累计指标。
四、保证金额精度和查询性能的实践建议
财务金额对精度要求极高,使用 FLOAT 或 DOUBLE 类型存储金额会导致二进制浮点误差,汇总后的总和可能出现小数点后很多位的尾差。正确的做法是使用 DECIMAL 或 NUMERIC 类型,例如 DECIMAL(18,2) 表示最多 18 位数字,其中 2 位小数。这样 SUM 和 AVG 的结果在绝大部分数据库中会保持精确的十进制运算,不会出现 0.1 + 0.2 = 0.30000000000000004 这样的问题。
对于 NULL 值的处理,可以在表设计时给金额字段设置默认值 0 并加上 NOT NULL 约束,从源头避免 NULL 带来的歧义。如果历史数据已经有 NULL,查询时使用 COALESCE(amount, 0) 包裹金额字段再参与聚合。不过要注意,如果业务上 NULL 代表未知而不是 0,直接替换成 0 可能会扭曲平均值,需要根据具体业务规则判断。
性能方面,包含 GROUP BY 的聚合查询应当为分组列建立索引。比如经常按 entry_date 和 account_code 分组,就应该在这两个列上建立复合索引。但要避免在大表上对未索引的字符串列做分组,这会触发全表扫描和临时表。如果报表只关心汇总数据,可以考虑创建物化视图或汇总表,定期从明细表更新,减少实时查询压力。另外,条件聚合中的 CASE WHEN 表达式虽然灵活,但如果条件很多且数据量巨大,可能需要评估是否拆分查询更高效。
最后提醒一点,SUM 和 AVG 返回的类型可能随数据库和输入类型不同而变化,例如在 PostgreSQL 中 AVG(int_column) 返回 numeric,在 MySQL 中 AVG(int_column) 返回 decimal,但有些数据库可能返回浮点。为确保结果稳定,可以显式转换:AVG(CAST(amount AS DECIMAL(18,2)))。这样在程序端接收数据时不会出现类型不一致的问题。
把这些细节处理好,SUM 和 AVG 就能成为财务报表计算中可靠的工具。无论是简单的月度合计,还是复杂的部门分摊和累计余额,只要理解了函数对 NULL 和分组的处理逻辑,配合合适的数据类型和索引,写出的查询就能既准确又高效。