分组内占比需用SUM() OVER(PARTITION BY ...)作分母,确保为当前组总和;分子用原始字段或条件聚合,注意NULL处理、除零防护及GROUP BY后须两层查询实现。

用 SUM() OVER(PARTITION BY ...) 算分组内占比最直接
分母必须是“当前分组的总和”,不能是全表总数,否则算出来的是全局占比而非组内分布。核心是把聚合值(如 amount)作为分子,用带 PARTITION BY 的窗口函数广播分组总和作分母。
常见错误是写成 SUM(amount) / SUM(amount) OVER() —— 这里分母没限定分组,结果每行都除以全表总和,完全偏离需求。
-
SUM(amount) OVER (PARTITION BY category)是合法且必要的分母,它把每个category内的总金额“复制”到该组每一行 - 分子直接写原始字段
amount,不要加任何聚合或窗口,否则会丢失明细粒度 - 如果
amount可能为NULL,先用COALESCE(amount, 0),否则整组占比可能变成NULL - 整数除法在 PostgreSQL/SQL Server 中默认截断,MySQL 虽宽松但建议统一乘
1.0或显式CAST(... AS DECIMAL)
COUNT(*) OVER(PARTITION BY ...) 和 SUM(CASE ...) 配合算条件占比
当你要算“某类记录在本组中占多少比例”,比如“每个用户里成功订单占比”,就不能只靠 COUNT(*) —— 它无法区分条件满足与否。必须拆开分子分母,各自独立走窗口计算。
典型误写:COUNT(CASE WHEN status = 'success' THEN 1 END) / COUNT(*),这在窗口函数里语法非法;更糟的是,如果漏掉 ELSE 0,NULL 行会被跳过,导致分母虚高、占比偏大。
- 分子用
SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) OVER (PARTITION BY user_id) - 分母用
COUNT(*) OVER (PARTITION BY user_id),不是COUNT(status)(后者会忽略NULL) - 除法前加
NULLIF(..., 0)防止某用户无记录时除零,返回NULL比报错更可控 - 绝对不要在
OVER()里加ORDER BY,除非你明确要累计占比;静态占比必须只有PARTITION BY
GROUP BY 后不能再裸用 COUNT(*) OVER() 算分组占比
很多人想一步到位:在 GROUP BY city 查询里直接写 COUNT(*) * 100.0 / COUNT(*) OVER(),结果要么报错,要么返回全 1 或全 0。这不是数据库版本问题,而是 SQL 语义冲突 —— 聚合后每行代表一个分组,而 COUNT(*) OVER() 仍作用于原始行集,两者“行粒度”不匹配。
正确路径只有一条:先聚合出各组值,再对聚合结果做窗口计算。哪怕多一层子查询,逻辑也干净、可读、可维护。
- 内层:用
GROUP BY得到各组计数,如SELECT city, COUNT(*) AS cnt FROM orders GROUP BY city - 外层:对
cnt列用SUM(cnt) OVER()求总和,再相除,如ROUND(100.0 * cnt / SUM(cnt) OVER(), 2) - 别图省事写
COUNT(*) OVER()当分母 —— 它在GROUP BY下永远是原始表总行数,不是你想要的“各组计数之和” - 如果过滤了数据(如
WHERE status = 'paid'),子查询和外层必须共用同一WHERE条件,否则分母失真
兼容旧版数据库(如 MySQL 5.7)的替代写法
不支持窗口函数时,SUM() OVER(PARTITION BY ...) 就没法用。只能退回到相关子查询或自连接,性能差一点,但逻辑清晰、版本无依赖。
最容易踩的坑是子查询 WHERE 条件漏字段。比如按 (region, product) 分组,子查询却只关联了 region,结果就变成 region 级占比,而不是 region+product 组合级。
- 子查询必须起别名,MySQL 会报
Every derived table must have its own alias - 关联条件要和外层
GROUP BY字段完全一致,类型也要匹配(比如字符串注意隐式转换) - 示例结构:
SELECT t.region, t.product, t.cnt, ROUND(t.cnt * 100.0 / total.grp_sum, 2) FROM (...) t JOIN (...) total ON t.region = total.region AND t.product = total.product - 如果分组字段含
NULL,=判断失效,得改用IS NOT DISTINCT FROM(PostgreSQL)或三值逻辑处理(MySQL 用IFNULL+ 拼接)
事情说清了就结束。真正麻烦的是分母到底该取哪一层的“总”——是原始行、分组后计数之和、还是某个条件子集。选错一层,整个占比就偏了,而且不容易一眼看出来。











