本页内容受版权保护 · 已添加水印 · 禁止任何形式转载
查询基础(SELECT / JOIN / 聚合 / 排序) · JOIN 多表连接

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 核心语法的掌握是否扎实、清晰。它主要考察两点:

  1. 语义精确性:你是否能准确描述每种 JOIN 如何根据匹配条件筛选和组合数据,特别是对 OUTER JOIN 中非匹配行的处理(即 NULL 的产生)。
  2. 边界情况理解:你是否清楚在没有匹配、连接键为 NULL 等情况下,不同 JOIN 的行为差异。这是区分“会用”和“精通”的关键。

核心概念

为了清晰地展示区别,我们使用两张表:employees (员工表) 和 departments (部门表)。

示例数据:

employees 表:包含有部门的员工、新入职暂未分配部门的员工。

idnamedept_id
1张三101
2李四102
3王五101
4赵六 (新人)NULL

departments 表:包含有员工的部门、以及刚成立暂无员工的部门。

iddept_name
101研发部
102测试部
103HR部 (新)

1. INNER JOIN (内连接)

语义:只返回两个表中连接键(ON 子句中的列)能够互相匹配上的行。这是最常用、最严格的连接类型。

图示:

text
1 Table A Table B
2 +-------+ +-------+
3 | | | |
4 | +---+-----+---+ |
5 | | | | | |
6 +---+---+-----+---+---+
7 | Matched |
8 +-----------+

SQL 示例:

sql
1SELECT e.name, d.dept_name
2FROM employees e
3INNER JOIN departments d ON e.dept_id = d.id;

结果: 只包含在 departments 表中能找到 dept_id 的员工。

namedept_name
张三研发部
李四测试部
王五研发部

2. LEFT JOIN (左外连接)

语义:返回左表(FROM 子句中先出现的表)的所有行,以及右表中与左表匹配的行。如果右表中没有匹配的行,则结果中右表的列将显示为 NULL。

图示:

text
1 Table A (Left) Table B (Right)
2 +-------+ +-------+
3 |#######| | |
4 |#######+-----+---+ |
5 |#######| | | |
6 +-------+-----+---+---+

SQL 示例:

sql
1SELECT e.name, d.dept_name
2FROM employees e
3LEFT JOIN departments d ON e.dept_id = d.id;

结果: 包含所有员工,即使他没有部门信息(赵六)。

namedept_name
张三研发部
李四测试部
王五研发部
赵六 (新人)NULL

3. RIGHT JOIN (右外连接)

语义:与 LEFT JOIN 相反。返回右表(JOIN 关键字后出现的表)的所有行,以及左表中与右表匹配的行。如果左表中没有匹配的行,则结果中左表的列将显示为 NULL。

图示:

text
1 Table A (Left) Table B (Right)
2 +-------+ +-------+
3 | | |#######|
4 | +---+-----+#######|
5 | | | |#######|
6 +---+---+-----+-------+

SQL 示例:

sql
1SELECT e.name, d.dept_name
2FROM employees e
3RIGHT JOIN departments d ON e.dept_id = d.id;

结果: 包含所有部门,即使它没有员工(HR部)。

namedept_name
张三研发部
王五研发部
李四测试部
NULLHR部 (新)

注意:A RIGHT JOIN B 等价于 B LEFT JOIN A。在工程实践中,为了代码可读性和风格统一,通常推荐优先使用 LEFT JOIN。


4. FULL OUTER JOIN (全外连接)

语义:返回左表和右表中的所有行。当某一行在另一个表中没有匹配时,另一个表的列为 NULL。它本质上是 LEFT JOIN 和 RIGHT JOIN 结果的并集。

图示:

text
1 Table A Table B
2 +-------+ +-------+
3 |#######| |#######|
4 |#######+-----+#######|
5 |#######| |#######|
6 +-------+-----+-------+

SQL 示例 (PostgreSQL / Oracle / SQL Server):

sql
1SELECT e.name, d.dept_name
2FROM employees e
3FULL OUTER JOIN departments d ON e.dept_id = d.id;

结果: 包含所有员工和所有部门。

namedept_name
张三研发部
李四测试部
王五研发部
赵六 (新人)NULL
NULLHR部 (新)

MySQL 中的模拟: MySQL 不支持 FULL OUTER JOIN 语法。可以通过 LEFT JOIN 和 RIGHT JOIN 的 UNION 来模拟。

sql
1-- 方法1: UNION 会去重,但对于 OUTER JOIN 场景,左右两部分没有交集,效果等同于 UNION ALL
2SELECT e.name, d.dept_name
3FROM employees e
4LEFT JOIN departments d ON e.dept_id = d.id
5UNION
6SELECT e.name, d.dept_name
7FROM employees e
8RIGHT JOIN departments d ON e.dept_id = d.id;

5. CROSS JOIN (交叉连接 / 笛卡尔积)

语义:返回左表中的每一行与右表中的每一行的组合。结果集的行数等于左表行数乘以右表行数。它不需要 ON 子句。

SQL 示例:

sql
1-- 显式语法
2SELECT e.name, d.dept_name
3FROM employees e
4CROSS JOIN departments d;
5
6-- 隐式语法 (老式写法,不推荐)
7SELECT e.name, d.dept_name
8FROM employees e, departments d;

结果: employees 表有 4 行,departments 表有 3 行,结果为 4 * 3 = 12 行。

namedept_name
张三研发部
张三测试部
张三HR部 (新)
李四研发部
李四测试部
......
赵六 (新人)HR部 (新)

常见低分回答

  1. 将 OUTER JOIN 的过滤条件写在 WHERE 子句中:这是一个非常经典的错误。ON 子句在连接阶段生效,WHERE 子句在连接结果生成后生效。对于 OUTER JOIN,将过滤条件放在 WHERE 会把 OUTER JOIN 降级为 INNER JOIN。
    sql
    1-- 错误示例:本意是想找所有员工,并显示出他们在研发部的信息
    2SELECT e.name, d.dept_name
    3FROM employees e
    4LEFT JOIN departments d ON e.dept_id = d.id
    5WHERE d.dept_name = '研发部'; -- 这会过滤掉 dept_name 为 NULL 的行(如赵六),导致结果等同于 INNER JOIN
    6
    7-- 正确写法:应将过滤条件放在 ON 子句中
    8SELECT e.name, d.dept_name
    9FROM employees e
    10LEFT JOIN departments d ON e.dept_id = d.id AND d.dept_name = '研发部';
  2. 不了解 MySQL 不支持 FULL OUTER JOIN:直接写出 FULL OUTER JOIN 语法,当被问到在 MySQL 中如何实现时,无法给出 UNION 的模拟方案。
  3. 滥用或误用 CROSS JOIN:在 FROM 后用逗号分隔多张表,但忘记在 WHERE 中写连接条件,导致意外生成笛卡尔积,在数据量大时会造成数据库严重性能问题甚至宕机。
    sql
    1-- 危险操作:忘记 WHERE 条件
    2SELECT * FROM orders, customers; -- 如果 orders 有100万行,customers 有10万行,会尝试生成 1000亿行结果
  4. 混淆 JOIN 和 UNION:JOIN 是横向连接,用于将不同表的列合并在一起;UNION 是纵向连接,用于将多个查询的结果集行合并在一起,要求列的数量和类型兼容。

工程实践与优化

  • 索引是生命线:所有 JOIN 的 ON 子句中用到的列(如 e.dept_id 和 d.id)都必须建立索引。否则,数据库可能采用效率极低的嵌套循环连接(Nested Loop Join),复杂度为 O(N×M)O(N \times M)。有索引时,可利用索引嵌套循环连接(Index Nested-Loop Join),复杂度降至 O(Nlog⁡M)O(N \log M)。
  • 小表驱动大表:在 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。

常见追问与延伸题目

  1. 追问: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。
  2. 追问:如果让你实现一个 ANTI JOIN,你会怎么写?比如,查找所有没有员工的部门。

    • 回答要点:ANTI JOIN 指的是返回左表中在右表中没有匹配的行。有多种实现方式,性能和适用场景各异。
      • LEFT JOIN ... WHERE ... IS NULL:这是最经典、最通用的写法。可读性好,且优化器通常能很好地处理。
        sql
        1SELECT d.*
        2FROM departments d
        3LEFT JOIN employees e ON d.id = e.dept_id
        4WHERE e.id IS NULL;
      • NOT EXISTS:使用子查询,语义清晰,通常性能也很好,尤其是在子查询的表(employees)连接键上有索引时。
        sql
        1SELECT d.*
        2FROM departments d
        3WHERE NOT EXISTS (
        4 SELECT 1 FROM employees e WHERE e.dept_id = d.id
        5);
      • NOT IN:需要特别小心 NULL 值。如果子查询 (SELECT dept_id FROM employees) 的结果中包含 NULL,NOT IN 的结果将永远为空集,因为 X NOT IN (..., NULL, ...) 的逻辑判断会是 UNKNOWN。所以使用 NOT IN 时要确保子查询结果不含 NULL。
        sql
        1SELECT d.*
        2FROM departments d
        3WHERE d.id NOT IN (SELECT dept_id FROM employees WHERE dept_id IS NOT NULL);
  3. 追问:如果 JOIN 的两张表都非常大,比如上亿行,有什么优化思路?

    • 回答要点:这是考察大数据场景下的工程能力。
      • SQL 层面:确保连接键上有合适的索引,并且数据类型完全一致以避免隐式转换。使用 EXPLAIN ANALYZE 分析详细的执行计划和耗时。考虑是否能用 STRAIGHT_JOIN 强制指定连接顺序,防止优化器选错。
      • 业务层面:能否通过增加冗余字段来避免 JOIN?比如在 employees 表中冗余 dept_name,用空间换时间。这需要评估数据一致性的维护成本。
      • 架构层面:
        • 分库分表:如果数据已经分片,JOIN 操作会变成跨节点的分布式 JOIN,非常低效。此时应尽量避免 JOIN,通过两次单表查询在应用层进行数据组装。
        • 数据异构:将需要 JOIN 的数据通过 ETL 任务,提前计算好并存入一个宽表,或者同步到 Elasticsearch、ClickHouse 等更适合复杂查询和分析的系统中。查询时直接查宽表或新系统,避免实时 JOIN。
        • 批处理:如果不是实时性要求高的查询,可以将计算任务放到离线的批处理框架(如 Spark/Flink)中完成,将结果写入结果表供线上查询。