很多人会写 JOIN 查询,但不清楚数据库底层是怎么执行的。这篇文章通过模拟执行过程,来加深理解 JOIN 的真实运行逻辑。
老演员电影数据库的例子
我们有电影数据库的例子,有两张表:
movies 表(4 行):
| id | title | year |
|---|---|---|
| 1 | Inception | 2010 |
| 2 | The Dark Knight | 2008 |
| 3 | Interstellar | 2014 |
| 4 | Tenet | 2016 |
actors 表(6 行):
| 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 |
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 的执行过程本质是 匹配 + 组合:
- 选择驱动表(通常是小表)
- 逐行扫描驱动表
- 对每一行,在被驱动表中查找匹配
- 根据 JOIN 类型决定如何处理不匹配的行
- INNER JOIN:丢弃不匹配的行
- LEFT JOIN:保留左表,右表用 NULL 填充
- RIGHT JOIN:保留右表,左表用 NULL 填充
- 生成最终结果集
不同 JOIN 的执行差异
| JOIN 类型 | 驱动表 | 不匹配时的处理 | 结果行数 |
|---|---|---|---|
| INNER JOIN | 小表优先 | 丢弃 | 只有匹配的行 |
| LEFT JOIN | 左表 | 保留左表,右表 NULL | ≥ 左表行数 |
| RIGHT JOIN | 右表 | 保留右表,左表 NULL | ≥ 右表行数 |
NOTE:
- 驱动表很重要:选错驱动表,性能相差 10 倍以上
- 索引是关键:JOIN 列必须有索引,否则会全表扫描
Reference
JOIN 的执行算法和性能优化?继续阅读: 👉 SQL JOIN 算法与性能优化