MySQL不看视频 · SSH 实操

第九单元 · 子查询与综合练习 · 第 21 / 21 节

十二道综合题:把连接和子查询放在同一页

按讲义 5.7 的十二个需求逐条做:第 1–4 题练隐式 / 显式内连接、去重和左连接保留无部门员工;第 5–8 题练工资档位区间连接、三表连接、部门平均和标量比较;第 9–12 题练全公司平均、相关子查询、SELECT 里的计数子查询和员工技能三表连接。表名、姓名、工资与档位边界全部另取,结果不对讲义答案。

4 个步骤 · 进度只存本机

约 45 分钟。这一节按讲义 5.7 的十二个需求走一遍:前四题练连接,第五到第八题把工资档位和聚合接进来,最后四题回到子查询。原题用的是课程自带的员工表、部门表和工资等级表,本节全部换成自己的 lesson21_* 表,题目条件逐条对应,姓名、部门、工资、档位边界都另取,结果不能拿去和讲义答案对数字。

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

已完成 0 / 4 步

你现在在哪个提示符

应该在 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;。

第一步:建好五张表,先做第 1–4 题

本节前缀 lesson21_,和前面两节完全无关:

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

Empty set 才往下建;已经有表就跳过建表和插入,直接看本节末尾的终态核对,不要删表。已经建过还重跑插入,会报 1062 Duplicate entry,那是“已经做过”,不是数据错。

MySQL 终端sql
CREATE TABLE lesson21_dept (
  id   INT PRIMARY KEY,
  name VARCHAR(20) NOT NULL,
  CONSTRAINT uk_lesson21_dept_name UNIQUE (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
输出示例(只读,不要粘贴执行)text不可复制
Query OK, 0 rows affected (0.04 sec)
MySQL 终端sql
INSERT INTO lesson21_dept (id, name) VALUES (1, '研发部'), (2, '市场部'), (3, '人事部'), (4, '审计部');
输出示例(只读,不要粘贴执行)text不可复制
Query OK, 4 rows affected (0.01 sec)
Records: 4  Duplicates: 0  Warnings: 0
MySQL 终端sql
CREATE TABLE lesson21_emp (
  id         INT PRIMARY KEY,
  name       VARCHAR(20) NOT NULL,
  age        TINYINT UNSIGNED,
  dept_id    INT,
  salary     INT,
  manager_id INT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
输出示例(只读,不要粘贴执行)text不可复制
Query OK, 0 rows affected (0.03 sec)
MySQL 终端sql
INSERT INTO lesson21_emp (id, name, age, dept_id, salary, manager_id) VALUES (1, '赵启', 45, 1, 26000, NULL), (2, '钱宁', 28, 1, 12000, 1), (3, '孙悦', 34, 1, 18000, 2), (4, '李蔓', 41, 2, 15000, 1), (5, '周禾', 26, 2, 9000, 4), (6, '吴桐', 52, NULL, 30000, 1), (7, '郑楠', 30, 3, 11000, 4), (8, '王澈', 23, 3, NULL, 7), (9, '冯玥', 47, 2, 22000, 1), (10, '陈屿', 38, NULL, 16000, 2);
输出示例(只读,不要粘贴执行)text不可复制
Query OK, 10 rows affected (0.00 sec)
Records: 10  Duplicates: 0  Warnings: 0
MySQL 终端sql
CREATE TABLE lesson21_salgrade (
  grade INT PRIMARY KEY,
  losal INT NOT NULL,
  hisal INT NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
输出示例(只读,不要粘贴执行)text不可复制
Query OK, 0 rows affected (0.03 sec)
MySQL 终端sql
INSERT INTO lesson21_salgrade (grade, losal, hisal) VALUES (1, 0, 9999), (2, 10000, 14999), (3, 15000, 19999), (4, 20000, 24999), (5, 25000, 99999);
输出示例(只读,不要粘贴执行)text不可复制
Query OK, 5 rows affected (0.00 sec)
Records: 5  Duplicates: 0  Warnings: 0
MySQL 终端sql
CREATE TABLE lesson21_skill (
  id   INT PRIMARY KEY,
  name VARCHAR(20) NOT NULL,
  CONSTRAINT uk_lesson21_skill_name UNIQUE (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
输出示例(只读,不要粘贴执行)text不可复制
Query OK, 0 rows affected (0.04 sec)
MySQL 终端sql
INSERT INTO lesson21_skill (id, name) VALUES (1, 'SQL'), (2, 'Python'), (3, '报表'), (4, '沟通');
输出示例(只读,不要粘贴执行)text不可复制
Query OK, 4 rows affected (0.01 sec)
Records: 4  Duplicates: 0  Warnings: 0
MySQL 终端sql
CREATE TABLE lesson21_emp_skill (
  emp_id   INT NOT NULL,
  skill_id INT NOT NULL,
  PRIMARY KEY (emp_id, skill_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
输出示例(只读,不要粘贴执行)text不可复制
Query OK, 0 rows affected (0.05 sec)
MySQL 终端sql
INSERT INTO lesson21_emp_skill (emp_id, skill_id) VALUES (1, 1), (1, 2), (2, 1), (3, 1), (3, 3), (5, 4), (8, 2);
输出示例(只读,不要粘贴执行)text不可复制
Query OK, 7 rows affected (0.00 sec)
Records: 7  Duplicates: 0  Warnings: 0

十名员工、四个部门(审计部没人)、五档工资、四类技能、七条“谁会什么”。王澈没有工资,吴桐和陈屿没有部门,赵启没有上级——这三处空值在十二题里都会碰到。

第 1 题:查员工的姓名、年龄、工资和部门名称,用隐式内连接。 讲义原题还要“职位”,本模型没有这一列,用工资替代,这是本页的原创变换。

MySQL 终端sql
SELECT e.name AS 姓名, e.age AS 年龄, e.salary AS 工资, d.name AS 部门 FROM lesson21_emp e, lesson21_dept d WHERE e.dept_id = d.id ORDER BY e.id;
输出示例(只读,不要粘贴执行)text不可复制
+--------+--------+--------+-----------+
| 姓名   | 年龄   | 工资   | 部门      |
+--------+--------+--------+-----------+
| 赵启   |     45 |  26000 | 研发部    |
| 钱宁   |     28 |  12000 | 研发部    |
| 孙悦   |     34 |  18000 | 研发部    |
| 李蔓   |     41 |  15000 | 市场部    |
| 周禾   |     26 |   9000 | 市场部    |
| 郑楠   |     30 |  11000 | 人事部    |
| 王澈   |     23 |   NULL | 人事部    |
| 冯玥   |     47 |  22000 | 市场部    |
+--------+--------+--------+-----------+
8 rows in set (0.00 sec)

八行:十名员工里吴桐、陈屿没有部门,内连接配不上就不出现。隐式内连接把两张表写在 FROM 里用逗号隔开,连接条件放在 WHERE。

第 2 题:查年龄小于 30 岁的员工,姓名、年龄、工资、部门,用显式内连接。

MySQL 终端sql
SELECT e.name AS 姓名, e.age AS 年龄, e.salary AS 工资, d.name AS 部门 FROM lesson21_emp e INNER JOIN lesson21_dept d ON e.dept_id = d.id WHERE e.age < 30 ORDER BY e.id;
输出示例(只读,不要粘贴执行)text不可复制
+--------+--------+--------+-----------+
| 姓名   | 年龄   | 工资   | 部门      |
+--------+--------+--------+-----------+
| 钱宁   |     28 |  12000 | 研发部    |
| 周禾   |     26 |   9000 | 市场部    |
| 王澈   |     23 |   NULL | 人事部    |
+--------+--------+--------+-----------+
3 rows in set (0.00 sec)

三行:钱宁 28、周禾 26、王澈 23。吴桐 52 岁且没有部门,年龄和连接条件都把他排除了;王澈没有工资,这一列显示 NULL,不影响他入选。连接条件放 ON,过滤条件放 WHERE,分工要清楚。

第 3 题:查有员工的部门编号和名称。

MySQL 终端sql
SELECT DISTINCT d.id, d.name FROM lesson21_emp e, lesson21_dept d WHERE e.dept_id = d.id ORDER BY d.id;
输出示例(只读,不要粘贴执行)text不可复制
+----+-----------+
| id | name      |
+----+-----------+
|  1 | 研发部    |
|  2 | 市场部    |
|  3 | 人事部    |
+----+-----------+
3 rows in set (0.00 sec)

三行:1、2、3 号部门。审计部没人,不出现。DISTINCT 是必要的:研发部和市场部各有三名员工,连接后部门行会重复三次。

第 4 题:查年龄大于 40 岁的员工及其部门名称,没有部门的也要出现。

MySQL 终端sql
SELECT e.name AS 姓名, e.age AS 年龄, d.name AS 部门 FROM lesson21_emp e LEFT JOIN lesson21_dept d ON e.dept_id = d.id WHERE e.age > 40 ORDER BY e.id;
输出示例(只读,不要粘贴执行)text不可复制
+--------+--------+-----------+
| 姓名   | 年龄   | 部门      |
+--------+--------+-----------+
| 赵启   |     45 | 研发部    |
| 李蔓   |     41 | 市场部    |
| 吴桐   |     52 | NULL      |
| 冯玥   |     47 | 市场部    |
+--------+--------+-----------+
4 rows in set (0.00 sec)

四行:赵启 45、李蔓 41、吴桐 52、冯玥 47。吴桐没有部门,LEFT JOIN 把他保留下来,部门列是 NULL。这一题的关键词是“没有部门的也要出现”,看到它就选外连接,并且把员工表放在左边。

本步四题的行数先记住:8、3、3、4。后面每加一个条件,都拿行数反推一次,比盯着语法找错快。

第二步:工资档位、三表连接、平均工资、标量比较(第 5–8 题)

第 5 题:查所有员工的工资等级。

MySQL 终端sql
SELECT e.name AS 姓名, e.salary AS 工资, s.grade AS 档位, s.losal AS 下限, s.hisal AS 上限 FROM lesson21_emp e, lesson21_salgrade s WHERE e.salary BETWEEN s.losal AND s.hisal ORDER BY e.id;
输出示例(只读,不要粘贴执行)text不可复制
+--------+--------+--------+--------+--------+
| 姓名   | 工资   | 档位   | 下限   | 上限   |
+--------+--------+--------+--------+--------+
| 赵启   |  26000 |      5 |  25000 |  99999 |
| 钱宁   |  12000 |      2 |  10000 |  14999 |
| 孙悦   |  18000 |      3 |  15000 |  19999 |
| 李蔓   |  15000 |      3 |  15000 |  19999 |
| 周禾   |   9000 |      1 |      0 |   9999 |
| 吴桐   |  30000 |      5 |  25000 |  99999 |
| 郑楠   |  11000 |      2 |  10000 |  14999 |
| 冯玥   |  22000 |      4 |  20000 |  24999 |
| 陈屿   |  16000 |      3 |  15000 |  19999 |
+--------+--------+--------+--------+--------+
9 rows in set (0.00 sec)

九行。工资档位表不是按“相等”连的,而是按区间:salary BETWEEN losal AND hisal,这是一个非等值连接。BETWEEN 含两端,所以 15000 正好落在第 3 档(15000–19999)而不是第 2 档。王澈工资是 NULL,NULL BETWEEN ... 得到 unknown,不会落进任何一档,所以十个人只出了九行。

第 6 题:查研发部所有员工的工资等级。

MySQL 终端sql
SELECT e.name AS 姓名, e.salary AS 工资, s.grade AS 档位 FROM lesson21_emp e, lesson21_dept d, lesson21_salgrade s WHERE e.dept_id = d.id AND e.salary BETWEEN s.losal AND s.hisal AND d.name = '研发部' ORDER BY e.id;
输出示例(只读,不要粘贴执行)text不可复制
+--------+--------+--------+
| 姓名   | 工资   | 档位   |
+--------+--------+--------+
| 赵启   |  26000 |      5 |
| 钱宁   |  12000 |      2 |
| 孙悦   |  18000 |      3 |
+--------+--------+--------+
3 rows in set (0.00 sec)

三行:赵启 26000 第 5 档、钱宁 12000 第 2 档、孙悦 18000 第 3 档。三张表的连接条件要写全:员工对部门是等值,员工对档位是区间,少写一个就会变成笛卡尔积。

第 7 题:查研发部的平均工资。

MySQL 终端sql
SELECT AVG(e.salary) AS 研发部平均 FROM lesson21_emp e, lesson21_dept d WHERE e.dept_id = d.id AND d.name = '研发部';
输出示例(只读,不要粘贴执行)text不可复制
+-----------------+
| 研发部平均      |
+-----------------+
|      18666.6667 |
+-----------------+
1 row in set (0.00 sec)

18666.6667,即(26000 + 12000 + 18000)÷ 3。AVG 返回带四位小数的精确值,不要手工截成 18666 再比较,差一点就可能改变入选结果。

第 8 题:查工资比孙悦高的员工。

MySQL 终端sql
SELECT name AS 姓名, salary AS 工资 FROM lesson21_emp WHERE salary > (SELECT salary FROM lesson21_emp WHERE name = '孙悦') ORDER BY id;
输出示例(只读,不要粘贴执行)text不可复制
+--------+--------+
| 姓名   | 工资   |
+--------+--------+
| 赵启   |  26000 |
| 吴桐   |  30000 |
| 冯玥   |  22000 |
+--------+--------+
3 rows in set (0.00 sec)

三行:赵启 26000、吴桐 30000、冯玥 22000。内层按姓名查到孙悦 18000,外层就是“工资大于 18000”。这里能安全用 =,是因为本节数据里姓名没有重复;真实业务里姓名可能重名,内层就会返回多行并报 1242,那时要么按工号查,要么改用 IN。

第三步:子查询四连(第 9–12 题)

第 9 题:查工资比全公司平均高的员工。

MySQL 终端sql
SELECT name AS 姓名, salary AS 工资 FROM lesson21_emp WHERE salary > (SELECT AVG(salary) FROM lesson21_emp) ORDER BY id;
输出示例(只读,不要粘贴执行)text不可复制
+--------+--------+
| 姓名   | 工资   |
+--------+--------+
| 赵启   |  26000 |
| 孙悦   |  18000 |
| 吴桐   |  30000 |
| 冯玥   |  22000 |
+--------+--------+
4 rows in set (0.00 sec)

四行:赵启、孙悦、吴桐、冯玥。全公司平均是 17666.6667(九个人的工资之和 159000 除以 9,王澈的 NULL 不参与),超过它的就是这四人。

第 10 题:查工资低于本部门平均工资的员工。

MySQL 终端sql
SELECT e.name AS 姓名, e.dept_id AS 部门号, e.salary AS 工资 FROM lesson21_emp e WHERE e.salary < (SELECT AVG(x.salary) FROM lesson21_emp x WHERE x.dept_id = e.dept_id) ORDER BY e.id;
输出示例(只读,不要粘贴执行)text不可复制
+--------+-----------+--------+
| 姓名   | 部门号    | 工资   |
+--------+-----------+--------+
| 钱宁   |         1 |  12000 |
| 孙悦   |         1 |  18000 |
| 李蔓   |         2 |  15000 |
| 周禾   |         2 |   9000 |
+--------+-----------+--------+
4 rows in set (0.00 sec)

四行:钱宁、孙悦、李蔓、周禾。内层带了 x.dept_id = e.dept_id,所以每换一行外层,内层就重算一次“这个人的部门平均”,这叫相关子查询。研发部平均 18666.6667,钱宁 12000、孙悦 18000 低于它;市场部平均 15333.3333,李蔓 15000、周禾 9000 低于它;人事部只有郑楠 11000 和王澈 NULL,平均 11000,没有人严格低于;吴桐、陈屿的部门是 NULL,内层 x.dept_id = NULL 查不到任何行,平均为 NULL,比较不成立,所以他们不出现。漏写 x.dept_id = e.dept_id 会退化成全公司平均,结果看着合理却是错的。

第 11 题:查所有部门及每个部门的员工人数。

MySQL 终端sql
SELECT d.id AS 部门号, d.name AS 部门, (SELECT COUNT(*) FROM lesson21_emp e WHERE e.dept_id = d.id) AS 人数 FROM lesson21_dept d ORDER BY d.id;
输出示例(只读,不要粘贴执行)text不可复制
+-----------+-----------+--------+
| 部门号    | 部门      | 人数   |
+-----------+-----------+--------+
|         1 | 研发部    |      3 |
|         2 | 市场部    |      3 |
|         3 | 人事部    |      2 |
|         4 | 审计部    |      0 |
+-----------+-----------+--------+
4 rows in set (0.00 sec)

四行,人数分别是 3、3、2、0。审计部没人也出现了,因为外层是部门表全表,子查询只负责“数这个部门有几个人”,数不到就是 0。这里的子查询在 SELECT 列表里,每行算一次;它返回的是标量,所以能直接当一列。

第 12 题:查所有员工的技能情况,展示员工姓名、工号、技能名称。

讲义第 12 题用的是学生、课程、选课三张表;本站没有学生和课程,改用员工、技能和“员工技能”中间表,仍然是“两张主表 + 一张中间表”的三表连接,形状一致、表和数据都不同。

MySQL 终端sql
SELECT e.name AS 员工, e.id AS 工号, s.name AS 技能 FROM lesson21_emp e, lesson21_emp_skill es, lesson21_skill s WHERE e.id = es.emp_id AND es.skill_id = s.id ORDER BY e.id, s.id;
输出示例(只读,不要粘贴执行)text不可复制
+--------+--------+--------+
| 员工   | 工号   | 技能   |
+--------+--------+--------+
| 赵启   |      1 | SQL    |
| 赵启   |      1 | Python |
| 钱宁   |      2 | SQL    |
| 孙悦   |      3 | SQL    |
| 孙悦   |      3 | 报表   |
| 周禾   |      5 | 沟通   |
| 王澈   |      8 | Python |
+--------+--------+--------+
7 rows in set (0.00 sec)

七行。赵启会 SQL 和 Python 两条,孙悦会 SQL 和报表两条,钱宁、周禾、王澈各一条。没有技能记录的员工不出现——源题的内连接也是这个行为;要保留他们,得把两张表都改成 LEFT JOIN,那是练习第 1 题。

到这里十二题走完。十二题不是十二种孤立语法,它们反复在做三件事:先定保留谁(内连接还是外连接)、再定怎么拼(等值、区间、中间表)、最后定要不要嵌套(标量、相关、SELECT 里的计数)。

小结:P50 多表查询小结

多表查询的决策可以压成三步。先看关系:一对多把外键放在多的一方,多对多需要中间表,一对一给外键加唯一约束。再看要不要保留配不上的行:只留匹配用内连接,保留一侧用左/右外连接,同一张表扮演两个身份用自连接。最后看结果怎么合:行方向合并用 UNION / UNION ALL,把内层结果当条件或当表,就按返回形状选标量、列、行、表子查询。

十二道题正好是这三步的组合:第 1–4 题只做连接和保留,第 5–7 题加上区间连接与分组聚合,第 8–11 题用子查询把“平均值”“本部门”“人数”变成可比较的值,第 12 题用中间表把多对多拆成两个一对多。

课程到这里停在 P50。事务、索引、存储引擎、锁和执行计划都在 P51 之后,本课不讲,也不要在这里提前引入。

常见报错怎么办

报错什么意思怎么办
1242 Subquery returns more than 1 row内层返回多行,外层却用 = 接改成 IN,或让内层按主键/聚合只返回一行
1062 Duplicate entry种子数据重复插入表已存在就跳过建表/插入,按行数核对,不删表
说明
MySQL 终端sql
SELECT name FROM lesson21_emp WHERE salary > (SELECT salary FROM lesson21_emp WHERE dept_id = 1);
输出示例(只读,不要粘贴执行)text不可复制
ERROR 1242 (21000): Subquery returns more than 1 row

内层是研发部三个人的工资,返回三行,外层用 > 接不住,直接 1242。这不是“比较不了”,是“内层给了一口锅”。看到 1242 先单独跑内层数行数,再决定用 ANY / ALL / IN,或者把内层改成聚合。

说明
MySQL 终端sql
INSERT INTO lesson21_dept (id, name) VALUES (1, '研发部');
输出示例(只读,不要粘贴执行)text不可复制
ERROR 1062 (23000): Duplicate entry '1' for key 'lesson21_dept.PRIMARY'

表已经存在时重跑插入会撞主键。跳过建表和插入、按行数核对即可,不要删表。行比较列数不符、派生表缺别名这两类写法问题,上一节已经实测过,本节不再重复。

练习

十二道题本身就是本节的练习。下面三题只换写法,用来检查你是不是真的理解“保留谁”和“怎么接集合”。

**1. 把第 12 题改成保留没有技能的员工,姓名、工号、技能三列,没有技能的人技能列显示 NULL。**

说明

员工表放左边,先 LEFT JOIN 中间表,再 LEFT JOIN 技能表;两个连接条件都放 ON,不要放 WHERE。

说明
MySQL 终端sql
SELECT e.name AS 员工, e.id AS 工号, s.name AS 技能 FROM lesson21_emp e LEFT JOIN lesson21_emp_skill es ON e.id = es.emp_id LEFT JOIN lesson21_skill s ON es.skill_id = s.id ORDER BY e.id, s.id;
输出示例(只读,不要粘贴执行)text不可复制
+--------+--------+--------+
| 员工   | 工号   | 技能   |
+--------+--------+--------+
| 赵启   |      1 | SQL    |
| 赵启   |      1 | Python |
| 钱宁   |      2 | SQL    |
| 孙悦   |      3 | SQL    |
| 孙悦   |      3 | 报表   |
| 李蔓   |      4 | NULL   |
| 周禾   |      5 | 沟通   |
| 吴桐   |      6 | NULL   |
| 郑楠   |      7 | NULL   |
| 王澈   |      8 | Python |
| 冯玥   |      9 | NULL   |
| 陈屿   |     10 | NULL   |
+--------+--------+--------+
12 rows in set (0.00 sec)
说明

只把第一段改成 LEFT JOIN、第二段仍用内连接,没有技能的人还是会被第二段筛掉。另一个错是把 es.skill_id = s.id 写进 WHERE,外连接立刻退化成内连接。

**2. 用 ANY 改写第 8 题:查工资比研发部任意一人高的员工。**

说明

内层查研发部三个人的工资,外层用 > ANY。> ANY 的门槛是集合最小值。

说明
MySQL 终端sql
SELECT name AS 姓名, salary AS 工资 FROM lesson21_emp WHERE salary > ANY (SELECT salary FROM lesson21_emp WHERE dept_id = 1) ORDER BY id;
输出示例(只读,不要粘贴执行)text不可复制
+--------+--------+
| 姓名   | 工资   |
+--------+--------+
| 赵启   |  26000 |
| 孙悦   |  18000 |
| 李蔓   |  15000 |
| 吴桐   |  30000 |
| 冯玥   |  22000 |
| 陈屿   |  16000 |
+--------+--------+
6 rows in set (0.01 sec)
说明

把 ANY 当成“比所有人都高”,那是 ALL。研发部最低工资 12000,所以 15000、16000 这类“比孙悦低”的人也会入选;先算清门槛再对行数。

3. 查出没有任何员工的部门编号和名称。

说明

可以沿用第 19 节的 NOT IN,但内层要先排除 NULL;也可以用第 18 节的左连接反查。

说明
MySQL 终端sql
SELECT id, name FROM lesson21_dept WHERE id NOT IN (SELECT dept_id FROM lesson21_emp WHERE dept_id IS NOT NULL) ORDER BY id;
输出示例(只读,不要粘贴执行)text不可复制
+----+-----------+
| id | name      |
+----+-----------+
|  4 | 审计部    |
+----+-----------+
1 row in set (0.00 sec)
说明

内层忘了 WHERE dept_id IS NOT NULL,吴桐和陈屿的 NULL 会让整个 NOT IN 变成 unknown,结果一行也没有——看起来像“每个部门都有人”,其实是条件失效。

本节终态

收工前把五张表的行数和三个具体值核对一遍,确认自己跑的是同一份数据:

MySQL 终端sql
SELECT (SELECT COUNT(*) FROM lesson21_dept) AS 部门数, (SELECT COUNT(*) FROM lesson21_emp) AS 员工数, (SELECT COUNT(*) FROM lesson21_salgrade) AS 档位数, (SELECT COUNT(*) FROM lesson21_skill) AS 技能数, (SELECT COUNT(*) FROM lesson21_emp_skill) AS 技能记录数, (SELECT COUNT(*) FROM lesson21_dept WHERE id NOT IN (SELECT dept_id FROM lesson21_emp WHERE dept_id IS NOT NULL)) AS 空部门数;
输出示例(只读,不要粘贴执行)text不可复制
+-----------+-----------+-----------+-----------+-----------------+--------------+
| 部门数    | 员工数    | 档位数    | 技能数    | 技能记录数      | 空部门数     |
+-----------+-----------+-----------+-----------+-----------------+--------------+
|         4 |        10 |         5 |         4 |               7 |            1 |
+-----------+-----------+-----------+-----------+-----------------+--------------+
1 row in set (0.00 sec)

本单元验收

  1. 能看着题干里的“所有 / 没有……也要 / 有员工的”判断该用内连接还是外连接,并说清保留的是哪张表。
  2. 能写出区间连接 BETWEEN losal AND hisal,并解释为什么它不能用 =。
  3. 能区分标量子查询、相关子查询和 SELECT 列表里的计数子查询,各写一条。
  4. 能解释第 10 题里吴桐、陈屿为什么不出现,以及第 12 题里没有技能的人为什么不出现。
  5. 能说出多对多为什么需要中间表,并把三表连接的两个条件写全。

做到这里

P43–P50 的子查询与十二题到这里结束。你现在能独立完成:先判断保留谁,再选连接方式,最后按内层返回形状接子查询。下一节不再有内容——课程停在 P50,事务、索引、引擎和优化不在本课范围。

来源与边界

本节对应 BV1Kr4y1i7ru 平台 part 48「48. 基础-多表查询-练习1」、part 49「49. 基础-多表查询-练习2」、part 50「50. 基础-多表查询-小结」,主题与同版讲义《MySQL-基础篇》5.7(p85–87)及 5.1–5.6(p69–85)对应;讲义为公开镜像,仅作事实核对。十二题的条件按讲义 5.7 逐条对应,措辞、表名、数据、档位边界全部重写:讲义用课程自带的员工表、部门表、工资等级表和学生选课表,本节用 lesson21_* 五张表,第 12 题把“学生选课”换成“员工技能”,第 1、2 题把“职位”换成“工资”。无逐字稿,未声称听过讲解;来源只写平台 part 号与讲义页码。课程停在 P50,本节不涉及事务、索引、引擎或执行计划。