zoukankan      html  css  js  c++  java
  • 浅谈SQL SERVER中的物理联接算法

    深入聚集索引与非聚集索引(一)(二)中,(好吧,由于没什么人看,因此没写二),我们详细的分析了SQL SERVER是如何用堆和B树来组织表,并用这两个数据结构帮助我们查询的。

    这里我们继续的内容就是探讨SQL SERVER中的连接算法。

    联接算法是指在物理上把多个数据源如何联接起来,SQL SERVER支持三种联接算法

    1.nested loop 嵌套循环算法

    2.merge 合并算法

    3.hash 哈希算法

    其实这几种算法我们在通常的编程中也经常会用到,并不是很难理解。

    一、嵌套循环

    一般来说嵌套循环就对应着两层for循环,对于外层for中的每一个项,在内层循环中都要匹配一次。

    相应的,外层for对应着外部输入表,在执行计划的图示中排在上面,内层for则是内层输入表,在执行计划中排在下面。

    这里值得强调的是,外部输入是每一行的都要使用来匹配的,而内部表却不一定每一行都在匹配中使用。假设外部输入有N行,内部输入表有M行,最差的时间复杂度就是O(N*M)。而在这种最差的情况下,优化器不会再采用嵌套循环,而是采用Hash匹配算法。关于Hash匹配算法,后面会有说明。

    因此我们可以得到下面的推论

    1.外部输入越小越好,因为外部输入每一行都要被用来匹配,不能减少。

    2.内部输入表作为匹配的,则可以利用索引来减少匹配条件的范围,这样就可以通过少量的搜索来获取匹配行。

    因此嵌套循环算法在连接条件的选择性比较强,而且在内部输入的连接列上有可以利用的有效索引时,是最有效的。

    在下面这个例子中,Customers表的记录数要远比Orders表的记录数少(客户数肯定要比订单数少),因此Customers表中的数据被优化器选择作为外部输入,从图中可以看出Customers表是在Orders表的上方。

    当我们直接查询是,SQL SERVER还会很智能的告诉你缺少什么索引。在下面这个例子中,我们缺少的正是作为内部输入的“连接列 custid”和“查询列 orderdate”上的非聚集索引

    image

    当我们输入下面这条语句加上索引后

    CREATE INDEX idx_nc_cid_od_i_oid_eid_sid
      ON dbo.Orders(custid, orderdate)
      INCLUDE(orderid, empid, shipperid);

    image

    SQL SERVER仍然报缺少索引,是因为我们对于外部输入的表仍然是可以利用索引来先筛选一轮,以起到减小外部输入的目的。

    为了达到这个目的,缺少的是这个非聚集索引,custname作为筛选列,加上custid同样是为了起到覆盖作用。

    CREATE INDEX idx_nc_cn_i_cid
      ON dbo.Customers(custname) INCLUDE(custid);

    这时对于Customers表的index scan就会变成Index seek。

    补全索引后,我们最终获得的执行计划

    image

    二、合并排序

    对于两个输入列都有序的情况下,合并联接的效率高。

    排序的重要性毋庸置疑了,什么二分查找等等查找都是建立在输入序列有序的基础上。

    为什么先讲索引呢?我们可以从索引中发现有现成的排序好的数据结构吗?有的,B树中的叶层就是按照一定的逻辑顺序维护的。也就是说,聚集索引和非聚集覆盖索引,都可以通过对叶层的有序扫描以较小的代价就可以获取有序的数据。在这种情况下,就算输入表的规模比较大,合并联接也相当给力。如果计划分析器确定连接的一侧记录集中的元素是唯一确定的,那么就会采用一对多的匹配方式(多指另一侧的元素会有重复),在这种情况下,合并排序效率应该是几种连接方式中最高的。

    但如果所需的数据列并不存在上述的条件的时候,对于较大的输入来说排序往往是一个开销非常大的操作(因为基于比较的排序最快也就是n log n的),因此优化器通常不会在这种情况下选用合并联接。但是对于较小的输入排序的消耗还是可以接受的。较小的输入可以像上例一样通过对自身的筛选来获得。

    image

    分析:

    对于连接列custid,对于Customers表不用说,是该表的聚集索引的聚集键,对于order表来说,我们在上面给custid创建了非聚集覆盖索引,所以也可以按照有序扫描以较小的代价获取有序数据。

    因为都可以从两张表中以较小的代价获取按照连接列custid的顺序获取有序的数据,因此优化器选择了合并排序。

    可以看出Orders表的扫描在整体开销中所占的比例是最大的达到68%,因为Orders表的数据非常多。那么假设我们在Orders表上还有其他的筛选条件呢?比如对orderdate进行限定

    image

    由于这个筛选器的选择性非常高,所获取的结果才1000多行,只占Orders表1000000总数下的0.1%。两权之下,另可放弃有序的全表扫描,而是先过滤再排序。

    当然,计划器也有可能估计错误,如果所获取的结果选择性不高的话,排序所占用的开销往往非常大。这也是我们在发现优化器生成Merge计划时要注意的地方。

    三、Hash联接

    原理参照我写的这篇文章,为了减少内存占用因此使用数据量较小的表来构造hashtable,然后另逐行扫描另一张表通过hash函数算出hashtable的某个位置上是否已存在值来判断相等。

    通常用到hash联接,是因为缺少现成的索引,特别是在数据仓库类型(OLTP)的应用中.

    我们在未创建任何索引的第一个示例中,采取的就是Hash匹配。逐行扫描Customer表构造hashtable,因为我们where条件中有orderdate可以减小匹配的范围,所以先用聚集索引减少Orders表中匹配的记录数,然后再用hash函数逐行匹配前面构造好的hashtable.

    image

    缺少合适的索引也可能会采用Hash匹配。我们把orderdate的范围增加了几十倍,由一天改成了查询几个月,这时合并联接算法不再适合。

    image

    总结:

    采用Hash联接算法,从时间复杂度上来说是最优的,联接一张M条记录的表和一张N条记录的表的时间复杂度为O(M+N),好于带有聚集索引的嵌套联接算法O(M * log2n)。但是如果在构造hashtable时内存不足以保存Hashtable时,会产生临时空间交换,导致而外大量的IO从而抵消了联接时所产生的益处。

    当然如果你觉得有帮助,请点击推荐,或者留点评论说说想法,讨论讨论。

    下面打算写写我在学习Object CGit中的一些心得体会系列文章。

     
     
  • 相关阅读:
    洛谷—— P2234 [HNOI2002]营业额统计
    BZOJ——3555: [Ctsc2014]企鹅QQ
    CodeVs——T 4919 线段树练习4
    python(35)- 异常处理
    August 29th 2016 Week 36th Monday
    August 28th 2016 Week 36th Sunday
    August 27th 2016 Week 35th Saturday
    August 26th 2016 Week 35th Friday
    August 25th 2016 Week 35th Thursday
    August 24th 2016 Week 35th Wednesday
  • 原文地址:https://www.cnblogs.com/lwzz/p/2644965.html
Copyright © 2011-2022 走看看