嵌套循环连接(Nested-Loop Join)
- 两表连接来说,驱动表只会被访问一遍,但被驱动表却要被访问到好多遍,具体访问几遍取决于对驱动表执行单表查询后的结果集中的记录条数
- 对于内连接来说,选取哪个表为驱动表都没关系,而外连接的驱动表是固定的,也就是说左(外)连接的驱动表就是左边的那个表,右(外)连接的驱动表就是右边的那个表。
内外连接区别?
对于外连接的驱动表的记录来说,如果无法在被驱动表中找到匹配ON子句中的过滤条件的记录,那么该记录仍然会被加入到结果集中,对应的被驱动表记录的各个字段使用NULL值填充;而内连接的驱动表的记录如果无法在被驱动表中找到匹配ON子句中的过滤条件的记录,那么该记录会被舍弃。
大概执行流程
- 选取驱动表,使用与驱动表相关的过滤条件,选取代价最低的单表访问方法来执行对驱动表的单表查询。
- 对上一步骤中查询驱动表得到的结果集中每一条记录,都分别到被驱动表中查找匹配的记录。
通用的两表连接过程如下图所示:
如果有3个表进行连接的话,那么步骤2
中得到的结果集就像是新的驱动表,然后第三个表就成为了被驱动表,重复上边过程,也就是步骤2
中得到的结果集中的每一条记录都需要到t3
表中找一找有没有匹配的记录,用伪代码表示一下这个过程就是这样:
for each row in t1 { #此处表示遍历满足对t1单表查询结果集中的每一条记录
for each row in t2 { #此处表示对于某条t1表的记录来说,遍历满足对t2单表查询结果集中的每一条记录
for each row in t3 { #此处表示对于某条t1和t2表的记录组合来说,对t3表进行单表查询
if row satisfies join conditions, send to client
}
}
}
- 在
嵌套循环连接
中,驱动表只需要执行一次,被驱动表需要被扫描很多次,如果被驱动表的查询没有使用索引列的话,那么将会是全表扫描,如果数据量大的话无疑是灾难性的,所以被驱动表的查询是很有必要使用索引的 - 有时候连接查询的查询列表和过滤条件中可能只涉及被驱动表的部分列,而这些列都是某个索引的一部分,这种情况下即使不能使用
eq_ref
、ref
、ref_or_null
或者range
这些访问方法执行对被驱动表的查询的话,也可以使用索引扫描,也就是index
的访问方法来查询被驱动表。所以我们建议在真实工作中最好不要使用*
作为查询列表,最好把真实用到的列作为查询列表。
- 扫描一个表的过程其实是先把这个表从磁盘上加载到内存中,然后从内存中比较匹配条件是否满足。但是内存里可能并不能完全存放的下表中所有的记录,所以在扫描表前边记录的时候后边的记录可能还在磁盘上,等扫描到后边记录的时候可能内存不足,所以需要把前边的记录从内存中释放掉。
- 采用
嵌套循环连接
算法的两表连接过程中,被驱动表可是要被访问好多次的,如果这个被驱动表中的数据特别多而且不能使用索引进行访问,那就相当于要从磁盘上读好几次这个表,这个I/O
代价就非常大了
buffer join
基于上面出现的问题,mysql出现一个buffer join的概念
join buffer
就是执行连接查询前申请的一块固定大小的内存,先把若干条驱动表结果集中的记录装在这个join buffer
中,然后开始扫描被驱动表,每一条被驱动表的记录一次性和join buffer
中的多条驱动表记录做匹配,因为匹配的过程都是在内存中完成的,所以这样可以显著减少被驱动表的I/O
代价。使用join buffer
的过程如下图所示:
最好的情况是
join buffer
足够大,能容纳驱动表结果集中的所有记录,这样只需要访问一次被驱动表就可以完成连接操作了。驱动表的记录并不是所有列都会被放到
join buffer
中,只有查询列表中的列和过滤条件中的列才会被放到join buffer
中,所以再次提醒我们,最好不要把*
作为查询列表,只需要把我们关心的列放到查询列表就好了,这样还可以在join buffer
中放置更多的记录。