SQL Server调优系列基础篇(经常使用运算符总结——三种物理链接方式剖析)

前言html

上一篇咱们介绍了如何查看查询计划,本篇将介绍在咱们查看的查询计划时的分析技巧,以及几种咱们经常使用的运算符优化技巧,一样侧重基础知识的掌握。算法

经过本篇能够了解咱们日常所写的T-SQL语句,在SQL Server数据库系统中是如何分解执行的,数据结果如何经过各个运算符组织造成的。数据库

技术准备并发

基于SQL Server2008R2版本,利用微软的一个更简洁的案例库(Northwind)进行解析。app

1、数据链接函数

数据链接是咱们在写T-SQL语句的时候最经常使用的,经过两个表之间关联获取想要的数据。oop

SQL Server默认支持三种物理链接运算符:嵌套循环链接、合并链接以及哈希链接。三种链接各有用途,各有特色,不一样的场景会数据库会为咱们选择最优的链接方式。post

 

a、嵌套循环链接(nested loops join)性能

嵌套循环链接是最简单也是最基础的链接方式。两张表经过关键字进行关联,而后经过双层循环依次进行两张表的行进行关联,而后经过关键字进行筛选。大数据

能够参照下图进行理解分析

其实嵌套扫描是很简单的获取数据的方式,简单点就是两层循环过滤出结果值。

咱们能够经过以下代码加深理解

for each row R1 in the outer table
   for each row R2 int the inner table
       if R1 join with R2
       return (R1,R2)

举个列子

SELECT o.OrderID
FROM Customers C JOIN Orders O
ON C.CustomerID=O.CustomerID
WHERE C.City=N'London'

以上这个图标就是嵌套循环链接的图标了。并且解释的很明确。

这种方法的消耗就是外表和内表的乘积,其实就是咱们所称呼的笛卡尔积。因此消耗的大小是随着两张表的数据量增大而增长的,尤为是内部表,由于它是屡次重复扫描的,因此咱们在实践中的采起的措施就是减小每一个外表或者内表的行数来减小消耗。

对于这种算法还有一种提升性能的方式,由于两张表是经过关键字进行关联的,因此在查询的时候对于底层的数据获取速度直接关乎着此算法的性能,这里优化的方式尽可能使用两个表关键字为索引查询,提升查询速度。

还有一点就是在嵌套循环链接中,在两张表关联的时候,对外表都是有筛选条件的,好比上面例子中【WHERE C.City=N'London'】就是对外表(Customers)的筛选,而且这里的City列在该表中存在索引,因此该语句的两个子查询都为索引查找(Index Seek)。

可是,有些状况咱们的查询条件不是索引所覆盖的,这时候,在嵌套循环链接下的子运算符就变成了索引扫描(Index scan)或者RID查找。

举个例子

SELECT E1.EmployeeID,COUNT(*)
FROM Employees E1 JOIN Employees E2
ON E1.HireDate>E2.HireDate
GROUP BY E1.EmployeeID

以上代码是从职工表中获取出每位职工入职前的人员数。咱们看一下该查询的执行计划

这里很显然两个表的关联经过的是HireDate列进行,而此列又不为索引项所覆盖,因此两张表的获取只能经过全表的汇集索引扫描进行,若是这两张表数据量特别大的话,无疑又是一个很是耗性能的查询。

经过文本能够看出,该T-SQL的查询结果的获取是经过在嵌套循环运算符中,对两个表通过全表扫描以后造成的笛卡儿积进行过滤筛选的。这种方式其实不是一个最优的方式,由于咱们获取的结果实际上是能够先经过两个表过滤以后,再经过嵌套循环运算符获取结果,这样的话性能会好不少。

咱们尝试改一下这个语句

SELECT E1.EmployeeID,ECNT.CNT 
FROM Employees E1 CROSS APPLY
(
   SELECT COUNT(*) CNT
   FROM Employees E2
   WHERE E1.HireDate<E2.HireDate
)ECNT

经过上述代码查询的结果项,和上面的是同样的,只是咱们根据外部表的结果对内部表进行了过滤,这样执行的时候就不须要获取所有数据项了。

咱们查看下文本执行计划

咱们比较一下,先后两条语句的执行消耗,对比一下执行效率

 

 执行时间从1秒179毫秒减小至93毫秒。效果明显。

对比CPU消耗、内存、编译时间等整体消耗都有所下降,参考上图。

因此对嵌套循环链接链接的优化方式就是集中在这几点:对两张表数据量的减小、链接关键字上创建索引、谓词查询条件上覆盖索引最好能减小符合谓词条件的记录数。

 

b、合并链接(merge join)

上面提到的嵌套循环链接方式存在着诸多的问题,尤为不适合两张表都是大表的状况下,由于它会产生N屡次的全表扫描,很显然这种方式会严重的消耗资源。

鉴于上述缘由,在数据库里又提供了另一种链接方式:合并链接。记住这里没有说SQL Server所提供的,是由于此链接算法是市面全部的RDBMS所共同使用的一种链接算法。

合并链接是依次读取两张表的一行进行对比。若是两个行是相同的,则输出一个链接后的行并继续下一行的读取。若是行是不相同的,则舍弃两个输入中较少的那个并继续读取,一直到两个表中某一个表的行扫描结束,则执行完毕,因此该算法执行只会产生每张表一次扫描,而且不须要整张表扫描完就能够中止。

 

该算法要求按照两张表进行依次扫描对比,可是有两个前提条件:一、必须预先将两张表的对应列进行排序;二、对两张表进行合并链接的条件必须存在等值链接。

咱们能够经过如下代码进行理解

get first row R1 from input1
get first row R2 from input2
while not at the end of either input
begin
     if R1 joins with R2
         begin
              output(R1,R2)
              get next row R2 from input2
         end
     else if R1<R2   
             get next row R1 from input1
          else
             get next row R2 from input2
end              

合并链接运算符总的消耗是和输入表中的行数成正比的,并且对表最多读取一次,这个和嵌套循环链接不同。所以,合并链接对于大表的链接操做是一个比较好的选择项。

对于合并链接能够从以下几点提升性能:

  1. 两张表间的链接值内容列类型,若是两张表中的关联列都为惟一列,也就说都不存在重复值,这种关联性能是最好的,或者有一张表存在惟一列也能够,这种方式关联为一对多关联方式,这种方式也是咱们最经常使用的,比咱们常用的主从表关联查询;若是两张表中的关联列存在重复值,这样在两表进行关联的时候还须要借助第三张表来暂存重复的值,这第三张表叫作”worktable “是存放在Tempdb或者内存中,而这样性能就会有所影响。因此鉴于此,咱们常作的优化方式有:关联连尽可能采用汇集索引(惟一性)
  2. 咱们知道采用该种算法的前提是,两张表都通过排序,因此咱们在应用的时候,最好优先使用排序后的表关联。若是没有排序,也要选择的关联项为索引覆盖项,由于大表的排序是一个很耗资源的过程,咱们选择索引覆盖列进行排序性能要远远好于普通列的排序。

咱们来举个例子

SELECT O.CustomerID,C.CustomerID,C.ContactName 
FROM Orders O JOIN Customers C
ON O.CustomerID=C.CustomerID

咱们知道这段T-SQL语句中关联项用的是CustomerID,而此列为主键汇集索引,都是惟一的而且通过排序的,因此这里面没有显示的排序操做。

并且凡是采用合并链接的全部输出结果项,都是已经通过排序的。

咱们找一个稍复杂的状况,没有提早排序的利用合并查询的T-SQL

SELECT O.OrderID,C.CustomerID,C.ContactName
FROM Orders O JOIN Customers C
ON O.CustomerID=C.CustomerID AND O.ShipCity<>C.City
ORDER BY C.CustomerID

上述代码返回那些客户的发货订单不在客户本地的。

上面的查询计划能够看出,排序的消耗老是巨大的,其实咱们上面的语句按照逻辑应该是在合并链接获取数据后,才采用显示的按照CustomerID进行排序。

可是由于合并链接运算符以前自己就须要排序,因此此处SQL Server采起了优先排序的策略,把排序操做提早到了合并链接以前进行,而且在合并链接以后,就不须要在作额外的排序了。

这其实这里咱们要求对查询结果排序,正好也利用了合并链接的特色。

 

c、哈希链接(hash join) 

咱们分析了上面的两种链接算法,两种算法各有特色,也各有本身的应用场景:嵌套循环链接适合于相对小的数据集链接,合并链接则应对与中型的数据集,可是又有它本身的缺点,好比要求必须有等值链接,而且须要预先排序等。

那对于大型的数据集合的链接数据库是怎么应对的呢?那就是哈希链接算法的应用场景了。

哈希链接对于大型数据集合的并行操做上都比其它方式要好不少,尤为适用于OLAP数据仓库的应用场景中。

哈希链接不少地方和合并链接相似,好比都须要至少一个等值链接,一样支持全部的外链接操做。但不一样于合并链接的是,哈希链接不须要预先对输入数据集合排序,咱们知道对于大表的排序操做是一个很大的消耗,因此去除排序操做,哈希操做性能无疑会提高不少。

哈希链接在执行的时候分为两个阶段:

  • 构建阶段

在构建阶段,哈希链接从一个表中读入全部的行,将等值链接键的行机型哈希话处理,而后建立造成一个内存哈希表,而将原来列中行数据依次放入不一样的哈希桶中。

  • 探索阶段

在第一个阶段完成以后,开始进入第二个阶段探索阶段,该阶段哈希链接从第二个数据表中读入全部的行,一样也是在相同的等值链接键上进行哈希。哈希过程桶上一阶段,而后再从哈希表中探索匹配的行。

上述的过程当中,在第一个阶段的构建阶段是阻塞的,也就是说在,哈希链接必须读入和处理全部的构建输入,以后才能返回行。并且这一过程是须要一块内存存储提供支持,而且利用的是哈希函数,因此相应的也会消耗CPU等。

而且上述流程过程当中通常采用的是并发处理,充分利用资源,固然系统会对哈希的数量有所限制,若是数据量超大,也会发生内存溢出等问题,而对于这些问题的解决,SQL Server有它自身的处理方式。

咱们可经过如下代码进行理解

--构建阶段
for each row R1 in the build table
begin
   calculate hash value on R1 join key(s)
   insert R1 into the appropriate hash bucket
end
--探索阶段
for each row R2 in the probe table
begin
   calculate hash value on R2 join key(s)   
   for each row R1 in the corresponding hash bucket
       if R1 joins with R2
          output(R1,R2)
end    

在哈希链接执行以前,SQL Server会估算须要多少内存来构建哈希表。基本估算的方式就是经过表的统计信息来估算,因此有时候统计信息不许确,会直接影响其运算性能。

SQL Server默认会尽力预留足够的内存来保证哈希链接成功的构建,可是有时候内存不足的状况下,就必须采起将一小部分的哈希表分配到硬盘中,这里就存入到了tempdb库中,而这一过程会反复屡次循环执行。

举个列子来看看

SELECT O.OrderID,O.OrderDate,C.CustomerID,C.ContactName
FROM Orders O JOIN Customers C
ON O.CustomerID=C.CustomerID

咱们来分析上面的执行语句,上面的执行结果经过CustomerID列进行关联,理论将最合适的应该是采用合并链接操做,可是合并链接须要排序,可是咱们在语句中没有指定Order by 选项,因此通过评估,此语句采用了哈希链接的方式进行了链接。

咱们给它加上一个显示的排序,它就选用合并链接做为最优的链接方式

咱们来总结一下这个算法的特色

  • 和合并链接同样算法复杂度基本就是分别遍历两边的数据集各一遍
  • 它不须要对数据集事先排序,也不要求上面有什么索引,经过的是哈希算法进行处理
  • 基本采起并行的执行计划的方式

 可是,该算法也有它自身的缺点,由于其利用的是哈希函数,因此运行时对CPU消耗高,一样对内存也比较大,可是它能够采用并行处理的方式,因此该算法用于超大数据表的链接查询上显示出本身独有的优点。

关于哈希算法在哈希处理过程的时候对内存的占用和分配方式,是有它本身独有哈希方法,好比:左深度树、右深度树、浓密哈希链接树等,这里不作详细介绍了,只须要知道其使用方式就能够了。

Hash Join并非一种最优的链接算法,只是它对输入不优化,由于输入数据集特别大,而且对链接符上有没有索引也没要求。其实这也是一种不得已的选择,可是该算法又有它适应的场景,尤为在OLAP的数据仓库中,在一个系统资源相对充足的环境下,该算法就获得了它发挥的场景。

固然前面所介绍的两种算法也并非一无可取,在业务的OLTP系统库中,这两种轻量级的链接算法,以其自身的优越性也得到了承认。

因此这三种算法,没有谁好谁坏,只有合适的场景应用合适的链接算法,这样才能发挥它自身的长处,而恰巧这些就是咱们要掌握的技能。

 

这三种链接算法咱们也能够显示的指定,可是通常不建议这么作,由于默认SQL Server会为咱们评估最优的链接方式进行操做,固然有时候它评估不对的时候就须要咱们本身指定了,方法以下:

 

2、聚合操做

聚合也是咱们在写T-SQL语句的时候常常遇到的,咱们来分析一下一些经常使用的聚合操做运算符的特性和可优化项。

a、标量聚合

标量聚合是一种经常使用的数据聚合方式,好比咱们写的语句中利用的如下聚合函数:MAX()、MIN()、AVG()、COUNT()、SUM()

以上的这些数据结果项的输出基本都是经过流聚合的方式产生,而且这个运算符也被称为:标量聚合

先来看一个列子

SELECT COUNT(*) FROM Orders

上面的图表就是流聚合的运算符了。

上图还有一个计算标量的运算符,这是由于在流聚合产生的结果项数据类型为Bigint类型,而默认输出为int类型,因此增长了一个类型转换的运算符。

咱们来看一个不须要转换的

SELECT MIN(OrderDate),MAX(OrderDate) FROM Orders

看一下求平均数的运算符

SELECT AVG(Freight) FROM Orders

求平均数的时候,在SQL Server执行的时候也给咱们添加了一个case when分类,防止分母为0的状况发生。

咱们来看DISTINCT下的状况下,执行计划

SELECT COUNT(DISTINCT ShipCity) FROM Orders
SELECT COUNT(DISTINCT OrderID) FROM Orders

上面相同的语句,可是产生了不一样的执行计划,只是由于发生在不一样列的数量汇总上,由于OrderID不存在重复列,因此SQL Server不须要排序直接流聚合就能够产生汇总值,而ShipCity不一样它会有重复的值,因此只能通过排序后再流聚合依次获取汇总值。

 

其实,流聚合这种算法最经常使用的方式是分组(GROUP BY)计算,上面的标量计算也是利用这个特性,只不过把总体造成了一个大组进行聚合。

我么经过以下代码理解

clear the current aggredate results
clear the current group by columns
for each input row
begin
    if the input row does not match the current group by columns
    begin
       output the current aggreagate results(if any)
       clear the current aggreagate results
       set the current group by columns to the input row
    end
   update the aggregate results with the input row
end

流聚合运算符其实过程很简单,维护一个聚合组和聚合值,依次扫描表中的数据,若是能不匹配聚合组则忽略,若是匹配,则加入到聚合组中而且更新聚合值结果项。

举个例子

SELECT ShipAddress,ShipCity,COUNT(*)
FROM Orders
GROUP BY ShipAddress,ShipCity

这里使用了流聚合,而且以前先对两列进行排序,排序的消耗老是很大。

以下代码就不会产生排序

SELECT CustomerID,COUNT(*)
FROM Orders
GROUP BY CustomerID

因此这里咱们已经总结出对于流聚合的一种优化方式:尽可能避免排序产生,而要避免排序就须要将分组(Group by)字段在索引覆盖范围内。

 

b、哈希聚合

上述的流聚合的方式须要提早排序,咱们知道排序是一个很是大的消耗过程,因此不适合大表的分组聚合操做,为了解决这个问题,又引入了另一种聚合运算:哈希聚合

所谓的哈希聚合内部的方法和本篇前面提到的哈希链接机制同样。

哈希聚合不须要排序和过大的内存消耗,而且很容易并行执行计划,利用多CPU同步进行,可是有一个缺点就是:这一过程是阻塞的,也就说哈希聚合不会产生任何结果直到完整的输入。

因此在大数据表中采用哈希聚合是一个很好的应用场景。

经过以下代码加深理解

for each input row
begin
   calculate hash value on group by columns
   check for a matching row in the hash table
   if maching row not found
      insert a new row into the hash table
   else
      update the matching row with the input row
end
--最后输出结果
ouput all rows in the hash table        

简单点将就是在进行运算匹配前,先将分组列进行哈希处理,分配至不一样的哈希桶中,而后再依次匹配,最后才输出结果。

举个例子

SELECT ShipCountry,COUNT(*)
FROM Orders
GROUP BY ShipCountry

 

这个语句颇有意思,咱们利用了ShipCountry进行了分组,咱们知道该列没有被索引覆盖,按照道理,其实选择流聚合应该也是不错的方式,跟上面咱们列举的列子同样,先对这个字段进行排序,而后利用流聚合造成结果项输出。

可是,为何这个语句SQL Server为咱们选择了哈希匹配做为了最优的算法呢!!!

我么来比较两个分组字段:ShipCountry和前面的ShipAddress

前面是国家,后面是地址,国家是不少重复的,而且只有少数的惟一值。而地址就不同了,离散型的分布,咱们知道排序是很耗资源的一件事情,可是利用哈希匹配只须要将不一样的列值进行提取就能够,因此相比性能而言,无疑哈希匹配算法在这里是略胜一筹的算法。

而上面关于这两列内容分布类型SQL Server是怎样知道的?这就是SQL Server的强大的统计信息在支撑了。

在SQL Server中并非固定的语句就会造成特定的计划,而且生成的特定计划也不是老是最优的,这和数据库现有数据表中的内容分布、数据量、数据类型等诸多因素有关,而记录这些详细信息的就是统计信息。

全部的最优计划的选择都是基于现有统计信息来评估,若是咱们的统计信息未及时更新,那么所评估出来最优的执行计划将不是最好的,有时候反而是最烂的。 

 

参考文献

结语

此篇文章先到此吧,本篇主要介绍了关于T-SQL语句调优从执行计划下手,并介绍了三个常见的链接运算符和聚合操做符,下一篇将着重介绍咱们其它最经常使用的一些运算符和调优技巧,包括:CURD等运算符、联合运算符、索引运算、并行运算等吧,关于SQL Server性能调优的内容涉及面很广,后续文章中依次展开分析。 

文章最后给出上一篇的链接

SQL Server调优系列基础篇

 

 

若是您看了本篇博客,以为对您有所收获,请不要吝啬您的“推荐”。

相关文章
相关标签/搜索