组内累计占比本质是“当前行及之前行的累计和 ÷ 当前组总和”,须用SUM() OVER(PARTITION BY g ORDER BY o)除以SUM() OVER(PARTITION BY g),并用NULLIF防零除、*1.0防整除截断、补唯一列保排序稳定。

用 SUM() 窗口函数算组内累计和,再除以组总和
累计占比本质是「当前行及之前行的累计和 ÷ 当前组的总和」,不能直接用 SUM(col) OVER (ORDER BY ...) 除以 SUM(col) OVER ()——后者是全表总和,不是组内总和。必须先明确分组边界,再分别算组内累计和与组内总和。
关键在 OVER 子句的写法:累计和需要 PARTITION BY + ORDER BY,组内总和只需 PARTITION BY(不加 ORDER BY 就是组内所有行的聚合)。
-
SUM(val) OVER (PARTITION BY group_col ORDER BY sort_col):组内按序累计求和 -
SUM(val) OVER (PARTITION BY group_col):组内静态总和(每行值相同) - 二者相除即得该行在组内的累计占比(注意处理
NULL和除零)
避免除零和 NULL 导致结果全为 NULL
当某组 SUM(val) OVER (PARTITION BY group_col) 为 0 或 NULL,整列占比会变成 NULL。SQL 中任何数除以 NULL 或 0 都不报错,但返回 NULL,容易被忽略。
- 用
NULLIF(..., 0)把分母的 0 转成NULL,再配合COALESCE统一兜底,例如:COALESCE(SUM(val) OVER (...) * 1.0 / NULLIF(SUM(val) OVER (PARTITION BY group_col), 0), 0) - 乘
1.0是为避免整数除法截断(如 PostgreSQL/MySQL 默认整除) - 如果原始
val允许NULL,先用COALESCE(val, 0)补零,否则SUM()会跳过NULL行,导致累计和与总和逻辑不一致
ORDER BY 的顺序决定“累计”的业务含义
累计占比是否合理,完全取决于 ORDER BY 字段是否反映业务排序逻辑。比如按销售额排序,还是按时间排序,结果差异极大。
- 若想按金额从大到小累计(如“Top N 贡献占比”),写
ORDER BY val DESC - 若想按时间先后累计(如“每日新增用户累计占比”),需确保时间字段无重复;如有重复,必须加唯一字段(如 ID)做二级排序:
ORDER BY event_time, id - 没写
ORDER BY会导致窗口帧默认为UNBOUNDED PRECEDING TO CURRENT ROW,但行为因数据库而异(MySQL 8.0+ 支持,旧版不支持),务必显式声明
不同数据库对窗口函数的兼容性细节
语法骨架一致,但几个关键点容易出错:
- PostgreSQL 和 SQL Server 支持直接写
SUM(val) OVER (PARTITION BY g ORDER BY t);MySQL 8.0+ 也支持,但 5.7 不支持窗口函数 - SQLite 3.25+ 支持,但不支持
FLOAT类型自动提升,需显式转CAST(val AS REAL) - Oracle 中
SUM() OVER默认帧是UNBOUNDED PRECEDING TO CURRENT ROW,和其他库一致;但若用ROWS BETWEEN ...自定义帧,累计逻辑可能变化 - 某些 BI 工具(如 Tableau、Superset)底层方言可能重写 SQL,把
OVER拆成子查询,导致性能骤降或报错,建议先在数据库原生客户端验证
真正难的不是写出来,而是确认 ORDER BY 字段是否真实承载了你要表达的“累计顺序”,以及分母是不是你心里想的那个“组”。这两点错一个,结果就偏了。










