正确做法是用窗口函数sum(count(*)) over()计算全量分母,再与各组count(*)做除法;需写100.0避免整数除法截断,用nullif(..., 0)防止除零错误,且逻辑必须在group by后、order by前执行。

用 SUM() 窗口函数算总数再除
直接在 GROUP BY 后用 COUNT(*) 除以总行数会报错,因为聚合和非聚合混用。正确做法是把总数作为窗口计算出来,再做除法:
SELECT category, COUNT(*) AS cnt,
ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER(), 2) AS pct
FROM orders
GROUP BY category;注意必须写 100.0(带小数点),否则整数除法在 PostgreSQL/SQL Server 里会截断小数;MySQL 8.0+ 和 SQLite 也建议保持一致。WHERE 条件影响百分比时要小心子查询或 CTE
如果原始数据已加了 WHERE status = 'done',那百分比分母必须和分子用同一过滤条件。不能先查总数再在外部 WHERE,否则分母变大、结果失真。推荐用 CTE 显式隔离:
WITH filtered AS ( SELECT category FROM orders WHERE status = 'done' ) SELECT category, COUNT(*) * 100.0 / (SELECT COUNT(*) FROM filtered) FROM filtered GROUP BY category;或者把
WHERE 写进窗口函数的 FILTER 子句(PostgreSQL 支持),但 MySQL 不支持,兼容性差。NULL 分类导致分母为 0 怎么办
当 category 允许为 NULL,且所有值都是 NULL 时,SUM(COUNT(*)) OVER() 仍能返回 0,除法会报错(如 PostgreSQL 报 division by zero)。加 NULLIF 是最稳妥的防护:
ROUND(COUNT(*) * 100.0 / NULLIF(SUM(COUNT(*)) OVER(), 0), 2)这个技巧在任何支持窗口函数的数据库里都有效。别用
CASE WHEN ... THEN ... ELSE 0 END 替代,它无法阻止除零错误触发。想按某列排序后取前 N 名占比?别在 GROUP BY 外套 ORDER BY LIMIT
常见误区:先 GROUP BY 出数量,再 ORDER BY cnt DESC LIMIT 5,最后算这 5 个的占比——这样分母变成了这 5 个之和,不是全量占比。真正需求是“Top 5 分类各自占全量的百分比”,那就不能动分母。正确顺序是:先算全量占比,再用外层 ORDER BY ... LIMIT 排序取行:
SELECT * FROM ( SELECT category, ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER(), 2) AS pct FROM orders GROUP BY category ) t ORDER BY pct DESC LIMIT 5;否则你看到的 85% 可能只是 Top 5 内部的相对占比,不是全局意义的 85%。
窗口函数的执行顺序在 GROUP BY 之后、ORDER BY 之前,这是整个逻辑能成立的前提。很多人调换子查询层级或提前 LIMIT,本质上是没意识到这一条隐含规则。











