join表连接的三种算法思想:Nested-Loop Join和Index Nested-Loop Join和Block Nested-Loop Join和BKA

一.Nested-Loop Join

在Mysql中,使用Nested-Loop Join的算法思想去优化join,Nested-Loop Join翻译成中文则是“嵌套循环连接”。

举个例子:
select * from t1 inner join t2 on t1.id=t2.tid
(1)t1称为外层表,也可称为驱动表。
(2)t2称为内层表,也可称为被驱动表。

在Mysql的实现中,Nested-Loop Join有3种实现的算法:

  • Simple Nested-Loop Join:SNLJ,简单嵌套循环连接
  • Index Nested-Loop Join:INLJ,索引嵌套循环连接
  • Block Nested-Loop Join:BNLJ,缓存块嵌套循环连接

在选择Join算法时,会有优先级,理论上会优先判断能否使用INLJ、BNLJ:
Index Nested-LoopJoin > Block Nested-Loop Join > Simple Nested-Loop Join

二.Simple Nested-Loop

  1. 简单嵌套循环连接实际上就是简单粗暴的嵌套循环,如果table1有1万条数据,table2有1万条数据,那么数据比较的次数=1万 * 1万 =1亿次,这种查询效率会非常慢。
  2. 所以Mysql继续优化,然后衍生出Index Nested-LoopJoin、Block Nested-Loop Join两种NLJ算法。在执行join查询时mysql会根据情况选择两种之一进行join查询。

三.Index Nested-LoopJoin(减少内层表数据的匹配次数)

  1. 索引嵌套循环连接是基于索引进行连接的算法,索引是基于内层表的,通过外层表匹配条件直接与内层表索引进行匹配,避免和内层表的每条记录进行比较, 从而利用索引的查询减少了对内层表的匹配次数,优势极大的提升了 join的性能:

原来的匹配次数 = 外层表行数 * 内层表行数
优化后的匹配次数= 外层表的行数 * 内层表索引的高度

  1. 使用场景:只有内层表join的列有索引时,才能用到Index Nested-LoopJoin进行连接。
  2. 由于用到索引,如果索引是辅助索引而且返回的数据还包括内层表的其他数据,则会回内层表查询数据,多了一些IO操作。

四.Block Nested-Loop Join(减少内层表数据的循环次数)

  1. 缓存块嵌套循环连接通过一次性缓存多条数据,把参与查询的列缓存到Join Buffer 里,然后拿join buffer里的数据批量与内层表的数据进行匹配,从而减少了内层循环的次数(遍历一次内层表就可以批量匹配一次Join Buffer里面的外层表数据)。
  2. 当不使用Index Nested-Loop Join的时候,默认使用Block Nested-Loop Join。
  3. 什么是Join Buffer?
    (1)Join Buffer会缓存所有参与查询的列而不是只有Join的列。
    (2)可以通过调整join_buffer_size缓存大小
    (3)join_buffer_size的默认值是256K,join_buffer_size的最大值在MySQL 5.1.22版本前是4G-1,而之后的版本才能在64位操作系统下申请大于4G的Join Buffer空间。
    (4)使用Block Nested-Loop Join算法需要开启优化器管理配置的optimizer_switch的设置block_nested_loop为on,默认为开启。

五.BKA

     Batched Key Access Join算法的工作步骤如下:

1) 将外部表中相关的列放入Join Buffer中。

2) 批量的将Key(索引键值)发送到Multi-Range Read(MRR)接口

3) Multi-Range Read(MRR)通过收到的Key,根据其对应的ROWID进行排序,然后再进行数据的读取操作。

 

4) 返回结果集给客户端。

六.如何优化Join速度

  1. 用小结果集驱动大结果集,减少外层循环的数据量:
    如果小结果集和大结果集连接的列都是索引列,mysql在内连接时也会选择用小结果集驱动大结果集,因为索引查询的成本是比较固定的,这时候外层的循环越少,join的速度便越快。
  2. 为匹配的条件增加索引:争取使用INLJ,减少内层表的循环次数
  3. 增大join buffer size的大小:当使用BNLJ时,一次缓存的数据越多,那么外层表循环的次数就越少
  4. 减少不必要的字段查询:
    (1)当用到BNLJ时,字段越少,join buffer 所缓存的数据就越多,外层表的循环次数就越少;
    (2)当用到INLJ时,如果可以不回表查询,即利用到覆盖索引,则可能可以提示速度。(未经验证,只是一个推论)
### MySQL将小作为驱动的IO角度深度解析 在MySQL中,将数据量小的作为驱动(即JOIN操作中`FROM`后的第一个)能显著提升性能,**核心原因在于减少磁盘I/O操作次数**。以下是具体机制分析: --- ### 一、JOIN操作的基本I/O流程 MySQL执行JOIN时采用 **Nested-Loop Join(NLJ)算法**,其I/O过程分为两步: 1. **读取驱动数据**:将驱动数据加载到内存 2. **匹配被驱动数据**:逐行用驱动数据扫描被驱动 ```mermaid graph LR A[驱动] -->|逐行读取| B[内存] B -->|用每行数据| C[扫描被驱动] C -->|磁盘I/O| D[返回匹配行] ``` --- ### 二、小驱动的I/O优势原理 #### 场景1:被驱动索引Index Nested-Loop Join) 假设: - 驱动 $A$:$M$ 行,占 $P_A$ 个磁盘页 - 被驱动 $B$:$N$ 行,占 $P_B$ 个磁盘页($P_B \gg P_A$) - $B$ 连接字段有索引(B+树高度 $H$) | 驱动选择 | I/O 次数公式 | 关键说明 | |----------------|----------------------------------|----------| | **小A驱动** | $P_A + M \times (H + 1)$ | 1. 加载 $A$ :$P_A$ 次 I/O<br>2. 每行 $A$ 数据通过索引查 $B$:$H$(索引访问)+1(数据页访问) | | **大B驱动** | $P_B + N \times (H + 1)$ | 1. 加载 $B$ :$P_B$ 次 I/O(代价更高)<br>2. 每行 $B$ 数据查 $A$:索引高度 $H$ 相同 | **优势体现**: 当 $M \ll N$ 且 $P_A \ll P_B$ 时(小驱动): - **磁盘扫描量减少**:$P_A$ 远小于 $P_B$(如 10页 vs 1000页) - **索引查找次数降低**:$M \times (H+1) \ll N \times (H+1)$ - **缓存利用率高**:小更易完整载入内存(Buffer Pool),避免反复磁盘I/O > 📌 **案例**:$M=1000, N=1e6, H=3, P_A=10, P_B=1000$ > -驱动:$10 + 1000 \times 4 = 4,010$ 次I/O > -驱动:$1000 + 10^6 \times 4 = 4,001,000$ 次I/O > **性能差距约1000倍!** #### 场景2:被驱动索引Block Nested-Loop Join) 当被驱动索引时,MySQL使用 **Block Nested-Loop Join(BNL)**: 1. 将驱动分块加载到 `join_buffer`(大小由 `join_buffer_size` 决定) 2. 全扫描被驱动与内存中的块匹配 **I/O 成本公式**: $$ \text{Total I/O} = P_A + \left \lceil \frac{P_A \times \text{Page\_Size}}{\text{join\_buffer\_size}} \right \rceil \times P_B $$ **小驱动的优势**: - 若 $P_A$ 足够小($\text{join\_buffer\_size} \geq P_A \times \text{Page\_Size}$): - 驱动可一次性加载到内存 - **只需1次全扫描 $B$ **:$Total I/O = P_A + P_B$ - 若大驱动: - 需分多次加载($\left \lceil \frac{P_B \times \text{Page\_Size}}{\text{join\_buffer\_size}} \right \rceil$ 增大) - **多次全扫描 $A$ **:$Total I/O = P_B + K \times P_A$($K$ 为分块数) > ⚠️ **极端案例**:$P_A=100, P_B=1000, \text{join\_buffer\_size}=1\text{MB}, \text{Page\_Size}=16\text{KB}$ > -驱动:分块数 $K= \lceil (100×16)/1024 \rceil=2$ → $100 + 2×1000=2,100$ 次I/O > -驱动:$K= \lceil (1000×16)/1024 \rceil=16$ → $1000 + 16×100=2,600$ 次I/O > 差距:**500次额外I/O**(约8MB磁盘读取) --- ### 三、其他I/O相关优化效应 1. **减少锁竞争**: -扫描时间短,锁定资源更快释放 -作为被驱动时,扫描过程可触发**MRR优化**(Multi-Range Read),将随机I/O转为顺序I/O 2. **预热缓存效应**: - 频繁访问的小常驻Buffer Pool - 后续JOIN操作直接命中内存,避免磁盘I/O 3. **顺序访问优势**: -驱动时,被驱动索引扫描更可能触发**Batched Key Access(BKA)** - 将随机I/O合并为顺序I/O(尤其对SSD重要) --- ### 四、优化器如何选择驱动MySQL优化器通过成本模型自动选择: ```sql EXPLAIN SELECT ... FROM table_A JOIN table_B ...; ``` 优化器优先考虑: 1. **大小**(通过 `SHOW TABLE STATUS` 估算) 2. **可用索引** 3. **连接缓冲区大小** 4. **I/O成本权重**(通常占总体成本70%+) > 💡 可通过 `STRAIGHT_JOIN` 强制驱动顺序: > ```sql > SELECT ... FROM small_table STRAIGHT_JOIN large_table ... > ``` --- ### 结论:为什么小驱动优化I/O? | 关键机制 | 效果 | |------------------------|--------------------------| | **减少驱动加载I/O** | 小占磁盘页少($P_A↓$) | | **降低匹配过程I/O** | 索引查找次数少($M↓$) | | **避免多次扫描大** | BNL算法分块数减少 | | **提高缓存利用率** | 小常驻内存 | **终极建议**: 1. **小作为驱动** 2. **被驱动连接字段必须建索引** 3. 适当增大 `join_buffer_size`(默认256KB) 4. 定期执行 `ANALYZE TABLE` 更新统计信息 --- ### 附录:I/O成本计算工具 ```sql -- 查看数据页数量 SELECT table_name, ROUND((data_length + index_length) / 1024 / 16) AS pages -- InnoDB默认16KB/页 FROM information_schema.TABLES WHERE table_schema = 'your_db'; ``` 能不能详细解释一下这个sql
08-07
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值