SQL分组占比计算:避开三大坑,写出精准高效的查询

在数据分析报告里,展示各组数据占总体的百分比,几乎是刚需。但就这么一个看似简单的需求,SQL写起来却处处是“坑”——整数除法截断、NULL值干扰、窗口函数误用,随便踩中一个,得出的数字就离题万&里。今天,我们就来把这些问题逐个拆解清楚。
GROUP BY 后怎么算每个分组占总数的百分比
核心思路其实很直接:先算出各组的数量,再拿这个数量除以全表的总记录数。但问题来了,GROUP BY之后,怎么拿到那个“全表总数”呢?
一个常见的错误写法是:SELECT dept, COUNT(*) / COUNT(*) OVER() FROM emp GROUP BY dept。这行代码多半会报错,原因在于,在同一个SELECT层级里,聚合函数(COUNT(*) for GROUP BY)和窗口函数(COUNT(*) OVER())的运算逻辑是冲突的,数据库引擎无法同时处理。
那正确的姿势是什么?答案是:先聚合,后窗口。你可以先通过GROUP BY完成分组计数,然后再利用窗口函数跨组求和,来得到分母。这里最稳妥、最推荐的写法是:
用 SUM(COUNT(*)) OVER() 获取全表总数,再计算各分组占比最稳妥;需乘100.0防整数截断,用ROUND(...,2)保留两位小数;过滤NULL须用WHERE而非HA VING。
- 关键语法:
COUNT(*) * 100.0 / SUM(COUNT(*)) OVER()。注意那个100.0,它可不是为了好看,而是为了强制进行浮点数除法,避免整数相除直接归零的尴尬。 - 兼容性提示:这种“聚合函数嵌套窗口函数”的写法,在MySQL 8.0+、PostgreSQL、SQL Server、Oracle等主流数据库中都得到了支持。如果你的环境是SQLite,由于不支持窗口函数,那就得老老实实用子查询来先算出总数了。
窗口函数里 COUNT(*) OVER() 和 SUM(COUNT(*)) OVER() 的区别
这可能是最容易混淆的一对概念。别看长得像,作用天差地别。
COUNT(*) OVER()是在每行上都计算一次全表的行数,结果就是每一行的这个值都相同。但是,请注意,这个“全表”指的是FROM和WHERE之后、GROUP BY之前的结果集。一旦你使用了GROUP BY,逻辑层级就变了。
这时,你需要的是SUM(COUNT(*)) OVER()。这句话怎么理解?它其实是两步操作:
- 内层的
COUNT(*)随着GROUP BY执行,计算出每个分组的行数。 - 外层的
SUM(...) OVER()作为一个窗口函数,对上一步得到的“各组行数”进行求和,得到的就是全表总记录数。
- 唯一正解:在带有
GROUP BY的查询中,想通过窗口函数拿到全表总计,SUM(COUNT(*)) OVER()是唯一安全的写法。 - 语法合法性:不必怀疑,这种“聚合函数嵌套在窗口函数内”的写法,是符合SQL标准的,数据库认这个账。
百分比结果要保留两位小数,怎么写不踩坑
算出比例后,用ROUND(x, 2)四舍五入到两位小数,这个想法没错。但坑往往藏在除法运算本身。
不同的数据库对整数除法的处理方式不同。在SQL Server等数据库中,两个整数相除,结果会自动截断为整数。这意味着,如果你的占比小于1%,直接COUNT(*) / SUM(...)得到的结果很可能是0,后面再怎么乘100、再怎么ROUND都无力回天。
- 安全写法:确保在除法运算发生前,至少有一个操作数是浮点数。最简便的方法就是在分子部分乘以
100.0,如:ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER(), 2)。 - 显式转换:如果追求绝对明确,可以使用
CAST函数:ROUND(CAST(COUNT(*) AS DECIMAL(10,2)) * 100 / SUM(COUNT(*)) OVER(), 2)。 - 别用错函数:千万不要用
PERCENT_RANK()来替代。那个函数计算的是某行在排序中的相对位置百分比,而不是我们需要的频次占比。
想排除 NULL 分组再算占比,WHERE 和 HA VING 怎么选
这是逻辑层面的一个关键抉择,选错了,分母就错了。原则很简单:过滤NULL值,必须用WHERE,而不是HA VING。
为什么?因为HA VING是在分组(GROUP BY)之后进行过滤的。如果某个字段(比如dept)存在NULL值,使用GROUP BY dept时,NULL本身会形成一个独立的分组。这时,即便你用HA VING dept IS NOT NULL把这个NULL组过滤掉,窗口函数SUM(COUNT(*)) OVER()计算总数时,依然会把NULL组的那部分行数计入分母。导致的结果就是:分母偏大,你算出的所有非NULL组的占比都会偏低。
- 正确做法:在分组前,就用
WHERE dept IS NOT NULL把NULL行剔除。这样,后续的所有计算,分子和分母都基于同一份“干净”的数据集。 - 特殊情况:如果业务上要求保留NULL组作为一个分类显示,但不希望它参与占比计算,那就需要更精细的条件聚合。例如,分母需要用
SUM(CASE WHEN dept IS NOT NULL THEN 1 ELSE 0 END) OVER()来构造。
最后,再提一个极易疏忽的细节:当你使用SUM(COUNT(*)) OVER()时,那个空的OVER()子句意味着“在整个结果集上开窗”,这正是我们想要的。但如果不小心手滑,写成了OVER(PARTITION BY dept),那可就麻烦了——窗口函数会在每个dept分组内部求和,结果每个组的“分母”都变成了自己组的数量,最终算出来的占比,可不就全是100%了么。
