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,无索引也能救急。
相关推荐
0xDevNull2 小时前
MySQL数据冷热分离详解
后端·mysql
科技小花2 小时前
数据治理平台架构演进观察:AI原生设计如何重构企业数据管理范式
数据库·重构·架构·数据治理·ai-native·ai原生
一江寒逸2 小时前
零基础从入门到精通MySQL(中篇):进阶篇——吃透多表查询、事务核心与高级特性,搞定复杂业务SQL
数据库·sql·mysql
D4c-lovetrain2 小时前
linux个人心得22 (mysql)
数据库·mysql
阿里小阿希3 小时前
CentOS7 PostgreSQL 9.2 升级到 15 完整教程
数据库·postgresql
荒川之神3 小时前
Oracle 数据仓库雪花模型设计(完整实战方案)
数据库·数据仓库·oracle
做个文艺程序员3 小时前
MySQL安全加固十大硬核操作
数据库·mysql·安全
不吃香菜学java3 小时前
Redis简单应用
数据库·spring boot·tomcat·maven
一个天蝎座 白勺 程序猿3 小时前
Apache IoTDB(15):IoTDB查询写回(INTO子句)深度解析——从语法到实战的ETL全链路指南
数据库·apache·etl·iotdb
不知名的老吴3 小时前
Redis的延迟瓶颈:TCP栈开销无法避免
数据库·redis·缓存