• SQL Server三种表连接原理


    http://msdn.microsoft.com/zh-cn/library/dn144699.aspx

    SQL Server三种表连接原理

    013年2月

     

    简介

     

    在SQL Server中,我们所常见的表与表之间的Inner Join,Outer Join都会被执行引擎根据所选的列,数据上是否有索引,所选数据的选择性转化为Loop Join,Merge Join,Hash Join这三种物理连接中的一种。理解这三种物理连接是理解在表连接时解决性能问题的基础,下面我来对这三种连接的原理,适用场景进行描述。

     

    嵌套循环连接(Nested Loop Join)

     

    循环嵌套连接是最基本的连接,正如其名所示那样,需要进行循环嵌套,嵌套循环是三种方式中唯一支持不等式连接的方式,这种连接方式的过程可以简单的用下图展示:

     

    Dn144699.445175445CEAC8D4EDDFA908C1FC8B6A(zh-cn,MSDN.10).png

     

    图1.循环嵌套连接的第一步 

     

    Dn144699.575AFB26620304AB254113AC028B57E1(zh-cn,MSDN.10).png

     

    图2.循环嵌套连接的第二步 

     

    由上面两个图不难看出,循环嵌套连接查找内部循环表的次数等于外部循环的行数,当外部循环没有更多的行时,循环嵌套结束。另外,还可以看出,这种连接方式需要内部循环的表有序(也就是有索引),并且外部循环表的行数要小于内部循环的行数,否则查询分析器就更倾向于Hash Join(会在本文后面讲到)。

     

    通过嵌套循环连接也可以看出,随着数据量的增长这种方式对性能的消耗将呈现出指数级别的增长,所以数据量到一定程度时,查询分析器往往就会采用这种方式。

     

    下面我们通过例子来看一下循环嵌套连接,利用微软的AdventureWorks数据库:

     

    Dn144699.5174A25748B22FA6CA62039899A1B218(zh-cn,MSDN.10).png

     

    图3.一个简单的嵌套循环连接 

     

    图3中ProductID是有索引的,并且在循环的外部表中(Product表)符合ProductID=870的行有4688条,因此,对应的SalesOrderDetail表需要查找4688次。让我们在上面的查询中再考虑另外一个例子,如图4所示。

     

    Dn144699.5429AF72B72048F1C8E3A953C12FBC34(zh-cn,MSDN.10).png

     

    图4.额外的列带来的额外的书签查找 

     

    由图4中可以看出,由于多选择了一个UnitPrice列,导致了连接的索引无法覆盖所求查询,必须通过书签查找来进行,这也是为什么我们要养成只Select需要的列的好习惯,为了解决上面的问题,我们既可以用覆盖索引,也可以减少所需的列来避免书签查找。另外,上面符合ProductID的行仅仅只有5条,所以查询分析器会选择书签查找,假如我们将符合条件的行进行增大,查询分析器会倾向于表扫描(通常来说达到表中行数的1%以上往往就会进行table scan而不是书签查找,但这并不绝对),如图5所示。

     

    Dn144699.632A9BF7050E7A626A5444771B24FC0E(zh-cn,MSDN.10).png

     

    图5.查询分析器选择了表扫描 

     

    可以看出,查询分析器此时选择了表扫描来进行连接,这种方式效率要低下很多,因此好的覆盖索引和Select *都是需要注意的地方。另外,上面情况即使涉及到表扫描,依然是比较理想的情况,更糟糕的情况是使用多个不等式作为连接时,查询分析器即使知道每一个列的统计分布,但却不知道几个条件的联合分布,从而产生错误的执行计划,如图6所示。

     

    Dn144699.F152BA99EF56EC1B496729936CA1D27C(zh-cn,MSDN.10).png

     

    图6.由于无法预估联合分布,导致的偏差 

     

    由图6中,我们可以看出,估计的行数和实际的行数存在巨大的偏差,从而应该使用表扫描但查询分析器选择了书签查找,这种情况对性能的影响将会比表扫描更加巨大。具体大到什么程度呢?我们可以通过强制表扫描和查询分析器的默认计划进行比对,如图7所示。

     

    Dn144699.3E38A3EFF15C5159D0283D6E058232A9(zh-cn,MSDN.10).png

     

    图7.强制表扫描性能反而更好 

     

    合并连接(Merge Join)

     

    谈到合并连接,我突然想起在西雅图参加SQL Pass峰会晚上酒吧排队点酒,由于我和另外一哥们站错了位置,貌似我们两个在插队一样,我赶紧说:I’m sorry,i thought here is end of line。对方无不幽默的说:”It’s OK,In SQL Server,We called it merge join”。

     

    由上面的小故事不难看出,Merge Join其实上就是将两个有序队列进行连接,需要两端都已经有序,所以不必像Loop Join那样不断的查找循环内部的表。其次,Merge Join需要表连接条件中至少有一个等号查询分析器才会去选择Merge Join。

     

    Merge Join的过程我们可以简单用下面图进行描述:

     

    Dn144699.2DEB0AF993E7A453430109CEC260A8ED(zh-cn,MSDN.10).png

     

    图8.Merge Join第一步 

     

    Merge Join首先从两个输入集合中各取第一行,如果匹配,则返回匹配行。加入两行不匹配,则有较小值的输入集合+1,如图9所示。

     

    Dn144699.CDD5667F45C6CA9271F38411AA9C0EA8(zh-cn,MSDN.10).png

     

    图9.更小值的输入集合向下进1 

     

    用C#代码表示Merge Join的话如代码1所示。

     

     
    public class MergeJoin
    {
        // Assume that left and right are already sorted
        public static Relation Sort(Relation left, Relation right)
        {
            Relation output = new Relation();
            while (!left.IsPastEnd() && !right.IsPastEnd())
            {
                if (left.Key == right.Key)
                {
                    output.Add(left.Key);
                    left.Advance();
                    right.Advance();
                }
                else if (left.Key < right.Key)
                    left.Advance();
                else //(left.Key > right.Key)
                    right.Advance();
            }
            return output;
        }
    }
    

     

    代码1.Merge Join的C#代码表示

     

    因此,通常来说Merge Join如果输入两端有序,则Merge Join效率会非常高,但是如果需要使用显式Sort来保证有序实现Merge Join的话,那么Hash Join将会是效率更高的选择。但是也有一种例外,那就是查询中存在order by,group by,distinct等可能导致查询分析器不得不进行显式排序,那么对于查询分析器来说,反正都已经进行显式Sort了,何不一石二鸟的直接利用Sort后的结果进行成本更小的MERGE JOIN?在这种情况下,Merge Join将会是更好的选择。

     

    另外,我们可以由Merge Join的原理看出,当连接条件为不等式(但不包括!=),比如说> < >=等方式时,Merge Join有着更好的效率。

     

    下面我们来看一个简单的Merge Join,这个Merge Join是由聚集索引和非聚集索引来保证Merge Join的两端有序,如图10所示。

     

    Dn144699.47FB414C72FCC89CE9F2070D20CF9B5E(zh-cn,MSDN.10).png

     

    图10.由聚集索引和非聚集索引保证输入两端有序

     

    当然,当Order By,Group By时查询分析器不得不用显式Sort,从而可以一箭双雕时,也会选择Merge Join而不是Hash Join,如图11所示。

     

    Dn144699.A5B8A44A2F9895A600EA44738A5EC2E8(zh-cn,MSDN.10).png

     

    图11.一箭双雕的Merge Join

     

    哈希匹配(Hash Join)

     

    哈希匹配连接相对前面两种方式更加复杂一些,但是哈希匹配对于大量数据,并且无序的情况下性能均好于Merge Join和Loop Join。对于连接列没有排序的情况下(也就是没有索引),查询分析器会倾向于使用Hash Join。

     

    哈希匹配分为两个阶段,分别为生成和探测阶段,首先是生成阶段,第一阶段生成阶段具体的过程可以如图12所示。

     

    Dn144699.CB4D97D8CFF4F9C6A2E51E11FBA103A2(zh-cn,MSDN.10).png

     

    图12.哈希匹配的第一阶段

     

    图12中,将输入源中的每一个条目经过散列函数的计算都放到不同的Hash Bucket中,其中Hash Function的选择和Hash Bucket的数量都是黑盒,微软并没有公布具体的算法,但我相信已经是非常好的算法了。另外在Hash Bucket之内的条目是无序的。通常来讲,查询优化器都会使用连接两端中比较小的哪个输入集来作为第一阶段的输入源。

     

    接下来是探测阶段,对于另一个输入集合,同样针对每一行进行散列函数,确定其所应在的Hash Bucket,在针对这行和对应Hash Bucket中的每一行进行匹配,如果匹配则返回对应的行。

     

    通过了解哈希匹配的原理不难看出,哈希匹配涉及到散列函数,所以对CPU的消耗会非常高,此外,在Hash Bucket中的行是无序的,所以输出结果也是无序的。图13是一个典型的哈希匹配,其中查询分析器使用了表数据量比较小的Product表作为生成,而使用数据量大的SalesOrderDetail表作为探测。

     

    Dn144699.DE1F3CD084DAB91DDF34FA2941A7F47C(zh-cn,MSDN.10).png

     

    图13.一个典型的哈希匹配连接

     

    上面的情况都是内存可以容纳下生成阶段所需的内存,如果内存吃紧,则还会涉及到Grace哈希匹配和递归哈希匹配,这就可能会用到TempDB从而吃掉大量的IO。这里就不细说了,有兴趣的同学可以移步:http://msdn.microsoft.com/zh-cn/library/aa178403(v=SQL.80).aspx

     

    总结

     

    下面我们通过一个表格简单总结这几种连接方式的消耗和使用场景:

     

     

    嵌套循环连接

    合并连接

    哈希连接

    适用场景

    外层循环小,内存循环条件列有序

    输入两端都有序

    数据量大,且没有索引

    CPU

    低(如果没有显式排序)

    内存

    低(如果没有显式排序)

    IO

    可能高可能低

    可能高可能低

     

    理解SQL Server这几种物理连接方式对于性能调优来说必不可少,很多时候当筛选条件多表连接多时,查询分析器就可能不是那么智能了,因此理解这几种连接方式对于定位问题变得尤为重要。此外,我们也可以通过从业务角度减少查询范围来减少低下性能连接的可能性。

     

    参考文献:

     

    http://msdn.microsoft.com/zh-cn/library/aa178403(v=SQL.80).aspx

     

    http://www.dbsophic.com/SQL-Server-Articles/physical-join-operators-merge-operator.html

    在SQL Server数据库中,查询优化器在处理表连接时,通常会使用一下三种连接方式:

      • 嵌套循环连接(Nested Loop Join)
      • 合并连接 (Merge Join)
      • Hash连接 (Hash Join)

      充分理解这三种表连接工作原理,可以使我们在优化SQL Server连接方面的代码有据可依,为开展优化工作提供一定的思路。接下来我们来认识下这三种连接。

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

      该连接方式通常在小数据量并且语句比较简单的场景中使用,也是比较常见的连接方式,比如以下示例:

       1:  use AdventureWorks2008 
       2:  go 
       3:  SELECT H.* 
       4:  FROM Sales.SalesOrderHeader H 
       5:  JOIN Sales.Sale
       1:  use AdventureWorks2008 
    sOrderDetail D
       6:  ON H.SalesOrderID=D.SalesOrderID 
       7:  WHERE H.SalesOrderID = 43659 

      AdventureWorks2008数据库是SQL Server的一个sample,你可以在微软官方网站上自由下载。http://msftdbprodsamples.codeplex.com/releases/view/37109

      我们在数据库中运行这段代码:

    clip_image002

      通过执行计划我们可以看到,数据库的优化器使用了嵌套连接(Neasted Loops),上面第一行中的Sales.SalesOrderHeader表因为只有一行数据所以做为外部表使用,SalesOrderDetail有12行数据做为内部表使用。

      嵌套循环的工作原理如图所示:

    clip_image004

    图1 嵌套循环工作原理图

      其原理就是根据条件从表中过滤出一个外部链接表,循环的从外部表中读取一行数据,去内部表中进行匹配,伪码如下:

    For (i=0;i< Number of outerTable Row;i++)

    {

        OuterTable[i] connect InnerTable[1,2.....N] To Create New Row

        WHERE OuterTable[i].data.value = OuterTable[1,2.....N].data.Value

    }

      了解嵌套的工作原理后,我们不难发现,这种连接的方式具有一定的局限性的:

    1. 因为算法是循环进行的,所以比较适合数据量较小的表进行连接,尤其是外部表的数据。

    2. 两张表最好是排序的。表中的条件列和连接列最好有索引,尤其是内部表必须有索引,这样工作效率会成倍增加。

    当外部表较小,而内部表较大并且连接字段上有索引的情况下,循环嵌套非常高效。并且嵌套循环是三种方式中唯一支持不等式连接的方式。

    2. 合并连接 (Merge Join)

      在SQL Server数据库中,如果查询优化器,发现要连接的两张对象表,在连接列上都已经排序并包含索引,那么优化器将会极大可能选择“合并”连接策略。条件是:两个表都是排序的,并且表连接条件中至少有一个等号连接,查询分析器会去选择合并连接。

      代码示例:

       1:  USE AdventureWorks2008 
       2:   
       3:  GO 
       4:   
       5:  SELECT P.* 
       6:   
       7:  FROM Production.ProductModel P 
       8:   
       9:  JOIN Production.ProductModelProductDescriptionCulture PPMD 
      10:   
      11:  ON P.ProductModelID = PPMD.ProductModelID 

      根据执行计划我们可以看到,这次的连接操作使用的合并连接:

    clip_image006

      这两张表中,数据量分别为128和762行数据,连接列是表中的主键并且数据是有序的,因此数据库的查询优化器自动选择了合并连接。合并连接的工作原理如下图所示:

    合并连接原理

    图2 合并连接的工作原理

      数据库优化器在决定使用合并连接后,并行的在两个表(术语叫输入集合)中各取第一行数据,进行匹配,匹配则返回匹配行并进行连接。如果不匹配,那么小的那一个表(输入集合),则顺序取下一行数据继续尝试匹配。

      通过其工作原理我们可以发现,合并连接可以看成是一个类似于并发工作机制。操作分别在两个表(输入集合)依次获取数据并进行比较,这就要求两张表是有序的,有序的排列会极大的提高工作的效率。

      有关表排序的问题,如果连接语句中使用Sort关键字来排序数据表,那么SQL Server的优化器会比较倾向于Hash Join。在合并连接中,并不排斥order by, group by, distinct等关键字,在使用这些语句时,查询优化器也有极大的可能选择合并连接。

      当我们使用一些查询限定条件,比如不等式(>,<,>=等)限定条件范围,那么合并连接的效率会有更好。

      合并连接的限定条件:

    1. 两张表的连接列需要排序

    2. 连接列必须有索引

    3. 哈希连接(Hash Join)

      当我们尝试将两张数据量较大,没有排序和索引的两张表进行连接时,SQL Server的查询优化器会尝试使用Hash Join。

    代码示例:

       1:  SELECT * 
       2:   
       3:  FROM Production.Product P 
       4:   
       5:  Join Production.ProductSubcategory SPC 
       6:   
       7:  on P.ProductSubcategoryID = SPC.ProductSubcategoryID 

      根据执行计划我们可以看到,这次的连接操作使用的哈希连接:

    clip_image010

      该连接在处理大量无序的数据时,效率较高,但是对处理器和内存资源的消耗较大。实现过程如下:

      Hash Join连接的执行操作分为两个阶段,建立和探测。

      建立是指对输入表进行的一系列的操作。首先优化器会将输入表中的每一行数据扫描到系统内存中,然后根据内置的散列算法计算出相应散列值,相同散列值的数据会被分到一个Hash池中。这些散列值和数据地址保存在一个Hash表中,提供给探测使用。通常优化器会选择数据较少的表作为建立输入表。

      建立完成后,开始探查工作。另一个连接表(我们叫探查输入)同样会被逐行的扫描、计算,得出一个Hash值。连接操作会使用探查输入的Hash值和建立输入的Hash值列表进行扫描和匹配工作,最终建立连接。

    clip_image012

      上图是Hash连接的工作流程,接下来我们可以来了解下哈希算法的实现的机制,以下的内容是个人对算法的理解,若有偏颇请指正。

      Hash的实际含义是“散列”的意思,它主要的功能就是将一组数据,通过算法,变换成固定长度的输出,这个输出我们就称之为散列值(Hash值),通常在安全领域,如密码学中使用较多。

      在SQL Server里面哈希散列函数是黑盒的,没有具体的算法可以参考。实际上很多开发人员在解决海量数据查询的时候,都会采用Hash方式,并且开发适合需求的散列算法。常用的一些算法包括一些取余、MD2、MD4、MD5 和 SHA-1等等。

      因为算法,不同的数据可能会生成相同的散列值。它将大量的数据按照规则分散到不同数据堆或者链表中,建立内部的映射关系。我们可以认为他是将数组和链表结合在一起,想要达到一种寻址容易、插入删除方便的数据结构,而Hash表就是一种数据内容和数据存放地址之间的映射关系。

      散列函数的选择会决定影响Hash表元数量大小和每个键值包含的数据多少,这个是数学上的问题这里不进行进一步讨论。

      说到这里,可能大家还是不太理解,我们这里举例来说明:

      比如说有两张表:

    表A{A,F,C,D,B,E……}

    表B{F,B,E,D,A,F…….}

      并且表A的数据量小于表B,这两张表进行Hash连接的过程如下:

    Hash连接原理2

    1. 首先数据库会将表A中的所有数据,扫描存入内存中。

    2. 内存中的表A的数据,经过散列函数依次得到对应的散列值(Hash值)。

    3. 表A中相同散列值(键值)的数据,会统一的放入到一个Hash池中。个人认为Hash池中的数据,就是数组和链表的集合。Hash的键值可以看到是一个数组的下标,而池中的数据以链表的形式连接在数组中。

    Hash【键值】-->数据1-->数据2..............

    如图中的一组数据,数据A和数据C具有相同的Hash值,值为001,那么他们都被分配到以001命名的Hash池中。

    4. 将Hash值和对应的数据,依次存入到一个Hash表中,建立结束。

    5. 探测阶段,数据库依次读取扫描表B中的每一行数据,并通过散列函数计算出一个Hash值。

    6. 根据Hash值,去Hash表中和表A的键值进行匹配,找到对应的Hash池。

    7. 接下来将表B的数据去和对应的Hash池中的每条数据,去对比和匹配。如果匹配成功则进行数据连接。

      通过对原理的了解,我们可以看到这种连接方式,需要大量的计算操作,对CPU带来一定的压力。通常Hash 连接操作在内存中进行,如果内存不足,数据库会将数据写入到硬盘中,影响性能。

        4.小结

    三种连接方式的特点:

    类型

    连接列上索引

    表的大小

    排序

    连接子句

    嵌套

    内部表:必须

    外部表:有最好

    可选

    所有类型

    合并

    内部表:必须 聚簇索引或者覆盖索引

    外部表:必须 聚簇索引或者覆盖索引

    需要

    Equi-join

    HASH

    内部表:不需要

    外部表:可选,最好有

    小的外部表,大得内部表

    任意

    不需要

    Equi-join

    三种方式对资源的压力:

     

    嵌套循环连接

    合并连接

    哈希连接

    CPU

    低(如果没有显式排序)

    内存

    低(如果没有显式排序)

    IO

    可能高可能低

    可能高可能低

      以上是个人对三种连接的个人理解,不当之处请指正。

    题外话:

      其实我们可以把这三种连接比喻成相亲。

      嵌套连接就是熟人介绍,亲戚朋友根据你的条件,搜索下周围的资源,然后安排你和几个姑娘见面,看看能不能匹配上。如果你的条件很明确(外部表索引),并且朋友对姑娘比较熟悉,对方的要求也很明确(内部表索引),那么成功率就会比较高。

      合并连接就是社区或者网站组织的小型相亲联谊会,比如电影《恋爱33天中》那种8分钟面对面的形式。男女双方面对面进行交谈(匹配判断),每几分钟就换一个人再次交谈,由于大家条件和目的性明确(都有索引),所以整个流程效率会比较高。

        Hash连接则就像是万人相亲大会,比如上海的中山公园(条件好的已婚人士慎入)。单身青年的父母,入园后由于各种原因随机的分成各个小群组(经过散列函数分成Hash池)。然后参与者根据自己的判断(确认Hash键值),找到合适小组后(Hash键值相等),依次交谈交换条件和信息(尝试匹配),看看里面有没有合适人选,有就进一步了解(匹配成功,连接)。

    2013年11月14日 Ralf Wang

     
     
    标签: SQL hash Join
  • 相关阅读:
    jquery 监听input的value值改变
    Win10家庭版共享打印机启用Guest账户
    js数组操作大全(pop,push,unshift,splice,shift方法)
    EF Core索引
    工作经验(C++篇)
    FFmpeg编译i386 OSX 脚本
    xcrun -sdk 选择
    Unity GL 画圆
    OpenGL ES无法获取贴图数据原因
    Unity在Android和iOS中如何调用Native API
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/3579527.html
Copyright © 2020-2023  润新知