老板要状态分布:聚合与 GROUP BY
老板不想一个数字一个数字地问:「订单按状态分个类,各多少单、多少钱,顺便告诉我每档金额的分布。」
学 · 40 min
01聚合函数 count / sum / avg / min / max 的输入输出;count 的三种语义
场景老板要的是「分布」,不是某一个数字--把 5 万行压成几个数的运算。
聚合函数吃很多行、吐一个值:
count数个数、sum求和、avg均值、min / max极值。count 有三种语义要分清:count(*)数行、count(列)数非空、count(distinct 列)数去重后的非空值。select count(*), -- 总行数 count(distinct user_id) -- 多少个不同用户下过单 from orders;易错count(distinct a, b) 这种多列组合写法 PG 不直接支持,要写 count(distinct (a, b))。
02GROUP BY:把很多行压成「每组一行」
场景「按状态分类」= 先按 status 把 5 万行分堆,再对每堆各算一个 count。
group by status 之后,5 万行变成 4 行(每个状态一行)。理解它最好的方式:把表想象成按状态码堆好的几摞,select 里的聚合函数对每一摞各算一次。status 为 NULL 的行不会消失,自己占一组。
select status, count(*) as 单数, sum(total_amount) as 总金额 from orders group by status order by 单数 desc;易错分组后每组只剩一行,select 里出现「既不在 group by、也没被聚合」的列会报错--W2 D11 专门拆这个约束。
03WHERE(分组前过滤)vs HAVING(分组后过滤)
场景「找出订单数超过 100 的日期」--这个 100 是数出来的,数之前它还不存在。
执行顺序说了算:WHERE 在分组前作用于原始行,HAVING 在分组后作用于组。判断条件用到聚合结果(每组数出来的那个数)的,只能进 HAVING;HAVING 里可以放心写
count(*),WHERE 里写它直接报错。select created_at::date as 日期, count(*) as 单数 from orders group by 1 having count(*) > 100 -- 过滤的是「组」 order by 1;易错报错 aggregate functions are not allowed in WHERE 就是把聚合条件写错了地方。
04sum / avg 自动跳过 NULL 带来的分母问题
场景运营质疑你算的「平均金额」:和 Excel 里算的不一样。
sum / avg 遇到 NULL 行直接跳过:sum 没影响(加零而已),但 avg 的分母变小了--Excel 的 AVERAGE 其实也一样,但没人意识到。想「NULL 按 0 参与平均」要自己
avg(coalesce(列, 0)),两个口径都对,错的是不说明口径。select avg(total_amount), -- 分母 = 非空行数 avg(coalesce(total_amount, 0)) -- 分母 = 全部行数 from orders;易错报「平均值」之前先回答:分母是谁?这是口径问题,不是语法问题。
05CASE 两种写法(简单式 / 搜索式):把值映射成标签、把金额分档
场景老板看不懂状态码,报表要输出「已支付 / 待支付」;还要把连续的金额切成 0-100 / 100-500 / 500+ 三档。
简单式
case 列 when 值 then ...做等值映射;搜索式case when 条件 then ...做任意判断(分档、多条件)。条件从上往下,第一个命中生效,所以分档的条件按从小到大排;else兜住没列到的值和 NULL。-- 简单式:值 -> 标签 select case status when 2 then '已支付' when 1 then '待支付' when 3 then '已取消' else '未知' end as 状态 from orders limit 5; -- 搜索式:金额分档(条件顺序就是判断顺序) select case when total_amount < 100 then '0-100' when total_amount < 500 then '100-500' else '500+' end as 金额档, count(*) from orders group by 1;易错分档条件写反(先 < 500 后 < 100),所有小单都进了第一档--顺序就是逻辑。
练 · 65 min
- 按状态统计订单数和总金额
参考答案
status 为 NULL 的行不会消失,自己成一组出现在结果里。
select status, count(*) as 单数, sum(total_amount) as 总金额 from orders group by status order by 单数 desc; - 算每种状态的订单占比(总单数用子查询再除)
参考答案
写 100.0 别写 100--整数除整数得 0。总数用标量子查询;第 4 题会见到免子查询的窗口写法。
select status, round(count(*) * 100.0 / (select count(*) from orders), 1) as 占比 from orders group by status order by 占比 desc; - 用 CASE 把状态映射成中文标签(如「已支付」「待支付」)再分组
参考答案
这是 CASE 的「简单式」;else 兜住 NULL 和没列出的值。group by 1 = 按第一列分组。
select case status when 2 then '已支付' when 1 then '待支付' when 3 then '已取消' else '未知' end as 状态, count(*) as 单数, sum(total_amount) as 总金额 from orders group by 1 order by 单数 desc; - 按金额分档(0–100 / 100–500 / 500+)统计订单数与占比
参考答案
这是「搜索式」CASE,条件从上往下第一个命中生效。sum(count(*)) over () 是窗口函数算总数,不用再写子查询(W3 正式讲)。
select case when total_amount < 100 then '0-100' when total_amount < 500 then '100-500' else '500+' end as 金额档, count(*) as 单数, round(count(*) * 100.0 / sum(count(*)) over (), 1) as 占比 from orders group by 1 order by 1; - 用 HAVING 找出订单数 > 100 的日期,并解释为什么这题不能用 WHERE
参考答案
「订单数 > 100」是对聚合结果的判断,分组之前这个数还不存在,所以不能用 WHERE。WHERE 过滤行(分组前),HAVING 过滤组(分组后)。
select created_at::date as 日期, count(*) as 单数 from orders group by 1 having count(*) > 100 order by 1;