導圖
下圖是我結合自己的經驗以及搜集整理的數據庫優化相關內容的思維導圖,如果圖片不清楚,可以在瀏覽器中右鍵,在新窗口中查看(Chrome)或者查看圖像(FireFox)。
常用關鍵字優化
在編寫T-SQL的時候,會使用很多功能類似的關鍵字,比如COUNT
和EXISTS
、IN
和BETWEEN AND
等,我們往往會根據需求直奔主題地來編寫查詢腳本,完成需求要求實現的業務邏輯即可,但是,我們編寫的腳本中卻存在着很多的可優化的空間。
EXISTS代替COUNT或IN
不要在子查詢中使用COUNT()
執行存在性檢查,不要使用類似於如下這樣的語句:
SELECT COLUMN_LIST FROM TABLENAME WHERE 0 < (SELECT COUNT(*) FROM TABLE2 WHERE ..)
而應該采用這樣的語句代替:
SELECT COLUMN_LIST FROM TABLENAME WHERE EXISTS(SELECT
COLUMN_LIST
FROM TABLE2 WHERE ...)
當你使用COUNT()
時,SQL SERVER不知道你要做的是存在性檢查,它會計算所有匹配的值,要么會執行全表掃描,要么會掃描最小的非聚集索引。當你使用EXISTS
時,SQL SERVER知道你要執行存在性檢查,當它發現第一個匹配的值時,就會返回TRUE
,並停止查詢。此外,很多時候用EXISTS
代替IN
是一個好的選擇,例如:
SELECT NUM FROM A WHERE NUM IN (SELECT NUM FROM B)
可以使用SELECT NUM FROM A WHERE EXISTS (SELECT 1 FROM B WHERE NUM=A.NUM)
進行替代。
盡量不用 SELECT *
絕大多數情況下,不要用 *
來代替查詢返回的字段列表,用 *
的好處是代碼量少,就算是表結構或視圖的列發生變化,編寫的查詢SQL語句也不用變,都返回所有的字段。但數據庫服務器在解析時,如果碰到 *
,則會先分析表的結構,然后把表的所有字段名再羅列出來,這就增加了分析的時間。另一個問題是,SELECT *
可能包含了不需要的列,增加了網絡流量。如果在視圖創建中使用了SELECT *
,在后期如果有對視圖基表的表結構進行了更改,當查詢視圖時,可能會生成意外結果,除非重建視圖或利用SP_REFRESHVIEW
更新視圖的元數據。
慎用 SELECT DISTINCT
DISTINCT
子句僅在特定功能的時候使用,即從記錄集中排除重復記錄的時候。這是因為DISTINCT
子句先獲取結果,進行排序集然后再去重,這樣增加了SQL SERVER資源的消耗。在實際的業務中,如果你已經預先知道SELECT
語句將從不返回重復記錄,那么使用DISTINCT
語句是對SQL SERVER資源不必要的浪費。當然,如果是符合特定的業務場景,是可以酌情使用的。
正確使用 UNION 和 UNION ALL 以及 WITH TEMPTABLENAME AS
許多人沒完全理解UNION
和UNION ALL
是怎樣工作的,因此,結果浪費了大量不必要的SQL Server資源。當使用UNION
時,它相當於在結果集上執行SELECT DISTINCT
。換句話說,UNION
將聯合兩個相類似的記錄集,然后搜索重復的記錄並排除。如果這是你的目的,那么使用UNION
是正確的。但如果你使用UNION
聯合的兩個記錄集本身就沒有重復記錄,那么使用UNION
會浪費資源,因為它要尋找重復記錄,即使你確定它們不存在。總而言之,聯合無重復的結果集采用UNION ALL
,聯合存在重復記錄的采用UNION
。對於WITH TEMP TABLENAME AS
,其實並沒有建立臨時表,只是子查詢部分(SUBQUERY FACTORING
),定義一個SQL片斷,該SQL片斷會被整個SQL語句所用到。有的時候,是為了讓SQL語句的可讀性更高些,也有可能是在UNION ALL
的不同部分,作為提供數據的部分。特別對於UNION ALL
比較有用。因為UNION ALL
的每個部分可能相同,但是如果每個部分都去執行一遍的話成本太高,所以可以使用WITH AS
短語,則只要執行一遍即可。
使用 SET NOCOUNT ON 選項
缺省地,每次執行SQL語句時,一個消息會從服務端發給客戶端以顯示SQL語句影響的行數。這些信息對客戶端來說很少有用,甚至有些客戶端會把這些信息當成錯誤信息處理。通過關閉這個缺省值,你能減少在服務端和客戶端的網絡流量,幫助全面提升服務器和應用程序的性能。為了關閉存儲過程級的這個特點,在每個存儲過程的開頭包含SET NOCOUNT ON
語句。同樣,為減少在服務端和客戶端的網絡流量,生產環境中應該去掉存儲過程中那些在調試過程中使用的SELECT
和PRINT
語句。
指定字段別名
當在SQL語句中連接多個表時,可以將表名或別名加到每個COLUMN
前面,這樣可以有效地減少解析的時間並減少那些由COLUMN
歧義引起的語法錯誤。例如:
SELECT COLUMN_A,COLUMN_B FROM TABLE1 T1 INNER JOIN TABLE2 T2 ON T1.ID = T2.UID
,其中COLUMN_A
是TABLE1
的數據列,COLUMN_B
是TABLE2
的數據列,這並不妨礙查詢的進行,但是改成下列語句是不是更好呢?SELECT T1.COLUMN_A,T2.COLUMN_B FROM TABLE T1 INNER JOIN TABLE T2 ON T1.ID = T2.UID
建立索引
關於索引,下圖展示出了索引的直觀結構:
索引按照索引的類型可以分為聚集索引和非聚集索引,一張數據表只能存在一個聚集索引,但可以建立若干非聚集索引,聚集索引通常是建立在主鍵上,當然主鍵上不一定需要強制建立聚集索引。關於索引的實現原理可以參考這篇數據庫索引的實現原理篇,以及建立索引的一般依據。對於聚集索引而言,表中存儲的數據按照索引的順序存儲,即邏輯順序決定了表中相應行的物理順序。對於非聚集索引,一般考慮在下列情形下使用非聚集索引:使用JOIN
的條件字段、使用GROUP BY
的字段、完全匹配的WHERE
條件字段、外鍵字段等等。索引是有900字節大小限制的,因此不要在超長字段上建立索引,索引字段的總字節數不要超過900字節,否則插入的數據達到900字節時會報錯。另外,並不是所有索引對查詢都有效,SQL是根據表中數據來進行查詢優化的,當索引列有大量數據重復時,SQL查詢可能不會去利用索引,如一表中有字段Gender
,Male
、Female
幾乎各一半,那么即使在Gender
上建了索引也對查詢效率起不了作用。索引並不是越多越好,索引固然可以提高查詢效率,但同時也降低了插入數據及更新數據的效率,因為插入或更新數據時有可能會重建索引,所以在建立索引時需要慎重考慮,視具體情況而定。總之,要根據實際的業務情景合理地為數據表建立索引。
存儲過程
存儲過程是數據庫中的一個重要對象。存儲過程實際上是對一些SQL腳本的有邏輯地組合而形成的,是一組為了完成特定功能的SQL 語句集。存儲在數據庫中,經過第一次編譯后再次調用不需要再次編譯,所以使用存儲過程可提高數據庫執行速度,用戶通過指定存儲過程的名字並給出參數(如果該存儲過程帶有參數)來執行它。存儲過程執行計划能夠重用,駐留在SQL SERVER內存的緩存里,減少服務器開銷。當業務相對復雜的時候,可以將該業務封裝成一個存儲過程存儲在數據庫服務器,可以大大降低網絡流量的傳輸,提高性能。例如,通過網絡發送一個存儲過程調用,而不是發送500行的T-SQL,這樣速度會更快,資源占用更少,有效地避免了每次執行SQL時,都會執行解析SQL語句、估算索引的利用率、綁定變量、讀取數據塊等工作。存儲過程可有效地降低數據庫連接次數,當對數據庫進行復雜操作時(如對多個表進行 Update,Insert,Query,Delete操作時),可將該復雜操作用存儲過程封裝起來與數據庫提供的事務處理結合一起使用。這些操作,如果用程序來完成,就變成了一條條的 SQL 語句,可能要多次連接數據庫。而采用成存儲過程,只需要連接一次數據庫就可以了。
事務和鎖
事務是數據庫應用中重要的工具,它具有原子性、一致性、隔離性、持久性這四個屬性,很多操作我們都需要利用事務來保證數據的正確性。在使用事務中我們需要做到盡量避免死鎖、盡量減少阻塞。開發過程中,可以通過以下幾種方式來避免問題的產生:事務操作過程要盡量小,能拆分的事務要拆分開來,在更細的粒度上應用事務;事務操作過程中不應該有交互,因為交互等待的時候,事務並未結束,可能鎖定了很多資源; 事務操作過程要按同一順序訪問對象,比如在某一事務中要按順序更新A、B兩表,那么在其他的事務中就不要按B、A的順序去更新這兩個表。我在實際工作中就遇到過這種問題(如下圖所示),由於在事務中需要同時更新主表和子表,子表的數據更新后主表匯總數據,但是更新兩個表的時候,順序不一致,由於事務的原子性,需要在同一事務中完成兩表的更新操作,這就形成了Transaction A需要的資源(子表B)被Transaction B占據着,Transaction B需要的資源(主表A)被Transaction A占據着,導致表被鎖住,造成了死鎖,后來對表的更新順序進行了調整,解決了這個問題。盡量不要指定鎖類型和索引,SQL SERVER允許我們自己指定語句使用的鎖類型和索引,但是一般情況下,SQL SERVER優化器選擇的鎖類型和索引是在當前數據量和查詢條件下是最優的,我們指定的可能只是在目前情況下更優,但是數據量和數據分布在將來是會變化的。
SARG WHERE條件
下面是百度百科對SARG的解釋:
SARG (Searchable Arguments)操作,用於限制搜索的一個操作,它通常是指一個特定的匹配,一個值的范圍內的匹配或者兩個以上條件的AND連接。
SARG來源於Search Argument
(搜索參數)的首字母拼成的SARG,它是指WHERE
子句里,列和常量的比較。如果WHERE
子句是SARGABLE(可SARG的),這意味着它能利用索引加速查詢的完成。如果WHERE
子句不是可SARG的,這意味着WHERE
子句不能利用索引(或至少部分不能利用),執行的是全表或索引掃描,這會引起查詢的性能下降。
在WHERE
子句中,可以SARG的搜索條件包含以下如:包含以下操作符=
、>
、<
、>=
、<=
、BETWEEN
及部分情況下的LIKE
(通配符在查詢關鍵字之后,如LIKE 'A%'
)
在WHERE
子句中,不可SARG的搜索條件如:IS NULL
, <>
, !=
, !>
, !<
, NOT
, NOT EXISTS
, NOT IN
, NOT LIKE
和LIKE '%500'
,通常(但不總是)會阻止查詢優化器使用索引執行搜索。另外在列上使用包括函數的表達式、兩邊都使用相同列的表達式、或和一個列(不是常量)比較的表達式,都是不可SARG的。並不是每一個不可SARG的WHERE
子句都注定要全表掃描。如果WHERE
子句包括兩個可SARG和一個不可SARG的子句,那么至少可SARG的子句能使用索引(如果存在的話)幫助快速訪問數據。
大多數情況下,如果表上有包括查詢里所有SELECT
、JOIN
、WHERE
子句用到的列的覆蓋索引,那么覆蓋索引能夠代替全表掃描去返回查詢的數據,即使它有不可SARG的WHERE
子句。某些情況下,可以把不可SARG的WHERE
子句重寫成可SARG的子句。例如:
WHERE SUBSTRING(FirstName,1,1) = 'M'
可以寫成:WHERE
這兩個FirstName
LIKE 'M%'WHERE
子句有相同的結果,但第一個是不可SARG的(因為使用了函數)將運行得慢些,而第二個是可SARG的,將運行得快些。如果你不知道特定的WHERE
子句是不是可SARG的,可以在查詢分析器里檢查查詢執行計划。這樣做,你能很快地知道查詢是使用了索引還是全表掃描來返回的數據。仔細分析,許多不可SARG的查詢能寫成可SARG的查詢,從而實現性能的優化和提升。
查詢條件中使用了不等於操作符(<>
, !=
)的SELECT
語句執行效率較低,因為不等於操作符會限制索引,引起全表掃描,即使被比較的字段上有索引,這時可以通過把不等於操作符改成OR,可以使用索引,從而避免全表掃描。例如, 可以把SELECT TOP 100 AGE FROM TABLE WHERE AGE <> 25
改寫為SELECT TOP 1000 AGE FROM TABLE WHERE AGE > 25 OR AGE < 25
應當盡量避免在WHERE
子句中對字段進行函數操作,這將導致引擎放棄使用索引而進行全表掃描。例如:SELECT ID FROM TABLE WHERE SUBSTRING(NAME, 1, 3) = 'ABC'
臨時表和表變量
在復雜系統中,如果業務是以存儲過程的方式組織的,那么中間必然會產生一些臨時查詢出的數據,此時臨時表和表變量很難避免,關於臨時表和表變量的用法,需要注意的是,如果語句很復雜,連接太多,可以考慮用臨時表和表變量分步完成,將需要的結果集存儲在臨時表或表變量中,便於復用;同樣地,如果需要多次用到一個大表的同一部分數據,考慮用臨時表和表變量暫存這部分數據; 如果需要綜合多個表的數據,形成一個結果集,可以考慮用臨時表和表變量分步匯總出這多個表的數據;其他情況下,應該控制臨時表和表變量的使用。另外,在臨時表完成自身功能后,要顯式地刪除臨時表,先TRUNCATE TABLE
,然后DROP TABLE
,以避免資源的占用。關於臨時表和表變量的選擇,很多說法是表變量儲存在內存,速度快,應該首選表變量,但是在實際使用中發現,這個選擇主要是考慮需要放在臨時表中的數據量,在數據量較多的情況下,臨時表的速度反而更快。關於臨時表的創建,使用SELECT INTO
和CREATE TABLE
+ INSERT INTO
的選擇,我們做過測試,一般情況下,SELECT INTO
會比CREATE TABLE
+ INSERT INTO
的方法快很多,但是SELECT INTO
會鎖定TEMPDB
的系統表SYSOBJECTS
、SYSINDEXES
、SYSCOLUMNS
,在多用戶並發環境下,容易阻塞其他進程,所以建議在並發系統中,盡量使用CREATE TABLE
+ INSERT INTO
,而大數據量的單個語句使用中,使用SELECT INTO
。
臨時表和表變量是有區別的,表變量是存儲在內存中的,當用戶在訪問表變量的時候,SQL SERVER是不產生日志的,而在臨時表中是產生日志的;在表變量中,是不允許有非聚集索引的;表變量是不允許有DEFAULT
默認值,也不允許有約束;臨時表上的統計信息是健全而可靠的,但是表變量上的統計信息是不可靠的;臨時表中是有鎖的機制,而表變量中就沒有鎖的機制。了解二者的區別,可以針對特定場景選擇最優方案,使用表變量主要需要考慮的就是應用程序對內存的壓力,如果代碼的運行實例很多,就要特別注意內存變量對內存的消耗。對於較小的數據或者是通過計算出來的數據推薦使用表變量。如果數據的結果比較大,在代碼中用於臨時計算,在選取的時候沒有什么分組或聚合,也可以考慮使用表變量。一般對於大的數據結果集,或者因為統計出來的數據為了便於更好的優化,我們就推薦使用臨時表,同時還可以創建索引,由於臨時表是存放在Tempdb中,一般默認分配的空間很少,需要對Tempdb進行調優,增大其存儲的空間。
結語
本篇博文對常用的數據庫優化方法進行了簡單的梳理和總結,如果文中有不正確或者不恰當的地方,歡迎提出質疑,共同討論共同進步,如果您有什么更好的優化方案,可以在評論區討論,我會把這些優化方案補充進來。如果您覺得有幫助,點個贊喲~
作者:悠揚的牧笛
博客地址:http://www.cnblogs.com/xhb-bky-blog/p/9051380.html
聲明:本博客原創文字只代表本人工作中在某一時間內總結的觀點或結論,與本人所在單位沒有直接利益關系。非商業,未授權貼子請以現狀保留,轉載時必須保留此段聲明,且在文章頁面明顯位置給出原文連接。如果您覺得文章對您有幫助,可以【打賞】博主或點擊文章右下角【推薦】一下。您的鼓勵是博主堅持原創和持續寫作的最大動力!