MySQL不看视频 · SSH 实操

第三单元 · 空值、多人分组、排序细节 · 第 7 / 21 节

两个 COUNT 不一样:空值、分组、排序

建本节自己的表 lesson03_students(8 行、3 处空值),看清 COUNT(*) 与 COUNT(列) 差在哪、平均值按几个人算;让空值单独成组,用 HAVING 筛出“一个城市两个人”的组;最后核对 NULL 在升序/降序中的位置、多列排序,以及中文排序由列的排序规则决定、不是拼音。

7 个步骤 · 进度只存本机

约 22 分钟。这一节只查不改:先弄清为什么“数行”和“数城市”差 1,再把“一个城市两个人”真正分出来,最后把中文和日期的排序看准。

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

已完成 0 / 7 步

你现在在哪个提示符

应该在 mysql>。断了就先回到学习终端(PowerShell):

SSH / Linux 终端bash
ssh root@8.148.204.152
SSH / Linux 终端bash
mysql-learning

进去以后确认当前库:

MySQL 终端sql
SELECT DATABASE();
输出示例(只读,不要粘贴执行)text不可复制
+------------+
| DATABASE() |
+------------+
| itcast     |
+------------+
1 row in set (0.00 sec)

显示 itcast 就对了。如果是 NULL,执行 USE itcast;——它只切换当前连接用哪个库,不动数据。

第一步:建表,8 行里留 3 个空

第三单元用一张自己的表 lesson03_students:第二单元那 5 个人,再加 3 个新同事。先确认没有同名表:

MySQL 终端sql
SHOW TABLES LIKE 'lesson03_students';
输出示例(只读,不要粘贴执行)text不可复制
Empty set (0.00 sec)

Empty set 说明可以建。如果显示了一行,说明你已经建过,先跳到本节「常见问题」核对起点,不用再建一次。

MySQL 终端sql
CREATE TABLE lesson03_students (
  id INT PRIMARY KEY COMMENT '学号',
  name VARCHAR(20) NOT NULL COMMENT '姓名',
  age TINYINT UNSIGNED COMMENT '年龄',
  city VARCHAR(20) COMMENT '所在城市',
  joined DATE COMMENT '加入日期'
) COMMENT='本教程第三单元练习表,与视频示例无关';
输出示例(只读,不要粘贴执行)text不可复制
Query OK, 0 rows affected (0.03 sec)
MySQL 终端sql
INSERT INTO lesson03_students (id, name, age, city, joined) VALUES
  (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'),
  (6, '孙悦', NULL, '上海', '2024-04-01'),
  (7, '周舟', 23, '北京', '2024-02-28'),
  (8, '吴迪', 27, NULL, NULL);
输出示例(只读,不要粘贴执行)text不可复制
Query OK, 8 rows affected (0.00 sec)
Records: 8  Duplicates: 0  Warnings: 0

表里已经有这 8 行时,再把这段 INSERT 跑一遍会报 ERROR 1062 (23000): Duplicate entry '1' for key 'lesson03_students.PRIMARY'——主键在拦重复,原来的 8 行不会被改坏;用下面的 SELECT … ORDER BY id 核对即可。

MySQL 终端sql
SELECT id, name, age, city, joined FROM lesson03_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 |
|  6 | 孙悦   | NULL | 上海   | 2024-04-01 |
|  7 | 周舟   |   23 | 北京   | 2024-02-28 |
|  8 | 吴迪   |   27 | NULL   | NULL       |
+----+--------+------+--------+------------+
8 rows in set (0.00 sec)

8 行里有 3 处是故意空的:id = 6 的孙悦没填年龄,id = 8 的吴迪没填城市、也没有加入日期。空值在 SQL 里写作 NULL,读作“不知道”——它不等于 0,也不等于空字符串。这一节剩下的时间基本都在和它打交道。

为什么另建一张表,而不是往 lesson02_students 里加人:那张表是你的练习现场,可能已经被改过;这一节又要放三个空值,混进第二单元的验收状态,两边的“应该是几行”就都说不清了。新表不读、也不写前两个单元的表。

第二步:COUNT(*) 和 COUNT(city) 差 1

四个数一起看:

MySQL 终端sql
SELECT COUNT(*) AS 行数, COUNT(age) AS 有年龄, COUNT(city) AS 有城市, COUNT(joined) AS 有日期
FROM lesson03_students;
输出示例(只读,不要粘贴执行)text不可复制
+--------+-----------+-----------+-----------+
| 行数   | 有年龄    | 有城市    | 有日期    |
+--------+-----------+-----------+-----------+
|      8 |         7 |         7 |         7 |
+--------+-----------+-----------+-----------+
1 row in set (0.00 sec)

COUNT(*) 是 8,另外三个都是 7。差别只来自空值:COUNT(*) 数“有几行”,COUNT(列) 数“这一列有几行不是 NULL”。孙悦没有年龄,COUNT(age) 少数一个;吴迪没有城市和日期,COUNT(city)、COUNT(joined) 各少数一个。

讲义 2.6.4 对这一点的原话是“NULL 值是不参与所有聚合函数运算的”(p45)。这条规则不只管 COUNT:

MySQL 终端sql
SELECT ROUND(AVG(age), 1) AS 平均年龄, ROUND(SUM(age) / COUNT(*), 1) AS 总和除以行数
FROM lesson03_students;
输出示例(只读,不要粘贴执行)text不可复制
+--------------+--------------------+
| 平均年龄     | 总和除以行数       |
+--------------+--------------------+
|         23.6 |               20.6 |
+--------------+--------------------+
1 row in set (0.00 sec)

同一列年龄,两种算法差 3.0。AVG(age) 的分母是 7 个填了年龄的人;SUM(age) / COUNT(*) 把 8 行都算进分母,等于默认“没填的那个人年龄是 0”。所以看到平均值,先问一句:它是按几个人算的。

提示 卡住:想确认分母到底是几

把 AVG(age) 和 COUNT(age) 写在同一条 SELECT 里,两个数并排出现,分母就不是猜的了。

第三步:一个城市两个人,只把这两组挑出来

先按城市分组,每组的行数一次看全:

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

6 行:北京、上海 各 2 人,广州、杭州、深圳 各 1 人,第一行 NULL 也是一组——吴迪没填城市,MySQL 把它单独当成一组,而不是悄悄丢掉。这一点讲义没有写,是本机 8.0.46 上实测的结果。

要把“人多的组”留下,条件写在 HAVING 里:

MySQL 终端sql
SELECT city AS 城市, COUNT(*) AS 人数 FROM lesson03_students
GROUP BY city HAVING COUNT(*) >= 2
ORDER BY 人数 DESC, 城市;
输出示例(只读,不要粘贴执行)text不可复制
+--------+--------+
| 城市   | 人数   |
+--------+--------+
| 上海   |      2 |
| 北京   |      2 |
+--------+--------+
2 rows in set (0.01 sec)

只剩 上海 和 北京。HAVING 筛的是“组”,比较对象是每一组汇总出来的那个数(这里是人数 2),不是某一行。

同样是“只要填了城市的”,还能用 WHERE 先把吴迪那行去掉:

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

WHERE 和 HAVING 的分工,讲义 2.6.5 讲得很清楚(p46):WHERE 在分组之前过滤行,HAVING 在分组之后过滤组。反过来说,WHERE 里不能判断聚合值——它执行的时候还没有“组”。

说明 常见报错:WHERE 里写 COUNT(*)
MySQL 终端sql
SELECT city, COUNT(*) AS 人数 FROM lesson03_students WHERE COUNT(*) > 1 GROUP BY city;
输出示例(只读,不要粘贴执行)text不可复制
ERROR 1111 (HY000): Invalid use of group function

1111 的意思是聚合函数用错了地方。要筛组,把条件搬到 HAVING。

第 6 节里那个 1064 不是同一回事:那次是把 WHERE 整段写到了 GROUP BY 后面,属于子句顺序错;这次 WHERE 的位置是对的,错在它里面放了 COUNT。

说明 常见报错:分组后还输出没分组的列
MySQL 终端sql
SELECT city, name, COUNT(*) AS 人数 FROM lesson03_students GROUP BY city;
输出示例(只读,不要粘贴执行)text不可复制
ERROR 1055 (42000): Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'itcast.lesson03_students.name' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by

1055:name 既不是分组列,也不是聚合值,MySQL 不知道该给你这一组里的哪一个人。第 6 节见过的 1140 是它的“没有 GROUP BY”版本,两个错误码说的是同一条规则。

第四步:组里也藏着空值

分组之后再同时看人数和平均年龄:

MySQL 终端sql
SELECT city AS 城市, COUNT(*) AS 人数, COUNT(age) AS 有年龄, ROUND(AVG(age), 1) AS 平均年龄
FROM lesson03_students GROUP BY city ORDER BY 城市;
输出示例(只读,不要粘贴执行)text不可复制
+--------+--------+-----------+--------------+
| 城市   | 人数   | 有年龄    | 平均年龄     |
+--------+--------+-----------+--------------+
| NULL   |      1 |         1 |         27.0 |
| 上海   |      2 |         1 |         21.0 |
| 北京   |      2 |         2 |         23.0 |
| 广州   |      1 |         1 |         25.0 |
| 杭州   |      1 |         1 |         24.0 |
| 深圳   |      1 |         1 |         22.0 |
+--------+--------+-----------+--------------+
6 rows in set (0.00 sec)

上海 有 2 个人,但“有年龄”只有 1,平均 21.0——这个平均值只代表李娜一个人,孙悦没参与。北京 是 2 个人、2 个年龄,平均 23.0。同一列 平均年龄,两个组的“按几个人算”并不一样。

这就是这一节最实用的一句话:**人数用 COUNT(*),平均用 AVG(列),两者分母不同;要放在一起解释时,把 COUNT(列) 也查出来。**

第五步:按日期排序,空值排在哪

MySQL 终端sql
SELECT id, name, joined FROM lesson03_students ORDER BY joined DESC;
输出示例(只读,不要粘贴执行)text不可复制
+----+--------+------------+
| id | name   | joined     |
+----+--------+------------+
|  6 | 孙悦   | 2024-04-01 |
|  5 | 陈晨   | 2024-03-12 |
|  4 | 赵敏   | 2024-03-09 |
|  2 | 李娜   | 2024-03-05 |
|  1 | 张伟   | 2024-03-02 |
|  7 | 周舟   | 2024-02-28 |
|  3 | 王强   | 2024-02-20 |
|  8 | 吴迪   | NULL       |
+----+--------+------------+
8 rows in set (0.00 sec)

从新到旧,最后一行是吴迪——他的 joined 是 NULL。MySQL 把 NULL 当成“最小的值”:降序时它落在最后,升序时它落在最前。

多列排序:

MySQL 终端sql
SELECT id, name, age FROM lesson03_students ORDER BY age DESC, id ASC;
输出示例(只读,不要粘贴执行)text不可复制
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  8 | 吴迪   |   27 |
|  3 | 王强   |   25 |
|  5 | 陈晨   |   24 |
|  1 | 张伟   |   23 |
|  7 | 周舟   |   23 |
|  4 | 赵敏   |   22 |
|  2 | 李娜   |   21 |
|  6 | 孙悦   | NULL |
+----+--------+------+
8 rows in set (0.00 sec)

张伟 和 周舟 都是 23 岁,第一列分不出先后,才用第二列 id 升序决定谁在前。DESC 只跟紧邻的那一列,不会延续到后面的列(讲义 2.6.6,p47)。

第六步:中文排序不是拼音

MySQL 终端sql
SELECT id, name, city FROM lesson03_students ORDER BY city ASC;
输出示例(只读,不要粘贴执行)text不可复制
+----+--------+--------+
| id | name   | city   |
+----+--------+--------+
|  8 | 吴迪   | NULL   |
|  2 | 李娜   | 上海   |
|  6 | 孙悦   | 上海   |
|  1 | 张伟   | 北京   |
|  7 | 周舟   | 北京   |
|  3 | 王强   | 广州   |
|  5 | 陈晨   | 杭州   |
|  4 | 赵敏   | 深圳   |
+----+--------+--------+
8 rows in set (0.00 sec)

第一行是 NULL(升序,空值最前),接着是 上海,然后才是 北京。按拼音,北京(B)该在上海(S)前面;这里反了。这一列用的是排序规则 utf8mb4_0900_ai_ci——UCA 9.0.0 的通用规则,没有中文拼音定制,汉字按这套规则的权重排;这两个字恰好与码点 U+4E0A < U+5317 同序。决定顺序的是列的排序规则,不是把两段 UTF-8 字节直接比大小:同一列换成中文拼音规则 utf8mb4_zh_0900_as_cs,北京 就会排到 上海 前面(B < S)。

所以中文列排出来的顺序,不能当成“字母顺序”或“拼音顺序”来读;换一种排序规则结果也可能不同。真要按拼音排得另外转换,不在本节范围。

反方向看一遍:

MySQL 终端sql
SELECT id, name, city FROM lesson03_students ORDER BY city DESC;
输出示例(只读,不要粘贴执行)text不可复制
+----+--------+--------+
| id | name   | city   |
+----+--------+--------+
|  4 | 赵敏   | 深圳   |
|  5 | 陈晨   | 杭州   |
|  3 | 王强   | 广州   |
|  1 | 张伟   | 北京   |
|  7 | 周舟   | 北京   |
|  2 | 李娜   | 上海   |
|  6 | 孙悦   | 上海   |
|  8 | 吴迪   | NULL   |
+----+--------+--------+
8 rows in set (0.00 sec)

NULL 又回到最后一行。空值的位置只由升序/降序决定,跟这一列存的是什么无关。

说明 常见报错:WHERE 里用 SELECT 起的别名
MySQL 终端sql
SELECT city AS 城市, COUNT(*) AS 人数 FROM lesson03_students WHERE 城市 = '上海' GROUP BY city;
输出示例(只读,不要粘贴执行)text不可复制
ERROR 1054 (42S22): Unknown column '城市' in 'where clause'

1054:WHERE 在 SELECT 之前执行,那时“城市”这个别名还不存在。条件里写原始列名 city 就好。别名可以用在 ORDER BY 和 HAVING 里,因为它们排在 SELECT 之后。

第七步:一条查询把这一节连起来

要一份“人多的城市、人数、平均年龄”的清单:

MySQL 终端sql
SELECT city AS 城市, COUNT(*) AS 人数, ROUND(AVG(age), 1) AS 平均年龄
FROM lesson03_students
WHERE city IS NOT NULL
GROUP BY city
HAVING COUNT(*) >= 2
ORDER BY 人数 DESC, 城市;
输出示例(只读,不要粘贴执行)text不可复制
+--------+--------+--------------+
| 城市   | 人数   | 平均年龄     |
+--------+--------+--------------+
| 上海   |      2 |         21.0 |
| 北京   |      2 |         23.0 |
+--------+--------+--------------+
2 rows in set (0.00 sec)

一句里五个子句各干一件事:WHERE 去掉没有城市的那行,GROUP BY 按城市分堆,HAVING 只留人数 ≥ 2 的堆,SELECT 挑列并算平均,ORDER BY 先按人数降序、人数相同再按城市。书写顺序和执行顺序不是一回事(讲义 2.6.9,p49,第 6 节也讲过),这里是把它们一次用全。

常见问题:断线、重来、回学习终端

说明 SSH 断了 / 网页关了,怎么接着学

学习终端是服务器的 SSH,不是网页。重新打开 PowerShell:

SSH / Linux 终端bash
ssh root@8.148.204.152
SSH / Linux 终端bash
mysql-learning
MySQL 终端sql
USE itcast;
输出示例(只读,不要粘贴执行)text不可复制
Database changed

然后核对这一节的起点(表在不在、几行):

MySQL 终端sql
SELECT COUNT(*) AS 行数, COUNT(age) AS 有年龄 FROM lesson03_students;
输出示例(只读,不要粘贴执行)text不可复制
+--------+-----------+
| 行数   | 有年龄    |
+--------+-----------+
|      8 |         7 |
+--------+-----------+
1 row in set (0.00 sec)

8 和 7 就对了,可以直接做练习。密码由你自己保管,网页和本教程都不保存。

说明 想从本节开头重做

这一节只建一张表、只做查询,不改任何数据,所以“重做”=把查询再跑一遍即可,不需要删表。

如果 SHOW TABLES LIKE 'lesson03_students'; 是空的(换了实例,或者第一次没建成功),回到第一步重新建:CREATE TABLE 和 INSERT 那两段就是全部起点。

不要 DROP DATABASE itcast、不要 TRUNCATE、不要“重置整个库”:itcast 里还有课程的数据和你前两个单元的练习表。也不要动 lesson01_students、lesson02_students,本节完全不依赖它们。

常见报错怎么办

看到意思怎么办
1054 Unknown column列名或别名找不到用 DESC 表名 核对列名;WHERE 里不要用 SELECT 的别名
1055 ... not in GROUP BY分组后输出了没分组的列去掉那列,或加进 GROUP BY,或套一个聚合函数
1111 Invalid use of group function聚合函数放错位置筛组用 HAVING,筛行用 WHERE
1146 Table ... doesn't exist表不在当前库SELECT DATABASE(); 看是不是 itcast,再 SHOW TABLES LIKE 'lesson03_students';

练习

先自己做,再展开提示和对答案。前两题是基础,接着两题要组合,最后一题只问概念。

基础 1. 有几个人没填城市?用一条查询说明。

提示 卡住时再看

“没填”就是这一列是 NULL:COUNT(*) 数行,COUNT(city) 数填了的,两者相减。

参考答案 参考答案
MySQL 终端sql
SELECT COUNT(*) AS 行数, COUNT(city) AS 有城市, COUNT(*) - COUNT(city) AS 没填城市
FROM lesson03_students;
输出示例(只读,不要粘贴执行)text不可复制
+--------+-----------+--------------+
| 行数   | 有城市    | 没填城市     |
+--------+-----------+--------------+
|      8 |         7 |            1 |
+--------+-----------+--------------+
1 row in set (0.00 sec)

8 − 7 = 1,只有吴迪没填。

基础 2. 人数恰好 1 人的城市有几个?NULL 那一组算不算?

提示 卡住时再看

条件写 HAVING COUNT(*) = 1。先不加条件,数一数上面分组结果有几行。

参考答案 参考答案
MySQL 终端sql
SELECT city AS 城市, COUNT(*) AS 人数 FROM lesson03_students
GROUP BY city HAVING COUNT(*) = 1 ORDER BY 城市;
输出示例(只读,不要粘贴执行)text不可复制
+--------+--------+
| 城市   | 人数   |
+--------+--------+
| NULL   |      1 |
| 广州   |      1 |
| 杭州   |      1 |
| 深圳   |      1 |
+--------+--------+
4 rows in set (0.00 sec)

4 行:NULL、广州、杭州、深圳。NULL 单独成组,也按 1 人计算——想排除它,就在 WHERE 里写 city IS NOT NULL。

进阶 3. 每个城市的人数与平均年龄,只保留平均年龄 ≥ 23 的组,按平均年龄从高到低。

提示 卡住时再看

HAVING 里可以直接写 AVG(age);排序用 SELECT 里起的别名。

参考答案 参考答案
MySQL 终端sql
SELECT city AS 城市, COUNT(*) AS 人数, ROUND(AVG(age), 1) AS 平均年龄
FROM lesson03_students
GROUP BY city
HAVING AVG(age) >= 23
ORDER BY 平均年龄 DESC, 城市;
输出示例(只读,不要粘贴执行)text不可复制
+--------+--------+--------------+
| 城市   | 人数   | 平均年龄     |
+--------+--------+--------------+
| NULL   |      1 |         27.0 |
| 广州   |      1 |         25.0 |
| 杭州   |      1 |         24.0 |
| 北京   |      2 |         23.0 |
+--------+--------+--------------+
4 rows in set (0.00 sec)

NULL 那组(吴迪 27 岁)也满足条件,所以出现在第一行。上海 是 21.0,被筛掉——这也说明“人数 2”不代表“平均年龄算的是 2 个人”。

进阶 4. 按加入日期从早到晚排,第一行是谁?为什么?

提示 卡住时再看

升序时 NULL 在最前。

参考答案 参考答案
MySQL 终端sql
SELECT id, name, joined FROM lesson03_students ORDER BY joined ASC;
输出示例(只读,不要粘贴执行)text不可复制
+----+--------+------------+
| id | name   | joined     |
+----+--------+------------+
|  8 | 吴迪   | NULL       |
|  3 | 王强   | 2024-02-20 |
|  7 | 周舟   | 2024-02-28 |
|  1 | 张伟   | 2024-03-02 |
|  2 | 李娜   | 2024-03-05 |
|  4 | 赵敏   | 2024-03-09 |
|  5 | 陈晨   | 2024-03-12 |
|  6 | 孙悦   | 2024-04-01 |
+----+--------+------------+
8 rows in set (0.00 sec)

第一行是吴迪,joined 为 NULL。他没有加入日期,MySQL 把 NULL 当最小的值,升序就排在最前。想让没填日期的排最后,得显式调整(本节不展开)。

概念 5. 这张表 8 行,为什么说“平均年龄 23.6”不是 8 个人的平均?

参考答案 参考答案

AVG(age) 只把填了年龄的 7 行放进分母(讲义 2.6.4:NULL 不参与聚合)。硬按 8 行算,等于把孙悦的空年龄当成 0,结果是 20.6——那是另一种口径,不是这张表的平均年龄。回答这类问题,先说清分母是几。

本单元验收

做到下面 5 条,这一节就完成了。每条都给出你应该看到的真实结果。

  1. SELECT COUNT(*), COUNT(age) FROM lesson03_students; 得到 8 和 7。
  2. GROUP BY city 得到 6 组,北京、上海 各 2 人,NULL 自成一组。
  3. HAVING COUNT(*) >= 2 只剩 上海、北京 两行。
  4. 分组后 上海 是 2 人、COUNT(age) 为 1、平均 21.0——能解释为什么。
  5. ORDER BY city ASC 时第一行是 NULL,且 上海 排在 北京 前面——能说出这是本表排序规则(utf8mb4_0900_ai_ci)的结果、不是拼音;换排序规则顺序会变。

做到这里

做到这里今天可以停。第三单元只有这一节:你现在能分清“数行”和“数列”,能让空值单独成组,也能在分组后按汇总值筛组、按中文和日期排序。

下一节的方向是把这张表和另一张表对起来看(课程后面的多表查询)。页面做好后会接在这里,不提供空链接。

说明 这一节和视频课、讲义是什么关系(可选读)

主题对应黑马程序员《MySQL 数据库入门到精通》基础篇的 P17 聚合函数、P18 分组查询、P19 排序查询、P22 执行顺序(BV1Kr4y1i7ru,平台 part 17–19、22,在 P01–P50 范围内)。视频只作主题对照,本页不嵌入、不播放,也不声称逐句对应讲师讲解。

文字底稿核对的是同版讲义《MySQL-基础篇》的公开镜像(95 页 PDF,SHA256 bc5e2916…c08510,仓库 shuhongfan/MySql_Demo02,commit e131315…):2.6.4 聚合函数 p45(NULL 不参与聚合、COUNT(*) 与 COUNT(字段) 的区别)、2.6.5 分组查询 p46(WHERE/HAVING 分工、多字段分组)、2.6.6 排序查询 p47(ASC/DESC、多字段排序)、2.6.9 执行顺序 p49。该镜像不是官方发布渠道,正文版权属原出版方;本页只做主题核对,不转载讲义原文。

讲义没有写到的三处——NULL 在 GROUP BY 里单独成组、NULL 在升序/降序中的位置、中文在本表排序规则下的顺序(非拼音)——都是在本机 MySQL 8.0.46 上实测后补充的,并按实测结果写;排序规则与 UCA 权重的定义见 MySQL 8.0 手册 12.10.1 Unicode Character Sets。

本页的表名、8 行数据、查询和练习均为本教程原创;字段沿用讲义 emp 的语义(姓名、年龄、工作城市、入职日期),但没有使用讲师的 emp 数据,也没有复制课堂 SQL。页面里每一段输出都在本机隔离实例上逐条实跑(与学习服务器同版本号,不是同一台);0.00 sec 是测试机耗时,你的屏幕可能不同,行列内容一致。