深刻sql server優化,MSSQL優化,T-SQL優化,查詢優化html
故事開篇:你和你的團隊通過不懈努力,終於使網站成功上線,剛開始時,註冊用戶較少,網站性能表現不錯,但隨着註冊用戶的增多,訪問速度開始變慢,一些用戶開始發來郵件表示抗議,事情變得愈來愈糟,爲了留住用戶,你開始着手調查訪問變慢的緣由。程序員
通過緊張的調查,你發現問題出在數據庫上,當應用程序嘗試訪問/更新數據時,數據庫執行得至關慢,再次深刻調查數據庫後,你發現數據庫表增加得很大,有些表甚至有上千萬行數據,測試團隊開始在生產數據庫上測試,發現訂單提交過程須要花5分鐘時間,但在網站上線前的測試中,提交一次訂單隻須要2/3秒。sql
相似這種故事在世界各個角落天天都會上演,幾乎每一個開發人員在其開發生涯中都會遇到這種事情,我也曾屢次遇到這種狀況,所以我但願將我解決這種問題的經驗和你們分享。數據庫
若是你正身處這種項目,逃避不是辦法,只有勇敢地去面對現實。首先,我認爲你的應用程序中必定沒有寫數據訪問程序,我將在這個系列的文章中介紹如何編寫最佳的數據訪問程序,以及如何優化現有的數據訪問程序。後端
範圍瀏覽器
在正式開始以前,有必要澄清一下本系列文章的寫做邊界,我想談的是「事務性(OLTP)SQL Server數據庫中的數據訪問性能優化」,但文中介紹的這些技巧也能夠用於其它數據庫平臺。緩存
同時,我介紹的這些技巧主要是面向程序開發人員的,雖然DBA也是優化數據庫的一支主要力量,但DBA使用的優化方法不在個人討論範圍以內。性能優化
當一個基於數據庫的應用程序運行起來很慢時,90%的可能都是因爲數據訪問程序的問題,要麼是沒有優化,要麼是沒有按最佳方法編寫代碼,所以你須要審查和優化你的數據訪問/處理程序。服務器
我將會談到10個步驟來優化數據訪問程序,先從最基本的索引提及吧!網絡
第一步:應用正確的索引
我之因此先從索引談起是由於採用正確的索引會使生產系統的性能獲得質的提高,另外一個緣由是建立或修改索引是在數據庫上進行的,不會涉及到修改程序,並能夠當即見到成效。
咱們仍是溫習一下索引的基礎知識吧,我相信你已經知道什麼是索引了,但我見到不少人都還不是很明白,我先給你們將一個故事吧。
好久之前,在一個古城的的大圖書館中珍藏有成千上萬本書籍,但書架上的書沒有按任何順序擺放,所以每當有人詢問某本書時,圖書管理員只有挨個尋找,每一次都要花費大量的時間。
[這就比如數據表沒有主鍵同樣,搜索表中的數據時,數據庫引擎必須進行全表掃描,效率極其低下。]
更糟的是圖書館的圖書愈來愈多,圖書管理員的工做變得異常痛苦,有一天來了一個聰明的小夥子,他看到圖書管理員的痛苦工做後,想出了一個辦法,他建議將每本書都編上號,而後按編號放到書架上,若是有人指定了圖書編號,那麼圖書管理員很快就能夠找到它的位置了。
[給圖書編號就象給表建立主鍵同樣,建立主鍵時,會建立彙集索引樹,表中的全部行會在文件系統上根據主鍵值進行物理排序,當查詢表中任一行時,數據庫首先使用匯集索引樹找到對應的數據頁(就象首先找到書架同樣),而後在數據頁中根據主鍵鍵值找到目標行(就象找到書架上的書同樣)。]
因而圖書管理員開始給圖書編號,而後根據編號將書放到書架上,爲此他花了整整一天時間,但最後通過測試,他發現找書的效率大大提升了。
[在一個表上只能建立一個彙集索引,就象書只能按一種規則擺放同樣。]
但問題並未徹底解決,由於不少人記不住書的編號,只記得書的名字,圖書管理員無賴又只有掃描全部的圖書編號挨個尋找,但此次他只花了20分鐘,之前未給圖書編號時要花2-3小時,但與根據圖書編號查找圖書相比,時間仍是太長了,所以他向那個聰明的小夥子求助。
[這就好像你給Product表增長了主鍵ProductID,但除此以外沒有創建其它索引,當使用Product Name進行檢索時,數據庫引擎又只要進行全表掃描,逐個尋找了。]
聰明的小夥告訴圖書管理員,以前已經建立好了圖書編號,如今只須要再建立一個索引或目錄,將圖書名稱和對應的編號一塊兒存儲起來,但這一次是按圖書名稱進行排序,若是有人想找「Database Management System」一書,你只須要跳到「D」開頭的目錄,而後按照編號就能夠找到圖書了。
因而圖書管理員興奮地花了幾個小時建立了一個「圖書名稱」目錄,通過測試,如今找一本書的時間縮短到1分鐘了(其中30秒用於從「圖書名稱」目錄中查找編號,另外根據編號查找圖書用了30秒)。
圖書管理員開始了新的思考,讀者可能還會根據圖書的其它屬性來找書,如做者,因而他用一樣的辦法爲做者也建立了目錄,如今能夠根據圖書編號,書名和做者在1分鐘內查找任何圖書了,圖書管理員的工做變得輕鬆了,故事也到此結束。
到此,我相信你已經徹底理解了索引的真正含義。假設咱們有一個Products表,建立了一個彙集索引(根據表的主鍵自動建立的),咱們還須要在ProductName列上建立一個非彙集索引,建立非彙集索引時,數據庫引擎會爲非彙集索引自動建立一個索引樹(就象故事中的「圖書名稱」目錄同樣),產品名稱會存儲在索引頁中,每一個索引頁包括必定範圍的產品名稱和它們對應的主鍵鍵值,當使用產品名稱進行檢索時,數據庫引擎首先會根據產品名稱查找非彙集索引樹查出主鍵鍵值,而後使用主鍵鍵值查找彙集索引樹找到最終的產品。
下圖顯示了一個索引樹的結構
圖 1 索引樹結構
它叫作B+樹(或平衡樹),中間節點包含值的範圍,指引SQL引擎應該在哪裏去查找特定的索引值,葉子節點包含真正的索引值,若是這是一個彙集索引樹,葉子節點就是物理數據頁,若是這是一個非彙集索引樹,葉子節點包含索引值和彙集索引鍵(數據庫引擎使用它在彙集索引樹中查找對應的行)。
一般,在索引樹中查找目標值,而後跳到真實的行,這個過程是花不了什麼時間的,所以索引通常會提升數據檢索速度。下面的步驟將有助於你正確應用索引。
確保每一個表都有主鍵
這樣能夠確保每一個表都有彙集索引(表在磁盤上的物理存儲是按照主鍵順序排列的),使用主鍵檢索表中的數據,或在主鍵字段上進行排序,或在where子句中指定任意範圍的主鍵鍵值時,其速度都是很是快的。
在下面這些列上建立非彙集索引:
1)搜索時常用到的;
2)用於鏈接其它表的;
3)用於外鍵字段的;
4)高選中性的;
5)ORDER BY子句使用到的;
6)XML類型。
下面是一個建立索引的例子:
NCLIX_OrderDetails_ProductID ON
dbo.OrderDetails(ProductID)
也能夠使用SQL Server管理工做臺在表上建立索引,如圖2所示。
圖 2 使用SQL Server管理工做臺建立索引
第二步:建立適當的覆蓋索引
假設你在Sales表(SelesID,SalesDate,SalesPersonID,ProductID,Qty)的外鍵列(ProductID)上建立了一個索引,假設ProductID列是一個高選中性列,那麼任何在where子句中使用索引列(ProductID)的select查詢都會更快,若是在外鍵上沒有建立索引,將會發生所有掃描,但還有辦法能夠進一步提高查詢性能。
假設Sales表有10,000行記錄,下面的SQL語句選中400行(總行數的4%):
咱們來看看這條SQL語句在SQL執行引擎中是如何執行的:
1)Sales表在ProductID列上有一個非彙集索引,所以它查找非彙集索引樹找出ProductID=112的記錄;
2)包含ProductID = 112記錄的索引頁也包括全部的彙集索引鍵(全部的主鍵鍵值,即SalesID);
3)針對每個主鍵(這裏是400),SQL Server引擎查找彙集索引樹找出真實的行在對應頁面中的位置;
SQL Server引擎從對應的行查找SalesDate和SalesPersonID列的值。
在上面的步驟中,對ProductID = 112的每一個主鍵記錄(這裏是400),SQL Server引擎要搜索400次彙集索引樹以檢索查詢中指定的其它列(SalesDate,SalesPersonID)。
若是非彙集索引頁中包括了彙集索引鍵和其它兩列(SalesDate,,SalesPersonID)的值,SQL Server引擎可能不會執行上面的第3和4步,直接從非彙集索引樹查找ProductID列速度還會快一些,直接從索引頁讀取這三列的數值。
幸運的是,有一種方法實現了這個功能,它被稱爲「覆蓋索引」,在表列上建立覆蓋索引時,須要指定哪些額外的列值須要和彙集索引鍵值(主鍵)一塊兒存儲在索引頁中。下面是在Sales 表ProductID列上建立覆蓋索引的例子:
ON dbo.Sales(ProductID)--Column on which index is to be created
INCLUDE(SalesDate, SalesPersonID)--Additional column values to include
應該在那些select查詢中常使用到的列上建立覆蓋索引,但覆蓋索引中包括過多的列也不行,由於覆蓋索引列的值是存儲在內存中的,這樣會消耗過多內存,引起性能降低。
建立覆蓋索引時使用數據庫調整顧問
咱們知道,當SQL出問題時,SQL Server引擎中的優化器根據下列因素自動生成不一樣的查詢計劃:
1)數據量
2)統計數據
3)索引變化
4)TSQL中的參數值
5)服務器負載
這就意味着,對於特定的SQL,即便表和索引結構是同樣的,但在生產服務器和在測試服務器上產生的執行計劃可能會不同,這也意味着在測試服務器上建立的索引能夠提升應用程序的性能,但在生產服務器上建立一樣的索引卻未必會提升應用程序的性能。由於測試環境中的執行計劃利用了新建立的索引,但在生產環境中執行計劃可能不會利用新建立的索引(例如,一個非彙集索引列在生產環境中不是一個高選中性列,但在測試環境中可能就不同)。
所以咱們在建立索引時,要知道執行計劃是否會真正利用它,但咱們怎麼才能知道呢?答案就是在測試服務器上模擬生產環境負載,而後建立合適的索引並進行測試,若是這樣測試發現索引能夠提升性能,那麼它在生產環境也就更可能提升應用程序的性能了。
雖然要模擬一個真實的負載比較困難,但目前已經有不少工具能夠幫助咱們。
使用SQL profiler跟蹤生產服務器,儘管不建議在生產環境中使用SQL profiler,但有時沒有辦法,要診斷性能問題關鍵所在,必須得用,在http://msdn.microsoft.com/en-us/library/ms181091.aspx有SQL profiler的使用方法。
使用SQL profiler建立的跟蹤文件,在測試服務器上利用數據庫調整顧問建立一個相似的負載,大多數時候,調整顧問會給出一些能夠當即使用的索引建議,在http://msdn.microsoft.com/en-us/library/ms166575.aspx有調整顧問的詳細介紹。
第三步:整理索引碎片
你可能已經建立好了索引,而且全部索引都在工做,但性能卻仍然很差,那極可能是產生了索引碎片,你須要進行索引碎片整理。
什麼是索引碎片?
因爲表上有過分地插入、修改和刪除操做,索引頁被分紅多塊就造成了索引碎片,若是索引碎片嚴重,那掃描索引的時間就會變長,甚至致使索引不可用,所以數據檢索操做就慢下來了。
有兩種類型的索引碎片:內部碎片和外部碎片。
內部碎片:爲了有效的利用內存,使內存產生更少的碎片,要對內存分頁,內存以頁爲單位來使用,最後一頁每每裝不滿,因而造成了內部碎片。
外部碎片:爲了共享要分段,在段的換入換出時造成外部碎片,好比5K的段換出後,有一個4k的段進來放到原來5k的地方,因而造成1k的外部碎片。
如何知道是否發生了索引碎片?
執行下面的SQL語句就知道了(下面的語句能夠在SQL Server 2005及後續版本中運行,用你的數據庫名替換掉這裏的AdventureWorks):
SELECTobject_name(dt.object_id) Tablename,si.name IndexName,dt.avg_fragmentation_in_percent AS ExternalFragmentation,dt.avg_page_space_used_in_percent AS InternalFragmentation FROM ( SELECTobject_id,index_id,avg_fragmentation_in_percent,avg_page_space_used_in_percent FROM sys.dm_db_index_physical_stats (db_id('AdventureWorks'),null,null,null,'DETAILED' ) WHERE index_id <>0) AS dt INNERJOIN sys.indexes si ON si.object_id=dt.object_id AND si.index_id=dt.index_id AND dt.avg_fragmentation_in_percent>10 AND dt.avg_page_space_used_in_percent<75ORDERBY avg_fragmentation_in_percent DESC
執行後顯示AdventureWorks數據庫的索引碎片信息。
圖 3 索引碎片信息
使用下面的規則分析結果,你就能夠找出哪裏發生了索引碎片:
1)ExternalFragmentation的值>10表示對應的索引起生了外部碎片;
2)InternalFragmentation的值<75表示對應的索引起生了內部碎片。
如何整理索引碎片?
有兩種整理索引碎片的方法:
1)重組有碎片的索引:執行下面的命令
ALTER INDEX ALL ON TableName REORGANIZE
2)重建索引:執行下面的命令
ALTER INDEX ALL ON TableName REBUILD WITH (FILLFACTOR=90,ONLINE=ON)
也能夠使用索引名代替這裏的「ALL」關鍵字重組或重建單個索引,也能夠使用SQL Server管理工做臺進行索引碎片的整理。
圖 4 使用SQL Server管理工做臺整理索引碎片
何時用重組,何時用重建呢?
當對應索引的外部碎片值介於10-15之間,內部碎片值介於60-75之間時使用重組,其它狀況就應該使用重建。
值得注意的是重建索引時,索引對應的表會被鎖定,但重組不會鎖表,所以在生產系統中,對大表重建索引要慎重,由於在大表上建立索引可能會花幾個小時,幸運的是,從SQL Server 2005開始,微軟提出了一個解決辦法,在重建索引時,將ONLINE選項設置爲ON,這樣能夠保證重建索引時表仍然能夠正常使用。
雖然索引能夠提升查詢速度,但若是你的數據庫是一個事務型數據庫,大多數時候都是更新操做,更新數據也就意味着要更新索引,這個時候就要兼顧查詢和更新操做了,由於在OLTP數據庫表上建立過多的索引會下降總體數據庫性能。
我給你們一個建議:若是你的數據庫是事務型的,平均每一個表上不能超過5個索引,若是你的數據庫是數據倉庫型,平均每一個表能夠建立10個索引都沒問題。
在前面咱們介紹瞭如何正確使用索引,調整索引是見效最快的性能調優方法,但通常而言,調整索引只會提升查詢性能。除此以外,咱們還能夠調整數據訪問代碼和TSQL,本文就介紹如何以最優的方法重構數據訪問代碼和TSQL。
第四步:將TSQL代碼從應用程序遷移到數據庫中
也許你不喜歡個人這個建議,你或你的團隊可能已經有一個默認的潛規則,那就是使用ORM(Object Relational Mapping,即對象關係映射)生成全部SQL,並將SQL放在應用程序中,但若是你要優化數據訪問性能,或須要調試應用程序性能問題,我建議你將SQL代碼移植到數據庫上(使用存儲過程,視圖,函數和觸發器),緣由以下:
一、使用存儲過程,視圖,函數和觸發器實現應用程序中SQL代碼的功能有助於減小應用程序中SQL複製的弊端,由於如今只在一個地方集中處理SQL,爲之後的代碼複用打下了良好的基礎。
二、使用數據庫對象實現全部的TSQL有助於分析TSQL的性能問題,同時有助於你集中管理TSQL代碼。
三、將TS QL移植到數據庫上去後,能夠更好地重構TSQL代碼,以利用數據庫的高級索引特性。此外,應用程序中沒了SQL代碼也將更加簡潔。
雖然這一步可能不會象前三步那樣立竿見影,但作這一步的主要目的是爲後面的優化步驟打下基礎。若是在你的應用程序中使用ORM(如NHibernate)實現了數據訪問例行程序,在測試或開發環境中你可能發現它們工做得很好,但在生產數據庫上卻可能遇到問題,這時你可能須要反思基於ORM的數據訪問邏輯,利用TSQL對象實現數據訪問例行程序是一種好辦法,這樣作有更多的機會從數據庫角度來優化性能。
我向你保證,若是你花1-2人月來完成遷移,那之後確定不止節約1-2人年的的成本。
OK!假設你已經照個人作的了,徹底將TSQL遷移到數據庫上去了,下面就進入正題吧!
第五步:識別低效TSQL,採用最佳實踐重構和應用TSQL
因爲每一個程序員的能力和習慣都不同,他們編寫的TSQL可能風格各異,部分代碼可能不是最佳實現,對於水平通常的程序員可能首先想到的是編寫TSQL實現需求,至於性能問題往後再說,所以在開發和測試時可能發現不了問題。
也有一些人知道最佳實踐,但在編寫代碼時因爲種種緣由沒有采用最佳實踐,等到用戶發飆的那天才乖乖地從新埋頭思考最佳實踐。
我以爲仍是有必要介紹一下具備都有哪些最佳實踐。
一、在查詢中不要使用「select *」
(1)檢索沒必要要的列會帶來額外的系統開銷,有句話叫作「該省的則省」;
(2)數據庫不能利用「覆蓋索引」的優勢,所以查詢緩慢。
二、在select清單中避免沒必要要的列,在鏈接條件中避免沒必要要的表
(1)在select查詢中若有沒必要要的列,會帶來額外的系統開銷,特別是LOB類型的列;
(2)在鏈接條件中包含沒必要要的表會強制數據庫引擎檢索和匹配不須要的數據,增長了查詢執行時間。
三、不要在子查詢中使用count()求和執行存在性檢查
(1)不要使用 SELECT column_list FROMtableWHERE0< (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語句:
這條語句會執行全表掃描才能得到行數。
(2)但下面的SQL語句不會執行全表掃描同樣能夠得到行數:
八、避免使用動態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_開頭的),同時這麼作還會增長與系統存儲過程名稱衝突的概率;能夠使用USP_xxx
(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受命來優化這個存儲過程,下面是這個存儲過程的代碼:
ALTERPROCEDURE uspGetSalesInfoForDateRange @startYearDateTime, @endYearDateTime, @keywordnvarchar(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 INNERJOIN OrderDetails ON Products.ProductID = OrderDetails.ProductID INNERJOIN Orders ON Orders.SalesOrderID = OrderDetails.SalesOrderID INNERJOIN ProductRates ON Products.ProductID = ProductRates.ProductID WHERE OrderDate between@startYearand@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+'%' ) ORDERBY 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有問題,代碼以下:
ALTERFUNCTION[dbo].[ufnGetLineTotal] ( @SalesOrderDetailIDint ) RETURNSmoney AS BEGIN DECLARE@CurrentProductRatemoney DECLARE@CurrentDiscountmoney DECLARE@Qtyint SELECT @CurrentProductRate= ProductRates.CurrentProductRate, @CurrentDiscount= ProductRates.CurrentDiscount, @Qty= OrderQty FROM ProductRates INNERJOIN 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在索引方面還提供了某些高級特性,可能你還從未使用過,利用高級索引會顯著地改善系統性能,本文將從高級索引技術談起,另外還將介紹反範式化技術。
第六步:應用高級索引
實施計算列並在這些列上建立索引
你可能曾經寫過從數據庫查詢一個結果集的應用程序代碼,對結果集中每一行進行計算生成最終顯示輸出的信息。例如,你可能有一個查詢從數據庫檢索訂單信息,在應用程序代碼中你可能已經經過對產品和銷售量執行算術操做計算出了總的訂單價格,但爲何你不在數據庫中執行這些操做呢?
請看下面這張圖,你能夠經過指定一個公式將一個數據庫表列做爲計算列,你的TSQL在查詢清單中包括這個計算列,SQL引擎將會應用這個公式計算出這一列的值,在執行查詢時,數據庫引擎將會計算訂單總價,併爲計算列返回結果。
圖 1 計算列
使用計算列你能夠將計算工做所有交給後端執行,但若是表的行數太多可能計算性能也不高,若是計算列出如今Select查詢的where子句中狀況會更糟,在這種狀況下,爲了匹配where子句指定的值,數據庫引擎不得不計算表中全部行中計算列的值,這是一個低效的過程,由於它老是須要全表掃描或全彙集索引掃描。
所以問題就來了,如何提升計算列的性能呢?解決辦法是在計算列上建立索引,當計算列上有索引後,SQL Server會提早計算結果,而後在結果之上構建索引。此外,當對應列(計算列依賴的列)的值更新時,計算列上的索引值也會更新。所以,在執行查詢時,數據庫引擎不會爲結果集中的每一行都執行一次計算公式,相反,經過索引可直接得到計算列預先計算出的值,所以在計算列上建立一個索引將會加快查詢速度。
提示:若是你想在計算列上建立索引,必須確保計算列上的公式不能包括任何「非肯定的」函數,例如getdate()就是一個非肯定的函數,由於每次調用它,它返回的值都是不同的。
建立索引視圖
你是否知道能夠在視圖上建立索引?OK,不知道不要緊,看了個人介紹你就明白了。
爲何要使用視圖?
你們都知道,視圖自己不存儲任何數據,只是一條編譯的select語句。數據庫會爲視圖生成一個執行計劃,視圖是能夠重複使用的,由於執行計劃也能夠重複使用。
視圖自己不會帶來性能的提高,我曾經覺得它會「記住」查詢結果,但後來我才知道它除了是一個編譯了的查詢外,其它什麼都不是,視圖根本記不住查詢結果,我敢打賭好多剛接觸SQL的人都會有這個錯誤的想法。
可是如今我要告訴你一個方法讓視圖記住查詢結果,其實很是簡單,就是在視圖上建立索引就能夠了。
若是你在視圖上應用了索引,視圖就成爲索引視圖,對於一個索引視圖,數據庫引擎處理SQL,並在數據文件中存儲結果,和彙集表相似,當基礎表中的數據發生變化時,SQL Server會自動維護索引,所以當你在索引視圖上查詢時,數據庫引擎簡單地從索引中查找值,速度固然就很快了,所以在視圖上建立索引能夠明顯加快查詢速度。
但請注意,天下沒有免費的午飯,建立索引視圖能夠提高性能,當基礎表中的數據發生變化時,數據庫引擎也會更新索引,所以,當視圖要處理不少行,且要求和,當數據和基礎表不常常發生變化時,就應該考慮建立索引視圖。
如何建立索引視圖?
1)建立/修改視圖時指定SCHEMABINDING選項: REATE VIEW dbo.vOrderDetails WITH SCHEMABINDING AS SELECT… 2)在視圖上建立一個惟一的彙集索引; 3)視須要在視圖上建立一個非彙集索引。
不是全部視圖上均可以建立索引,在視圖上建立索引存在如下限制:
1)建立視圖時使用了SCHEMABINDING選項,這種狀況下,數據庫引擎不容許你改變表的基礎結構;
2)視圖不能包含任何非肯定性函數,DISTINCT子句和子查詢;
3)視圖中的底層表必須由彙集索引(主鍵)。
若是你發現你的應用程序中使用的TSQL是用視圖實現的,但存在性能問題,那此時給視圖加上索引可能會帶來性能的提高。
爲用戶定義函數(UDF)建立索引
在用戶定義函數上也能夠建立索引,但不能直接在它上面建立索引,須要建立一個輔助的計算列,公式就使用用戶定義函數,而後在這個計算列字段上建立索引。具體步驟以下:
1)首先建立一個肯定性的函數(若是不存在的話),在函數定義中添加SCHEMABINDING選項,如:
CREATEFUNCTION[dbo.ufnGetLineTotal] ( -- Add the parameters for the function here @UnitPrice[money], @UnitPriceDiscount[money], @OrderQty[smallint] ) RETURNS money WITH SCHEMABINDING AS BEGIN return (((@UnitPrice*((1.0)-@UnitPriceDiscount))*@OrderQty)) END
2)在目標表上增長一個計算列,使用前面定義的函數做爲該列的計算公式,如圖2所示。
CREATE TABLE #t ( UnitPrice money, UnitPriceDiscount money, OrderQty smallint OrderTotal AS dbo.ufnGetLineTotal(UnitPrice ,UnitPriceDiscount ,OrderQty) )
圖 2 指定UDF爲計算列的結算公式
3)在計算列上建立索引
當你的查詢中包括UDF時,若是在該UDF上建立了以計算列爲基礎的索引,特別是兩個表或視圖的鏈接條件中使用了UDF,性能都會有明顯的改善。
在XML列上建立索引
在SQL Server(2005和後續版本)中,XML列是以二進制大對象(BLOB)形式存儲的,能夠使用XQuery進行查詢,但若是沒有索引,每次查詢XML數據類型時都很是耗時,特別是大型XML實例,由於SQL Server在運行時須要分隔二進制大對象評估查詢。爲了提高XML數據類型上的查詢性能,XML列能夠索引,XML索引分爲兩類。
主XML索引
建立XML列上的主索引時,SQL Server會切碎XML內容,建立多個數據行,包括元素,屬性名,路徑,節點類型和值等,建立主索引讓SQL Server更輕鬆地支持XQuery請求。下面是建立一個主XML索引的示例語法。
index_name
ON<object> ( xml_column )
次要XML索引
雖然XML數據已經被切條,但SQL Server仍然要掃描全部切條的數據才能找到想要的結果,爲了進一步提高性能,還須要在主XML索引之上建立次要XML索引。有三種次要XML索引。
1)「路徑」(Path)次要XML索引:使用.exist()方法肯定一個特定的路徑是否存在時它頗有用;
2)「值」(Value)次要XML索引:用於執行基於值的查詢,但不知道完整的路徑或路徑包括通配符時;
3)「屬性」(Secondary)次要XML索引:知道路徑時檢索屬性的值。
下面是一個建立次要XML索引的示例:
CREATE XML INDEX index_name ON<object> ( xml_column ) USING XML INDEX primary_xml_index_name FOR { VALUE | PATH | PROPERTY }
請注意,上面講的原則是基礎,若是盲目地在表上建立索引,不必定會提高性能,由於有時在某些表的某些列上建立索引時,可能會導致插入和更新操做變慢,當這個表上有一個低選中性列時更是如此,一樣,當表中的記錄不多(如<500)時,若是在這樣的表上建立索引反倒會使數據檢索性能下降,由於對於小表而言,全表掃描反而會更快,所以在建立索引時應放聰明一點。
第七步:應用反範式化,使用歷史表和預計算列
反範式化
若是你正在爲一個OLTA(在線事務分析)系統設計數據庫,主要指爲只讀查詢優化過的數據倉庫,你能夠(和應該)在你的數據庫中應用反範式化和索引,也就是說,某些數據能夠跨多個表存儲,但報告和數據分析查詢在這種數據庫上可能會更快。
但若是你正在爲一個OLTP(聯機事務處理)系統設計數據庫,這樣的數據庫主要執行數據更新操做(包括插入/更新/刪除),我建議你至少實施第1、2、三範式,這樣數據冗餘能夠降到最低,數據存儲也能夠達到最小化,可管理性也會好一點。
不管咱們在OLTP系統上是否應用範式,在數據庫上總有大量的讀操做(即select查詢),當應用了全部優化技術後,若是發現數據檢索操做仍然效率低下,此時,你可能須要考慮應用反範式設計了.
但問題是如何應用反範式化,以及爲何應用反範式化會提高性能?讓咱們來看一個簡單的例子,答案就在例子中。
假設咱們有兩個表OrderDetails(ID,ProductID,OrderQty) 和 Products(ID,ProductName)分別存儲訂單詳細信息和產品信息,如今要查詢某個客戶訂購的產品名稱和它們的數量,查詢SQL語句以下:
SELECT Products.ProductName,OrderQty FROM OrderDetails INNERJOIN Products ON OrderDetails.ProductID = Products.ProductID WHERE SalesOrderID =47057
若是這兩個都是大表,當你應用了全部優化技巧後,查詢速度仍然很慢,這時能夠考慮如下反範式化設計:
1)在OrderDetails表上添加一列ProductName,並填充好數據;
2)重寫上面的SQL語句
SELECT ProductName,OrderQty FROM OrderDetails WHERE SalesOrderID =47057
注意在OrderDetails表上應用了反範式化後,再也不須要鏈接Products表,所以在執行SQL時,SQL引擎不會執行兩個表的鏈接操做,查詢速度固然會快一些。
爲了提升select操做性能,咱們不得不作出一些犧牲,須要在兩個地方(OrderDetails 和 Products表)存儲相同的數據(ProductName),當咱們插入或更新Products 表中的ProductName字段時,不得不一樣步更新OrderDetails表中的ProductName字段,此外,應用這種反範式化設計時會增長存儲資源消耗。
所以在實施反範式化設計時,咱們必須在數據冗餘和查詢操做性能之間進行權衡,同時在應用反範式化後,咱們不得不重構某些插入和更新操做代碼。有一個重要的原則須要遵照,那就是隻有當你應用了全部其它優化技術都還不能將性能提高到理想狀況時才使用反範式化。同時還需注意不能使用太多的反範式化設計,那樣會使本來清晰的表結構設計變得越來模糊。
歷史表
若是你的應用程序中有按期運行的數據檢索操做(如報表),若是涉及到大表的檢索,能夠考慮按期將事務型規範化表中的數據複製到反範式化的單一的歷史表中,如利用數據庫的Job來完成這個任務,並對這個歷史表創建合適的索引,那麼週期性執行的數據檢索操做能夠遷移到這個歷史表上,對單個歷史表的查詢性能確定比鏈接多個事務表的查詢速度要快得多。
例如,假設有一個連鎖商店的月度報表須要3個小時才能執行完畢,你被派去優化這個報表,目的只有一個:最小化執行時間。那麼你除了應用其它優化技巧外,還能夠採起如下手段:
1)使用反範式化結構建立一個歷史表,並對銷售數據創建合適的索引;
2)在SQL Server上建立一個按期執行的操做,每隔24小時運行一次,在半夜往歷史表中填充數據;
3)修改報表代碼,從歷史表獲取數據。
建立按期執行的操做
按照下面的步驟在SQL Server中建立一個按期執行的操做,按期從事務表中提取數據填充到歷史表中。
1)首先確保SQL Server代理服務處於運行狀態;
2)在SQL Server配置管理器中展開SQL Server代理節點,在「做業」節點上建立一個新做業,在「常規」標籤頁中,輸入做業名稱和描述文字;
3)在「步驟」標籤頁中,點擊「新建」按鈕建立一個新的做業步驟,輸入名字和TSQL代碼,最後保存;
4)切換到「調度」標籤頁,點擊「新建」按鈕建立一個新調度計劃;
5)最後保存調度計劃。
在數據插入和更新中提早執行耗時的計算,簡化查詢
大多數狀況下,你會看到你的應用程序是一個接一個地執行數據插入或更新操做,一次只涉及到一條記錄,但數據檢索操做可能同時涉及到多條記錄。
若是你的查詢中包括一個複雜的計算操做,毫無疑問這將致使總體的查詢性能降低,你能夠考慮下面的解決辦法:
1)在表中建立額外的一列,包含計算的值;
2)爲插入和更新事件建立一個觸發器,使用相同的計算邏輯計算值,計算完成後更新到新建的列;
3)使用新建立的列替換查詢中的計算邏輯。
實施完上述步驟後,插入和更新操做可能會更慢一點,由於每次插入和更新時觸發器都會執行一下,但數據檢索操做會比以前快得多,由於執行查詢時,數據庫引擎不會執行計算操做了。
小結
至此,咱們已經應用了索引,重構TSQL,應用高級索引,反範式化,以及歷史表加速數據檢索速度,但性能優化是一個永無終點的過程,最下一篇文章中咱們將會介紹如何診斷數據庫性能問題。
診斷數據庫性能問題就象醫生診斷病人病情同樣,既要結合本身積累的經驗,又要依靠科學的診斷報告,才能準確地判斷問題的根源在哪裏。前面三篇文章咱們介紹了許多優化數據庫性能的方法,當然掌握優化技巧很重要,但診斷數據庫性能問題是優化的前提,本文就介紹一下如何診斷數據庫性能問題。
第八步:使用SQL事件探查器和性能監控工具備效地診斷性能問題
在SQL Server應用領域SQL事件探查器多是最著名的性能故障排除工具,大多數狀況下,當獲得一個性能問題報告後,通常首先啓動它進行診斷。
你可能已經知道,SQL事件探查器是一個跟蹤和監控SQL Server實例的圖形化工具,主要用於分析和衡量在數據庫服務器上執行的TSQL性能,你能夠捕捉服務器實例上的每一個事件,將其保存到文件或表中供之後分析。例如,若是生產數據庫速度很慢,你能夠使用SQL事件探查器查看哪些存儲過程執行時耗時過多。
SQL事件探查器的基本用法(sqlprofile)
你可能已經知道如何使用它,那麼你能夠跳過這一小節,但我仍是要重複一下,也許有許多新手閱讀本文。
1)啓動SQL事件探查器,鏈接到目標數據庫實例,建立一個新跟蹤,指定一個跟蹤模板(跟蹤模板預置了一些事件和用於跟蹤的列),如圖1所示;
圖 1 選擇跟蹤模板
2)做爲可選的一步,你還能夠選擇特定事件和列
圖 2 選擇跟蹤過程要捕捉的事件
3)另外你還能夠點擊「組織列」按鈕,在彈出的窗口中指定列的顯示順序,點擊「列過濾器」按鈕,在彈出的窗口中設置過濾器,例如,經過設置數據庫的名稱(在like文本框中),只跟蹤特定的數據庫,若是不設置過濾器,SQL事件探查器會捕捉全部的事件,跟蹤的信息會很是多,要找出有用的關鍵信息就如大海撈針。
圖 3 過濾器設置
4)運行事件探查器,等待捕捉事件
圖 4 運行事件探查器
5)跟蹤了足夠的信息後,停掉事件探查器,將跟蹤信息保存到一個文件中,或者保存到一個數據表中,若是保存到表中,須要指定表名,SQL Server會自動建立表中的字段。
圖 5 將探查器跟蹤數據保存到表中
6)執行下面的SQL查詢語句找出執行代價較高的TSQL
Duration DESC
圖 6 查找成本最高的TSQL/存儲過程
有效利用SQL事件探查器排除與性能相關的問題
SQL事件探查器除了能夠用於找出執行成本最高的那些TSQL或存儲過程外,還能夠利用它許多強大的功能診斷和解決其它不一樣類型的問題。當你收到一個性能問題報告後,或者想提早診斷潛在的性能問題時均可以使用SQL事件探查器。下面是一些SQL事件探查器使用技巧,或許對你有幫助。
1)使用現有的模板,但須要時應建立你本身的模板
大多數時候現有的模板可以知足你的需求,但當診斷一個特殊類型的數據庫性能問題時(如數據庫發生死鎖),你可能須要建立本身的模板,在這種狀況下,你能夠點擊「文件」*「模板」*「新建模板」建立一個新模板,須要指定模板名、事件和列。固然也能夠從現有的模板修改而來。
圖 7 建立一個新模板
圖 8 爲新模板指定事件和列
2)捕捉表掃描(TableScan)和死鎖(DeadLock)事件
沒錯,你能夠使用SQL事件探查器監聽這兩個有趣的事件。
先假設一種狀況,假設你已經在你的測試庫上建立了合適的索引,通過測試後,如今你已經將索引應用到生產服務器上了,但因爲某些不明緣由,生產數據庫的性能一直沒達到預期的那樣好,你推測執行查詢時發生了表掃描,你但願有一種方法可以檢測出是否真的發生了表掃描。
再假設另外一種狀況,假設你已經設置好了將錯誤郵件發送到一個指定的郵件地址,這樣開發團隊能夠第一時間得到通知,並有足夠的信息進行問題診斷。某一天,你忽然收到一封郵件說數據庫發生了死鎖,並在郵件中包含了數據庫級別的錯誤代碼,你須要找出是哪一個TSQL創造了死鎖。
這時你能夠打開SQL事件探查器,修改一個現有模板,使其能夠捕捉表掃描和死鎖事件,修改好後,啓動事件探查器,運行你的應用程序,當再次發生表掃描和死鎖事件時,事件探查器就能夠捕捉到,利用跟蹤信息就能夠找出執行代價最高的TSQL。
注意:從SQL Server日誌文件中可能也能夠找到死鎖事件記錄,在某些時候,你可能須要結合SQL Server日誌和跟蹤信息才能找出引發數據庫死鎖的數據庫對象和TSQL。
圖 9 檢測表掃描
圖 10 檢測死鎖
3)建立重放跟蹤
某些時候,爲了解決生產數據庫的性能問題,你須要在測試服務器上模擬一個生產環境,這樣能夠重演性能問題。使用SQL事件探查器的TSQL_Replay模板捕捉生產庫上的事件,並將跟蹤信息保存爲一個.trace文件,而後在測試服務器上播放跟蹤文件就能夠重現性能問題是如何出現的了。
圖 11 建立重放跟蹤
4)建立優化跟蹤
數據庫調優顧問是一個偉大的工具,它能夠給你提供很好的調優建議,但要真正從它那得到有用的建議,你須要模擬出與生產庫同樣的負載,也就是說,你須要在測試服務器上執行相同的TSQL,打開相同數量的併發鏈接,而後運行調優顧問。SQL事件探查器的Tuning模板能夠捕捉到這類事件和列,使用Tuning模板運行事件探查器,捕捉跟蹤信息並保存,經過調優顧問使用跟蹤文件在測試服務器上建立相同的負載。
圖 12 建立Tuning事件探查器跟蹤
5)捕捉ShowPlan在事件探查器中包括SQL執行計劃
有時相同的查詢在測試服務器和生產服務器上的性能徹底不同,假設你遇到這種問題,你應該仔細查看一下生產數據庫上TSQL的執行計劃。但問題是如今不能在生產庫上執行這個TSQL,由於它已經有嚴重的性能問題。這時SQL事件探查器能夠派上用場,在跟蹤屬性中選中ShowPlan或ShowPlan XML,這樣能夠捕捉到SQL執行計劃和TSQL文本,而後在測試服務器上執行相同的TSQL,並比較二者的執行計劃。
圖 13 指定捕捉執行計劃
圖 14 在事件探查器跟蹤中的執行計劃
使用性能監視工具(PerfMon)診斷性能問題
當你的數據庫遇到性能問題時,大多數時候使用SQL事件探查器就可以診斷和找出引發性能問題的背後緣由了,但有時SQL事件探查器並非萬能的。
例如,在生產庫上使用SQL事件探查器分析查詢執行時間時,對應的TSQL執行很慢(假設須要10秒),但一樣的TSQL在測試服務器上執行時間卻只要200毫秒,經過分析執行計劃和數據列,發現它們都沒有太大的差別,所以在生產庫上確定有其它問題,那該如何揪出這些問題呢?
此時性能監視工具(著名的PerfMon)能夠幫你一把,它能夠按期收集硬件和軟件相關的統計數據,還有它是內置於Windows操做系統的一個免費的工具。
當你向SQL Server數據庫發送一條TSQL語句,會產生許多相關的執行參與者,包括TSQL執行引擎,服務器緩存,SQL優化器,輸出隊列,CPU,磁盤I/O等,只要這些參與者任何一環執行節奏沒有跟上,最終的查詢執行時間就會變長,使用性能監視工具能夠對這些參與者進行觀察,以找出根本緣由。
使用性能監視工具能夠建立多個不一樣的性能計數器,經過圖形界面分析計數器日誌,此外還能夠將性能計數器日誌和SQL事件探查器跟蹤信息結合起來分析。
性能監視器基本用法介紹
Windows內置了許多性能監視計數器,安裝SQL Server時會添加一個SQL Server性能計數器,下面是建立一個性能計數器日誌的過程。
1)在SQL事件探查器中啓動性能監視工具(「工具」*「性能監視器」);
圖 15 啓動性能監視工具
2)點擊「計數器日誌」*「新建日誌設置」建立一個新的性能計數器日誌
圖 16 建立一個性能計數器日誌
指定日誌文件名,點擊「肯定」。
圖 17 爲性能計數器日誌指定名字
3)點擊「添加計數器」按鈕,選擇一個須要的計數器
圖 18 爲性能計數器日誌指定計數器
4)從列表中選擇要監視的對象和對應的計數器,點擊「關閉」
圖 19 指定對象和對應的計數器
5)選擇的計數器應顯示在窗體中
圖 20 指定計數器
6)點擊「日誌文件」標籤,再點擊「配置」按鈕,指定日誌文件保存位置,若是須要如今還能夠修改日誌文件名
圖 21 指定性能計數器日誌文件保存位置
7)點擊「調度」標籤,指定一個時間讀取計數器性能,寫入日誌文件,也能夠選擇「手動」啓動和中止計數器日誌。
圖 22 指定性能計數器日誌運行時間
8)點擊「常規」標籤,指定收集計數器數據的間隔時間
圖 23 設置計數器間隔採樣時間
9)點擊「肯定」,選擇剛剛建立的計數器日誌,點擊右鍵啓動它。
圖 24 啓動性能計數器日誌
10)爲了查看日誌數據,再次打開性能監視工具,點擊查看日誌圖標(紅色),在「源」標籤上選中「日誌文件」單選按鈕,點擊「添加」按鈕添加一個日誌文件。
圖 25 查看性能計數器日誌
11)默認狀況下,在日誌輸出中只有三個計數器被選中,點擊「數據」標籤能夠追加其它計數器。
圖 26 查看日誌數據時追加計數器
12)點擊「肯定」,返回圖形化的性能計數器日誌輸出界面
圖 27 查看性能計數器日誌
關聯性能計數器日誌和SQL事件探查器跟蹤信息進行深刻的分析
經過SQL事件探查器能夠找出哪些SQL執行時間過長,但它卻不能給出致使執行時間過長的上下文信息,但性能監視工具能夠提供獨立組件的性能統計數據(即上下文信息),它們正好互補。
若是相同的查詢在生產庫和測試庫上的執行時間差異過大,那說明測試服務器的負載,環境和查詢執行上下文都和生產服務器不同,所以須要一種方法來模擬生產服務器上的查詢執行上下文,這時就須要結合SQL事件探查器的跟蹤信息和性能監視工具的性能計數器日誌。
將兩者結合起來分析能夠更容易找出性能問題的根本緣由,例如,你可能發如今生產服務器上每次查詢都須要10秒,CPU利用率達到了100%,這時就應該放下SQL調優,先調查一下爲何CPU利用率會上升到100%。
關聯SQL事件探查器跟蹤信息和性能計數器日誌的步驟以下:
1)建立性能計數器日誌,包括下列常見的性能計數器,指定「手動」方式啓動和中止計數器日誌:
--網絡接口\輸出隊列長度
--處理器\%處理器時間
--SQL Server:緩衝管理器\緩衝區緩存命中率
--SQL Server:緩衝管理器\頁面生命週期
--SQL Server:SQL統計\批量請求數/秒
--SQL Server:SQL統計\SQL 編譯
--SQL Server:SQL統計\SQL 從新編譯/秒
建立好性能計數器日誌,但不啓動它。
2)使用SQL事件探查器TSQL Duration模板建立一個跟蹤,添加「開始時間」和「結束時間」列跟蹤,同時啓動事件探查器跟蹤和前一步建立的性能計數器日誌;
3)跟蹤到足夠信息後,同時停掉SQL事件探查器跟蹤和性能計數器日誌,將SQL事件探查器跟蹤信息保存爲一個.trc文件;
4)關閉SQL事件探查器跟蹤窗口,再使用事件探查器打開.trc文件,點擊「文件」*「導入性能數據」關聯性能計數器日誌,此時會打開一個文件瀏覽器窗口,選擇剛剛保存的性能計數器日誌文件進行關聯;
5)在打開的窗口中選擇全部計數器,點擊「肯定」,你將會看到下圖所示的界面,它同時顯示SQL事件探查器的跟蹤信息和性能計數器日誌;
圖 28 關聯SQL事件探查器和性能監視工具輸出
6)在事件探查器跟蹤信息輸出中選擇一條TSQL,你將會看到一個紅色豎條,這表明這條TSQL執行時相關計數器的統計數據位置,一樣,點擊性能計數器日誌輸出曲線中高於正常值的點,你會看到對應的TSQL在SQL事件探查器輸出中也是突出顯示的。
我相信你學會如何關聯這兩個工具的輸出數據後,必定會以爲很是方便和有趣。
小結
診斷SQL Server性能問題的工具和技術有不少,例如查看SQL Server日誌文件,利用調優顧問(DTA)得到調優建議,不管使用哪一種工具,你都須要深刻了解內部的細節緣由,只有找出最根本的緣由以後,解決性能問題纔會駕輕就熟。
本系列最後一篇將介紹如何優化數據文件和應用分區。
優化技巧主要是面向DBA的,但我認爲即便是開發人員也應該掌握這些技巧,由於不是每一個開發團隊都配有專門的DBA的。
第九步:合理組織數據庫文件組和文件
建立SQL Server數據庫時,數據庫服務器會自動在文件系統上建立一系列的文件,以後建立的每個數據庫對象實際上都是存儲在這些文件中的。SQL Server有下面三種文件:
1).mdf文件
這是最主要的數據文件,每一個數據庫只能有一個主數據文件,全部系統對象都存儲在主數據文件中,若是不建立次要數據文件,全部用戶對象(用戶建立的數據庫對象)也都存儲在主數據文件中。
2).ndf文件
這些都是次要數據文件,它們是可選的,它們存儲的都是用戶建立的對象。
3).ldf文件
這些是事務日誌文件,數量從一到幾個不等,它裏面存儲的是事務日誌。
默認狀況下,建立SQL Server數據庫時會自動建立主數據文件和事務日誌文件,固然也能夠修改這兩個文件的屬性,如保存路徑。
文件組
爲了便於管理和得到更好的性能,數據文件一般都進行了合理的分組,建立一個新的SQL Server數據庫時,會自動建立主文件組,主數據文件就包含在主文件組中,主文件組也被設爲默認組,所以全部新建立的用戶對象都自動存儲在主文件組中(具體說就是存儲在主數據文件中)。
若是你想將你的用戶對象(表、視圖、存儲過程和函數等)存儲在次要數據文件中,那須要:
1)建立一個新的文件組,並將其設爲默認文件組;
2)建立一個新的數據文件(.ndf),將其歸於第一步建立的新文件組中。
之後建立的對象就會所有存儲在次要文件組中了。
注意:事務日誌文件不屬於任何文件組。
文件/文件組組織最佳實踐
若是你的數據庫不大,那麼默認的文件/文件組應該就能知足你的須要,但若是你的數據庫變得很大時(假設有1000MB),你能夠(應該)對文件/文件組進行調整以得到更好的性能,調整文件/文件組的最佳實踐內容以下:
1)主文件組必須徹底獨立,它裏面應該只存儲系統對象,全部的用戶對象都不該該放在主文件組中。主文件組也不該該設爲默認組,將系統對象和用戶對象分開能夠得到更好的性能;
2)若是有多塊硬盤,能夠將每一個文件組中的每一個文件分配到每塊硬盤上,這樣能夠實現分佈式磁盤I/O,大大提升數據讀寫速度;
3)將訪問頻繁的表及其索引放到一個單獨的文件組中,這樣讀取表數據和索引都會更快;
4)將訪問頻繁的包含Text和Image數據類型的列的表放到一個單獨的文件組中,最好將其中的Text和Image列數據放在一個獨立的硬盤中,這樣檢索該表的非Text和Image列時速度就不會受Text和Image列的影響;
5)將事務日誌文件放在一個獨立的硬盤上,千萬不要和數據文件共用一塊硬盤,日誌操做屬於寫密集型操做,所以保證日誌寫入具備良好的I/O性能很是重要;
6)將「只讀」表單獨放到一個獨立的文件組中,一樣,將「只寫」表單獨放到一個文件組中,這樣只讀表的檢索速度會更快,只寫表的更新速度也會更快;
7)不要過分使用SQL Server的「自動增加」特性,由於自動增加的成本實際上是很高的,設置「自動增加」值爲一個合適的值,如一週,一樣,也不要過分頻繁地使用「自動收縮」特性,最好禁用掉自動收縮,改成手工收縮數據庫大小,或使用調度操做,設置一個合理的時間間隔,如一個月。
第十步:在大表上應用分區
什麼是表分區?
表分區就是將大表拆分紅多個小表,以避免檢索數據時掃描的數據太多,這個思想參考了「分而治之」的理論。
當你的數據庫中有一個大表(假設有上百萬行記錄),若是其它優化技巧都用上了,但查詢速度仍然很是慢時,你就應該考慮對這個表進行分區了。首先來看一下分區的類型:
水平分區:假設有一個表包括千萬行記錄,爲了便於理解,假設表有一個自動增加的主鍵字段(如id),咱們能夠將表拆分紅10個獨立的分區表,每一個分區包含100萬行記錄,分區就要依據id字段的值實施,即第一個分區包含id值從1-1000000的記錄,第二個分區包含1000001-2000000的記錄,以此類推。這種以水平方向分割表的方式就叫作水平分區。
垂直分區:假設有一個表的列數和行數都很是多,其中某些列被常常訪問,其他的列不是常常訪問。因爲表很是大,全部檢索操做都很慢,所以須要基於頻繁訪問的列進行分區,這樣咱們能夠將這個大表拆分紅多個小表,每一個小表由大表的一部分列組成,這種垂直拆分表的方法就叫作垂直分區。
另外一個垂直分區的原則是按有索引的列無索引列進行拆分,但這種分區法須要當心,由於若是任何查詢都涉及到檢索這兩個分區,SQL引擎不得不鏈接這兩個分區,那樣的話性能反而會低。
本文主要對水平分區作一介紹。
分區最佳實踐
1)將大表分區後,將每一個分區放在一個獨立的文件中,並將這個文件存放在獨立的硬盤上,這樣數據庫引擎能夠同時並行檢索多塊硬盤上的不一樣數據文件,提升併發讀寫速度;
2)對於歷史數據,能夠考慮基於歷史數據的「年齡」進行分區,例如,假設表中存儲的是訂單數據,能夠使用訂單日期列做爲分區的依據,如將每一年的訂單數據作成一個分區。
如何分區?
假設Order表中包含了四年(1999-2002)的訂單數據,有上百萬的記錄,那若是要對這個表進行分區,採起的步驟以下:
1)添加文件組
使用下面的命令建立一個文件組:
ALTER DATABASE OrderDB ADD FILEGROUP [1999]
ALTER DATABASE OrderDB ADD FILE (NAME = N'1999', FILENAME
= N'C:\OrderDB\1999.ndf', SIZE = 5MB, MAXSIZE = 100MB, FILEGROWTH = 5MB) TO
FILEGROUP [1999]
經過上面的語句咱們添加了一個文件組1999,而後增長了一個次要數據文件「C:\OrderDB\1999.ndf」到這個文件組中。
使用上面的命令再建立三個文件組2000,2001和2002,每一個文件組存儲一年的銷售數據。
2)建立分區函數
分區函數是定義分界點的一個對象,使用下面的命令建立分區函數:
CREATE PARTITION FUNCTION FNOrderDateRange (DateTime) AS
RANGE LEFT FOR VALUES ('19991231', '20001231', '20011231')
上面的分區函數指定:
DateTime<=1999/12/31的記錄進入第一個分區;
DateTime > 1999/12/31 且 <= 2000/12/31的記錄進入第二個分區;
DateTime > 2000/12/31 且 <= 2001/12/31的記錄進入第三個分區;
DateTime > 2001/12/31的記錄進入第四個分區。
RANGE LEFT指定應該進入左邊分區的邊界值,例如小於或等於1999/12/31的值都應該進入第一個分區,下一個值就應該進入第二個分區了。若是使用RANGE RIGHT,邊界值以及大於邊界值的值都應該進入右邊的分區,所以在這個例子中,邊界值2000/12/31就應該進入第二個分區,小於這個邊界值的值就應該進入第一個分區。
3)建立分區方案
經過分區方案在表/索引的分區和存儲它們的文件組之間創建映射關係。建立分區方案的命令以下:
CREATE PARTITION SCHEME OrderDatePScheme AS PARTITION FNOrderDateRange
TO ([1999], [2000], [2001], [2002])
在上面的命令中,咱們指定了:
第一個分區應該進入1999文件組;
第二個分區就進入2000文件組;
第三個分區進入2001文件組;
第四個分區進入2002文件組。
4)在表上應用分區
至此,咱們定義了必要的分區原則,如今須要作的就是給表分區了。首先使用DROP INDEX命令刪除表上現有的彙集索引,一般主鍵上有彙集索引,若是是刪除主鍵上的索引,還能夠經過DROP CONSTRAINT刪除主鍵來間接刪除主鍵上的索引,以下面的命令刪除PK_Orders主鍵:
ALTER TABLE Orders DROP CONSTRAINT PK_Orders;
在分區方案上從新建立彙集索引,命令以下:
CREATE UNIQUE CLUSTERED INDEX PK_Orders ON Orders(OrderDate) ON
OrderDatePScheme (OrderDate)
假設OrderDate列的數據在表中是惟一的,表將基於分區方案OrderDatePScheme被分區,最終被分紅四個小的部分,存放在四個文件組中。若是你對如何分區還有不清楚的地方,建議你去看看微軟的官方文章「SQL Server 2005中的分區表和索引」(地址:http://msdn.microsoft.com/en-us/library/ms345146%28SQL.90%29.aspx)。
第十一步:使用TSQL模板更好地管理DBMS對象(額外的一步)
爲了更好地管理DBMS對象(存儲過程,函數,視圖,觸發器等),須要遵循一致的結構,但因爲某些緣由(主要是時間限制),咱們未能維護一個一致的結構,所以後來遇到性能問題或其它緣由須要從新調試這些代碼時,那感受就像是作噩夢。
爲了幫助你們更好地管理DBMS對象,我建立了一些TSQL模板,利用這些模板你能夠快速地開發出結構一致的DBMS對象。
若是你的團隊有人專門負責檢查團隊成員編寫的TSQL代碼,在這些模板中專門有一個「審查」段落用來描寫審查意見。
我提交幾個常見的DBMS對象模板,它們是:
Template_StoredProcedure.txt:存儲過程模板(http://www.codeproject.com/KB/database/OrganizeFilesAndPartition/Template_StoredProcedure.txt)
Template_View.txt:視圖模板(http://www.codeproject.com/KB/database/OrganizeFilesAndPartition/Template_Trigger.txt)
Template_Trigger.txt:觸發器模板(http://www.codeproject.com/KB/database/OrganizeFilesAndPartition/Template_ScalarFunction.txt)
Template_ScalarFunction.txt:標量函數模板(http://www.codeproject.com/KB/database/OrganizeFilesAndPartition/Template_TableValuedFunction.txt)
emplate_TableValuedFunction.txt:表值函數模板(http://www.codeproject.com/KB/database/OrganizeFilesAndPartition/Template_View.txt)
1)如何建立模板?
首先下載前面給出的模板代碼,然打開SQL Server管理控制檯,點擊「查看」*「模板瀏覽器」;
點擊「存儲過程」節點,點擊右鍵,在彈出的菜單中選擇「新建」*「模板」,爲模板取一個易懂的名字;
在新建立的模板上點擊右鍵,選擇「編輯」,在彈出的窗口中輸入身份驗證信息,點擊「鏈接」;
鏈接成功後,在編輯器中打開下載的Template_StoredProcedure.txt,拷貝文件中的內容粘貼到新建的模板中,而後點擊「保存」。
上面是建立一個存儲過程模板的過程,建立其它DBMS對象過程相似。
2)如何使用模板?
建立好模板後,下面就演示如何使用模板了。
首先在模板瀏覽器中,雙擊剛剛建立的存儲過程模板,彈出身份驗證對話框,輸入對應的身份信息,點擊「鏈接」;
鏈接成功後,模板將會在編輯器中打開,變量將會賦上適當的值;
按Ctrl+Shift+M爲模板指定值,以下圖所示;
圖 1 爲模板參數指定值
點擊「OK」,而後在SQL Server管理控制檯中選擇目標數據庫,而後點擊「執行」按鈕;
若是一切順利,存儲過程就建立成功了。你能夠根據上面的步驟建立其它DBMS對象。
小結
優化講究的是一種「心態」,在優化數據庫性能時,首先要相信性能問題老是能夠解決的,而後就是結合經驗和最佳實踐努力進行優化,最重要的是要儘可能預防性能問題的發生,在開發和部署期間,要利用一切可利用的技術和經驗進行提早評估,千萬不要等問題出現了纔去想辦法解決,在開發期間多花一個小時實施最佳實踐,最後可能會給你節約上百小時的故障診斷和排除時間,要學會聰明地工做,而不是辛苦地工做!