老板要状态分布:聚合与 GROUP BY

老板不想一个数字一个数字地问:「订单按状态分个类,各多少单、多少钱,顺便告诉我每档金额的分布。」

学 40 min
练 65 min
盘 15 min
共 120 分钟

学 · 40 min

  1. 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))。

  2. 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 专门拆这个约束。

  3. 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 就是把聚合条件写错了地方。

  4. 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;

    易错报「平均值」之前先回答:分母是谁?这是口径问题,不是语法问题。

  5. 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

  1. 按状态统计订单数和总金额
    参考答案

    status 为 NULL 的行不会消失,自己成一组出现在结果里。

    select status, count(*) as 单数, sum(total_amount) as 总金额
    from orders
    group by status
    order by 单数 desc;
  2. 算每种状态的订单占比(总单数用子查询再除)
    参考答案

    写 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;
  3. 用 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;
  4. 按金额分档(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;
  5. 用 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;
过关标准 一条 SQL 输出「状态 | 订单数 | 金额 | 占比%」完整报表。