用SUM() OVER(PARTITION BY ...)计算分组内占比,分子为当前行值或组内聚合值,分母为同组窗口和,需防整除截断、NULL及除零错误。

SQL里怎么算分组内占比(不是全表占比)
直接用 SUM() 配合窗口函数 SUM() OVER (PARTITION BY ...),别用子查询套聚合——慢、易错、难维护。
常见错误是写成 SUM(x) / (SELECT SUM(x) FROM t),这算的是全表占比;或者漏写 PARTITION BY,结果全是同一份总数。
- 必须用窗口函数对每个分组单独求和:比如按
category分组,就写SUM(amount) OVER (PARTITION BY category) - 分子是当前行的值或该组聚合值(如
amount或SUM(amount)),分母必须是同组窗口和 - 注意数据类型:整数除整数会截断,加
::decimal或* 1.0强转(PostgreSQL/SQL Server 语法略有差异)
PostgreSQL 和 MySQL 8+ 的写法差异
核心逻辑一致,但细节上容易翻车:MySQL 8+ 支持标准窗口函数,PostgreSQL 默认支持,而 MySQL 5.7 及更早版本根本不支持 OVER,硬上会报错 ERROR 1064。
示例(计算每类销售额占本类总销售额的百分比):
SELECT category, amount, ROUND(amount * 100.0 / SUM(amount) OVER (PARTITION BY category), 2) AS pct_in_category FROM sales;
- PostgreSQL 中可写
::numeric替代* 100.0;MySQL 8+ 必须用浮点乘数避免整除归零 - 如果
amount为NULL,SUM()窗口会忽略它,但amount / NULL结果仍是NULL,需提前用COALESCE(amount, 0) - MySQL 5.7 用户只能用自连接或变量模拟窗口,性能差且不可靠,建议升级或换思路
GROUP BY 后再算占比的典型陷阱
很多人先 GROUP BY category 汇总,再想“在汇总结果里算占比”,这时已经没有原始行粒度,窗口函数失效,只能靠二次聚合或 CTE。
正确做法是:先用窗口函数算出分组总数,再在同一个查询中做除法——不落地、不嵌套、不重复扫描。
- 错误示范:
SELECT category, SUM(amount) AS s FROM t GROUP BY category; -- 然后怎么除?没分母来源 - 正确路径:把
SUM(amount) OVER (PARTITION BY category)当作“虚拟列”参与计算,哪怕你最终只想要汇总行 - 若真要输出汇总行+占比,用 CTE 更清晰:
WITH grp AS (SELECT category, SUM(amount) s, SUM(amount) OVER() total FROM t GROUP BY category) SELECT *, ROUND(s*100.0/total,2) FROM grp;
百分比结果为 NULL 或 0 的真实原因
不是语法错了,大概率是分母为 0 或隐式类型转换失败。特别是当某组所有 amount 都是 NULL 或 0 时,SUM(amount) OVER (...) 返回 0,除零在多数数据库中返回 NULL(不是报错)。
- 检查分母是否可能为 0:加
NULLIF(SUM(amount) OVER (...), 0),让除零变NULL而非崩掉 - 确认字段类型:
amount是integer但值很小,1 / 10得 0,必须提前转浮点 - 某些数据库(如 SQL Server)对
NULL和空字符串处理敏感,''不等于NULL,但参与数值运算可能意外转成 0
窗口函数本身不难,难的是意识到“分组内占比”本质是两层聚合:一层按组聚,一层跨行算比例。漏掉任何一层,结果就偏了。

















