直接用SUM(col) OVER()算百分比常得100%,根本原因是未先GROUP BY聚合:窗口函数作用于原始行而非分组结果,导致分子分母同值;正确做法是将聚合查询作为子查询或CTE,再在外层用SUM() OVER()对聚合后结果求和,并用100.0避免整数除法、NULLIF防除零。

为什么直接用 SUM(col) OVER() 算百分比经常得到 100%?
常见错觉是:写成 col * 100.0 / SUM(col) OVER() 就能算占比,结果每行都显示 100%。根本原因是没先做聚合——SUM(col) OVER() 是对原始行逐行计算窗口和,不是对 GROUP BY 后的结果求和。
正确路径一定是:先 GROUP BY 得到各组汇总值,再用窗口函数在分组结果集上算总和。
比如统计各品类销售额占比,必须先 GROUP BY category 求出每个品类的 SUM(sales),然后在这个结果集上套 SUM() OVER()。
怎么写才能让 SUM() OVER() 作用在聚合结果上?
核心技巧:把聚合查询作为子查询或 CTE,再在其外层用窗口函数。窗口函数不能“穿透”到 GROUP BY 内部,只能作用于它看到的那张临时结果表。
SELECT category, total_sales, ROUND(total_sales * 100.0 / SUM(total_sales) OVER(), 2) AS pct FROM ( SELECT category, SUM(sales) AS total_sales FROM orders GROUP BY category ) t;
- 子查询
t输出的是已聚合的行(每品类一行),SUM(total_sales) OVER()对这组行求和,才是全量销售额 - 务必用
100.0(带小数点)避免整数除法截断,尤其在 PostgreSQL/SQL Server 中 - MySQL 8.0+、PostgreSQL、SQL Server、Oracle 都支持;SQLite 不支持窗口函数
遇到 NULL 或零值时百分比会崩吗?
如果某组 SUM(sales) 为 NULL(比如全为 NULL 值),或总和为 0,total_sales / SUM(...) OVER() 会返回 NULL 或报错(如 SQL Server 的除零错误)。
安全写法要加保护:
ROUND( COALESCE(total_sales, 0) * 100.0 / NULLIF(SUM(total_sales) OVER(), 0), 2 ) AS pct
-
COALESCE(total_sales, 0)把分子的 NULL 转成 0 -
NULLIF(SUM(...) OVER(), 0)把分母的 0 变成 NULL,使整除结果为 NULL 而非报错 - 部分数据库(如 BigQuery)允许用
SAFE_DIVIDE(),但标准 SQL 还是靠NULLIF更通用
想按时间维度滚动占比,比如每月占季度总额?
这时需要在窗口函数里加 PARTITION BY,但注意:分区依据必须来自聚合后的字段,不能是原始时间字段。
例如算“每个季度内各月份销售额占比”:
SELECT
quarter,
month,
monthly_sales,
ROUND(monthly_sales * 100.0 / SUM(monthly_sales) OVER(PARTITION BY quarter), 2) AS pct_in_qtr
FROM (
SELECT
EXTRACT(YEAR FROM order_date) || '-Q' || EXTRACT(QUARTER FROM order_date) AS quarter,
EXTRACT(MONTH FROM order_date) AS month,
SUM(sales) AS monthly_sales
FROM orders
GROUP BY 1, 2
) t;
- 子查询里已按季度+月份聚合,所以外层
PARTITION BY quarter才有意义 - 如果误在子查询外直接
PARTITION BY EXTRACT(QUARTER FROM order_date),会因缺少该字段而报错 - 日期函数写法依数据库而异:
DATEPART(qq, order_date)(SQL Server)、TO_CHAR(order_date, 'YYYY-"Q"Q')(PostgreSQL)
实际跑通的关键,是盯住“窗口函数看到的数据形状”——它永远只对当前查询结果集的每一行生效,不会回溯到未聚合的原始数据。很多人卡在这一步,反复调却始终算不对,其实只是少套了一层查询。

















