第二单元 · 精准修改、安全删除、基础汇总 · 第 6 / 21 节
五个汇总函数,加上按城市分组
COUNT / SUM / AVG / MIN / MAX 与 WHERE 组合,GROUP BY 分组、ORDER BY 按汇总值排序、HAVING 筛组;看清 1064(WHERE 里写 COUNT)与 1140(未分组裸列)的原因。
约 22 分钟。这一节不改数据,只回答“一共多少人”“平均多大”“哪个城市人多”这类问题。
$ / mysql> 提示符和输出。已完成 0 / 7 步
你现在在哪个提示符
应该在 mysql>。断了就 ssh root@8.148.204.152 → mysql-learning,然后核对起点:
SELECT id, name, age, city, joined FROM lesson02_students ORDER BY id;
+----+--------+------+--------+------------+
| id | name | age | city | joined |
+----+--------+------+--------+------------+
| 1 | 张伟 | 23 | 北京 | 2024-03-02 |
| 2 | 李娜 | 21 | 上海 | 2024-03-05 |
| 3 | 王强 | 25 | 广州 | 2024-02-20 |
| 4 | 赵敏 | 22 | 深圳 | 2024-03-09 |
| 5 | 陈晨 | 24 | 杭州 | 2024-03-12 |
+----+--------+------+--------+------------+
5 rows in set (0.00 sec)
必须是这 5 行:id 1 到 5,北京和上海各一个,没有 id = 6。第 5 节删掉了周舟,所以这里的年龄合计、平均值都按这 5 个人算。如果状态不一样,先回第 5 节的「断线再学怎么核」。
汇总函数是什么
前面学的 SELECT 都是“一行一行取出来”。汇总函数(也叫聚合函数)不一样:它把很多行的同一个列压成一个值。
| 写法 | 回答的问题 |
|---|---|
COUNT(*) | 一共多少行 |
SUM(age) | 这一列加起来是多少 |
AVG(age) | 这一列的平均值 |
MIN(age) | 这一列的最小值 |
MAX(age) | 这一列的最大值 |
age 换成 SUM(joined) 没有意义,汇总函数用在数字列上才有意义。
先数人数
SELECT COUNT(*) AS 人数 FROM lesson02_students;
+--------+
| 人数 |
+--------+
| 5 |
+--------+
1 row in set (0.00 sec)
COUNT(*) 里的 * 表示“数行”,不去看这一行每列有没有值。5 行就是 5。上一节数行数用的也是它,这里把它正式归进“汇总”。
AS 人数 是给这一列起的中文名,只影响显示的表头,不影响数据。
一次算完五项
SELECT COUNT(*) AS 人数, SUM(age) AS 年龄合计, AVG(age) AS 平均年龄,
MIN(age) AS 最低年龄, MAX(age) AS 最高年龄 FROM lesson02_students;
+--------+--------------+--------------+--------------+--------------+
| 人数 | 年龄合计 | 平均年龄 | 最低年龄 | 最高年龄 |
+--------+--------------+--------------+--------------+--------------+
| 5 | 115 | 23.0000 | 21 | 25 |
+--------+--------------+--------------+--------------+--------------+
1 row in set (0.00 sec)
五个函数写在同一行 SELECT 里,按逗号分开,各自算各自的。结果永远只有 1 行:5 个人的年龄是 23、21、25、22、24,加起来 115,平均 23。
AVG会给 4 位小数23.0000,因为它的结果是精确小数类型,不是整数。想只保留一位,交给ROUND。MIN和MAX出的都是整数,因为它们返回的是原来那一列的值,没有经过计算。
ROUND 是个普通函数,作用是把数字按指定位数四舍五入:
SELECT ROUND(AVG(age), 1) AS 平均年龄 FROM lesson02_students;
+--------------+
| 平均年龄 |
+--------------+
| 23.0 |
+--------------+
1 row in set (0.00 sec)
ROUND(AVG(age), 1) 的意思是“先算平均,再把结果留 1 位小数”。ROUND 可以套在任何数字外面,不只 AVG。
汇总和 WHERE 一起用
WHERE 先筛行,汇总函数再对筛出来的行算——顺序是先筛后算,不是反过来。先看看 age >= 23 是谁:
SELECT id, name, age FROM lesson02_students WHERE age >= 23 ORDER BY age;
+----+--------+------+
| id | name | age |
+----+--------+------+
| 1 | 张伟 | 23 |
| 5 | 陈晨 | 24 |
| 3 | 王强 | 25 |
+----+--------+------+
3 rows in set (0.00 sec)
三个人,年龄 23、24、25。对这三行做汇总:
SELECT COUNT(*) AS 人数, SUM(age) AS 年龄合计 FROM lesson02_students WHERE age >= 23;
+--------+--------------+
| 人数 | 年龄合计 |
+--------+--------------+
| 3 | 72 |
+--------+--------------+
1 row in set (0.00 sec)
3 行、72 岁,和上一条 SELECT 数出来的完全一致。先跑明细、再跑汇总,两个数字能对上,这是检查汇总有没有写错的好办法。
按城市分组:GROUP BY
上面的问题都是“整张表一起算”。如果要“每个城市各有多少人”,需要先按 city 把行分成几堆,再对每一堆分别汇总。这一步叫分组,写法是 GROUP BY city。
SELECT city AS 城市, COUNT(*) AS 人数 FROM lesson02_students GROUP BY city ORDER BY 城市;
+--------+--------+
| 城市 | 人数 |
+--------+--------+
| 上海 | 1 |
| 北京 | 1 |
| 广州 | 1 |
| 杭州 | 1 |
| 深圳 | 1 |
+--------+--------+
5 rows in set (0.00 sec)
逐句读:
GROUP BY city:把 city 相同的行放进同一堆。5 个人 5 个城市,就是 5 堆,每堆 1 行。SELECT city, COUNT(*):每一堆输出一行,city是这一堆的城市,COUNT(*)是这一堆的行数。- 结果现在有 5 行,不是 1 行——
GROUP BY把“整表 1 行”变成了“每组 1 行”。 ORDER BY 城市用中文别名排序。ORDER BY是在分组和汇总之后才执行的,所以这里能直接用别名。
也可以对每组算平均值(这一节只放一条分组平均,够用就行;多个汇总并列的写法下一单元展开):
SELECT city AS 城市, ROUND(AVG(age), 1) AS 平均年龄
FROM lesson02_students GROUP BY city ORDER BY 城市;
+--------+--------------+
| 城市 | 平均年龄 |
+--------+--------------+
| 上海 | 21.0 |
| 北京 | 23.0 |
| 广州 | 25.0 |
| 杭州 | 24.0 |
| 深圳 | 22.0 |
+--------+--------------+
现在每个城市只有一个人,所以“平均年龄”就等于本人的年龄。等以后某个城市有两个人,这一列才会真的开始“平均”。
ORDER BY 也可以按汇总结果排,比如按人数从多到少:
SELECT city AS 城市, COUNT(*) AS 人数 FROM lesson02_students
GROUP BY city ORDER BY 人数 DESC, 城市;
+--------+--------+
| 城市 | 人数 |
+--------+--------+
| 上海 | 1 |
| 北京 | 1 |
| 广州 | 1 |
| 杭州 | 1 |
| 深圳 | 1 |
+--------+--------+
5 rows in set (0.00 sec)
五个人人数都一样,ORDER BY 人数 DESC 排不出先后,所以加了第二个排序条件 城市,保证顺序稳定。这个习惯在第 3 节就养成了:并列时再补一个不会重复的列。
HAVING:筛“组”,不是筛“行”
WHERE 在分组之前筛行,它看不到 COUNT(*) 的结果。想筛“人数大于 1 的城市”,要用 HAVING——它在分组和汇总之后执行。
先记住分组后的样子(上面那条查询的结果):
+--------+--------+
| 城市 | 人数 |
+--------+--------+
| 上海 | 1 |
| 北京 | 1 |
| 广州 | 1 |
| 杭州 | 1 |
| 深圳 | 1 |
+--------+--------+
5 rows in set (0.00 sec)
现在给这 5 组加一个“人数大于 1”的门槛:
SELECT city AS 城市, COUNT(*) AS 人数 FROM lesson02_students
GROUP BY city HAVING 人数 > 1 ORDER BY 城市;
Empty set (0.00 sec)
Empty set 表示查询跑通了,只是没有任何一组满足条件。这不是写错:5 个城市各 1 个人,没有哪一组的人数大于 1。
把门槛放宽到“大于 3”也一样,因为每组的 COUNT(*) 都是 1。HAVING 比较的是每组汇总出来的那个数,不是某一行。
HAVING 人数 > 1 和 HAVING COUNT(*) > 1 等价,都能用:HAVING 在汇总之后执行,所以认识 SELECT 里起的别名。
说明 为什么 WHERE 里写 COUNT 会报错(可选读)
如果把 HAVING 人数 > 1 换成 WHERE COUNT(*) > 1,MySQL 会在解析阶段就拦下来:
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'WHERE COUNT(*) > 1' at line 1
原因是语句里子句的顺序是固定的:FROM → WHERE → GROUP BY … → HAVING → ORDER BY。WHERE 必须写在 GROUP BY 前面,而那时还没汇总,COUNT(*) 无从谈起。
另外,汇总函数也不能和没分组的裸列混在一起。下面这条会被 ONLY_FULL_GROUP_BY 拒绝:
ERROR 1140 (42000): In aggregated query without GROUP BY, expression #1 of SELECT list contains nonaggregated column 'itcast.lesson02_students.city'; this is incompatible with sql_mode=only_full_group_by
一句话:city 是每行自己的值,COUNT(*) 是整组的值,两者要放在一起输出,就必须先 GROUP BY city 说清楚“按谁分组”。
一条查询从书写到执行
写的时候是这个顺序:
SELECT 列 FROM 表 WHERE 条件 GROUP BY 分组列 HAVING 组条件 ORDER BY 排序列;
MySQL 处理时是这个顺序:
FROM → WHERE → GROUP BY → 汇总 → HAVING → SELECT → ORDER BY
所以 WHERE 里不能用别名、不能用汇总函数;GROUP BY 之后才有“每组一个值”;HAVING 和 ORDER BY 才认识别名。
每个城市的人数,为什么都是 1
这不是 MySQL 的问题,是数据的问题。第 5 节把周舟删掉之后,剩下 5 个人正好在 5 个不同城市。如果第 5 节没有删周舟,北京就是 2 个人,HAVING 人数 > 1 就会返回北京那一行。
想看一次真实的分组效果,可以在练习里自己加一个人再跑同一组查询,跑完删掉。不要为了看到数字而改动第 5 节结束时的这 5 行——下一单元会接着用它们。
重复学习怎么办
说明 数字对不上时按这个顺序查
汇总查询不改数据,重复跑一百次结果都一样。所以“数字不对”几乎总是数据状态不对。按顺序核:
SELECT COUNT(*) AS 人数 FROM lesson02_students;
+--------+
| 人数 |
+--------+
| 5 |
+--------+
1 row in set (0.00 sec)
- 人数是 5 吗?如果是 6,说明第 5 节的删除没做或没生效,回第 5 节。
- 如果是 4,看看是不是被你自己在练习里删掉了某一行:
SELECT * FROM lesson02_students ORDER BY id;。 - 人数对了但
SUM(age)不是 115,看SELECT id, age FROM lesson02_students ORDER BY id;,某一行的年龄被改过。
SELECT SUM(age) FROM lesson02_students; 自己也能当体检:23 + 21 + 25 + 22 + 24 = 115,对不上就是数据变了。
练习
先自己想,再展开。
1. 不加任何分组,求出这 5 个人的最高年龄和人数。
参考答案 参考答案
SELECT COUNT(*) AS 人数, MAX(age) AS 最高年龄 FROM lesson02_students;
+--------+--------------+
| 人数 | 最高年龄 |
+--------+--------------+
| 5 | 25 |
+--------+--------------+
1 row in set (0.00 sec)
最高的是王强 25 岁。结果永远只有 1 行。
2. 按城市分组,每个城市的人数和平均年龄一起列出来,按平均年龄从高到低排。
参考答案 参考答案
SELECT city AS 城市, COUNT(*) AS 人数, ROUND(AVG(age), 1) AS 平均年龄
FROM lesson02_students GROUP BY city ORDER BY 平均年龄 DESC, 城市;
+--------+--------+--------------+
| 城市 | 人数 | 平均年龄 |
+--------+--------+--------------+
| 广州 | 1 | 25.0 |
| 杭州 | 1 | 24.0 |
| 北京 | 1 | 23.0 |
| 深圳 | 1 | 22.0 |
| 上海 | 1 | 21.0 |
+--------+--------+--------------+
5 rows in set (0.00 sec)
注意每一组只有 1 人,所以平均年龄就是本人的年龄。第二个排序条件 城市 是为了让并列时不乱序。
**3. 想让 HAVING 人数 > 1 真的返回一行,你会怎么改数据?改完记得改回来。**
参考答案 参考答案
往北京再加一个人。用 id = 8,因为 7 在上一节练习里用过,虽然已经删了,也不要和它撞号:
INSERT INTO lesson02_students (id, name, age, city, joined)
VALUES (8, '何静', 27, '北京', '2024-04-10');
Query OK, 1 row affected (0.00 sec)
SELECT city AS 城市, COUNT(*) AS 人数 FROM lesson02_students
GROUP BY city HAVING 人数 > 1 ORDER BY 城市;
+--------+--------+
| 城市 | 人数 |
+--------+--------+
| 北京 | 2 |
+--------+--------+
1 row in set (0.00 sec)
这个时候总人数会变成 6、年龄合计变成 142——所以演示完必须删掉这一行:
DELETE FROM lesson02_students WHERE id = 8;
Query OK, 1 row affected (0.00 sec)
还没完。删完不算结束,必须再查一次:
SELECT COUNT(*) AS 人数 FROM lesson02_students;
+--------+
| 人数 |
+--------+
| 5 |
+--------+
1 row in set (0.00 sec)
回到 5 行才算收干净,否则下一单元会从错误的数据开始。
本单元验收
DESC lesson02_students;能说出 5 个字段,知道id是主键。- 行数:
SELECT COUNT(*) AS 人数 FROM lesson02_students;
+--------+
| 人数 |
+--------+
| 5 |
+--------+
1 row in set (0.00 sec)
- 三列汇总合在一句里,能说出结果为什么只有 1 行:
SELECT ROUND(AVG(age), 1) AS 平均年龄, MIN(age) AS 最低年龄, MAX(age) AS 最高年龄 FROM lesson02_students;
+--------------+--------------+--------------+
| 平均年龄 | 最低年龄 | 最高年龄 |
+--------------+--------------+--------------+
| 23.0 | 21 | 25 |
+--------------+--------------+--------------+
1 row in set (0.00 sec)
- 一句分组,能解释为什么结果是 5 行、每行 1 个人。
SELECT city AS 城市, COUNT(*) AS 人数 FROM lesson02_students
GROUP BY city HAVING 人数 > 1 ORDER BY 城市;
Empty set (0.00 sec)
能说出“现在是空集,因为每个城市只有 1 个人”。
- 看到
1064(WHERE里写了COUNT(*))和1140(没分组就输出裸列)时,知道该改哪一部分。
做到这里
做到这里今天可以停,第二单元也结束了。现在你会精确改一行、用主键删一行、并在需要时用事务把改错的东西退回来;还会用五个汇总函数和 GROUP BY / HAVING 回答“总共多少、平均多少、每个城市多少”。
下一单元用一张新的练习表 lesson03_students(不碰前两个单元的表),讲真正有多于一人的分组、排序里的中文与日期细节,以及 COUNT(列) 和 COUNT(*) 在空值上的区别。
说明 这些结果是怎么验证的(可选读)
本节所有 SQL 都在本机独立的 MySQL 8.0.46 上逐条实跑(与学习服务器同版本号,不是同一台),实例使用私有数据目录和 Unix socket、不监听网络,也没有连接你的阿里云学习库。
验证按学习顺序执行:第 4 节的更新、第 5 节的删除与三次事务、第 6 节的汇总,每次改完都用另一条独立连接核对表状态,确保第 6 节的 5 行、115、23.0、21、25 是从上一步真实走过来的,而不是另一份数据。
Empty set、1064、1140 三种结果都是实跑的输出。练习里“加一个人再删掉”的答案按最终 5 行验收:加进去时总人数 6、年龄合计 142,删掉后回到 5 行。