SQL 中累计求和(也叫运行总计、Running Total)的标准做法是窗口函数 SUM() OVER(...):用 ORDER BY 定义累加的时间轴或业务顺序,从第一行加总到当前行,行数不变、明细全留。给 OVER() 加上 PARTITION BY 则把数据切成多个区,在每个区内独立从头累加、跨区不串号。据 OneUptime 的 MySQL 累计求和教程,SUM(amount) OVER (ORDER BY sale_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) 是通用写法;据 ufcn.cn 的实战解析,漏掉 PARTITION BY 会让所有分组被当成一张表累加,漏掉 ORDER BY 则累计失去意义。MySQL 8.0+、PostgreSQL、SQL Server、SQLite 3.25+、Oracle 均支持该语法。
一、累计求和在算什么
累计求和是”按某列排序后,从第一条记录一直加到当前这一条”。它和 GROUP BY 根本不同:聚合会折叠行,窗口函数不折叠——每一行都保留,并在行尾多出一列”截至本行的累计值”。
据 SQL Habit 的 SUM 窗口函数文档,SUM(col) OVER() 把整张表当一个窗口,每行都得到同一个总和;一旦加上 ORDER BY,窗口函数就逐行把前面的值收拢进来,变成递增的累计值。
二、基础写法:全表逐行累加
SELECT order_date, amount,
SUM(amount) OVER (
ORDER BY order_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_total
FROM sales
ORDER BY order_date;
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW 把累加范围钉死在”从分区首行到当前行”。据 OneUptime 对比,带 ORDER BY 时数据库默认框架其实是 RANGE 而非 ROWS——当 ORDER BY 列有重复值时,RANGE 会把所有并列行当成同一边界一次性加进去,而 ROWS 保证每行逐次步进。做严格逐行累计,显式写 ROWS 更稳妥。
三、PARTITION BY 的作用:分区分头累加
PARTITION BY 是窗口函数的”分组但不折叠”开关。它把数据按指定列切成多个区,窗口函数在每个区内独立计算、计算起点在每个区第一行重启,跨区互不干扰。据 Iconics 官方窗口函数文档,PARTITION BY 把结果集划分成若干分区,窗口函数分别作用于每个分区,且”对每一个分区计算都会重新开始”。
SELECT customer_id, order_date, amount,
SUM(amount) OVER (
PARTITION BY customer_id
ORDER BY order_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS cum_per_customer
FROM sales;
上例中,每个客户从第一次下单起独立累计,北京客户的第 1 笔不会加上上海客户的记录。ufcn.cn 特别提醒:漏掉 PARTITION BY 时,OVER(ORDER BY ...) 会把整张表当一个组,导致各分组首尾相接错误累加——这是最常见的 bug。
四、ORDER BY 缺省与陷阱
| 写法 | 结果 |
|---|---|
SUM(col) OVER(ORDER BY k) |
按 k 排序,逐行累加(正确累计) |
SUM(col) OVER(PARTITION BY g) |
只分区不排序,每行得到”本组总和”,不是累计 |
SUM(col) OVER() |
全表一列同值的总额,非累计 |
据 ufcn.cn 与 CSDN 的实战文章,三条铁律必须守住:一、累计必须带 ORDER BY,否则顺序不确定甚至报错(PostgreSQL 14+ 强制要求);二、漏 PARTITION BY 会变成全表累计;三、ORDER BY 列有重复时要补二级排序键(如 id)保证结果稳定,避免并列行的累计值随机。
五、灵活控制累加范围
ROWS/RANGE 子句可精确圈定”当前行前后的范围”,实现滑动窗口:
-- 最近 3 条(含当前)滚动累计
SUM(amount) OVER (
PARTITION BY customer_id
ORDER BY order_date
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS rolling_3
据 OneUptime 文档,PRECEDING/FOLLOWING 配 ROWS 表示物理行偏移,RANGE 表示逻辑值范围;UNBOUNDED PRECEDING 只能用在起点。想要”近 N 条”而非”从开头”,就用带数字的 PRECEDING。
六、累计占比:累计求和的高频衍生
把累计值除以分区总额,即得累计百分比,常用于帕累托分析:
SELECT order_date, amount,
SUM(amount) OVER (ORDER BY order_date) AS cum,
ROUND(
100.0 * SUM(amount) OVER (ORDER BY order_date)
/ SUM(amount) OVER (),
1
) AS pct_of_total
FROM sales;
SQL Habit 指出,空 OVER() 在这里起到”全表总额分母”的作用——一个表达式同时拿到累计与总额,比 GROUP BY 再 JOIN 简洁得多。
七、旧版本兜底写法
MySQL 5.7 及更早不支持窗口函数,需用用户变量或关联子查询(据 OneUptime 旧版教程):
-- MySQL 5.7 用户变量写法
SET @cum := 0;
SELECT order_date, amount, @cum := @cum + amount AS running_total
FROM sales ORDER BY order_date;
关联子查询写法为 SELECT order_date, amount, (SELECT SUM(amount) FROM sales s2 WHERE s2.order_date <= s1.order_date) AS running_total FROM sales s1,但每行重跑一次,性能明显弱于窗口函数。
八、累计求和执行步骤
- 明确累加依据列(日期、序号),写进
ORDER BY;有重复值补二级键。 - 需要分组独立累计时加
PARTITION BY;全表累计则省略。 - 选
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW做严格逐行累计。 - 需要滚动窗口时用
N PRECEDING限定近 N 条。 - 数据库支持窗口函数直接用
SUM() OVER;旧版回退用户变量或关联子查询。
常见问题(FAQ)
PARTITION BY 和 GROUP BY 冲突吗?
不冲突。GROUP BY 折叠行、每组一行;PARTITION BY 不折叠,只在窗口内分区计算,二者可并存。
为什么累计结果有时几行相同?
ORDER BY 列有并列时默认 RANGE 框架会把并列行一起累加;改成 ROWS 或补唯一排序键即可逐行递增。
只写 SUM() OVER(ORDER BY k) 行吗?
可以,默认即为”首行到当前行”的累计;显式写 ROWS 框架语义更清晰,建议保留。