MySQL 8.0+ 推荐用窗口函数 SUM() OVER (PARTITION BY ... ORDER BY ...) 实现保序累计求和,须显式指定分区与排序;低版本需用变量模拟但风险高;注意 NULL、重复值、时区及图表补缺等生产细节。

MySQL 8.0+ 用 ROW_NUMBER() 和窗口函数直接累加
累计求和本质是“按某顺序逐行叠加”,不是简单 GROUP BY 能解决的——GROUP BY 是分组聚合,会丢掉行序;而累计求和必须保序。MySQL 8.0+ 支持窗口函数,这是最干净的做法。
常见错误是写成 SUM(value) OVER (ORDER BY date) 却忘了分区:如果数据含多用户、多品类,不加 PARTITION BY user_id 就会跨组累加,结果全乱。
- 必须显式指定排序字段,比如
ORDER BY created_at,否则结果不可靠 - 多维度累计(如每个用户每日销售额的累计)要写成
SUM(sales) OVER (PARTITION BY user_id ORDER BY created_at) - 注意
NULL值:若sales有NULL,SUM()会跳过它,但行序不变——这通常符合预期
SELECT date, sales, SUM(sales) OVER (ORDER BY date) AS cum_sales FROM daily_sales;
低版本 MySQL(5.7 及以下)用变量模拟累加,但要注意初始化时机
变量方案在 5.7 是主流 workaround,但极易出错——核心问题在于 MySQL 不保证 SELECT 中变量赋值的执行顺序,尤其当语句带 ORDER BY 或涉及连接时。
典型错误现象:cum_sales 数值跳跃、重复、归零,甚至不同执行结果不一致。
- 必须把排序逻辑塞进子查询里,再在外层用变量,不能直接在带
ORDER BY的主查询中混用变量赋值 - 变量初始化必须和数据读取在同一
SELECT中完成,推荐写法:@cum := 0放在JOIN或(SELECT @cum := 0) AS _子句里 - 避免在 WHERE 或 HAVING 中引用累加变量,MySQL 可能提前剪枝
SELECT date, sales, @cum := @cum + sales AS cum_sales FROM ( SELECT date, sales FROM daily_sales ORDER BY date ) t CROSS JOIN (SELECT @cum := 0) AS _;
PostgreSQL / SQL Server 直接用 SUM() OVER () 更稳,但别漏写 ROWS UNBOUNDED PRECEDING
这些数据库对窗口函数支持更严谨,默认 SUM() OVER (ORDER BY x) 确实等价于累计求和,但显式写上 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW 更安全——它明确告诉优化器“只累加当前行及之前所有行”,避免某些旧版本或复杂嵌套下行为漂移。
容易踩的坑是误以为 RANGE 和 ROWS 一样:当排序字段有重复值(比如多条记录同一天),RANGE 会把同值所有行一起算进当前窗口,导致突增;ROWS 则严格按物理顺序逐行推进。
- 时间类累计优先用
ROWS,除非你明确需要“同日期全部合并累加” - 如果排序字段可能
NULL,记得加NULLS LAST或NULLS FIRST,否则NULL行可能被挤到开头或结尾,打乱业务逻辑
SELECT
date,
sales,
SUM(sales) OVER (
ORDER BY date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS cum_sales
FROM daily_sales;
生成趋势图数据时,别直接用原始累计值做 Y 轴
前端画趋势图(比如 ECharts 或 Chart.js)要的是「时间点 + 累计值」配对,但真实业务常要求补全缺失日期(比如某天没销售,图上仍需显示前一日累计值),这就不能只靠 SQL 累加。
SQL 累加只负责“已有数据的有序叠加”,补缺、对齐、降采样都得靠应用层或预处理。强行在 SQL 里 LEFT JOIN 日历表再累加,会因空值导致 SUM() 中断,且性能差。
- 如果图表需展示连续日期,先用程序或日历表生成完整日期序列,再 LEFT JOIN 原始数据,最后用 COALESCE 填充 0,再做窗口累加
- 高频数据(如每分钟一笔)建议先按小时/天聚合再累加,否则前端渲染卡顿,SQL 扫描量也爆炸
- 注意时区:数据库时间字段和前端期望的“业务日”可能跨时区,累加前务必统一转换,否则凌晨时段会断层
累计求和本身不难,难的是它总被当成独立问题来解——而实际生产里,它永远嵌在时间对齐、空值处理、权限过滤、分页缓存这一整条链路里。少一个环节,图就画歪了。










