用SUM()窗口函数计算分类占比最准确,如100.0COUNT()/SUM(COUNT(*)) OVER(),配合COALESCE和NULLIF处理NULL与除零,避免GROUP BY子查询导致的逻辑错位。

怎么用窗口函数算每个分类占总数的百分比
直接用 SUM() 窗口函数除以总和最稳,别先 GROUP BY 再连表——那样容易漏掉零值分类或重复计数。
典型错误是写成:SELECT category, COUNT(*) / (SELECT COUNT(*) FROM t) AS pct FROM t GROUP BY category,看似能跑,但一旦有 WHERE 条件或需要保留空分类,结果就偏了。
- 正确姿势:在聚合后用
SUM(COUNT(*)) OVER()拿全局总数,一行搞定 - 注意
COUNT(*)和COUNT(column)区别:后者会跳过NULL,算占比时若分类字段本身可空,得先COALESCE(category, 'unknown') - 除法结果默认是整数(尤其 PostgreSQL/MySQL 旧版本),必须显式转类型,比如
100.0 * COUNT(*) / SUM(COUNT(*)) OVER()
为什么不能只靠 GROUP BY + 子查询算占比
因为子查询的执行时机和外层 GROUP BY 不在一个上下文里,条件过滤、分区逻辑都可能错位。特别是加了 PARTITION BY 或要按时间窗口滚动计算时,子查询根本拿不到当前分区的“局部总数”。
- 例如想算“每个地区内各品类销量占比”,用子查询只能拿到全表总数,不是该地区的
- 窗口函数天然支持
PARTITION BY region,SUM(sales) OVER(PARTITION BY region)就是每个地区的总销量 - MySQL 8.0+、PostgreSQL、SQL Server 2012+ 都支持;SQLite 3.25+ 也行,但不支持
FILTER子句
NULL 分类或空分组导致占比为 NULL 怎么办
不是代码写错了,是 COUNT(*) 对空组返回 0,但 0 除以 0 是 NULL,或者某分类没数据,COUNT(*) 为 0,除出来还是 0%——但业务上你可能希望它显示 0.00% 而不是 NULL。
- 用
COALESCE(..., 0)包住整个百分比表达式,比如COALESCE(100.0 * COUNT(*) / NULLIF(SUM(COUNT(*)) OVER(), 0), 0) -
NULLIF(denominator, 0)比直接写/ 0安全,避免除零错误 - 如果源表里分类字段允许
NULL,且你想把它们当独立分类统计,务必先GROUP BY COALESCE(category, 'N/A'),否则NULL会被GROUP BY合并掉
性能差异:窗口函数 vs 多次扫描 vs CTE
窗口函数通常最快,因为只扫一遍表;CTE 看似清晰,但 PostgreSQL 可能物化中间结果,反而更慢;多次子查询等于扫表 N+1 次。
- 简单占比场景,
SUM(COUNT(*)) OVER()是最优解,优化器能自动合并聚合 - 如果还带排序或分页(比如 TOP 3 分类),加
ORDER BY ... LIMIT在窗口之后,别在子查询里提前截断——会把分母变小 - 大数据量时,确保分类字段有索引,尤其是做
PARTITION BY的列,否则窗口函数可能无法利用索引下推
实际写的时候,最容易被忽略的是除法精度和 NULL 边界——不是语法报错才叫问题,0.0 和 NULL 在报表里经常被当成同一回事,但下游系统解析逻辑可能完全不同。

















