正确计算分组内累计百分比应使用sum() over(partition by...order by...)÷sum() over(partition by...),分子需带rows unbounded preceding确保顺序,分母用nullif防除零,且须与分子严格对齐分组边界。

分组内累计百分比的正确计算逻辑
直接用 SUM() OVER 算累计和,再除以该分组总和,才是可靠做法。很多人误用 COUNT(*) OVER 或全局 SUM(),导致百分比加起来不等于 100%——尤其当分组内有重复值或 NULL 时,结果会漂移。
核心公式是:SUM(value) OVER (PARTITION BY group_col ORDER BY sort_col ROWS UNBOUNDED PRECEDING) ÷ SUM(value) OVER (PARTITION BY group_col)
- 分子必须带
ORDER BY,否则窗口无法确定“累计”顺序,结果不可控 - 分母不能写成常量或子查询,必须用同级
SUM() OVER (PARTITION BY ...),保证与分子严格对齐分组边界 - 如果
value列含 NULL,SUM()自动忽略,但需确认业务是否允许跳过(否则先用COALESCE(value, 0))
MySQL 8.0+ 和 PostgreSQL 中的写法差异
两者语法一致,但 MySQL 对 ROWS UNBOUNDED PRECEDING 更敏感:省略它可能退化为 RANGE 模式,在排序键重复时多算一行,导致累计值虚高。
PostgreSQL 允许省略 ROWS 子句,默认按行;MySQL 默认是 RANGE,必须显式声明 ROWS。
- 安全写法统一加
ROWS UNBOUNDED PRECEDING - 排序字段(
ORDER BY)必须具有确定性:若用时间戳,建议追加主键如ORDER BY created_at, id,避免相同时间戳打乱累计顺序 - 在 MySQL 中,如果漏写
PARTITION BY,整个结果集会被当成一个组,分组百分比全错
常见错误现象:百分比超过 100% 或出现 NULL
典型报错或异常表现:NULL 出现在百分比列、某组首行就显示 120%、不同分组间累计值跳跃。
- 分母为 0 → 整个分组百分比全为
NULL:加NULLIF(SUM(...) OVER (...), 0)防除零 - ORDER BY 字段含 NULL → 累计窗口可能把 NULL 排最前或最后,影响顺序:用
ORDER BY col ASC NULLS LAST(PG 支持)或ORDER BY IFNULL(col, '9999-12-31')(MySQL)显式控制 - 用了
COUNT(*) OVER (PARTITION BY ... ORDER BY ...)替代SUM(value)...:这算的是“行数占比”,不是“数值占比”,业务语义完全不对
实际可运行示例(以销售数据为例)
SELECT
region,
product,
amount,
SUM(amount) OVER (PARTITION BY region ORDER BY amount ROWS UNBOUNDED PRECEDING) AS cum_amount,
ROUND(
100.0 * SUM(amount) OVER (PARTITION BY region ORDER BY amount ROWS UNBOUNDED PRECEDING)
/ NULLIF(SUM(amount) OVER (PARTITION BY region), 0),
2
) AS cum_pct
FROM sales;
注意 NULLIF 和小数点后保留位数:直接除可能产生浮点误差,ROUND(..., 2) 是交付报表时的硬需求。另外,如果业务要求“按时间先后累计”,就把 ORDER BY amount 换成 ORDER BY sale_date——顺序一换,语义就变了,这点极易被忽略。










