MySQL不看视频 · SSH 实操

第二单元 · 精准修改、安全删除、基础汇总 · 第 6 / 21 节

五个汇总函数,加上按城市分组

COUNT / SUM / AVG / MIN / MAX 与 WHERE 组合,GROUP BY 分组、ORDER BY 按汇总值排序、HAVING 筛组;看清 1064(WHERE 里写 COUNT)与 1140(未分组裸列)的原因。

7 个步骤 · 进度只存本机

约 22 分钟。这一节不改数据,只回答“一共多少人”“平均多大”“哪个城市人多”这类问题。

SSH / Linux 终端MySQL 终端输出示例(只读)复制按钮只复制命令,不含 $ / mysql> 提示符和输出。

已完成 0 / 7 步

你现在在哪个提示符

应该在 mysql>。断了就 ssh root@8.148.204.152 → mysql-learning,然后核对起点:

MySQL 终端sql
SELECT id, name, age, city, joined FROM lesson02_students ORDER BY id;
输出示例(只读,不要粘贴执行)text不可复制
+----+--------+------+--------+------------+
| 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) 没有意义,汇总函数用在数字列上才有意义。

先数人数

MySQL 终端sql
SELECT COUNT(*) AS 人数 FROM lesson02_students;
输出示例(只读,不要粘贴执行)text不可复制
+--------+
| 人数   |
+--------+
|      5 |
+--------+
1 row in set (0.00 sec)

COUNT(*) 里的 * 表示“数行”,不去看这一行每列有没有值。5 行就是 5。上一节数行数用的也是它,这里把它正式归进“汇总”。

AS 人数 是给这一列起的中文名,只影响显示的表头,不影响数据。

一次算完五项

MySQL 终端sql
SELECT COUNT(*) AS 人数, SUM(age) AS 年龄合计, AVG(age) AS 平均年龄,
MIN(age) AS 最低年龄, MAX(age) AS 最高年龄 FROM lesson02_students;
输出示例(只读,不要粘贴执行)text不可复制
+--------+--------------+--------------+--------------+--------------+
| 人数   | 年龄合计     | 平均年龄     | 最低年龄     | 最高年龄     |
+--------+--------------+--------------+--------------+--------------+
|      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 是个普通函数,作用是把数字按指定位数四舍五入:

MySQL 终端sql
SELECT ROUND(AVG(age), 1) AS 平均年龄 FROM lesson02_students;
输出示例(只读,不要粘贴执行)text不可复制
+--------------+
| 平均年龄     |
+--------------+
|         23.0 |
+--------------+
1 row in set (0.00 sec)

ROUND(AVG(age), 1) 的意思是“先算平均,再把结果留 1 位小数”。ROUND 可以套在任何数字外面,不只 AVG。

汇总和 WHERE 一起用

WHERE 先筛行,汇总函数再对筛出来的行算——顺序是先筛后算,不是反过来。先看看 age >= 23 是谁:

MySQL 终端sql
SELECT id, name, age FROM lesson02_students WHERE age >= 23 ORDER BY age;
输出示例(只读,不要粘贴执行)text不可复制
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | 张伟   |   23 |
|  5 | 陈晨   |   24 |
|  3 | 王强   |   25 |
+----+--------+------+
3 rows in set (0.00 sec)

三个人,年龄 23、24、25。对这三行做汇总:

MySQL 终端sql
SELECT COUNT(*) AS 人数, SUM(age) AS 年龄合计 FROM lesson02_students WHERE age >= 23;
输出示例(只读,不要粘贴执行)text不可复制
+--------+--------------+
| 人数   | 年龄合计     |
+--------+--------------+
|      3 |           72 |
+--------+--------------+
1 row in set (0.00 sec)

3 行、72 岁,和上一条 SELECT 数出来的完全一致。先跑明细、再跑汇总,两个数字能对上,这是检查汇总有没有写错的好办法。

按城市分组:GROUP BY

上面的问题都是“整张表一起算”。如果要“每个城市各有多少人”,需要先按 city 把行分成几堆,再对每一堆分别汇总。这一步叫分组,写法是 GROUP BY city。

MySQL 终端sql
SELECT city AS 城市, COUNT(*) AS 人数 FROM lesson02_students GROUP BY city ORDER BY 城市;
输出示例(只读,不要粘贴执行)text不可复制
+--------+--------+
| 城市   | 人数   |
+--------+--------+
| 上海   |      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 是在分组和汇总之后才执行的,所以这里能直接用别名。

也可以对每组算平均值(这一节只放一条分组平均,够用就行;多个汇总并列的写法下一单元展开):

MySQL 终端sql
SELECT city AS 城市, ROUND(AVG(age), 1) AS 平均年龄
FROM lesson02_students GROUP BY city ORDER BY 城市;
输出示例(只读,不要粘贴执行)text不可复制
+--------+--------------+
| 城市   | 平均年龄     |
+--------+--------------+
| 上海   |         21.0 |
| 北京   |         23.0 |
| 广州   |         25.0 |
| 杭州   |         24.0 |
| 深圳   |         22.0 |
+--------+--------------+

现在每个城市只有一个人,所以“平均年龄”就等于本人的年龄。等以后某个城市有两个人,这一列才会真的开始“平均”。

ORDER BY 也可以按汇总结果排,比如按人数从多到少:

MySQL 终端sql
SELECT city AS 城市, COUNT(*) AS 人数 FROM lesson02_students
GROUP BY city ORDER BY 人数 DESC, 城市;
输出示例(只读,不要粘贴执行)text不可复制
+--------+--------+
| 城市   | 人数   |
+--------+--------+
| 上海   |      1 |
| 北京   |      1 |
| 广州   |      1 |
| 杭州   |      1 |
| 深圳   |      1 |
+--------+--------+
5 rows in set (0.00 sec)

五个人人数都一样,ORDER BY 人数 DESC 排不出先后,所以加了第二个排序条件 城市,保证顺序稳定。这个习惯在第 3 节就养成了:并列时再补一个不会重复的列。

HAVING:筛“组”,不是筛“行”

WHERE 在分组之前筛行,它看不到 COUNT(*) 的结果。想筛“人数大于 1 的城市”,要用 HAVING——它在分组和汇总之后执行。

先记住分组后的样子(上面那条查询的结果):

输出示例(只读,不要粘贴执行)text不可复制
+--------+--------+
| 城市   | 人数   |
+--------+--------+
| 上海   |      1 |
| 北京   |      1 |
| 广州   |      1 |
| 杭州   |      1 |
| 深圳   |      1 |
+--------+--------+
5 rows in set (0.00 sec)

现在给这 5 组加一个“人数大于 1”的门槛:

MySQL 终端sql
SELECT city AS 城市, COUNT(*) AS 人数 FROM lesson02_students
GROUP BY city HAVING 人数 > 1 ORDER BY 城市;
输出示例(只读,不要粘贴执行)text不可复制
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 会在解析阶段就拦下来:

输出示例(只读,不要粘贴执行)text不可复制
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 拒绝:

输出示例(只读,不要粘贴执行)text不可复制
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 说清楚“按谁分组”。

一条查询从书写到执行

写的时候是这个顺序:

输出示例(只读,不要粘贴执行)text不可复制
SELECT 列 FROM 表 WHERE 条件 GROUP BY 分组列 HAVING 组条件 ORDER BY 排序列;

MySQL 处理时是这个顺序:

输出示例(只读,不要粘贴执行)text不可复制
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 行——下一单元会接着用它们。

重复学习怎么办

说明 数字对不上时按这个顺序查

汇总查询不改数据,重复跑一百次结果都一样。所以“数字不对”几乎总是数据状态不对。按顺序核:

MySQL 终端sql
SELECT COUNT(*) AS 人数 FROM lesson02_students;
输出示例(只读,不要粘贴执行)text不可复制
+--------+
| 人数   |
+--------+
|      5 |
+--------+
1 row in set (0.00 sec)
  1. 人数是 5 吗?如果是 6,说明第 5 节的删除没做或没生效,回第 5 节。
  2. 如果是 4,看看是不是被你自己在练习里删掉了某一行:SELECT * FROM lesson02_students ORDER BY id;。
  3. 人数对了但 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 个人的最高年龄和人数。

参考答案 参考答案
MySQL 终端sql
SELECT COUNT(*) AS 人数, MAX(age) AS 最高年龄 FROM lesson02_students;
输出示例(只读,不要粘贴执行)text不可复制
+--------+--------------+
| 人数   | 最高年龄     |
+--------+--------------+
|      5 |           25 |
+--------+--------------+
1 row in set (0.00 sec)

最高的是王强 25 岁。结果永远只有 1 行。

2. 按城市分组,每个城市的人数和平均年龄一起列出来,按平均年龄从高到低排。

参考答案 参考答案
MySQL 终端sql
SELECT city AS 城市, COUNT(*) AS 人数, ROUND(AVG(age), 1) AS 平均年龄
FROM lesson02_students GROUP BY city ORDER BY 平均年龄 DESC, 城市;
输出示例(只读,不要粘贴执行)text不可复制
+--------+--------+--------------+
| 城市   | 人数   | 平均年龄     |
+--------+--------+--------------+
| 广州   |      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 在上一节练习里用过,虽然已经删了,也不要和它撞号:

MySQL 终端sql
INSERT INTO lesson02_students (id, name, age, city, joined)
VALUES (8, '何静', 27, '北京', '2024-04-10');
输出示例(只读,不要粘贴执行)text不可复制
Query OK, 1 row affected (0.00 sec)
MySQL 终端sql
SELECT city AS 城市, COUNT(*) AS 人数 FROM lesson02_students
GROUP BY city HAVING 人数 > 1 ORDER BY 城市;
输出示例(只读,不要粘贴执行)text不可复制
+--------+--------+
| 城市   | 人数   |
+--------+--------+
| 北京   |      2 |
+--------+--------+
1 row in set (0.00 sec)

这个时候总人数会变成 6、年龄合计变成 142——所以演示完必须删掉这一行:

MySQL 终端sql
DELETE FROM lesson02_students WHERE id = 8;
输出示例(只读,不要粘贴执行)text不可复制
Query OK, 1 row affected (0.00 sec)

还没完。删完不算结束,必须再查一次:

MySQL 终端sql
SELECT COUNT(*) AS 人数 FROM lesson02_students;
输出示例(只读,不要粘贴执行)text不可复制
+--------+
| 人数   |
+--------+
|      5 |
+--------+
1 row in set (0.00 sec)

回到 5 行才算收干净,否则下一单元会从错误的数据开始。

本单元验收

  1. DESC lesson02_students; 能说出 5 个字段,知道 id 是主键。
  2. 行数:
MySQL 终端sql
SELECT COUNT(*) AS 人数 FROM lesson02_students;
输出示例(只读,不要粘贴执行)text不可复制
+--------+
| 人数   |
+--------+
|      5 |
+--------+
1 row in set (0.00 sec)
  1. 三列汇总合在一句里,能说出结果为什么只有 1 行:
MySQL 终端sql
SELECT ROUND(AVG(age), 1) AS 平均年龄, MIN(age) AS 最低年龄, MAX(age) AS 最高年龄 FROM lesson02_students;
输出示例(只读,不要粘贴执行)text不可复制
+--------------+--------------+--------------+
| 平均年龄     | 最低年龄     | 最高年龄     |
+--------------+--------------+--------------+
|         23.0 |           21 |           25 |
+--------------+--------------+--------------+
1 row in set (0.00 sec)
  1. 一句分组,能解释为什么结果是 5 行、每行 1 个人。
MySQL 终端sql
SELECT city AS 城市, COUNT(*) AS 人数 FROM lesson02_students
GROUP BY city HAVING 人数 > 1 ORDER BY 城市;
输出示例(只读,不要粘贴执行)text不可复制
Empty set (0.00 sec)

能说出“现在是空集,因为每个城市只有 1 个人”。

  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 行。