说透 SQL 连接:一文讲清 INNER JOIN、LEFT JOIN、RIGHT JOIN、FULL JOIN
连表查询是 SQL 中最核心、最常用的操作,但很多人对 LEFT JOIN 和 RIGHT JOIN 的理解只停留在"左表全保留,右表匹配"的表面。今天,我们用图解 + 实例 + 深度剖析,彻底把这几种连接搞明白。
在数据库查询中,连接(JOIN)堪称灵魂操作。无论是单表查询还是多表联查,掌握连接的原理都是进阶高手的必经之路。本文将从集合论的角度出发,结合生动的例子,帮你一次性理清 INNER JOIN、LEFT JOIN、RIGHT JOIN 和 FULL OUTER JOIN 的本质区别。
一、为什么需要连接?
现实世界中的数据往往是分散存储的:用户表、订单表、商品表......要获取"购买了某商品的用户信息",就必须把用户表和订单表关联起来。连接就是用来根据两个表之间的共有列(通常是外键关系)合并数据的。
二、四种连接的核心概念速览
假设我们有两个简单的表:
学生表 (student)
| id | name | class_id |
|---|---|---|
| 1 | 张三 | 1 |
| 2 | 李四 | 2 |
| 3 | 王五 | NULL |
班级表 (class)
| id | class_name |
|---|---|
| 1 | 计算机1班 |
| 2 | 计算机2班 |
| 4 | 计算机3班 |
注意:学生表中的王五没有班级;班级表中 id=4 的班级没有学生。
我们用这两张表来演示各种连接的结果。
1. INNER JOIN(内连接)
定义 :返回两个表中连接字段相等 的记录行。即只有满足 ON 条件的行才会出现在结果集中。
SQL:
sql
SELECT * FROM student
INNER JOIN class ON student.class_id = class.id;
结果:
| student.id | name | class_id | class.id | class_name |
|---|---|---|---|---|
| 1 | 张三 | 1 | 1 | 计算机1班 |
| 2 | 李四 | 2 | 2 | 计算机2班 |
王五(class_id 为 NULL)和计算机3班(id=4)都没有匹配上,因此不出现。
适用场景:只关心两个表都有对应关系的数据,比如查询"已分班的学生及其班级信息"。
2. LEFT JOIN(左连接,也称为 LEFT OUTER JOIN)
定义 :返回左表的所有行,即使右表中没有匹配的行。右表中没有匹配的列用 NULL 填充。
SQL:
sql
SELECT * FROM student
LEFT JOIN class ON student.class_id = class.id;
结果:
| student.id | name | class_id | class.id | class_name |
|---|---|---|---|---|
| 1 | 张三 | 1 | 1 | 计算机1班 |
| 2 | 李四 | 2 | 2 | 计算机2班 |
| 3 | 王五 | NULL | NULL | NULL |
王五虽然班级为空,但因为是左表(student)的记录,仍然被保留。
适用场景:查询"所有学生及其班级,没有班级的也要显示学生信息"。
3. RIGHT JOIN(右连接,也称为 RIGHT OUTER JOIN)
定义 :返回右表的所有行,即使左表中没有匹配的行。左表中没有匹配的列用 NULL 填充。
SQL:
sql
SELECT * FROM student
RIGHT JOIN class ON student.class_id = class.id;
结果:
| student.id | name | class_id | class.id | class_name |
|---|---|---|---|---|
| 1 | 张三 | 1 | 1 | 计算机1班 |
| 2 | 李四 | 2 | 2 | 计算机2班 |
| NULL | NULL | NULL | 4 | 计算机3班 |
计算机3班虽然没有学生,但仍被保留。
适用场景:查询"所有班级及其学生,没有学生的班级也要显示"。
注意 :
RIGHT JOIN可以完全用LEFT JOIN替换(只需交换两表顺序)。实际开发中LEFT JOIN使用频率远高于RIGHT JOIN,但理解它有助于对称思维。
4. FULL OUTER JOIN(全外连接)
定义 :返回左表和右表的所有行 。当某一行在另一表中没有匹配时,对应列填充 NULL。相当于 LEFT JOIN 与 RIGHT JOIN 的并集。
SQL (MySQL 不直接支持 FULL OUTER JOIN,但可以用 LEFT JOIN UNION RIGHT JOIN 模拟):
sql
SELECT * FROM student
LEFT JOIN class ON student.class_id = class.id
UNION
SELECT * FROM student
RIGHT JOIN class ON student.class_id = class.id;
结果:
| student.id | name | class_id | class.id | class_name |
|---|---|---|---|---|
| 1 | 张三 | 1 | 1 | 计算机1班 |
| 2 | 李四 | 2 | 2 | 计算机2班 |
| 3 | 王五 | NULL | NULL | NULL |
| NULL | NULL | NULL | 4 | 计算机3班 |
适用场景:需要完整的关联信息,比如"两表的全量数据对比"。
三、图解四种连接(ASCII 字符画版)
1. INNER JOIN(内连接)------ 交集
text
┌──────────────┐ ┌──────────────┐
│ 学生表 │ │ 班级表 │
│ ┌───────┐ │ │ ┌───────┐ │
│ │ 匹配行 │◄──┼─────┼──►│ 匹配行 │ │
│ └───────┘ │ │ └───────┘ │
│ 其他行 ✗ │ │ 其他行 ✗ │
└──────────────┘ └──────────────┘
结果:只有两个圈重叠的部分
2. LEFT JOIN(左连接)------ 左表全部 + 交集
text
┌──────────────┐ ┌──────────────┐
│ 学生表 │ │ 班级表 │
│ ┌───────┐ │ │ ┌───────┐ │
│ │ 匹配行 │◄──┼─────┼──►│ 匹配行 │ │
│ ├───────┤ │ │ └───────┘ │
│ │ 无匹配行│ │ │ 其他行 ✗ │
│ │ (保留) │ │ │ │
│ └───────┘ │ └──────────────┘
└──────────────┘
结果:左表所有行 + 右表匹配到的列(无匹配补 NULL)
3. RIGHT JOIN(右连接)------ 右表全部 + 交集
text
┌──────────────┐ ┌──────────────┐
│ 学生表 │ │ 班级表 │
│ ┌───────┐ │ │ ┌───────┐ │
│ │ 匹配行 │◄──┼─────┼──►│ 匹配行 │ │
│ └───────┘ │ │ ├───────┤ │
│ 其他行 ✗ │ │ │ 无匹配行│ │
│ │ │ │ (保留) │ │
└──────────────┘ │ └───────┘ │
└──────────────┘
结果:右表所有行 + 左表匹配到的列(无匹配补 NULL)
4. FULL OUTER JOIN(全外连接)------ 并集
text
┌──────────────┐ ┌──────────────┐
│ 学生表 │ │ 班级表 │
│ ┌───────┐ │ │ ┌───────┐ │
│ │ 匹配行 │◄──┼─────┼──►│ 匹配行 │ │
│ ├───────┤ │ │ ├───────┤ │
│ │ 无匹配行│ │ │ │ 无匹配行│ │
│ │ (保留) │ │ │ │ (保留) │ │
│ └───────┘ │ │ └───────┘ │
└──────────────┘ └──────────────┘
结果:左表所有行 + 右表所有行(合并去重,无匹配补 NULL)
四、过滤条件放在 WHERE 还是 ON?------ 经典陷阱
很多初学者在使用 LEFT JOIN 时,误将过滤条件放在 WHERE 子句中,导致左连接"退化"为内连接。
错误示例:
sql
SELECT * FROM student
LEFT JOIN class ON student.class_id = class.id
WHERE class.class_name = '计算机1班';
意图是想保留所有学生,同时只显示班级为"计算机1班"的匹配信息。但 WHERE 会在连接完成后过滤,由于不匹配的行中 class_name 为 NULL,被 WHERE 条件过滤掉,结果只剩下匹配的那一行(相当于 INNER JOIN)。
正确做法:把过滤条件放在 ON 子句中:
sql
SELECT * FROM student
LEFT JOIN class ON student.class_id = class.id AND class.class_name = '计算机1班';
此时左表所有行保留,不匹配的班级信息显示 NULL。
经验法则:
- ON 决定如何连接两个表(哪些行匹配)
- WHERE 决定连接后哪些行被保留
对于 INNER JOIN,放在 ON 和 WHERE 效果相同(因为不匹配的行本来就不会出现);但对于 OUTER JOIN(LEFT/RIGHT/FULL),两者结果可能完全不同。
五、性能与优化建议
- 优先使用 INNER JOIN:如果业务逻辑不需要保留未匹配的行,INNER JOIN 通常比 OUTER JOIN 更快,因为数据库优化器有更多选择(如使用索引合并)。
- 小表驱动大表:在连接条件上,让返回行数较少的表作为驱动表(LEFT JOIN 的左表就是驱动表)。
- 确保连接列有索引:无论是 INNER 还是 OUTER JOIN,连接列上的索引至关重要,尤其是当表数据量较大时。
- **避免 SELECT ***:只选择需要的列,减少数据传输和内存开销。
- 小心 LEFT JOIN 产生笛卡尔积:如果左表的一行在右表有多行匹配,结果集会膨胀。这是连接的正常行为,但要注意是否符合业务预期。
六、其他连接类型
1. CROSS JOIN(交叉连接)
返回两表的笛卡尔积,即左表每一行与右表每一行组合。很少显式使用,但可能因忘记写 ON 条件而意外产生。
2. NATURAL JOIN(自然连接)
自动根据两个表中同名的列进行等值连接。不推荐使用,因为不可控(容易因表结构变化导致结果异常)。
3. SELF JOIN(自连接)
一个表与自身连接,通常用于查询层级关系(如员工-经理)。可以配合 INNER/LEFT 使用。
七、实战案例:复杂查询中的 LEFT JOIN
假设有三张表:orders(订单)、customers(客户)、payments(支付)。要查询所有客户及其订单和支付信息,即使没有订单或支付的客户也要显示。
sql
SELECT
c.customer_id,
c.name,
o.order_id,
p.amount
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
LEFT JOIN payments p ON o.order_id = p.order_id;
这条查询:
- 保留了所有客户(第一个 LEFT JOIN)
- 即使某个客户没有订单,orders 列为 NULL;即使某个订单没有支付,payments 列为 NULL。
- 结果是客户的全量视图。
如果换成 INNER JOIN,那些没有订单的客户就会被过滤掉。
八、面试高频问题
Q1:LEFT JOIN 和 RIGHT JOIN 可以互换吗?
A:可以,只需交换表的顺序。A LEFT JOIN B 等价于 B RIGHT JOIN A。通常建议统一使用 LEFT JOIN 以保持代码风格一致。
Q2:FULL OUTER JOIN 在 MySQL 中如何实现?
A:LEFT JOIN UNION RIGHT JOIN,注意 UNION 会去重,如果不需要去重可以用 UNION ALL(但通常 LEFT 和 RIGHT 的结果没有重复行,因为两边的 NULL 行不同)。
Q3:ON 条件中可以使用 != 吗?
A:可以,但非常少见。例如 LEFT JOIN ... ON A.id != B.id 会产生近乎笛卡尔积的结果,通常不是业务想要的效果。
Q4:多个 LEFT JOIN 的执行顺序?
A:按书写顺序从左到右逻辑处理,但数据库优化器可能会重排。建议用括号明确优先级。
Q5:如何判断一个查询应该用 INNER JOIN 还是 LEFT JOIN?
A:问自己:"是否必须保留左表的全部行,即使右表没有匹配?" 如果答案是"是",用 LEFT JOIN;否则用 INNER JOIN。
九、总结
| 连接类型 | 结果集特征 | 典型关键词 |
|---|---|---|
| INNER JOIN | 只返回匹配的行 | "两个表都有" |
| LEFT JOIN | 左表全部 + 右表匹配 | "以左表为基准" |
| RIGHT JOIN | 右表全部 + 左表匹配 | "以右表为基准" |
| FULL OUTER JOIN | 两个表全部 | "全部数据" |
记住一句话:LEFT JOIN 以左表为尊,右表随缘;INNER JOIN 双方门当户对;FULL JOIN 海纳百川。
掌握了这些,你就能应对 90% 以上的连表查询需求。
思考题:
- 对于 OUTER JOIN,若 ON 条件中包含对右表的非空字段过滤(如
RIGHT_TABLE.column = 'value'),为什么结果可能会变成 INNER JOIN?如何避免? - 在多表 LEFT JOIN 中,如果某个中间表没有匹配,后续的 LEFT JOIN 还能关联到数据吗?
欢迎评论区讨论!
如果觉得有帮助,点赞、收藏、转发~
本文首发于 CSDN,未经授权禁止转载。