MySQL JOIN:底层原理、算法演进与多表性能之谜

目录

​编辑

前言

一、Join本质

二、Join算法

[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 的过程实际上就是:

  1. 确定驱动表 (Driving Table)和被驱动表(Driven Table)。

  2. 从驱动表中取出一条记录。

  3. 根据连接条件(ON 子句)到被驱动表中寻找匹配的记录。

  4. 将匹配的记录合并后返回或放入结果集中。

  5. 重复上述步骤,直到驱动表遍历完毕。

为了让这个过程更高效,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 算法。

执行逻辑:

  1. 依然是从驱动表 A 取出一条数据。

  2. 但这次去被驱动表 B 寻找匹配行时,直接利用表 B 的索引进行查找,而不再是全表扫描。

性能飞跃: 通过 B+ 树索引,原本被驱动表 O(N) 的扫描复杂度瞬间降到了树的高度(通常为 O(logN))。 优化核心: 永远保证被驱动表的 JOIN 字段上有合适的索引!

3. Block Nested-Loop Join (BNLJ) - 块嵌套循环连接 (Buffer)

如果被驱动表的连接字段没有索引 怎么办?MySQL 也不傻,它不会退化去使用 SNLJ,而是引入了 Join Buffer(连接缓冲区)来优化,这就是 BNLJ。

执行逻辑:

  1. MySQL 会在内存中开辟一块名为 Join Buffer 的空间。

  2. 它会将驱动表 A 的数据批量读取到 Join Buffer 中。

  3. 然后扫描一次被驱动表 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。

执行逻辑(分为两阶段):

  1. Build(构建阶段): 遍历驱动表,以 JOIN 的条件字段为键,将其记录放入内存中构建出一个哈希表(Hash Table)。

  2. Probe(探测阶段): 全表扫描被驱动表,取出每一行数据,计算其连接字段的哈希值,去内存中的哈希表中以 O(1) 的时间复杂度快速查找匹配项。

性能革命: Hash Join 特别适合大表之间且没有索引的等值连接。它的出现,让 MySQL 在应对复杂数据分析查询(OLAP)时终于有了底气。如果内存中的哈希表放不下,MySQL 会将数据分块落盘处理(Grace Hash Join),依然比 BNLJ 快得多。

三、 多表 JOIN 为什么容易导致性能雪崩?

理解了上面的算法,我们再来看为什么老司机总是警告我们:"线上查询不要 JOIN 超过 3 张以上的表"。多表 JOIN 性能下降的核心原因有以下三点:

  1. 组合爆炸(优化器解析成本骤增) 当进行多表 JOIN 时,MySQL 的查询优化器需要决定表的连接顺序(谁是第一张驱动表,谁是第二张......)。如果是 N 张表 JOIN,可能的连接顺序有 N! (N的阶乘)种。表越多,优化器评估最佳执行计划的时间就呈指数级上升,甚至可能因为超时而选择一个极其糟糕的执行计划。

  2. 中间结果集的急剧膨胀 多表 JOIN 是一层一层嵌套执行的:表 A 和 表 B 得到一个中间结果集,这个结果集再作为驱动表去 JOIN 表 C。如果前面的 JOIN 过滤性不好,中间结果集会非常大,导致后续的被驱动表承受巨大的扫描压力。

  3. 内存与磁盘的激烈博弈 无论是 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 带来的性能红利是性价比最高的选择。

相关推荐
追随者永远是胜利者7 小时前
(LeetCode-Hot100)253. 会议室 II
java·算法·leetcode·go
Jason_Honey28 小时前
【平安Agent算法岗面试-二面】
人工智能·算法·面试
程序员酥皮蛋8 小时前
hot 100 第三十五题 35.二叉树的中序遍历
数据结构·算法·leetcode
追随者永远是胜利者8 小时前
(LeetCode-Hot100)207. 课程表
java·算法·leetcode·go
仰泳的熊猫9 小时前
题目1535:蓝桥杯算法提高VIP-最小乘积(提高型)
数据结构·c++·算法·蓝桥杯
那起舞的日子9 小时前
动态规划-Dynamic Programing-DP
算法·动态规划
闻缺陷则喜何志丹10 小时前
【前后缀分解】P9255 [PA 2022] Podwyżki|普及+
数据结构·c++·算法·前后缀分解
每天吃饭的羊10 小时前
时间复杂度
数据结构·算法·排序算法
ValhallaCoder11 小时前
hot100-堆
数据结构·python·算法·