面试知识库
高 进阶

Join算法原理#

一句话答案#

MySQL 的 join 主要有三种算法:被驱动表关联列有索引时用 Index Nested-Loop Join(驱动表每行去索引里查,最优);无索引时旧版用 Block Nested-Loop Join(驱动表批量放进 join buffer 再扫被驱动表,减少扫描次数);MySQL 8.0.18 引入 Hash Join,8.0.20 起彻底移除 BNL,原来用 BNL 的场景(含非等值连接、外连接)一律改用 Hash Join。优化关键是”小表驱动大表 + 被驱动表关联列建索引”。

核心要点

算法触发条件原理复杂度
Index Nested-Loop(INLJ)被驱动表关联列有索引驱动表逐行,拿连接值去被驱动表走索引查最优,接近 O(N·logM)
Block Nested-Loop(BNL)被驱动表关联列无索引(8.0.20 前;8.0.20 起已移除)驱动表批量读入 join buffer,被驱动表只全表扫一次按 buffer 匹配O(N·M),但减少被驱动表扫描遍数
Hash Join无索引连接(8.0.18 起用于等值连接;8.0.20 起也用于非等值/外连接/半连接,全面取代 BNL)小表在内存建哈希表(超出 join_buffer_size 时落盘分片),大表逐行探测O(N+M),大表等值连接最快

关键概念:

  • 驱动表(外表)vs 被驱动表(内表):优化器通常让结果集小的表做驱动表(“小表驱动大表”),减少外层循环次数
  • join buffer:BNL 用,由 join_buffer_size 控制;放不下时分多批,被驱动表要多扫几遍(8.0.20 起 join_buffer_size 改为限制 Hash Join 的内存,超出就写临时文件)
  • INLJ 性能由被驱动表索引决定:所以 join 优化第一步永远是给关联字段加索引,让 BNL/Hash Join 退化为 INLJ
  • 8.0.18/8.0.19 的 Hash Join 要求至少有一个等值连接条件,否则仍走 BNL;8.0.20 起 BNL 被移除,非等值(<、>、between)、外连接、semi/anti join 也都走 Hash Join

面试回答(2分钟版)

MySQL 的 join 本质是嵌套循环,外层叫驱动表、内层叫被驱动表。具体分三种算法:第一种 Index Nested-Loop Join,前提是被驱动表的关联列有索引,这时驱动表每取一行,就拿连接值去被驱动表走索引精确查,效率最高,是我们追求的形态。第二种 Block Nested-Loop Join,用在被驱动表关联列没索引的老版本上:如果还是逐行去全表扫被驱动表,那扫描次数等于驱动表行数,非常慢;所以 MySQL 改成先把驱动表的数据批量装进 join buffer,被驱动表只全表扫一次,扫的时候和 buffer 里所有行做匹配,这样被驱动表的扫描遍数从”驱动表行数”降到”buffer 批数”。第三种是 MySQL 8.0.18 引入的 Hash Join,它把较小一侧在内存里建哈希表,另一侧逐行探测,复杂度接近线性,比 BNL 快很多;8.0.20 起 BNL 被彻底移除,凡是以前走 BNL 的无索引 join(包括非等值连接和外连接)都改走 Hash Join。

优化上记两条:一是小表驱动大表,让优化器拿结果集小的当外层减少循环;二是给被驱动表的关联列建索引,把 BNL 或 Hash Join 优化成 Index Nested-Loop Join。可以用 explain 看,Extra 里出现 Using join buffer (Block Nested Loop)(8.0.20 前)或 Using join buffer (hash join) 就说明没走索引,需要补索引。

追问与易错

追问方向:

  • “什么叫小表驱动大表?谁是小表?”→ 不是看总行数,而是看经过 where 过滤后参与 join 的结果集大小;优化器会基于成本自动选驱动表,但写 SQL 时让过滤性强的表先过滤有助于它选对
  • “BNL 为什么比朴素嵌套循环快?”→ 朴素做法被驱动表要被全表扫”驱动表行数”遍;BNL 把驱动表攒一批放 join buffer,被驱动表只扫”批数”遍,buffer 越大批数越少,扫得越少
  • “Hash Join 和 INLJ 哪个快?”→ 被驱动表有合适索引时 INLJ 通常更快(直接索引定位);无索引的大表等值连接 Hash Join 完胜 BNL。所以有索引优先 INLJ,没索引才靠 Hash Join 兜底
  • “怎么判断走了哪种算法?”→ 看 explain 的 Extra:空着且被驱动表 type 为 ref/eq_ref 是 INLJ;Using join buffer (Block Nested Loop) 是 BNL(只会出现在 8.0.20 之前);Using join buffer (hash join) 是 Hash Join;8.0.18+ 用 EXPLAIN FORMAT=TREE 能直接看到 Inner hash join / Nested loop inner join

易错点:

  • ❌ 以为 join 慢只能拆成多次单表查——大多数情况补被驱动表索引即可解决
  • ❌ 以为 Hash Join 万能——它仍要全量扫描两侧、内存不够会落盘,有索引时通常不如 INLJ;(另:8.0.18/8.0.19 只支持含等值条件的连接,8.0.20 起非等值连接也走 Hash Join)
  • ❌ 盲目调大 join_buffer_size——它是按连接分配的会话级内存,过大浪费内存(8.0.20 起它控制 Hash Join 内存,能减少落盘但治标);根本解法是给关联列加索引走 INLJ