在学连接之前,先讲一个我课堂上反复遇到的现象:很多同学做单表查询时一马平川,一到"两表一起查"就开始犯怵。为什么?因为单表查询的思维是"我从一张表里挑数据",而多表查询逼你换一种脑回路——"我得想办法把两张表'拼'起来,再决定拼完之后怎么过滤"。
这一课就专门讲"怎么把多张表拼起来"。我们会把内连接、外连接(左/右)、自连接一次性掰开揉碎,把 ON 和 WHERE 到底有什么区别、整个结果集的行数到底谁说了算、外连接到底"保留"哪一侧这些最容易打架的细节,全部讲透。文章末尾还配了两道多表综合思考题,带详解答案,你可以拿来自测。
你应该有的知识准备
读这一课之前,我默认你手头已经有这几块地基,缺的可以先回头补一补:
- 一列单表查询:
SELECT ... FROM 表 WHERE ...,知道投影(挑哪些列)和筛选(挑哪些行)是两回事。 - 多行插入:
INSERT INTO 表 VALUES (..),(..),(..);一次插多行,后面建演示表会大量用到。 as别名:列名 as 新名或表名 as 新名,能给列和表起个短名字,连接时写起来省事(注意 MySQL 里as可以省略,直接空格隔开)。- NULL 是什么:一个列里"什么都没有",不是 0、不是空串。理解外连接能不能看懂,很大程度上取决于你对"没匹配上时那个位置填的 NULL"有没有手感。
如果你上面几条都顺溜,那这一课对你就是纯增量:你只需要把一个核心念头装进脑子——连接的本质是"先拼、再筛、可留空"。
连接的基础:先回到"乘"上找感觉
我们分开来看连接这件事。要理解内连接和外连接,必须先理解一张"虚无缥缈"的超大表——笛卡尔积。
回想一下我们当初学单表查询时,是不是见过这样的写法?
-- 把 EMP 员工表和 DEPT 部门表,不带任何条件地连在一起看
-- 这时 MySQL 会做"笛卡尔积",也就是把两张表的每一行两两组合
SELECT e.ename, d.dname
FROM EMP e, DEPT d;如果 EMP 有 14 行、DEPT 有 4 行,这个查询的结果就是 14 × 4 = 56 行。每一条员工记录,都会和每一个部门记录配对一次。这就是笛卡尔积——它像把两副牌一张张交叉配对,得到的是"所有的组合"。肉眼一看就知道这里面九成是废话:SMITH 明明是某个部门的,结果他那一行会和 4 个部门分别配对出 4 行,只有 1 行是他真实所属的部门,另外 3 行都是"错配"。
内连接的本质,正是从这张巨大却充满错配的笛卡尔积里,把"正确的一对"筛选出来。 怎么筛?靠连接条件。员工表里的 deptno(部门号)和部门表里的 deptno 对上号,这一对就是正确的。所以你可以把内连接理解成一句话:
内连接 = 笛卡尔积 + 连接条件筛选。 先把两表交叉配对成一张大表,再用
ON后的条件把"对不上号的错配"统统删掉,剩下的就是两边能匹配上的记录。
那"外连接"又是在干什么?你可以把它想成内连接的"补漏版":内连接只留下"两边都匹配得上"的行,一旦某一行在另一边没有对应(比如有个部门没员工、有个学生没成绩),它就会悄悄消失。外连接则多了一个动作——把"没匹配上的那一侧"也保下来,另一边对应的位置用 NULL 顶上。至于是保左边还是保右边,取决于你用 LEFT 还是 RIGHT,这正是后面要重点死磕的地方。
先把"先拼、再筛、可留空"这句总纲挂在心上,我们逐个击破。
内连接 INNER JOIN
概念与语法
内连接(INNER JOIN)返回的是左右两张表里能匹配上连接条件的那些行。匹配不上的,左右两边都不出现在结果里。它也是我们前面所有"用 WHERE 筛多表"的查询的规范说法——你之前写的 SELECT ... FROM EMP, DEPT WHERE EMP.deptno = DEPT.deptno; 在语义上就是一个内连接,只不过用的是老式的 逗号 + WHERE 写法。
标准的内连接语法长这样:
-- 标准内连接写法
-- INNER 关键字也可以省略,直接写 JOIN 就是内连接
SELECT 列...
FROM 表1
[INNER] JOIN 表2
ON 连接条件; -- 后面还可以继续跟 WHERE 等其他过滤条件必要的术语先立住:ON 是连接中用来书写"连接条件"的关健字,它告诉数据库"这两张表靠哪个字段对上号"。而后面我们会看到,ON 和 WHERE 看似都是过滤,实际分工完全不同——差异点就得靠"内/外连接"的场合才能体现,先记下"它俩不一样"这个结论,等讲外连接时再给你看铁证。
一个完整的例子
经典场景:EMP 员工表 + DEPT 部门表,要查出"每个员工叫什么名字、他在哪个部门"。用两种写法对比着看:
-- 写法一:老式的逗号连接,用 WHERE 当连接条件
-- 等价于内连接:先用逗号拼出笛卡尔积,再用 WHERE 把 deptno 对得上的留下
SELECT e.ename, d.dname
FROM EMP e, DEPT d
WHERE e.deptno = d.deptno;
-- 写法二:标准内连接,用 ON 写明连接条件(推荐,语义更清晰)
SELECT e.ename, d.dname
FROM EMP e
INNER JOIN DEPT d ON e.deptno = d.deptno;上面两段查询的结果完全一样。注意我用的表别名 e 和 d——在 FROM EMP e 里,e 就是 EMP 的别名,后面所有地方写 e.ename 就等于写 EMP.ename。别小看这个写法,两张表一旦有了同名字段(比如都叫 id),不加表名/别名限定,MySQL 会报"列不明确"(column ambiguous)的错。
在连接条件之外再加过滤条件
内连接最常见的坑之一,是"我想按条件只要某几个员工,条件该放 ON 还是 WHERE"。对内连接来说,放 ON 和放 WHERE 的结果一模一样——因为内连接本来就不保留"没匹配上"的行,所以无论你在哪个环节把某一行滤掉,最终留下来的行集合都一样。这一点我们先用直观例子感受一下:
-- 需求:显示 SMITH 的名字和他的部门名称
-- 写法一:老式写法,连接条件放 WHERE,再去 WHERE 里加 ename='SMITH' 做过滤
SELECT e.ename, d.dname
FROM EMP e, DEPT d
WHERE e.deptno = d.deptno -- 连接条件
AND e.ename = 'SMITH'; -- 附加的过滤条件
-- 写法二:标准内连接,连接条件放 ON,过滤条件照旧放 WHERE
SELECT e.ename, d.dname
FROM EMP e
INNER JOIN DEPT d ON e.deptno = d.deptno -- 连接条件
WHERE e.ename = 'SMITH'; -- 附加的过滤条件
-- 写法三:标准内连接,把过滤条件也塞进 ON 里一起写
-- 注意:对内连接而言,这样写结果和上面两种完全一致
SELECT e.ename, d.dname
FROM EMP e
INNER JOIN DEPT d
ON e.deptno = d.deptno AND e.ename = 'SMITH';ON 后面如果用 AND 接多个条件,就是在指出"连上号 + 顺便滤掉一部分"。对内连接,三个写法输出完全一致。但别急着把"都行"记成万能结论——等到了外连接,"放 ON"和"放 WHERE"将产生天壤之别,那是这节课的重头戏,先记住:内连接里它俩等价,外连接里它俩分家。
连接条件与结果行数的关系
这是内连接最容易晕的地方,我单独拎出来讲。内连接一句口诀:能有几行结果,取决于"能匹配上的组合"有几个,匹配不上的一律不上榜。
举个例子,假设我们有两张小小的表:
-- 部门表 DEPT:4 个部门
CREATE TABLE DEPT (deptno INT, dname VARCHAR(30));
INSERT INTO DEPT VALUES
(10, '会计部'),
(20, '研发部'),
(30, '销售部'),
(40, '后勤部');再假设员工表 EMP 里,有 12 名员工分布在这 4 个部门中,另外有 2 名员工 deptno 写错、指向了一个不存在的部门(比如 99)。那 INNER JOIN 的结果行数是几?
12 名"部门对得上"的员工各产生 1 行,共 12 行;那 2 名 deptno=99 的员工在 DEPT 里找不到 99 号部门,匹配不上,被内连接直接丢弃。所以结果就是 12 行——而不是 14 行。请务必记住这个"会掉行"的特性:内连接只关心"能不能匹配上",至于"有没有人落单",它不管、也不保留。
再想深一层:如果某部门恰好在 EMP 里对应 3 名员工,那么这个部门和这 3 名员工会产生 3 行(一对多,一行部门 × 每个员工各一行)。也就是说,一对多连接会让"一"那边的一行被放大成多行,这正是"连接数量控制结果集"的第一层含义:结果行数 = 左表中能匹配的行 × 右表中能匹配的行,逐个累加。
口诀一(内连接):要走内连接,两边必须都匹配;少一边,这一行就没了。
外连接概览:什么叫"保留行"
我们用一个"天然有落单"的场景来引出外连接。用一张学生表和一张成绩表:
-- 学生表 stu:4 名学生
CREATE TABLE stu (id INT, name VARCHAR(30));
INSERT INTO stu VALUES
(1, 'jack'),
(2, 'tom'),
(3, 'kity'),
(4, 'nono');
-- 成绩表 exam:只有 3 条成绩,其中 id=3 的 kity 没考,id=11 是陌生考生
CREATE TABLE exam (id INT, grade INT);
INSERT INTO exam VALUES
(1, 56),
(2, 76),
(11, 8);看到没有,这里故意埋了两个"落单":
- kity(id=3)在 exam 里没有成绩 → 她是"没成绩的学生";
- 成绩 id=11 在 stu 里没有对应学生 → 它是"查无此人的成绩"。
现在做内连接,看看会怎样:
-- 内连接:只保留两边都能匹配上的
-- 结果只会出现 jack、tom(1、2 号都有成绩),kity 没有成绩被丢弃
SELECT s.name, e.grade
FROM stu s
INNER JOIN exam e ON s.id = e.id;
-- 结果:
-- +------+-------+
-- | name | grade |
-- +------+-------+
-- | jack | 56 |
-- | tom | 76 |
-- +------+-------+问题来了:如果我做教务系统,偏偏希望"就算 kity 没考试,也要把 kity 的学生信息显示出来,成绩那一栏空着就空着"。内连接做不到这事——它会把 kity 整个丢掉。这时候就需要外连接。
那"外连接"到底在保留什么?两个字:保留行。外连接会在内连接"只留匹配行"的基础上,额外把"某一边没匹配上的行"也放进结果里,另一边填 NULL。至于是保左、保右、还是两边都保,由表一侧的关键字 LEFT / RIGHT(或 FULL,MySQL 不支持后文会讲)决定。这一个概念必须扎死:外连接 = 内连接的结果 + 保住的那一侧的"落单行"(空缺处填 NULL)。
左外连接 LEFT JOIN
概念与语法
左外连接(LEFT JOIN / LEFT OUTER JOIN)规则一句话:以左侧表为"主角",左侧表的每一行都一定出现在结果里。 左侧某行在右侧找不到匹配,它的右侧字段就全是 NULL;左侧某行在右侧能匹配多行,它也会被放大成多行。
左外连接的语法:
-- 左外连接:左侧表(stu)所有行都保留
SELECT 列...
FROM 表1 -- 这一侧就是"主角表",全保留
LEFT JOIN 表2
ON 连接条件;注意 LEFT 和 OUTER 可以一起写 LEFT OUTER JOIN,也可以省略 OUTER 直接写 LEFT JOIN,语义完全相同。绝大多数人写 LEFT JOIN。
完整例子:查询所有学生的成绩,没成绩的也要显示
直接套我们那张 stu / exam 表:
-- 左外连接:以 stu(学生表)为主角
-- 每个学生都出现;exam 里匹配不上的(kity),成绩填 NULL
SELECT s.name, e.grade
FROM stu s
LEFT JOIN exam e ON s.id = e.id;
-- 结果:
-- +------+-------+
-- | name | grade |
-- +------+-------+
-- | jack | 56 |
-- | tom | 76 |
-- | kity | NULL | <- kity 被保留了,成绩没有,显示 NULL
-- | nono | NULL | <- nono 也没成绩,同样被保留
-- +------+-------+逐行拆解给你看:
- jack、tom:在 exam 中找到对应 id,正常显示成绩;
- kity:id=3 在 exam 里查无此人,但她作为"主角表 stu"的行被强制保留,右侧没有可用值,于是
grade显示为NULL; - nono:同理,id=4 在 exam 里不存在,保留,成绩
NULL。
注意成绩 id=11 那条"查无此人的成绩"在左外连接中并没有出现——因为 11 号成绩属于右侧表 exam,而左侧才是主角,右侧的落单行(11 号成绩没有学生对应)被丢弃。这就是"保留哪一侧"的直观感受:左连接保左,右表的落单行跟你没关系。
思考题 1:保左还是保右,一测便知
思考题:把上面那个查询改成"查所有学生的成绩",但这次我们不用
LEFT JOIN而是用RIGHT JOIN的镜像写法FROM exam e RIGHT JOIN stu s ON e.id = s.id,结果一样吗?为什么?
展开看详解
答案:完全一样。 因为 FROM exam e RIGHT JOIN stu s 的右侧是 stu,而右连接是"保右侧",保的恰好还是 stu 这张表。所以它和 FROM stu s LEFT JOIN exam e(左连接保左侧 stu)本质上保的是同一个主角表,结果自然逐行完全相同:
-- 写法一:左连接,保 stu(左侧)
SELECT s.name, e.grade
FROM stu s LEFT JOIN exam e ON s.id = e.id;
-- 写法二:右连接,依然保 stu(此时 stu 在右侧)
SELECT s.name, e.grade
FROM exam e RIGHT JOIN stu s ON e.id = s.id;
-- 两个查询结果完全一致。这教会我们一个判断技巧:
-- 不要死记"哪边表要用 LEFT",要记"谁是要保全的主角",把主角表放在 LEFT 的左侧,或放在 RIGHT 的右侧即可。右外连接 RIGHT JOIN
概念与语法
右外连接(RIGHT JOIN / RIGHT OUTER JOIN)和左连接完全对称:以右侧表为"主角",右侧每一行都保留。 右侧某行在左侧找不到匹配,左侧字段填 NULL。
右外连接语法:
-- 右外连接:右侧表(exam)所有行都保留
SELECT 列...
FROM 表1
RIGHT JOIN 表2 -- 这一侧(表2)是主角表,全保留
ON 连接条件;完整例子:把所有的成绩都显示出来,哪怕这个成绩没有学生对应
同一个场景,换个需求:这次我要的是"每一笔成绩都必须出现,即便查不到学生是谁"。
-- 右外连接:以 exam(成绩表)为主角
-- 每条成绩都出现;stu 里匹配不上的(id=11)学生信息填 NULL
SELECT s.name, e.grade
FROM stu s
RIGHT JOIN exam e ON s.id = e.id;
-- 结果:
-- +------+-------+
-- | name | grade |
-- +------+-------+
-- | jack | 56 |
-- | tom | 76 |
-- | NULL | 8 | <- 11 号成绩没有学生,保留,学生名 NULL
-- +------+-------+拆解:
- jack、tom 的成绩照常;
- 成绩 id=11 在 stu 里查无此人,但它是主角表 exam 的行,强制保留,左侧
name填NULL; - 而"没成绩的学生"kity、nono 这次不出现——因为主角是右侧的 exam,左侧学生表的落单行被丢弃。
对比上一节的结果,你发现了吗——同样两张表,只是换了 LEFT 还是 RIGHT,输出的行集合就完全不同。这就是外连接最核心、也最容易搞反的坑:你得先想清楚"这一题要求全保留哪一边",再决定写 LEFT JOIN 还是 RIGHT JOIN,以及把主角表放在哪一侧。
口诀二(外连接):左连接只有左表全保留,右连接只有右表全保留;谁当主角,谁一个都不能少。
左 / 右连接之间怎么互转
既然左连接和右连接本质上是一对镜像,实际开发里就有个人尽皆知的偷懒技巧:同一个结果,既可以用 LEFT JOIN 写,也可以用 RIGHT JOIN 写,只要把表的摆放位置对调、把 LEFT 换成 RIGHT(或反过来)即可。
拿最初那个"列出部门名称和每个部门的员工信息,同时把没有员工的部门也列出来"来说——需求里要保全的是"部门",因为"没有员工的部门"也要显示。部门是 DEPT,所以主角是 DEPT。我们可以从两个方向写:
-- 方法一:主角 DEPT 放左侧,用 LEFT JOIN
-- DEPT 每个部门都保留,EMP 里匹配不上的(空部门),员工信息显示 NULL
SELECT d.dname, e.empno, e.ename
FROM dept d
LEFT JOIN emp e ON d.deptno = e.deptno;
-- 方法二:主角 DEPT 放右侧,用 RIGHT JOIN
-- 结果和方法一完全一致,只是写法从另一边"切"进来
SELECT d.dname, e.empno, e.ename
FROM emp e
RIGHT JOIN dept d ON d.deptno = e.deptno;方法二里 FROM emp e RIGHT JOIN dept d:右侧是 dept,右连接保右侧 = 保 dept,于是"没员工的部门"同样被保留,员工那几列填 NULL。两个查询结果逐行一致。
一句话记住套路:如果题里"必须一个不少地出现"的表是 DEPT,那就把 DEPT 当作主角——放在 LEFT 的左边或 RIGHT 的右边,怎么顺怎么写。 多练习几次,你就不会被"到底该用 LEFT 还是 RIGHT"卡住了。
口诀三(保谁):先问"这题必须保全谁",谁就是主角;再把它放到 LEFT 左 / RIGHT 右,剩下的照抄。
ON 与 WHERE 的时机:外连接的分水岭
这是连接里最重要的一个坑,内连接里它俩等价,一到外连接就分道扬镳,必须在脑子里钉死。
先说结论:在外连接里,ON 是"拼表时用来找匹配"的条件,WHERE 是"拼完表之后,对整体结果再过滤"的条件。 因为在执行顺序上,数据库是 先按 ON 完成连接(此时落单的行被保留、空缺填 NULL),再执行 WHERE。所以:
- 放
ON的条件,作用在"决定匹配关系"这一步——它决定"这行算不算匹配上",落单的行保留其位置; - 放
WHERE的条件,作用在"连接已经完成之后"——它会把"刚被保留下来的 NULL 行"再次整行滤掉。
换句话说,把条件放进 WHERE,会"意外撤销"掉外连接的保留效果。 这绝对是外连接使用中最经典的一个坑,很多新手写"外连接加分页/加过滤"时就莫名其妙地发现有数据丢了,根子大多在这。
我们用 stu LEFT JOIN exam 演一遍,条件分别放 ON 和 WHERE,看结果怎么变:
-- 场景:还是这张学生成绩左连接,现在只想看"成绩 ≥ 60"的记录
-- 数据里只有 jack=56、tom=76,其实都没有 ≥60 的成绩……
-- 写法一:条件放 WHERE
-- 结果必为空!因为 kity/nono 的成绩是 NULL,NULL 不满足 >=60;
-- 而 jack=56、tom=76 也都不满足 >=60,于是所有行都被 WHERE 滤光
SELECT s.name, e.grade
FROM stu s
LEFT JOIN exam e ON s.id = e.id
WHERE e.grade >= 60;
-- 写法二:条件放 ON
-- 结果把所有学生都保留下来(左连接特性),只是"匹配"受限:
-- 只有满足 grade>=60 的成绩才会被连上来,不满足的当"没匹配上",填 NULL
SELECT s.name, e.grade
FROM stu s
LEFT JOIN exam e ON s.id = e.id AND e.grade >= 60;在给定数据下,写法一返回 0 行(全被 WHERE 滤掉),写法二返回 4 行(每个学生都保住了)。同一个 grade >= 60,一个放对位置价值千金,一个放错位置让数据凭空消失。
再补一个 NULL 相关的致命细节:因为落单行那一侧填的是 NULL,而任何 NULL 参与的关系比较(NULL >= 60 之类)结果都不是 TRUE,而是不确定(在 MySQL 里表现为不满足条件、整行不出)。所以你只要在 WHERE 里写了 e.grade >= 60,那些"没有成绩 = NULL"的学生,无论他们多想留下,都会被一扫而空。想把他们留下,唯一的办法是把该条件放回 ON。
口诀四(ON vs WHERE):内连接它俩等价;外连接里,ON 定"谁算匹配",WHERE 定"留下谁"——条件放 WHERE 会把被 NULL 保留的行又滤掉,除非你就是想要这个效果。
自连接
概念与语法
自连接,字面上"自己连自己",指同一张表自己和自己做连接。你可能觉得这听起来像"左右互搏",但它在现实里极其常见——只要一张表里存了"同类型的关系",比如员工与其上司、分类与其父分类、城市与其所属省份,就可以用自连接查。"和谁亲"这类"表内关系",单表查不出来,必须给自己的表起两个别名,当成两张独立的表来连。
自连接的套路:给同一张表取两个别名 a、b,然后像连两张表一样写连接。两个别名指向同一张物理表,但语义上你当它们是"表A"和"表B"。
-- 自连接一般形态:自己 join 自己,靠两个别名区分"两个角色"
SELECT a.列, b.列
FROM emp a -- 别名为 a(比如当"员工")
JOIN emp b ON ... -- 别名还是同一张 emp 表(比如当"上司")
WHERE ...; -- 附加条件完整例子:查每个员工的姓名和他的上司姓名
经典"员工找上司"考题。我们假设 emp 表里有一列 mgr(manager,上司的员工号),员工 mgr 指向其上司的 empno。同一个 emp 表,既要当"员工",又要当"上司",那就让它自己和自己连:
-- 自连接:查每个员工的上司是谁
-- a 代表"下属"视角,b 代表"上司"视角
SELECT a.ename AS '员工', b.ename AS '上司'
FROM emp a -- a 是"员工角色"
JOIN emp b ON a.mgr = b.empno; -- 员工 a 的 mgr,等于上司 b 的 empno逐行解释:
- 阅读顺序从左往右,"员工 a 列出一名员工,他的 mgr 若能在 b 里找到一个 empno 相等的记录,那 b 就是他的上司";
- 因为是内连接,没有上司(mgr 为 NULL,比如老板本人)的员工会被过滤掉——他匹配不到任何上司。如果老板也要显示出来、上司列填 NULL,就得把
JOIN换成LEFT JOIN。这是个很好的自省:自连接同样尊重内/外连接的规则,自连接只是"表是同一张",不是"规则变了"。
顺便说一句,自连接不一定非得是内连接,也可以配 LEFT/RIGHT,全看需求。上面"老板也要出"的需求就是典型:FROM emp a LEFT JOIN emp b ON a.mgr = b.empno;。
思考题 2:自连接加一层条件
思考题:沿用上面 emp 结构(含
mgr列与sal工资列),请写出"查询所有员工以及他的直接上司,只要这个上司的工资在 50000 以上"的 SQL,并说明该条件该放ON还是WHERE、为什么。
展开看详解
参考答案:
-- 需求:员工 + 他工资 > 50000 的直接上司
-- 这里"上司工资高"是过滤条件,且放 WHERE 即可(因为它是内连接)
SELECT a.ename, b.ename AS '上司'
FROM emp a
JOIN emp b ON a.mgr = b.empno
WHERE b.sal > 50000; -- 过滤:直接上司工资要高于 50000为什么放 WHERE 而不是 ON: 因为这道题用的是普通 JOIN(内连接)。内连接里 ON 与 WHERE 结果等价,把"b.sal > 50000"放 ON 里效果完全一样:
FROM emp a JOIN emp b ON a.mgr = b.empno AND b.sal > 50000;两个写法结果一致。但如果把内连接换成 LEFT JOIN(想同时保留没合格上司的员工),那就必须把条件放进 ON,放进 WHERE 会把未匹配(mgr 落单)的行又滤掉——这正是我们"ON vs WHERE"一节强调的分水岭。这道题给"为什么放 WHERE 也行"的标准答案,就一句话:内连接里它俩等价;一旦外连接,条件必须进 ON 才能保留落单侧。
连接的进阶坑与细节
到这里三种连接都能写了,但要把连接写得"稳",还有几个坑值得逐个排掉。
坑 1:MySQL 没有 FULL OUTER JOIN
标准 SQL 里还有第三种外连接叫全外连接(FULL OUTER JOIN),作用是"左右两边全部保留,哪边落单就哪边补 NULL"。但 MySQL 并不直接支持 FULL OUTER JOIN 语法——如果你直接写,会收到语法错误。想要"两边都保留"的效果,一个常见替代方案是把左连接和右连接的结果用 UNION 合并:
-- MySQL 里模拟"全外连接":左连接 ∪ 右连接,去重合并
-- 左连接:保左表全部
SELECT a.id, b.id FROM stu a LEFT JOIN exam b ON a.id = b.id
UNION
-- 右连接:保右表全部,UNION 自动去重
SELECT a.id, b.id FROM stu a RIGHT JOIN exam b ON a.id = b.id;这一招在需要"两边落单都不漏"的场景(比如做数据对账)很实用。你暂不必深究,知道"MySQL 不支持 FULL JOIN、想两头都要就 UNION 两个连接"即可。
坑 2:连接数量控制结果集——多对多会翻倍
"连接数量控制结果集"这句话,在外连接里同样成立,而且更容易引发数据量"翻倍"的惊悚感。核心规则是:连接后会产生的行数 = 左侧每一行 × 它在右侧匹配到的行数,逐行累加。 这不是"走一遍就没了"的 filter,而是"每一对匹配都产出一行"。所以:
- 一对一 / 多对一:右侧最多匹配 1 行 → 不翻倍,一行对一行(或多行对同一行,那一行被放大);
- 一对多:右侧能匹配多行 → 左侧那一行被放大成多行;
- 多对多:两表都有重复的连接键 → 会发生"乘积式"翻倍,数据量爆炸。
最危险的翻倍,藏在"中间都连着 JOIN 多张表"时。 你连着 JOIN 了三张表,只要某两张之间是"一对多",结果行数就可能远大于你的直觉。所以写完连接,第一件事是心里过一遍每一段是几对几,防止结果集悄悄膨胀。
坑 3:同名列需要限定归属,否则报错
两张表都有 id 列时,SELECT id ... 这种不指明哪张表的写法会直接报"列不明确"错误。
-- 错误示范:id 在 stu 和 exam 里都有,MySQL 不知道你指的是哪个
-- SELECT s.id AS 学生号, e.id AS 考试号 (正确,要这么写)
-- 不加前缀直接写 id 会报:Column 'id' in field list is ambiguous别嫌写前缀麻烦,这是连接查询的专业姿势:多表连接时,所有列都养成 表名.列名(或别名.列名)的习惯,既避免歧义报错,也让读 SQL 的人一眼知道这列来自哪张表。
坑 4:显式留给面试官的 NULL 判断
连接后"落单"的位置是 NULL,于是经常有这种需求:"找出所有没有成绩的学生"、"找出所有没有员工的部门"。这类"判断某侧落单"的题,标准做法就是:查外连接的结果,再看右侧那列为 NULL。
-- 找出"没有任何成绩记录的学生"(即 exam 侧落单的学生)
SELECT s.id, s.name
FROM stu s
LEFT JOIN exam e ON s.id = e.id
WHERE e.id IS NULL; -- 重点:判断是否落单,要用 IS NULL,不能用 = NULL这里藏着另一个高频坑:判断 NULL 必须用 IS NULL / IS NOT NULL,不能用 = NULL。 因为 NULL 和任何值(包括它自己)用 = 比较,结果都不是 TRUE,e.id = NULL 永远不成立、选不出任何行。这是 SQL 语言普遍的规定,务必记住。
综合练习 + 详解答案
我们来做两道"一鱼多吃"的综合题,把内、外、自连接全串一遍。下面统一沿用 DEPT(deptno,dname)、EMP(empno,ename,deptno,mgr,sal) 这两张经典表。
练习 1:老总视角——每个部门的员工数
题目:查询每个部门的名字、以及该部门的员工数量,没有员工的部门也要显示出来,员工数填 0。
展开看详解
解题思路:要"每个部门都有",主角是 DEPT,所以用 LEFT JOIN 让 DEPT 全保留。再用 GROUP BY 按部门分组、COUNT(e.empno) 统计员工数。
注意:统计"人数"最稳妥的写法是针对员工的主键列 COUNT(e.empno),而不是 COUNT(*)——因为对没有员工的部门来说,LEFT JOIN 会造出一行"部门 + 全部 NULL 的员工列",COUNT(*) 会把这一行 NULL 也数进去得到 1,而 COUNT(e.empno) 数的是非 NULL 值,正好是 0。
-- 每个部门及其员工数;空部门保留、人数记 0
SELECT d.dname,
COUNT(e.empno) AS 员工数 -- 数非空 empno,空部门自然为 0
FROM dept d
LEFT JOIN emp e ON d.deptno = e.deptno -- 左连接保全部部门
GROUP BY d.dname; -- 按部门分组统计详解答案:
LEFT JOIN保证每个部门都出现在分组中,即使它在 EMP 里匹配不到任何人;COUNT(e.empno)只统计该部门匹配到的员工主键个数。空部门匹配不到任何 empno,所以是 0;用COUNT(*)则会错误地得到 1(它会把"部门+NULL员工"那行当一行数进去);- 这条题活生生地演示了"连接数量影响结果集 + COUNT 的 NULL 陷阱"两个知识点。
练习 2:同级联查——员工、部门、上司三合一
题目:查询每个员工的名字、所属部门名、以及他的上司名字。老板(没有上司的)也要显示出来,上司名填
NULL。
展开看详解
解题思路:这是"两段连接"的综合题:
- emp ↔ dept 按
deptno内连接/左连接,得到部门名; - emp 自连接(emp 与 emp 自身)按
mgr = empno找上司;因为老板没有上司要保留,所以这一段用LEFT JOIN。
注意:既然要求"老板也要出",那么"emp 连自身找上司"这一段必须 LEFT JOIN(否则老板因匹配不到上司被内连接丢弃)。而"emp 连 dept 找部门名"这一段,若你想"连部门都不存在的员工也显示",同样应 LEFT JOIN;本题默认所有员工都有部门,两段都用 LEFT JOIN 更稳妥。
-- 员工 + 部门名 + 上司名;老板无上司也要显示(上司列为 NULL)
SELECT e.ename AS 员工,
d.dname AS 部门,
m.ename AS 上司
FROM emp e
LEFT JOIN emp m ON e.mgr = m.empno -- 找上司:左连接,保老板
LEFT JOIN dept d ON e.deptno = d.deptno -- 找部门:左连接,保所有员工
ORDER BY e.ename;详解答案:
- 第一段
emp e LEFT JOIN emp m:m是同一张 emp 的"上司角色",e.mgr = m.empno让每个员工找到上司;老板mgr为 NULL,匹配不到m,但LEFT JOIN让他保留、m.ename为NULL; - 第二段
emp e LEFT JOIN dept d:按部门号拿到部门名; - 关键不是"会不会写",而是给每张表都起清晰别名(e 员工 / m 上司 / d 部门),否则同表自连接会让你一会儿就写晕;
- 这段代码演示了:内/外连接可以混用、自连接嵌套进外连接、以及"谁要全保留谁就用 LEFT JOIN"的统一套路。
小结
我们这一课把 MySQL 连接从头到尾过了一遍,把最关键的那根筋捋顺了:连接的本质是先拼(笛卡尔积)、再筛(ON 定匹配)、可留空(外连接保落单)。
- 内连接 INNER JOIN:两边都匹配才出现,匹配不上就掉行;
ON和WHERE等价; - 左外连接 LEFT JOIN:保左侧,右侧落单填
NULL; - 右外连接 RIGHT JOIN:保右侧,左侧落单填
NULL;左右可互转,只要把主角表放到对应一侧; - ON vs WHERE:外连接里
ON定"谁算匹配"、WHERE定"留下谁",条件放WHERE会把刚保留的 NULL 行又滤掉; - 自连接:同一张表起两个别名当两张表连,专门处理"表内关系";
- 连接数量控制结果集:几对几决定了会不会翻倍,多表连接前先心算一对几;
- NULL 判断:落单用
IS NULL,不能用= NULL;数人数用COUNT(主键列)而非COUNT(*)。
如果你跟着上面每一段 SQL 亲手敲一遍、把"放 ON 还是放 WHERE"的真实差异亲眼看上一回,那这"变两张表为一张表"的本事就真正长在你手里了。开篇我说连接要换脑回路——现在你应该体会到了:连接考的不是记性,是**"谁当主角"的取舍意识**。下一次当你看到一道多表题,第一反应不再是"我要连哪张表",而是"这道题必须保全谁"——恭喜,你已经跨过连接的分水岭了。下一课我们接着聊分组聚合与 HAVING,那是数据统计的另一座山,等你来爬。
还没有评论 — 第一条由你来留。