用 SUM() OVER() 计算分组占比最稳妥,因聚合后无法访问原始行级总数;应先用窗口函数算总和,再分组除法,MySQL 8.0+、PostgreSQL等均支持,常见错误是漏 GROUP BY 导致只返回一行。

用 SUM() OVER() 计算分组占比最稳妥
直接在 GROUP BY 后除以总数容易出错,因为聚合后无法直接访问原始行级总数。正确做法是用窗口函数先算出总和,再在每组内做除法。MySQL 8.0+、PostgreSQL、SQL Server、Oracle 都支持,兼容性好,逻辑清晰。
常见错误是写成 COUNT(*) / COUNT(*) OVER() 却忘了加 GROUP BY,结果只返回一行;或者在旧版 MySQL(ERROR 1064。
- 确保数据库版本支持窗口函数,否则得用子查询或 JOIN 模拟
-
SUM() OVER()不带PARTITION BY就是全表总和,适合算全局占比 - 除法结果默认为整数(尤其在 MySQL 中),必须显式转成浮点,比如乘以
1.0或用CAST(... AS DECIMAL) - 示例:统计各城市订单数占总订单数的百分比
SELECT city, COUNT(*) AS cnt, ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER(), 2) AS pct FROM orders GROUP BY city;
旧版 MySQL 或不支持窗口函数时用 JOIN 拆解
如果用的是 MySQL 5.6 或 SQLite 这类不支持窗口函数的环境,就得把总数单独查出来,再跟分组结果 JOIN。本质是把“一次扫描”拆成“两次扫描”,性能略差,但语义明确、无版本限制。
容易踩的坑是 JOIN 条件写错,比如漏掉 ON 1=1 导致笛卡尔积;或者子查询没起别名,MySQL 报 Every derived table must have its own alias。
- 子查询必须起别名,哪怕只是
AS total - JOIN 用
CROSS JOIN或INNER JOIN ... ON 1=1都可以,目的是让每行都带上总数 - 注意 NULL 处理:如果分组字段有 NULL,
GROUP BY会单独成组,但总数仍包含所有行,占比计算不受影响
SELECT t.city, t.cnt, ROUND(t.cnt * 100.0 / total.all_cnt, 2) AS pct FROM ( SELECT city, COUNT(*) AS cnt FROM orders GROUP BY city ) t CROSS JOIN (SELECT COUNT(*) AS all_cnt FROM orders) total;
用 WITH ROLLUP 加合计行但不解决占比问题
WITH ROLLUP 能自动加小计/总计行,看起来像能辅助算占比,其实不能——它只增加汇总行,不提供可用于计算的“总值变量”。强行用会导致逻辑混乱,比如在 HAVING 里过滤时误把合计行当普通组处理。
典型误用:以为 GROUP BY city WITH ROLLUP 后,最后一行的 city IS NULL 就是总数,然后在 SELECT 里写 COUNT(*) / COUNT(*) 去除,结果全是 100.00。
-
WITH ROLLUP适合生成报表合计,不适合参与占比计算 - 它的合计值是按分组层级叠加的,不是简单全表总数(比如多列
GROUP BY a, b WITH ROLLUP会有 a 级、a+b 级、总计三级) - 真要复用合计值,仍得靠子查询或窗口函数,
WITH ROLLUP只是锦上添花
百分比结果要四舍五入但别提前截断
直接 ROUND(x * 100, 2) 没问题,但有人会先 ROUND(x, 4) 再乘 100,这会损失精度。比如真实占比是 0.123456,先截到 4 位得 0.1235,再 ×100 是 12.35;而一步到位是 12.3456 → 12.35,结果一致。但若中间用 CAST(x AS DECIMAL(5,4)),可能因存储精度引发偏差。
- 推荐统一用
ROUND(value * 100.0, 2),避免中间类型转换 - 显示时补零(如
12.3→12.30)用FORMAT()(MySQL)或TO_CHAR()(PostgreSQL),但别用于计算逻辑 - 百分比加
%符号是展示层的事,SQL 里保持纯数字,前端或报表工具处理格式化
实际写的时候,先看数据库版本是否支持窗口函数——支持就用 SUM() OVER(),干净利落;不支持就老实用子查询 JOIN。最容易被忽略的是除法类型隐式转换,尤其是 MySQL 默认整除,一个 1.0 没加,整张表占比全变 0。

















