MySQL 连表查询的底层核心是 通过特定算法将多个表的记录组合成符合查询条件的结果集,其性能直接依赖于表的大小、索引设计、连接条件等因素。MySQL 底层主要实现了三种经典的连接算法,分别是 嵌套循环连接(Nested Loop Join) 、哈希连接(Hash Join) 、排序合并连接(Sort-Merge Join) ,且不同版本对算法的支持不同(例如 Hash Join 从 MySQL 8.0.18 开始正式支持)。
核心前提:连接的本质
连表查询(JOIN)的本质是 笛卡尔积过滤:
- 首先对参与连接的表计算 “笛卡尔积”(所有表的记录两两组合,结果集大小 = 表 1 行数 × 表 2 行数 × ... × 表 N 行数);
- 再通过
ON条件(连接条件)、WHERE条件过滤笛卡尔积中的无效记录,最终得到目标结果。
但直接计算笛卡尔积会产生海量无效数据(例如两个 10 万行的表,笛卡尔积是 1e9 行),因此所有底层连接算法的核心目标都是 避免全量计算笛卡尔积,通过 “有序遍历”“哈希匹配” 等方式快速筛选有效记录。
一、嵌套循环连接(Nested Loop Join,NLJ)
1. 算法原理
最基础、最常用的连接算法,核心是 “外层循环遍历驱动表,内层循环匹配被驱动表” ,类似程序中的嵌套循环(for 循环套 for 循环)。
核心步骤:
-
选择驱动表和被驱动表:
- 驱动表(Outer Table):外层循环遍历的表,优先选择 数据量小(小表)或 有索引 的表(减少外层循环次数);
- 被驱动表(Inner Table):内层循环遍历的表,需通过连接条件快速匹配驱动表的记录(依赖索引优化)。
-
外层循环:遍历驱动表的每一条记录(记为
r); -
内层循环:对于驱动表的每条记录
r,根据连接条件(如r.id = s.user_id),在被驱动表中查找匹配的记录(记为s); -
结果合并:将匹配成功的
(r, s)组合加入结果集。
示意图:
for each 记录 r in 驱动表:
for each 记录 s in 被驱动表 where s.连接列 = r.连接列:
输出 (r, s)
2. 关键优化:索引嵌套循环(Index Nested Loop,INLJ)
如果 被驱动表的连接列上有索引,内层循环无需全表扫描,而是通过索引快速查找匹配记录(类似 WHERE 条件 + 索引查询),这是 NLJ 的最优形态,也是 MySQL 中最常用的连接方式。
性能分析:
- 时间复杂度:
O(M × log N)(M = 驱动表行数,N = 被驱动表行数,log N 是索引查找的时间); - 核心优势:无需扫描被驱动表全表,仅通过索引匹配,效率极高;
- 适用场景:中小表连接、被驱动表连接列有索引。
3. 退化情况:朴素嵌套循环(Naive Nested Loop)
如果 被驱动表的连接列无索引,内层循环只能全表扫描,时间复杂度为 O(M × N)(例如 M=1 万、N=1 万时,需执行 1 亿次循环),性能极差,MySQL 会尽量避免这种情况(若无法避免,会触发 “块嵌套循环” 优化)。
4. 优化变种:块嵌套循环(Block Nested Loop,BNL)
为解决 “朴素嵌套循环” 的低效问题,MySQL 引入了 BNL 优化(默认开启,通过 join_buffer_size 控制):
-
核心思路:批量读取驱动表的记录到内存缓冲区(join buffer),再批量与被驱动表匹配,减少被驱动表的全表扫描次数。
-
步骤:
- 外层循环读取驱动表的一批记录(大小由
join_buffer_size决定),存入 join buffer; - 内层循环扫描一次被驱动表,将被驱动表的每条记录与 join buffer 中的所有驱动表记录批量匹配;
- 重复上述步骤,直到驱动表所有记录处理完毕。
- 外层循环读取驱动表的一批记录(大小由
-
性能提升:将被驱动表的扫描次数从
M次(朴素 NLJ)减少到ceil(M / 缓冲区记录数)次(例如 M=1 万,缓冲区可存 1000 条,则仅需扫描 10 次被驱动表)。
二、哈希连接(Hash Join,HJ)
1. 算法原理
MySQL 8.0.18 及以上版本支持的连接算法,核心是 “用哈希表快速匹配连接条件” ,适用于 无索引的大表连接(NLJ 此时会退化为全表扫描,效率极低,而 HJ 更高效)。
核心步骤(以两表连接为例):
-
选择构建表(Build Table)和探测表(Probe Table) :
-
构建表:优先选择 数据量小 的表(小表),将其所有记录按 “连接列” 构建哈希表(内存中);
- 哈希表的 Key = 连接列的值(如
user_id),Value = 构建表的整行记录; - 若构建表过大,内存存不下,会拆分到磁盘(分区哈希表),但性能会下降。
- 哈希表的 Key = 连接列的值(如
-
-
探测匹配:
- 遍历探测表(大表)的每一条记录,提取其连接列的值,计算哈希值;
- 用该哈希值在哈希表中快速查找匹配的 Key,找到后将探测表记录与哈希表中的 Value(构建表记录)组合,加入结果集。
示意图:
# 第一步:构建哈希表(小表)
hash_table = {}
for each 记录 r in 构建表:
key = hash(r.连接列)
hash_table[key].append(r)
# 第二步:探测匹配(大表)
for each 记录 s in 探测表:
key = hash(s.连接列)
if key in hash_table:
for each 记录 r in hash_table[key]:
if r.连接列 == s.连接列: # 哈希碰撞校验
输出 (r, s)
2. 关键细节
- 哈希碰撞处理:不同连接列的值可能计算出相同的哈希值(哈希碰撞),因此找到 Key 后需再次校验原始连接列的值是否相等;
- 内存限制:若构建表过大,MySQL 会使用 “分段哈希连接”(Partitioned Hash Join):将构建表和探测表按连接列的哈希值拆分成多个分区,逐个分区加载到内存处理,避免内存溢出;
- 适用场景:大表连接、连接列无索引(NLJ 效率低)、等值连接(
=条件,哈希表仅支持等值匹配)。
3. 性能分析
- 时间复杂度:
O(M + N)(M = 构建表行数,N = 探测表行数),构建哈希表耗时O(M),探测匹配耗时O(N),效率远高于朴素 NLJ(O(M×N)); - 优势:无需索引,适合无索引大表的等值连接;
- 劣势:仅支持等值连接(不支持
<、>等范围连接),依赖内存(大表分区会影响性能)。
三、排序合并连接(Sort-Merge Join,SMJ)
1. 算法原理
适用于 非等值连接(如 <、>、BETWEEN)或 无索引但可排序的场景,核心是 “先排序,后合并” ,避免嵌套循环的全表扫描。
核心步骤:
-
排序阶段:
- 对两个参与连接的表,分别按 “连接列” 进行排序(若表已按连接列排序,可跳过此步骤);
- 排序后的表满足 “连接列有序” 的特性。
-
合并阶段:
-
用两个指针(i 指向表 A,j 指向表 B)同时遍历两个排序后的表;
-
根据连接条件(如
A.id < B.user_id)移动指针,匹配符合条件的记录:- 若
A[i].id < B[j].user_id:匹配成功,将(A[i], B[j])加入结果集,移动 i 指针(继续找 A 的下一条记录); - 若
A[i].id >= B[j].user_id:移动 j 指针(找 B 的下一条更大的记录);
- 若
-
直到其中一个表遍历完毕。
-
示意图:
# 第一步:排序(假设已排序)
A_sorted = 表A按连接列排序
B_sorted = 表B按连接列排序
# 第二步:合并遍历
i = 0, j = 0
while i < len(A_sorted) and j < len(B_sorted):
a = A_sorted[i]
b = B_sorted[j]
if a.连接列 满足 连接条件 (如 a.id < b.user_id):
输出 (a, b)
i += 1
else:
j += 1
2. 关键细节
- 排序开销:排序阶段的时间复杂度是
O(M log M + N log N)(M、N 为两表行数),若表已按连接列排序(如连接列是主键、有排序索引),可省略排序步骤,性能大幅提升; - 适用场景:非等值连接(哈希连接不支持)、两表数据量大但可排序、连接列有排序索引;
- MySQL 支持情况:MySQL 底层支持 SMJ,但实际优化器更倾向于 NLJ(有索引时)或 HJ(无索引等值连接时),仅在非等值连接且无更好选择时才会使用 SMJ。
3. 性能分析
- 时间复杂度:
O(M log M + N log N + M + N)(排序 + 合并); - 优势:支持非等值连接,避免嵌套循环的全表扫描;
- 劣势:排序开销较大,若表未排序,性能可能不如 HJ(等值连接场景)。
四、MySQL 优化器的算法选择逻辑
MySQL 的查询优化器(Optimizer)会根据 表的大小、索引情况、连接条件类型 自动选择最优的连接算法,核心逻辑如下:
-
优先选择 INLJ(索引嵌套循环) :
- 条件:被驱动表的连接列有索引,且驱动表是小表;
- 原因:效率最高(
O(M log N)),无需排序或哈希表开销。
-
其次选择 HJ(哈希连接) :
- 条件:等值连接(
=)、被驱动表连接列无索引、两表数据量较大(大表连接); - 原因:避免 NLJ 的全表扫描,效率高于朴素 NLJ 和 SMJ(等值连接场景)。
- 条件:等值连接(
-
最后选择 SMJ 或 BNL:
- SMJ:非等值连接(
<、>等),无索引或索引无效; - BNL:无索引的小表连接(
join_buffer_size足够时),避免朴素 NLJ 的低效。
- SMJ:非等值连接(
示例:优化器选择逻辑
sql
-- 场景1:t1是小表(100行),t2的user_id有索引(连接列)
SELECT * FROM t1 JOIN t2 ON t1.id = t2.user_id;
-- 优化器选择:INLJ(驱动表t1,被驱动表t2通过索引匹配)
-- 场景2:t1和t2都是大表(100万行),t2的user_id无索引,等值连接
SELECT * FROM t1 JOIN t2 ON t1.id = t2.user_id;
-- 优化器选择:HJ(t1作为构建表,t2作为探测表,哈希匹配)
-- 场景3:非等值连接,无索引
SELECT * FROM t1 JOIN t2 ON t1.id < t2.user_id;
-- 优化器选择:SMJ(先排序,后合并)
五、底层优化建议(基于算法原理)
- 优先给连接列建索引:触发 INLJ(最优算法),避免全表扫描;
- 小表作为驱动表:减少外层循环次数(INLJ)或降低哈希表构建开销(HJ);
- 控制 join_buffer_size:BNL 场景下,适当增大缓冲区(默认 256K),减少被驱动表扫描次数;
- 等值连接优先:哈希连接(HJ)仅支持等值连接,非等值连接需依赖 SMJ(排序开销大);
- 避免大表无索引连接:无索引的大表连接会触发 BNL 或 HJ(分区哈希),性能较差,尽量给连接列建索引。
总结
MySQL 连表查询的底层算法围绕 “减少无效数据扫描” 展开:
- NLJ:依赖索引,适合中小表、有索引场景,是最优选择;
- HJ:依赖哈希表,适合大表、无索引、等值连接场景;
- SMJ:依赖排序,适合非等值连接场景。
理解底层算法的核心是:索引决定是否能触发高效的 INLJ,表大小决定 HJ/SMJ 的选择,连接条件类型决定是否支持 HJ。优化连表查询的本质,就是让优化器能选择最优的连接算法(通常是 INLJ 或 HJ)