笔记57-徐 调整语句设计提升性能

1 --调整语句设计提升性能 2013-2-11  P449
  2
  3 --当一个问题语句,已经排除了系统资源瓶颈、阻塞与死锁、物理I/O、编译与重编译
  4 --参数嗅探这些因素,也发现调整索引或使用计划指南不能达到要求,那怎么办?
  5 --不幸的是,咱们已经基本上把SQL上常见的调优方法都介绍了。在有些状况下,一个语句
  6 --的写法决定了他天生是一条复杂的语句,SQL很难使用最优的方法来运行他。这时候
  7 --调整SQLSERVER,可能效果都不会很明显。用户要想一想,若是这个问题更多的是由语句
  8 --自己致使的话,那调整语句设计是否是更好的解决方法。有时候,多是解决问题
  9 --惟一的选择
10
11 --常见的语句优化方法:
12
13 --一、筛选条件与计算字段
14 --筛选条件的写法是有讲究的。最好可以使用SARG的运算符包括=、>、<、>=、<=、in、between、like
15 --有时还包括like(在前缀匹配时,如like'john%')SARG能够包括由and联接的多个条件。SARG不但能够是匹配
16 --特定值的查询,例如
17 CustomerID=ANTON
18 DOE=last name
19
20 --还能够是匹配必定范围的值的查询,例如
21 order date>'1/1/2002'
22 order date>'1/1/2002' and order date<'1/1/2009'
23 doe in('anton','about')
24
25
26 --对于不使用SARG运算符的表达式,SQL对它们很难使用比较优化的作法,极可能就不使用
27 --索引。
28
29 --非SARG运算符包括 not、<>、not exists、not in、not like和内部函数,例如
30 --convert、upper等。下面的查询,就不会使用在Production.Product.Name字段上
31 --的索引 非汇集索引
32
33 --三个都是使用索引扫描
34 USE [AdventureWorks]
35 GO
36 SET STATISTICS PROFILE ON
37 SET STATISTICS TIME ON
38 SELECT [Name] FROM [Production].[Product]
39 WHERE [Name] LIKE'%Deca%'  --不是前缀匹配,因此使用索引扫描
40 GO
41 -----------------------------------------------
42 USE [AdventureWorks]
43 GO
44 SET STATISTICS PROFILE ON
45 SET STATISTICS TIME ON
46 SELECT [Name] FROM [Production].[Product]
47 WHERE [Name] NOT LIKE'%Deca%'
48 GO
49 ------------------------------------------------
50 USE [AdventureWorks]
51 GO
52 SET STATISTICS PROFILE ON
53 SET STATISTICS TIME ON
54 SELECT [Name] FROM [Production].[Product]
55 WHERE LEFT([Name],4)='Deca'
56 GO
57
58 --另外,若是要对字段先进性计算,再比较,也会妨碍索引的使用。例如
59 USE [AdventureWorks]
60 GO
61 SET STATISTICS PROFILE ON
62 SELECT [Name] FROM [Production].[Product]
63 WHERE [Name]+'_end'='Decal 1_end'
64
65 ---下面这样才会使用索引查找
66 USE [AdventureWorks]
67 GO
68 SET STATISTICS PROFILE ON
69 SELECT [Name] FROM [Production].[Product]
70 WHERE [Name]='Decal 1'
71
72 --在写语句的时候,要尽可能避免这些写法。有时候,程序里会有不少过滤是要经过
73 --先计算字段值再进行的。假如,咱们找产品的时候,代入的都是大写的字母,
74 --作比较的时候须要先将“name”字段转成大写,再比较。这时候很天然的,语句
75 --就是:
76 SELECT [Name] FROM [Production].[Product]
77 WHERE UPPER([Name])='Decal 1'
78
79 --他是不会使用索引seek的。若是程序里的许多语句都是这样,那SQL的性能是不会很好的。
80 --解决的办法,能够是在表格里再加一个字段,专门存放UPPER([Name])之后的结果。而后
81 --在这个新字段上创建索引。新插入的字段能够是一个计算字段(computed column)这样
82 --甚至insert语句都不用太多改动
83
84 --例如:咱们能够这样修改Production.Product表
85 USE [AdventureWorks]
86 GO
87 ALTER TABLE [Production].[Product] ADD UpperName AS UPPER([Name]) PERSISTED --带有Persisted值的计算列 会存储实际数据
88 --http://database.ctocio.com.cn/dbzjdysummary/48/8730048_4.shtml
89 --若是Persisted属性被关掉了,那么计算列只是虚拟列(书本是没有加PERSISTED关键字的)。该列将没有数据存储到磁盘上,而且这些值每次在一个脚本中参照时都会被计算。若是这个属性被设置成激活的,那么计算列的数据将会存储在磁盘中。
90 --加入一个计算字段,其值直接从UPPER([Name])算出
91 GO
92
93 CREATE NONCLUSTERED INDEX AK_Product_UName ON [Production].[Product](uppername)
94 --在上面建立一个非汇集索引
95 GO
96
97 USE [AdventureWorks]
98 GO
99 SELECT UpperName
100 FROM [Production].[Product]
101 WHERE UpperName='DECAL 1'
102 --改语句查询UpperName字段,会使用他上面的索引
103 GO
104
105 --删除
106 ALTER TABLE [Production].[Product] DROP COLUMN UpperName
107 DROP INDEX AK_Product_UName ON [Production].[Product]
108
109
110
111 --一些表格里有时间字段,而语句常常按年、月查询的时候,也可使用这种方法
112 --引入计算字段,事先把计算的值存储在索引数据结构里。这样每次查询的时候
113 --就不用再算一遍了
114
115 --不过,不是全部的计算字段上都能加索引。有些定义,可能每次运行返回值会
116 --不同non-deterministic。例如,根据employee的生日算年龄,值就会随
117 --当前的时间而发生不断的变化。
118
119 USE [AdventureWorks]
120 GO
121 ALTER TABLE [HumanResources].[Employee] ADD age AS DATEDIFF(yy,GETDATE(),[BirthDate])
122 GO
123
124 CREATE NONCLUSTERED INDEX AK_Employee_Age ON [HumanResources].[Employee](age)
125 GO
126
127
128 --对这样的语句,查询的修改可能得换一个思路.好比,我想找年龄大于30岁的员工,
129 --能够写成
130 USE [AdventureWorks]
131 GO
132 SELECT * FROM [HumanResources].[Employee] WHERE DATEDIFF(yy,[BirthDate],GETDATE())>30
133
134 --可是这样性能会有问题,不能使用索引,若是写成:
135 USE [AdventureWorks]
136 GO
137 SELECT * FROM [HumanResources].[Employee] WHERE [BirthDate] <DATEADD(yy,-30,GETDATE())
138
139 --SQL就有办法使用索引了。两种写法获得的结果很近似,可是第二种对SQL性能比较有利
140
141 --二、会在运行前改变值的变量
142 --在谈到参数嗅探的时候,提到过SQL在编译的时候,对存储过程代入的变量,SQL是
143 --知道他的值的,也会根据他的值对语句进行优化。可是若是在语句使用他以前,被
144 --其余语句修改过,那SQL生成的执行计划就不许了。这种状况,有时也会致使性能
145 --问题
146
147 --例如,下面这个存储过程,@date是他代入的参数。SQL会根据参数的值,生成执行计划
148 USE [AdventureWorks]
149 GO
150 CREATE PROCEDURE GetRecentSales(@date DATETIME)
151 AS
152 BEGIN
153 SELECT SUM(d.[OrderQty])
154 FROM [dbo].[SalesOrderHeader_test] h,[dbo].[SalesOrderDetail_test] d
155 WHERE h.[SalesOrderID]=d.[SalesOrderID]
156 AND h.[OrderDate]>@date
157 END
158 ----------------------------------------------------------------------
159 EXEC [sys].[sp_recompile] @objname = N'GetRecentSales' -- nvarchar(776)
160 GO
161 DBCC freeproccache
162 GO
163 SET STATISTICS PROFILE ON
164 GO
165 EXEC GetRecentSales NULL
166 --预估结果集很小,会使用nested loops
167 GO
168
169 EXEC [sys].[sp_recompile] @objname = N'GetRecentSales' -- nvarchar(776)
170 GO
171 DECLARE @date DATETIME
172 SET @date=DATEADD(mm,-3,(SELECT MAX([OrderDate]) FROM [dbo].[SalesOrderHeader_test]))
173 SET STATISTICS PROFILE ON
174 EXEC GetRecentSales @date
175 --预估结果集比较大,会使用hash match
176 GO
177
178
179
180 --可是若是咱们把存储过程改为下面这个样子:
181 USE [AdventureWorks]
182 GO
183 ALTER PROC GetRecentSales ( @date DATETIME )
184 AS
185     BEGIN
186         IF @date IS NULL
187             SET @date = DATEADD(mm, -3,
188                                 ( SELECT    MAX([OrderDate])
189                                   FROM      [dbo].[SalesOrderHeader_test]
190                                 ))
191      --若是是null值,会代入一个新的日期
192         SELECT  SUM(d.[OrderQty])
193         FROM    [dbo].[SalesOrderHeader_test] h ,
194                 [dbo].[SalesOrderDetail_test] d
195         WHERE   h.[SalesOrderID] = d.[SalesOrderID]
196                 AND h.[OrderDate] > @date
197     END
198
199 -------------------------------------------------------------
200 EXEC [sys].[sp_recompile] @objname = N'GetRecentSales' -- nvarchar(776)
201 GO
202 DBCC freeproccache
203 GO
204 SET STATISTICS PROFILE ON
205 EXEC GetRecentSales NULL
206 GO
207
208 --咱们再用null值来运行,会发现SQL没办法感知到值发生了变化,仍是使用了
209 --nested loop完成了查询。这个执行计划不是最优的
210
211 --怎麽来解决这个问题呢?固然,你能够在使用变量的语句后面加一个
212 --option(recompile)的query hint。这样当SQL运行到这句话的时候,
213 --会重编译可能出问题的语句。在那个时候,就能根据修改过的值
214 --生成更精确的执行计划了
215
216 USE [AdventureWorks]
217 GO
218 ALTER PROC GetRecentSales ( @date DATETIME )
219 AS
220     BEGIN
221         IF @date IS NULL
222             SET @date = DATEADD(mm, -3,
223                                 ( SELECT    MAX([OrderDate])
224                                   FROM      [dbo].[SalesOrderHeader_test]
225                                 ))
226      --若是是null值,会代入一个新的日期
227         SELECT  SUM(d.[OrderQty])
228         FROM    [dbo].[SalesOrderHeader_test] h ,
229                 [dbo].[SalesOrderDetail_test] d
230         WHERE   h.[SalesOrderID] = d.[SalesOrderID]
231                 AND h.[OrderDate] > @date
232         OPTION(recompile)
233     END
234   
235 --还有一种方法,是把可能出问题的语句单独作成一个子存储过程,让原来
236 --的存储过程调用子存储过程,而不是语句自己。例如
237
238 USE [AdventureWorks]
239 GO
240 CREATE PROCEDURE GetRecentSalesHelper(@date DATETIME)
241 AS
242 BEGIN
243         SELECT  SUM(d.[OrderQty])
244         FROM    [dbo].[SalesOrderHeader_test] h ,
245                 [dbo].[SalesOrderDetail_test] d
246         WHERE   h.[SalesOrderID] = d.[SalesOrderID]
247                 AND h.[OrderDate] > @date
248               
249 END
250
251
252 ALTER PROC GetRecentSales ( @date DATETIME )
253 AS
254     BEGIN
255         IF @date IS NULL
256             SET @date = DATEADD(mm, -3,
257                                 ( SELECT    MAX([OrderDate])
258                                   FROM      [dbo].[SalesOrderHeader_test]
259                                 ))
260         EXEC GetRecentSalesHelper @date
261     END   
262   
263   
264 --这样作的好处,是能够省下语句重编译的时间.两种方法,各有好处.能够根据
265 --实际状况作选择
266
267
268 --三、临时表和表变量
269 --不知道你们有没有注意到,SQL里有两种对象能够暂时存放表结构的数据
270 --一种就是你们很熟悉的临时表(temp table),另外一种是名气小一点
271 --,是表变量(table variable)。这两种对象功能相似,差别不太明显
272 --功能上比较大的差异是,表变量能够做为存储过程的返回参数,而
273 --临时表不行
274
275 --那是否是用表变量就能够了,为什麽SQL还要保留临时表这个功能呢?
276 --其实这两个对象在内部实现上仍是有很大区别的。
277
278
279 --最显著的区别:
280
281 --SQL会像对普通表同样,在临时表上维护统计信息,用户也能够在上面创建
282 --索引。而表变量上,既不能创建索引,也不会有统计信息。SQL在作执行
283 --计划的时候,老是认为表变量里的数据量只有不多的几行
284
285 --如今来作一个测试体会一下
286
287 --表变量情形
288 USE [AdventureWorks]
289 GO
290 DECLARE @tmp TABLE(ProductID INT,OrderQty INT)
291 INSERT INTO @tmp
292 SELECT [ProductID],[OrderQty]
293 FROM [dbo].[SalesOrderDetail_test]
294 WHERE [SalesOrderID]=75124
295 --语句会插入12万条记录
296
297 SET STATISTICS PROFILE ON
298 SELECT p.[Name],p.[Color],SUM(t.[OrderQty])
299 FROM @tmp t
300 INNER JOIN [Production].[Product] p
301 ON t.[ProductID]=p.[ProductID]
302 GROUP BY p.[Name],p.[Color]
303 ORDER BY p.[Name]
304 --含有12万条记录的表变量和另外一张表作join
305 GO
306
307
308 --从执行计划里能够看到,SQL认为表变量只会返回1行,因此选择了nested loops。
309 --在这里是不太合适的
310
311
312 --临时表情形
313 USE [AdventureWorks]
314 GO
315 CREATE  TABLE #tmp(ProductID INT,OrderQty INT)
316 INSERT INTO #tmp
317 SELECT [ProductID],[OrderQty]
318 FROM [dbo].[SalesOrderDetail_test]
319 WHERE [SalesOrderID]=75124
320 --语句会插入12万条记录
321
322 SET STATISTICS PROFILE ON
323 SELECT p.[Name],p.[Color],SUM(t.[OrderQty])
324 FROM #tmp t
325 INNER JOIN [Production].[Product] p
326 ON t.[ProductID]=p.[ProductID]
327 GROUP BY p.[Name],p.[Color]
328 ORDER BY p.[Name]
329 --含有12万条记录的表变量和另外一张表作join
330 GO
331 DROP TABLE [#tmp]
332 GO
333
334 --在 SQL Trace里会看到Auto Stats -Created 事件
335
336 --和表变量很不相同的是,SQL在insert语句以后,select语句以前,触发了
337 --一个自动建立统计信息auto stats created事件。创建了统计信息之后
338 --SQL就知道临时表里有不少数据了。join的方式,所以改用了merge join
339 --性能比前一种好不少
340
341 --因此表变量的好处是,他的维护成本很低,大量并发使用时对系统的负担
342 --比临时表要低。可是缺点是没有统计信息,存放大量的数据时性能很难保证。
343 --因此,表变量比较适合存放一些很小(几十行或更小)的结果集
344
345 --临时表的好处是,他的功能和普通用户表接近,可以为大数据集作优化
346 --可是缺点是维护成本高。大量并发使用临时表,会对系统带来比较重
347 --的负荷。因此临时表比较适合存放一些大的结果集
348
349 --在设计数据库应用,尤为是OLTP这样性能很敏感的应用时,要根据实际状况
350 --做出合理选择
351
352
353
354
355 --四、尽量限定语句的复杂度
356 --这是一我的人皆知的道理。若是语句不太复杂,固然性能会好。若是语句很是复杂
357 --固然开销会大,恨呐调快。但是,为了支持业务逻辑,仍是经常在SQL里看到
358 --很是复杂的语句。这为调优带来了很大麻烦。有时候简直是没法可想,除非
359 --是对应用设计作大手术
360
361
362 --这里列举出一些容易产生复杂语句的情形。在你们设计应用的时候,要当心使用
363
364 --(1)动态语句
365 --一些应用为了实现客户端的灵活性,会根据用户的选择,动态拼出TSQL语句,发给SQL
366 --运行。例如,在用户界面上列出各类条件,让用户根据本身的喜爱,输入条件,进行
367 --组合查询。这样在功能上来说比较强大,可是在复杂度控制上就有可能会出问题。
368 --若是用户选择的条件太多,或者根据条件返回的记录太多,就有可能会形成问题。
369 --而有些可以过滤大量数据,或者在索引上的条件若是没有被选上,就有可能形成在
370 --大表上的table scan。最好在程序里有动态语句复杂度的控制机制,限制选择的条件
371 --限制返回记录的数量
372
373
374
375 --(2)表格联接的数量
376 --为了支持复杂的业务逻辑,一个应用每每会有成百上千的表格,一些查询每每会联接
377 --十几张甚至几十张表。应用设计的时候对这样的查询要很慎重。若是表格很大,十几张
378 --表作联接,确定不会有好的性能。若是应用是支持数据分析系统,那可能还好。若是
379 --应用是一个OLTP系统,这样的设计失败的风险可能会很大。有时候可能须要下降
380 --数据库范式级别,多保存一些冗余数据列,以减小表格联接的数量
381
382
383
384
385 --(3)视图和存储过程的深度
386 --视图和存储过程可以抽象出一些业务逻辑,简化设计,是很推荐的作法。可是若是
387 --在引用视图和存储过程时不加注意,视图套视图,存储过程嵌存储过程,最后
388 --嵌套上四五层,那复杂度累积起来,可能会超出你想象。对SQL的优化,也是很
389 --严重的考验。因此在引用他们的时候,也要考虑累积的复杂度
390
391
392
393 --(4)没必要要的排序和计算
394 --对一个大结果集作排序,或者求惟一值,都是比较昂贵的计算,会占用大量系统资源
395 --若是用户对结果集排序或惟一性的要求不高,能够适当去掉这些计算
396
397
398
399
400 --(5)超大结果集申请和返回
401 --若是根据用户选择的过滤条件,SQL会返回十几万条记录,那应用层该如何处理?
402 --若是一次性返回给应用层,那应用层要缓存和处理这么多记录,本身的性能
403 --会受到很大的挑战。若是一次只取一部分记录,其余记录由SQL代为缓存
404 --(通常是应用服务器端游标),那不但会给SQL的内存使用带来负担,并且
405 --容易产生阻塞问题。若是应用层处理得很差,甚至会产生内存泄漏的问题。
406 --因此程序设计的时候,要确保应用只会申请合适的、有必要的结果集。
407 --例如一个用户在网页上查询他感兴趣的产品,可能最多只会看前面的
408 --100个。若是你返回一万一个产品记录给他,除了暗示你产品多之外,
409 --对用户没有任何意义。这时候在语句里设置一个top 100,多是
410 --个合理的选择
411
412
413
414
415 --(6)用多个简单语句替代一个复杂语句
416 --若是一个复杂的语句有不少张表要联接,要作不少计算,不少时候,要
417 --根据表和表的逻辑关系,知道某一张表和另外一张表若是先作联接,
418 --可能会过滤掉更多数据。获得的小的结果集再作其余联接,会更快
419 --相似的,有些计算能够先作,也能够后作,人在了解了表格的逻辑以后
420 --会知道是先作好仍是后作好。惋惜SQL做为一个计算机程序,在这方面
421 --没有人那么聪明。当语句太复杂的时候,他有可能看不出来了。为了
422 --提升性能,对这种特别复杂的语句,能够把一句话拆成两句,甚至三句
423 --分步作完,中间结果集,能够以临时表的形式存放。这样作对程序员来
424 --讲作了不少事,可是对SQL来说,大大简化了复杂度。不少时候对性能
425 --也会有帮助html

相关文章
相关标签/搜索