笔记52-徐 读懂语句运行的统计信息

笔记52-徐 读懂语句运行的统计信息

  1 --读懂语句运行的统计信息 2013-1-2 P397
  2 
  3 --对于语句的运行,除了执行计划本身,还有一些其他因素要考虑,例如
  4 --语句的编译时间、执行时间、做了多少次磁盘读等。如果DBA能够把
  5 --问题语句单独测试运行,可以在运行前打开下面这三个开关,收集
  6 --语句运行的统计信息。这些信息对分析问题也很有价值
  7 
  8 SET STATISTICS TIME ON
  9 SET STATISTICS IO ON
 10 SET STATISTICS PROFILE ON
 11 
 12 --要说明的是,后面的测试结果,是在一台很普通的PC上获得的。读者机器
 13 --上得到的结果,可能会比书上的快
 14 
 15 --SET STATISTICS TIME ON-------------------------------------------
 16 --请先来看看SET STATISTICS TIME ON会返回什么信息。先运行语句:
 17 DBCC DROPCLEANBUFFERS
 18 --清除buffer pool里的所有缓存数据
 19 DBCC freeproccache
 20 GO
 21 
 22 --清除buffer pool里的所有缓存的执行计划
 23 SET STATISTICS TIME ON
 24 GO
 25 USE [AdventureWorks]
 26 GO
 27 SELECT DISTINCT([ProductID]),[UnitPrice] FROM [dbo].[SalesOrderDetail_test]
 28 WHERE [ProductID]=777
 29 --UNION
 30 --SELECT [ProductID],[UnitPrice] FROM [dbo].[SalesOrderDetail_test]
 31 --WHERE [ProductID]=777
 32 GO
 33 SET STATISTICS TIME OFF
 34 GO
 35 
 36 
 37 --除了结果集之外,SQL还会返回下面这两段信息
 38 --SQL Server 分析和编译时间:
 39 --   CPU 时间 = 15 毫秒,占用时间 = 104 毫秒。
 40 --SQL Server 分析和编译时间:
 41 --   CPU 时间 = 0 毫秒,占用时间 = 0 毫秒。
 42 --
 43 --(4 行受影响)
 44 --
 45 --SQL Server 执行时间:
 46 --   CPU 时间 = 171 毫秒,占用时间 = 1903 毫秒。
 47 --SQL Server 分析和编译时间:
 48 --   CPU 时间 = 0 毫秒,占用时间 = 0 毫秒。
 49 
 50 --1、CPU时间
 51 --这个值的含义指的是在这一步,SQL所花的纯CPU时间是多少。也就是说,语句花了
 52 --多少CPU资源
 53 
 54 --2、占用时间
 55 --此值指这一步一共用了多少时间。也就是说,这是语句运行的时间长短
 56 --有些动作会发生I/O操作,产生了I/O等待,或者是遇到阻塞、产生了阻塞等待。
 57 --总之时间用掉了,但是没有用CPU资源。所以占用时间比CPU时间长是很正常的
 58 --但是CPU时间是语句在所有CPU上的时间总和。如果语句使用了多颗CPU,而其他
 59 --等待几乎没有,那么CPU时间大于占用时间也是正常的
 60 
 61 --3、分析和编译时间
 62 --这一步,就是语句的编译时间。由于语句运行之前清空了所有执行计划,SQL必须要对
 63 --他编译。这里的编译时间就不为0了。由于编译主要是CPU的运算,所以一般CPU时间
 64 --和占用时间是差不多的。如果这里相差比较大,就有必要看看SQL在系统资源上有没有
 65 --瓶颈了。
 66 
 67 --这里他们是一个15毫秒,一个是104毫秒
 68 
 69 
 70 --4、SQL执行时间
 71 --语句真正运行的时间。由于语句是第一次运行,SQL需要把数据从磁盘读到内存里,这里
 72 --语句的运行发生了比较长的I/O等待。所以这里的CPU时间和占用时间差别就很大了,一个
 73 --是171毫秒,而另一个是1903毫秒
 74 
 75 --总的来讲,这条语句花了104+1903+186=2193毫秒,其中CPU时间为15+171=186毫秒。语句的主要时间
 76 --应该是都花在了I/O等待上
 77 
 78 --现在再做一遍语句,但是不清除任何缓存
 79 SET STATISTICS TIME ON
 80 GO
 81 
 82 SELECT DISTINCT([ProductID]),[UnitPrice] FROM [dbo].[SalesOrderDetail_test]
 83 WHERE [ProductID]=777
 84 --UNION
 85 --SELECT [ProductID],[UnitPrice] FROM [dbo].[SalesOrderDetail_test]
 86 --WHERE [ProductID]=777
 87 GO
 88 SET STATISTICS TIME OFF
 89 GO
 90 
 91   
 92 --这次比上次快很多。输出时间统计信息是:
 93 --SQL Server 分析和编译时间:
 94 --   CPU 时间 = 0 毫秒,占用时间 = 0 毫秒。
 95 --SQL Server 分析和编译时间:
 96 --   CPU 时间 = 0 毫秒,占用时间 = 0 毫秒。
 97 --
 98 --(4 行受影响)
 99 --
100 --SQL Server 执行时间:
101 --   CPU 时间 = 156 毫秒,占用时间 = 169 毫秒。
102 --SQL Server 分析和编译时间:
103 --   CPU 时间 = 0 毫秒,占用时间 = 0 毫秒。
104 
105 
106 --由于执行计划被重用,“SQL分析和编译时间” CPU时间是0,占用时间是0
107 
108 
109 --由于数据已经缓存在内存里,不需要从磁盘上读取,SQL执行时间 CPU时间是156
110 --占用时间这次和CPU时间非常接近,是169。这里省下运行时间1903-169=1734毫秒
111 --从这里可以再次看出,缓存对语句执行性能起着至关重要的作用
112 
113 --为了不影响其他测试,请运行下面的语句关闭SET STATISTICS TIME ON
114 SET STATISTICS TIME OFF
115 GO
116 
117 
118 
119 --SET STATISTICS IO ON--------------------------------------------------
120 
121 --这个开关能够输出语句做的物理读和逻辑读的数目。对分析语句的复杂度有很重要的作用
122 --还是以刚才那个查询作为例子
123 DBCC DROPCLEANBUFFERS
124 GO
125 SET STATISTICS IO ON
126 GO
127 
128 SELECT DISTINCT([ProductID]),[UnitPrice] FROM [dbo].[SalesOrderDetail_test]
129 WHERE [ProductID]=777
130 GO
131 
132 --他的返回是:
133 
134 --(4 行受影响)
135 --表 'SalesOrderDetail_test'。扫描计数 5,逻辑读取 15064 次,物理读取 0 次,预读 15064 次,lob 逻辑读取 0 次,
136 --lob 物理读取 0 次,lob 预读 0 次。
137 
138 
139 --各个输出的含义是:
140 --表:表的名称。这里的表就是SalesOrderDetail_test
141 
142 --扫描计数:执行的扫描次数。按照执行计划,表格被扫描了几次。一般来讲大表扫描的次数
143 --越多越不好。唯一的例外是如果执行计划选择了并发运行,由多个thread线程同时做一个表
144 --的读取,每个thread读其中的一部分,但是这里会显示所有thread的数目。也就是有几个
145 --thread在并发做,就会有几个扫描。这时数目大一点没问题的。
146 
147 --逻辑读取:从数据缓存读取的页数。页数越多,说明查询要访问的数据量就越大,内存消耗
148 --量越大,查询也就越昂贵。可以检查是否应该调整索引,减少扫描的次数,缩小扫描范围
149 
150 
151 --物理读取:从磁盘读取的页数
152 
153 
154 --预读:为进行查询而预读入缓存的页数
155 
156 
157 --物理读取+预读:就是SQL为了完成这句查询而从磁盘上读取的页数。如果不为0,说明数据没有
158 --缓存在内存里。运行速度一定会受到影响
159 
160 
161 --LOB逻辑读取:从数据缓存读取的text、ntext、image、
162 --大值类型(varchar(max)、nvarchar(max)、varbinary(max))页的数目
163 
164 
165 --LOB物理读取:从磁盘读取的text、ntext、image、大值类型页的数目
166 
167 
168 --LOB预读:为进行查询而放入缓存的text、ntext、image、大值类型页的数目
169 
170 
171 --然后再来运行一遍,不清空缓存
172 SET STATISTICS IO ON
173 GO
174 
175 SELECT DISTINCT([ProductID]),[UnitPrice] FROM [dbo].[SalesOrderDetail_test]
176 WHERE [ProductID]=777
177 GO
178 
179 --结果集返回:
180 --表 'SalesOrderDetail_test'。扫描计数 5,逻辑读取 15064 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,
181 --lob 物理读取 0 次,lob 预读 0 次。
182 
183 --这次逻辑读取不变,还是15064页。但是物理读取和预读都是0了。说明数据已经缓存在内存里
184 --第二次运行不需要再从磁盘上读一遍,节省了时间
185 
186 
187 --为了不影响其他测试,请运行下面语句关闭SET STATISTICS IO ON
188 SET STATISTICS IO OFF
189 GO
190 
191 
192 
193 --SET STATISTICS PROFILE ON--------------------------------------------
194 --这是三个设置中返回最复杂的一个,他返回语句的执行计划,以及语句运行在每一步的实际
195 --返回行数统计。通过这个结果,不仅可以得到执行计划,理解语句执行过程,分析语句
196 --调优方向,也可以判断SQL是否选择了一个正确的执行计划。如果不正确,原因又大概是什么呢?
197 
198 --本章前面已经使用过这个设置的返回结果来分析执行计划。现在再来完整介绍一遍。
199 --先从最简单的查询谈起
200 
201 SET STATISTICS PROFILE ON
202 GO
203 SELECT DISTINCT([ProductID]),[UnitPrice] FROM [dbo].[SalesOrderDetail_test]
204 WHERE [ProductID]=777
205 GO
206 
207 --返回的结果集很长,下面说一下重要字段
208 --注意:这里是从最下面开始向上看的,也就是说从最下面开始一直执行直到得到结果集
209 --所以(行1)里的rows字段显示的值就是这个查询返回的结果集。而且有多少行
210 --表明SQL执行了多少个步骤
211 
212 
213 --rows:执行计划的每一步返回的实际行数
214 
215 --executes:执行计划的每一步被运行了多少次
216 
217 --stmttext:执行计划的具体内容。执行计划以一棵树的形式显示。每一行,都是运行的一步
218 --都会有结果集返回,也都会有自己的cost
219 
220 --estimaterows:SQL根据表格上的统计信息,预估的每一步的返回行数。在分析执行计划时,
221 --我们会经常将rows和estimaterows这两列做对比,先确认SQL预估得是否正确,以判断
222 --统计信息是否有更新
223 
224 --estimateio:SQL根据estimaterows和统计信息里记录的字段长度,预估的每一步会产生的I/O cost
225 
226 
227 
228 --estimatecpu:SQL根据estimaterows和统计信息里记录的字段长度,以及要做的事情的复杂度,预估
229 --的每一步会产生的CPU cost
230 
231 
232 --totalsubtreecost:SQL根据estimateio和estimatecpu通过某种计算公式,计算出每一步执行
233 --计划子树的cost(包括这一步自己的cost和他的所有下层步骤的cost总和),下面介绍的cost
234 --说的都是这个字段值
235 
236 
237 --warnings:SQL在运行每一步时遇到的警告,例如,某一步没有统计信息支持cost预估等。
238 
239 
240 --parallel:执行计划的这一步是不是使用了并行的执行计划
241 
242 --所以上面这个简单的执行计划意思是:
243 --(1)执行计划分成两步:
244 --步骤a(行3)
245 --使用clustered index scan(全表扫描)的方式找出所有[ProductID]=777的记录
246 --这一步SQL预测会返回522.8行,实际返回484行。所以这一步预估的还算准确。预估cost
247 --是2.502
248 
249 --步骤b (行2)
250 --s使用sort的方式把步骤a返回的484行做一个排序,从中选出distinct的值
251 
252 --这一步SQL预测会返回146.1行,实际只返回了4行。所以这一步的预估有些不准确。这和
253 --SQL在([ProductID]),[UnitPrice] )上没有直接的统计信息有关系。好在这个结果集
254 --没有用来做其他事情,直接就是查询的结果。所以预估不太准也没有什么影响
255 
256 
257 --这一步的预估cost是2.637。除掉他的子步骤a的cost2.502,这一步自己的cost是2.637-2.502=0.135
258 
259 
260 --(2)执行计划是单线程运行的,也没有报出什么warning。应该讲,是一个正常的语句
261 
262 --(3)比较rows和estimaterows这两列,SQL没有少估计某一步的返回。而多估计的那一步(步骤b)
263 --在多估的情况下,cost依然比较小,自己cost只有0.135。实际cost因为返回结果集小,应该更低。
264 --所以可以讲,整个cost估计的还是比较准确的。主要的cost都花在了全表扫描上了(步骤a)
265 --如果要对这个语句进行调优,就要引导SQL不要使用clustered index scan这样cost大的动作。
266 --在[ProductID]上加一个索引是一个比较自然的想法
267 
268 --让我们再运行一句稍微复杂一点的语句,看看结果会怎麽样
269 SET STATISTICS PROFILE ON
270 GO
271 SELECT COUNT(b.[SalesOrderID])
272 FROM [dbo].[SalesOrderHeader_test] a
273 INNER JOIN [dbo].[SalesOrderDetail_test] b
274 ON a.[SalesOrderID]=b.[SalesOrderID]
275 WHERE a.[SalesOrderID]>43659 AND a.[SalesOrderID]<53660
276 GO
277 
278 --从这个结果看出执行计划分成4步,其中第一步又分成并列的两个子步骤
279 
280 --步骤a1(第5行):从[SalesOrderHeader_test]表里找出所有a.[SalesOrderID]>43659 AND a.[SalesOrderID]<53660
281 --的值
282 --因为表在这个字段上有一个聚集索引,所以SQL可以直接使用这个索引的seek
283 
284 --SQL预测返回10000条记录,实际也就返回了10000条记录.。这个预测是准确的。这一步的cost是0.202(totalsubtreecost)
285 --果然比前面的clustered index scan要低很多
286 
287 --步骤a2(第6行):从[SalesOrderDetail_test]表里找出所有 a.[SalesOrderID]>43659 AND a.[SalesOrderID]<53660
288 --的值
289 --因为表在这个字段上有一个非聚集索引,所以SQL可以直接使用这个索引的seek
290 
291 --这里能够看出SQL聪明的地方。虽然查询语句只定义了[SalesOrderHeader_test]表上有
292 --a.[SalesOrderID]>43659 AND a.[SalesOrderID]<53660过滤条件,但是根据语义分析,
293 --SQL知道这个条件在[SalesOrderDetail_test]上也为真。所以SQL选择先把这个条件过滤
294 --然后再做join。这样能够大大降低join的cost
295 
296 --在这一步SQL预估返回50561条记录,实际返回50577条。cost是0.127,也不高
297 
298 --步骤b(第4行):将a1和a2两步得到的结果集做一个join
299 --因为SQL通过预估知道这两个结果集比较大,所以他直接选择了Hash Match的join方法。
300 --SQL预估这个join能返回50313行,实际返回50577行。因为SQL在两张表的[SalesOrderID]上
301 --都有统计信息,所以这里的预估非常准确
302 
303 --这一步的cost等于totalsubtreecost减去他的子步骤,0.715-0.202-0.127=0.386。由于预估值
304 --非常准确,可以相信这里的cost就是实际每一步的cost
305 
306 
307 --步骤c(第3行)在join返回的结果集基础上算count(*)的值
308 --这一步比较简单,count(*)的结果总是1,所以预测值是正确的。其实这一步的cost是根据上一步
309 --(b)join返回的结果集大小预估出来的。我们知道步骤b的预估返回值非常准确,所以这一步的预估
310 --cost也不会有什么大问题
311 
312 --这棵子树的cost是0.727,减去他的子节点cost,他自己的cost是0.727-0.697=0.03。是花费很小
313 --的一步
314 
315 --步骤b(第2行):将步骤c返回的值转换为int类型,作为结果返回
316 
317 --这一步是上一步的继续,更为简单。convert一个值的数据类型所要的cost几乎可以忽略不计。
318 --所以这棵子树的cost和他的子节点相等,都是0.745。也就是说,他自己的cost是0
319 
320 --通过这样的方法,用户可以了解到语句的执行计划、SQL预估的准确性、cost的分布
321 
322 
323 
324 --------------执行计划里cost的值绝对大小问题----------------------------------
325 --经常有人会问,两句查询的cost,是不是一句比另一句高,就一定是高的那一句比较耗时?
326 --SQL是根据预测结果集(estimaterows)大小和结果集里的数据类型推算出estimateio和estimatecpu
327 --然后再根据estimateio和estimatecpu推算出totalsubtreecost。计算方法是所有SQLSERVER都一样的
328 --(只要SQLSERVER版本一样),所以在同一个SQLSERVER里,可以说,cost高的那一句一定会比较复杂。
329 --当然,前提是SQL要estimate得准确
330 
331 --对SQL来讲,他也是选择cost最低的执行计划。在前面联接一节里,那三条查询,cost最低的是使用
332 --hash match的联接方式。所以如果让SQL自己选择而不用join hint,他会选择hash match
333 
334 --但是对于不同的SQLSERVER,由于硬件差异,SQL能够调动的内存,CPU,磁盘资源也会有所差异。
335 --同样cost的语句,执行的速度会有很大不同。所以一般不会去对比从不同SQL服务器上取得的
336 --执行计划里的cost

 

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