INNER JOIN / LEFT JOIN / RIGHT JOIN / FULL JOIN / CROSS JOIN 各自语义?示例区别?
What are the semantics and example differences of INNER JOIN / LEFT JOIN / RIGHT JOIN / FULL JOIN
考察要点
这道题是 SQL 面试的“送分题”,但也是“筛选题”。面试官想通过这道题,快速判断你对 SQL 核心语法的掌握是否扎实、清晰。它主要考察两点:
- 语义精确性:你是否能准确描述每种
JOIN如何根据匹配条件筛选和组合数据,特别是对OUTER JOIN中非匹配行的处理(即NULL的产生)。 - 边界情况理解:你是否清楚在没有匹配、连接键为
NULL等情况下,不同JOIN的行为差异。这是区分“会用”和“精通”的关键。
核心概念
为了清晰地展示区别,我们使用两张表:employees (员工表) 和 departments (部门表)。
示例数据:
employees 表:包含有部门的员工、新入职暂未分配部门的员工。
| id | name | dept_id |
|---|---|---|
| 1 | 张三 | 101 |
| 2 | 李四 | 102 |
| 3 | 王五 | 101 |
| 4 | 赵六 (新人) | NULL |
departments 表:包含有员工的部门、以及刚成立暂无员工的部门。
| id | dept_name |
|---|---|
| 101 | 研发部 |
| 102 | 测试部 |
| 103 | HR部 (新) |
1. INNER JOIN (内连接)
语义:只返回两个表中连接键(ON 子句中的列)能够互相匹配上的行。这是最常用、最严格的连接类型。
图示:
1 Table A Table B2 +-------+ +-------+3 | | | |4 | +---+-----+---+ |5 | | | | | |6 +---+---+-----+---+---+7 | Matched |8 +-----------+
SQL 示例:
1SELECT e.name, d.dept_name2FROM employees e3INNER JOIN departments d ON e.dept_id = d.id;
结果: 只包含在 departments 表中能找到 dept_id 的员工。
| name | dept_name |
|---|---|
| 张三 | 研发部 |
| 李四 | 测试部 |
| 王五 | 研发部 |
2. LEFT JOIN (左外连接)
语义:返回左表(FROM 子句中先出现的表)的所有行,以及右表中与左表匹配的行。如果右表中没有匹配的行,则结果中右表的列将显示为 NULL。
图示:
1 Table A (Left) Table B (Right)2 +-------+ +-------+3 |#######| | |4 |#######+-----+---+ |5 |#######| | | |6 +-------+-----+---+---+
SQL 示例:
1SELECT e.name, d.dept_name2FROM employees e3LEFT JOIN departments d ON e.dept_id = d.id;
结果: 包含所有员工,即使他没有部门信息(赵六)。
| name | dept_name |
|---|---|
| 张三 | 研发部 |
| 李四 | 测试部 |
| 王五 | 研发部 |
| 赵六 (新人) | NULL |
3. RIGHT JOIN (右外连接)
语义:与 LEFT JOIN 相反。返回右表(JOIN 关键字后出现的表)的所有行,以及左表中与右表匹配的行。如果左表中没有匹配的行,则结果中左表的列将显示为 NULL。
图示:
1 Table A (Left) Table B (Right)2 +-------+ +-------+3 | | |#######|4 | +---+-----+#######|5 | | | |#######|6 +---+---+-----+-------+
SQL 示例:
1SELECT e.name, d.dept_name2FROM employees e3RIGHT JOIN departments d ON e.dept_id = d.id;
结果: 包含所有部门,即使它没有员工(HR部)。
| name | dept_name |
|---|---|
| 张三 | 研发部 |
| 王五 | 研发部 |
| 李四 | 测试部 |
| NULL | HR部 (新) |
注意:A RIGHT JOIN B 等价于 B LEFT JOIN A。在工程实践中,为了代码可读性和风格统一,通常推荐优先使用 LEFT JOIN。
4. FULL OUTER JOIN (全外连接)
语义:返回左表和右表中的所有行。当某一行在另一个表中没有匹配时,另一个表的列为 NULL。它本质上是 LEFT JOIN 和 RIGHT JOIN 结果的并集。
图示:
1 Table A Table B2 +-------+ +-------+3 |#######| |#######|4 |#######+-----+#######|5 |#######| |#######|6 +-------+-----+-------+
SQL 示例 (PostgreSQL / Oracle / SQL Server):
1SELECT e.name, d.dept_name2FROM employees e3FULL OUTER JOIN departments d ON e.dept_id = d.id;
结果: 包含所有员工和所有部门。
| name | dept_name |
|---|---|
| 张三 | 研发部 |
| 李四 | 测试部 |
| 王五 | 研发部 |
| 赵六 (新人) | NULL |
| NULL | HR部 (新) |
MySQL 中的模拟: MySQL 不支持 FULL OUTER JOIN 语法。可以通过 LEFT JOIN 和 RIGHT JOIN 的 UNION 来模拟。
1-- 方法1: UNION 会去重,但对于 OUTER JOIN 场景,左右两部分没有交集,效果等同于 UNION ALL2SELECT e.name, d.dept_name3FROM employees e4LEFT JOIN departments d ON e.dept_id = d.id5UNION6SELECT e.name, d.dept_name7FROM employees e8RIGHT JOIN departments d ON e.dept_id = d.id;
5. CROSS JOIN (交叉连接 / 笛卡尔积)
语义:返回左表中的每一行与右表中的每一行的组合。结果集的行数等于左表行数乘以右表行数。它不需要 ON 子句。
SQL 示例:
1-- 显式语法2SELECT e.name, d.dept_name3FROM employees e4CROSS JOIN departments d;56-- 隐式语法 (老式写法,不推荐)7SELECT e.name, d.dept_name8FROM employees e, departments d;
结果: employees 表有 4 行,departments 表有 3 行,结果为 4 * 3 = 12 行。
| name | dept_name |
|---|---|
| 张三 | 研发部 |
| 张三 | 测试部 |
| 张三 | HR部 (新) |
| 李四 | 研发部 |
| 李四 | 测试部 |
| ... | ... |
| 赵六 (新人) | HR部 (新) |
常见低分回答
- 将
OUTER JOIN的过滤条件写在WHERE子句中:这是一个非常经典的错误。ON子句在连接阶段生效,WHERE子句在连接结果生成后生效。对于OUTER JOIN,将过滤条件放在WHERE会把OUTER JOIN降级为INNER JOIN。sql1-- 错误示例:本意是想找所有员工,并显示出他们在研发部的信息2SELECT e.name, d.dept_name3FROM employees e4LEFT JOIN departments d ON e.dept_id = d.id5WHERE d.dept_name = '研发部'; -- 这会过滤掉 dept_name 为 NULL 的行(如赵六),导致结果等同于 INNER JOIN67-- 正确写法:应将过滤条件放在 ON 子句中8SELECT e.name, d.dept_name9FROM employees e10LEFT JOIN departments d ON e.dept_id = d.id AND d.dept_name = '研发部'; - 不了解 MySQL 不支持
FULL OUTER JOIN:直接写出FULL OUTER JOIN语法,当被问到在 MySQL 中如何实现时,无法给出UNION的模拟方案。 - 滥用或误用
CROSS JOIN:在FROM后用逗号分隔多张表,但忘记在WHERE中写连接条件,导致意外生成笛卡尔积,在数据量大时会造成数据库严重性能问题甚至宕机。sql1-- 危险操作:忘记 WHERE 条件2SELECT * FROM orders, customers; -- 如果 orders 有100万行,customers 有10万行,会尝试生成 1000亿行结果 - 混淆
JOIN和UNION:JOIN是横向连接,用于将不同表的列合并在一起;UNION是纵向连接,用于将多个查询的结果集行合并在一起,要求列的数量和类型兼容。
工程实践与优化
- 索引是生命线:所有
JOIN的ON子句中用到的列(如e.dept_id和d.id)都必须建立索引。否则,数据库可能采用效率极低的嵌套循环连接(Nested Loop Join),复杂度为 。有索引时,可利用索引嵌套循环连接(Index Nested-Loop Join),复杂度降至 。 - 小表驱动大表:在
JOIN操作中,数据库优化器会尝试用小表(结果集行数少的表)去驱动大表(扫描大表)。对于INNER JOIN,优化器会自动选择。对于LEFT JOIN,左表是驱动表;对于RIGHT JOIN,右表是驱动表。手动优化时,应将小表作为驱动表,可以减少外层循环次数。 EXPLAIN分析:使用EXPLAIN查看JOIN的执行计划。type列:eq_ref(最佳,通常是主键/唯一键连接)、ref(良好,非唯一索引连接) 是我们期望看到的。ALL(全表扫描) 则表示性能瓶颈。Extra列:Using index表示使用了覆盖索引,性能很好。Using join buffer (Block Nested Loop)表示没有有效利用索引,性能很差。
Hash Join的应用:MySQL 8.0.18+ 引入了Hash Join。当连接的列没有索引,且内存充足时,优化器会选择它。它将一张表(通常是小表)在内存中构建哈希表,然后流式读取大表进行匹配。这对于大数据量下的JOIN是一种有效的优化,避免了无索引时的磁盘 I/O 噩梦。OUTER JOIN的代价:OUTER JOIN需要保留未匹配的行,可能产生比INNER JOIN大得多的中间结果集,增加了内存和 CPU 的消耗。因此,在确认业务逻辑允许的情况下,优先使用INNER JOIN。
常见追问与延伸题目
-
追问:
LEFT JOIN ON ... AND ...和LEFT JOIN ON ... WHERE ...的区别是什么?- 回答要点:这是考察对 SQL 执行顺序理解的经典问题。
LEFT JOIN ... ON a.col = b.col AND b.filter_col = 'value':AND条件是 连接过程的一部分。它在生成连接结果集时,就对右表b进行了过滤。即使右表某行因为b.filter_col不满足条件而未被匹配,左表a的行仍然会被保留,对应的右表列为NULL。LEFT JOIN ... ON a.col = b.col WHERE b.filter_col = 'value':WHERE条件是在 连接结果集生成之后 进行过滤。由于LEFT JOIN会为不匹配的行生成NULL,WHERE子句的b.filter_col = 'value'会将这些NULL的行(以及其他不满足条件的行)全部过滤掉。这导致LEFT JOIN的外连接特性失效,其效果等同于INNER JOIN。
- 回答要点:这是考察对 SQL 执行顺序理解的经典问题。
-
追问:如果让你实现一个
ANTI JOIN,你会怎么写?比如,查找所有没有员工的部门。- 回答要点:
ANTI JOIN指的是返回左表中在右表中没有匹配的行。有多种实现方式,性能和适用场景各异。LEFT JOIN ... WHERE ... IS NULL:这是最经典、最通用的写法。可读性好,且优化器通常能很好地处理。sql1SELECT d.*2FROM departments d3LEFT JOIN employees e ON d.id = e.dept_id4WHERE e.id IS NULL;NOT EXISTS:使用子查询,语义清晰,通常性能也很好,尤其是在子查询的表(employees)连接键上有索引时。sql1SELECT d.*2FROM departments d3WHERE NOT EXISTS (4 SELECT 1 FROM employees e WHERE e.dept_id = d.id5);NOT IN:需要特别小心NULL值。如果子查询(SELECT dept_id FROM employees)的结果中包含NULL,NOT IN的结果将永远为空集,因为X NOT IN (..., NULL, ...)的逻辑判断会是UNKNOWN。所以使用NOT IN时要确保子查询结果不含NULL。sql1SELECT d.*2FROM departments d3WHERE d.id NOT IN (SELECT dept_id FROM employees WHERE dept_id IS NOT NULL);
- 回答要点:
-
追问:如果
JOIN的两张表都非常大,比如上亿行,有什么优化思路?- 回答要点:这是考察大数据场景下的工程能力。
- SQL 层面:确保连接键上有合适的索引,并且数据类型完全一致以避免隐式转换。使用
EXPLAIN ANALYZE分析详细的执行计划和耗时。考虑是否能用STRAIGHT_JOIN强制指定连接顺序,防止优化器选错。 - 业务层面:能否通过增加冗余字段来避免
JOIN?比如在employees表中冗余dept_name,用空间换时间。这需要评估数据一致性的维护成本。 - 架构层面:
- 分库分表:如果数据已经分片,
JOIN操作会变成跨节点的分布式JOIN,非常低效。此时应尽量避免JOIN,通过两次单表查询在应用层进行数据组装。 - 数据异构:将需要
JOIN的数据通过 ETL 任务,提前计算好并存入一个宽表,或者同步到 Elasticsearch、ClickHouse 等更适合复杂查询和分析的系统中。查询时直接查宽表或新系统,避免实时JOIN。 - 批处理:如果不是实时性要求高的查询,可以将计算任务放到离线的批处理框架(如 Spark/Flink)中完成,将结果写入结果表供线上查询。
- 分库分表:如果数据已经分片,
- SQL 层面:确保连接键上有合适的索引,并且数据类型完全一致以避免隐式转换。使用
- 回答要点:这是考察大数据场景下的工程能力。