多表查询基础:INNER JOIN、LEFT JOIN、RIGHT JOIN

我曾经把右表的过滤条件写进 WHERE,一个 LEFT JOIN 在线上悄悄退化成 INNER JOIN,运营报表里「零订单用户」整列消失,整整三天都没人发现。

这篇用一套电商 users/orders 数据,把三种 JOIN 的结果差异、ON 与 WHERE 的区别、笛卡尔积坑逐个跑给你看,照着写就能避开。

一、场景:数据为什么非得拆成多张表

真实业务里,把用户、商品、订单全塞进一张大宽表,看似省事,代价是同一个用户下多次订单,姓名、手机号要重复存几十遍;改一次手机号,就得更新几十上百行,稍有遗漏就是数据不一致。

做法 数据存储 改一条信息 一致性风险
全塞一张大宽表 大量重复 要改很多行 高
一事一表 + 外键关联 每个主题只存一次 只改一行 低

数据库设计因此遵循「一事一表」,把不同主题拆开,用外键(Foreign Key)建立表与表的关联;查询时再按关联关系把多张表「拼」成一个结果集(Result Set)。

表拆开容易,拼的时候新问题随之而来:两张表里「对不上」的那些行,到底留还是不留?

二、踩坑:三个让结果集悄悄变样的写法

我在写多表查询时踩过的坑,SQL 都不会报错,出错的是结果行数。

现象 触发写法 后果
数据量瞬间爆炸 FROM users, orders 漏写连接条件 退化成笛卡尔积,行数相乘
主体行莫名消失 LEFT JOIN 后用 WHERE 过滤右表 退化成 INNER,NULL 行被滤掉
该保留的行被丢 没搞清谁是基准表,用错 JOIN 左表或右表的行被误删

最隐蔽的是第二个。我当时盯着报表核了半天,SQL 没有语法错误,执行也正常返回,只是该出现的那批用户整列没了。

为什么一个关键字、一个条件位置的差别,结果行数能差这么多?得先回到 JOIN 的本质。

三、原理:连接只干两件事

连接(JOIN)的本质,是按匹配条件把两张表中满足条件的行组合成新行。它一共只做两件事,而三种 JOIN 的分水岭就在第二件。

latex 复制代码
                  JOIN 连接做的两件事
+----------------------------------------------------+
| 1. 按匹配条件找行          ON u.id = o.user_id       |
| 2. 匹配不上的行怎么处理                              |
|       +--> 丢弃            -> INNER JOIN(内连接)     |
|       +--> 保左表补 NULL   -> LEFT JOIN(左外连接)    |
|       +--> 保右表补 NULL   -> RIGHT JOIN(右外连接)   |
+----------------------------------------------------+

匹配条件决定「哪些行能对上」,第二件事决定「对不上的行留不留」:INNER 直接丢弃,LEFT 保左表,RIGHT 保右表。

注意:连接 = 匹配条件 + 匹配不上的行怎么处理,三种 JOIN 的差别只在第二步。

四、方案:三种 JOIN 各自保留谁

先把同一套测试数据建出来,后面所有结果都基于它,你可以直接复制执行。

sql 复制代码
DROP TABLE IF EXISTS orders;
DROP TABLE IF EXISTS users;

CREATE TABLE users (
  id   INT PRIMARY KEY,
  name VARCHAR(20)
);

CREATE TABLE orders (
  id      INT PRIMARY KEY,
  user_id INT,
  amount  INT
);

INSERT INTO users (id, name) VALUES
  (1, '张三'),
  (2, '李四'),
  (3, '王五');

INSERT INTO orders (id, user_id, amount) VALUES
  (101, 1, 99),
  (102, 1, 200),
  (103, 2, 150),
  (104, 9, 80);

4.1 INNER JOIN:取交集,两边都对得上才留

sql 复制代码
SELECT u.name, o.amount
FROM users u
INNER JOIN orders o ON u.id = o.user_id;
name amount
张三 99
张三 200
李四 150

王五没有订单,被排除;订单 104 的 user_id 是 9,在 users 里找不到对应用户,也被排除。只有「两边都对得上」的组合才保留。

INNER JOIN 适合你只关心「两边都存在」的数据:下过订单的用户及金额、有成绩记录的学生及分数、有库存记录的商品及仓库;缺失一边的数据本就不在分析范围内。

4.2 LEFT JOIN:左表全保留,右表匹配不上补 NULL

sql 复制代码
SELECT u.name, o.amount
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;
name amount
张三 99
张三 200
李四 150
王五 NULL

王五出现了,因为他是左表 users 的行,即使没有订单也保留。订单 104 依然不出现------它属于右表,右表匹配不上的行 LEFT JOIN 不保。

LEFT JOIN 是最常用的外连接,因为很多分析都要以某个主体为基准:查所有用户及其订单、没订单的也要列出,查所有商品及其销量、没卖出的显示为 0 或 NULL,查所有部门及其员工、空部门也要列出。

4.3 RIGHT JOIN:右表全保留,左表匹配不上补 NULL

sql 复制代码
SELECT u.name, o.amount
FROM users u
RIGHT JOIN orders o ON u.id = o.user_id;
name amount
张三 99
张三 200
李四 150
NULL 80

订单 104 出现了,王五不出现。RIGHT JOIN 保留右表全部行,左表按需匹配。它完全可以由 LEFT JOIN 替代,只要交换两张表的位置:

sql 复制代码
SELECT u.name, o.amount
FROM orders o
LEFT JOIN users u ON u.id = o.user_id;

多数团队约定只用 LEFT JOIN:阅读习惯从左到右,左表为基准更符合直觉;RIGHT JOIN 要求读者先跳到右边看基准表,认知负担更大。

三种结果放在一起对比:

连接 保留范围 结果行数 匹配不上的行
INNER JOIN 两表交集 3 丢弃
LEFT JOIN 左表全部 4 右表补 NULL
RIGHT JOIN 右表全部 4 左表补 NULL

注意:INNER 取交集、缺一边就丢行,LEFT 保左表、RIGHT 保右表。

4.4 ON 与 WHERE:右表条件放错位置,语义就变了

LEFT JOIN 有一个高频出错点:同一个条件写在 ON 和写在 WHERE,结果完全不同。

写法 A,条件写在 ON:

sql 复制代码
SELECT u.name, o.amount
FROM users u
LEFT JOIN orders o
  ON u.id = o.user_id
 AND o.amount > 100;
name amount
张三 200
李四 150
王五 NULL

写法 B,条件写在 WHERE:

sql 复制代码
SELECT u.name, o.amount
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.amount > 100;
name amount
张三 200
李四 150

两者的执行时机不同:

  • ON 在连接时生效,右表先按条件过滤再连接,匹配不上的左表行仍然保留,只是补 NULL,所以左表一条不少。
  • WHERE 在连接完成后生效,对整个结果集过滤;NULL 参与比较结果是 unknown 而不是 true,amount 为 NULL 的行被全部滤掉,左表保留行随之消失。

注意:要保留主体表全部行,右表的过滤条件写在 ON;写在 WHERE 会滤掉 NULL 行,悄悄改成 INNER。

五、验证:你跑一遍,看行数怎么变

把第四节的脚本整体执行,再对照下表核对,就能验证你是否真的理解了。

连接写法 预期行数 必现 / 必隐的行
INNER JOIN 3 王五与订单 104 都不出现
LEFT JOIN 4 王五 NULL 必现
RIGHT JOIN 4 NULL 80 必现
LEFT + 条件在 ON 3 左表用户全在,王五 NULL 保留
LEFT + 条件在 WHERE 2 王五与 NULL 行消失

再用两个小动作亲手验证边界:

  • 故意漏写连接条件,你会拿到 12 行(3 × 4),这就是笛卡尔积:
sql 复制代码
SELECT u.name, o.amount
FROM users u, orders o;
  • 给关联字段建好索引后跑 EXPLAIN,确认连接是否真正命中索引:
sql 复制代码
EXPLAIN
SELECT u.name, o.amount
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;

正确:条件在 ON,结果 3 行,左表用户全保留。
错误:条件在 WHERE,结果 2 行,NULL 行被误滤、语义被改变。

六、总结:怎么选不踩坑

动手前先问自己三个问题:

  • 我只关心「两边都有」,还是「某一边全都要」?只取匹配成功的用 INNER;要保留某一侧全部行的用外连接。
  • 谁是主体表?主体在左用 LEFT,在右用 RIGHT,或者交换表顺序统一写成 LEFT。
  • 关联表的过滤条件放哪?要保留主体全部行就放 ON;放 WHERE 可能滤掉 NULL 行,改变连接语义。

「LEFT JOIN 一定比 INNER JOIN 慢」是误区。真正决定性能的是连接字段有没有索引、条件能否命中索引、是否产生笛卡尔积。INNER 结果集更小往往更快,但不能一概而论,用 EXPLAIN 看执行计划再下结论。

注意:先定谁是主体、谁允许缺失,再选 JOIN,最后用 EXPLAIN 验证,别凭感觉。

术语速查表

术语 英文 含义
连接 JOIN 按匹配条件把多张表的行组合成新行
内连接 INNER JOIN 只返回两表匹配的交集行
左外连接 LEFT JOIN 保留左表全部行,右表匹配不上补 NULL
右外连接 RIGHT JOIN 保留右表全部行,左表匹配不上补 NULL
外键 Foreign Key 建立并约束两表关联关系的字段
空值 NULL 表示值未知或缺失,不等于 0 或空字符串
结果集 Result Set 查询执行后返回的行集合
笛卡尔积 Cartesian Product 漏连接条件时两表行数相乘得到的结果

参考链接

相关推荐
卡卡~~5 小时前
“101计划”核心课程实践教材《数据实验结构指导-C++语言版》题目集
开发语言·c++·学习·算法
为思念酝酿的痛5 小时前
MySQL数据类型
数据库·mysql
smilejingwei5 小时前
时间列转行再转置为新交叉表
sql·sqlazy·ai写sql·可信sql
企业数字化笔记5 小时前
AI视频增强后画质怎么验收?插值基线、伪细节与帧间闪烁的检查方法
人工智能·算法·音视频
学逆向的6 小时前
C 内存属性
c语言·开发语言·算法·网络安全
泡海椒6 小时前
JQuick-SQL 与关系型数据库集成:应用层读取后注册查询
数据库·sql
Carol06306 小时前
上肢及肩肘关节
java·服务器·数据库
lengxuenong6 小时前
树状数组 · 信息学学习手册1-求和3个模板
c++·学习·算法·青少年编程
ComputerInBook6 小时前
泛函分析概观
人工智能·算法·机器学习·泛函分析