SQL調優常用方法
在使用DBMS時經常對系統的性能有非常高的要求:不能占用過多的系統內存和 CPU資源、要盡可能快的完成的數據庫操作、要有盡可能高的系統吞吐量。如果系統開發出來不能滿足要求的所有性能指標,則必須對系統進行調整,這個工作被稱為調優。絕定DBMS的性能的因素有兩個因素:硬件和軟件。使用頻率高的CPU、使用多處理器、加大內存容量、增加Cache、提高網絡速度等這些都是非常有效的硬件調優方式,不過對硬件進行調優對系統性能的提高是有限的,如果有非常好的硬件條件但是如果編寫的SQL質量非常差的話系統的性能並不會有明顯的改善,而如果能對SQL語句進行充分的優化的話即使硬件條件稍差的話,系統性能的變化也是非常驚人的。硬件的調優涉及到非常多的內容,不是本節所能覆蓋的,因此本節將主要講解SQL的調優。
SQL調優的基本原則
"二八原理" 是一個普遍的真理,特別是在計算機的世界中表現的更加明顯,那就是20%的代碼的資源消耗占用了80%的總資源消耗。SQL語句也是一種代碼,因此它也符合這個原理。在進行SQL調優的時候應該把主要精力放到這20%的最消耗系統資 源的SQL語句中,不要想把所有的SQL語句都調整到最優狀態。
很多DBMS都提供了非常好的工具用來分析系統中所有SQL語句資源消耗的工具,借助於這些工具發現占用系統資源排在前面的SQL語句,然後嘗試對它們進行優化,優化後再次執行分析,叠代這一過程,直到系統中沒有明顯的系統資源消耗異常的SQL語句為止。
索引
索引是數據庫調優的最根本的優化方法
根據索引的順序與數據表的物理順序是否相同,可以把索引分成兩種類型:聚簇索引,數據表的物理順序與索引順序相同;非聚簇索引,數據表的物理順序與索引順序不相同。下面,我們舉例來說明一下聚集索引和非聚集索引的區別:
字典的目錄就是一種索引,因為通過目錄我們可以很快的定位到要檢索的內容,而不用從頭到尾把字典翻一遍。漢語字典一般都至少提供兩種目錄,一種是拼音目錄,一種是偏旁部首目錄。漢語字典是按照拼音的順序排列的,因此拼音目錄就是聚集索引,而偏旁部首目錄則是非聚集索引。應該在表中經常搜索的列或者按照順序訪問的列上創建聚簇索引。當創建聚簇索引時要需要每一個表只能有一個聚簇索引,因為表中數據的物理順序只能有一個,而非聚集索引則可以創建多個。
由於索引需要占據一定的存儲空間,而且索引也會降低數據插入、更新和刪除的速度,所以應該只創建必要的索引,一般是在檢索的時候用的字段中創建索引。
索引還會造成存儲碎片的問題。當刪除一條記錄時將會導致對應的索引中的該記錄的對應項為空,由於索引是采用B樹結構存儲的,所以對應的索引項並不會被刪除,經過一段時間的增刪改操作後,數據庫中就會出現大量的存儲碎片,這和磁盤碎片、內 存碎片產生原理是類似的,這些存儲碎片不僅占用了存儲空間,而且降低了數據庫運行的速度。如果發現索引中存在過多的存儲碎片的話就要進行“碎片整理”了,最方便的“碎片整理”手段就是重建索引,重建索引會將先前創建的索引刪除然後重新創建索引,主流數據庫管理系統都提供了重建索引的功能,比如REINDEX、REBUILD等,如果 使用的數據庫管理系統沒有提供重建索引的功能,可以首先用DROP INDEX語句刪除索引,然後用ALTER TABLE語句重新創建索引。
表掃描和索引查找
一般地,系統訪問數據庫中的數據,可以使用兩種方法:全表掃描和索引查找。
全表掃描,就是指系統必須在數據表中逐條檢索表中的每條記錄,以檢查該記錄是否匹配檢索條件。全表掃描有可能會造成巨大的性能損失,當然也有可能不會影響性能,這取決於表中的數據量,如果表中有上千萬條甚至上億條記錄的話,全表掃描的速度會 非常慢,而如果表中只有幾條、幾十條記錄的話表掃描的性能消耗就可以忽略不計了。當表中數據量比較小的時候,使用全表掃描非常有用。但是隨著表中數據量的增加,全表掃描會導致系統性能嚴重下降。
如果表中有索引並且待匹配條件符合索引的要求的話,DBMS就不會執行全表掃描,而是直接到索引中查找,這將大大加快檢索的速度。
DBMS中都有查詢優化器,它會根據分布的統計信息生成該查詢語句的優化執行規劃,以提高訪問數據的效率為目標,確定是使用全表掃描還是使用索引查找。註意並不是表中存在索引在進行檢索的時候就會使用索引查找,如果使用不當檢索的過程仍然會是采用全表掃描,這樣索引就起不到效果了。
優化手法
下面將會列出了一些常用的優化手法,註意這些優化手法只是一些常規條件下的優化手法,具體的優化效果是與使用的DBMS以及數據的特點密切相關的,需要根據具體情況來使用不同的優化手法,如果使用不當的話有可能會適得其反。
1、創建必要的索引
在經常需要進行檢索的字段上創建索引,比如經常要按照圖書名稱進行檢索,那麽就應該在圖書名稱字段上創建索引,如果經常要按照員工部門和員工崗位級別進行檢索,那麽就應該在員工部門和員工崗位級別這兩個字段上創建索引。創建索引給檢索帶來的性能提升往往是巨大的,因此在發現檢索速度過慢的時候應該首先想到的就是創建索引。
2、使用預編譯查詢
程序中通常是根據用戶的輸入來動態執行SQL語句,這時應該盡量使用參數化SQL,這樣不僅可以避免SQL註入漏洞攻擊,最重要數據庫會對這些參數化SQL執行預編譯,這樣第一次執行的時候DBMS會為這個SQL語句進行查詢優化並且執行預編譯,這樣以後再執行這個SQL的時候就直接使用預編譯的結果,這樣可以大大提高執行的速度。
3、調整WHERE子句中的連接順序
DBMS一般采用自下而上的順序解析WHERE子句,根據這個原理,表連接最好寫在其他WHERE條件之前,那些可以過濾掉最大數量記錄的條件寫在WHERE子句的末尾。
比如下面的SQL語句性能較差:
1 SELECT * 2 FROM T_Person 3 WHERE FSalary > 50000 4 AND FPosition= ‘MANAGER‘ 5 AND 25 < (SELECT COUNT(*) 6 FROM T_Manager 7 WHERE FManagerId=2);
我們將子查詢的條件放到最前面,下面的SQL語句性能比較好:
1 SELECT * 2 FROM T_Person 3 WHERE 4 25 < (SELECT COUNT(*) 5 FROM T_Manager 6 WHERE FManagerId = 2) 7 AND FSalary > 50000 8 AND FPosition= ‘MANAGER‘;
4、SELECT語句中避免使用‘*‘
SELECT *比較簡單,但是除非確實需要檢索所有的列,否則將會檢索出不需要的列,這回增加網絡的負載和服務器的資源消耗;即使確實需要檢索所有列,也不要使用 SELECT *,因為這是一個非常低效的方法,DBMS在解析的過程中,會將*依次轉換成所有的列名,這意味著將耗費更多的時間。
5、盡量將多條SQL語句壓縮到一句SQL中
每次執行SQL的時候都要建立網絡連接、進行權限校驗、進行SQL語句的查詢優化、發送執行結果,這個過程是非常耗時的,因此應該盡量避免過多的執行SQL語句, 能夠壓縮到一句SQL執行的語句就不要用多條來執行。
6、用Where子句替換HAVING子句
避免使用HAVING子句,因為HAVING 只會在檢索出所有記錄之後才對結果集進行過濾。如果能通過WHERE子句限制記錄的數目,那就能減少這方面的開銷。 HAVING 中的條件一般用於聚合函數的過濾,除此而外,應該將條件寫在WHERE子句中。
7、使用表的別名
當在SQL語句中連接多個表時,請使用表的別名並把別名前綴於每個列名上。這樣就可以減少解析的時間並減少那些由列名歧義引起的語法錯誤。
8、用EXISTS替代IN
在查詢中,為了滿足一個條件,往往需要對另一個表進行聯接,在這種情況下,使用EXISTS而不是IN通常將提高查詢的效率,因為IN 子句將執行一個子查詢內部的排序和合並。下面的語句2就比語句1效率更加高。
1 --語句1: 2 SELECT * FROM T_Employee 3 WHERE FNumber> 0 4 AND FDEPTNO IN (SELECT FNumber 5 FROM T_Department 6 WHERE FMangerName = ‘Tome‘) 7 8 --語句2: 9 SELECT * FROM T_Employee EMP 10 WHERE EMP.FNumber > 0 11 AND EXISTS (SELECT 1 12 FROM T_Department DEP 13 WHERE DEP.FDEPTNO = EMP.FNumber 14 AND DEP.FMangerName = ‘Tome‘)
9 、用表連接替換EXISTS
通常來說,表連接的方式比EXISTS更有效率,因此如果可能的話盡量使用表連接替換EXISTS。下面的語句2就比語句1效率更加高。
1 --語句1: 2 SELECT * FROM T_Employee EMP 3 WHERE EMP.FNumber > 0 4 AND EXISTS (SELECT 1 5 FROM T_Department DEP 6 WHERE DEP.FDeptno = EMP.FNumber 7 AND DEP.FKind = ‘A‘) 8 --語句2: 9 SELECT FName FROM T_Department DEP,T_Employee EMP 10 WHERE DEP.FDeptno = EMP.FNumber 11 AND DEP.FKind = ‘A‘;
10、避免在索引列上使用計算
在WHERE子句中,如果索引列是計算或者函數的一部分,DBMS的優化器將不會使用索引而使用全表掃描。
例如下面的SQL語句用於檢索月薪的12倍大於兩萬五千元的員工:
1 SELECT * 2 FROM T_Employee 3 WHERE FSalary * 12 >25000;
由於在大於號左邊的是FSalary與12的成績表達式,這樣DBMS的優化器將不會使用字段FSalary的索引,因為DBMS必須對T_Employee表進行全表掃描,從而計算 FSalary * 12 的值,然後與25000進行比較。將上面的SQL語句修改為下面的等價寫法後DBMS將會使用索引查找,從而大大提高了效率:
1 SELECT * 2 FROM T_Employee 3 WHERE FSalary >25000/12;
同樣的,不能在索引列上使用函數,因為函數也是一種計算,會造成全表掃描。下面的語句2就比語句1效率更加高。
1 --語句1: 2 SELECT * 3 FROM T_Example 4 WHERE ABS(FAmount)=300 5 --語句2: 6 SELECT * 7 FROM T_Example 8 WHERE FAmount=300 OR FAmount=-300
11、用UNION ALL 替換UNION
當SQL語句需要UNION兩個查詢結果集合時,即使檢索結果中不會有重復的記錄,如果使用UNION這兩個結果集同樣會嘗試進行合並,然後在輸出最終結果前進行排序。
因此,如果檢索結果中不會有重復的記錄的話,應該用UNION ALL替代UNION,這樣效率就會因此得到提高。下面的語句2就比語句1效率更加高。
1 --語句1: 2 SELECT ACCT_NUM, BALANCE_AMT 3 FROM DEBIT_TRANSACTIONS1 4 WHERE TRAN_DATE = ‘20010101‘ 5 UNION 6 SELECT ACCT_NUM, BALANCE_AMT 7 FROM DEBIT_TRANSACTIONS2 8 WHERE TRAN_DATE =‘20010102‘; 9 --語句2: 10 SELECT ACCT_NUM, BALANCE_AMT 11 FROM DEBIT_TRANSACTIONS1 12 WHERE TRAN_DATE =‘20010101‘ 13 UNION ALL 14 SELECT ACCT_NUM, BALANCE_AMT 15 FROM DEBIT_TRANSACTIONS2 16 WHERE TRAN_DATE = ‘20010102‘;
12、避免隱式類型轉換造成的全表掃描
T_Person表的字符串類型字段FLevel為人員的級別,在FAge字段上建有索引。我們執行下面的SQL語句用於檢索所有級別等於10的員工:
1 SELECT FId,FAge,FName 2 FROM T_Person 3 WHERE FAge=10
在這個SQL語句中,將字符串類型字段FLevel與數值10進行比較,由於在大部分數據庫中隱式轉換類型中數值類型的優先級高於字符串類型,因此DBMS會對FAge字段進行隱式類型轉換,相當於執行了下面的SQL語句:
1 SELECT FId,FAge,FName 2 FROM T_Person 3 WHERE TO_INT(FAge)=10;
由於在索引字段上進行了計算,所以造成了索引失效而使用全表掃描。因此應將 SQL語句做如下修改:
1 SELECT FId,FAge,FName 2 FROM T_Person 3 WHERE FAge=‘10‘;
13、防止檢索範圍過寬
如果DBMS優化器認為檢索範圍過寬,那麽它將放棄索引查找而使用全表掃描。下面是幾種可能造成檢索範圍過寬的情況:使用IS NOT NULL或者不等於判斷,可能造成優化器假設匹配的記錄數太多。 使用LIKE運算符的時候,"a%"將會使用索引,而"a%c"和"%c"則會使用全表掃描,因此"a%c"和"%c"不能被有效的評估匹配的數量。
14、SQL語句盡量用大寫
對Oracle來說,Oracle總是先解析sql語句,把小寫的字母轉換成大寫的再執行。
15、利用with字句重用查詢
如獲得工資大於平均工資的員工和工資信息:
1 select * from ( 2 select employee_id,avg(salary) avg_salary 3 from salary 4 group by employee_id 5 )t 6 where t.avg_salary >( 7 select avg(avg_salary) from ( 8 select employee_id,avg(salary) avg_salary 9 from salary 10 group by employee_id 11 ));
可以看到子查詢:
1 select employee_id,avg(salary) avg_salary 2 from salary 3 group by employee_id;
被重復執行,通過with子句可以將該子查詢獨立出來,並重用其查詢結果,如下所示:
1 with employee_avg_salary as ( 2 select employee_id,avg(salary) avg_salary 3 from salary 4 group by employee_id) 5 select * from employee_avg_salary t 6 where t.avg_salary > ( 7 select avg(avg_salary) 8 from employee_avg_salary);
SQL調優常用方法