除非你遵循本文介紹的這些技巧,不然很容易編寫出減慢查詢速度或鎖死數據庫的數據庫代碼。前端
因爲數據庫領域仍相對不成熟,每一個平臺上的 SQL 開發人員都在苦苦掙扎,一次又一次犯一樣的錯誤。程序員
固然,數據庫廠商在取得一些進展,並繼續在竭力處理較重大的問題。數據庫
不管 SQL 開發人員在 SQL Server、Oracle、DB二、Sybase、MySQL,仍是在其餘任何關係數據庫平臺上編寫代碼,併發性、資源管理、空間管理和運行速度都仍困擾着他們。後端
問題的一方面是,不存在什麼靈丹妙藥;針對幾乎每條最佳實踐,我均可以舉出至少一個例外。緩存
一般,開發人員找到本身青睞的方法,而懶得研究其餘方法。這也許是缺少教育的表現,或者開發人員沒有認識到本身什麼時候作錯了。也許針對一組本地測試數據,查詢運行起來順暢,可是換成生產級系統,表現就差強人意。服務器
我沒有指望 SQL 開發人員成爲管理員,但他們在編寫代碼時必須考慮到生產級環境的問題。若是他們在開發初期不這麼作,數據庫管理員後期會讓他們返工,遭殃的就是用戶。網絡
咱們說調優數據庫既是門藝術,又是門科學,這是有道理的,由於不多有全面適用的硬性規則。你在一個系統上解決的問題在另外一個系統上不是問題,反之亦然。併發
說到調優查詢,沒有正確的答案,但這並不意味着就此應該放棄。你能夠遵循如下17條原則,有望收到很好的效果。函數
不要用 UPDATE 代替 CASE高併發
這個問題很常見,卻很難發覺,許多開發人員經常忽視這個問題,緣由是使用 UPDATE 再天然不過,這彷佛合乎邏輯。
以這個場景爲例:你把數據插入一個臨時表中,若是另外一個值存在,須要它顯示某個值。
也許你從 Customer 表中提取記錄,想把訂單金額超過 100000 美圓的客戶標記爲「Preferred」。
於是,你將數據插入到表中,運行 UPDATE 語句,針對訂單金額超過 100000 美圓的任何客戶,將 CustomerRank 這一列設爲「Preferred」。
問題是,UPDATE 語句記入日誌,這就意味着每次寫入到表中,要寫入兩次。
解決辦法:在 SQL 查詢中使用內聯 CASE 語句,這檢驗每一行的訂單金額條件,並向表寫入「Preferred」標記以前,設置該標記,這樣處理性能提高幅度很驚人。
不要盲目地重用代碼
這個問題也很常見,咱們很容易拷貝別人編寫的代碼,由於你知道它能獲取所需的數據。
問題是,它經常獲取過多你不須要的數據,而開發人員不多精簡,所以到頭來是一大堆數據。
這一般表現爲 WHERE 子句中的一個額外外鏈接或額外條件。若是你根據本身的確切要求精簡重用的代碼,就能大幅提高性能。
須要幾列,就提取幾列
這個問題相似第 2 個問題,但這是列所特有的。很容易用 SELECT* 來編寫全部查詢代碼,而不是把列逐個列出來。
問題一樣是,它提取過多你不須要的數據,這個錯誤我見過無數次了。開發人員對一個有 120 列、數百萬行的表執行 SELECT* 查詢,但最後只用到其中的三五列。
所以,你處理的數據比實際須要的多得多,查詢返回結果是個奇蹟。你不只處理過多不須要的數據,還奪走了其餘進程的資源。
不要查詢兩次(double-dip)
這是我看到好多人犯的另外一個錯誤:寫入存儲過程,從一個有數億行的表中提取數據。
開發人員想提取住在加利福尼亞州,年收入高於 4 萬美圓的客戶信息。因而,他查詢住在加利福尼亞州的客戶,把查詢結果放到一個臨時表中。
而後再來查詢年收入高於 4 萬美圓的客戶,把那些結果放到另外一個臨時表中。最後他鏈接這兩個表,得到最終結果。
你是在逗我吧?這應該用一次查詢來完成,相反你對一個超大表查詢兩次。別犯傻了:大表儘可能只查詢一次,你會發現存儲過程執行起來快多了。
一種略有不一樣的場景是,某個過程的幾個步驟須要大表的一個子集時,這致使每次都要查詢大表。
想避免這個問題,只需查詢這個子集,並將它持久化存儲到別處,而後將後面的步驟指向這個比較小的數據集。
知道什麼時候使用臨時表
這個問題解決起來要麻煩一點,但效果顯著。在許多狀況下可使用臨時表,好比防止對大表查詢兩次。還可使用臨時表,大幅減小鏈接大表所需的處理能力。
若是你必須將一個錶鏈接到大表,該大表上又有條件,只需將大表中所需的那部分數據提取到臨時表中,而後再與該臨時錶鏈接,就能夠提高查詢性能。
若是存儲過程當中有幾個查詢須要對同一個表執行相似的鏈接,這一樣大有幫助。
預暫存數據
這是我最愛聊的話題之一,由於這是一種常常被人忽視的老方法。
若是你有一個報表或存儲過程(或一組)要對大表執行相似的鏈接操做,經過提早鏈接表,並將它們持久化存儲到一個表中來預暫存數據,就能夠對你大有幫助。
如今,報表能夠針對該預暫存表來運行,避免大鏈接。你並不是老是可使用這個方法,但一旦用得上,你會發現這絕對是節省服務器資源的好方法。
請注意:許多開發人員避開這個鏈接問題的作法是,將注意力集中在查詢自己上,根據鏈接建立只讀視圖,那樣就沒必要一次又一次鍵入鏈接條件。
但這種方法的問題是,仍要爲須要它的每一個報表運行查詢。若是預暫存數據,你只要運行一次鏈接(好比說報表前 10 分鐘),別人就能夠避免大鏈接了。
你不知道我有多喜歡這一招,在大多數環境下,有些經常使用表一直被鏈接起來,因此沒理由不能先預暫存起來。
批量刪除和更新
這是另外一個常常被忽視的技巧,若是你操做不當,刪除或更新來自大表的大量數據多是一場噩夢。
問題是,這兩種語句都做爲單一事務來運行。若是你須要終結它們,或者它們在執行時系統遇到了問題,系統必須回滾(roll back)整個事務,這要花很長的時間。
這些操做在持續期間還會阻塞其餘事務,實際上給系統帶來了瓶頸,解決辦法就是,小批量刪除或更新。
這經過幾個方法來解決問題:
不管事務因什麼緣由而被終結,它只有少許的行須要回滾,那樣數據庫聯機返回快得多。
小批量事務被提交到磁盤時,其餘事務能夠進來處理一些工做,於是大大提升了併發性。
一樣,許多開發人員一直執拗地認爲:這些刪除和更新操做必須在同一天完成。事實並不是老是如此,若是你在歸檔更是如此。
若是你須要延長該操做,能夠這麼作,小批量有助於實現這點;若是你花更長的時間來執行這些密集型操做,切忌拖慢系統的運行速度。
使用臨時表來提升遊標性能
若是可能的話,最好避免遊標。遊標不只存在速度問題,而速度問題自己是許多操做的一大問題,還會致使你的操做長時間阻塞其餘操做,這大大下降了系統的併發性。
然而沒法老是避免使用遊標,避免不了使用遊標時,能夠改而對臨時表執行遊標操做,以此擺脫遊標引起的性能問題。
不妨以查閱一個表,基於一些比較結果來更新幾個列的遊標爲例。你也許能夠將該數據放入臨時表中,而後針對臨時表進行比較,而不是針對活動表進行比較。
而後你能夠針對小得多,鎖定時間很短的活動表運行單一的 UPDATE 語句。
進行這樣的數據修改可大大提升併發性。最後我要說,你根本不須要使用遊標,老是會有一種基於集合的解決方法。
不要嵌套視圖
視圖也許很方便,不過使用視圖時要當心。
雖然視圖有助於將龐大查詢遮掩起來、無須用戶操心,並實現數據訪問標準化,但你很容易發現本身陷入這種困境:視圖 A 調用視圖 B,視圖 B 調用視圖 C,視圖 C 又調用視圖 D,這就是所謂的嵌套視圖。
這會致使嚴重的性能問題,尤爲是這兩方面:
返回的數據頗有可能比你須要的多得多。
查詢優化器將放棄並返回一個糟糕的查詢方案。
我遇到過喜歡嵌套視圖的客戶,這個客戶有一個視圖用於幾乎全部數據,由於它有兩個重要的鏈接。
問題是,視圖返回的一個列裏面竟然有 2MB 大小的文檔,有些文檔甚至更大。
在運行的幾乎每一次查詢中,這個客戶要在網絡上爲每一行至少多推送 2MB 的數據。天然,查詢性能糟糕透頂。
沒有一個查詢實際使用該列!固然,該列被埋在七個視圖的深處,要找出來都很難。我從視圖中刪除該文檔列後,最大查詢的時間從 2.5 小時縮短至 10 分鐘。
我最後層層解開了嵌套視圖(有幾個沒必要要的鏈接和列),並寫了一個普通的查詢,結果一樣這個查詢的時間縮短至不到 1 秒。
使用表值函數
這是一直以來我最愛用的技巧之一,由於它是隻有專家才知道的那種祕訣。
在查詢的 SELECT 列表中使用標量函數時,該函數因結果集中的每一行而被調用,這會大幅下降大型查詢的性能。
然而能夠將標量函數轉換成表值函數,而後在查詢中使用 CROSS APPLY,就能夠大幅提高性能,這個奇妙的技巧能夠顯著提高性能。
使用分區避免移動大型數據
不是每一個人都能利用依賴 SQL Server Enterprise 中分區的這個技巧,可是對於能利用它的人來講,這個技巧很棒。
大多數人沒有意識到 SQL Server 中的全部表都是分區的。若是你喜歡,能夠把一個表分紅多個分區,但即便簡單的表也從建立那一刻起就分區了。
然而,它們是做爲單個分區建立的。若是你在運行 SQL Server Enterprise,已經能夠隨時享用分區表的優勢了。
這意味着你可使用 SWITCH 之類的分區功能,歸檔來自倉庫加載的大量數據。
舉個實際例子,去年我碰到過這樣一個客戶:該客戶須要將數據從當日的表複製到歸檔表中;那樣萬一加載失敗,公司能夠迅速用當日的表來恢復。
因爲各類緣由,沒法每次將表的名稱改來改去,因此公司天天在加載前將數據插入到歸檔表中,而後從活動表刪除當日的數據。
這個過程一開始很順利,但一年後,複製每一個表要花 1 個半小時,天天要複製幾個表,問題只會愈來愈糟。
解決辦法是拋棄 INSERT 和 DELETE 進程,使用 SWITCH 命令。
SWITCH 命令讓該公司得以免全部寫入,由於它將頁面分配給了歸檔表。
這只是更改了元數據,SWITCH 運行平均只要兩三秒鐘,若是當前加載失敗,你能夠經過 SWTICH 將數據切換回到原始表。
若是你非要用 ORM,請使用存儲過程
ORM 是我常常炮轟的對象之一。簡而言之,別使用 ORM(對象關係映射器)。
ORM 會生成世界上最糟糕的代碼,我遇到的幾乎每一個性能問題都是由它引發的。
相比知道本身在作什麼的人,ORM 代碼生成器不可能寫出同樣好的 SQL。可是若是你使用 ORM,那就編寫本身的存儲過程,讓 ORM 調用存儲過程,而不是寫本身的查詢。
我知道使用 ORM 的種種理由,也知道開發人員和經理都喜歡 ORM,由於它們有助於產品迅速投向市場。可是若是你看一下查詢對數據庫作了什麼,就會發現代價過高了。
存儲過程有許多優勢,首先,你在網絡上推送的數據少得多。若是有一個長查詢,那麼它可能在網絡上要往返三四趟才能讓整個查詢到達數據庫服務器。
這不包括服務器將查詢從新組合起來並運行所花的時間;另外考慮這點:查詢可能每秒運行幾回或幾百次。
使用存儲過程可大大減小傳輸的流量,由於存儲過程調用老是短得多。另外,存儲過程在 Profiler 或其餘任何工具中更容易追蹤。
存儲過程是數據庫中的實際對象,這意味着相比臨時查詢(ad-hoc query),獲取存儲過程的性能統計數字要容易得多,於是發現性能問題、查明異常狀況也要容易得多。
此外,存儲過程參數化更一致,這意味着你更可能會重用執行方案,甚至處理緩存問題,要查明臨時查詢的緩存問題很難。
有了存儲過程,處理邊界狀況(edge case),甚至增長審計或變動鎖定行爲變得容易多了。存儲過程能夠處理困擾臨時查詢的許多任務。
幾年前,我妻子理清了 Entity Framework 的一個兩頁長的查詢,該查詢花了 25 分鐘來運行。
她化繁爲簡,將這個大型查詢改寫爲 SELECT COUNT(*) fromT1,這不是開玩笑。
那些只是要點,我知道,許多 .NET 程序員認爲業務邏輯不適宜放在數據庫中,這大錯特錯。
若是將業務邏輯放在應用程序的前端,僅僅爲了比較就得將全部數據傳送一遍,那樣不會有好的性能。
我有個客戶將全部邏輯保存在數據庫的外面,在前端處理一切。該公司將成千上萬行數據發送到前端,以便可以運用業務邏輯,並顯示所需的數據。
這個過程花了 40 分鐘,我把存儲過程放在後端,讓它從前端調用;頁面在三秒鐘內加載完畢。
固然,有時邏輯適宜放在前端上,有時適宜放在數據庫中,可是 ORM 老是讓我上火。
不要對同一批次的許多表執行大型操做
這個彷佛很明顯,但實則否則。我會用另外一個鮮活的例子,由於它更能說明問題。
我有一個系統存在大量的阻塞,衆多操做處於停滯狀態。結果查明,天天運行幾回的刪除例程在刪除顯式事務中 14 個表的數據。處理一個事務中的全部 14 個表意味着,鎖定每一個表,直到全部刪除完成。
解決辦法就是,將每一個表的刪除分解成單獨的事務,以便每一個刪除事務只鎖定一個表。
這解放了其餘表,緩解了阻塞,讓其餘操做得以繼續運行。你老是應該把這樣的大事務分解成單獨的小事務,以防阻塞。
不要使用觸發器
這個與前一個大致同樣,但仍是值得一提。觸發器的問題:不管你但願觸發器執行什麼,都會在與原始操做同一個的事務中執行。
若是你寫一個觸發器,以便更新 Orders 表中的行時將數據插入到另外一個表中,會同時鎖定這兩個表,直到觸發器執行完畢。
若是你須要在更新後將數據插入到另外一個表中,要將更新和插入放入到存儲過程當中,並在單獨的事務中執行。
若是你須要回滾,就很容易回滾,沒必要同時鎖定這兩個表。與往常同樣,事務要儘可能短小,每次不要鎖定多個資源。
不要在 GUID 上聚類
這麼多年後,我難以相信咱們竟然還在爲這個問題而苦惱。但我仍然每一年遇到至少兩次聚類 GUID。
GUID(全局惟一標識符)是一個 16 字節的隨機生成的數字。相比使用一個穩定增長的值(好比 DATE 或 IDENTITY),按此列對你表中的數據進行排序致使表碎片化快得多。
幾年前我作過一項基準測試,我將一堆數據插入到一個帶聚類 GUID 的表中,將一樣的數據插入到另外一個帶 IDENTITY 列的表中。
GUID 表碎片化極其嚴重,僅僅過了 15 分鐘,性能就降低了幾千個百分點。
5 小時後,IDENTITY 表的性能才降低了幾個百分點,這不只僅適用於 GUID,它適用於任何易失性列。
若是隻需查看數據是否存在,就不要計數行
這種狀況很常見,你須要查看數據存在於表格中,根據這番檢查的結果,你要執行某個操做。
我常常見到有人執行 SELECT COUNT(*)FROMdbo.T1來檢查該數據是否存在:
SET @CT=(SELECT COUNT(*) FROM
dbo.T1);
If@CT>0
BEGIN
<Do something>
END
這徹底不必,若是你想檢查數據是否存在,只要這麼作:
If EXISTS (SELECT 1 FROM dbo.T1)
BEGIN
<Do something>
END
不要計數表中的一切,只要取回你找到的第一行。SQL Server 聰明得很,會正確使用 EXISTS,第二段代碼返回結果超快。
表越大,這方面的差距越明顯。在你的數據變得太大以前作正確的事情。調優數據庫永不嫌早。
實際上,我只是在個人其中一個生產數據庫上運行這個例子,針對一個有 2.7 億行的表。
第一次查詢用時 15 秒,包含 456197 個邏輯讀取,第二次查詢不到 1 秒就返回結果,只包含 5 個邏輯讀取。
然而若是你確實須要計數表的行數,表又很大,另外一種方法就是從系統表中提取,SELECT rows fromsysindexes 將爲你得到全部索引的行數。
又因爲聚類索引表明數據自己,因此只要添加 WHERE indid = 1,就能得到錶行,而後只需包含表名稱便可。
因此,最後的查詢是:
SELECT rows from sysindexes where object_name(id)='T1'and indexid =1
在我 2.7 億行的表中,不到 1 秒就返回結果,只有 6 個邏輯讀取,如今性能不同了。
不要進行逆向搜索
以簡單的查詢 SELECT * FROMCustomers WHERE RegionID <> 3 爲例。
你不能將索引與該查詢結合使用,由於它是逆向搜索,須要藉助表掃描來逐行比較。
若是你須要執行這樣的任務,可能發現若是重寫查詢以使用索引,性能會好得多。
該查詢很容易重寫,就像這樣:
SELECT * FROM Customers WHERE RegionID<3 UNION ALL SELECT * FROM Customers WHERE RegionID
這個查詢將使用索引,因此若是你的數據集很大,其性能會遠賽過表掃描版本。
固然,沒有什麼是那麼容易的,也許性能更糟,因此使用以前先試一下。它百分之百管用,雖然涉及太多的因素。
最後,我意識到這個查詢違反了第 4 條規則:不要查詢兩次,但這也代表沒有硬性規則。雖然咱們在這裏查詢兩次,但這麼作是爲了不開銷很大的表掃描。
你沒法一直運用全部這些技巧,但若是牢記它們,有一天你會用它們來解決一些大問題。
要記住的最重要一點是,別將我說的話當成教條。在你的實際環境中試一下,一樣的解決辦法不是在每種狀況下都管用,不過我排查糟糕的性能時一直使用這些方法,並且屢試不爽。