最常见做法是在select中用子查询计算全局分母,如count()100.0/(select count() from employees);推荐用sum(count()) over()窗口函数替代以提升性能,并注意nullif防除零及明确分母粒度。

子查询放在 SELECT 列表里直接算占比
最常见也最直观的做法,是在 SELECT 里用子查询算出分母(如总行数、总金额),再除以当前分组的聚合值。比如统计每个部门人数占公司总人数的百分比:
SELECT dept, COUNT(*) AS cnt, ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM employees), 2) AS pct FROM employees GROUP BY dept;
注意三点:
• 分母子查询必须返回单个标量值,否则报错 Subquery returns more than 1 row;
• 用 100.0 而不是 100,避免整数除法截断(MySQL/PostgreSQL 中尤其关键);
• 子查询不带 WHERE 条件,它要算的是全局总数,不是当前分组的“上层聚合”。
用窗口函数替代子查询更高效
如果数据库支持窗口函数(PostgreSQL、SQL Server、MySQL 8.0+、Oracle),SUM() OVER() 比子查询快得多,尤其数据量大时。等价写法如下:
SELECT dept, COUNT(*) AS cnt, ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER(), 2) AS pct FROM employees GROUP BY dept;
这里的关键是:
• SUM(COUNT(*)) OVER() 是“聚合上的窗口”,先按 dept 分组计数,再对所有分组结果求和;
• 不需要额外子查询,执行计划通常少一次全表扫描;
• 如果还需按时间范围动态计算(比如只算 2023 年数据的占比),就把主查询加 WHERE year = 2023,窗口函数自动继承这个过滤条件——而子查询若没同步加 WHERE,结果就错了。
处理 NULL 或零值导致的除零错误
当分母可能为 0(比如空表、筛选后无数据),直接除会出错或返回 NULL。安全写法要用条件判断:
- PostgreSQL/SQL Server:用
NULLIF(denominator, 0),再配合COALESCE(..., 0) - MySQL:用
IF(denominator = 0, 0, numerator / denominator) - 通用兜底(推荐):
ROUND( COUNT(*) * 100.0 / NULLIF((SELECT COUNT(*) FROM employees), 0), 2 ) AS pct
NULLIF(x, 0) 在 x 为 0 时返回 NULL,除法遇到 NULL 整体得 NULL,再用 COALESCE(pct, 0) 统一转成 0 —— 这比让应用层处理异常更可控。
嵌套两层 GROUP BY 时别误用相关子查询
如果想算「每个部门中某职级人数占该部门总人数的百分比」,容易错写成相关子查询:
-- ❌ 错误:子查询里没关联外层 dept,算的是全局职级占比 SELECT dept, title, COUNT(*) / (SELECT COUNT(*) FROM employees WHERE title = e.title) -- 缺少 dept 关联! FROM employees e GROUP BY dept, title;
正确做法只有两个:
• 用窗口函数:在 OVER(PARTITION BY dept) 里算部门内汇总;
• 或改用 JOIN + 派生表,先算出部门总数表,再关联:
SELECT e.dept, e.title, COUNT(*) AS cnt, ROUND(COUNT(*) * 100.0 / t.dept_total, 2) AS pct FROM employees e JOIN (SELECT dept, COUNT(*) AS dept_total FROM employees GROUP BY dept) t ON e.dept = t.dept GROUP BY e.dept, e.title, t.dept_total;这种写法可读性强,但要注意
JOIN 后的 GROUP BY 必须包含所有非聚合字段,包括 t.dept_total。真正难的不是语法,是想清楚“占比的分母到底属于哪一层粒度”——写子查询前,先手写一句自然语言:“我要算的是【X】占【Y】的百分比”,Y 就是子查询该查什么。漏掉这步,后面全是调试陷阱。










