前两篇里,你写出的每条查询都是“一行进、一行出”: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_iduser_idcityamountcreated_at
10018823杭州129.002024-03-01 10:23:11
10029107上海58.502024-03-01 11:07:42
10038823杭州320.002024-03-02 09:15:03
10049310NULL76.002024-03-02 14:02:50
10059107杭州45.002024-03-03 08:40:12
10069107上海210.002024-03-03 19:22:05
10079310上海91.502024-03-04 12:05:33
10088823NULL19.902024-03-04 21:41:07

8 行按 city 分成三堆:杭州 3 行(1001、1003、1005),上海 3 行(1002、1006、1007),城市为空的那 2 行(1004、1008)归在一起。然后每个堆各算一次聚合,最后每堆输出一行:

cityorder_counttotal_amount
杭州3494.00
上海3360.00
NULL295.90

杭州那一行就是 129 + 320 + 45 = 494.00。输入 8 行,输出 3 行——行数等于组的个数,这是 GROUP BY 最本质的变化。

不写 ORDER BY 时,这三行的先后顺序是不保证的:数据库觉得怎么方便就怎么给,下次跑可能就换了顺序。要固定顺序,就在语句最后加一句排序,比如按总额从高到低:

1ORDER BY total_amount DESC

DESC 是从大到小,不写则默认从小到大。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>
绘制中
</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 一行都没筛掉——它的作用是保证以后数据变了、或者换一张表跑时,答案仍然符合你要的口径。