SUM() OVER() 月累计错误的根本原因是缺失或错误的 ORDER BY 子句;必须显式按可比较的时间字段(如 order_date 或格式化后的年月)排序,且跨月累计不可加 PARTITION BY。

为什么 SUM() OVER() 算出来的月累计总是不对
常见现象是:结果里某个月份的累计值比预期小,或者整列都是同一值。根本原因通常是 ORDER BY 子句缺失或写错——OVER() 默认不保证窗口内行序,必须显式指定排序依据。月份字段如果只是字符串(如 '2024-01'),还要确认它能被正确比较;用 DATE 类型或带前导零的 CHAR(7) 更稳妥。
实操建议:
- 确保分区键和排序键分离:按年月分组累计,就用
PARTITION BY YEAR(order_date), MONTH(order_date)或直接PARTITION BY DATE_FORMAT(order_date, '%Y-%m')(MySQL);但注意——如果真要「跨月累计」(比如 1月→2月→3月逐月累加),不能加PARTITION BY,否则每月从头算起 - 排序必须用可比较的时间字段,例如
ORDER BY order_date或ORDER BY year_month,别用ORDER BY month_name('Jan'/'Feb' 字典序错乱) - 避免在
SELECT中混用聚合和窗口函数却没写GROUP BY,容易触发 SQL mode 报错(如 MySQL 5.7+ 的ONLY_FULL_GROUP_BY)
MySQL / PostgreSQL 中正确写法示例
假设表 sales 有字段 sale_date(DATE)、amount(DECIMAL):
SELECT
DATE_FORMAT(sale_date, '%Y-%m') AS year_month,
SUM(amount) AS monthly_total,
SUM(SUM(amount)) OVER (
ORDER BY DATE_FORMAT(sale_date, '%Y-%m')
ROWS UNBOUNDED PRECEDING
) AS cumsum_by_month
FROM sales
GROUP BY DATE_FORMAT(sale_date, '%Y-%m')
ORDER BY year_month;
关键点:
-
GROUP BY和外层SUM() OVER()配合:先聚出每月总额,再对这些聚合结果做窗口累计 -
ROWS UNBOUNDED PRECEDING显式声明窗口范围(默认行为,但显写更安全,尤其在旧版本或兼容性要求高时) - PostgreSQL 用户把
DATE_FORMAT(sale_date, '%Y-%m')换成TO_CHAR(sale_date, 'YYYY-MM'),逻辑一致
遇到 NULL 或重复月份怎么办
如果某月无销售记录,SUM() OVER() 不会自动补 0,结果里直接跳过该月——这不是函数问题,是源数据缺失。需要先生成完整月份序列再 LEFT JOIN。
常见错误操作:COALESCE(SUM(amount), 0) 放在窗口函数里没用,因为 SUM() 聚合后已是 NULL(当月无数据),但窗口函数作用在已聚合的结果集上,补 0 必须在 JOIN 后、窗口前做。
建议步骤:
- 用递归 CTE 或日历表生成连续的
year_month列(如 2023-01 到 2024-12) -
LEFT JOIN销售聚合结果,用COALESCE(monthly_total, 0)填空 - 再对填完的
monthly_total列跑SUM() OVER(ORDER BY year_month)
性能隐患:大数据量下 OVER() 会变慢吗
会,但不是因为窗口函数本身,而是排序开销。当按月累计需扫描数百万行时,ORDER BY year_month 若无索引,就会触发 filesort。解决方式很直接:
- 在用于
ORDER BY的表达式上建函数索引(MySQL 8.0+ 支持CREATE INDEX idx_yearmonth ON sales ((DATE_FORMAT(sale_date, '%Y-%m')))) - 或冗余一个
year_month CHAR(7)字段,每日/每批写入时自动填充,并对该字段建普通索引 - 避免在
OVER()中用复杂表达式(如嵌套CASE WHEN)做排序依据,先算好存起来
真正容易被忽略的是:累计值一旦涉及业务对账,就必须确认时间精度是否包含时区、是否按自然月(而非滚动30天)、以及退款是否冲减当月还是发生月——这些逻辑不在 SQL 里,但在 amount 字段入库前就得定死。

















