最稳的方式是用 sum() over() 算全局总计再除:只扫描一次表、避免子查询性能问题和聚合混用报错,需处理分母为零、null 逻辑、整数除法、精度控制及 null 分组等细节。

用 SUM() OVER() 算全局总计再除,别用子查询
直接在 GROUP BY 查询里算占比,最稳的方式是用窗口函数。很多人第一反应写子查询求总和,但子查询会强制全表扫描两次,数据量一大就明显变慢;而 SUM() OVER() 只扫一次表,性能好得多。
常见错误是写成 SUM(col) / (SELECT SUM(col) FROM t) —— 这在 MySQL 5.7 或 SQL Server 里可能报错(聚合与非聚合混用),在 PostgreSQL 里也可能因执行计划不佳拖慢响应。
- 确保分母不为零:加
CASE WHEN SUM(col) OVER() = 0 THEN 0 ELSE ... END - 如果
col是NULL,SUM()会自动忽略,但你要确认业务上是否该把NULL当 0 处理 - MySQL 8.0+、PostgreSQL 9.4+、SQL Server 2012+ 都支持,老版本得换方案
ROUND() 和小数位数要显式控制
数据库默认除法结果精度不一致:MySQL 返回 DECIMAL 但位数常不够,PostgreSQL 返回 numeric 但可能带一堆小数位,SQL Server 默认截断为整数。不加 ROUND() 容易看到 0.23999999999999999 这种值,前端展示或导出时出问题。
- 推荐写法:
ROUND(100.0 * SUM(col) / SUM(col) OVER(), 2) - 注意
100.0不是100:避免整数除法(尤其 SQL Server 和旧版 MySQL) - 百分比字段别名建议加
%后缀,比如as pct%,方便下游识别
当分组字段含 NULL 时,GROUP BY 行为要小心
如果分组字段(如 category)有 NULL 值,多数数据库会把所有 NULL 归为同一组,但这个“NULL 组”的占比计算容易被忽略——它算进分母,但业务上你可能根本不想展示这一行。
- 若需排除
NULL分组:在WHERE category IS NOT NULL中过滤,而不是靠HAVING - 若需单独看
NULL占比:确保GROUP BY允许NULL,且理解它和其他值一样参与SUM() OVER()计算 - Oracle 对
NULL分组更敏感,建议显式用NVL(category, 'UNKNOWN')替换
替代方案:CTE 比子查询清晰,但别滥用
如果窗口函数不支持(比如 MySQL 5.6),可以用 CTE 先算总数,再 JOIN。比嵌套子查询可读性高,也方便复用。但 CTE 不是万能的——某些数据库(如旧版 SQLite)不支持,而且如果总数逻辑复杂(比如带多条件过滤),CTE 里写错一处,所有分组结果都错。
- 基本结构:
WITH total AS (SELECT SUM(col) AS t FROM t WHERE ...) SELECT g, SUM(col), ROUND(100.0 * SUM(col) / t, 2) FROM t, total GROUP BY g
- 避免在 CTE 里
GROUP BY,否则后续 JOIN 时容易笛卡尔积 - 如果只是单个指标占比,优先窗口函数;如果要多个不同维度的占比(比如按月、按地区分别算各自占比),才考虑拆 CTE
实际跑起来快不快,取决于你的 col 是否有索引、分组键基数高低,以及数据库对窗口函数的优化程度——这些没法靠 SQL 写法绕过,得看执行计划。










