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