博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

SQL Server 优化-执行计划

Posted on 2016-10-25 16:49  徐正柱-  阅读(1386)  评论(0编辑  收藏  举报

  对于SQL Server的优化来说,优化查询可能是很常见的事情。由于数据库的优化,本身也是一个涉及面比较的广的话题, 因此本文只谈优化查询时如何看懂SQL Server查询计划。毕竟我对SQL Server的认识有限,如有错误,也恳请您在发现后及时批评指正。

  首先,打开【SQL Server Management Studio】,输入一个查询语句看看SQL Server是如何显示查询计划的吧。

set showplan_all on:将执行计划的信息写入到一个表中,其中包含的一些估计的值有:StmtText, StmtId, NodeId, Parent, PhysicalOp, LogicalOp, Argument, Defi nedValues,EstimateRows, EstimateIO, EstimateCPU, AvgRowSize, TotalSubtreeCost, OutputList,Warnings, Type, Parallel, and EstimateExecutions。

set statistics profile on :选项会产生一个实际的计划。设置这个选项为ON的时候显示的结果和设置SHOWPLAN_ALL为ON差不多,不过多了两个属性Rosw和Executes,表示实际的行数和运行行数。

注意:在命名用set showplan_all on或set statistics profile on 时,要先关闭对方,即set statistics profile  off  或 set showplan_all off

 

use Portal
set statistics profile on

 Select a.*,d.JobsApplyID
 From Whir_U_JobsApply a 
 left join (select * from Whir_U_ReviewProc 
                    where ISNULL(JobsApplyID,'')<>'') d 
                    on convert(int,d.JobsApplyID)=convert(int,a.Whir_U_JobsApply_PID)
 Where 1=1 And a.IsDel='false'
示例

  从这个图中,我们至少可以得到3个有用的信息:

  1. 哪些执行步骤花费的成本比较高。显然,最右边的二个步骤的成本是比较高的。

  2. 哪些执行步骤产生的数据量比较多。对于每个步骤所产生的数据量, SQL Server的执行计划是用【线条粗细】来表示的,因此也很容易地从分辨出来。

  3. 每一步执行了什么样的动作。

  对于一个比较慢的查询来说,我们通常要知道哪些步骤的成本比较高,进而,可以尝试一些改进的方法。 一般来说,如果您不能通过:提高硬件性能或者调整OS,SQL Server的设置之类的方式来解决问题,那么剩下的可选方法通常也只有以下这些了:

  1. 为【scan】这类操作增加相应字段的索引。

  2. 有时重建索引或许也是有效的,具体情形请参考后文。

  3. 调整语句结构,引导SQL Server采用其它的查询方案去执行。

  4. 调整表结构(分表或者分区)。

  下面再来说说一些很重要的理论知识,这些内容对于执行计划的理解是很有帮助的。

一、SQL Server 查找记录的方法

  我们先说说SQL Server的索引了。SQL Server有二种索引:聚集索引和非聚集索引。

  二者的差别在于:

  【聚集索引】直接决定了记录的存放位置, 或者说:根据聚集索引可以直接获取到记录。

  【非聚集索引】保存了二个信息:1.相应索引字段的值,2.记录对应聚集索引的位置(如果表没有聚集索引则保存记录指针)。

  因此,如果能通过【聚集索引】来查找记录,显然也是最快的。

  SQL Server 会有以下方法来查找您需要的数据记录:

1. 【Table Scan】:遍历整个表,查找所有匹配的记录行。这个操作将会一行一行的检查,当然,效率也是最差的。

2. 【Index Scan】:根据索引,从表中过滤出来一部分记录,再查找所有匹配的记录行,显然比第一种方式的查找范围要小,因此比【Table Scan】要快。

3. 【Index Seek】:根据索引,定位(获取)记录的存放位置,然后取得记录,因此,比起前二种方式会更快。

4. 【Clustered Index Scan】:和【Table Scan】一样。注意:不要以为这里有个Index,就认为不一样了。 其实它的意思是说:按聚集索引来逐行扫描每一行记录,因为记录就是按聚集索引来顺序存放的。 而【Table Scan】只是说:要扫描的表没有聚集索引而已,因此这二个操作本质上也是一样的。

5. 【Clustered Index Seek】:直接根据聚集索引获取记录,最快!

  所以,当发现某个查询比较慢时,可以首先检查哪些操作的成本比较高,再看看那些操作在查找记录时, 是不是【Table Scan】或者【Clustered Index Scan】,如果确实和这二种操作类型有关,则要考虑增加索引来解决了。 不过,增加索引后,也会影响数据表的修改动作,因为修改数据表时,要更新相应字段的索引。所以索引过多,也会影响性能。 还有一种情况是不适合增加索引的:某个字段用0或1表示的状态。例如可能有绝大多数是1,那么此时加索引根本就没有意义。 这时只能考虑为0或者1这二种情况分开来保存了,分表或者分区都是不错的选择。

  如果不能通过增加索引和调整表来解决,那么可以试试调整语句结构,引导SQL Server采用其它的查询方案去执行。 这种方法要求: 1.对语句所要完成的功能很清楚, 2.对要查询的数据表结构很清楚, 3.对相关的业务背景知识很清楚。 如果能通过这种方法去解决,当然也是很好的解决方法了。不过,有时SQL Server比较智能,即使你调整语句结构,也不会影响它的执行计划。

  如何比较二个相同功能的SQL语句的性能好坏呢,我建议采用二种方法: 1. 直接把二个查询语句放在【SQL Server Management Studio】,然后去看它们的【执行计划】,SQL Server会以百分比的方式告诉你二个查询的【查询开销】。 这种方法简单,通常也是可以参考的,不过,有时也会不准,具体原因请接着往下看(可能索引统计信息过旧)。 2. 根据真实的程序调用,写相应的测试代码去调用:这种方法就麻烦一些,但是它更能代表现实调用情况, 得到的结果也是更具有参考价值的,因此也是值得的。

二、SQL Server Join 方式

  在SQL Server中,每个join命令,都会在内部执行时采用三种更具体的方式来运行:

1. 【Nested Loops join】,如果一个联接输入很小,而另一个联接输入很大而且已在其联接列上创建了索引, 则索引 Nested Loops 连接是最快的联接操作,因为它们需要的 I/O 和比较都最少。

  嵌套循环联接也称为“嵌套迭代”,它将一个联接输入用作外部输入表(显示为图形执行计划中的顶端输入),将另一个联接输入用作内部(底端)输入表。外部循环逐行处理外部输入表。内部循环会针对每个外部行执行,在内部输入表中搜索匹配行。可以用下面的伪码来理解:

foreach(row r1 in outer table)
    foreach(row r2 in inner table)
        if( r1, r2 符合匹配条件 )
            output(r1, r2);
伪码示例:

  最简单的情况是,搜索时扫描整个表或索引;这称为“单纯嵌套循环联接”。如果搜索时使用索引,则称为“索引嵌套循环联接”。如果将索引生成为查询计划的一部分(并在查询完成后立即将索引破坏),则称为“临时索引嵌套循环联接”。查询优化器考虑了所有这些不同情况。 

  如果外部输入较小而内部输入较大且预先创建了索引,则嵌套循环联接尤其有效。在许多小事务中(如那些只影响较小的一组行的事务),索引嵌套循环联接优于合并联接和哈希联接。但在大型查询中,嵌套循环联接通常不是最佳选择。

 

2.Merge Join】,如果两个联接输入并不小但已在二者联接列上排序(例如,如果它们是通过扫描已排序的索引获得的),则合并联接是最快的联接操作。如果两个联接输入都很大,而且这两个输入的大小差不多,则预先排序的合并联接提供的性能与哈希联接相近。但是,如果这两个输入的大小相差很大,则哈希联接操作通常快得多。

  合并联接要求两个输入都在合并列上排序,而合并列由联接谓词的等效 (ON) 子句定义。通常,查询优化器扫描索引(如果在适当的一组列上存在索引),或在合并联接的下面放一个排序运算符。在极少数情况下,虽然可能有多个等效子句,但只用其中一些可用的等效子句获得合并列。

  由于每个输入都已排序,因此 Merge Join 运算符将从每个输入获取一行并将其进行比较。例如,对于内联接操作,如果行相等则返回。如果行不相等,则废弃值较小的行并从该输入获得另一行。这一过程将重复进行,直到处理完所有的行为止。

  合并联接操作可以是常规操作,也可以是多对多操作。多对多合并联接使用临时表存储行(会影响效率)。如果每个输入中有重复值,则在处理其中一个输入中的每个重复项时,另一个输入必须重绕到重复项的开始位置。 可以创建唯一索引告诉SQL Server不会有重复值。

如果存在驻留谓词,则所有满足合并谓词的行都将对该驻留谓词取值,而只返回那些满足该驻留谓词的行。

  合并联接本身的速度很快,但如果需要排序操作,选择合并联接就会非常费时。然而,如果数据量很大且能够从现有 B 树索引中获得预排序的所需数据,则合并联接通常是最快的可用联接算法。

 

3. 【Hash Join】,哈希联接可以有效处理未排序的大型非索引输入。它们对复杂查询的中间结果很有用,因为: 1. 中间结果未经索引(除非已经显式保存到磁盘上然后创建索引),而且通常不为查询计划中的下一个操作进行适当的排序。 2. 查询优化器只估计中间结果的大小。由于对于复杂查询,估计可能有很大的误差,因此如果中间结果比预期的大得多,则处理中间结果的算法不仅必须有效而且必须适度弱化。

  哈希联接可以减少使用非规范化。非规范化一般通过减少联接操作获得更好的性能,尽管这样做有冗余之险(如不一致的更新)。哈希联接则减少使用非规范化的需要。哈希联接使垂直分区(用单独的文件或索引代表单个表中的几组列)得以成为物理数据库设计的可行选项。

  哈希联接有两种输入:生成输入和探测输入。查询优化器指派这些角色,使两个输入中较小的那个作为生成输入。

  哈希联接用于多种设置匹配操作:内部联接;左外部联接、右外部联接和完全外部联接;左半联接和右半联接;交集;联合和差异。此外,哈希联接的某种变形可以进行重复删除和分组,例如 SUM(salary) GROUP BY department。这些修改对生成和探测角色只使用一个输入。

  哈希联接又分为3个类型:内存中的哈希联接、Grace 哈希联接和递归哈希联接。

  内存中的哈希联接:哈希联接先扫描或计算整个生成输入,然后在内存中生成哈希表。根据计算得出的哈希键的哈希值,将每行插入哈希存储桶。如果整个生成输入小于可用内存,则可以将所有行都插入哈希表中。生成阶段之后是探测阶段。一次一行地对整个探测输入进行扫描或计算,并为每个探测行计算哈希键的值,扫描相应的哈希存储桶并生成匹配项。

  Grace 哈希联接:如果生成输入大于内存,哈希联接将分为几步进行。这称为“Grace 哈希联接”。每一步都分为生成阶段和探测阶段。首先,消耗整个生成和探测输入并将其分区(使用哈希键上的哈希函数)为多个文件。对哈希键使用哈希函数可以保证任意两个联接记录一定位于相同的文件对中。因此,联接两个大输入的任务简化为相同任务的多个较小的实例。然后将哈希联接应用于每对分区文件。

  递归哈希联接:如果生成输入非常大,以至于标准外部合并的输入需要多个合并级别,则需要多个分区步骤和多个分区级别。如果只有某些分区较大,则只需对那些分区使用附加的分区步骤。为了使所有分区步骤尽可能快,将使用大的异步 I/O 操作以便单个线程就能使多个磁盘驱动器繁忙工作。

  在优化过程中不能始终确定使用哪种哈希联接。因此,SQL Server 开始时使用内存中的哈希联接,然后根据生成输入的大小逐渐转换到 Grace 哈希联接和递归哈希联接。 如果优化器错误地预计两个输入中哪个较小并由此确定哪个作为生成输入,生成角色和探测角色将动态反转。哈希联接确保使用较小的溢出文件作为生成输入。这一技术称为“角色反转”。至少一个文件溢出到磁盘后,哈希联接中才会发生角色反转。

  说明:您也可以显式的指定联接方式,SQL Server会尽量尊重您的选择。比如你可以这样写:inner loop join, left outer merge join, inner hash join 但是,我还是建议您不要这样做,因为SQL Server的选择基本上都是正确的,不信您可以试一下。

 

三、执行计划执行过程

  前面的图片,它反映了SQL Server在执行某个查询的执行计划,但它反映的信息可能不太细致,当然,您可以把鼠标指标移动某个节点上,会有以下信息出现:

执行计划指标值

logical operation:基于微软查询处理概念模型的逻辑操作。例如,联接运算符的physical operation属性表示联接算法(nested loops,merge ,hash)物理运算符
logical operation属性表示逻辑联接类型(Inner join,outer join,semi join 等等)逻辑运算符
如果没有与该运算符关联的逻辑操作,则这项度量的值与physical operation相同

actual number of rows:从该运算符实际返回的行数(只显示在实际的计划中)
estimated I/O cost和estimated cpu cost:运算符在特定资源上的估计成本(I/O或CPU)这两个度量将帮助你确定运算符是否是I/O密集或CPU密集的

例如,你可以看到clustered index seek运算符主要与I/O有关,而hash match运算符主要与cpu有关

estimated operator cost:执行该操作的成本
estimated subtree cost:如前所述,他表示到当前节点为止整个子树的累积成本
estimated number of rows:该运算符预计的返回行数。在有些情况下,通过观察实际行数和估计行数之间的差异,你可以找出因统计信息不足或其他原因而导致的成本问题
estimated row size:你可能会奇怪为什么在实际的查询计划中没有显示该属性的实际值。因为你的表可能包含可变长度类型,表中行的大小各异
actual rebinds和actual rewinds:这两个度量仅与作为nested loops联接内侧的运算符有关,在其他运算符中,rebinds将显示为1,rewinds将显示为0
它们表示内部init方法被调用的次数。重新绑定次数和重绕次数之和等于联接外侧所处理的行数。重新绑定意味着联接的一个或多个参数发生更改后,必须重新计划

 

  【Rows】:表示在一个执行步骤中,所产生的记录条数。(真实数据,非预期)
  【Executes】:表示某个执行步骤被执行的次数。(真实数据,非预期)
  【Stmt Text】:表示要执行的步骤的描述。
  【EstimateRows】:表示要预期返回多少行数据。