多表查询基础: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 漏连接条件时两表行数相乘得到的结果

参考链接

相关推荐
IvorySQL1 小时前
PostgreSQL 日报 | 可见性映射数据损坏修复(9 月 26 日)
数据库·postgresql
Seraphina361 小时前
DVWA(SQL注入-low,medium,XSS反射-low)
前端·数据库·笔记·sql·网络安全·web·xss
大侠归来2 小时前
C语言素数判断:从入门到进阶
java·c语言·算法
v:PLLucky202 小时前
农作物病虫害检测识别系统
深度学习·scrapy·算法·百度·微信公众平台
CHEEVEN_QY2 小时前
搅拌摩擦焊FSW工艺参数对焊缝成形影响的实验研究10.1
linux·服务器·数据库
朝朝辞暮i2 小时前
C++ 第一阶段总结:从零基础到 ROS2 前置语法
开发语言·c++·算法
帷幕落秋2 小时前
Redis哨兵模式
运维·数据库
H.莓飛2 小时前
【数据结构】链表_OJ题
linux·数据结构·算法·链表
All for pursuit.2 小时前
【回溯-4】46.全排列
数据结构·c++·算法·leetcode