SQL 中 left join 的底层原理 记

01. 前言

写过或者学过 SQL 的人应该都知道 left join,知道 left join 的实现的效果,就是保留左表的全部信息,然后把右表往左表上拼接,如果拼不上就是 null。除了 left join 以外,还有 inner join、outer join、right join,这些不同的 join 能达到的什么样的效果,大家应该都了解了,如果不了解的可以看看网上的帖子或者随便一本 SQL 书都有讲的。今天我们不讲这些 join 能达到什么效果,我们主要讲这些 join 的底层原理是怎么实现的,也就是具体的效果是怎么呈现出来的。

join 主要有 Nested Loop、Hash Join、Merge Join 这三种方式,我们这里只讲最普遍的,也是最好的理解的 Nested Loop,Nested Loop 翻译过来就是嵌套循环的意思,那什么又是嵌套循环呢?嵌套大家应该都能理解,就是一层套一层;那循环呢,你可以理解成是 for 循环。

Nested Loop 里面又有三种细分的连接方式,分别是 Simple Nested-Loop Join、Index Nested-Loop Join、Block Nested-Loop Join,接下来我们就分别去看一下这三种细分的连接方式。

在正式开始之前,先介绍两个概念:驱动表(也叫外表)和被驱动表(也叫非驱动表,还可以叫匹配表,亦可叫内表),简单来说,驱动表就是主表,left join 中的左表就是驱动表,right join 中的右表是驱动表。一个是驱动表,那另一个就只能是非驱动表了,在 join 的过程中,其实就是从驱动表里面依次(注意理解这里面的依次)取出每一个值,然后去非驱动表里面进行匹配,那具体是怎么匹配的呢?这就是我们接下来讲的这三种连接方式。

02.Simple Nested-Loop Join

Simple Nested-Loop Join 是这三种方法里面最简单,最好理解,也是最符合大家认知的一种连接方式,现在有两张表 table A 和 table B,我们让 table A left join table B,如果是用第一种连接方式去实现的话,会是怎么去匹配的呢?直接上图:

上面的 left join 会从驱动表 table A 中依次取出每一个值,然后去非驱动表 table B 中从上往下依次匹配,然后把匹配到的值进行返回,最后把所有返回值进行合并,这样我们就查找到了 table A left join table B 的结果。是不是和你的认知是一样的呢?利用这种方法,如果 table A 有10行,table B 有10行,总共需要执行 10 x 10 = 100 次查询。

这种暴力匹配的方式在数据库中一般不使用。

03.Index Nested-Loop Join

Index Nested-Loop Join 这种方法中,我们看到了 Index,大家应该都知道这个就是索引的意思,这个 Index 是要求非驱动表上要有索引,有了索引以后可以减少匹配次数,匹配次数减少了就可以提高查询的效率了。

为什么会有了索引以后可以减少查询的次数呢?这个其实就涉及到数据结构里面的一些知识了,给大家举个例子就清楚了。

上图中左边就是普通列的存储方式,右边是树结构索引,什么是树结构呢?就是数据分布的像树这样一层一层的,树结构有一个特点就是左边的数据小于顶点的数,右边的数大于顶点的数,你看右图中,左边的数3是不是小于顶点6,右边的数7是不是大于顶点6;左边的数1是不是小于顶点3,右边的数4是不是大于顶点3。

假如我们现在要匹配数值9,如果是左边这种数据存储方式的话,我们需要从第一行依次匹配到最后一行才能找到数值9,总共需要匹配7次;但是如果我们是用右边这种树结构索引的话,我们先拿9和最上层顶点6去匹配,发现9比6大,我们就去顶点的右边去找,再去和7匹配,发现9仍然比7大,再去7的右边找,就找到了9,这样我们只匹配了3次就把我们想要的9找到了。是不是相比匹配7次节省了很多时间。

数据库中的索引一般用 B+ 树,为了让大家更好的理解,我上面画的图只是最简单的一种树结构,而非真实的 B+ 树,但是原理是一样的。

如果索引是主键的话,效率会更高,因为主键必须是唯一的,所以如果被驱动表是用主键去连接,只会出现多对一或者一对一的情况,而不会出现多对多和一对多的情况。

04.Block Nested-Loop Join

理想情况下,用索引匹配是最高效的一种方式,但是在现实工作中,并不是所有的列都是索引列,这个时候就需要用到 Block Nested-Loop Join 方法了,这种方法与第一种方法比较类似,唯一的区别就是会把驱动表中 left join 涉及到的所有列(不止是用来on的列,还有select部分的列)先取出来放到一个缓存区域,然后再去和非驱动表进行匹配,这种方法和第一种方法相比所需要的匹配次数是一样的,差别就在于驱动表的列数不同,也就是数据量的多少不同。所以虽然匹配次数没有减少,但是总体的查询性能还是有提升的。

Join操作是一种常见的数据库操作,通过Join可以将多个表关联起来,根据用户的条件共同提供数据。一般情况,在数据库中都会内置多种Join算法,优化器在优化的时候会根据SQL语句和表的统计信息选择合适的算法。

Hash Join

ddce85be217b5281de922cd84705e9cf.png

Hash Join

在执行Hash Join时,1. 会根据Join条件将一张表进行Hash运算加载到内存中的一张Hash表中。Hash表类似与Java中的HashTable;2.遍历另外一张表,进行Hash运算后在内存中查找满足条件的记录。

select * from t1 join t2 on t1.a = t2.b;在执行这个SQL的时候,先加载表t1的数据,然后根据表t1的a字段作为key构造Hash表。之后,从表t2中逐条取出记录,计算字段b的Hash值,去Hash表中查找是否存在满足条件的记录。

Hash Join的性能很高,但是前提条件是内存中能够存放下其中一张表的Hash表。所以一般适用于大小表Join。在一些大数据分析的数据查询引擎中,当内存放不下这种Hash表的时候,会将小表进行分区保存到磁盘上,之后再执行Join。

嵌套循环Join

0d47de89f37af25b69160a079eae344b.png

嵌套循环Join

嵌套循环Join中,至少一张表存在索引,且Join的条件是对索引列的比对。带有索引的表作为被检索表,对不带有索引或者两张都带有索引的表中较小的那张表进行遍历。这个算法充分利用了索引的优势,让Join的时间复杂度从O(m*n)变成了O(n),其中m为被检索表的行数,n为遍历表的行数。

Merge Hash

相对于上述两个算法,这个算法的性能差些,但是使用范围更广些。在这个算法中,相对两张表中的数据进行排序,之后再分别取一段进行Join。

Semi Join

半连接,对于左边的表输出满足条件的记录,而对于右边的表则不管是否满足条件都不会被输出,也就是,最终的结果是左边表数据记录的一个子集,类似于in、exists。Semi Join本身就是Join的一种。在大数据跨数据源的查询中,Semi Join是对inner join、left join、right join的一种优化。查询跨数据源时,尽量减少从每个数据源出来的数据量是一种很有效的优化方式,毕竟网络传输是要花费时间的。将Join转化成Semi Join是一种有效减小数据量的方式。

对于:select * from t1 join t2 where t1.a = t2.b,Semi Join的过程如下:

1.将表t1的数据加载到内存;

2.根据t1的数据,改写加载表t2的条件,即将SQL语句改写成in、exists等。假设表t1中,全部记录的a字段只有两个值:aa和bb,那么SQL将被改写为select * from t2 in ('aa','bb');

3.对从表t1和t2加载的数据做Join;

第2步中对加载t2数据的SQL的改写,使原本需要加载整个t2表改为仅加载t2中满足条件的数据。

原文地址:https://www.cnblogs.com/zuochanzi/p/14189862.html