浅谈Mysql多表连接查询的执行细节


Posted in MySQL onApril 24, 2021

先构建本篇博客的案列演示表:

create table a(a1 int primary key, a2 int ,index(a2));  		--双字段都有索引
create table c(c1 int primary key, c2 int ,index(c2), c3 int);  --双字段都有索引
create table b(b1 int primary key, b2 int);						--有主键索引
create table d(d1 int, d2 int); 								--没有索引

insert into a values(1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8),(9,9),(10,10);
insert into b values(1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8),(9,9),(10,10);
insert into c values(1,1,1),(2,4,4),(3,6,6),(4,5,5),(5,3,3),(6,3,3),(7,2,2),(8,8,8),(9,5,5),(10,3,3);  
insert into d values(1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8),(9,9),(10,10);

驱动表如何选择?

驱动表的概念是指多表关联查询时,第一个被处理的表,使用此表的记录去关联其他表。驱动表的确定很关键,会直接影响多表连接的关联顺序,也决定了后续关联时的查询性能。

驱动表的选择遵循一个原则:在对最终结果集没影响的前提下,优先选择结果集最小的那张表作为驱动表。改变驱动表就意味着改变连接顺序,只有在不会改变最终输出结果的前提下才可以对驱动表做优化选择。在外连接情况下,很多时候改变驱动表会对输出结果有影响,比如left join的左边表和right join的右边表,驱动表选择join的左边或者右边最终输出结果很有可能会不同。

用结果集来选择驱动表,那结果集是什么?如何计算结果集?mysql在选择前会根据where里的每个表的筛选条件,相应的对每个可作为驱动表的表做个结果记录预估,预估出每个表的返回记录行数,同时再根据select里查询的字段的字节大小总和做乘积:

每行查询字节数 * 预估的行数 = 预估结果集

通过where预估结果行数,遵循以下规则:

  • 如果where里没有相应表的筛选条件,无论on里是否有相关条件,默认为全表
  • 如果where里有筛选条件,但是不能使用索引来筛选,那么默认为全表
  • 如果where里有筛选条件,而且可以使用索引,那么会根据索引来预估返回的记录行数

我们以上述创建的表为基础,用如下sql作为案列来演示:

select a.*,c.c2 from a join c on a.a2=c.c2 where a.a1>5 and c.c1>5;

通过explain查看其执行计划:

浅谈Mysql多表连接查询的执行细节

explain显示结果里排在第一行的就是驱动表,此时表c为驱动表。

如果将sql修改一下,将select 里的条件c.c2 修改为 c.*

select a.*,c.* from a join c on a.a2=c.c2 where a.a1>5 and c.c1>5;

通过explain查看其执行计划:

浅谈Mysql多表连接查询的执行细节

此时驱动表还是c,按理来说 c.* 的数据量肯定是比 a.*大的,似乎结果集大小的规则在这里没有起作用。

此情形下如果用a作为驱动表,通过索引c2关联到c表,那么还需要再回表查询一次,因为仅仅通过c2获取不到c.*的数据,还需要通过c2上的主键c1再查询一次。而上一个sql查询的是c2,不需要额外查询。同时因为a表只有两个字段,通过a2索引能够直接获得a.*,不需要额外查询。

综上所述,虽然使用c表来驱动,结果集大一些,但是能够减少一次额外的回表查询,所以mysql认为使用c表作为驱动来效率更高。

结果集是作为选择驱动表的一个主要因素,但不是唯一因素。

两表关联查询的内在逻辑是怎样的?

mysql表与表之间的关联查询使用Nested-Loop join算法,顾名思义就是嵌套循环连接,但是根据场景不同可能有不同的变种:比如Index Nested-Loop join,Simple Nested-Loop join,Block Nested-Loop join, Betched Key Access join等。

  • 使用索引关联的情况下,有Index Nested-Loop joinBatched Key Access join两种算法;
  • 未使用索引关联的情况下,有Simple Nested-Loop joinBlock Nested-Loop join两种算法;

我们先来看有索引的情形,使用的是博客刚开始时建立的表,sql如下:

select a.*,c.* from a join c on a.a2=c.c2 where a.a1>4;

通过explain查看其执行计划:

浅谈Mysql多表连接查询的执行细节

首先根据第一步的逻辑来确定驱动表a,然后通过a.a1>4,a.来查询一条记录a1=5,将此记录的c2关联到c表,取得c2索引上的主键c1,然后用c1的值再去聚集索引上查询c.*,组成一条完整的结果,放入net buffer,然后再根据条件a.a1>4,a. 取下一条记录,循环此过程。过程图如下:

浅谈Mysql多表连接查询的执行细节

通过索引关联被驱动表,使用的是Index Nested-Loop join算法,不会使用msyql的join buffer。根据驱动表的筛选条件逐条地和被驱动表的索引做关联,每关联到一条符合的记录,放入net-buffer中,然后继续关联。此缓存区由net_buffer_length参数控制,最小4k,最大16M,默认是1M。 如果net-buffer满了,将其发送给client,清空net-buffer,继续上一过程。

通过上述流程知道,驱动表的每条记录在关联被驱动表时,如果需要用到索引不包含的数据时,就需要回表一次,去聚集索引上查询记录,这是一个随机查询的过程。每条记录就是一次随机查询,性能不是非常高。mysql对这种情况有选择的做了优化,将这种随机查询转换为顺序查询,执行过程如下图:

浅谈Mysql多表连接查询的执行细节

此时会使用Batched Key Access join 算法,顾名思义,就是批量的key访问连接。

逐条的根据where条件查询驱动表,将符合记录的数据行放入join buffer,然后根据关联的索引获取被驱动表的索引记录,存入read_rnd_buffer。join buffer和read_rnd_buffer都有大小限制,无论哪个到达上限都会停止此批次的数据处理,等处理完清空数据再执行下一批次。也就是驱动表符合条件的数据可能不能够一次处理完,而要分批次处理。

当达到批次上限后,对read_rnd_buffer里的被驱动表的索引按主键做递增排序,这样在回表查询时就能够做到近似顺序查询:

浅谈Mysql多表连接查询的执行细节 

浅谈Mysql多表连接查询的执行细节 

如上图,左边是未排序前的随机查询示意图,右边是排序后使用MRR( Multi-Range Read)的顺序查询示意图。

因为mysql的InnoDB引擎的数据是按聚集索引来排列的,当对非聚集索引按照主键来排序后,再用主键去查询就使得随机查询变为顺序查询,而计算机的顺序查询有预读机制,在读取一页数据时,会向后额外多读取最多1M数据。此时顺序读取就能排上用场。

BKA算法在需要对被驱动表回表的情况下能够优化执行逻辑,如果不需要会表,那么自然不需要BKA算法。

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

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

前两个参数的作用是要启用 MRR(Multi-Range Read)。这么做的原因是,BKA 算法的优化需要依赖于MRR,官方文档的说法,是现在的优化器策略,判断消耗的时候,会更倾向于不使用 MRR,把 mrr_cost_based 设置为 off,就是固定使用 MRR 了。)

最后再用explain查看开启参数后的执行计划:

浅谈Mysql多表连接查询的执行细节

上述都是有索引关联被驱动表的情况,接下来我们看看没有索引关联被驱动表的情况。

没有使用索引关联,那么最简单的Simple Nested-Loop join,就是根据where条件,从驱动表取一条数据,然后全表扫面被驱动表,将符合条件的记录放入最终结果集中。这样驱动表的每条记录都伴随着被驱动表的一次全表扫描,这就是Simple Nested-Loop join。

当然mysql没有直接使用Simple Nested-Loop join,而是对其做了一个优化,不是逐条的获取驱动表的数据,而是多条的获取,也就是一块一块的获取,取名叫Block Nested-Loop join。每次取一批数据,上限是达到join buffer的大小,然后全表扫面被驱动表,每条数据和join buffer里的所有行做匹配,匹配上放入最终结果集中。这样就极大的减少了扫描被驱动表的次数。

BNL(Block Nested-Loop join) 和 BKA(Batched Key Access join)的流程有点类似, 但是没有read_rnd_buffer这个步骤。

示例sql如下:

select a.*, d.* from a join d on a.a2=d.d2  where a.a1>7;

用explain查看其执行计划:

浅谈Mysql多表连接查询的执行细节

多表连接如何执行?是先两表连接的结果集然后关联第三张表,还是一条记录贯穿全局?

其实看连接算法的名称:Nested-Loop join,嵌套循环连接,就知道是多表嵌套的循环连接,而不是先两表关联得出结果,然后再依次关联的形式,其形式类似于下面这样:

for row1 in table1 filtered by where{
	for row2 in table2 associated by table1.index1 filtered by where{
		for row3 in table3 associated by table2.index2 filtered by where{
			put into net-buffer then send to client;
		}
	}	
}

对于不同的join方式,有下列情况:

Index Nested-Loop join

sql如下:

select a.*,b.*,c.* from a join c on a.a2=c.c2 join b on c.c2=b.b2 where b.b1>4;

通过explain查看其执行计划:

浅谈Mysql多表连接查询的执行细节

其内部执行流程如下:

浅谈Mysql多表连接查询的执行细节

执行前mysql执行器会确定好各个表的关联顺序。首先通过where条件,筛选驱动表b的第一条记录b5,然后将用此记录的关联字段b2与第二张表a的索引a2做关联,通过Btree定位索引位置,匹配的索引可能不止一条。当匹配上一条,查看where里是否有a2的过滤条件且条件是否需要索引之外的数据,如果要则回表,用a2索引上的主键去查询数据,然后做判断。通过则用join后的信息再用同样的方式来关联第三章表c。

Block Nested-Loop joinBatched Key Access join : 这两个关联算法和Index Nested-Loop join算法类似,不过因为他们能使用join buffer,所以他们可以每次从驱动表筛选一批数据,而不是一条。同时每个join关键字就对应着一个join buffer,也就是驱动表和第二张表用一个join buffer,得到的块结果集与第三章表用一个join buffer。

本篇博客主要就是讲述上述三个问题,如何确定驱动表,两表关联的执行细节,多表关联的执行流程。

到此这篇关于浅谈Mysql多表连接查询的执行细节的文章就介绍到这了,更多相关Mysql多表连接查询内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

MySQL 相关文章推荐
MySQL update set 和 and的区别
May 08 MySQL
MySql学习笔记之事务隔离级别详解
May 12 MySQL
MySQL 全文索引使用指南
May 25 MySQL
my.ini优化mysql数据库性能的十个参数(推荐)
May 26 MySQL
mysql中between的边界,范围说明
Jun 08 MySQL
Mysql 如何查询时间段交集
Jun 08 MySQL
浅谈MySQL之select优化方案
Aug 07 MySQL
MySQL面试题讲解之如何设置Hash索引
Nov 01 MySQL
VS2019连接MySQL数据库的过程及常见问题总结
Nov 27 MySQL
MySQL数据库完全卸载的方法
Mar 03 MySQL
解决Mysql报错 Table 'mysql.user' doesn't exist
May 06 MySQL
MYSQL事务的隔离级别与MVCC
May 25 MySQL
Mysql基础之常见函数
Apr 22 #MySQL
MySQL分库分表与分区的入门指南
MySQL8.0.24版本Release Note的一些改进点
Apr 22 #MySQL
详解MySQL的半同步
详解MySQL事务的隔离级别与MVCC
Apr 22 #MySQL
MySQL之高可用集群部署及故障切换实现
教你用eclipse连接mysql数据库
You might like
关于Appserv无法打开localhost问题的解决方法
2009/10/16 PHP
在PHP中检查PHP文件是否有语法错误的方法
2009/12/23 PHP
如何使用PHP计算上一个月的今天
2013/05/23 PHP
PHP 抽象方法与抽象类abstract关键字介绍及应用
2014/10/16 PHP
PHP连接MSSQL时nvarchar字段长度被截断为255的解决方法
2014/12/25 PHP
WordPress中获取指定分类及其子分类下的文章数目
2015/12/31 PHP
php usort 使用用户自定义的比较函数对二维数组中的值进行排序
2017/05/02 PHP
thinkPHP5实现的查询数据库并返回json数据实例
2017/10/23 PHP
php curl优化下载微信头像的方法总结
2018/09/07 PHP
在JavaScript中构建ArrayList示例代码
2014/09/17 Javascript
利用HTML5的画布Canvas实现刮刮卡效果
2015/09/06 Javascript
JS组件Bootstrap Select2使用方法详解
2020/04/17 Javascript
jQuery实现鼠标滑过图片移动特效
2016/12/08 Javascript
vue 实现搜索的结果页面支持全选与取消全选功能
2019/05/10 Javascript
使用 js 简单的实现 bind、call 、aplly代码实例
2019/09/07 Javascript
jquery validate 实现动态增加/删除验证规则操作示例
2019/10/28 jQuery
JavaScript indexOf()原理及使用方法详解
2020/07/09 Javascript
js实现购物车商品数量加减
2020/09/21 Javascript
JavaScript 实现继承的几种方式
2021/02/19 Javascript
[01:54]TI珍贵瞬间系列(三):翻盘
2020/08/28 DOTA
介绍Python的Django框架中的静态资源管理器django-pipeline
2015/04/25 Python
python安装教程 Pycharm安装详细教程
2017/05/02 Python
用pycharm开发django项目示例代码
2018/10/24 Python
Python 中list ,set,dict的大规模查找效率对比详解
2019/10/11 Python
Python局部变量与全局变量区别原理解析
2020/07/14 Python
Pycharm无法打开双击没反应的问题及解决方案
2020/08/17 Python
详解使用postMessage解决iframe跨域通信问题
2019/11/01 HTML / CSS
很酷的小工具和电子产品商城:GearBest
2016/11/19 全球购物
Java方面的关于数组和继承的笔面试题
2015/09/18 面试题
大龄毕业生求职别忘职业规划
2014/03/11 职场文书
学生会竞选演讲稿学习部
2014/08/25 职场文书
2015年试用期工作总结
2014/12/12 职场文书
小学安全教育主题班会
2015/08/12 职场文书
李白经典诗之一:全文无一“月”字,却句句有月
2019/07/12 职场文书
2019辞职报告范本3篇!
2019/07/23 职场文书
Python中的 enumerate和zip详情
2022/05/30 Python