group by后不能计算累计和,因其压缩行数丢失明细与顺序,窗口函数必须作用于未聚合的原始明细行;正确写法为sum() over(partition by col1 order by col2),缺一不可。

GROUP BY 后不能直接计算累计和——因为 GROUP BY 会把原始行压缩掉,窗口函数必须作用在未聚合的明细行上。
为什么 GROUP BY + SUM() 得不到累计值
GROUP BY 的结果是一组聚合后的新行,每组只剩一行。你没法在这行上再“逐行累加”,因为已经没有“行序”可言。比如按 month 分组后,1月、2月各一条记录,SUM(sales) 只能返回两行各自的总和,无法表达“1月+2月=前两月累计”这种跨行逻辑。
常见错误写法:SELECT month, SUM(sales), SUM(SUM(sales)) OVER (ORDER BY month) —— 这在多数数据库(如 PostgreSQL、SQL Server)会直接报错;MySQL 8.0+ 虽可能执行,但语义混乱:外层窗口函数作用在已聚合的结果集上,ORDER BY month 有效,但数据粒度已丢失,无法还原业务意义上的“逐月追加”。
- 真正需要的是:原始表中每条销售记录都保留,再按
month分组、按时间排序,逐条累加 - 正确起点永远是明细行,不是
GROUP BY后的结果 - 如果非要先聚合再累加(比如已有月度汇总表),那这张表本身就得包含有序的、不可合并的行(如每行代表一个独立月份)
SUM() OVER 必须用在明细行,且 PARTITION BY 和 ORDER BY 缺一不可
累计和不是数学加法,而是带上下文的有序叠加。数据库只认你明确定义的两个边界:
-
PARTITION BY category:告诉它“在哪个业务单元里算”,漏掉就变成全表累计 -
ORDER BY create_time:告诉它“从哪开始、按什么顺序加”,漏掉就退化成该组总和(每行值相同)
典型正确写法:SUM(amount) OVER (PARTITION BY product_line ORDER BY create_time)
容易被忽略的细节:
-
create_time有 NULL?MySQL 默认排最前,PostgreSQL 默认排最后,会导致首行累计值异常——建议显式处理:ORDER BY COALESCE(create_time, '1970-01-01') - 多笔订单同一天?仅靠
create_time排序会导致累计顺序不确定——补唯一字段:ORDER BY create_time, order_id - 排序字段没索引?大表上
OVER性能会断崖下跌,尤其PARTITION BY + ORDER BY组合
想先聚合再累加?得重构查询结构
如果你的数据源已经是分组后的结果(比如视图或中间表),又必须做累计,就不能依赖原始明细。这时要确保该结果集本身具备两个条件:
- 每行代表一个不可再分的业务单元(如“2024-01”、“华东区”)
- 存在严格单调递增/递减的序号字段(如
month_seq或自增id),不能只靠字符串'2024-01'排序(否则 2024-10 会排在 2024-2 前面)
示例(安全写法):
SELECT
ym,
monthly_sales,
SUM(monthly_sales) OVER (ORDER BY ym_seq) AS cum_sales
FROM (
SELECT
DATE_FORMAT(create_time, '%Y-%m') AS ym,
COUNT(*) AS monthly_sales,
ROW_NUMBER() OVER (ORDER BY DATE_FORMAT(create_time, '%Y-%m')) AS ym_seq
FROM orders
GROUP BY DATE_FORMAT(create_time, '%Y-%m')
) t
注意:ROW_NUMBER() 必须在子查询里生成,不能在外层对已分组结果再套窗口——否则 ROW_NUMBER() 会按当前结果集重新编号,失去时间先后语义。
MySQL 5.7 或旧版 SQLite 怎么办
没有窗口函数,就只能用变量模拟,但这是妥协方案,不是解法:
- 变量执行顺序不保证,尤其在 JOIN、WHERE 或 LIMIT 存在时极易错乱
- 并发查询下变量可能被多个会话覆盖,结果不可复现
- MySQL 8.0+ 已明确标记用户变量用于计算为“不建议”,后续版本可能移除支持
最小风险写法(仅限调试或小数据量):
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 _
关键点:ORDER BY date 必须在子查询里完成,CROSS JOIN 初始化变量必须在同一 SELECT 中——少一个环节,累计值就可能跳变或归零。
真正难的不是写出语法,而是确认“累计”在业务中到底指什么:是按自然日顺延?按事件发生顺序?是否要排除测试数据?这些决定了 PARTITION BY 的粒度和 ORDER BY 的字段选择,而不仅仅是技术能不能跑通。











