前两篇里,你写出的每条查询都是“一行进、一行出”:WHERE 只是在决定哪些行留下,行的条数变了,每一行的形状没变。可现实里的取数问题经常是另一类——“每个城市一共多少单、总共多少钱”。这类问题的答案不是某几行订单,而是每个城市一个数。要把 8 行订单变成 3 行城市统计,需要两件事配合:聚合函数负责把一堆值压成一个数,GROUP BY 负责决定“按什么把它们分成堆”。
分完组之后还会冒出一个新问题:有些条件筛的是一行一行的订单,有些条件筛的是一个一个的组,两者不能放在同一个位置。这就是 HAVING 出场的地方。
聚合函数:把一堆值压成一个数
聚合函数的作用,就是把一个“堆”里的很多值算成一个值。最常用的几个:COUNT() 数个数,SUM() 求和,AVG() 求平均,MAX()、MIN() 取最大和最小值。
1SELECT COUNT(*) AS order_count,
2 SUM(amount) AS total_amount
3FROM orders;这条查询没有 GROUP BY,含义是“整张 orders 表算一个堆”:一共 8 行,金额合计 949.90。
这里顺带认识了 AS:它给结果里的这一列起个名字,表头显示成 order_count、total_amount,比 COUNT(*) 好读,后面也用得上。
GROUP BY:按某一列分成几堆,每堆出一行
现在要求“每个城市的下单情况”。整张表当一个堆显然不够,得先按城市分开:
1SELECT city,
2 COUNT(*) AS order_count,
3 SUM(amount) AS total_amount
4FROM orders
5GROUP BY city;GROUP BY city 让数据库逐行去看 city 这一格的值,值相同的行归到同一堆。这一篇里 orders 又多了三行(1006 到 1008):
| order_id | user_id | city | amount | created_at |
|---|---|---|---|---|
| 1001 | 8823 | 杭州 | 129.00 | 2024-03-01 10:23:11 |
| 1002 | 9107 | 上海 | 58.50 | 2024-03-01 11:07:42 |
| 1003 | 8823 | 杭州 | 320.00 | 2024-03-02 09:15:03 |
| 1004 | 9310 | NULL | 76.00 | 2024-03-02 14:02:50 |
| 1005 | 9107 | 杭州 | 45.00 | 2024-03-03 08:40:12 |
| 1006 | 9107 | 上海 | 210.00 | 2024-03-03 19:22:05 |
| 1007 | 9310 | 上海 | 91.50 | 2024-03-04 12:05:33 |
| 1008 | 8823 | NULL | 19.90 | 2024-03-04 21:41:07 |
8 行按 city 分成三堆:杭州 3 行(1001、1003、1005),上海 3 行(1002、1006、1007),城市为空的那 2 行(1004、1008)归在一起。然后每个堆各算一次聚合,最后每堆输出一行:
| city | order_count | total_amount |
|---|---|---|
| 杭州 | 3 | 494.00 |
| 上海 | 3 | 360.00 |
| NULL | 2 | 95.90 |
杭州那一行就是 129 + 320 + 45 = 494.00。输入 8 行,输出 3 行——行数等于组的个数,这是 GROUP BY 最本质的变化。
不写 ORDER BY 时,这三行的先后顺序是不保证的:数据库觉得怎么方便就怎么给,下次跑可能就换了顺序。要固定顺序,就在语句最后加一句排序,比如按总额从高到低:
1ORDER BY total_amount DESCDESC 是从大到小,不写则默认从小到大。ORDER BY 可以写在 SELECT 里起的别名,所以这里直接写 total_amount。
SELECT 里哪些列能写:要么分组,要么聚合
分组之后每个组只输出一行,所以组里只有分组列还剩唯一确定的值。看这条:
1SELECT city, order_id, SUM(amount) AS total_amount
2FROM orders
3GROUP BY city;杭州这一组里有 1001、1003、1005 三个 order_id,让数据库填哪一个都不对——它没有依据判断你想看哪个。MySQL 5.7.5 及以后版本默认开启 ONLY_FULL_GROUP_BY,遇到这种情况会直接报错:
1ERROR 1055 (42000): Expression #2 of SELECT list is not in GROUP BY
2clause and contains nonaggregated column ... which is incompatible
3with sql_mode=only_full_group_by在更老的 MySQL 版本,或者有人手动关掉这个设置之后,这句不会报错,但数据库只能从组里随便挑一个 order_id 给你,挑到哪个是不确定的,加 ORDER BY 也影响不了它 1。所以看到 1055 这类报错,正确做法是改查询,而不是去关设置。
规则记成一句话就是:SELECT 里的每一列,要么出现在 GROUP BY 里,要么包在聚合函数里。 两种都不满足,就是让数据库在一组里做无缘无故的选择。
WHERE 和 HAVING:同一个条件,位置不同,筛的东西也不同
现在想“只保留下了 3 单及以上的城市”。很自然会想接着写 WHERE COUNT(*) >= 3,但这条会报错:
1SELECT city, COUNT(*) AS order_count
2FROM orders
3WHERE COUNT(*) >= 3
4GROUP BY city;原因在于 SQL 各子句的逻辑执行顺序,跟你写它们的顺序并不一样:
<figure>WHERE 排在聚合之前,它面对的还是一行一行的订单。在那个时刻,“这一组的 COUNT(*) 是多少”根本还不存在,所以 WHERE 里放聚合函数会直接报错。同理,WHERE 里也不能用 SELECT 起的别名,别名是更后面才出现的东西。
按组筛选要交给 HAVING,它排在聚合之后:
1SELECT city, COUNT(*) AS order_count
2FROM orders
3GROUP BY city
4HAVING COUNT(*) >= 3;结果是杭州 3、上海 3,城市为空那一组因为只有 2 单被丢掉。这里的 HAVING 看的不是某一行,而是整个组算出来的那个数。
同一个业务问题,条件放哪儿,答案真的不一样
“只看金额超过 100 的订单,按城市汇总”和“按城市汇总,只看总额超过 100 的城市”,听起来接近,要的是两张表。第一句:
1SELECT city, COUNT(*) AS order_count, SUM(amount) AS total_amount
2FROM orders
3WHERE amount > 100
4GROUP BY city;杭州剩 1001、1003,共 2 单、449.00;上海只剩 1006,共 1 单、210.00;城市为空的两单都不足 100 元,整组消失。
第二句:
1SELECT city, SUM(amount) AS total_amount
2FROM orders
3GROUP BY city
4HAVING SUM(amount) > 100;杭州是 494.00,上海是 360.00。两个查询里杭州都留下了,但数字不同:WHERE 版的 449.00 是“大额订单加起来”,HAVING 版的 494.00 是“所有订单加起来,只是这家够大所以没被踢掉”。WHERE 削掉的是行,被削掉的行从头到尾不参与统计;HAVING 削掉的是组,组里的每一行都已经算进去了。 这就是判断条件该放哪里的依据。
两者当然可以同时出现:WHERE 先把不看的订单删掉,GROUP BY 分组聚合,HAVING 再把不够格的城市删掉。写成这个顺序还有个附带好处:WHERE 越早筛掉不用的行,后面要分组和计算的数据就越少。
两件顺带要说清楚的事
NULL 在分组里会被当成一个值。 所有 city 为空的行归到了同一组,这就是结果里出现一行 NULL 的原因——SQL 在分组时把 NULL 视为彼此相等 2。但要和 COUNT 的两种写法区分开:COUNT(*) 数的是组里有多少行,COUNT(city) 只数 city 这一格不是空的行。整张表 COUNT(*) 是 8,COUNT(city) 只有 6(1004 和 1008 的城市是空的)。想把城市为空的订单排除在统计之外,就在 WHERE 里写 city IS NOT NULL。
没有 GROUP BY 时,整张表就是一个组。 所以 SELECT COUNT(*), SUM(amount) FROM orders; 只返回一行;这种情况下 SELECT 里更不能出现裸列,因为那个唯一的值同样是无从选择的。
把这一篇的规则收成一句:行的问题交给 WHERE,组的问题交给 HAVING,中间夹着 GROUP BY 把行分成堆。
练一下:每个用户各下了几单、总金额多少,只保留下单 2 单以上的用户,并按总金额从高到低排。
1SELECT user_id,
2 COUNT(*) AS order_count,
3 SUM(amount) AS total_amount
4FROM orders
5GROUP BY user_id
6HAVING COUNT(*) >= 2
7ORDER BY total_amount DESC;结果是 8823(3 单、468.90)、9107(3 单、313.50)、9310(2 单、167.50)。这里三个用户都下了 2 单以上,所以 HAVING 一行都没筛掉——它的作用是保证以后数据变了、或者换一张表跑时,答案仍然符合你要的口径。