很多人会写 JOIN 查询,但不清楚数据库底层是怎么执行的。这篇文章通过模拟执行过程,来加深理解 JOIN 的真实运行逻辑。

老演员电影数据库的例子

我们有电影数据库的例子,有两张表:

movies 表(4 行):

idtitleyear
1Inception2010
2The Dark Knight2008
3Interstellar2014
4Tenet2016

actors 表(6 行):

idnamemovie_id
1Leonardo DiCaprio1
2Christian Bale2
3Matthew McConaughey3
4Anne Hathaway3
5Anne Hathaway1
6Tom HardyNULL

Note

  • Tenet (id=4) 没有演员记录
  • Tom Hardy (id=6) 的 movie_id 是 NULL

INNER JOIN 执行过程

需求:查询所有有演员的电影及其演员名。

SELECT m.title, a.name
FROM movies m
INNER JOIN actors a ON m.id = a.movie_id;

执行步骤

第 1 步:扫描驱动表 movies

开始处理 movies 表(4 行)
驱动表:movies
被驱动表:actors

第 2 步:逐行匹配 actors 表

迭代 1:处理 movies.id = 1 (Inception)

当前行:{ id: 1, title: 'Inception', year: 2010 }
JOIN 条件:找 actors.movie_id = 1

扫描 actors 表:
┌────┬────────────────────┬──────────┬─────────┐
│ id │ name               │ movie_id │ 匹配?  │
├────┼────────────────────┼──────────┼─────────┤
│ 1  │ Leonardo DiCaprio  │ 1        │ ✅ 匹配 │
│ 2  │ Christian Bale     │ 2        │ ❌      │
│ 3  │ Matthew McConaughey│ 3        │ ❌      │
│ 4  │ Anne Hathaway      │ 3        │ ❌      │
│ 5  │ Anne Hathaway      │ 1        │ ✅ 匹配 │
│ 6  │ Tom Hardy          │ NULL     │ ❌      │
└────┴────────────────────┴──────────┴─────────┘

生成结果:
→ (Inception, Leonardo DiCaprio)
→ (Inception, Anne Hathaway)

迭代 2:处理 movies.id = 2 (The Dark Knight)

当前行:{ id: 2, title: 'The Dark Knight', year: 2008 }
JOIN 条件:找 actors.movie_id = 2

扫描 actors 表:
匹配到 1 行:Christian Bale (movie_id=2)

生成结果:
→ (The Dark Knight, Christian Bale)

迭代 4:处理 movies.id = 4 (Tenet)

当前行:{ id: 4, title: 'Tenet', year: 2016 }
JOIN 条件:找 actors.movie_id = 4

扫描 actors 表:
没有任何 movie_id = 4 的记录 ❌

⚠️ INNER JOIN 规则:没有匹配就丢弃
不生成任何结果行

最终结果

┌────────────────┬────────────────────┐
│ title          │ name               │
├────────────────┼────────────────────┤
│ Inception      │ Leonardo DiCaprio  │
│ Inception      │ Anne Hathaway      │
│ The Dark Knight│ Christian Bale     │
│ Interstellar   │ Matthew McConaughey│
│ Interstellar   │ Anne Hathaway      │
└────────────────┴────────────────────┘

共 5 行

关键点

  • Tenet 没有演员 → 被过滤掉
  • Tom Hardy 的 movie_id 是 NULL → 无法匹配任何电影

LEFT JOIN 执行过程

需求:查询所有电影,显示演员(如果有的话)。

SELECT m.title, a.name
FROM movies m
LEFT JOIN actors a ON m.id = a.movie_id;

与 INNER JOIN 的差异

前 3 次迭代完全相同(Inception、The Dark Knight、Interstellar),区别在第 4 次:

迭代 4:处理 movies.id = 4 (Tenet)

当前行:{ id: 4, title: 'Tenet', year: 2016 }
JOIN 条件:找 actors.movie_id = 4

扫描 actors 表:
没有任何 movie_id = 4 的记录 ❌

✅ LEFT JOIN 规则:左表必须保留,右表用 NULL 填充
生成结果:
→ (Tenet, NULL)  ← 这一行被保留了!

最终结果

┌────────────────┬────────────────────┐
│ title          │ name               │
├────────────────┼────────────────────┤
│ Inception      │ Leonardo DiCaprio  │
│ Inception      │ Anne Hathaway      │
│ The Dark Knight│ Christian Bale     │
│ Interstellar   │ Matthew McConaughey│
│ Interstellar   │ Anne Hathaway      │
│ Tenet          │ NULL               │ ← 多了这一行
└────────────────┴────────────────────┘

共 6 行

关键点

  • 左表(movies)的所有行都会出现在结果中
  • 没有匹配的行,右表列用 NULL 填充
  • Tom Hardy 仍然不会出现(他不在左表 movies 里)

RIGHT JOIN 执行过程

需求:查询所有演员,显示电影(如果有的话)。

SELECT m.title, a.name
FROM movies m
RIGHT JOIN actors a ON m.id = a.movie_id;

执行逻辑:反过来了

RIGHT JOIN 的执行是 以右表为驱动

第 1 步:扫描驱动表 actors(右表)

驱动表:actors (6 行)
被驱动表:movies

第 2 步:逐行匹配 movies 表

迭代 1:处理 actors.movie_id = 1 (Leonardo DiCaprio)

当前行:{ id: 1, name: 'Leonardo DiCaprio', movie_id: 1 }
JOIN 条件:找 movies.id = 1

在 movies 表找到:Inception ✅

生成结果:
→ (Inception, Leonardo DiCaprio)

迭代 2:处理 actors.movie_id = 2 (Christian Bale)

当前行:{ id: 2, name: 'Christian Bale', movie_id: 2 }
JOIN 条件:找 movies.id = 2

在 movies 表找到:The Dark Knight ✅

生成结果:
→ (The Dark Knight, Christian Bale)

迭代 6:处理 actors.movie_id = NULL (Tom Hardy)

当前行:{ id: 6, name: 'Tom Hardy', movie_id: NULL }
JOIN 条件:找 movies.id = NULL

NULL 无法匹配任何值 ❌

✅ RIGHT JOIN 规则:右表必须保留,左表用 NULL 填充
生成结果:
→ (NULL, Tom Hardy)  ← Tom Hardy 出现了!

最终结果

┌────────────────┬────────────────────┐
│ title          │ name               │
├────────────────┼────────────────────┤
│ Inception      │ Leonardo DiCaprio  │
│ The Dark Knight│ Christian Bale     │
│ Interstellar   │ Matthew McConaughey│
│ Interstellar   │ Anne Hathaway      │
│ Inception      │ Anne Hathaway      │
│ NULL           │ Tom Hardy          │ ← 多了这一行
└────────────────┴────────────────────┘

共 6 行

关键点

  • 右表(actors)的所有行都会出现在结果中
  • Tom Hardy 的 movie_id 是 NULL → 左表用 NULL 填充
  • Tenet 不会出现(它不在右表 actors 里)

总结

JOIN 的本质

JOIN 的执行过程本质是 匹配 + 组合

  1. 选择驱动表(通常是小表)
  2. 逐行扫描驱动表
  3. 对每一行,在被驱动表中查找匹配
  4. 根据 JOIN 类型决定如何处理不匹配的行
    • INNER JOIN:丢弃不匹配的行
    • LEFT JOIN:保留左表,右表用 NULL 填充
    • RIGHT JOIN:保留右表,左表用 NULL 填充
  5. 生成最终结果集

不同 JOIN 的执行差异

JOIN 类型驱动表不匹配时的处理结果行数
INNER JOIN小表优先丢弃只有匹配的行
LEFT JOIN左表保留左表,右表 NULL≥ 左表行数
RIGHT JOIN右表保留右表,左表 NULL≥ 右表行数

NOTE:

  1. 驱动表很重要:选错驱动表,性能相差 10 倍以上
  2. 索引是关键:JOIN 列必须有索引,否则会全表扫描

Reference

JOIN 的执行算法和性能优化?继续阅读: 👉 SQL JOIN 算法与性能优化