PARTITION BY 显式指定分母范围是窗口函数算占比的关键,如全量占比用 SUM(sales) OVER(),部门占比用 SUM(sales) OVER(PARTITION BY dept),需配合 ROUND、NULLIF 和 100.0 避免除零、截断及精度问题。

用 PARTITION BY 控制分母范围是关键
窗口函数算占比,核心不是写 AVG() 或 SUM(),而是想清楚「对谁求百分比」。比如「每个部门销售额占全公司比例」和「每个部门内各员工占本部门比例」,分母完全不同,必须靠 PARTITION BY 显式指定。
常见错误是漏写 PARTITION BY,导致所有行都除以同一个总和(即不加 PARTITION BY 时默认按整个结果集计算),结果看似有数,但逻辑错位。
- 全量占比:分母是整张表的聚合值 →
SUM(sales) OVER() - 按部门占比:分母是各部门内部总和 →
SUM(sales) OVER(PARTITION BY dept) - 按年份+地区组合占比:分母是每组年份和地区交集的总和 →
SUM(sales) OVER(PARTITION BY year, region)
ROUND() 和除零保护必须手动加
直接写 sales / SUM(sales) OVER(...) 很容易遇到两个问题:小数位太多、遇到 NULL 或除零报错。SQL 标准里 SUM() 在空组返回 0,但若某组 sales 全为 NULL,SUM() 返回 NULL,再做除法就变成 NULL;更危险的是某些数据库(如 PostgreSQL)在 0 / 0 时抛 division by zero 错误。
- 统一保留两位小数:用
ROUND(sales * 100.0 / NULLIF(SUM(sales) OVER(...), 0), 2) -
NULLIF(..., 0)把分母为 0 的情况转成NULL,避免崩溃 - 乘
100.0而非100,防止整数除法截断(尤其在 PostgreSQL/SQL Server 中)
MySQL 8.0+ 和 SQLite 3.25+ 支持没问题,但旧版 MySQL 不行
窗口函数在 MySQL 中直到 8.0 才正式支持,如果你用的是 mysqld 5.7 或更早,执行含 OVER() 的语句会直接报错 ERROR 1064 (42000): You have an error in your SQL syntax。别急着改写法,先确认版本:SELECT VERSION();。
- PostgreSQL 9.4+、SQL Server 2012+、Oracle 11gR2+ 均原生支持
- SQLite 需 3.25.0+(2018 年后发布),旧版只能用自关联或子查询模拟
- 如果无法升级,替代方案是用
(SELECT SUM(sales) FROM t WHERE dept = t1.dept)替代窗口求和,但性能差、不可读、难维护
ORDER BY 在 OVER() 里会影响累计占比,不是必须项
很多人一看到 OVER() 就下意识加 ORDER BY,其实除非你要算「到当前行为止的累计占比」(比如销售排名前 N% 的客户),否则 ORDER BY 是多余的,还可能引入意料外的排序开销,甚至改变结果——因为带 ORDER BY 的窗口帧默认是 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,而没 ORDER BY 是 RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING,后者才等价于「整个分区求和」。
- 静态占比(如「各产品类目占总销量比」)→ 删掉
ORDER BY - 动态累计占比(如「按销量排序后,前几行累计占多少」)→ 必须加
ORDER BY sales DESC - 加了
ORDER BY却没想清帧范围,容易把「占比」算成「累计占比」,数值越来越大,最后超 100%
实际中最容易被忽略的是分母的语义是否和业务口径一致——比如「活跃用户占比」该按日活算,还是按注册用户总数算?窗口函数不会替你判断这个,它只忠实地执行你写的 PARTITION BY 和聚合逻辑。

















