目录
[1. Simple Nested-Loop Join (SNLJ) - 简单嵌套循环连接](#1. Simple Nested-Loop Join (SNLJ) - 简单嵌套循环连接)
[2. Index Nested-Loop Join (INLJ) - 索引嵌套循环连接](#2. Index Nested-Loop Join (INLJ) - 索引嵌套循环连接)
[3. Block Nested-Loop Join (BNLJ) - 块嵌套循环连接 (Buffer)](#3. Block Nested-Loop Join (BNLJ) - 块嵌套循环连接 (Buffer))
[4. Hash Join - 哈希连接 (MySQL 8.0 杀手锏)](#4. Hash Join - 哈希连接 (MySQL 8.0 杀手锏))
[三、 多表 JOIN 为什么容易导致性能雪崩?](#三、 多表 JOIN 为什么容易导致性能雪崩?)
前言
大家好,这里是程序员阿亮,相信大家肯定使用过SQL中的Join语法
那么这个Join的底层原理、算法、性能又是怎么样的呢,我来给大家解释一波!
一、Join本质
在关系型数据库的世界里,JOIN(连接)是我们每天都会打交道的老朋友。无论是简单的报表查询,还是复杂的业务逻辑处理,都离不开它。但你是否遇到过这样的场景:两张表 JOIN 飞快,三张表变慢,五张表直接让数据库 CPU 飙升?
在深入算法之前,我们必须明确一个核心概念:JOIN 的本质是基于两张表的笛卡尔积进行数据过滤。
假设有表 A(100行)和表 B(1000行)。如果在没有任何条件的情况下把它们连接起来,数据库会生成一个包含 100 * 1000 = 100,000 行的临时大表,这就是笛卡尔积。显然,这种无脑组合绝大多数都是无意义的数据。
因此,JOIN 的过程实际上就是:
-
确定驱动表 (Driving Table)和被驱动表(Driven Table)。
-
从驱动表中取出一条记录。
-
根据连接条件(
ON子句)到被驱动表中寻找匹配的记录。 -
将匹配的记录合并后返回或放入结果集中。
-
重复上述步骤,直到驱动表遍历完毕。
为了让这个过程更高效,MySQL 的优化器和执行引擎在背后付出了巨大的努力,演进出了多种 JOIN 算法。
二、Join算法
MySQL 处理 JOIN 的主要算法经历了从简单的嵌套循环到利用内存和哈希表的演进。理解它们,是进行 SQL 优化的前提。
1. Simple Nested-Loop Join (SNLJ) - 简单嵌套循环连接
这是最原始、最暴力的连表方式。它的逻辑就和我们在代码里写两个嵌套的 for 循环一模一样。
执行逻辑: 从驱动表 A 中取出每一行,然后去被驱动表 B 中进行全表扫描匹配。
性能痛点: 如果驱动表有 1,000 行,被驱动表有 10,000 行,那么被驱动表就要被全表扫描 1,000 次!在磁盘 I/O 层面,这是极其恐怖的开销。因此,MySQL 实际上绝对不会在生产环境中使用如此简陋的算法。
2. Index Nested-Loop Join (INLJ) - 索引嵌套循环连接
为了拯救 SNLJ 糟糕的性能,索引派上了用场。这是日常开发中最常见,也是我们最期望数据库走的一种 JOIN 算法。
执行逻辑:
依然是从驱动表 A 取出一条数据。
但这次去被驱动表 B 寻找匹配行时,直接利用表 B 的索引进行查找,而不再是全表扫描。
性能飞跃: 通过 B+ 树索引,原本被驱动表 O(N) 的扫描复杂度瞬间降到了树的高度(通常为 O(logN))。 优化核心: 永远保证被驱动表的 JOIN 字段上有合适的索引!
3. Block Nested-Loop Join (BNLJ) - 块嵌套循环连接 (Buffer)
如果被驱动表的连接字段没有索引 怎么办?MySQL 也不傻,它不会退化去使用 SNLJ,而是引入了 Join Buffer(连接缓冲区)来优化,这就是 BNLJ。
执行逻辑:
MySQL 会在内存中开辟一块名为
Join Buffer的空间。它会将驱动表 A 的数据批量读取到 Join Buffer 中。
然后扫描一次被驱动表 B,将 B 中的每一行与 Join Buffer 中的所有 A 表记录进行批量匹配。
为什么这样能变快? 对比 SNLJ 中驱动表每读取一行,被驱动表就要被扫描一次;BNLJ 是驱动表的一批数据读入内存后,被驱动表扫描一次,就可以和内存中的这一批数据全部对比完毕。这极大地减少了被驱动表的磁盘扫描次数 。(注意:Join Buffer 大小由参数 join_buffer_size 控制,默认 256KB)。
4. Hash Join - 哈希连接 (MySQL 8.0 杀手锏)
虽然 BNLJ 缓解了无索引 JOIN 的磁盘 I/O 问题,但在内存中做嵌套比对的 CPU 开销依然很大。于是,千呼万唤始出来,MySQL 8.0.18 正式引入了 Hash Join,并在 8.0.20 版本中彻底废弃了 BNLJ。
执行逻辑(分为两阶段):
Build(构建阶段): 遍历驱动表,以 JOIN 的条件字段为键,将其记录放入内存中构建出一个哈希表(Hash Table)。
Probe(探测阶段): 全表扫描被驱动表,取出每一行数据,计算其连接字段的哈希值,去内存中的哈希表中以 O(1) 的时间复杂度快速查找匹配项。
性能革命: Hash Join 特别适合大表之间且没有索引的等值连接。它的出现,让 MySQL 在应对复杂数据分析查询(OLAP)时终于有了底气。如果内存中的哈希表放不下,MySQL 会将数据分块落盘处理(Grace Hash Join),依然比 BNLJ 快得多。
三、 多表 JOIN 为什么容易导致性能雪崩?
理解了上面的算法,我们再来看为什么老司机总是警告我们:"线上查询不要 JOIN 超过 3 张以上的表"。多表 JOIN 性能下降的核心原因有以下三点:
-
组合爆炸(优化器解析成本骤增) 当进行多表 JOIN 时,MySQL 的查询优化器需要决定表的连接顺序(谁是第一张驱动表,谁是第二张......)。如果是 N 张表 JOIN,可能的连接顺序有 N! (N的阶乘)种。表越多,优化器评估最佳执行计划的时间就呈指数级上升,甚至可能因为超时而选择一个极其糟糕的执行计划。
-
中间结果集的急剧膨胀 多表 JOIN 是一层一层嵌套执行的:表 A 和 表 B 得到一个中间结果集,这个结果集再作为驱动表去 JOIN 表 C。如果前面的 JOIN 过滤性不好,中间结果集会非常大,导致后续的被驱动表承受巨大的扫描压力。
-
内存与磁盘的激烈博弈 无论是 Join Buffer 还是 Hash Join 构建哈希表,都需要消耗大量内存。当多张大表关联导致临时数据超出内存限制时,MySQL 就会被迫将其写入磁盘(产生大量
Created_tmp_disk_tables),引发严重的磁盘 I/O 瓶颈,查询性能直接跌入谷底。
总结
掌握了底层原理后,我们在日常写 SQL 时,应该牢记以下几条铁律:
-
小表驱动大表: 让结果集小的表作为驱动表,可以有效减少循环次数(
STRAIGHT_JOIN可以强制连接顺序,但一般交给优化器即可)。 -
确保被驱动表走索引: 这是 JOIN 优化的核心。尽量让连接过程走 INLJ 算法。
-
只 SELECT 需要的字段: 尤其是当使用 BNLJ 或 Hash Join 时,少查无用字段可以节省 Join Buffer / Hash Table 的内存空间,避免频繁落盘。
-
拥抱 MySQL 8.0: 如果你的业务重度依赖复杂 JOIN,升级到 8.0 享受 Hash Join 带来的性能红利是性价比最高的选择。

