每组一个数字
GROUP BY 把取值相同的行收在一起,聚合函数再把每一堆压成一个数字。
1
SELECT status, COUNT(*) AS orders
FROM orders
GROUP BY status
ORDER BY orders DESC;
结果 — 共 7 行,这里是前 5 行
| status | orders |
|---|---|
| delivered | 1351 |
| paid | 695 |
| shipped | 641 |
| pending | 349 |
| refunded | 327 |
浏览器会另开一个标签页打开 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;
结果 — 1 行
| customers | with_phone | without_phone |
|---|---|---|
| 1500 | 1221 | 279 |
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;
结果 — 共 20 行,这里是前 5 行
| category | products | cheapest | average | dearest |
|---|---|---|---|---|
| Wearables | 11 | 417.3 | 1672.29 | 2433.73 |
| Programming | 10 | 6.89 | 1585.67 | 2420.02 |
| Tablets | 17 | 314.76 | 1529.45 | 2412.93 |
| Audio | 11 | 593.8 | 1482.89 | 2334.28 |
| Kitchen | 12 | 670.54 | 1443.21 | 2293.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;
结果 — 共 27 行,这里是前 5 行
| category | orders | units | revenue |
|---|---|---|---|
| Watches | 395 | 815 | 933900.92 |
| Board Games | 395 | 801 | 929534.55 |
| Laptops | 366 | 768 | 900778.09 |
| Science | 340 | 709 | 888542.78 |
| Cycling | 405 | 823 | 858478.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;
结果 — 共 8 行,这里是前 5 行
| category | revenue |
|---|---|
| Board Games | 1377454.99 |
| Tablets | 1348572.94 |
| Science | 1347046.09 |
| Watches | 1325717.58 |
| Cycling | 1288143.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;
结果 — 共 12 行,这里是前 5 行
| month | orders | revenue |
|---|---|---|
| 2025-01 | 52 | 311707.73 |
| 2025-02 | 53 | 350775.62 |
| 2025-03 | 58 | 351184.96 |
| 2025-04 | 52 | 374465.94 |
| 2025-05 | 53 | 381541.09 |
几乎所有"按月""按周"的报表,都是这样按派生值分组写出来的。还要注意 GROUP BY month 复用了 SELECT 里的别名,每个引擎在 GROUP BY 和 ORDER BY 里都允许这么做——严格的只有 WHERE 和 HAVING。
现在你能做什么
你能出报表了。还做不到的是拿一行去和一个汇总值比较——"价格高于平均值的商品""每个分类里的前三名"——因为那需要同时跑两条查询。下一课讲这个。