SUM() OVER()计算分组内累计和再除以组总和得累计百分比,本质是“当前行累计值÷当前分组总值”,必须显式使用PARTITION BY分组、ORDER BY排序并指定ROWS UNBOUNDED PRECEDING窗口帧,避免跨组累加或默认RANGE语义导致偏差。

用 SUM() OVER() 计算分组内累计和再除以组总和
累计百分比本质是「当前行累计值 ÷ 当前分组总值」,不能直接用 ROW_NUMBER() 或全局 COUNT(*)。必须先按分组和排序维度定义窗口,再嵌套计算。
常见错误是写成 SUM(value) OVER (ORDER BY time) —— 这会跨分组累加,结果完全错乱。正确做法是显式加上 PARTITION BY group_col。
- 先确保排序字段(如
date、rank)在组内唯一或有明确业务顺序 -
SUM(value) OVER (PARTITION BY category ORDER BY date ROWS UNBOUNDED PRECEDING)得到组内累计值 - 用子查询或 CTE 先算出每组的
SUM(value) OVER (PARTITION BY category),再做除法 - 注意 NULL 值:若
value可为空,需提前COALESCE(value, 0)
避免浮点精度与除零错误的写法
直接写 cumsum * 100.0 / total 在某些数据库(如 PostgreSQL)里可能因整数除法截断为 0;而 total 为 0 时会报 division by zero 错误。
- 强制转浮点:
cumsum * 100.0 / NULLIF(total, 0)——NULLIF是关键,它把分母为 0 转成 NULL,不报错 - MySQL 用户注意:
NULLIF支持,但除法默认返回 DECIMAL,需加CAST(... AS DECIMAL(5,2))控制小数位 - SQL Server 中建议用
CONVERT(DECIMAL(5,2), cumsum * 100.0 / NULLIF(total, 0)),避免隐式转换丢失精度
PostgreSQL 和 SQLite 的 ROUND() 行为差异
想保留两位小数时,ROUND(x, 2) 看似通用,但 SQLite 不支持第二参数(只支持四舍五入到整数),PostgreSQL 支持但默认使用“银行家舍入”(偶数舍入),可能和预期不符。
- PostgreSQL:用
ROUND(x * 100.0 / NULLIF(total, 0), 2)可行,但若要严格四舍五入,加ROUND(... + 0.005, 2)更稳妥 - SQLite:只能先乘 1000,
ROUND(x * 1000.0 / NULLIF(total, 0)) / 10.0模拟两位小数 - 所有数据库都建议把最终百分比字段显式起别名,如
AS cum_pct,避免后续引用歧义
一次查出占比(% of group)和累计占比(running %)的典型结构
实际报表常需并列显示「本行占组内比重」和「从首行累加到本行的占比」,二者计算逻辑不同,但可共用同一窗口定义减少重复计算。
SELECT
category,
item,
value,
ROUND(100.0 * value / NULLIF(SUM(value) OVER (PARTITION BY category), 0), 2) AS pct_of_group,
ROUND(100.0 * SUM(value) OVER (PARTITION BY category ORDER BY sort_order ROWS UNBOUNDED PRECEDING)
/ NULLIF(SUM(value) OVER (PARTITION BY category), 0), 2) AS running_pct
FROM sales
ORDER BY category, sort_order;这里 SUM(value) OVER (PARTITION BY category) 被用了两次,但现代优化器通常只算一次;如果性能敏感,可改用 CTE 提前物化。
最易被忽略的是 ROWS UNBOUNDED PRECEDING —— 缺少它,某些数据库(如 older MySQL 8.0)可能默认用 RANGE 语义,在排序值相同时多计一行,导致累计值跳变。

















