MySQL Block Nested-Loop Join(BNL)

本文介绍了MySQL中嵌套循环关联(NestedLoop)及其优化版本Block Nested Loop(BNL)的工作原理。BNL通过缓存外层循环的结果来减少内层循环的次数,进而提升多表关联查询的效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

5.5 版本之前,MySQL本身只支持一种表间关联方式,就是嵌套循环(Nested Loop)。如果关联表的数据量很大,则join关联的执行时间会非常长。在5.5以后的版本中,MySQL通过引入BNL算法来优化嵌套执行
Nested Loop Join
       NLJ 算法:将驱动表/外部表的结果集作为循环基础数据,然后循环从该结果集每次一条获取数据作为下一个表的过滤条件查询数据,然后合并结果。如果有多表join,则将前面的表的结果集作为循环数据,取到每行再到联接的下一个表中循环匹配,获取结果集返回给客户端。
Nested-Loop 的伪算法如下:

for each row in t1 matching range {
  for each row in t2 matching reference key {
     for each row in t3 {
      if row satisfies join conditions,
      send to client
    }
  }
 }

       因为普通Nested-Loop一次只将一行传入内层循环, 所以外层循环(的结果集)有多少行, 内存循环便要执行多少次.在内部表的连接上有索引的情况下,其扫描成本为O(Rn),若没有索引,则扫描成本为O(Rn*Sn)。如果内部表S有很多记录,则Simpl eNested-Loops Join会扫描内部表很多次,执行效率非常差。

Block Nested-Loop Join
       BNL 算法:将外层循环的行/结果集存入join buffer, 内层循环的每一行与整个buffer中的记录做比较,从而减少内层循环的次数.
       举例来说,外层循环的结果集是100行,使用NLJ 算法需要扫描内部表100次,如果使用BNL算法,先把对Outer Loop表(外部表)每次读取的10行记录放到join buffer,然后在InnerLoop表(内部表)中直接匹配这10行数据,内存循环就可以一次与这10行进行比较, 这样只需要比较10次,对内部表的扫描减少了9/10。所以BNL算法就能够显著减少内层循环表扫描的次数.
       前面描述的query, 如果使用join buffer, 那么实际join示意如下:

for each row in t1 matching range {
   for each row in t2 matching reference key {
    store used columns from t1, t2 in join buffer
    if buffer is full {
      for each row in t3 {
         for each t1, t2 combination in join buffer {
          if row satisfies join conditions,
          send to client
        }
       }
      empty buffer
    }
  }
}

if buffer is not empty {
   for each row in t3 {
    for each t1, t2 combination in join buffer {
      if row satisfies join conditions,
      send to client
     }
  }
}

       如果t1, t2参与join的列长度只和为s, c为二者组合数, 那么t3表被扫描的次数为
(S * C)/join_buffer_size + 1
扫描t3的次数随着join_buffer_size的增大而减少, 直到join buffer能够容纳所有的t1, t2组合, 再增大join buffer size, query 的速度就不会再变快了

 

  • MySQL使用Join Buffer有以下要点:

1. join_buffer_size变量决定buffer大小。
2. 只有在join类型为all, index, range的时候才可以使用join buffer。
3. 能够被buffer的每一个join都会分配一个buffer, 也就是说一个query最终可能会使用多个join buffer。
4. 第一个nonconst table不会分配join buffer, 即便其扫描类型是all或者index。
5. 在join之前就会分配join buffer, 在query执行完毕即释放。
6. join buffer中只会保存参与join的列, 并非整个数据行。

 

  • 如何使用

       5.6版本及以后,优化器管理参数optimizer_switch中中的block_nested_loop参数控制着BNL是否被用于优化器。默认条件下是开启,若果设置为off,优化器在选择 join方式的时候会选择NLJ算法。

 

转载于:https://www.cnblogs.com/vadim/p/7403728.html

### 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 JoinBNL)**: 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、付费专栏及课程。

余额充值