最直接有效的累加方式是使用窗口函数SUM() OVER(),需指定PARTITION BY和ORDER BY,漏掉ORDER BY或误用PARTITION BY会导致结果错误;MySQL 8.0+、PostgreSQL 9.4+等支持,旧版MySQL需用应用层累加替代。

用窗口函数 SUM() OVER() 直接累加
最直接有效的方式就是用窗口函数,它能在不改变原行数的前提下,按指定顺序逐行累加。关键不是 GROUP BY,而是定义好分区和排序逻辑。
常见错误是漏掉 ORDER BY 子句——没有排序,累计就失去意义,结果可能随机或重复;另一个坑是误写成 SUM(sales) OVER (PARTITION BY product_id),这只会返回每个商品的总销售额(单值),不是逐笔累计。
典型写法:
SELECT
product_id,
sale_date,
amount,
SUM(amount) OVER (
PARTITION BY product_id
ORDER BY sale_date, id
) AS cumulative_amount
FROM sales;
-
PARTITION BY product_id确保只在同商品内累计 -
ORDER BY sale_date, id保证时间先后,id是防时间相同时序不确定的兜底 - 如果销售记录有严格唯一时间戳,
id可省略;但生产环境建议保留
MySQL 8.0+ 和 PostgreSQL 都支持,但旧版 MySQL 不行
窗口函数在 MySQL 8.0 才正式引入,如果你用的是 MySQL 5.7 或更早版本,SUM() OVER() 会直接报错:ERROR 1064: You have an error in your SQL syntax。PostgreSQL 9.4+、SQL Server 2005+、Oracle 从很早就支持。
替代方案只能靠自连接或变量,但都更脆弱:
- 自连接方式性能差,数据量稍大(比如单商品超万条)就明显变慢
- 用户变量(如
@cum := @cum + amount)在 MySQL 中依赖执行顺序,官方已明确不保证稳定性,高并发或优化器改写后容易出错 - 如果必须兼容老 MySQL,优先考虑应用层累加,而非硬写 SQL
累计值要“截至当前行”,不是“截至当天”
很多人想算“每个商品每天的累计销售额”,结果写出 ORDER BY DATE(sale_date),导致同一天多笔销售共享同一个累计值——这是错的。窗口函数的 ORDER BY 是针对每一行,不是按日期分组。
正确做法分两步:
- 先用窗口函数算出每笔订单的累计值(按真实时间戳逐行)
- 再外层按日期聚合,取当天最后一笔的累计值,例如:
MAX(cumulative_amount) OVER (PARTITION BY product_id, DATE(sale_date)) - 或者用子查询 +
ROW_NUMBER()找出每日最后一条记录
别试图在一个 OVER 里同时处理“按日分组”和“行级累计”,语义冲突,结果不可控。
注意 NULL 和金额符号对累计的影响
amount 字段如果有 NULL,SUM() 会自动跳过,不会中断累计,这点没问题。但要注意负值销售(退货)——它们会真实参与计算,导致累计值下降。这不是 bug,是预期行为。
如果业务上要求“只累计正向销售”,得提前过滤:
WHERE amount > 0
或者用条件聚合:
SUM(CASE WHEN amount > 0 THEN amount ELSE 0 END) OVER (...)
但后者会让退货记录的累计值“卡住不动”,需确认是否符合业务口径。实际中,很多团队发现退货也该体现在累计趋势里,只是后续要叠加分析指标(如净销售额)。
窗口函数本身不难,难的是想清楚“累计”到底对应哪一层业务含义:是按订单粒度?按天?是否含退货?这些一旦定错,后面所有报表都偏移。

















