当前位置: 首页 > news >正文

MySQL连接原理深度解析:从算法到源码的全链路优化

第1章 连接操作全景图

在进入源码与优化策略之前,我们需要先从 连接(Join)的数学本质SQL语义映射 入手,构建整体认知框架。只有理解了 为什么数据库需要JOIN 以及 不同JOIN语义的差异,才能更好地把握后续的执行引擎和性能优化。


1.1 连接的数学基础:从笛卡尔积说起

数据库的连接操作,本质上是 集合论中的笛卡尔积(Cartesian Product)条件过滤 的结合。

  • 笛卡尔积定义
    给定两个集合 A、B,笛卡尔积记为 A × B,其结果是所有可能的 (a, b) 组合。
    在数据库中,如果有表 OrdersCustomers,它们的笛卡尔积等价于:

    SELECT * FROM Orders, Customers;

    结果会生成 |Orders| * |Customers| 行数据。

显然,笛卡尔积的数据量呈乘法级膨胀,因此在实际SQL中几乎不会直接使用,而是通过 连接条件(Join Condition) 约束结果。


1.2 连接的分类与SQL语义映射

MySQL遵循SQL:1992标准,支持以下几类连接:

  • INNER JOIN(内连接)
    返回两表中满足连接条件的记录。

    SELECT * FROM Orders o
    INNER JOIN Customers c ON o.customer_id = c.id;
    
  • LEFT JOIN(左外连接)
    保留左表的所有记录,若右表无匹配,则以NULL填充。

  • RIGHT JOIN(右外连接)
    保留右表的所有记录,若左表无匹配,则以NULL填充。

  • FULL JOIN(全外连接) (MySQL不直接支持,但可通过UNION模拟)


1.3 Venn图文字描述法

为了帮助读者在没有图形的情况下理解连接语义,我们用文字版的Venn图进行描述:

  • INNER JOIN:取交集 → 两个集合重叠部分

  • LEFT JOIN:取左集合全部 + 右集合重叠部分

  • RIGHT JOIN:取右集合全部 + 左集合重叠部分

  • FULL JOIN:取并集 → 两个集合所有部分


1.4 MySQL连接处理的流程全景

一次连接查询的大致执行路径如下(文字版时序图):

[SQL Parser] → 解析JOIN语法树↓
[Optimizer] → 选择连接顺序 & 算法 (Nested Loop / BKA / Hash Join)↓
[Executor] → 为每个JOIN_TAB分配JOIN_BUFFER↓
[Storage Engine] → 逐行/批量读取数据行↓
[Join Condition Eval] → 判断是否满足ON条件↓
[Result Set] → 输出符合条件的行

这里的关键在于 优化器的决策:选择什么样的连接顺序、使用什么JOIN算法;以及 执行器的内存管理:如何利用JOIN_BUFFER缓存数据行以避免频繁磁盘访问。


1.5 本章小结

  • JOIN的本质是笛卡尔积 + 条件过滤。

  • INNER/LEFT/RIGHT/FULL JOIN可以用集合论和Venn图清晰表达。

  • MySQL在执行JOIN时,会经历 解析 → 优化 → 执行 → 返回结果 的完整流程。

  • 优化器选择算法、执行器分配JOIN_BUFFER,是后续性能优化的关键点。

第2章 连接执行引擎解析

在 MySQL 中,JOIN 的执行完全由 SQL 执行器(Executor) 驱动。优化器负责选择 连接顺序算法,而执行器则真正负责 逐行扫描、缓存管理、条件判断、结果输出。其中,JOIN_BUFFER 是整个执行过程中性能表现的关键。


2.1 JOIN_BUFFER 的地位与作用

为什么需要 JOIN_BUFFER?

  • 当 MySQL 使用 Nested Loop Join (NLJ) 时,外层表每一行都会驱动内层表进行匹配。

  • 如果内表没有合适的索引,就需要 全表扫描,成本极高。

  • 为了减少反复扫描,MySQL 引入了 JOIN_BUFFER,将一部分行缓存起来,提高批量匹配效率。

👉 可以理解为:JOIN_BUFFER 是执行器在内存里开辟的一块 临时缓存区,帮助存放中间结果,避免频繁回表。


2.2 源码入口:JOIN 执行主流程

JOIN 的执行逻辑主要位于 sql/sql_executor.ccsql/sql_select.cc
核心方法是:

bool JOIN::exec() {// 1. 初始化执行环境if (setup_join_buffers()) {return true; // 内存分配失败}// 2. 执行 join loopreturn do_select(this);
}

这里可以看到:

  1. setup_join_buffers():为每个参与 JOIN 的表分配 buffer

  2. do_select():进入执行循环,逐行读取并进行连接


2.3 JOIN_BUFFER 内存分配机制

源码片段(简化版):

void setup_join_buffer(JOIN_TAB *tab) {if (tab->need_buffer) {tab->buff = join_buffer_alloc(tab->buffer_size);init_io_cache(&tab->file, ...);  // 初始化IO缓存}
}

几个关键点:

  • tab->need_buffer:优化器判定是否需要使用 buffer

  • join_buffer_alloc:实际申请内存

  • buffer_size:由参数 join_buffer_size 控制,默认 256KB

内存分配公式可以抽象为:

总JOIN_BUFFER消耗 ≈ join_buffer_size × (参与JOIN的表数 - 1)

⚠️ 如果连接表较多,JOIN_BUFFER 会被 乘法放大,因此配置过大会导致内存暴涨。


2.4 EXPLAIN 中的 "Using join buffer"

执行计划分析时,我们经常会看到:

EXPLAIN SELECT * FROM orders o
JOIN customers c ON o.customer_id = c.id;

可能输出:

id | select_type | table | type | key | Extra
1  | SIMPLE      | o     | ALL  | NULL| 
1  | SIMPLE      | c     | ALL  | NULL| Using join buffer (Block Nested Loop)
  • Using join buffer (BNL)
    表示内表没有索引可用,MySQL 使用了 Block Nested Loop Join,内表行被缓存到 JOIN_BUFFER 里进行匹配。

  • 在 MySQL 8.0 中,还可能看到:

    • BNL:Block Nested Loop

    • BKA:Batch Key Access


2.5 MySQL 5.7 vs 8.0 的 JOIN_CACHE 差异

  • MySQL 5.7
    主要支持 BNL,JOIN_BUFFER 相对简单,只是行级缓存。

  • MySQL 8.0
    引入了 JOIN_CACHE Framework,支持:

    • BNL(Block Nested Loop)

    • BKA(Batch Key Access)

    • HASH JOIN(从 8.0.18 开始引入,优化大表连接场景)

伪代码比较:

// MySQL 5.7
for (row in outer_table) {load rows from inner_table into join_buffer;compare row with buffer;
}// MySQL 8.0
for (batch of rows in outer_table) {probe inner_table using indexes (BKA);cache results in join_buffer;
}

可以看到,8.0 的 JOIN_BUFFER 不再只是单纯的内存缓存,而是演变为一个 多策略缓存框架


2.6 本章小结

  • JOIN 执行核心流程:setup_join_buffers → do_select → 逐行匹配

  • JOIN_BUFFER 是执行器级别的优化工具,用于减少扫描成本

  • EXPLAIN 中的 "Using join buffer" 是识别 JOIN 执行方式的关键标志

  • 从 5.7 到 8.0,JOIN_CACHE 演进为支持 BNL、BKA、HASH JOIN 的多策略体系

第3章 连接算法详解

MySQL 的 JOIN 执行,归根结底是基于不同场景下的 连接算法选择。优化器负责选择合适的算法,执行器依赖 JOIN_BUFFER 作为缓存区来支撑这些算法的运行。


3.1 Nested Loop Join (NLJ)

抽象原理

  • 外层表每一行,去内层表逐行匹配。

  • 最简单的连接方式,时间复杂度 O(M × N)

SQL 示例

SELECT * 
FROM orders o
JOIN customers c ON o.customer_id = c.id;

伪代码

for (row_o in Orders) {for (row_c in Customers) {if (row_o.customer_id == row_c.id) {output(row_o, row_c);}}
}

MySQL实现细节

  • 如果 customers.id 上有索引,内层循环可通过 索引快速定位,复杂度近似 O(M × logN)

  • 如果没有索引,就需要全表扫描,效率极低。


3.2 Block Nested Loop Join (BNL)

抽象原理

  • NLJ 的优化版本:将外层表的一批行放入 JOIN_BUFFER,然后批量与内层表比对。

  • 避免频繁回表扫描,减少磁盘 I/O。

伪代码

while (fetch batch from Orders into join_buffer) {for (row_c in Customers) {for (row_o in join_buffer) {if (row_o.customer_id == row_c.id) {output(row_o, row_c);}}}
}

EXPLAIN 标志

  • 会显示:Using join buffer (Block Nested Loop)

特点

  • 减少内层表重复扫描,但仍需全表遍历。

  • JOIN_BUFFER 大小直接影响性能。


3.3 Batch Key Access Join (BKA)

引入背景

  • BNL 仍需扫描内表 → 如果内表有索引,但不能高效利用,性能不足。

  • BKA 的目标:批量利用索引探查内表

抽象原理

  • 外表一批行放入 JOIN_BUFFER

  • 从 JOIN_BUFFER 中批量取出 连接键

  • 内表使用索引执行批量探查,返回结果

伪代码

while (fetch batch from Orders into join_buffer) {keys = extract_keys(join_buffer);  // 批量收集 customer_idrows = index_lookup_batch(Customers, keys);  // 批量索引探查for (row in rows) {if (match_condition(row, join_buffer)) {output(row);}}
}

EXPLAIN 标志

  • 显示:Using join buffer (Batched Key Access)

优点

  • 大幅减少随机 I/O

  • 特别适合 大表连接 + 有索引 的场景


3.4 Hash Join (MySQL 8.0.18+)

引入原因

  • 对于没有索引的连接,大表之间的 BNL 依旧低效。

  • Hash Join 通过 哈希表 加速匹配。

抽象原理

  1. 选择较小的表(Build 表),建立哈希表:key → rows

  2. 遍历大表(Probe 表),用连接键查哈希表,快速匹配

伪代码

hash_table = {}
for (row_c in Customers) {hash_table[row_c.id].add(row_c);
}for (row_o in Orders) {if (hash_table.contains(row_o.customer_id)) {output(row_o, hash_table[row_o.customer_id]);}
}

MySQL实现

  • MySQL 8.0.18 引入

  • Hash Join 通过 join_buffer 存放哈希表

  • 需要配置参数:optimizer_switch='hash_join=on'


3.5 不同算法的性能对比

算法索引依赖内存依赖时间复杂度场景
NLJO(M×N) / O(M×logN)小表连接,有索引
BNLO(M×N),但减少I/O大表连接,无索引
BKA近似 O(M×logN),批量优化大表连接,有索引
Hash JoinO(M+N)大表连接,无索引

3.6 本章小结

  • NLJ → 最简单,但依赖索引

  • BNL → 利用 JOIN_BUFFER,减少重复扫描

  • BKA → 批量索引探查,大表场景利器

  • Hash Join → MySQL 8.0 的新引擎,解决大表无索引 JOIN 的瓶颈

第4章 索引与连接性能优化

在前面我们看到,JOIN 的本质是 外层表驱动内层表匹配。而匹配过程的效率,几乎完全取决于 内表能否高效定位到数据
👉 索引就是 MySQL 在 JOIN 性能上的“第一杀器”。


4.1 索引在 JOIN 中的角色

  • 外表(Driving Table):负责提供连接键的输入。索引影响不大,主要影响扫描顺序。

  • 内表(Driven Table):负责接收连接键查找数据。是否存在合适的索引,直接决定性能。

结论:JOIN 优化的重点,几乎总在 被驱动表(Driven Table) 上。


4.2 前缀索引与等值连接优化

有时候连接字段是长字符串,比如邮箱、URL。如果直接建全列索引,存储和维护成本高。

前缀索引示例

CREATE INDEX idx_email_prefix ON users(email(20));

适用场景

  • 等值 JOIN 条件:ON u.email = o.user_email

  • 查询只在前缀范围内能有效过滤

  • 如果选择过短,可能造成索引选择性下降

👉 调优策略:

  • 测试不同前缀长度下的 Cardinality(区分度)

  • 通过 SHOW INDEX FROM users; 查看选择性


4.3 覆盖索引与回表消除

在 JOIN 查询中,MySQL 如果需要额外回表取数据,会增加磁盘 I/O。
覆盖索引 可以避免这一点。

示例

CREATE INDEX idx_customer_country ON customers(country, id);

查询:

SELECT o.id, c.id FROM orders o JOIN customers c ON o.customer_id = c.id WHERE c.country = 'US';
  • 覆盖索引包含 (country, id),因此 WHEREJOIN 条件都能在索引层完成

  • 不需要回表,提高性能


4.4 Batch Key Access (BKA) 与索引批量探查

回忆第3章:BKA 的本质是 批量利用索引
如果内表有索引(比如二级索引 customer_id),BKA 可以将外表的多个键一次性送给存储引擎,执行批量查找。

👉 调优策略:

  • 确保内表连接键有索引

  • 调整 join_buffer_size 使得 BKA 批量效果最大化


4.5 连接顺序优化(Join Reordering)与索引选择

MySQL 优化器会尝试 重新排列 JOIN 顺序,其代价模型考虑了:

  • 连接列是否有索引

  • 索引的选择性(Cardinality)

  • 表大小估算

示例

EXPLAIN SELECT * FROM big_table b JOIN small_table s ON b.key = s.key;

可能优化器选择:

  • small_table → big_table(如果 small_table.key 有高选择性索引)

  • 这样大表扫描可以大幅缩小范围


4.6 分布式场景下的索引与 Sharding

在分布式数据库(如 MySQL Sharding 或 TiDB)中,JOIN 性能更依赖索引:

  • 本地索引:如果 JOIN 字段是分片键,可以路由到单个分片执行 → 高效

  • 全局索引缺失:需要跨分片 JOIN,代价高

  • 优化策略

    • 尽量让 JOIN 字段成为分片键

    • 使用预聚合表减少跨库 JOIN

    • 引入全局二级索引(如 TiDB 的 Global Index)


4.7 索引优化效果对比实验

假设两表:

CREATE TABLE orders (id BIGINT PRIMARY KEY,customer_id BIGINT,amount DECIMAL(10,2)
);CREATE TABLE customers (id BIGINT PRIMARY KEY,country VARCHAR(50)
);

测试用例

EXPLAIN SELECT * 
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE c.country = 'US';
  • 无索引Using join buffer (BNL),全表扫描

  • 单列索引 (c.id):走索引,Nested Loop / BKA

  • 组合索引 (c.country, id):WHERE 与 JOIN 同时命中 → 性能最佳


4.8 本章小结

  • JOIN 优化的核心 = 内表是否有高效索引

  • 前缀索引:降低存储成本,但要关注选择性

  • 覆盖索引:避免回表,提升查询性能

  • BKA:充分利用索引进行批量探查

  • Join Reordering:优化器会利用索引特性重新安排顺序

  • 分布式场景:分片键 & 全局索引是关键

第5章 高级连接场景解析

在真实业务中,JOIN 往往不止两张表,甚至涉及 子查询多表链式连接、以及复杂的 过滤条件。MySQL 在优化器和执行器层面都引入了特殊机制来提升这些场景下的性能。


5.1 子查询转换为 JOIN 的优化策略

背景

子查询(Subquery)在 MySQL 中性能往往较差,尤其是 相关子查询
优化器会尝试将其改写为 半连接 (Semi-Join) 或 JOIN。

示例

-- 子查询写法
SELECT * FROM orders o
WHERE o.customer_id IN (SELECT c.id FROM customers c WHERE c.country = 'US');

优化器会将其改写为:

-- JOIN 化写法
SELECT o.* FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE c.country = 'US';

优点

  • 子查询 → JOIN 转换后,可以利用索引、BKA、Hash Join

  • 避免逐行执行子查询,提高整体性能


5.2 多表连接的执行路径选择

当连接涉及 3张及以上表 时,问题变成了:
👉 优化器如何选择连接顺序?

代价模型

优化器会基于 动态规划 (DP) 算法,计算每个可能的连接顺序的代价:

  • 表大小

  • 索引可用性

  • 选择性估算

示例

EXPLAIN SELECT * 
FROM orders o
JOIN customers c ON o.customer_id = c.id
JOIN addresses a ON c.address_id = a.id;

可能的执行顺序:

  1. (orders JOIN customers) → addresses

  2. (customers JOIN addresses) → orders

优化器会选择 预估结果集最小 的顺序。


5.3 半连接 (Semi-Join) 优化技术

背景

Semi-Join 指的是:

  • 只关心 是否存在匹配,而不是返回所有匹配行。

  • 常见于 EXISTSIN 子查询。

示例

SELECT * FROM customers c
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id);

优化器可能使用的 Semi-Join 策略

  • Materialization:先将子查询结果物化到临时表,再做 JOIN

  • Duplicate Weedout:JOIN 后去重,避免重复匹配

  • Loose Index Scan:直接用索引扫描子查询的去重结果

  • Batched Key Access (BKA):对子查询键进行批量索引探查


5.4 条件预过滤 (Condition Filtering)

背景

当 JOIN 的结果集很大时,如果过滤条件在后期才生效,会造成不必要的计算。
👉 条件预过滤就是把 WHERE 条件 尽量提前 到内表访问阶段。

示例

SELECT * FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE c.country = 'US';

优化过程

  • 如果 c.country 上有索引,优化器会在访问 customers 时就应用过滤条件

  • 避免将所有 customers 行拉入 JOIN_BUFFER,再做后置过滤

源码提示

sql/optimizer_costs.cc 中,优化器会为每个条件计算 过滤率 (filtering effect),用于判断条件能否下推。


5.5 本章小结

  • 子查询 JOIN 化:将 IN/EXISTS 子查询转换为 JOIN,提高执行效率

  • 多表 JOIN 顺序优化:优化器使用代价模型 + 动态规划,选择最优路径

  • 半连接优化:Materialization、Loose Index Scan、BKA 等方式减少不必要计算

  • 条件预过滤:将过滤条件尽量下推到内表,减少无效数据进入 JOIN_BUFFER

一个 用具体数据示例来展现 Join 的执行过程,包括:

  • 两个表的数据

  • 执行 Join 时,数据库如何扫描表

  • 数据怎么进入内存 buffer

  • 如何匹配、输出结果

那我们用一个最简单的 Hash Join 过程举例(文字流程图),涉及两个表:


表结构与数据

表A:订单表(orders)

order_id | customer_id
---------+------------1     |     1012     |     1023     |     103

表B:客户表(customers)

customer_id | name
------------+--------101     | Alice102     | Bob104     | Carol

我们执行 SQL:

SELECT o.order_id, c.name
FROM orders o
JOIN customers c
ON o.customer_id = c.customer_id;

文字版流程图(Hash Join示例)

【Step1】扫描表A (orders)-> 读取每一行(order_id, customer_id)-> 将(customer_id)作为key,放入Hash BufferBuffer内容:101 -> order_id=1102 -> order_id=2103 -> order_id=3【Step2】扫描表B (customers)-> 逐行读取(customer_id, name)-> 对每一行customer_id在Hash Buffer中查找匹配Row1: 101, Alice-> 在Buffer中找到 key=101-> 输出 (order_id=1, name=Alice)Row2: 102, Bob-> 在Buffer中找到 key=102-> 输出 (order_id=2, name=Bob)Row3: 104, Carol-> Buffer中没有 key=104-> 不输出【Step3】结果输出(1, Alice)(2, Bob)

总结:数据在内存 buffer 的流动

  1. Build 阶段:先把 小表A 加载到内存 buffer,构建哈希表。

  2. Probe 阶段:扫描 大表B,每行数据都去 buffer 查找匹配。

  3. 输出结果:匹配成功的数据组合后输出到结果集。

Block Nested Loop Join具体的例子

示例表

  • 表A (驱动表)

    A.id | A.name
    --------------
    1    | Alice
    2    | Bob
    3    | Carol
    

  • 表B (被驱动表,有索引B.a_id)

    B.id | B.a_id | B.city
    -----------------------
    10   | 1      | Beijing
    11   | 2      | Shanghai
    12   | 2      | Shenzhen
    13   | 3      | Guangzhou
    

查询语句:

SELECT A.id, A.name, B.city
FROM A
JOIN B ON A.id = B.a_id;

BKA Join 执行过程(文字版流程图)

开始
│
├─▶ Step1: 从表A批量取数据到 Buffer
│      取出 [ (1, Alice), (2, Bob), (3, Carol) ]
│
├─▶ Step2: 遍历 Buffer 中的数据,批量收集 join key
│      keys = [1, 2, 3]
│
├─▶ Step3: 去表B,用索引(B.a_id)批量查找 keys
│      - 查找 a_id=1 → 返回 (10,1,Beijing)
│      - 查找 a_id=2 → 返回 (11,2,Shanghai), (12,2,Shenzhen)
│      - 查找 a_id=3 → 返回 (13,3,Guangzhou)
│
├─▶ Step4: 将B表结果放入临时结果集,并与 Buffer 中A的行做匹配
│      匹配结果:
│        (1,Alice) ↔ (10,1,Beijing)
│        (2,Bob)   ↔ (11,2,Shanghai)
│        (2,Bob)   ↔ (12,2,Shenzhen)
│        (3,Carol) ↔ (13,3,Guangzhou)
│
└─▶ Step5: 输出结果集(1,Alice,Beijing)(2,Bob,Shanghai)(2,Bob,Shenzhen)(3,Carol,Guangzhou)

🔑 和普通 NLJ 的区别

  • 普通 NLJ:对 A 的每一行,立刻去 B 索引查一次 → 查索引次数 = A行数。

  • BKA:先把 A 的多行放进 Buffer,批量生成一组 join keys → 一次批量索引查找,大幅减少索引访问开销。

http://www.dtcms.com/a/344193.html

相关文章:

  • 微信扫码登陆 —— 接收消息
  • 关于日本服务器的三种线路讲解
  • 【Day01】堆与字符串处理算法详解
  • SHA 系列算法教程
  • C++ STL 中算法与具体数据结构分离的原理
  • Apache HTTP Server:深入探索Web世界的磐石基石!!!
  • SSM从入门到实战:2.5 SQL映射文件与动态SQL
  • C#中的LOCK
  • 关于 WebDriver Manager (自动管理浏览器驱动)
  • 第二阶段Winform-4:MDI窗口,布局控件,分页
  • 3.4 缩略词抽取
  • 企业级 AI 智能体安全落地指南:从攻击面分析到纵深防御体系构建
  • FileCodeBox 文件快递柜 一键部署
  • 获取后台返回的错误码
  • 如何使用命令行将DOCX文档转换为PDF格式?
  • Linux应用软件编程---网络编程1(目的、网络协议、网络配置、UDP编程流程)
  • Matplotlib 可视化大师系列(八):综合篇 - 在一张图中组合多种图表类型
  • 2.4G和5G位图说明列表,0xff也只是1-8号信道而已
  • QT QImage 判断图像无效
  • 高通平台WIFI学习-- 基于高通基线如何替换移植英飞凌WIFI芯片代码
  • mysql编程(简单了解)
  • 【Android】include复用布局 在xml中静态添加Fragment
  • 计数组合学7.20(平面分拆与RSK算法)
  • [测试技术] 接口测试中如何高效开展幂等性测试
  • pthon实现bilibili缓存视频音频分离
  • Redis内存碎片深度解析:成因、检测与治理实战指南
  • K8s存储类(StorageClass)设计与Ceph集成实战
  • 为什么应用会突然耗尽所有数据库连接
  • 智慧清洁时代来临:有鹿机器人重新定义城市清洁标准
  • 【数据结构】B 树——高度近似可”独木成林“的榕树——详细解说与其 C 代码实现