MySQL连接查询优化算法及可能存在的性能问题

InnoDB 引擎中处理所有 JOIN 语句(LEFT JOIN/INNER JOIN/RIGHT JOIN 等),只会用到「3 种物理连接算法」,这三种算法没有绝对的好坏,MySQL 的查询优化器会根据 数据量大小、是否有索引、索引类型 自动选择最优算法

前置知识点

连接查询的本质

MySQL 的JOIN是多表关联查询,本质是:从驱动表中逐行取出数据,去匹配被驱动表的数据,最终筛选出满足 ON 关联条件 + WHERE 过滤条件的组合结果集,子查询本质上也是多表关联。

驱动表 & 被驱动表(重中之重)

这是连接算法的核心概念,所有连接算法的执行逻辑,都围绕「驱动表」和「被驱动表」展开

  • 驱动表(驱动方):先被访问、先被扫描的表,是「主表」,连接的发起方;
  • 被驱动表(被驱动方):后被访问的表,是「从表」,用驱动表的数据去匹配查询的表;
  • 核心原则:MySQL优化器会优先选择「数据量更小」的表作为驱动表 ------ 驱动表的数据量越少,需要发起的匹配次数越少,整体查询开销越低。

所有 JOIN 的底层统一规则

不管是 INNER JOIN(内连接)、LEFT JOIN(左连接)、RIGHT JOIN(右连接),底层执行时都是这 3 种算法的一种,区别只在于:

  • 左连接:固定左表为驱动表,右表为被驱动表,且驱动表的所有数据都会被保留,匹配不到的补 NULL;
  • 右连接:固定右表为驱动表,左表为被驱动表,同理保留驱动表所有数据;
  • 内连接:优化器可以自由选择驱动表和被驱动表(选数据量更小的),只保留匹配成功的数据;

三种连接算法

算法名称 适用场景
Nested Loop Join(嵌套循环连接)- 嵌套循环算法【NLJ 】
Block Nested Loop Join(块嵌套循环连接)- 块嵌套循环算法 【BNL】
Hash Join(哈希连接)- 哈希匹配算法

一、Nested Loop Join(嵌套循环连接)- 嵌套循环算法

✅ 官方简称:NLJ,MySQL 的默认连接算法,也是最基础、最常用的连接算法

  1. 核心原理
    两层嵌套循环:外层循环遍历「驱动表」的每一行数据,内层循环拿着驱动表当前行的关联字段值,去「被驱动表」中逐行扫描匹配 关联条件,匹配成功则拼接结果返回。
  2. 完整执行流程(无索引场景)
    假设执行 SQL:SELECT * FROM t1 JOIN t2 ON t1.id = t2.t1_id,MySQL 选择t1为驱动表(数据量小),t2为被驱动表:
    ① 扫描驱动表t1,取出t1的第一行数据,拿到关联字段 t1.id 的值;
    ② 扫描被驱动表t2的全部数据,逐行对比 t2.t1_id 是否等于 t1.id
    ③ 匹配成功则将两行数据拼接,加入结果集;匹配失败则跳过;
    ④ 回到步骤①,遍历驱动表t1的下一行数据,重复②③,直到驱动表遍历完成。
  3. ✅ 优点
    不需要把两张表的数据都加载到内存,逐行匹配逐行返回,内存开销极低;
  4. ❌ 缺点
    性能极差(无索引时):时间复杂度是 O(M*N)(M = 驱动表行数,N = 被驱动表行数),如果被驱动表数据量大,比如M=1000,N=10万,就需要执行1 亿次匹配,磁盘 IO 会直接拉满,查询巨慢;
  5. ✅ 适用场景
    • 小表连小表:两张表的数据量都很小(比如都小于 1000 行),即使全表扫描,总匹配次数少,开销可以接受;
    • 被驱动表的关联字段有索引(最优场景):这是NLJ 的黄金使用场景,也是生产环境中最常见的用法
    • 返回结果快:匹配到第一条符合条件的数据就可以立即返回,适合分页查询 / 实时查询
  6. 关键优化
    被驱动表关联字段有「索引」时的 NLJ(重中之重,生产最优)
    这是嵌套循环连接的最优形态,也是 MySQL 中效率最高的连接方式,90% 的生产环境 JOIN 都用这种优化后的 NLJ,核心变化:
    被驱动表的关联字段(比如t2.t1_id)创建了索引(主键索引 / 辅助索引) 后,内层循环不再「全表扫描被驱动表」,而是通过索引快速查找匹配值。
  • 优化后的性能质变
    时间复杂度从 O(MN) 降到了 O(MlogN)(logN是 B + 树索引的查询复杂度),比如M=1000,N=10万,匹配次数从1 亿次降到了1000*17 ≈ 1.7 万次,性能提升上万倍!

✔ 结论:只要被驱动表的关联字段有索引,就优先用 NLJ,性能拉满。

二、Block Nested Loop Join(块嵌套循环连接)- 块嵌套循环算法

官方简称:BNL,是 NLJ 的「优化升级版」,专门解决「被驱动表无索引」的场景

  1. 为什么会有 BNL?
    NLJ 的致命问题是:被驱动表无索引时,每遍历驱动表的一行,就要全表扫描一次被驱动表,磁盘 IO 次数 = 驱动表行数 × 被驱动表的磁盘页数量,IO 开销爆炸。
    MySQL 为了解决这个问题,在 NLJ 的基础上做了缓存优化,诞生了 BNL 算法 ------ 核心思想:减少磁盘 IO 的次数。
  2. 核心原理
    在 NLJ 的「逐行匹配」基础上,增加了内存缓存块(Join Buffer):
    1. 不再逐行取驱动表数据,而是一次性读取驱动表的「一批数据」,加载到内存的 Join Buffer 中;
    2. 拿着内存中这一批驱动表数据,一次性扫描一次被驱动表全表,用这批数据批量匹配被驱动表的每一行;
    3. 匹配成功则拼接结果,匹配失败则跳过;
    4. 驱动表的数据分批加载,重复上述步骤直到驱动表遍历完成。
  3. 优点
    • 在被驱动表无索引情况下大幅减少磁盘 IO 次数
    • 兼容所有无索引的连接场景,是无索引下的最优解
  4. 缺点
    时间复杂度依然是 O(M*N),只是实际执行开销降低,性能还是远不如「有索引的 NLJ」
    内存开销比 NLJ 高(需要缓存批量数据);
    必须扫描完被驱动表的全表数据,才能返回结果,不适合分页查询

三、Hash Join(哈希连接)- 哈希匹配算法

✅ 官方说明:MySQL 8.0.18 版本才正式引入 Hash Join,5.7 及以下版本完全不支持

  1. 为什么会有 Hash Join?

    在 MySQL8.0 之前,处理「无索引的大表连接」只有 BNL 一种选择,但 BNL 的时间复杂度还是O(M*N),当两张超大表(比如千万级)做连接时,BNL 的性能依然很差。

    Hash Join 就是为了解决:无索引的大表之间的连接查询,是目前 MySQL 中处理无索引大表连接的最优算法。

  2. 核心前提

    Hash Join 只适用于 等值连接(ON t1.id = t2.t1_id),不支持非等值连接(> / < / >= / <= / like),这是 Hash Join 的硬性限制!

  3. 核心原理

    1. 选择「数据量更小」的表作为驱动表,构建哈希表,减少内存开销
    2. 扫描驱动表的所有数据,根据「关联字段」计算出对应的 哈希值;
    3. 在内存中创建一个哈希表(Hash Table),哈希表的key是「关联字段的哈希值 + 关联字段值」,value是驱动表的整行数据;
    4. 扫描被驱动表的所有数据,取出每一行的「关联字段值」,计算相同的哈希值;
    5. 拿着这个哈希值,去内存中的哈希表中快速查找:如果哈希值匹配,再校验实际的关联字段值是否相等(哈希冲突校验);
  4. ✅ 优点

    • 在被驱动表无索引的情况下性能碾压 BNL:时间复杂度从 O(M*N) 降到了 O(M+N)(构建哈希表O(M) + 探测匹配O(N)),这是质的飞跃;
    • 内存中执行哈希匹配,磁盘 IO 开销极低,只有「扫描驱动表 + 扫描被驱动表」两次全表扫描的 IO;
    • 适合超大表的无索引连接,是 MySQL8.0 的王牌连接算法
  5. ❌ 缺点

    • 仅支持等值连接:硬性限制,非等值连接无法使用;
    • 内存开销高:需要把驱动表的全部数据加载到内存构建哈希表,如果驱动表数据量过大,内存装不下,会触发「磁盘哈希表」,性能会下降;
    • MySQL5.7 及以下版本不支持,有版本限制;
  6. ✅ 适用场景

    核心场景:MySQL8.0 + 版本,无索引的「大表等值连接」

    比如两张千万级的表,关联字段无索引,且是等值匹配(ON a=b),此时 Hash Join 的性能是 BNL 的 10~100 倍,是绝对的最优解。

子查询

MySQL 一定会把子查询等价转换为 JOIN 连接查询,而且是「绝大多数场景下自动完成」,这是 MySQL 查询优化器的核心优化规则,没有例外!

  1. 核心原则
    • MySQL 的查询优化器,本质上「不喜欢子查询」,对「子查询的执行效率天然不友好」,它的核心优化策略是:尽可能将所有能转换的子查询 → 等价的 JOIN 连接查询,再基于 JOIN,选择 NLJ/BNL/Hash Join 这三种连接算法执行。
    • MySQL 的优化器对「子查询转 JOIN」的支持度极高(99%),除了极少数特殊场景,所有子查询都会被优化器重写成 JOIN 语法,执行计划、底层算法、性能开销,和你「手动写的 JOIN 语句完全一致」。
  2. 转换场景

✅ 场景 1:WHERE 中 非关联子查询(最常见,必转 JOIN)

这类子查询是独立的,子查询内部不依赖外层表的字段,执行一次就出结果,MySQL 会直接转为 INNER JOIN

sql 复制代码
-- 原SQL:子查询
SELECT * FROM t1 WHERE t1.id IN (SELECT t2.t1_id FROM t2 WHERE t2.status=1);
-- MySQL转换后的等价JOIN SQL(优化器自动生成)
SELECT DISTINCT t1.* FROM t1 INNER JOIN t2 ON t1.id = t2.t1_id WHERE t2.status=1;

✅ 场景 2:WHERE 中 关联子查询(高频,必转 JOIN,重点)

子查询内部有 t1.id = t2.t1_id 这种依赖外层表的关联条件,也叫相关子查询,100% 被转为 JOIN

sql 复制代码
-- 原SQL:关联子查询(自查询的典型)
SELECT * FROM t1 WHERE EXISTS (SELECT 1 FROM t2 WHERE t2.t1_id = t1.id AND t2.status=1);
-- MySQL转换后的等价JOIN SQL
SELECT t1.* FROM t1 INNER JOIN t2 ON t1.id = t2.t1_id WHERE t2.status=1;

✔ 关键补充:IN 和 EXISTS 类型的关联子查询,转换后的 JOIN 完全等价,性能无差异,MySQL 优化器会自动选择最优写法,不用纠结业务中写IN还是EXISTS。

✅ 场景 3:自表子查询,绝对转 JOIN

一张表自己查自己的子查询,100% 被转为「自连接(SELF JOIN)」

sql 复制代码
-- 原SQL:自表子查询(比如:查t1中,父ID对应的名称)
SELECT t1.*, (SELECT t2.name FROM t1 t2 WHERE t2.id = t1.parent_id) AS parent_name FROM t1;
-- MySQL转换后的等价JOIN SQL(自连接)
SELECT t1.*, t2.name AS parent_name FROM t1 LEFT JOIN t1 t2 ON t2.id = t1.parent_id;
  1. ❌ 可能存在的性能问题
    分页 + 子查询的「顶级大坑」。子查询写分页,MySQL 转换为 JOIN 后,变成「先 JOIN 再分页」,而不是「先分页再 JOIN」,这个是子查询的致命性能问题
sql 复制代码
-- ❌ 错误写法:子查询+分页
SELECT * FROM t1 
WHERE EXISTS (SELECT 1 FROM t2 WHERE t2.t1_id = t1.id) 
LIMIT 10; -- 分页

子查询优化:先单表分页,再关联子查询

sql 复制代码
-- ✅ 正确写法:先对t1单表分页,再做子查询(等价于先分页再JOIN)
SELECT * FROM (
    SELECT * FROM t1 LIMIT 10,10 -- 第一步:先分页,只取10条数据
) AS t1_temp
WHERE EXISTS (SELECT 1 FROM t2 WHERE t2.t1_id = t1_temp.id);

✔ 效果对比:错误写法耗时 5 秒 +,正确写法耗时 0.05 秒,性能提升 100 倍!

总结

  1. MySQL 的 JOIN 只有3 种物理算法:NLJ、BNL、Hash Join,所有逻辑连接(内 / 左 / 右连接)都基于这 3 种算法执行;
  2. 性能优先级:有索引的 NLJ > Hash Join(8.0 + 等值) > BNL > 无索引的 NLJ;
  3. 索引是 JOIN 的灵魂:90% 的 JOIN 性能问题,根源都是「被驱动表的关联字段没有索引」,加索引就能解决;
  4. 驱动表选择:小表当驱动表,永远是最优选择;
  5. 版本红利:升级到 MySQL8.0,能获得 Hash Join 的极致性能,对无索引大表连接友好。
  6. 被驱动表无索引 + 分页 → MySQL 只能走 NLJ,绝对不走 BNL/Hash Join,原因是 NLJ 支持提前终止,适配分页;
  7. 当被驱动表的关联字段存在「可用的索引」(主键索引 / 普通辅助索引 / 联合索引)时,MySQL 查询优化器会「100%、无条件、无例外」选择 NLJ(嵌套循环连接),彻底放弃 Hash Join / BNL 两种算法
  8. 有LIMIT分页时,无论是否有索引、是否等值连接,优先走 NLJ;

性能优化黄金法则

  1. 🔥多表关联时,给「被驱动表的关联字段」创建索引,强制走 NLJ 算法
    假设 SELECT * FROM t1 JOIN t2 ON t1.id = t2.t1_id,给t2.t1_id创建索引即可
  2. 🔥让「小表」作为驱动表
    • 内连接(INNER JOIN):MySQL 优化器会自动选小表当驱动表,无需干预;
    • 左 / 右连接(LEFT/RIGHT JOIN):驱动表是固定的(左连左表是驱动表,右连右表是驱动表)。尽量改成内连接(业务允许的话),让优化器自动选择驱动表。
  3. 🔥禁止使用 SELECT *,只查需要的字段
    • 减少内存开销:Join Buffer 和 Hash Table 中存储的数据更少,能缓存更多数据,减少分批 / 磁盘哈希的概率;
    • 更容易触发「覆盖索引」:如果被驱动表的关联字段有索引,且查询字段都在索引中,能彻底避免回表,性能再上一个台阶。
  4. 🔥避免「大表联大表」的无索引非等值连接
    这种场景是 MySQL 的噩梦:没有索引 + 非等值连接 + 大表,只能走 BNL,时间复杂度O(M*N),查询会巨慢
  5. 🔥分页 JOIN 的黄金优化:先单表分页,再 JOIN,无索引也能救急。
相关推荐
oMcLin8 小时前
如何在 Oracle Linux 8.3 上通过配置 LVM 与 RAID 结合,提升存储系统的性能与数据冗余性
linux·数据库·oracle
上课摸鱼的喵酱8 小时前
【前端性能优化】指标篇:卡顿率——如何去定义你的页面卡不卡
性能优化
罗马苏丹默罕默德8 小时前
Ubuntu下部署.NetCore WebApi的方法
数据库·ubuntu·.netcore
抹茶苹果梨8 小时前
Mysql:简单易懂了解MVCC
mysql
AC赳赳老秦8 小时前
医疗数据安全处理:DeepSeek实现敏感信息脱敏与结构化提取
大数据·服务器·数据库·人工智能·信息可视化·数据库架构·deepseek
喵叔哟8 小时前
18.核心服务实现(下)
数据库·后端·微服务·架构
列御寇8 小时前
MongoDB分片集群分片模式——哈希分片(Hashed Sharding)
数据库·mongodb·哈希算法
Coder_Boy_8 小时前
基于SpringAI的在线考试系统-数据库表设计
java·数据库·算法
IT 行者8 小时前
Claude之父AI编程技巧四:共享团队CLAUDE.md——打造统一的项目智能指南
数据库·ai编程