笔记59-徐 问题定位与解决方法

 笔记59-徐  问题定位与解决方法  

  1 --问题定位与解决方法     第13章 2013-2-6
  2 --思路:
  3 --1、确认是否因为做了物理I/O而导致性能不佳
  4 --如果一条语句所要访问的数据页面没有事先缓存在内存里,那么数据
  5 --页面要在语句运行的过程中,从磁盘读到内存里,这是一个很昂贵的动作。
  6 --如果语句要访问的数据页面比较多,就会产生比较长的时间消耗。
  7 --多数情况下,有数据缓存时的语句运行时间,会比没有数据缓存的时间,
  8 --相差一个到几个数量级
  9 
 10 
 11 --所以语句调优的前提,是要确认数据页面能够事先缓存在内存里。如果这个
 12 --问题得到解决后,性能还不能达到要求,才有后续调优必要。如果这个
 13 --问题解决后就能跑得足够快,那说明这个问题更是一个系统资源瓶颈问题
 14 --而不是语句本身问题。
 15 
 16 
 17 --2、确认是否是因为编译时间长而导致的性能不佳
 18 --语句的执行总时间,由编译时间和运行时间两部分组成。大部分情况下,
 19 --编译时间会远小于运行时间。而且很多时候SQL会重用执行计划,所以
 20 --语句的第一次执行有编译时间,后面的执行常常就只有运行时间了。因此,
 21 --调优的大部分精力,会花在降低运行时间上。
 22 --但是,有些语句处理的数据量比较小,可以很快做完,但是语句本身并不很
 23 --简单。SQL还是要“想一想”才能找到合适的执行计划。这样的语句,其编译
 24 --时间可能会占总时间的50%甚至更高
 25 
 26 --所以对于这类执行总时间比较短,但是在应用程序里会被反复调用的语句,就有
 27 --必要检查他们编译时间占总时间的比重。如果编译相对比较昂贵,而运行速度
 28 --又很快,调优的重点会转向如何避免重编译,或者降低编译时间。语句运行
 29 --调优的空间,往往不是很大
 30 
 31 --如果语句要访问的数据都已经缓存在内存里,而且编译时间很短,那么调优就进入
 32 --如何降低语句运行时间这个比较纯粹的课题了。而这个工作,也要分两步进行
 33 
 34 --3、确认SQLSERVER是否正确地预估了每一步的cost,选择了正确的执行计划
 35 --语句要能高效运行,前提是SQL选择的执行计划是正确的。而选择正确的执行计划
 36 --的前提,又是SQL正确地预估了每一个执行子句的cost,SQL是根据EstimateRows
 37 --的大小来预估cost的。所以简单来讲,要判断一个执行计划是否合适,最重要
 38 --的就是要判定SQL是不是正确预估了执行计划每一步返回的结果集的大小。
 39 --如果预估值和实际值相差很多(这倒是经常发生的),就要判定他是否造成
 40 --SQL选择了一个错误的执行计划。这里的判断需要一些经验,有时候也要做一些
 41 --测试,才能下结论
 42 
 43 --如果这一步SQL也没问题,就说明以现有的表结构和索引,SQL无法做到在预期
 44 --时间内完成语句运行。这时就进入最后一步,也就是纯粹的语句调优步骤
 45 
 46 --4、检查表结构和语句逻辑,确认是否有调优空间,提高语句的运行速度
 47 --调整索引,当然是语句调优的重要途径。可以通过执行计划里的每个子句的cost
 48 --找到最花时间的部分,看看是否可以通过调整索引的方法,提高运行效率。
 49 --但是对于一些语句,可能天生就比较复杂,很难降低他的运行时间,有可能
 50 --无法通过调整索引来达到目的。但是,做同样的业务逻辑可能会有不同的做法
 51 --调整表格结构或者语句的写法,往往能起到事半功倍的效果。所以要提醒DBA和
 52 --开发者,有时候要灵活解决问题。修改数据库或程序设计虽然比较麻烦,但是
 53 --可能会比光调SQL设置效果好很多
 54 
 55 
 56 
 57 --1、是否是因为做了物理I/O而导致性能不佳
 58 --用什么办法能知道一条语句的运行有没有做物理I/O呢?怎样才知道,一条语句
 59 --在数据提前缓存在内存里的前提下,要运行多久呢?这个信息DBA要打开
 60 SET STATISTICS IO ON
 61 --
 62 SET STATISTICS TIME ON
 63 --开关以后,运行语句,才能得到
 64 --例如,运行下面这段话,因为他在运行之前使用
 65 DBCC DROPCLEANBUFFERS
 66 --指令清除了缓冲池里所有缓存的页面,所以一定会做物理I/O动作。开启
 67 SET STATISTICS IO ON
 68 --
 69 SET STATISTICS TIME ON --就能知道问题的答案
 70 
 71 DBCC DROPCLEANBUFFERS
 72 --清除buffer pool里的所有缓存的数据页面
 73 GO
 74 
 75 SET STATISTICS IO ON
 76 GO
 77 
 78 SET STATISTICS TIME ON
 79 GO
 80 
 81 USE [AdventureWorks]
 82 GO
 83 
 84 SELECT
 85 DISTINCT [ProductID],
 86 [UnitPrice]
 87 FROM [dbo].[SalesOrderDetail_test]
 88 WHERE [ProductID]=777
 89 GO
 90 
 91 --DBCC 执行完毕。如果 DBCC 输出了错误信息,请与系统管理员联系。
 92 --SQL Server 分析和编译时间:
 93 --   CPU 时间 = 0 毫秒,占用时间 = 0 毫秒。
 94 --
 95 --SQL Server 执行时间:
 96 --   CPU 时间 = 0 毫秒,占用时间 = 0 毫秒。
 97 --SQL Server 分析和编译时间:
 98 --   CPU 时间 = 16 毫秒,占用时间 = 188 毫秒。
 99 --SQL Server 分析和编译时间:
100 --   CPU 时间 = 0 毫秒,占用时间 = 0 毫秒。
101 --
102 --(4 行受影响)
103 --表 'SalesOrderDetail_test'。扫描计数 5,逻辑读取 15064 次,物理读取 0 次,预读 15064 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
104 --
105 --SQL Server 执行时间:
106 --   CPU 时间 = 187 毫秒,占用时间 = 2108 毫秒。
107 
108 --检查SET STATISTICS IO ON的输出,会发现表[SalesOrderDetail_test]的
109 --“物理读取”和“预读”不为0.这意味着语句做了物理I/O。运行这个例子的机器是台
110 --I/O很慢的机器,所以语句花了11009ms才跑完
111 
112 
113 --立刻再运行一次查询,不清空缓存区
114 SET STATISTICS IO ON
115 GO
116 SET STATISTICS TIME ON
117 GO
118 USE [AdventureWorks]
119 GO
120 SELECT DISTINCT [ProductID],[UnitPrice]
121 FROM [dbo].[SalesOrderDetail_test]
122 WHERE [ProductID]=777
123 GO
124 
125 --这次由于语句刚刚跑过,内存里的数据页还在,可以感觉到速度明显不同
126 --SQL Server 分析和编译时间:
127 --   CPU 时间 = 0 毫秒,占用时间 = 0 毫秒。
128 --
129 --SQL Server 执行时间:
130 --   CPU 时间 = 0 毫秒,占用时间 = 0 毫秒。
131 --SQL Server 分析和编译时间:
132 --   CPU 时间 = 0 毫秒,占用时间 = 0 毫秒。
133 --
134 --SQL Server 执行时间:
135 --   CPU 时间 = 0 毫秒,占用时间 = 0 毫秒。
136 --SQL Server 分析和编译时间:
137 --   CPU 时间 = 0 毫秒,占用时间 = 0 毫秒。
138 --
139 --SQL Server 执行时间:
140 --   CPU 时间 = 0 毫秒,占用时间 = 0 毫秒。
141 --SQL Server 分析和编译时间:
142 --   CPU 时间 = 16 毫秒,占用时间 = 125 毫秒。
143 --SQL Server 分析和编译时间:
144 --   CPU 时间 = 0 毫秒,占用时间 = 0 毫秒。
145 --
146 --(4 行受影响)
147 --表 'SalesOrderDetail_test'。扫描计数 5,逻辑读取 15064 次,物理读取 0 次,预读 15064 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
148 --
149 --SQL Server 执行时间:
150 --   CPU 时间 = 218 毫秒,占用时间 = 1743 毫秒。
151 
152 
153 
154 --检查SET STATISTICS IO ON的输出,会发现表[SalesOrderDetail_test]的
155 --“物理读取”和“预读”都是0。这意味着语句完全没有做物理I/O
156 --语句花了262ms就跑完。可见在这台服务器上,做不做物理I/O的差别有多大
157 
158 --如果你在单个语句调优时发现性能问题,只在物理I/O的时候才出现,那需要做的事情
159 --有下面几件:
160 
161 --(1)检查生产服务器是否有内存瓶颈,是否存在经常换页的现象
162 --如果生产环境下,内存没有瓶颈,或者很少有page out/page in的动作,那说明
163 --SQL能够把数据页面维护在内存里,你看到的性能问题就不太会发生,所以无须太过忧虑
164 --如果有明显的内存瓶颈,那首先要解决的是SQL服务器层面的内存瓶颈。因为这个
165 --瓶颈不但会造成这句话运行慢,也会影响其他语句的性能
166 
167 --(2)检查这句话,和他访问的数据,是被经常使用的,还是偶尔使用的
168 --如果问题语句经常会被不同的用户调用到,那按道理他说访问的数据应该缓存在
169 --内存中。如果这句话只是被某个特定的用户或任务偶尔调用到,而他访问的数据量既大
170 --其他人又不经常使用,那SQL没有把他们放在内存里也是正常的。对这样的语句,
171 --其运行时间里有物理I/O时间是合理的。这个可能要和最终用户做一下沟通,设定
172 --他们合理的性能期望值。并且安排他们在非业务高峰期时运行,以避免影响
173 --其他关键业务
174 
175 
176 --(3)检查语句执行计划,是否能减少其访问的数据量
177 --语句访问的数据量越小,要做的物理I/O就越少,当然能够越快。从这个角度
178 --做语句本身的调优也是有用的。
179 
180 --(4)检查磁盘子系统的性能
181 --如果语句访问的数据很可能就不在内存里,而其数据量也很大,还一定要提高其性能,
182 --那唯一的出路就是提高磁盘子系统的性能了。这种情况,SQL内部的设置可能帮不了
183 --什么忙。要做的工作可能更多地会在硬件层面
184 
185 
186 
187 
188 --2、是否是因为编译时间长而导致性能不佳
189 --一般DBA需要对两类语句重点检查编译时间。一种是比较简单,长度比较短
190 --涉及表格比较少,但是在应用或任务里反复调用的语句。比较简单的语句执行
191 --起来会比较快。如果一句话执行只要100毫秒,编译也要100毫秒,那SQL
192 --花这麽多时间编译似乎有点不划算。如果能够通过执行计划重用来去除
193 --编译时间,或者通过调整数据库设计来降低编译时间,那整体的效率就能够
194 --提高40%~50%。如果语句只执行一次,从200毫秒降低到100毫秒似乎意义不大
195 --但是,如果一个任务要调用同样的语句成千上百次,效果就很明显了。还有
196 --一类,是语句本身比较复杂,或者其所基于的表格上有太多的索引可供
197 --选择,使得编译时间超过1秒,甚至更长
198 
199 --比较好的是,用户可以有两种方法检查语句的编译时间。SET STATISTICS TIME ON
200 --当然是一种方法,但是其缺点是只能在测试连接里收集,不能收集到应用发过来
201 --的语句信息。使用SQL Trace,也可以收集到语句的编译时间,而且没有限制,
202 --可以收集到任何一个连接的信息
203 
204 --下面用一个存储过程,来模拟一句会有比较长的编译时间的语句。存储过程
205 --的主体是一个三张表的查询,其中有一个“in”子句。根据代入参数@i的值,
206 --脚本会为其代入相应个数的SaleOrderID值。如果代入的值很多,“in”子句很长,
207 --语句本身就变得很长,SQL就要花比较多的时间和资源做编译
208 USE [AdventureWorks]
209 GO
210 DROP PROC LongCompile
211 GO
212 
213 CREATE PROC LongCompile(@i INT)
214 AS
215 DECLARE @cmd VARCHAR(MAX)
216 DECLARE @j INT
217 SET @j =0
218 SET @cmd='
219 select count(b.SalesOrderID),sum(p.Weight)
220 from dbo.SalesOrderHeader_test a
221 inner join dbo.SalesOrderDetail_test b
222 on a.SalesOrderID=b.SalesOrderID
223 inner join Production.Product p
224 on b.ProductID=p.ProductID
225 where a.SalesOrderID in(43659'
226 WHILE @j<@i
227 BEGIN
228 SET @cmd=@cmd+','+STR(@j+43659)
229 SET @j=@j+1
230 END
231 SET @cmd=@cmd +')'
232 EXEC(@cmd)
233 GO
234 
235 --SET STATISTICS TIME ON的检查方法前一章已经介绍过,这里不重复了。现在使用
236 --SQL Trace里的下面事件来跟踪语句的编译时间。这种方法得到的结果,虽然没有
237 --SET STATISTICS TIME ON的那么精确,但还是可以拿来参考的
238 
239 --编译会发生下面情况:
240 --(1)一个新的批处理batch开始运行前
241 --(2)一个新的存储过程stored procedure开始运行前
242 --(3)语句发生重编译recompile的时候
243 
244 --SQL Trace的事件不会直接打印出编译时间,但是你可以比较某些事件开始时间点starttime
245 --之间的间隔,算出当时的编译时间
246 
247 --如果你发现语句性能问题和编译有关系,必须考虑的方向有:
248 --(1)检查语句本身是否过于复杂,长度太长
249 --如果是,可以考虑把一句话拆成几句更简单的语句,或者用temp table来替代大的
250 --“in”子句。
251 --因为SQL是先编译整个batch,然后再开始运行第一句话
252 --因为SQL是先编译整个SP,然后再运行第一句话
253 
254 --(2)检查语句使用的表格上是不是有太多的索引
255 --索引越多,SQL要评估的执行计划就越多,花的时间越长。作为一个设计严谨的数据库,
256 --要把没有用的索引及时删除
257 
258 --(3)引导SQL尽量多重用执行计划,减少编译
259 
260 
261 --3、判断执行计划是否合适
262 --如果SQL选错了执行计划,那当然会直接影响到语句的性能。在排除了数据缓存和
263 --编译这两大因素以后,用户就要来检查SQL现在选择的执行计划是否正确。但是,
264 --什么是“正确”的执行计划呢?大部分情况下SQL计算得会比人脑要准确,他选择
265 --的执行计划是有道理的。要找出一个比他选的更好的执行计划,不是那么容易
266 
267 --但是SQL还是会犯错误。通常可以从以下这几个角度,判断现在得到的执行计划是否
268 --准确,以及有没有提高的空间
269 
270 ---------------------------------------------------------------------------------------------------
271 --预估cost的准确性
272 --之前介绍过SQL计算一个候选的执行计划开销的算法,他是根据EstimateRows、AvgRowSize
273 --和每一步要做的事情,估算出EstimateIO和EstimateCPU,然后再根据这两个值算出
274 --TotalSubtreeCost。SQL在候选的执行计划中,挑一个他算出来TotalSubtreeCost
275 --最低的。而一般AvgRowSize是不会估错的,所以,如果SQL选择的执行计划有问题,
276 --常常是因为EstimateRows估错。
277 
278 --因此判断每一个子句的TotalSubtreeCost是否准确,常常就转化为检查EstimateRows和
279 --真实Rows的差别。这两个数据,可以通过
280 SET STATISTICS PROFILE ON
281 --的输出获得,也可以在SQL Trace里showplan statistics profile事件里得到
282 
283 --需要说明的是,当SQL预估某一步不会记录返回时,他不是把EstimateRows设置为0,
284 --而是设置为1。所以EstimateRows等于1,常常意味着SQL认为该步骤不会有记录返回。
285 --如果实际的Rows不是0,也不是1,而EstimateRows等于1,那就要好好检查SQL在
286 --这里的预估开销是否准确,是否会影响到执行计划的准确性
287 
288 --之前讲过一个案例突然很慢,重建索引以后性能就会恢复正常的查询。当时说,通过
289 --分析语句执行计划和统计信息,作者将问题定位到了两个表的联接动作上。是怎麽
290 --定位到这一点呢?
291 
292 --查询的执行计划很复杂,下面的拷屏是其中的主体部分
293 
294 --可以看到查询做了很多联接动作,而且都是用nested loops完成的。在拿到执行计划
295 --以后,第一步要做的事情,不是去看哪一部分的TotalSubtreeCost最高,而是要
296 --比较Rows和EstimateRows这两列的值差别大不大。我们把EstimateRow这一列,拷贝
297 --到执行计划的开头。这样比较起来就方便多了。
298 
299 
300 --在这个执行计划里,Rows的值和EstimateRows的值有明显差距。根据SQL的预估,
301 --联接最里面的几层应该不返回记录(EstimateRows=1.0)。而实际上,他们返回
302 --了9653条记录。这麽大的结果集,用nested loops是不太合适的。直接的结果,
303 --是Executes这一列的值在很多步骤上很高。从这种现象,笔者开始怀疑他是不是
304 --问题的直接原因
305 
306 --很有利的是,还有一份查询速度正常的时候的执行计划。从这份执行计划里,可以
307 --看到正确的做法和那时的EstimateRows值
308 
309 --很明显,在运行正常的时候,SQL预估的值要高得多,所以SQL选择的是HASH MATCH JOIN
310 --和MERGE JOIN的方式。这证明了前面的猜想。在查询有性能问题的时候,SQL没有正确
311 --地估计联接返回记录的多少,所以错误地估计了nested loops联接动作的开销,选择了
312 --一个错误的执行计划
313 
314 --要强调的是,执行计划没有绝对的对错。只是一种执行计划在某种上下文,是合适的。
315 --但是在另一种上下文,可能又是不合适的。如果能够拿到一份既有EstimateRows列,
316 --又有Rows列的执行计划,那比较一下两列的差别,问题就能比较清楚。如果因为
317 --种种限制而拿不到两列的值,可能就要靠经验来猜测了
318 
319 --现在再来做一个案例。在这个案例里,读者不但要用到读取执行计划的知识,还要
320 --用到编译和重编译的知识
321 
322 --有一个查询,会返回大约12万条记录。请运行他们三次。第一次是正常运行,
323 --第二次运行之前,会先清空执行计划,然后设置语句只返回一条记录(set rowcount 1)
324 --第三次运行之前,不清空执行计划,但是设置语句返回所有记录(set rowcount 0)
325 --按道理,第一次运行和第三次运行应该速度相当,因为他们都要返回12万条记录。
326 --第二次会最快,因为他只返回一条记录
327 
328 --第一次
329 USE [AdventureWorks]
330 GO
331 DBCC DROPCLEANBUFFERS --清空缓存数据页面 buffer pool
332 GO
333 
334 DBCC freeproccache  --清空执行计划
335 GO
336 
337 SET ROWCOUNT 0
338 GO
339 SET STATISTICS PROFILE ON
340 SELECT p.[ProductID],p.[Weight]
341 FROM [dbo].[SalesOrderHeader_test] a
342 INNER JOIN [dbo].[SalesOrderDetail_test] b
343 ON a.[SalesOrderID]=b.[SalesOrderID]
344 INNER JOIN [Production].[Product] p
345 ON b.[ProductID]=p.[ProductID]
346 WHERE a.[SalesOrderID]=75124
347 
348 GO
349 ---------------------------------------------------------
350 --第二次
351 USE [AdventureWorks]
352 GO
353 DBCC freeproccache
354 GO
355 SET ROWCOUNT 1
356 GO
357 SET STATISTICS PROFILE ON
358 SELECT p.[ProductID],p.[Weight]
359 FROM [dbo].[SalesOrderHeader_test] a
360 INNER JOIN [dbo].[SalesOrderDetail_test] b
361 ON a.[SalesOrderID]=b.[SalesOrderID]
362 INNER JOIN [Production].[Product] p
363 ON b.[ProductID]=p.[ProductID]
364 WHERE a.[SalesOrderID]=75124
365 
366 GO
367 
368 -----------------------------------------------------
369 --第三次
370 USE [AdventureWorks]
371 GO
372 SET ROWCOUNT 0
373 GO
374 SET STATISTICS PROFILE ON
375 SELECT p.[ProductID],p.[Weight]
376 FROM [dbo].[SalesOrderHeader_test] a
377 INNER JOIN [dbo].[SalesOrderDetail_test] b
378 ON a.[SalesOrderID]=b.[SalesOrderID]
379 INNER JOIN [Production].[Product] p
380 ON b.[ProductID]=p.[ProductID]
381 WHERE a.[SalesOrderID]=75124
382 
383 GO
384 
385 
386 --第三次运行代价比第一次高的原因是,他重用了set rowcount1时的执行计划,而在
387 --set rowcount 0这个执行计划是不合适的。SQL在重用执行计划的时候没有考虑到
388 --set rowcount的情况,是他不够聪明的地方。希望在将来版本里,SQL能够有所改进
389 
390 -----------------------------------------------------------------------------------------------------
391 --是Index Seek还是Table Scan
392 --检查执行计划的第二个重点,是要检查SQL从表格里检索数据的时候,是否选择了
393 --合适的方法。前面介绍过SQL检索数据的方法。在不同的数据结果上,SQL会根据
394 --检索数据的多少,以及索引的结构,选择是用Index Seek还是Table Scan
395 
396 --不同结构上的数据检索方法          扫描                查找
397 --堆(没有聚集索引的表格数据页)  表扫描                 无
398 --聚集索引                        聚集索引扫描         聚集索引查找
399 --非聚集索引                      索引扫描             索引查找
400 
401 
402 --一般来说,如果检索返回的数据量,占整个表格的数据量比较小,那么用seek还是划算的
403 --所以常常说,seek会比scan要好。但是,如果检索返回的数据量差不多是整个表格的数据
404 --的一大部分,那么索引上的seek不会有什么帮助,甚至直接用scan可能会更快一些。
405 --所以也不是scan就绝对地比seek要差,不能看到scan就要想办法变成seek。关键还是要
406 --看EstimateRows和实际的Rows的大小
407 
408 --如果某个数据检索动作实际返回的行数不多,但是SQL选择了scan方法,那就要重视了,
409 --因为这种scan,会带来比较大的性能影响,会慢几十倍,几百倍,甚至上千倍。
410 --下面是一个例子
411 --第一个查询
412 USE [AdventureWorks]
413 GO
414 SET STATISTICS PROFILE ON
415 GO
416 SELECT COUNT(b.[CarrierTrackingNumber])
417 FROM dbo.[SalesOrderDetail_test] b
418 WHERE b.[SalesOrderDetailID]>10000 AND
419 b.[SalesOrderDetailID]<=10100
420 GO
421 -------------------------------------
422 --第二个查询
423 USE [AdventureWorks]
424 GO
425 SET STATISTICS PROFILE ON
426 GO
427 SELECT COUNT(b.[CarrierTrackingNumber])
428 FROM [dbo].[SalesOrderDetail_test] b
429 WHERE CONVERT(NUMERIC(9,3),b.[SalesOrderDetailID]/100)=100
430 GO
431 
432 --第一个查询很简单,[SalesOrderDetailID]字段是表格的聚集索引所在字段
433 --所以很自然地,SQL使用的是clustered index seek
434 
435 --第二个查询的意思,和第一个一样,但是写法不同,在[SalesOrderDetailID]
436 --字段上做了计算。其结果是直接妨碍了SQL在这个字段的索引上使用seek
437 --SQL将不得不用scan的方法,找出所有符合条件的记录。但是这里,
438 --SQL做得还是比较聪明的
439 
440 --SQL知道,如果要去scan整个表,是一件非常浩大的工程.所以他找找自己
441 --有没有其他索引覆盖了[SalesOrderDetailID]这个字段。因为索引只
442 --包含了表格的一小部分字段,占用的页面数量会比表格本身要小很多。
443 --去scan这样的索引,可以大大降低scan的消耗。非聚集索引SalesOrderDetail_test_NCL
444 --就是这样的一个索引。SQL选择了在他身上做scan。这是上面这个
445 --index scan的来源。美中不足的是,这个非聚集索引没有覆盖[CarrierTrackingNumber]
446 --这个字段。所以SQL还要根据挑出来的记录的[SalesOrderDetailID]
447 --值,到聚集索引SalesOrderDetail_test_CL上去找[CarrierTrackingNumber]
448 --这是第二个clustered index seek的来源
449 
450 --这样做,显然比第一种做法要费事很多,所以两句话的性能是有很大差别的
451 --第一句话只要3个毫秒就做完了,而第二句话用953毫秒。两者相差300多倍,
452 --CPU和Reads的消耗也有很大差别
453 
454 --那scan就一定比seek差麽?还是用这张表,再试一个例子。先创建一个存储过程,
455 --在[SalesOrderDetail_test]表上返回指定的6个SalesOrderID的所有记录的
456 --COUNT(b.[CarrierTrackingNumber])值
457 
458 USE [AdventureWorks]
459 GO
460 DROP PROC Scan_Seek
461 GO
462 CREATE PROC Scan_Seek(@i INT)
463 AS
464 SELECT COUNT(b.[CarrierTrackingNumber])
465 FROM [dbo].[SalesOrderDetail_test] b
466 WHERE b.[SalesOrderDetailID]>@i AND
467 b.[SalesOrderID]<@i+7
468 GO
469 
470 
471 --在[SalesOrderID]等于43659到75123之间的记录里,每个[SalesOrderID]
472 --平均有三四条记录。但是在75124到75132之间,每个[SalesOrderID]都有
473 --12万条记录。所以对于这个查询,如果@i的值小于75115,那么检索条件
474 --只会返回几十条记录,占表格的很小一部分。如果@i的值等于75124,那
475 --这个查询就会返回表格的绝大多数记录。在这两种情况下,SQL会选择
476 --同样的执行计划麽?如果是的,结果会怎样?
477 
478 --用下面的脚本测试一下
479 USE [AdventureWorks]
480 GO
481 [sys].[sp_recompile] @objname = N'Scan_Seek' -- nvarchar(776)
482 GO
483 SET STATISTICS PROFILE ON
484 EXEC [dbo].[Scan_Seek]  @i = 75124 -- int
485 GO
486 --------------------------------------------------------------
487 [sys].[sp_recompile] @objname = N'Scan_Seek' -- nvarchar(776)
488 GO
489 SET STATISTICS PROFILE ON
490 EXEC [dbo].[Scan_Seek]  @i = 43659 -- int
491 GO
492 -------------------------------------------------------------------
493 SET STATISTICS PROFILE ON
494 EXEC [dbo].[Scan_Seek]  @i = 75124 -- int
495 GO
496 
497 --第一次运行和第二次运行前,因为都运行了[sp_recompile],存储过程
498 --都发生了编译。第一次要计算的数据量比较大,用了808毫秒。第二次
499 --数据量小,只用了6毫秒。这个差异是数据量的差异造成的。
500 
501 --但是第三次运行,代入的参数和第一次一样,却用了5391毫秒。CPU时间
502 --和Reads要高很多。这是为什麽呢?分析一下两次的执行计划差异
503 
504 --第一次运行的时候,SQL选择的是一个clustered index scan的检索方法,
505 --直接scan整个表格,把要取的数据都找出来,最后返回727902行。相对于
506 --数据量,这是一个比较合适的做法
507 
508 --第二次运行和第三次运行的执行计划是一样的,SQL选择SalesOrderDetail_test_NCL
509 --这个索引,找出所有 b.[SalesOrderDetailID]>@i AND b.[SalesOrderID]<@i+7
510 --的记录。索引能够返回SalesOrderID的值,但不能返回CarrierTrackingNumber
511 --的值。所以光seek这个索引不能完成查询。SQL还要根据挑出来的记录的
512 --SalesOrderDetailID值,到聚集索引SalesOrderDetail_test_CL上去找
513 --CarrierTrackingNumber。所以这里有一个clustered index seek,也有
514 --一个nested loops(其实这个nested loops就是一个bookmark lookup)
515 
516 --这样的方法当b.[SalesOrderDetailID]>@i AND b.[SalesOrderID]<@i+7
517 --的记录比较少的时候,是比较有效的。但是像现在这种情况,符号条件
518 --的记录有727902行,SQL在聚集索引上就不得不做727902次lookup。所以
519 --最后的代价,反而比第一次运行,做整个表格的scan还要慢。这是一个
520 --很好的seek反而比scan要慢的例子
521 
522 --因此,用户还是要比较实际返回行数和表格的整体行数,具体看,到底
523 --在语句的上下文,是scan好,还是seek好
524 
525 
526 --------------------------------------------------------------------------------
527 --是nested loops还是hash (merge) join
528 
529 
530 --之前详细介绍了SQL的三种join方式,也提到了,nested loops比较合适于
531 --联接的双方结果集比较小的情况,而hash(merge)join 适合结果集比较大
532 --的情况。做这麽详细的介绍,是因为很多SQL没有选对执行计划,而导致
533 --性能问题,都是因为误选了nested loops导致的。当outer table比较大
534 --时,使用nested loops方法,inner table会被loop很多次,导致执行
535 --复杂度急剧增加
536 
537 --前面的例子,已经对这个问题有所体现。现在再来写一个例子,加深一下印象
538 --同样还是一个存储过程
539 USE [AdventureWorks]
540 GO
541 DROP PROC Sniff
542 GO
543 CREATE PROC Sniff(@i INT)
544 AS
545 SELECT *
546 FROM [dbo].[SalesOrderHeader_test] a
547 INNER JOIN [dbo].[SalesOrderDetail_test] b
548 ON a.[SalesOrderID]=b.[SalesOrderID]
549 INNER JOIN [Production].[Product] p
550 ON b.[ProductID]=p.[ProductID]
551 WHERE a.[SalesOrderID]=@i
552 GO
553 
554 --然后运行一下
555 USE [AdventureWorks]
556 GO
557 DBCC freeproccache
558 GO
559 SET STATISTICS TIME ON
560 SET STATISTICS PROFILE ON
561 EXEC [dbo].[Sniff] @i = 50000 -- int
562 GO
563 ----------------------------------------------------
564 USE [AdventureWorks]
565 GO
566 DBCC freeproccache
567 GO
568 SET STATISTICS TIME ON
569 SET STATISTICS PROFILE ON
570 EXEC [dbo].[Sniff] @i = 75124 -- int
571 GO
572 
573 
574 --第二次以75124为参数的运行要花3125毫秒。执行计划里都是用nested loops
575 
576 --一般来说,如果看到一个执行计划的Executes值很大,又和一个nested loops
577 --相关,那就应该好好研究一下这个nested loops是否合适
578 
579 
580 ------------------------------------------------------------------------------
581 --Filter运算的位置
582 --在一句查询里,常见的是几个表格做联接,同时又有一些where子句filter掉
583 --一些记录。那么是先filter掉记录,再做联接好呢,还是先做联接,再filter呢?
584 --两种方法都能计算出正确结果。但一般来讲,先filter掉一些记录,使得做联接
585 --的记录集小一点,会大大降低联接的消耗。所以filter先做,会提高查询效率
586 --在检查执行计划的时候,用户也要看看,是不是SQL及时做了filter
587 
588 --现在有两个查询。对用户来讲,他们是一样的,只不过第二个查询把p.ProductID
589 --加了个1以后再做比较。返回的结果集也会一样
590 --查询一
591 USE [AdventureWorks]
592 GO
593 SET STATISTICS PROFILE ON
594 GO
595 SELECT COUNT(b.[ProductID])
596 FROM [dbo].[SalesOrderHeader_test] a
597 INNER JOIN [dbo].[SalesOrderDetail_test] b
598 ON a.[SalesOrderID]=b.[SalesOrderID]
599 INNER JOIN  [Production].[Product] p
600 ON b.[ProductID]=p.[ProductID]
601 WHERE p.[ProductID] BETWEEN 758 AND 800
602 GO
603 -----------------------------------------------
604 --查询二
605 USE [AdventureWorks]
606 GO
607 SET STATISTICS PROFILE ON
608 GO
609 SELECT COUNT(b.[ProductID])
610 FROM [dbo].[SalesOrderHeader_test] a
611 INNER JOIN [dbo].[SalesOrderDetail_test] b
612 ON a.[SalesOrderID]=b.[SalesOrderID]
613 INNER JOIN  [Production].[Product] p
614 ON b.[ProductID]=p.[ProductID]
615 WHERE (p.[ProductID]+1) BETWEEN 759 AND 801
616 GO
617 
618 
619 --但是两条语句的执行效率却有很大差别,第二条比第一条慢一倍还要多,
620 --CPU时间也高。这是为什麽呢?
621 --这里要分析这两句话的执行计划。先大致看一下,SQL在做返回结果集大小预估
622 --的时候,做得是不是准确。为了方便比较,我把EstimateRows和TotalSubtreeCost
623 --这两列拷贝到了前面
624 
625 --两个查询的执行计划区别
626 --不管是第一句还是第二句,他们的EstimateRows和Rows的值都比较接近,所以SQL
627 --在cost预估上是准确的。现在可以通过分析TotalSubtreeCost,来找到语句最花
628 --资源的地方。
629 --这两句的主要消耗,都在一个hash match的联接上。但是两句的消耗又不一样。
630 --第一句的hash match,是一个31474的结果集和一个225990的结果集联接,cost是
631 --15.59,结果集大小是225990行。第二句的hash match,是一个31474的结果集和
632 --一个1213170的结果集联接,cost是19.09,大小是1213170行。他们的子步骤的
633 --cost是一样的,0.63和12.49。所以cost的差别就在hash match这个动作上,
634 --而原因就是联接双方的结果集,第二句的要比第一句大得多
635 
636 --让我们仔细研究这两个执行计划,看看是什么导致了他们的差别
637 
638 --第一句的做法,是dbo.SalesOrderDetail_test和dbo.SalesOrderHeader_test
639 --这两张表先做join,再和product表做join。有意思的是,虽然语句里只有一个where
640 --子句:WHERE p.[ProductID] BETWEEN 758 AND 800,但是执行计划里可以看到
641 --两个filter动作。一个在dbo.SalesOrderDetail_test上,一个在Product上。
642 --这是因为SQL发现这两张表将要通过ON b.[ProductID]=p.[ProductID]
643 --做联接,所以在Product上的条件,同样适合在dbo.SalesOrderDetail_test上
644 --这样,SQL先在dbo.SalesOrderDetail_test上做一个filter,结果集就小得多
645 --再做join,花费就能节省不少
646 
647 
648 --第二句的做法,也是SalesOrderHeader_test和SalesOrderDetail_test
649 --这两张表先做join,再和Product做join。但是filter动作只发生在Product上
650 --没有发生在SalesOrderDetail_test上。所以
651 --SalesOrderHeader_test和SalesOrderDetail_test做联接的时候,结果集会大一些
652 --产生这种现象的原因,是语句的where子句的写法:
653 --WHERE (p.[ProductID]+1) BETWEEN 759 AND 801
654 --SQL没有办法把这样的filter也适用在SalesOrderDetail_test这张表上
655 
656 --从这个例子,可以看出filter动作的位置对语句性能的影响
657 
658 ---------------------------------------------------------------------------------------------
659 --确认问题产生的原因
660 --现在总结一下产生问题的原因有哪些:
661 --(1)预估返回结果集大小(EstimateRows)不准确,导致执行计划实际TotalSubtreeCost
662 --比预估的高很多
663 --统计信息不存在,或者没有及时更新,是产生这个问题的主要原因。
664 --应对办法:开启“自动创建统计信息”,“自动更新统计信息”
665 --如果这样还不能保证统计信息的精确性,可以定义一个任务,定期更新统计信息
666 
667 
668 --子句太过复杂,也可能使SQL猜不出一个准确的值,只好猜一个平均数。比如
669 --where子句里对字段做计算,代入函数等行为,都可能会影响SQL预估的准确性。
670 --如果发现这种情况,就要想办法简化语句,降低复杂度,提高效率
671 --当语句代入的变量值是一个参数,而SQL在编译的时候可能不知道这个参数的值,
672 --只好根据某些规则,“猜”一个预估值,这也会影响到预估的准确性。这个话题
673 --就是“参数嗅探”
674 
675 
676 --(2)语句重用了一个不合适的执行计划
677 --SQL的执行计划重用机理,是一次编译,多次重用。根据代入的第一个参数值进行编译
678 --以后不管参数值是多少,都重用根据前面的那个值编译出来的执行计划。这对一些
679 --数据分布比较均匀的表格是没有问题的,例如我们的dbo.SalesOrderHeader_test
680 --不管你代入什么值,返回的结果集数量都差不多
681 
682 --但是,有些表格的数据分布不均匀,例如前面使用的dbo.SalesOrderDetail_test。
683 --他在某些值上,重复的记录很少,但是在另外一些值上,重复的记录又很多。这
684 --导致了对于不同的值,SQL必须使用不同的执行计划才能达到最优效率。如果
685 --重用的执行计划不合适,就会出现性能问题。
686 
687 --关于这个话题:参数嗅探会说到
688 
689 
690 --(3)筛选子句写得不太合适,妨碍SQL选取更优的执行计划
691 --当语句要筛选掉一些记录时,索引会帮上忙。一般来讲,筛选动作做得越早,越能
692 --提高效率。SQL对筛选条件(search argument/SARG)的写法有一定建议
693 --筛选条件应该采用以下格式之一:
694 --列名 运算符 <常量或变量>
695 --<常量或变量>  运算符 列名
696 
697 --例如:
698 name='annie'
699 amount>4000
700 6000<amount
701 department='hr'
702 
703 --SARG运算符包括:=、>、<、>=、<=、in、between、like(在进行前缀匹配时,
704 --如like'john%')
705 --SARG可以包括由AND联接的多个条件。
706 
707 --SARG不但可以是匹配特定值的查询,
708 --例如:
709 name='annie'
710 
711 --还可以是匹配一定范围的值的查询,例如:
712 amount>4000 and amount<6000
713 customerid IN('action','about')
714 
715 --对于不是有SARG运算符的表达式,索引是没有用的,SQL对它们很难使用
716 --比较优化的做法。非SARG运算符包括NOT、<>、NOT EXISTS、NOT IN、NOT LIKE
717 --和内部函数,例如:
718 CONVERT(),UPPER()等
719 
720 --当确认了执行计划出问题的原因,就可以对症下药,引导SQL总是选择一个
721 --好的执行计划,当然在有些情况下,基于现有语句和数据库结构,很难
722 --选择更好的执行计划。那就要通过修改数据库结构,或者修改语句设计
723 --来解决问题,提高效率

 

posted @ 2013-07-29 08:57  桦仔  阅读(637)  评论(0编辑  收藏  举报