(轉)SQLServer_十步優化SQL Server中的數據訪問 二

       原文地址:http://tech.it168.com/a2009/1125/814/000000814758_all.shtml html

第五步:識別低效TSQL,採用最佳實踐重構和應用TSQL

  因爲每一個程序員的能力和習慣都不同,他們編寫的TSQL可能風格各異,部分代碼可能不是最佳實現,對於水平通常的程序員可能首先想到的是編寫TSQL實現需求,至於性能問題往後再說,所以在開發和測試時可能發現不了問題。程序員

  也有一些人知道最佳實踐,但在編寫代碼時因爲種種緣由沒有采用最佳實踐,等到用戶發飆的那天才乖乖地從新埋頭思考最佳實踐。數據庫

  我以爲仍是有必要介紹一下具備都有哪些最佳實踐。網絡

  一、在查詢中不要使用「select *」函數

  (1)檢索沒必要要的列會帶來額外的系統開銷,有句話叫作「該省的則省」;oop

  (2)數據庫不能利用「覆蓋索引」的優勢,所以查詢緩慢。性能

  二、在select清單中避免沒必要要的列,在鏈接條件中避免沒必要要的表測試

  (1)在select查詢中若有沒必要要的列,會帶來額外的系統開銷,特別是LOB類型的列;優化

  (2)在鏈接條件中包含沒必要要的表會強制數據庫引擎檢索和匹配不須要的數據,增長了查詢執行時間。3d

  三、不要在子查詢中使用count()求和執行存在性檢查

  (1)不要使用

SELECT column_list FROM table WHERE 0 < (SELECT count(*) FROM table2 WHERE ..)

  使用

SELECT column_list FROM table WHERE EXISTS (SELECT * FROM table2 WHERE ...)

  代替;

  (2)當你使用count()時,SQL Server不知道你要作的是存在性檢查,它會計算全部匹配的值,要麼會執行全表掃描,要麼會掃描最小的非彙集索引;

  (3)當你使用EXISTS時,SQL Server知道你要執行存在性檢查,當它發現第一個匹配的值時,就會返回TRUE,並中止查詢。相似的應用還有使用IN或ANY代替count()。

  四、避免使用兩個不一樣類型的列進行表的鏈接

  (1)當鏈接兩個不一樣類型的列時,其中一個列必須轉換成另外一個列的類型,級別低的會被轉換成高級別的類型,轉換操做會消耗必定的系統資源;

  (2)若是你使用兩個不一樣類型的列來鏈接表,其中一個列本來可使用索引,但通過轉換後,優化器就不會使用它的索引了。例如: 

SELECT column_list FROM small_table, large_table WHERE

  smalltable.float_column = large_table.int_column

  在這個例子中,SQL Server會將int列轉換爲float類型,由於int比float類型的級別低,large_table.int_column上的索引就不會被使用,但smalltable.float_column上的索引能夠正常使用。

  五、避免死鎖

  (1)在你的存儲過程和觸發器中訪問同一個表時老是以相同的順序;

  (2)事務應經可能地縮短,在一個事務中應儘量減小涉及到的數據量;

  (3)永遠不要在事務中等待用戶輸入。

  六、使用「基於規則的方法」而不是使用「程序化方法」編寫TSQL

  (1)數據庫引擎專門爲基於規則的SQL進行了優化,所以處理大型結果集時應儘可能避免使用程序化的方法(使用遊標或UDF[User Defined Functions]處理返回的結果集) ;

  (2)如何擺脫程序化的SQL呢?有如下方法:

  - 使用內聯子查詢替換用戶定義函數;

  - 使用相關聯的子查詢替換基於遊標的代碼;

  - 若是確實須要程序化代碼,至少應該使用表變量代替遊標導航和處理結果集。

  七、避免使用count(*)得到表的記錄數

  (1)爲了得到表中的記錄數,咱們一般使用下面的SQL語句:

 SELECT COUNT(*) FROM dbo.orders

  這條語句會執行全表掃描才能得到行數。

  (2)但下面的SQL語句不會執行全表掃描同樣能夠得到行數:

SELECT rows FROM sysindexes

  WHERE id = OBJECT_ID('dbo.Orders') AND indid < 2

  八、避免使用動態SQL

  除非無可奈何,應儘可能避免使用動態SQL,由於:

  (1)動態SQL難以調試和故障診斷;

  (2)若是用戶向動態SQL提供了輸入,那麼可能存在SQL注入風險。

  九、避免使用臨時表

  (1)除非卻有須要,不然應儘可能避免使用臨時表,相反,可使用表變量代替;

  (2)大多數時候(99%),表變量駐紮在內存中,所以速度比臨時表更快,臨時表駐紮在TempDb數據庫中,所以臨時表上的操做須要跨數據庫通訊,速度天然慢。

  十、使用全文搜索搜索文本數據,取代like搜索

  全文搜索始終優於like搜索:

  (1)全文搜索讓你能夠實現like不能完成的複雜搜索,如搜索一個單詞或一個短語,搜索一個與另外一個單詞或短語相近的單詞或短語,或者是搜索同義詞;

  (2)實現全文搜索比實現like搜索更容易(特別是複雜的搜索);

  十一、使用union實現or操做

  (1)在查詢中儘可能不要使用or,使用union合併兩個不一樣的查詢結果集,這樣查詢性能會更好;

  (2)若是不是必需要不一樣的結果集,使用union all效果會更好,由於它不會對結果集排序。

  十二、爲大對象使用延遲加載策略

  (1)在不一樣的表中存儲大對象(如VARCHAR(MAX),Image,Text等),而後在主表中存儲這些大對象的引用;

  (2)在查詢中檢索全部主表數據,若是須要載入大對象,按需從大對象表中檢索大對象。

  1三、使用VARCHAR(MAX),VARBINARY(MAX) 和 NVARCHAR(MAX)

  (1)在SQL Server 2000中,一行的大小不能超過800字節,這是受SQL Server內部頁面大小8KB的限制形成的,爲了在單列中存儲更多的數據,你須要使用TEXT,NTEXT或IMAGE數據類型(BLOB);

  (2)這些和存儲在相同表中的其它數據不同,這些頁面以B-Tree結構排列,這些數據不能做爲存儲過程或函數中的變量,也不能用於字符串函數,如REPLACE,CHARINDEX或SUBSTRING,大多數時候你必須使用READTEXT,WRITETEXT和UPDATETEXT;

  (3)爲了解決這個問題,在SQL Server 2005中增長了VARCHAR(MAX),VARBINARY(MAX) 和 NVARCHAR(MAX),這些數據類型能夠容納和BLOB相同數量的數據(2GB),和其它數據類型使用相同的數據頁;

  (4)當MAX數據類型中的數據超過8KB時,使用溢出頁(在ROW_OVERFLOW分配單元中)指向源數據頁,源數據頁仍然在IN_ROW分配單元中。

  1四、在用戶定義函數中使用下列最佳實踐

  不要在你的存儲過程,觸發器,函數和批處理中重複調用函數,例如,在許多時候,你須要得到字符串變量的長度,不管如何都不要重複調用LEN函數,只調用一次便可,將結果存儲在一個變量中,之後就能夠直接使用了。
 

  1五、在存儲過程當中使用下列最佳實踐

  (1)不要使用SP_xxx做爲命名約定,它會致使額外的搜索,增長I/O(由於系統存儲過程的名字就是以SP_開頭的),同時這麼作還會增長與系統存儲過程名稱衝突的概率;

  (2)將Nocount設置爲On避免額外的網絡開銷;

  (3)當索引結構發生變化時,在EXECUTE語句中(第一次)使用WITH RECOMPILE子句,以便存儲過程能夠利用最新建立的索引;

  (4)使用默認的參數值更易於調試。

  1六、在觸發器中使用下列最佳實踐

  (1)最好不要使用觸發器,觸發一個觸發器,執行一個觸發器事件自己就是一個耗費資源的過程;

  (2)若是可以使用約束實現的,儘可能不要使用觸發器;

  (3)不要爲不一樣的觸發事件(Insert,Update和Delete)使用相同的觸發器;

  (4)不要在觸發器中使用事務型代碼。

  1七、在視圖中使用下列最佳實踐

  (1)爲從新使用複雜的TSQL塊使用視圖,並開啓索引視圖;

  (2)若是你不想讓用戶意外修改表結構,使用視圖時加上SCHEMABINDING選項;

  (3)若是隻從單個表中檢索數據,就不須要使用視圖了,若是在這種狀況下使用視圖反倒會增長系統開銷,通常視圖會涉及多個表時纔有用。

  1八、在事務中使用下列最佳實踐

  (1)SQL Server 2005以前,在BEGIN TRANSACTION以後,每一個子查詢修改語句時,必須檢查@@ERROR的值,若是值不等於0,那麼最後的語句可能會致使一個錯誤,若是發生任何錯誤,事務必須回滾。從SQL Server 2005開始,Try..Catch..代碼塊能夠處理TSQL中的事務,所以在事務型代碼中最好加上Try…Catch…;

  (2)避免使用嵌套事務,使用@@TRANCOUNT變量檢查事務是否須要啓動(爲了不嵌套事務);

  (3)儘量晚啓動事務,提交和回滾事務要儘量快,以減小資源鎖定時間。

  要徹底列舉最佳實踐不是本文的初衷,當你瞭解了這些技巧後就應該拿來使用,不然瞭解了也沒有價值。此外,你還須要評審和監視數據訪問代碼是否遵循下列標準和最佳實踐。

  如何分析和識別你的TSQL中改進的範圍?

  理想狀況下,你們都想預防疾病,而不是等病發了去治療。但實際上這個願望根本沒法實現,即便你的團隊成員全都是專家級人物,我也知道你有進行評審,但代碼仍然一團糟,所以須要知道如何治療疾病同樣重要。

  首先須要知道如何診斷性能問題,診斷就得分析TSQL,找出瓶頸,而後重構,要找出瓶頸就得先學會分析執行計劃。

  理解查詢執行計劃

  當你將SQL語句發給SQL Server引擎後,SQL Server首先要肯定最合理的執行方法,查詢優化器會使用不少信息,如數據分佈統計,索引結構,元數據和其它信息,分析多種可能的執行計劃,最後選擇一個最佳的執行計劃。

  可使用SQL Server Management Studio預覽和分析執行計劃,寫好SQL語句後,點擊SQL Server Management Studio上的評估執行計劃按鈕查看執行計劃,如圖1所示。

  圖 1 在Management Studio中評估執行計劃

  在執行計劃圖中的每一個圖標表明計劃中的一個行爲(操做),應從右到左閱讀執行計劃,每一個行爲都一個相對於整體執行成本(100%)的成本百分比。

  在上面的執行計劃圖中,右邊的那個圖標表示在HumanResources表上的一個「彙集索引掃描」操做(閱讀表中全部主鍵索引值),須要100%的整體查詢執行成本,圖中左邊那個圖標表示一個select操做,它只須要0%的整體查詢執行成本。

  下面是一些比較重要的圖標及其對應的操做:

  圖 2 常見的重要圖標及對應的操做

  注意執行計劃中的查詢成本,若是說成本等於100%,那極可能在批處理中就只有這個查詢,若是在一個查詢窗口中有多個查詢同時執行,那它們確定有各自的成本百分比(小於100%)。

  若是想知道執行計劃中每一個操做詳細狀況,將鼠標指針移到對應的圖標上便可,你會看到相似於下面的這樣一個窗口。

  圖 3 查看執行計劃中行爲(操做)的詳細信息

  這個窗口提供了詳細的評估信息,上圖顯示了彙集索引掃描的詳細信息,它要查找AdventureWorks數據庫HumanResources方案下Employee表中 Gender = ‘M’的行,它也顯示了評估的I/O,CPU成本。

  查看執行計劃時,咱們應該得到什麼信息

  當你的查詢很慢時,你就應該看看預估的執行計劃(固然也能夠查看真實的執行計劃),找出耗時最多的操做,注意觀察如下成本一般較高的操做:

  一、表掃描(Table Scan)

  當表沒有彙集索引時就會發生,這時只要建立彙集索引或重整索引通常均可以解決問題。

  二、彙集索引掃描(Clustered Index Scan)

  有時能夠認爲等同於表掃描,當某列上的非彙集索引無效時會發生,這時只要建立一個非彙集索引就ok了。

  三、哈希鏈接(Hash Join)

  當鏈接兩個表的列沒有被索引時會發生,只需在這些列上建立索引便可。

  四、嵌套循環(Nested Loops)

  當非彙集索引不包括select查詢清單的列時會發生,只須要建立覆蓋索引問題便可解決。

  五、RID查找(RID Lookup)

  當你有一個非彙集索引,但相同的表上卻沒有彙集索引時會發生,此時數據庫引擎會使用行ID查找真實的行,這時一個代價高的操做,這時只要在該表上建立彙集索引便可。

  TSQL重構真實的故事

  只有解決了實際的問題後,知識才轉變爲價值。當咱們檢查應用程序性能時,發現一個存儲過程比咱們預期的執行得慢得多,在生產數據庫中檢索一個月的銷售數據竟然要50秒,下面就是這個存儲過程的執行語句:

  exec uspGetSalesInfoForDateRange ‘1/1/2009’, 31/12/2009,’Cap’

  Tom受命來優化這個存儲過程,下面是這個存儲過程的代碼:

 ALTER PROCEDURE uspGetSalesInfoForDateRange

  @startYear DateTime,

  @endYear DateTime,

  @keyword nvarchar(50)

  AS

  BEGIN

  SET NOCOUNT ON;

  SELECT

  Name,

  ProductNumber,

  ProductRates.CurrentProductRate Rate,

  ProductRates.CurrentDiscount Discount,

  OrderQty Qty,

  dbo.ufnGetLineTotal(SalesOrderDetailID) Total,

  OrderDate,

  DetailedDescription

  FROM

  Products INNER JOIN OrderDetails

  ON Products.ProductID = OrderDetails.ProductID

  INNER JOIN Orders

  ON Orders.SalesOrderID = OrderDetails.SalesOrderID

  INNER JOIN ProductRates

  ON

  Products.ProductID = ProductRates.ProductID

  WHERE

  OrderDate between @startYear and @endYear

  AND

  (

  ProductName LIKE '' + @keyword + ' %' OR

  ProductName LIKE '% ' + @keyword + ' ' + '%' OR

  ProductName LIKE '% ' + @keyword + '%' OR

  Keyword LIKE '' + @keyword + ' %' OR

  Keyword LIKE '% ' + @keyword + ' ' + '%' OR

  Keyword LIKE '% ' + @keyword + '%'

  )

  ORDER BY

  ProductName

  END

  GO

  分析索引

  首先,Tom想到了審查這個存儲過程使用到的表的索引,很快他發現下面兩列的索引無端丟失了:

  OrderDetails.ProductID

  OrderDetails.SalesOrderID

  他在這兩個列上建立了非彙集索引,而後再執行存儲過程:

  exec uspGetSalesInfoForDateRange ‘1/1/2009’, 31/12/2009 with recompile

  性能有所改變,但仍然低於預期(此次花了35秒),注意這裏的with recompile子句告訴SQL Server引擎從新編譯存儲過程,從新生成執行計劃,以利用新建立的索引。

  分析查詢執行計劃

  Tom接下來查看了SQL Server Management Studio中的執行計劃,經過分析,他找到了某些重要的線索:

  一、發生了一次表掃描,即便該表已經正確設置了索引,而表掃描佔據了整體查詢執行時間的30%;

  二、發生了一個嵌套循環鏈接。

  Tom想知道是否有索引碎片,由於全部索引配置都是正確的,經過TSQL他知道了有兩個索引都產生了碎片,很快他重組了這兩個索引,因而表掃描消失了,如今執行存儲過程的時間減小到25秒了。

  爲了消除嵌套循環鏈接,他又在表上建立了覆蓋索引,時間進一步減小到23秒。

  實施最佳實踐

  Tom發現有個UDF有問題,代碼以下: 

ALTER FUNCTION [dbo].[ufnGetLineTotal]

  (

  @SalesOrderDetailID int

  )

  RETURNS money

  AS

  BEGIN

  DECLARE @CurrentProductRate money

  DECLARE @CurrentDiscount money

  DECLARE @Qty int

  SELECT

  @CurrentProductRate = ProductRates.CurrentProductRate,

  @CurrentDiscount = ProductRates.CurrentDiscount,

  @Qty = OrderQty

  FROM

  ProductRates INNER JOIN OrderDetails ON

  OrderDetails.ProductID = ProductRates.ProductID

  WHERE

  OrderDetails.SalesOrderDetailID = @SalesOrderDetailID

  RETURN (@CurrentProductRate-@CurrentDiscount)*@Qty

  END

  在計算訂單總金額時看起來代碼很程序化,Tom決定在UDF的SQL中使用內聯SQL。

  dbo.ufnGetLineTotal(SalesOrderDetailID) Total -- 舊代碼

  (CurrentProductRate-CurrentDiscount)*OrderQty Total -- 新代碼

  執行時間一會兒減小到14秒了。

  在select查詢清單中放棄沒必要要的Text列

  爲了進一步提高性能,Tom決定檢查一下select查詢清單中使用的列,很快他發現有一個Products.DetailedDescription列是Text類型,經過對應用程序代碼的走查,Tom發現其實這一列的數據並不會當即用到,因而他將這一列從select查詢清單中取消掉,時間一會兒從14秒減小到6秒,因而Tom決定使用一個存儲過程應用延遲加載策略加載這個Text列。

  最後Tom仍是不死心,認爲6秒也沒法接受,因而他再次仔細檢查了SQL代碼,他發現了一個like子句,通過反覆研究他認爲這個like搜索徹底能夠用全文搜索替換,最後他用全文搜索替換了like搜索,時間一會兒下降到1秒,至此Tom認爲調優應該暫時結束了。

  小結

  看起來咱們介紹了好多種優化數據訪問的技巧,但你們要知道優化數據訪問是一個無止境的過程,一樣你們要相信一個信念,不管你的系統多麼龐大,多麼複雜,只要靈活運用咱們所介紹的這些技巧,你同樣能夠馴服它們。下一篇將介紹高級索引和反範式化。

  通過索引優化,重構TSQL後你的數據庫還存在性能問題嗎?徹底有可能,這時必須得找另外的方法才行。SQL Server在索引方面還提供了某些高級特性,可能你還從未使用過,利用高級索引會顯著地改善系統性能,本文將從高級索引技術談起,另外還將介紹反範式化技術。

相關文章
相關標籤/搜索