累计占比分母必须是组内总和而非全表总和,正确写法为SUM(x) OVER (PARTITION BY group_col ORDER BY order_col) / SUM(x) OVER (PARTITION BY group_col),需用NULLIF防零除、*1.0防整除截断、补唯一列保排序稳定。

累计占比的计算逻辑必须先明确分母
累计占比不是简单用 SUM() 窗口函数一除就完事——分母必须是该组的总和,且不能随窗口移动变化。常见错误是写成 SUM(x) OVER (ORDER BY t) / SUM(x) OVER (),这会把整个表总和当分母,跨组污染结果。
正确做法是:先用 SUM(x) OVER (PARTITION BY group_col) 固定每组总和作为分母,再用 SUM(x) OVER (PARTITION BY group_col ORDER BY order_col) 计算组内累计值。
- 分母必须带
PARTITION BY,否则就是全表求和 - 分子的
ORDER BY列决定累计顺序,缺失会导致“全组一次性累加”,失去逐行意义 - 如果
order_col有重复值,需额外加唯一列(如主键)避免排序不确定
PostgreSQL/MySQL 8.0+/SQL Server 中的标准写法
以销售记录按产品类别分组、按日期累计为例:
SELECT
category,
sale_date,
amount,
SUM(amount) OVER (PARTITION BY category ORDER BY sale_date, id) AS cum_amount,
SUM(amount) OVER (PARTITION BY category) AS total_per_cat,
ROUND(
SUM(amount) OVER (PARTITION BY category ORDER BY sale_date, id) * 1.0 /
NULLIF(SUM(amount) OVER (PARTITION BY category), 0),
4
) AS cum_pct
FROM sales;
注意三点:
-
NULLIF(..., 0)防止分母为零报错,比CASE WHEN更简洁 -
* 1.0强制转浮点,避免整数除法截断(尤其在 PostgreSQL 和 SQL Server 中) -
id补在ORDER BY里,解决同天多笔订单时窗口累计结果不稳定的常见问题
SQLite 和旧版 MySQL(
这些环境不支持 SUM() OVER,得用自连接或相关子查询模拟累计逻辑,性能差但可行:
SELECT
s1.category,
s1.sale_date,
s1.amount,
(SELECT SUM(s2.amount)
FROM sales s2
WHERE s2.category = s1.category
AND (s2.sale_date <p>关键限制:</p>
- 子查询中必须严格复现分组条件(
s2.category = s1.category),漏掉就变全表累计 - 日期+ID 的复合比较逻辑不能简化为仅日期,否则重复日期下累计值会跳变
- 数据量超 5000 行时,这种写法可能明显变慢,别在线上大表硬扛
为什么用 ROUND(..., 4) 而不是 CAST(... AS DECIMAL(5,4))
累计占比常用于报表展示,精度需求明确(比如保留 4 位小数),但不同数据库对 DECIMAL 的隐式转换行为不一致:
- MySQL 中
CAST(x/y AS DECIMAL(5,4))可能因中间结果溢出报错 - SQLite 不支持
DECIMAL类型,强制转换会静默失败 -
ROUND()是通用函数,语义清晰,且能规避浮点误差导致的0.999999999这类显示异常
真正容易被忽略的是:累计占比加起来未必等于 1.0000——最后一条记录的 cum_pct 才应接近 1,中间行只是过程值;如果拿所有行的 cum_pct 求和,数值本身没有业务意义。










