直接用 COUNT(CASE WHEN condition THEN 1 END) / COUNT(*) 计算分组内条件占比,需确保浮点除法、避免 COUNT(condition) 错误,并注意 WHERE 过滤会扭曲分母;AVG(CASE WHEN ... THEN 1.0 ELSE 0 END) 更简洁安全;跨组对比需用窗口函数明确参照系。

用 COUNT + CASE WHEN 计算分组内条件占比
直接在 GROUP BY 后用 COUNT(CASE WHEN condition THEN 1 END) 除以 COUNT(*) 就能得到比例。注意必须用浮点数除法,否则整数除法结果为 0。
常见错误是写成 COUNT(condition) —— COUNT 不管值是 true 还是 false 都计数,只有 NULL 才被忽略;所以必须用 CASE WHEN 把不满足条件的转成 NULL。
- MySQL / PostgreSQL / SQL Server 都支持这种写法
- 如果字段本身可能为
NULL,条件里要显式写IS NOT NULL,比如CASE WHEN status = 'done' AND id IS NOT NULL THEN 1 END - 想保留小数位数,用
ROUND(..., 4)或强制转类型,如CAST(... AS DECIMAL(5,4))
SELECT
category,
ROUND(
COUNT(CASE WHEN score >= 80 THEN 1 END) * 1.0 / COUNT(*), 4
) AS pass_rate
FROM exam_results
GROUP BY category;避免 WHERE 过滤导致分母失真
有人习惯先用 WHERE 筛出满足条件的行再分组,但这会让分母变成“过滤后的总行数”,而不是“该组全部行数”,比例就完全不对了。
比如想算“每部门中薪资 > 15000 的员工占比”,如果写 WHERE salary > 15000,那每个部门的分母就只含高薪员工,结果全是 1.0。
- 正确做法:所有行都参与分组,仅在分子中用
CASE WHEN判定 - 例外情况:如果业务明确要求“只统计某类群体内的子比例”(比如“在职员工中男性占比”),才先
WHERE is_active = 1,但此时分母含义已改变,需在字段名或注释里说明清楚
用 AVG(CASE WHEN ...) 替代 COUNT 简化写法
AVG 对布尔表达式天然友好:CASE WHEN condition THEN 1 ELSE 0 END 的平均值,就等于 true 占比。语义更直白,且不用处理除零问题(AVG 在空组返回 NULL,而 COUNT(*) 为 0 时除法会报错)。
- PostgreSQL 支持直接写
AVG(score >= 80)(返回 bool → int 自动转换) - MySQL 和 SQL Server 需显式写
AVG(CASE WHEN score >= 80 THEN 1.0 ELSE 0 END) - 注意:
AVG会忽略NULL,所以ELSE 0不能省略,否则未命中条件的行不参与计算,结果偏高
SELECT dept, AVG(CASE WHEN salary > 15000 THEN 1.0 ELSE 0 END) AS high_salary_ratio FROM employees GROUP BY dept;
窗口函数实现跨组对比比例(如全量基准)
如果需求是“每个部门的通过率 vs 全公司通过率”,就不能只靠 GROUP BY,得用窗口函数把全量统计“广播”进每行。
典型陷阱是误用 SUM() OVER() 但没加 PARTITION BY,导致分母变成全表总数,而分子仍是分组内计数,数值失去可比性。
- 分子用
COUNT(CASE WHEN ...) OVER(PARTITION BY dept) - 分母用
COUNT(*) OVER(PARTITION BY dept)(组内总数)或COUNT(*) OVER()(全表总数) - SQL Server 和 PostgreSQL 支持,MySQL 8.0+ 也支持,老版本需用变量或自连接模拟
复杂点在于业务语义:比例的参照系必须清晰——是组内占比?还是相对于全局的相对强度?这点容易在需求沟通时被忽略,写完才发现指标解释不通。

















