join大小表傻傻分不清楚~那就看过来吧~~

这是我参与11月更文挑战的第23天,活动详情查看:2021最后一次更文挑战

join如何选择驱动表和被驱动表,大小表怎么选??

其实平时两张表做join时,我并不会考虑那么多,表和表之间做关联,取对应的字段join一下就好了,但今天在学习了李玥老师的专栏后,其实这后面关系到效率的问题,要是没有处理好,对性能将会造成很大的影响,接下来我们一起来探究,如果有两个大小不同的表做join到底是怎么执行的呢?

create Table `t1` (
    `id` int(11) NOT NULL,
    `a` int(11) DEFAULT NULL,
    `b` int(11) DEFAULT NULL,
    PRIMARY KEY (`id`),
    KEY `a` (`a`)
)ENGINE=InnoDB;

复制代码
create Table `t2` (
    `id` int(11) NOT NULL,
    `a` int(11) DEFAULT NULL,
    `b` int(11) DEFAULT NULL,
    PRIMARY KEY (`id`),
    KEY `a` (`a`)
)ENGINE=InnoDB;

复制代码

t1和t2表,a字段上有索引,b字段上没有索引,我们往t1表插入100条数据,t2表插入1000行数据。

我们看如下的sql:

select * from t1 stright_join t2 on (t1.a = t2.a);
复制代码

如果我们直接用join,MySQL优化器可能会帮我们选择t1或者t2作为驱动表,但为了我们能够更直观地研究,我们用straight_join让MySQL使用固定的连接方式来执行语句,也就是说,前面的t1表是驱动表,t2表是被驱动表。

我们看下explain的结果可以看出,join的过程中,我们使用了t2表的索引a。

截屏2021-11-25 下午11.57.56.png

执行过程如下所示:

  1. 从t1读入一行数据 R
  2. 从R中取出字段a到t2去查找
  3. 取出t2表中满足条件的行,和 R组成一行,加入结果集
  4. 重复步骤1和3,直到检索到t1的最后一行为止

在这个过程中,我们先遍历t1,然后然后根据t1表中取出的数据,去t2表中查找满足条件的记录,因为用到了t2表的索引,所以我们称之为“index Nested-Loop Join”,NLJ

“index Nested-Loop Join”

我们再来回顾下这个流程:

  1. 对驱动表t1做全表扫描,也就是扫描了100行
  2. 对于每一行的R,根据字段a去t2中扫描,因为a在t2中有索引,我们知道,索引的结构其实就是树结构,所以走的是树搜索过程,因此每次的搜索过程只需要扫描一行,所以对于t2表来说,总共扫描100行
  3. 所以t1的100行加上t2的100行,我们总共扫描了200行数据。

如果我们把上面的流程转换成用伪代码来实现是什么样的呢?

List<object> a_list = SQL(select * from t1)
for(a_list.size) {
    objectB = select * from t2 where a = a_list.a
    把返回的objectB和a_list[i]组成结果集的一行
}
// 其实这个写法很像salesForce里的SOQL,大家能看懂啥意思就行。
复制代码

我们可以看到,在这个查询的过程中,我们扫描了200行(t1扫描了100行,t2扫描了100行),但是总共执行了101条语句,对t1只执行了一条sql查询出了所有数据,然后执行100次对t2的查询。对比可见,如果我们用join的话,我们只需要执行一次sql,而用程序来做查询的话,我们需要多执行100次sql。

那我们回归正题,我们应该怎么选择驱动表呢,接着来分析最开始的这条sql。

select * from t1 stright_join t2 on (t1.a = t2.a);
复制代码

这条语句中,驱动表是走全表扫描,而被驱动表走的是树的索引。假设被驱动表的行数是M,每次在被驱动表查一行数据,我们要先搜索索引a,因为a是辅助索引,所以我们需要回表到主键索引上再次搜索,我们知道,检索一棵树的复杂度是以2为底的M的对数log2M,因为我们要查找两棵树,所以时间乘2,为2log2M。

假设驱动表的行数是N,因为要全表扫描,所以要扫描驱动表N行,每一行数据,又要到被驱动表上匹配一次,所以复杂度为 N + N*2log2M (驱动表的扫描次数 + 驱动表的每一行都要在被驱动表的两棵数上做对比查询也就是N*(上面我们得出的查询两棵树的复杂度2log2M))

根据N + N*2log2M我们可以知道,N越大,复杂度越高,假设N扩大1000倍,那么扫描行数就会扩大1000倍,但是M扩大1000倍,扫描的行数只会扩大10倍,所以应该让小表来做驱动表。 但是该结论的前提是:“可以使用到被驱动表上的索引”

那如果被驱动表上的索引我们用不到,会发生什么情况呢?

我们改下sql

 select * from t1 straight_join t2 on (t1.a = t2.b);
复制代码

因为t2表的字段b没有索引,所以我们在去t2表做匹配的时候,需要做一次全表扫描。所以扫描t1表100行,扫描出来的每一行,又要去t2表做全表扫描,总共是100*1000=100000行。这个算法叫 “simple Nested- Loop Join”,但我们很容易发现,这种算法太笨了。

MySQL使用了另一种算法,叫做 “Block Nested-Loop Join”

“Block Nested-Loop Join”

这时候,被驱动表上没有可用的索引,算法的流程变成这样:

  1. 把select * from t1的数据读如线程内存 join_buffer内,相当于把整个t1表放入了内存。
  2. 扫描t2,把t2中的每一行都拿出来和t1进行对比,满足的加入结果集。

C01AED95-A9BD-4FF8-9D3D-9031757A1F92.png

我们可以从explain中看到,两张表都没有用到索引,所以需要做全表扫描,扫描的行数是t1的100行加上t2的1000行,1100行。但是每次从t2中扫描出来的一行,都需要和内存中t1的数据做对比,也就是1000*100=100000次判断。结果和上面我们说的笨笨的 “simple Nested- Loop Join” 是一样的,但是 “Block Nested-Loop Join” 因为是在内存中进行比对,所以速度上会快很多。

那我们在没用到被驱动表上的索引时,应该怎么选驱动表呢?

我们假设小表的行数是N,大表是M,那么

  1. 两个表都做一次全表扫描,所以总的扫描的行数是M+N
  2. 内存的判断次数是M*N

所以此时,不管选谁做大表小表驱动表,结果都是一样的,因为我们在 “Block Nested-Loop Join” 中运用到了join_buffer,我们会想到,那如果join_buffer放不下数据怎么办。

join_buffer大小可以由join_buffer_size来控制,默认值是256k,要是放不下的话,就分段来放,我们如果把join_buffer_size的值改为1200,再执行

select * from t1 stright_join t2 on (t1.a = t2.a);
复制代码

执行的过程就会变成:

  1. 扫描t1,按顺序读取数据放入join_buffer中,假如说放到88行join_buffer放满了
  2. 扫描t2,把t2中的每一行都取出来,跟join_buffer中的数据做对比,满足的放入结果集
  3. 清空join_buffer
  4. 继续扫描t1,顺序读取最后的12行数据放入join_buffer,然后继续执行步骤2

这也就是 “Block Nested-Loop Join”Block 的含义 分段

虽然我们把t1的数据分成两次放入join_buffer,但是我们总的判断次数还是没变,依旧是(88+12)*1000=100000次。

那我们在这种分段的情况下,应该如何选择驱动表呢?

假设驱动表的行数是N,需要分成K次放入join_buffer,被驱动表的行数是M。因为我们的join_buffer是固定不变的大小,所以我们可以把 K 表示为 λ*N ,因为驱动表的行数是N,我们就算每次分段的大小是1,也不会超过N,所以 λ的大小就在(0,1)。

所以,在执行的过程中:

  1. 扫描的行数就是N+λNM(驱动表所有行都扫描存入join_buffer中 + 分成的断数*被驱动表的行数)
  2. 内存判断次数 N*M

所以,内存的判断次数和驱动表和被驱动表的大小都没关系,但是扫描的行数和N有关。并且在N的大小固定时,其实就是和λ的大小有关,那我们应该尽量让λ的大小越小越好,那就是让join_buffer_size的值越大,一次可以放更多的行,那分段的次数自然就下去了。

所以结论是,应该让小表来做驱动表,并且改大join_buffer_size的大小。

最后,我们来探讨一个问题,什么是“小表”

难道行数越小的表就是小表吗?不见得如此,如果我们在sql中再加上对驱动表的限制,例如:

select * from t1 straight_join t2 on (t1.b=t2.b) where t2.id<=50; 
select * from t2 straight_join t1 on (t1.b=t2.b) where t2.id<=50;
复制代码

上面这两条sql都没有用上索引,但是第二行中,位于驱动表位置的t2限制了只有前50行,所以我们只需要将这50行数据放入join_buffer中,显然这么做更好,所以这里的t2的前50行,就是 “小表”。

再看下面这两条sql

select t1.b,t2.* from t1 straight_join t2 on (t1.b=t2.b) where t2.id<=100; 2 select t1.b,t2.* from t2 straight_join t1 on (t1.b=t2.b) where t2.id<=100;
复制代码

这个例子里,表 t1 和 t2 都是只有 100 行参加 join。但是,这两条语句每次查询放入 join_buffer 中的数据是不一样的:

  • 表 t1 只查字段 b,因此如果把 t1 放到 join_buffer 中,则 join_buffer 中只需要放入 b 的值;

  • 表 t2 需要查所有的字段,因此如果把表 t2 放到 join_buffer 中的话,就需要放入三个字 段 id、a 和 b。

这里,我们应该选择表 t1 作为驱动表。也就是说在这个例子里,“只需要一列参与 join 的 表 t1”是那个相对小的表。

在决定哪个表做驱动表的时候,应该是两个表按照各自的条件过滤,过滤完成之后,计算参与 join 的各个字段的总数据量,数据量小的那个表,就是“小表”, 应该作为驱动表。

小结

  1. “index Nested-Loop Join” ,也就是有用到被驱动表的索引,让小表来做驱动表

  2. “Block Nested-Loop Join” ,在没用到被驱动表上的索引时,应该让小表来做驱动表,并且改大join_buffer_size的大小

反正就是在使用 join 的时候,应该让小表做驱动表。

おすすめ

転載: juejin.im/post/7034536186614186014