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 带来的性能红利是性价比最高的选择。

相关推荐
江畔柳前堤4 小时前
大语言模型分布式训练:从并行策略到万卡工程的系统梳理
人工智能·分布式·深度学习·算法·目标检测·机器学习·语言模型
Forever Nore5 小时前
LeetCode 4 寻找两个正序数组的中位数 - 二分
算法·leetcode
罗西的思考7 小时前
【OpenClaw具身硬件】MiniClaw 阅读笔记---(1)基础
人工智能·算法·机器学习
蛋先生DX7 小时前
大模型参数存储格式揭秘:BF不是男朋友
深度学习·算法·llm
爱跳舞的烤冷面8 小时前
自学嵌入式第22天(数据结构——哈希)
数据结构·算法·哈希算法
猎嘤一号8 小时前
博弈论(Game Theory)的理论、算法与工程
人工智能·算法·安全·博弈论
月光船幽幽8 小时前
影子模式下保护 logits 不被修改
人工智能·python·算法
马拉AI8 小时前
腾讯开源 Agent 记忆系统,AI“换对话就忘”的问题有了新解法(附安装使用教程)
人工智能·算法·开源·科研
地平线开发者10 小时前
征程6工具链模型X86推理方式说明
算法
地平线开发者10 小时前
【征程6】校准量化中HistogramObserver解析
算法