从Search Sort到Join

发表于《程序员》2015年4月B的一篇文章,在博客归档下。根据杂志社要求,在自己博客发表该文章亦须注明:本文为CSDN编译整理,未经允许不得转载,如需转载请联系market#csdn.net(#换成@)

一、前言

Join的语义是把两张表的通过属性的关联条件值组合在一起,一般意义上数据库范式越高,表被拆分的越多,应用中要被Join的表可能会越多。在我们日常开发中几乎找不到不涉及Join的SQL语句,哪怕未显示包含Join这个关键字。在数据库系统执行时,都会选用一种明确最优的Join方式来执行。通过对Join原理的理解能帮助理解数据库的Join选择,进而更深刻的理解查询计划,理解同样的表结构数据行数变化、索引的变化为什么会影响数据库Join方式的选择,从而更针对性的做好性能优化。

如下图分别是oracle和mssql的执行计划,是涉及四个表的内连接。在查询计划中显示每一个步骤的表连接方式,从图中可以看到共有三个表连接操作(有意思的是,对于相似的数据集,两个数据库选择的连接方式不同)。

图表 1 Oracle执行计划

Join原理在数据库厂商的文档中和经典的《数据库系统概念》中分别从应用和理论的角度上描述的足够详细。本文尝试从另外一个视角,通过数据结构中最基础的查找排序算法(Search&Sort)来类推和归纳最常用的三种Join方式的原理:嵌套循环连接(Nested Loop Join)、排序归并连接 (Sorted Merge Join)、 哈希连接(Hash Join)。进而了解三种方式之间的联系、演变以及原理决定的其应用上的细微差别。说明这几种表连接方式是数据库的技术,但和数据库索引等其他数据库技术一样,也是是典型的数据结构的延伸。

图表 2 Mssql执行计划

二、实例数据

为了示意清楚,本文中用到的示例中的两个数据集比较简单。为如下两个表。

T_FC


Fc_id


Fc_name


OfficialSite


CityId


1


Inter


www.inter.it


2


2


Barcelona


http://www.fcbarcelona.com/


8


3


Manchester united


http://www.manutd.com


12


4


Liverpool


http://www.liverpoolfc.com/


11


5


Arsenal


http://www.arsenal.com/


36


6


ACM


http://www.acmilan.com/


2


7


Chelsea


http://www.chelseafc.com/


36


8


Manchester city


http://www.mcfc.co.uk/


12

图表 3 测试数据T_FC

T_City


Fc_idCityId


Fc_name


Country


Desc


36


Landon


Britain


11


Liverpool


Britain


12

Manchester Britain  
 2 Milan Italy

图表 4 测试数据 T_City

第一个表是一个实体表,记录了几个Football Club的一般性描述。其中第一条记录是作者支持了17年的球队:位于Milan城的大国米!第二个表是一个城市信息的实体表,两个表通过CityId列关联,表示那个俱乐部位于那座城市。从示例数据中可以看到,不同的俱乐部可以有相同的CityId,这就是传说中的德比,最著名、最伟大、最火爆的,也是作者一直关注的米兰德比,还有最近几年才开始关注的曼彻斯特德比。

为了简单期间,以最简单的内连接中的等值连接条件举例。

SELECT T_FC.Fc_id, T_FC.Fc_name, T_FC.OfficialSite, T_FC.CityId, T_City.City_Name, T_City.Country
FROM  T_FC , T_City
WHERE T_FC. CityId = T_City. CityId

数据准备好了,然后进入正文,看看如何从数据结构的Search Sort 归纳出Join。

三 、从查找排序算法看表连接原理分析

表连接主要考察的是连接列的关系,于是作为切入点,暂时忽略掉连接键之外的列,发现待连接的两个表变成了两个数组,讨论问题陡然看着清爽简单了很多。所谓的连接无非就是把这两个关联列中符合连接条件对应元素找出来配对即可。我们例子是一个等值条件,这个匹配条件转为查找的讨论就更直接了,即考察一个集合的元素,找出另外一个集合值其相等的元素。

图表 5 关联列

看出来了吗?我们在试图偷换概念。用检索search来替换连接Join。是的,这是我们的切入点,后面会再绕回来的。

检索,这在数据结构和算法中是多么经典的技术。后面讨论我们会发现数据库系统中“发明”的高大上的连接方式,其实也也是从对应的经典的检索算法推演而来。

1. 嵌套循环连接(Nested Loop Join)

看着前面的这两组数字,不用太浪费脑细胞,最笨的办法,穷举呗,拿着一边的数据,一个一个拿出来,看看和隔壁的哪个对上眼。表现在算法实现上其实就是两重循环,循环体内就是一个匹配看是否满足连接条件,对于这个例子就是取值是否相等。即拿着一组中的数字,逐个另一组中去匹配,看是否有等值的元素存在。

假设我们先考虑T_City的数据,即外层循环是遍历T_City表的数据。则写成伪代码如下。

for each CityId in T_City do begin
for each tuple CityId in T_FC do begin
if T_City.CityId=T_FC.CityId
   output(T_FC.CityId,T_City.CityId)

下图描述了这种连接方式的过程,从T_City中逐行选取记录,在T_FCCityId列上按个查找符合连接条件T_City.CityId=T_FC.CityId的记录。找到则添上一个箭头表示建立连接(Join),多么形象!从图中看到总共添加了7个箭头,对应Join结果集中的7个Join结果。

图表 6 嵌套循环连接示例

这就是检索中对于没有规则的数据集的最笨的检索办法。恭喜你,你上面写的这段最笨的检索伪代码正是数据库系统中嵌套循环连接(Nested Loop Join)的主要思路。术语上,外围循环的数据集T_City被称为外部表(Outer Table),又称驱动表(Driving Table);内部循环的表T_FC称为内部表(Inner Table)。

实现中扫描外部表的每一行,对每一行数据,扫描内部表中满足Join条件的记录,将其作为输出结果集。最终的结果集是每次外部扫描的结果集并在一起。因为要通过两重循环对两组中的每一个数据进行比较,以判断是否满足Join条件,很容易发现这种方法是开销是相对比较昂贵的。

对于Nested Loop Join,OracleMysqlMssql几乎所有的关系数据库都支持。虽然实现上细节上稍微有不同。思路都一样。

根据这个原理我们不难分析到最好外部表即驱动表不能太大。如果是两张表比较大,尤其是Outer Table比较大的情况,Inner Table会被扫描很多次,这时候的算法复杂度增加得非常快,总的资源消耗量也会增加得很快。同时,如果Inner Table在Join条件上有索引,则每次内循环的效率会很高,Inner表数据集大也没有关系,不用挨个扫描Inner表就可以定位到匹配的行。

2. 排序归并连接(Sort Merge Join)

那有没有比整个列表扫描更高效的方法呢?当然还是按照检索的思路。对于一个没有规则的数据集只能全部扫描,检索的时间复杂度是O(n),而如果记录集是排好序的,查找会变得简单。

结合上面的例子,如果把两组数都排好序,那么Join会是什么样子?马上脑海里浮现出了如下场景:把一个有序集合插入到另外一个有序集合的对应位置,使得最终的集合也保持有序,即把两个有序集合合并成一个有序集合。如图,把左边T_City表中黄色的记录和右边T_FC中绿色的记录合并在一起。

图表 7 归并排序示例

这个不正是经典的归并排序吗?没错,就是归并!假设两个集合都是从小到大排序的,归并排序的主要思路是:在输入的有序集合设定两个指针,起始位置分别为两个已经排序序列的起始位置,比较两个指针指向元素,选择相对小的元素放入输出的合并空间,并移动指针到下一个位置,直到一个集合为空,把另外一个集合的剩余元素一下全部放到输出的合并空间。哪怕不仔细了解内部逻辑,只是看循环,就能发现和嵌套循环连接最大的区别是循环只有一层了。因为不用对两个集合的每两个元素都要进行比较。那时间复杂度就一定降低了一个级别。

上面归并排序示例图经过小的改动,就会成下图。输入是一样的,两个有序集合。中间的merge过程也类似,不同的是,没有把一个有序集合的元素对应插入到另外一个有序集合中,而只是按顺序在两个有序集合中相等的元素间建立联系。

图表 8 排序归并连接示例

参照连接条件T_City.CityId=T_FC.CityId,根据左边T_City表上CityId上列的取值,挨个在右边的T_FC上查找,和嵌套循环连接的示例图一样,结果也是添加了满足条件的7个箭头,并且也同样得到了7个Join结果,但是箭头的分布还是有挺大差别。在Sort Merge Join中的箭头是没有交叉的,而Nested Loop Join中箭头交叉很多。这个简单的特征其实反应了两种方法的原理上的差别。以T_CityCitypId=2的行发起的箭头为例,在第Sort Merge Join中,因为右边行集是排序的,因此是扫描了前两行的两个匹配记录后,碰到第一个不匹配的第三行,则结束扫描;而在Nested Loop Join中,右边的行集是没有顺序的,要找到符合连接条件的,必须对右边行集进行完整扫描。

观察到Outer Table中的连接的键值每次和Inner Table 的连接键值比较时,都不用从第一个元素开始,也不必一直扫描到最后一个元素。因此时间复杂度也会比Nested Loop Join降低。

根据这个原理很容易发现使用排序归并连接可以获得比较高的连接效率。但是所谓没有免费的午餐,使用排序归并连接需要待连接的两个集合在连接列上是排好序的。我们知道对于大的数据集排序代价一般是不小的,因此优化器在选择是否采用排序归并连接还要参考其他条件。如果输入的集合本身是在有序的(如在Join条件的列上有B树索引),或者SQL语义中有order by要求,在连接列上本来就是要求排序的,则优化器可能会考虑选择排序归并连接。一般对于大的集合如果不能采用排序归并连接,则另外一种连接方式Hash连接会被考虑。

3. 哈希连接(Hash Join)

还是从查找算法思路来考虑。查找算法中有一个通过构建一点额外空间而换取O(1)的时间复杂度的检索方法,那就是hash查找。将该算法思路类推到数据库中,就是大名鼎鼎且功能强大的哈希连接。Hash查找中通过一个Hash函数,对待查找的元素集合进行分组,使得元素的存储位置和元素值之间有一定联系。则对于要查找的元素,只要计算其Hash值得到存储位置,直接访问即可(Hash函数的选择不好引起的过多的Hash冲突,造成计算得到的存储位置定位精准度不够在此不作讨论)。

在Hash Join中,同样使用一个合适的Hash函数对两个行集上的Join条件列分别进行分组,在匹配中只要比较两边相同分组内的记录即可。不用像嵌套循环连接一样,挨个比较两个集合中每两个元素。其实也可以认为,嵌套循环连接就是Hash Join中只有一个分组的情况。如果Hash函数选择越好,分的组越均匀,则Hash冲突越少,分组就越有效,性能也越好。

在示例中使用最简单的哈希函数Hash(n)=n%10将两个输入行集的Join条件列CityId分为10个组。由Hash函数的定义可以知道,Join条件满足如下推导:

因此在图中左边和右边Hash后partiton的对应记录的箭头是水平的。如图标黄的部分中12和2均落入partition2,则Join时左边标黄的partition2中的记录只用考察和右边partition2中的记录建立连接即可。

图表 10 哈希连接示例

在数据库系统实现中,Hash Join方式一般通过一个输入集来构造Hash Table,扫描另外一个输入集,对其中的每个元素通过Hash查找的方式快速定位到Hash Table的对应位置,即找了满足连接条件的记录。如示例中,根据T_City表中的CityId建立Hash Table。对T_FC中的CityId使用同一个Hash函数,计算得到Hash值,定位到Hash Table的元素位置,只需在该位置中查找匹配的元素建立连接即可。这种方式和上面的描述的对两个数据集都采用同样的Hash函数进行分组,然后组内检查匹配逻辑上是一个意思。

通过分析Hash Join的原理,我们可以分析出如果用于两个数据集差别比较大的时候,如果小的数据集能加载到内存构造Hash Table,然后从磁盘上扫描另外一个较大的数据集,对大数据集中的Join列计算Hash值得到内存Hash Table的Index,然后建立连接。这样性能会非常高,这时真正的物理IO的花销就只有造Hash Table时对小数据集的扫描和匹配时对大数据集进行的扫描。当然,很容易看出Hash Join的一个天然限制,那就是连接条件必须是等值条件,因为Hash Join的机制需要计算Hash值得到Hash Table的位置,来建立连接。但是Hash Join不需要数据集事先按照什么顺序排序,也不要求上面有索引。

四、总结

本文尝试以数据结构中最基础的查找排序算法为切入点来归纳三种经典排序算法在实现原理上的联系和差别。比较有意思的是,最简单的办法不是看作者啰啰嗦嗦的描述,而是分别观察几张Join示意图中连接箭头的特征,同样的两个数据集做连接,都是T_City在左T_FC在右,因此箭头的方向都是从左指向右。但是箭头的特征还是有差别:嵌套循环连接(Nested Loop Join)的箭头是偏上或者偏下各种方向都有,且是彼此交叉的;排序归并连接(Sort Merge Join)的箭头是虽然也是偏上偏下方向都有,但彼此没有交叉;而哈希连接(Hash Join)的箭头最具有特征,所有箭头都是水平的。

以上仅仅是在逻辑数据集上比较纯粹的分析数据库系统中三种连接方式的原理。实际情况要复杂的多,并不存在这样一种方法,比其他的方法高明高效。数据库的查询优化器在选择一个连接方式时会结合操作对象的数据统计情况,查询语句的要求,选择一个合适的连接方式。另外支持一种以上Join方式的数据库系统(如oracle和mssql)中都支持Join hint使得用户可以要求优化器在使用指定的连接方式覆盖优化器自己选择的Join方式。一般还是认为优化器足够聪明,不使用Join Hint。

最后,基于之上的原理分析在下表中对三种连接方式在使用场景等几个细节方面做个更全面的比较和归纳:

  嵌套循环连接(Nested Loop Join) 排序归并连接(Sort Merge Join) 哈希连接(Hash Join)
支持的常用数据库 oraclemssql,mysql oraclemssql oraclemssql
适用场景 两个较小的数据集,Inner table Join列上有索引 较大数据集,Join字段上有索引,或者语句要求结果集被排序。 大数据集,Join字段上没有索引也不要求排序

连接字段表达式


任何表达式


<、 <=、 >、 >=;不包括 <>


仅用于等价连接

数据集排序特征 不要求 要求 不要求

内存资源要求


要求较少


当需要对数据集排序时,内存要求较高。



返回第一次查询结果


当索引选择性比较高或限制条件比较多时能较快返回第一次查询结果。


必须在得到全部结果后才能返回数据。


因为要在内存中简历哈希表,第一次返回结果较慢。

完。

原创文章。为了维护文章的版本一致、最新、可追溯,转载请注明: 转载自idouba

本文链接地址: 从Search Sort到Join

时间: 2024-10-16 02:59:24

从Search Sort到Join的相关文章

Sort merge join、Nested loops、Hash join(三种连接类型)

目前为止,典型的连接类型有3种: Sort merge join(SMJ排序-合并连接): 首先生产driving table需要的数据,然后对这些数据按照连接操作关联列进行排序:然后生产probed table需要的数据,然后对这些数据按照与driving table对应的连接操作列进行排序:最后两边已经排序的行被放在一起执行合并操作.排序是一个费时.费资源的操作,特别对于大表.所以smj通常不是一个特别有效的连接方法,但是如果driving table和probed table都已经预先排序

NESTED LOOPS &amp; HASH JOIN &amp; SORT MERGE JOIN

表连接方式及使用场合 NESTED LOOP 对于被连接的数据子集较小的情况,nested loop连接是个较好的选择.nested loop就是扫描一个表,每读到一条记录,就根据索引去另一个表里面查找,没有索引一般就不会是 nested loops.一般在nested loop中, 驱动表满足条件结果集不大,被驱动表的连接字段要有索引,这样就走nstedloop.如果驱动表返回记录太多,就不适合nested loops了.如果连接字段没有索引,则适合走hash join,因为不需要索引. 可用

sort、join 處理 csv 檔案

雖然理想上,使用者要的資料最好都是由系統產生報表,使用者直接瀏覽或下載,但是,使用者常臨時要一些業績或管理性報表,IT 人員也只能從資料庫中下 sql statement 後匯出給使用者,有時,IT 人員更可能拿到別家公司匯出的數個 csv 檔,使用者卻希望將它們合併成一個檔案,當然,一般情況 IT 人員應該會找到一台資料庫,把檔案匯入處理後,再匯出給使用者,不過,IT 人員的環境裡並不總是有資料庫可以使用 (可能是控管嚴格沒有權限) ,這時候可以使用 Unix 中的一些指令來處理,Unix 通

深入理解Oracle表(3):三大表连接方式详解之Nested loop join和 Sort merge join

深入理解Oracle表(3):三大表连接方式详解之Nested loop join和 Sort merge join 分类: Oracle 基础管理 Oracle SQL 开发2013-01-28 00:33 2536人阅读 评论(1) 收藏 举报 关系数据库技术的精髓就是通过关系表进行规范化的数据存储       并通过各种表连接技术和各种类型的索引技术来进行信息的检索和处理       这里Think愿意和大家一起来学习分享Oracle的三大表连接技术              在早期版本,

Nested Loops,Hash Join , Sort Merge Join

在多表联合查询的时候,如果我们查看它的执行计划,就会发现里面有多表之间的连接方式.多表之间的连接有三种方式:Nested Loops,Hash Join 和 Sort Merge Join.具体适用哪种类型的连接取决于 当前的优化器模式 (ALL_ROWS 和 RULE) 取决于表大小 取决于连接列是否有索引 取决于连接列是否排序 下面来介绍三种不同连接工作方式的不同: 实验sql 假如有10000个城市,对应于10个国家(此例子仅仅可以解释join工作的过程) 更换优化器,添加索引,会影响下面

转-Shell笔记——命令:Sort,uniq,join,cut,paste,split

转自:http://blog.csdn.net/wklken/article/details/6562098 Sort,uniq,join,cut,paste,split 命令—— Sort Sort 可将许多不同的域按不同的列顺序分类 命令格式: sort –cmu –o out-putfile [other options] *pos1 *pos2 input-files 选项: -c 测试文件是否已经分类 -m 合并两个分类文件 -u 删除所有复制行 -o 存储 sort 结果的输出文件名

11g sort merge join

测试sort merge join When the Optimizer Considers Sort Merge Joins A hash join requires one hash table and one probe of this table, whereas a sort merge join requires two sorts. The optimizer may choose a sort merge join over a hash join for joining lar

文本处理命令--cut、sort、join

声明:下面介绍的只是命令的常用选项,如果需要详细了解命令全部细节,需要参考其他的资料. 一.cut cut是一个选取命令,就是将一段数据经过分析,取出我们想要的.一般来说,选取信息通常是针对“行”来进行分析的,并不是整篇信息分析的. -d:自定义分隔符,默认分隔符是制表符. -f:指定显示哪个区域 示例: (my_python_env)[[email protected] ~]# echo "a b c" | cut -d ' ' -f 1 a (my_python_env)[[ema

oracle表之间的连接之------&gt;排序合并连接(Merge Sort Join)

排序合并连接 (Sort Merge Join)是一种两个表在做连接时用排序操作(Sort)和合并操作(Merge)来得到连接结果集的连接方法. 对于排序合并连接的优缺点及适用场景如下: a,通常情况下,排序合并连接的执行效率远不如哈希连接,但前者的使用范围更广,因为哈希连接只能用于等值连接条件,而排序合并连接还能用于其他连接条件(如<,<=,>.>=) b,通常情况下,排序合并连接并不适合OLTP类型的系统,其本质原因是对于因为OLTP类型系统而言,排序是非常昂贵的操作,当然,如