事情要从我在群里回答一个问题说起。有人贴了两条 SQL,一条是 inner join,另一条是 left join,然后问我:这两条结果行数一样,为什么还要分两种?
这个问题看起来基础,实际上戳中了很多写 SQL 的人共同的弱点——JOIN 不是语法不会写,而是语义边界不清楚。我见过不少写了三五年 SQL 的人,提起 JOIN 能背出"内连接取交集、外连接带主表、交叉连接是笛卡尔积",可真到了排查一个行数翻倍的报表时,照样抓瞎。你问他和 left join 和 inner join 什么时候结果等价、什么时候差十万八千里,他说不清楚。
所以这篇东西的定位就不是入门教程,而是把内连接、外连接、交叉连接这些 JOIN 家族成员,从"背定义"推进到"懂逻辑"。我会用一套固定的案例数据,把每种 JOIN 的产生、执行过程、结果形态和踩坑点全部摊开来讲,配上可直接运行的 SQL 代码。适合刚学完 JOIN 基本语法、但想彻底理顺语义的初学者,也适合天天写 SQL 但偶尔被多表关联搞到怀疑人生的业务开发。
1. 造一张不容易"骗自己"的测试表:数据设计里藏了五个坑
学 JOIN 最容易犯的错误是拿"完美数据"练手——两张表的主键一一对应,每条记录都能匹配上,跑出来结果都一样,然后你自以为领悟了,实际什么都没学到。要真正搞懂内连接和外连接的区别,表里必须同时存在三类数据:能匹配上的、左表有但右表没有的、右表有但左表没有的。缺了任何一类,你看到的执行结果都有迷惑性。
1.1 表结构设计:让"匹配得到"和"匹配不到"同时出现
我设计了两张极简业务表:users(用户表)和 dept(部门表)。用户表里每个用户挂一个 dept_id,部门表里存部门信息。这个模型非常贴近真实场景——用户可能没有分配部门,部门也可能暂时没有成员,这就天然形成了 JOIN 的边界测试数据。
sql复制-- MySQL 8.0 语法,SQL Server / PostgreSQL 改一下字符串类型即可
CREATE TABLE dept (
id INT PRIMARY KEY,
dept_name VARCHAR(50) NOT NULL
);
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
dept_id INT NULL
);
INSERT INTO dept (id, dept_name) VALUES
(101, '研发部'),
(102, '市场部'),
(104, '人事部');
INSERT INTO users (id, name, dept_id) VALUES
(1, '张三', 101),
(2, '李四', 102),
(3, '王五', NULL),
(4, '赵六', 103),
(5, '钱七', 101);
注意我刻意埋的几个雷:
users表里王五的dept_id是NULL,对应"用户没有部门";赵六的dept_id是103,但部门表里根本没有103,对应"用户指向了不存在的部门"(业务上可能是脏数据,也可能是部门已删除但用户没同步);dept表里有104 人事部,但没有任何用户挂在它下面,对应"空部门"。
这三个条件同时具备,你才能在同一套数据上观察到 JOIN 类型对结果集的影响。如果只造"能匹配上"的数据,跑完 inner join 和 left join 行数一模一样,谁也看不出区别,也练不出排查的感觉。
1.2 明确样例数据分布,后面每一步都能核对
列一下后续所有 SQL 的预期基准:
users共 5 行;dept共 3 行;- 两表能通过
users.dept_id = dept.id匹配上的,只有 3 行:张三→研发部、李四→市场部、钱七→研发部; 王五(无部门)、赵六(指向 103)在部门表里没有归属记录;104 人事部在用户表里没有任何成员。
所以内连接的正确答案是 3 行,左连接是 5 行,交叉连接是 5 × 3 = 15 行。拿着这三组预期数字去验证你写的 SQL,比盲跑工具然后看着结果猜逻辑要有效得多。
提示:学 JOIN 自己练习时,强烈建议先手算出预期结果再执行。执行结果和你预期不一致时,不要急着改 SQL,先搞清楚是哪张表的哪一行数据导致数量变化,这个过程才是涨功力的地方。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 内连接(INNER JOIN):为什么它默认"两边都要有"才算数
内连接的语义用一句话讲:只保留左右两张表中满足连接条件的记录。说"交集"不算全对——它更准确的说法是:对左表的每一行,去右表里找"符合 ON 条件"的行,找到就拼成一行输出,找不到就丢弃。
2.1 ON 子句的匹配过程,是逐行"配对"而不是整体"求交"
很多人以为 JOIN 像集合运算中的 INTERSECT,先把两张表分别算出来再取公共部分。其实数据库执行等值连接时,通常的行为是嵌套循环或者哈希匹配——你可以理解为拿着左表的一行,去右表里搜匹配行。这就解释了为什么 JOIN 之后行数可能超过左表:如果左表一行在右表里匹配到多行,结果集就会翻倍放大。
sql复制-- 标准的显式内连接
SELECT u.id AS user_id,
u.name AS user_name,
u.dept_id,
d.id AS dept_id,
d.dept_name
FROM users u
INNER JOIN dept d ON u.dept_id = d.id;
这段 SQL 的执行逻辑,我习惯拆成三步理解:
- 从
users取第一行(比如张三,dept_id = 101); - 拿
101去dept表找id = 101的部门,找到了就拼一行; - 如果找不到(比如赵六的
103),整行直接丢弃,不出现在结果里。
所以最终输出是 3 行。这里有一个新手常犯的认知错误:他们以为 INNER JOIN 会保留"两张表都出现过的部门"。实际上它保留的是"有用户且用户指向的部门存在"的组
