Skip to content

详细介绍一下 MySQL 中的 JOIN 操作,可以适当进行类比


一、JOIN 是什么?——从生活场景理解

假设你有两张纸条:

  • 纸条 A(订单表):记录了每笔订单的编号、客户 ID、金额
  • 纸条 B(客户表):记录了每个客户的 ID、姓名、电话

现在老板问你:"每笔订单是哪个客户下的?金额多少?客户电话是什么?"

你需要做的事情就是:根据「客户 ID」这个共同信息,把两张纸条的内容对照着拼在一起。这个"拼接"动作,就是 SQL 中的 JOIN

纸条 A(orders)                纸条 B(customers)
┌─────────┬─────────┬───────┐   ┌─────────┬────────┬─────────────┐
│order_id │cust_id  │amount │   │cust_id  │name    │phone        │
├─────────┼─────────┼───────┤   ├─────────┼────────┼─────────────┤
│ 1001    │ C01     │ 200   │   │ C01     │ 张三   │ 138xxxx1111 │
│ 1002    │ C02     │ 350   │   │ C02     │ 李四   │ 139xxxx2222 │
│ 1003    │ C01     │ 120   │   │ C03     │ 王五   │ 137xxxx3333 │
│ 1004    │ C99     │  80   │   └─────────┴────────┴─────────────┘
└─────────┴─────────┴───────┘         ↑ C03 没有订单
       ↑ C99 在客户表中不存在

                    JOIN


"把两张纸条按 cust_id 对齐,拼成一张大表"

核心本质:JOIN 就是在两张(或多张)表之间,通过一个关联条件(通常是外键 = 主键),将相关的行"横向拼接"在一起。


二、JOIN 的类型——用集合的思维理解

可以把两张表看成两个圆(集合 A 和集合 B),JOIN 的不同类型就是取这两个圆的不同部分:

         集合 A (左表)          集合 B (右表)
         ┌──────────┐          ┌──────────┐
         │          │          │          │
         │    A     │          │    B     │
         │   only   ├──────────┤   only   │
         │          │  A ∩ B   │          │
         │          │ (交集)   │          │
         └──────────┴──────────┴──────────┘

┌──────────────────────────────────────────────────────────────┐
│  INNER JOIN     →  只取交集部分 (A ∩ B)                       │
│  LEFT  JOIN     →  左表全部 + 交集 (A + A∩B)                  │
│  RIGHT JOIN     →  右表全部 + 交集 (B + A∩B)                  │
│  FULL OUTER JOIN→  两表全部 (A + B) — MySQL 不直接支持         │
│  CROSS JOIN     →  笛卡尔积 (A × B)                           │
│  SELF  JOIN     →  自己和自己连接                              │
└──────────────────────────────────────────────────────────────┘

三、各种 JOIN 详细解析

示例数据准备

以下所有示例基于这两张表:

sql
-- 学生表
CREATE TABLE students (
  id INT PRIMARY KEY,
  name VARCHAR(50),
  class_id INT
);

INSERT INTO students VALUES
  (1, '小明', 101),
  (2, '小红', 102),
  (3, '小刚', 101),
  (4, '小美', NULL),   -- 没有分配班级
  (5, '小李', 103);

-- 班级表
CREATE TABLE classes (
  id INT PRIMARY KEY,
  class_name VARCHAR(50),
  teacher VARCHAR(50)
);

INSERT INTO classes VALUES
  (101, '一班', '张老师'),
  (102, '二班', '李老师'),
  (104, '四班', '赵老师');  -- 没有学生选这个班
students 表                    classes 表
┌────┬──────┬──────────┐      ┌─────┬────────────┬────────┐
│ id │ name │ class_id │      │ id  │ class_name │teacher │
├────┼──────┼──────────┤      ├─────┼────────────┼────────┤
│  1 │ 小明 │ 101      │      │ 101 │ 一班       │张老师  │
│  2 │ 小红 │ 102      │      │ 102 │ 二班       │李老师  │
│  3 │ 小刚 │ 101      │      │ 104 │ 四班       │赵老师  │  ← 没有学生
│  4 │ 小美 │ NULL     │      └─────┴────────────┴────────┘
│  5 │ 小李 │ 103      │  ← 103 不存在
└────┴──────┴──────────┘

3.1 INNER JOIN(内连接)——只要"双方都有的"

类比:相亲大会上,男嘉宾和女嘉宾配对成功的才留下,没匹配上的都淘汰。

规则:只返回左表和右表中都能匹配上的行。

sql
SELECT
  s.name AS student_name,
  c.class_name,
  c.teacher
FROM students s
INNER JOIN classes c ON s.class_id = c.id;

执行过程(逐行扫描):

s.id=1, s.class_id=101  →  classes 中有 id=101  →  ✅ 匹配,拼接输出
s.id=2, s.class_id=102  →  classes 中有 id=102  →  ✅ 匹配,拼接输出
s.id=3, s.class_id=101  →  classes 中有 id=101  →  ✅ 匹配,拼接输出
s.id=4, s.class_id=NULL →  classes 中没有 NULL  →  ❌ 丢弃
s.id=5, s.class_id=103  →  classes 中没有 103   →  ❌ 丢弃

结果

┌──────────────┬────────────┬─────────┐
│ student_name │ class_name │ teacher │
├──────────────┼────────────┼─────────┤
│ 小明         │ 一班       │ 张老师  │
│ 小红         │ 二班       │ 李老师  │
│ 小刚         │ 一班       │ 张老师  │
└──────────────┴────────────┴─────────┘
只有 3 行:小美(NULL) 和 小李(103) 因为没有匹配的班级被过滤掉了。

关键特征:INNER JOIN 结果行数 ≤ min(左表行数, 右表行数)。任何一边没有匹配就不会出现。


3.2 LEFT JOIN(左外连接)——"左边一个不能少"

类比:班级点名。不管你有没有来(右表有没有匹配),花名册上的人(左表)一个都不能少。没来的在签到栏写"缺席"(NULL)。

规则:返回左表的所有行,右表没有匹配的部分用 NULL 填充。

sql
SELECT
  s.name AS student_name,
  c.class_name,
  c.teacher
FROM students s
LEFT JOIN classes c ON s.class_id = c.id;

结果

┌──────────────┬────────────┬─────────┐
│ student_name │ class_name │ teacher │
├──────────────┼────────────┼─────────┤
│ 小明         │ 一班       │ 张老师  │
│ 小红         │ 二班       │ 李老师  │
│ 小刚         │ 一班       │ 张老师  │
│ 小美         │ NULL       │ NULL    │  ← 左表保留,右表填 NULL
│ 小李         │ NULL       │ NULL    │  ← 左表保留,右表填 NULL
└──────────────┴────────────┴─────────┘
5 行全部返回,左表一个不少。

常见用法:找出"没有匹配的"

sql
-- 找出没有分配到有效班级的学生
SELECT s.name
FROM students s
LEFT JOIN classes c ON s.class_id = c.id
WHERE c.id IS NULL;

-- 结果:小美、小李

这个模式非常实用,相当于求 A - A∩B(左表独有的部分)。


3.3 RIGHT JOIN(右外连接)——"右边一个不能少"

类比:和 LEFT JOIN 完全对称。这次是"教室点名"——不管教室里有没有人(左表有没有匹配),每间教室(右表)都要登记。

规则:返回右表的所有行,左表没有匹配的部分用 NULL 填充。

sql
SELECT
  s.name AS student_name,
  c.class_name,
  c.teacher
FROM students s
RIGHT JOIN classes c ON s.class_id = c.id;

结果

┌──────────────┬────────────┬─────────┐
│ student_name │ class_name │ teacher │
├──────────────┼────────────┼─────────┤
│ 小明         │ 一班       │ 张老师  │
│ 小刚         │ 一班       │ 张老师  │
│ 小红         │ 二班       │ 李老师  │
│ NULL         │ 四班       │ 赵老师  │  ← 右表保留,左表填 NULL
└──────────────┴────────────┴─────────┘
四班没有学生,但依然出现在结果中。

实际建议:RIGHT JOIN 可以通过交换表位置改写成 LEFT JOIN。为了代码一致性,建议统一使用 LEFT JOIN

上面的 RIGHT JOIN 等价于:

sql
SELECT
  s.name AS student_name,
  c.class_name,
  c.teacher
FROM classes c
LEFT JOIN students s ON s.class_id = c.id;

3.4 FULL OUTER JOIN(全外连接)——"两边全都要"

类比:同学聚会,既要统计所有参加的人,也要统计所有被邀请但没来的人。反正所有相关的人都列出来。

规则:返回左表和右表的所有行,没有匹配的都用 NULL 填充。

⚠️ MySQL 不直接支持 FULL OUTER JOIN,需要用 UNION 模拟:

sql
-- 模拟 FULL OUTER JOIN
SELECT s.name, c.class_name, c.teacher
FROM students s
LEFT JOIN classes c ON s.class_id = c.id

UNION

SELECT s.name, c.class_name, c.teacher
FROM students s
RIGHT JOIN classes c ON s.class_id = c.id;

结果

┌──────┬────────────┬─────────┐
│ name │ class_name │ teacher │
├──────┼────────────┼─────────┤
│ 小明 │ 一班       │ 张老师  │
│ 小红 │ 二班       │ 李老师  │
│ 小刚 │ 一班       │ 张老师  │
│ 小美 │ NULL       │ NULL    │  ← 左表独有
│ 小李 │ NULL       │ NULL    │  ← 左表独有
│ NULL │ 四班       │ 赵老师  │  ← 右表独有
└──────┴────────────┴─────────┘

3.5 CROSS JOIN(交叉连接 / 笛卡尔积)——"所有组合"

类比:你有 3 件上衣和 4 条裤子,CROSS JOIN 就是列出所有可能的穿搭组合 = 3 × 4 = 12 种。

规则:左表的每一行 × 右表的每一行,没有 ON 条件。

sql
SELECT s.name, c.class_name
FROM students s
CROSS JOIN classes c;

-- 5 个学生 × 3 个班级 = 15 行

结果(部分)

┌──────┬────────────┐
│ name │ class_name │
├──────┼────────────┤
│ 小明 │ 一班       │
│ 小明 │ 二班       │
│ 小明 │ 四班       │
│ 小红 │ 一班       │
│ 小红 │ 二班       │
│ 小红 │ 四班       │
│ ...  │ ...        │  (共 15 行)
└──────┴────────────┘

应用场景

sql
-- 生成日历 × 商品的组合(用于统计每天每个商品是否有销售)
SELECT d.date, p.product_name
FROM dates d
CROSS JOIN products p;

-- 生成所有可能的对战组合
SELECT a.team_name AS home, b.team_name AS away
FROM teams a
CROSS JOIN teams b
WHERE a.id != b.id;

⚠️ 注意:CROSS JOIN 产生的行数 = 左表行数 × 右表行数。两张大表做 CROSS JOIN 会产生天文数字的行数,非常危险。


3.6 SELF JOIN(自连接)——"自己跟自己比"

类比:一个公司的组织架构图——每个人既是"员工",也可能是别人的"上级"。同一张表,要用两个身份来关联。

规则:同一张表使用不同的别名,然后 JOIN 自身。

sql
CREATE TABLE employees (
  id INT PRIMARY KEY,
  name VARCHAR(50),
  manager_id INT,
  salary DECIMAL(10, 2)
);

INSERT INTO employees VALUES
  (1, '总经理',   NULL, 100000),
  (2, '技术总监', 1,    80000),
  (3, '销售总监', 1,    75000),
  (4, '高级开发', 2,    60000),
  (5, '初级开发', 2,    40000);
sql
-- 查询每个员工的上级是谁
SELECT
  e.name  AS employee,
  m.name  AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;
┌──────────┬──────────┐
│ employee │ manager  │
├──────────┼──────────┤
│ 总经理   │ NULL     │  ← 没有上级
│ 技术总监 │ 总经理   │
│ 销售总监 │ 总经理   │
│ 高级开发 │ 技术总监 │
│ 初级开发 │ 技术总监 │
└──────────┴──────────┘
sql
-- 找出工资高于上级的员工
SELECT
  e.name AS employee,
  e.salary AS emp_salary,
  m.name AS manager,
  m.salary AS mgr_salary
FROM employees e
INNER JOIN employees m ON e.manager_id = m.id
WHERE e.salary > m.salary;

四、JOIN 的底层执行原理

理解 JOIN 怎么执行的,有助于写出高性能的 SQL。

4.1 三种执行算法

┌────────────────────────────────────────────────────────────────┐
│                   MySQL JOIN 执行算法                           │
├─────────────────┬──────────────────────────────────────────────┤
│                 │                                              │
│  Simple Nested  │  最朴素的方式:                               │
│  Loop Join      │  遍历左表每一行,对每行都全表扫描右表         │
│  (简单嵌套循环)  │  时间复杂度:O(M × N)                       │
│                 │  类比:在电话簿里一页一页翻找每个人             │
│                 │                                              │
├─────────────────┼──────────────────────────────────────────────┤
│                 │                                              │
│  Index Nested   │  如果右表关联列有索引:                       │
│  Loop Join      │  遍历左表每一行,通过索引快速查找右表         │
│  (索引嵌套循环)  │  时间复杂度:O(M × logN)                    │
│                 │  类比:在电话簿里直接查目录找到人               │
│                 │                                              │
├─────────────────┼──────────────────────────────────────────────┤
│                 │                                              │
│  Block Nested   │  将左表数据分批加载到内存(Join Buffer):    │
│  Loop Join      │  一次比对一批而不是一行                       │
│  (块嵌套循环)    │  减少右表的扫描次数                           │
│                 │  类比:把一摞简历摊开,一次对照多份             │
│                 │                                              │
├─────────────────┼──────────────────────────────────────────────┤
│                 │                                              │
│  Hash Join      │  MySQL 8.0.18+ 新增:                        │
│  (哈希连接)      │  将小表构建 Hash 表,大表逐行探测匹配        │
│                 │  时间复杂度:O(M + N)                         │
│                 │  类比:先把一张表做成字典,另一张表查字典       │
│                 │                                              │
└─────────────────┴──────────────────────────────────────────────┘

4.2 驱动表与被驱动表

执行 A JOIN B ON A.key = B.key 时:

  驱动表(外层循环)     被驱动表(内层循环)
  ┌──────────┐          ┌──────────┐
  │  Table A │──遍历──→ │  Table B │
  │  (小表)  │  每一行  │  (大表)  │  ← 被驱动表应该有索引
  └──────────┘          └──────────┘

  优化器通常选小表作为驱动表(减少外层循环次数)
  被驱动表的关联列需要有索引(加速内层查找)

口诀:小表驱动大表,大表建索引。


五、JOIN 的执行顺序与 ON vs WHERE

5.1 执行顺序

  FROM / JOIN       ← 第 1 步:确定表和连接方式

  ON 条件过滤       ← 第 2 步:按 ON 条件匹配行

  添加外表行         ← 第 3 步:LEFT/RIGHT JOIN 会把未匹配的行加回来(填 NULL)

  WHERE 过滤        ← 第 4 步:对 JOIN 结果进一步过滤

  GROUP BY → HAVING → SELECT → ORDER BY → LIMIT

5.2 ON 和 WHERE 的区别(非常重要!)

对于 INNER JOIN,ON 和 WHERE 效果相同。但对于 LEFT JOIN,区别巨大:

sql
-- 示例:查找一班的学生

-- 写法 1:条件放在 ON 里
SELECT s.name, c.class_name
FROM students s
LEFT JOIN classes c ON s.class_id = c.id AND c.id = 101;

-- 结果:
-- ┌──────┬────────────┐
-- │ name │ class_name │
-- ├──────┼────────────┤
-- │ 小明 │ 一班       │  ← ON 匹配成功
-- │ 小红 │ NULL       │  ← ON 匹配失败,但 LEFT JOIN 保留左表行
-- │ 小刚 │ 一班       │  ← ON 匹配成功
-- │ 小美 │ NULL       │  ← 保留
-- │ 小李 │ NULL       │  ← 保留
-- └──────┴────────────┘
-- 返回 5 行(所有学生都在)

-- 写法 2:条件放在 WHERE 里
SELECT s.name, c.class_name
FROM students s
LEFT JOIN classes c ON s.class_id = c.id
WHERE c.id = 101;

-- 结果:
-- ┌──────┬────────────┐
-- │ name │ class_name │
-- ├──────┼────────────┤
-- │ 小明 │ 一班       │
-- │ 小刚 │ 一班       │
-- └──────┴────────────┘
-- 返回 2 行(WHERE 在 JOIN 之后执行,把不符合的都过滤了)
总结:

  ON 条件:决定 JOIN 时如何匹配,不影响 LEFT JOIN 保留左表全部行
  WHERE 条件:在 JOIN 结果上再次过滤,会去掉不满足条件的行

  INNER JOIN → ON / WHERE 效果一样
  LEFT JOIN  → ON 不会减少左表行数,WHERE 会

六、多表 JOIN——链式连接

实际业务中经常需要连接三张甚至更多的表。

sql
-- 三表连接:查询订单详情(订单 → 客户 → 地址)
SELECT
  o.order_id,
  c.name AS customer_name,
  a.city,
  o.amount
FROM orders o
INNER JOIN customers c ON o.customer_id = c.id
INNER JOIN addresses a ON c.address_id = a.id
WHERE o.amount > 100;

执行流程

  Step 1:  orders  JOIN  customers    →  产生临时结果集 T1
  Step 2:  T1      JOIN  addresses    →  产生临时结果集 T2
  Step 3:  对 T2 执行 WHERE 过滤      →  最终结果

注意事项

  • 每多一次 JOIN,数据量可能会膨胀(一对多关系时)
  • 建议 JOIN 的表不超过 3~5 张,超过的考虑拆分查询或使用临时表
  • 每个 JOIN 的关联列都应该有索引

七、JOIN 性能优化

7.1 优化清单

┌─────┬──────────────────────────────────────────────────────┐
│  1  │ 被驱动表的 JOIN 列必须建索引                          │
│     │ ALTER TABLE orders ADD INDEX idx_cust_id(cust_id);   │
├─────┼──────────────────────────────────────────────────────┤
│  2  │ 小表驱动大表(优化器通常自动选择)                     │
│     │ 可通过 STRAIGHT_JOIN 强制指定驱动表顺序                │
├─────┼──────────────────────────────────────────────────────┤
│  3  │ 避免 SELECT *,只查需要的列                           │
│     │ 减少数据传输量和 Join Buffer 的内存占用                │
├─────┼──────────────────────────────────────────────────────┤
│  4  │ 增大 join_buffer_size                                │
│     │ 对于无法使用索引的 JOIN,更大的 Buffer 可减少扫描次数  │
├─────┼──────────────────────────────────────────────────────┤
│  5  │ 关联列类型和字符集必须一致                             │
│     │ 类型不一致会导致隐式转换,索引失效                     │
├─────┼──────────────────────────────────────────────────────┤
│  6  │ 用 EXPLAIN 检查执行计划                               │
│     │ 关注 type 列:eq_ref > ref > ALL                     │
│     │ 出现 ALL 说明是全表扫描,需要优化                      │
├─────┼──────────────────────────────────────────────────────┤
│  7  │ 避免在 ON 条件中使用函数                              │
│     │ ON YEAR(a.date) = b.year  → 索引失效                 │
│     │ 改为 ON a.date BETWEEN '2026-01-01' AND '2026-12-31'│
└─────┴──────────────────────────────────────────────────────┘

7.2 用 EXPLAIN 分析 JOIN

sql
EXPLAIN SELECT s.name, c.class_name
FROM students s
INNER JOIN classes c ON s.class_id = c.id;
┌────┬─────────┬───────┬────────┬──────────────┬──────┬─────────┐
│ id │ table   │ type  │ key    │ key_len      │ rows │ Extra   │
├────┼─────────┼───────┼────────┼──────────────┼──────┼─────────┤
│  1 │ classes │ ALL   │ NULL   │ NULL         │    3 │ NULL    │
│  1 │ students│ ref   │ idx_cls│ 5            │    2 │ NULL    │
└────┴─────────┴───────┴────────┴──────────────┴──────┴─────────┘

关注点:
- type=ALL → 全表扫描(classes 表小,优化器选它做驱动表)
- type=ref → 通过索引查找(students 表通过 class_id 索引定位)
- rows → 预估扫描行数

八、JOIN 的七种经典用法(维恩图总结)

  1. INNER JOIN              2. LEFT JOIN              3. RIGHT JOIN
     A ∩ B                      A                         B
     ╭──╮  ╭──╮                ╭──╮  ╭──╮                ╭──╮  ╭──╮
     │  ├██┤  │                │██├██┤  │                │  ├██┤██│
     ╰──╯  ╰──╯                ╰──╯  ╰──╯                ╰──╯  ╰──╯

  4. LEFT EXCLUSIVE          5. RIGHT EXCLUSIVE        6. FULL OUTER JOIN
     A - B                      B - A                     A ∪ B
     ╭──╮  ╭──╮                ╭──╮  ╭──╮                ╭──╮  ╭──╮
     │██├──┤  │                │  ├──┤██│                │██├██┤██│
     ╰──╯  ╰──╯                ╰──╯  ╰──╯                ╰──╯  ╰──╯

  7. FULL EXCLUSIVE (A - B) ∪ (B - A)
     ╭──╮  ╭──╮
     │██├──┤██│
     ╰──╯  ╰──╯

对应 SQL:

sql
-- 1. INNER JOIN: A ∩ B
SELECT * FROM A INNER JOIN B ON A.key = B.key;

-- 2. LEFT JOIN: A(包含交集)
SELECT * FROM A LEFT JOIN B ON A.key = B.key;

-- 3. RIGHT JOIN: B(包含交集)
SELECT * FROM A RIGHT JOIN B ON A.key = B.key;

-- 4. LEFT EXCLUSIVE: A - B(A 独有的)
SELECT * FROM A LEFT JOIN B ON A.key = B.key WHERE B.key IS NULL;

-- 5. RIGHT EXCLUSIVE: B - A(B 独有的)
SELECT * FROM A RIGHT JOIN B ON A.key = B.key WHERE A.key IS NULL;

-- 6. FULL OUTER JOIN: A ∪ B
SELECT * FROM A LEFT JOIN B ON A.key = B.key
UNION
SELECT * FROM A RIGHT JOIN B ON A.key = B.key;

-- 7. FULL EXCLUSIVE: (A - B) ∪ (B - A)
SELECT * FROM A LEFT JOIN B ON A.key = B.key WHERE B.key IS NULL
UNION
SELECT * FROM A RIGHT JOIN B ON A.key = B.key WHERE A.key IS NULL;

九、常见面试题与陷阱

Q1: INNER JOIN 和 LEFT JOIN 的区别?

对比项INNER JOINLEFT JOIN
匹配行为只返回两表都匹配的行返回左表全部行,右表不匹配填 NULL
结果行数≤ min(左表, 右表)= 左表行数(一对一时)
NULL 值不会产生因 JOIN 而来的 NULL右表列可能为 NULL
使用场景只需要有关联的数据需要保留左表全部记录

Q2: ON 和 WHERE 的区别?

  • INNER JOIN 中:ON 和 WHERE 效果一样,因为不匹配的行最终都会被丢弃。
  • LEFT JOIN 中:ON 中的条件不会过滤掉左表的行(不匹配的行以 NULL 形式保留);WHERE 中的条件会在 JOIN 之后过滤,可能会移除左表的行。
  • 记忆口诀:ON 管"怎么连",WHERE 管"连完后留谁"。

Q3: 如何避免笛卡尔积?

sql
-- ❌ 忘写 ON 条件 → 笛卡尔积
SELECT * FROM students, classes;                    -- 5 × 3 = 15 行
SELECT * FROM students CROSS JOIN classes;          -- 同上

-- ❌ ON 条件逻辑错误
SELECT * FROM students s
JOIN classes c ON s.id = c.id;                      -- 语义上不对

-- ✅ 正确写法
SELECT * FROM students s
JOIN classes c ON s.class_id = c.id;

-- 排查方法:结果行数远超预期时,检查 ON 条件是否正确

Q4: JOIN 和子查询(Subquery)怎么选?

sql
-- 场景:查找有学生的班级

-- 方式 1:JOIN(推荐,优化器容易处理)
SELECT DISTINCT c.class_name
FROM classes c
INNER JOIN students s ON c.id = s.class_id;

-- 方式 2:EXISTS(适合大表判断存在性)
SELECT c.class_name FROM classes c
WHERE EXISTS (SELECT 1 FROM students s WHERE s.class_id = c.id);

-- 方式 3:IN(适合子查询结果集小的场景)
SELECT c.class_name FROM classes c
WHERE c.id IN (SELECT class_id FROM students WHERE class_id IS NOT NULL);

选择原则

  • 能用 JOIN 就用 JOIN(性能好、可读性强)
  • 判断"是否存在"用 EXISTS
  • 子查询结果集小用 IN
  • 需要复用中间结果用 CTE (WITH ... AS)

Q5: 一对多 JOIN 结果行数会膨胀吗?

会的! 这是一个常见的坑。

sql
-- 假设一个作者写了 5 本书
-- authors 表 1 行 × books 表 5 行 = JOIN 结果 5 行

-- ❌ 直接 COUNT(*) 会得到错误的作者数
SELECT COUNT(*) FROM authors a JOIN books b ON a.id = b.author_id;
-- 结果不是作者数,而是作者-书籍配对数

-- ✅ 正确做法
SELECT COUNT(DISTINCT a.id) FROM authors a JOIN books b ON a.id = b.author_id;

十、JOIN 实战速查表

┌──────────────────────────────────────────────────────────────────┐
│                     JOIN 速查表                                   │
├──────────────────┬───────────────────────────────────────────────┤
│                  │                                               │
│  INNER JOIN      │  SELECT * FROM A JOIN B ON A.k = B.k         │
│                  │  只返回匹配行                                  │
│                  │                                               │
│  LEFT JOIN       │  SELECT * FROM A LEFT JOIN B ON A.k = B.k    │
│                  │  左表全部,右表不匹配填 NULL                    │
│                  │                                               │
│  LEFT EXCLUSIVE  │  ... LEFT JOIN ... WHERE B.k IS NULL         │
│                  │  只返回左表独有的行                             │
│                  │                                               │
│  CROSS JOIN      │  SELECT * FROM A CROSS JOIN B                │
│                  │  笛卡尔积 (M × N 行)                          │
│                  │                                               │
│  SELF JOIN       │  SELECT * FROM A a1 JOIN A a2 ON a1.x = a2.y │
│                  │  自己和自己连接                                 │
│                  │                                               │
├──────────────────┼───────────────────────────────────────────────┤
│                  │                                               │
│  性能优化         │  1. 被驱动表 JOIN 列建索引                     │
│                  │  2. 小表驱动大表                               │
│                  │  3. 避免 SELECT *                              │
│                  │  4. EXPLAIN 检查执行计划                       │
│                  │  5. 关联列类型必须一致                          │
│                  │                                               │
├──────────────────┼───────────────────────────────────────────────┤
│                  │                                               │
│  易错点           │  1. 忘写 ON → 笛卡尔积                        │
│                  │  2. LEFT JOIN + WHERE 过滤右表 → 变成 INNER    │
│                  │  3. 一对多 JOIN 行数膨胀                        │
│                  │  4. ON 中用函数 → 索引失效                     │
│                  │                                               │
└──────────────────┴───────────────────────────────────────────────┘

📝 学习建议:先彻底理解 INNER JOIN 和 LEFT JOIN(覆盖 90% 的场景),再掌握 SELF JOIN 和 CROSS JOIN。遇到复杂 JOIN 时,先画出表之间的关系图,明确"谁是左表、谁是右表、关联列是什么、需要保留哪边的全部数据",SQL 就容易写了。