正确计算组内占比需用sum() over(partition by group_col)作分母,再除以当前行值,并用case when或nullif处理除零和null,避免整数除法截断,推荐cte预计算提升可读性与兼容性。

用窗口函数 SUM() OVER() 计算组内占比
直接在 GROUP BY 后用子查询或自连接求总额,既冗余又难维护。窗口函数才是标准解法:先按组聚合,再用 SUM() OVER() 拿到全局总和,最后相除。
常见错误是写成 SUM(sales) / SUM(sales) —— 这会返回 1.0,因为两个 SUM() 都在当前分组内计算,没跨组。
-
SUM(sales) OVER()不带PARTITION BY,表示全表总和 -
SUM(sales) OVER(PARTITION BY category)才是每组小计 - 结果默认是整数除法(如 PostgreSQL/MySQL 8.0+),需显式转
DECIMAL或乘1.0
SELECT category, SUM(sales) AS group_sales, ROUND(SUM(sales) * 1.0 / SUM(sales) OVER(), 4) AS ratio FROM orders GROUP BY category;
MySQL 5.7 或旧版不支持窗口函数怎么办?
必须退回到子查询或变量方案。子查询最稳妥,但性能随数据量下降明显;变量方案快但不可靠(执行顺序不保证,易出错)。
典型报错:ERROR 1054 (42S22): Unknown column 'total' in 'field list',是因为子查询别名不能在同级 SELECT 中引用。
- 正确写法:把总额查出来作为派生表,再
JOIN或用(SELECT ...)放在SELECT列中 - 避免用用户变量
@total,尤其在并发或复杂ORDER BY场景下结果可能错乱 - 如果表有索引(如
category),子查询可走Using temporary; Using filesort,注意监控执行计划
SELECT t1.category, t1.group_sales, ROUND(t1.group_sales / t2.total, 4) AS ratio FROM ( SELECT category, SUM(sales) AS group_sales FROM orders GROUP BY category ) t1 CROSS JOIN ( SELECT SUM(sales) AS total FROM orders ) t2;
NULL 值和零值会让比例变成 NULL 或除零错误
只要 sales 字段含 NULL,SUM() 会自动忽略它 —— 这没问题;但若某组所有 sales 都是 NULL,该组 SUM(sales) 返回 NULL,除法结果也是 NULL。更危险的是总额为 0(比如测试空表),直接触发除零异常。
- 用
COALESCE(SUM(sales), 0)把组内空值转 0,但要注意:0 占比是否符合业务语义 - 总额为 0 时,建议用
CASE WHEN SUM(sales) OVER() = 0 THEN 0 ELSE ... END拦截 - 某些数据库(如 SQL Server)对
0 / 0返回NULL,而 PostgreSQL 直接报错,行为不统一
想加百分号或保留两位小数,别在 SQL 里拼字符串
CONCAT(ROUND(..., 4) * 100, '%') 看似方便,但破坏了数值类型 —— 后续无法排序、求平均、导出到 BI 工具做计算。比例本质是小数,格式化应交给应用层或报表工具。
- BI 工具(如 Tableau、Power BI)自带「格式为百分比」选项,精度可控
- 如果必须 SQL 输出带 %,至少用
TO_CHAR(..., 'FM90D00%')(PostgreSQL)或FORMAT(..., 'P2')(SQL Server),保留数值语义 - 前端 JavaScript 处理时,注意
Number.toFixed(2)会四舍五入,而数据库的ROUND()行为可能不同
实际写的时候,窗口函数那行最容易漏掉 * 1.0 或 ::DECIMAL,一跑全是 0 或整数,查半天才发现是整除。还有人把 OVER() 写成 OVER(PARTITION BY ...),结果算的是组内占比而非占总额比——这两个括号差别很小,但意义完全相反。











