必须指定ORDER BY,否则SUM() OVER()无法实现累计和;默认帧为ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW;需结合PARTITION BY按业务粒度分区,并避免重复排序键导致的RANGE跳变。

SQL里用SUM()加OVER算累计和,核心就一条:必须指定ORDER BY
不写ORDER BY,SUM() OVER()会默认按无序聚合,结果不可预测——多数引擎(如PostgreSQL、SQL Server)会报错,MySQL 8.0+ 虽允许但返回的是全表总和而非“累计”。累计和本质是“按某顺序逐行累加”,顺序缺失,逻辑就崩了。
实操建议:
-
OVER (ORDER BY date)是最常见写法,date必须是能体现业务时序的列(比如订单时间、录入时间),不能用主键ID代替,除非ID严格递增且与业务时间一致 - 如果需按多列排序(如先按部门、再按日期),写成
OVER (ORDER BY dept_id, create_time) - 避免在
ORDER BY中使用表达式或函数(如ORDER BY YEAR(date)),部分数据库不支持或导致索引失效
窗口帧默认行为容易被忽略:ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
当你只写 SUM(amount) OVER (ORDER BY date),数据库其实自动补上了这个帧定义。它表示“从第一行到当前行”,这才是累计和的语义。但一旦你显式写了ROWS或RANGE子句,就可能覆盖默认行为。
常见陷阱:
- 误写成
SUM(amount) OVER (ORDER BY date ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)→ 这是“剩余和”,不是累计和 - 用
RANGE替代ROWS(如RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)→ 在ORDER BY列有重复值时,会把同值的所有行一起累加,导致跳变(例如两天日期相同,第二天的累计值直接+两天之和) - 想排除当前行做“截至上一行”的累计?得手动写
ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING,但要注意首行会返回NULL
分区(PARTITION BY)下累计和必须和业务粒度对齐
比如要算每个用户的消费累计,PARTITION BY user_id ORDER BY create_time 是对的;但如果漏了PARTITION BY,所有用户数据混在一起排序,用户A的第3笔订单可能被算进用户B的累计里。
关键点:
-
PARTITION BY列必须是分组维度本身,不能是其派生字段(如用PARTITION BY SUBSTR(phone, 1, 3)分运营商,不如先建好operator_code字段再分区,避免每次计算都执行函数) - 分区列和
ORDER BY列的组合,应保证每行唯一可排序(否则仍可能触发RANGE的重复值问题);必要时加一个唯一列兜底:ORDER BY create_time, id - 某些场景需要“跨分区重置累计”,比如每月重置为0,则
PARTITION BY YEAR(month), MONTH(month),而不是只按年分区
不同数据库对OVER子句的支持差异会影响迁移
MySQL 8.0+、PostgreSQL 8.4+、SQL Server 2005+、Oracle 8i+ 都支持标准SUM() OVER(),但细节有出入:
- SQLite 直到 3.25.0 才支持窗口函数,旧版本只能用自连接或相关子查询模拟,性能极差
- BigQuery 和 Redshift 支持,但Redshift对
RANGE帧支持有限,优先用ROWS - ClickHouse 的
runningAccumulate()是专用函数,不走标准OVER语法,得单独适配 - 如果SQL里混用了
WHERE和窗口函数,注意执行顺序:WHERE先于OVER生效,所以过滤后的结果才参与累计——这点常被当成bug,其实是设计如此
ORDER BY没写、重复值没处理、或者分区维度和业务理解错位。尤其当数据量上来后,RANGE帧引发的隐式分组膨胀,会让结果偏移得毫无征兆。

















