MySQL中的join语句算法如何优化

52次阅读
没有评论

共计 5702 个字符,预计需要花费 15 分钟才能阅读完成。

本篇内容主要讲解“MySQL 中的 join 语句算法如何优化”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让丸趣 TV 小编来带大家学习“MySQL 中的 join 语句算法如何优化”吧!

一、join 语句算法

创建两个表 t1 和 t2

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;
CREATE DEFINER=`root`@`%` PROCEDURE `idata`()
BEGIN
 declare i int;
 set i=1;
 while(i =1000)do
 insert into t2 values(i, i, i);
 set i=i+1;
 end while;
create table t1 like t2;
insert into t1 (select * from t2 where id =100);

这两个表都有一个主键索引 id 和一个索引 a,字段 b 上无索引。存储过程 idata() 往表 t2 里插入了 1000 行数据,在表 t1 里插入的是 100 行数据

1、Index Nested-Loop Join

select * from t1 straight_join t2 on (t1.a=t2.a);

如果直接使用 join 语句,MySQL 优化器可能会选择表 t1 或 t2 作为驱动表,通过 straight_join 让 MySQL 使用固定的连接方式执行查询,在这个语句里,t1 是驱动表,t2 是被驱动表

被驱动表 t2 的字段 a 上有索引,join 过程用上了这个索引,因此这个语句的执行流程是这样的:

1. 从表 t1 中读入一行数据 R

2. 从数据行 R 中,取出 a 字段到表 t2 里去查找

3. 取出表 t2 中满足条件的行,跟 R 组成一行,作为结果集的一部分

4. 重复执行步骤 1 到 3,直到表 t1 的末尾循环结束

这个过程可以用上被驱动表的索引,称之为 Index Nested-Loop Join,简称 NLJ

在这个流程里:

1. 对驱动表 t1 做了全表扫描,这个过程需要扫描 100 行

2. 而对于每一行 R,根据 a 字段去表 t2 查找,走的是树搜索过程。由于我们构造的数据都是一一对应的,因此每次的搜索过程都只扫描一行,也是总共扫描 100 行

3. 所以,整个执行流程,总扫描行数是 200

假设不使用 join,只能用单表查询:

1. 执行 select * from t1,查出表 t1 的所有数据,这里有 100 行

2. 循环遍历这 100 行数据:

从每一行 R 取出字段 a 的值 $R.a

执行 select * from t2 where a=$R.a

把返回的结果和 R 构成结果集的一行

这个查询过程,也是扫描了 200 行,但是总共执行了 101 条语句,比直接 join 多了 100 次交互。客户端还要自己拼接 SQL 语句和结果。这么做还不如直接 join 好

在可以使用被驱动表的索引的情况下:

使用 join 语句,性能比强行拆成多个单表执行 SQL 语句的性能要好

如果使用 join 语句的话,需要让小表做驱动表

2、Simple Nested-Loop Join

select * from t1 straight_join t2 on (t1.a=t2.b);

由于表 t2 的字段 b 上没有索引,因此每次到 t2 去匹配的时候,就要做一次全表扫描。这个算法叫做 Simple Nested-Loop Join

这样算来,这个 SQL 请求就要扫描表 t2 多达 100 次,总共扫描 100*100=10 万行

MySQL 没有使用这个 Simple Nested-Loop Join 算法,而是使用了另一个叫作 Block Nested-Loop Join 的算法,简称 BNL

3、Block Nested-Loop Join

被驱动表上没有可用的索引,算法的流程如下:

1. 把表 t1 的数据读入线程内存 join_buffer 中,由于这个语句中写的是 select *,因此是把整个表 t1 放入了内存

2. 扫描表 t2,把表 t2 中的每一行取出来,跟 join_buffer 中的数据作比对,满足 join 条件的,作为结果集的一部分返回

在这个过程中,对表 t1 和表 t2 都做了一次全表扫描,因此总的扫描行数是 1100。由于 join_buffer 是以无序数组的方式组织的,因此对表 t2 中的每一行,都要做 100 次判断,总共需要在内存中做的判断次数是 100*1000=10 万次

使用 Simple Nested-Loop Join 算法进行查询,扫描行数也是 10 万行。因此,从时间复杂度上来说,这两个算法是一样的。但是,Block Nested-Loop Join 算法的这 10 万次判断是内存操作,速度上会快很多,性能也更好

假设小表的行数是 N,大表的行数是 M,那么在这个算法里:

1)两个表都做一次全表扫描,所以总的扫描行数是 M + N

2)内存中的判断次数是 M ∗ N

这时候选择大表还是小表做驱动表,执行耗时是一样的

join_buffer 的大小是由参数 join_buffer_size 设定的,默认值是 256k。如果放不下表 t1 的所有数据话,策略很简单,就是分段放

1)扫描表 t1,顺序读取数据行放入 join_buffer 中,假设放到第 88 行 join_buffer 满了

2)扫描表 t2,把 t2 中的每一行取出来,跟 join_buffer 中的数据做对比,满足 join 条件的,作为结果集的一部分返回

3)清空 join_buffer

4)继续扫描表 t1,顺序读取最后的 12 行放入 join_buffer 中,继续执行第 2 步

由于表 t1 被分成了两次放入 join_buffer 中,导致表 t2 会被扫描两次。虽然分成两次放入 join_buffer,但是判断等值条件的此时还是不变的

假设,驱动表的数据行数是 N,需要分成 K 段才能完成算法流程,被驱动表的数据行数是 M。这里的 K 不是常数,N 越大 K 就会越大,因此把 K 表示为 λ ∗ N,λ 的取值范围是 (0,1)。所以,在这个算法的执行过程中:

1. 扫描行数是 N + λ ∗ N ∗ M

2. 内存判断 N ∗ M

考虑到扫描行数,N 小一些,整个算式的结果会更小。所以应该让小表当驱动表

4、能不能使用 join 语句?

1. 如果可以使用 Index Nested-Loop Join 算法,也就是说可以用上被驱动表上的索引,其实是没问题的

2. 如果使用 Block Nested-Loop Join 算法,扫描行数就会过多。尤其是在大表上的 join 操作,这样可能要扫描被驱动表很多次,会占用大量的系统资源。所以这种 join 尽量不要用

5、如果使用 join,应该选择大表做驱动表还是选择小表做驱动表

1. 如果是 Index Nested-Loop Join 算法,应该选择小表做驱动表

2. 如果是 Block Nested-Loop Join 算法:

在 join_buffer_size 足够大的时候,是一样的

在 join_buffer_size 不够大的时候,应该选择小表做驱动表

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

二、join 语句优化

创建两个表 t1、t2

create table t1(id int primary key, a int, b int, index(a));
create table t2 like t1;
CREATE DEFINER = CURRENT_USER PROCEDURE `idata`()
BEGIN
 declare i int;
 set i=1;
 while(i =1000)do
 insert into t1 values(i, 1001-i, i);
 set i=i+1;
 end while;
 
 set i=1;
 while(i =1000000)do
 insert into t2 values(i, i, i);
 set i=i+1;
 end while;
END;

在表 t1 中,插入了 1000 行数据,每一行的 a =1001-id 的值。也就是说,表 t1 中字段 a 是逆序的。同时,在表 t2 中插入了 100 万行数据

1、Multi-Range Read 优化

Multi-Range Read(MRR)优化主要的目的是尽量使用顺序读盘

select * from t1 where a =1 and a =100;

主键索引是一棵 B + 树,在这棵树上,每次只能根据一个主键 id 查到一行数据。因此,回表是一行行搜索主键索引的

如果随着 a 的值递增顺序查找的话,id 的值就变成随机的,那么就会出现随机访问,性能相对较差

因为大多数的数据都是按照主键递增顺序插入得到的,所以如果按照主键的递增顺序查询,对磁盘的读比较接近顺序读,能够提升读性能

这就是 MRR 优化的设计思路,语句的执行流程如下:

1. 根据索引 a,定位到满足条件的记录,将 id 值放入 read_rnd_buffer 中

2. 将 read_rnd_buffer 中的 id 进行递增排序

3. 排序后的 id 数组,依次到主键 id 索引中查记录,并作为结果返回

read_rnd_buffer 的大小是由 read_rnd_buffer_size 参数控制的。如果步骤 1 中,read_rnd_buffer 放满了,就会先执行完步骤 2 和 3,然后清空 read_rnd_buffer。之后继续找索引 a 的下个记录,并继续循环

如果想要稳定地使用 MRR 优化的话,需要设置 set optimizer_switch= mrr_cost_based=off

explain 结果中,Extra 字段多了 Using MRR,表示的是用上了 MRR 优化。由于在 read_rnd_buffer 中按照 id 做了排序,所以最后得到的结果也是按照主键 id 递增顺序的

MRR 能够提升性能的核心在于,这条查询语句在索引 a 上做的是一个范围查询,可以得到足够多的主键 id。这样通过排序以后,再去主键索引查数据,才能体现出顺序性的优势

2、Batched Key Access

MySQL5.6 引入了 Batched Key Access(BKA)算法。这个 BKA 算法是对 NLJ 算法的优化

NLJ 算法流程图:

MySQL 中的 join 语句算法如何优化

NLJ 算法执行的逻辑是从驱动表 t1,一行行地取出 a 的值,再到被驱动表 t2 去做 join

BKA 算法流程图:

MySQL 中的 join 语句算法如何优化

BKA 算法执行的逻辑是把表 t1 的数据取出来一部分,先放到一个 join_buffer,一起传给表 t2。在 join_buffer 中只会放入查询需要的字段,如果 join_buffer 放不下所有数据,就会将数据分成多段执行上图的流程

如果想要使用 BKA 优化算法的话,执行 SQL 语句之前,先设置

set optimizer_switch= mrr=on,mrr_cost_based=off,batched_key_access=on

其中前两个参数的作用是启用 MRR,原因是 BKA 算法的优化要依赖与 MRR

3、BNL 算法的性能问题

InnoDB 对 Buffer Pool 的 LRU 算法做了优化,即:第一次从磁盘读入内存的数据页,会先放在 old 区域。如果 1 秒之后这个数据页不再被访问了,就不会被移动到 LRU 链表头部,这样对 Buffer Pool 的命中率影响就不大

如果一个使用 BNL 算法的 join 语句,多次扫描一个冷表,而且这个语句执行时间超过 1 秒,就会在再次扫描冷表的时候,把冷表的数据页移到 LRU 链表头部。这种情况对应的,是冷表的数据量小于整个 Buffer Pool 的 3 /8,能够完全放入 old 区域的情况

如果这个冷表很大,就会出现另外一种情况:业务正常访问的数据页,没有机会进入 young 区域。

由于优化机制的存在,一个正常访问的数据页,要进入 young 区域,需要隔 1 秒后再次被访问到。但是,由于 join 语句在循环读磁盘和淘汰内存页,进入 old 区域的数据页,很可能在 1 秒之内就被淘汰了。这样就会导致 MySQL 实例的 Buffer Pool 在这段时间内,young 区域的数据页没有被合理地淘汰

BNL 算法对系统的影响主要包括三个方面:

1. 可能会多次扫描被驱动表,占用磁盘 IO 资源

2. 判断 join 条件需要执行 M∗N 次对比,如果是大表就会占用非常多的 CPU 资源

3. 可能会导致 Buffer Pool 的热数据被淘汰,影响内存命中率

4、BNL 转 BKA

一些情况下,我们可以直接在被驱动表上建索引,这时就可以直接转成 BKA 算法了

如果碰到一些不适合在被驱动表上建索引的情况,可以考虑使用临时表。大致思路如下:

select * from t1 join t2 on (t1.b=t2.b) where t2.b =1 and t2.b =2000;

1)把表 t2 中满足条件的数据放在临时表 tmp_t 中

2)为了让 join 使用 BKA 算法,给临时表 tmp_t 的字段 b 加上索引

3)让表 t1 和 tmp_t 做 join 操作

SQL 语句写法如下:

create temporary table temp_t(id int primary key, a int, b int, index(b))engine=innodb;insert into temp_t select * from t2 where b =1 and b =2000;select * from t1 join temp_t on (t1.b=temp_t.b);

5、扩展 hash join

MySQL 的优化器和执行器不支持哈希 join,可以自己实现在业务端,实现流程大致如下:

1.select * from t1; 取得表 t1 的全部 1000 行数据,在业务端存入一个 hash 结构

2.select * from t2 where b =1 and b =2000; 获取表 t2 中满足条件的 2000 行数据

3. 把这 2000 行数据,一行一行地取到业务端,到 hash 结构的数据表中寻找匹配的数据。满足匹配的条件的这行数据,就作为结果集的一行

到此,相信大家对“MySQL 中的 join 语句算法如何优化”有了更深的了解,不妨来实际操作一番吧!这里是丸趣 TV 网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-07-13发表,共计5702字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)