汇总行:GROUP BY

聚合会把多行汇总为数量、合计或平均值。你可以得到按状态、客户或分类分组的结果,而不只是逐条记录。

每组一个数字

GROUP BY 把取值相同的行收在一起,聚合函数再把每一堆压成一个数字。

1

SELECT status, COUNT(*) AS orders
FROM orders
GROUP BY status
ORDER BY orders DESC;
在 Planche 中运行

结果 — 共 7 行,这里是前 5 行

statusorders
delivered1351
paid695
shipped641
pending349
refunded327

浏览器会另开一个标签页打开 Planche,示例数据库已经载入,这条查询也已经放好。本页其他示例都会复用同一个标签页;在你按下 F9 之前,什么都不会执行。

七种状态,七行。SELECT 里的每个列,要么是你分组用的,要么是你聚合出来的——正是这条规则让结果有确定的含义。你要是在这儿要 order_number,问题本身就没意义了:那 1,351 张已送达订单里,该给你哪一个订单号?

COUNT(*) 不等于 COUNT(列)

2

SELECT COUNT(*) AS customers,
       COUNT(phone) AS with_phone,
       COUNT(*) - COUNT(phone) AS without_phone
FROM customers;
在 Planche 中运行

结果 — 1 行

customerswith_phonewithout_phone
15001221279

COUNT(*) 数的是行。COUNT(phone) 数的是 phone 不为 NULL 的行。差额正是筛选那一课里的 279 位客户,而这确实是个好用的小技巧:两个计数相减,不用第二条查询就知道缺了多少。

其他聚合函数同样跳过 NULL,这一点对 AVG 影响最大。10、20 和 NULL 的平均值是 15,不是 10——NULL 没有被当成零,而是根本没被算进去。真想把它当零,就用 COALESCE(列, 0) 明说。

一次算好几个聚合

3

SELECT cat.name AS category,
       COUNT(*) AS products,
       ROUND(MIN(p.price), 2) AS cheapest,
       ROUND(AVG(p.price), 2) AS average,
       ROUND(MAX(p.price), 2) AS dearest
FROM products p
JOIN categories cat ON cat.id = p.category_id
GROUP BY cat.id
ORDER BY average DESC
LIMIT 20;
在 Planche 中运行

结果 — 共 20 行,这里是前 5 行

categoryproductscheapestaveragedearest
Wearables11417.31672.292433.73
Programming106.891585.672420.02
Tablets17314.761529.452412.93
Audio11593.81482.892334.28
Kitchen12670.541443.212293.8

扫一遍表,就回答了每个分类的五个问题。同样的事情放到应用代码里做,意味着把 400 行商品全传过去再循环一遍;数据库在行本来就在的地方算出这五个数字,只回传 20 行。

跨连接分组

聚合和连接可以叠在一起用。先连接,再对连接后的行分组。

4

SELECT cat.name AS category,
       COUNT(DISTINCT o.id) AS orders,
       SUM(oi.quantity) AS units,
       ROUND(SUM(oi.quantity * oi.unit_price), 2) AS revenue
FROM order_items oi
JOIN orders o ON o.id = oi.order_id
JOIN products p ON p.id = oi.product_id
JOIN categories cat ON cat.id = p.category_id
WHERE o.status IN ('paid', 'shipped', 'delivered')
GROUP BY cat.id
ORDER BY revenue DESC;
在 Planche 中运行

结果 — 共 27 行,这里是前 5 行

categoryordersunitsrevenue
Watches395815933900.92
Board Games395801929534.55
Laptops366768900778.09
Science340709888542.78
Cycling405823858478.88

WHERE 在前,HAVING 在后

两者都是筛选。它们筛选的时机不同,而这就是它们唯一的区别。

5

SELECT cat.name AS category,
       ROUND(SUM(oi.quantity * oi.unit_price), 2) AS revenue
FROM order_items oi
JOIN products p ON p.id = oi.product_id
JOIN categories cat ON cat.id = p.category_id
GROUP BY cat.id
HAVING SUM(oi.quantity * oi.unit_price) > 1000000
ORDER BY revenue DESC;
在 Planche 中运行

结果 — 共 8 行,这里是前 5 行

categoryrevenue
Board Games1377454.99
Tablets1348572.94
Science1347046.09
Watches1325717.58
Cycling1288143.46

WHERE 在分组之前运行,决定哪些行参与进来。HAVING 在之后运行,决定哪些组留下来。所以针对原始列的条件写进 WHERE,针对聚合值的条件只能写进 HAVING——WHERE 运行时聚合还不存在。

把 SELECT 那一课的子句顺序摆到旁边,这条规则就不言自明了:

FROM  ->  WHERE  ->  GROUP BY  ->  HAVING  ->  SELECT  ->  ORDER BY  ->  LIMIT

按算出来的东西分组

分组的依据可以是表达式,不限于列。把时间戳的前七个字符切出来就是 2025-01——一个月份。

6

SELECT substr(ordered_at, 1, 7) AS month,
       COUNT(*) AS orders,
       ROUND(SUM(total), 2) AS revenue
FROM orders
WHERE status IN ('paid', 'shipped', 'delivered')
  AND ordered_at >= '2025-01-01'
GROUP BY month
ORDER BY month;
在 Planche 中运行

结果 — 共 12 行,这里是前 5 行

monthordersrevenue
2025-0152311707.73
2025-0253350775.62
2025-0358351184.96
2025-0452374465.94
2025-0553381541.09

几乎所有"按月""按周"的报表,都是这样按派生值分组写出来的。还要注意 GROUP BY month 复用了 SELECT 里的别名,每个引擎在 GROUP BY 和 ORDER BY 里都允许这么做——严格的只有 WHERE 和 HAVING。

现在你能做什么

你能出报表了。还做不到的是拿一行去和一个汇总值比较——"价格高于平均值的商品""每个分类里的前三名"——因为那需要同时跑两条查询。下一课讲这个。

自己动手写

  1. 每个国家各有多少客户?只显示至少 100 位的国家,多的排前面。

    查看答案

    查看答案

    SELECT country, COUNT(*) AS customers
    FROM customers
    GROUP BY country
    HAVING COUNT(*) >= 100
    ORDER BY customers DESC;
    在 Planche 中运行

    结果 — 共 12 行,这里是前 5 行

    countrycustomers
    KR155
    AU132
    IT130
    ES126
    GB126

    GROUP BY 让每个国家出一行,而条件是针对计数的,所以属于 HAVING 而不是 WHERE。那里重写了表达式而没有用别名,因为这样在所有引擎上都能跑。

  2. 评分最高的 15 个商品,只算评价数不少于 8 条的商品。

    查看答案

    查看答案

    SELECT p.name,
           COUNT(r.id) AS reviews,
           ROUND(AVG(r.rating), 2) AS avg_rating
    FROM products p
    JOIN reviews r ON r.product_id = p.id
    GROUP BY p.id
    HAVING COUNT(r.id) >= 8
    ORDER BY avg_rating DESC
    LIMIT 15;
    在 Planche 中运行

    结果 — 共 15 行,这里是前 5 行

    namereviewsavg_rating
    Pulse Yoga Mat84.63
    A Winter Letter (3rd ed.)94.56
    Terra Planter84.5
    Nimbus Cosmos84.5
    Ivory Toaster94.33

    同一组上的两个聚合:一个数评价条数,一个算平均分。HAVING 那个条件,正是为了不让一条五星评价把商品顶到榜首。