# 問題的提出
在應用系統開發初期,由於開發數據庫數據比較少,對於查詢SQL語句,復雜視圖的的編寫等體會不出SQL語句各種寫法的性能優劣,但是如果將應用 系統提交實際應用后,隨着數據庫中數據的增加,系統的響應速度就成為目前系統需要解決的最主要的問題之一。系統優化中一個很重要的方面就是SQL語句的優化。對於海量數據,劣質SQL語句和優質SQL語句之間的速度差別可以達到上百倍,可見對於一個系統不是簡單地能實現其功能就可,而是要寫出高質量的 SQL語句,提高系統的可用性。
在多數情況下,Oracle使用索引來更快地遍歷表,優化器主要根據定義的索引來提高性能。但是,如果在SQL語句的where子句中寫的 SQL代碼不合理,就會造成優化器刪去索引而使用全表掃描,一般就這種SQL語句就是所謂的劣質SQL語句。在編寫SQL語句時我們應清楚優化器根據何種原則來刪除索引,這有助於寫出高性能的SQL語句。
隨着軟件技術的不斷發展,系統性能越來越重要。系統性能主要用:系統響應時間和並發性來衡量。
造成SQL語句性能不佳大致有兩個原因:
l 開發人員只關注查詢結果的正確性,忽視查詢語句的效率。
l 開發人員只關注SQL語句本身的效率,對SQL語句執行原理、影響SQL執行效率的主要因素不清楚。
* 前者可以通過深入學習SQL語法及各種SQL調優技巧進行解決。
SQL調優是一個系統工程,熟悉SQL語法、掌握各種內嵌函數、分析函數的用法只是編寫高效SQL的必要條件。
* 后者從分析SQL語句執行原理入手,指出SQL調優應在優化SQL解析和優化CBO上。
# SQL語句調優
80%的數據庫性能問題都是由於糟糕的SQL語句造成的。
## SQL語句優化的過程:
l 定位有問題的語句
l 檢查執行計划
l 檢查執行過程中優化器的統計信息
l 分析相關表的記錄數、索引情況
l 改寫SQL語句、使用HINT、調整索引、表分析
l 有些SQL語句不具備優化的可能,需要優化處理方式
l 達到最佳執行計划
## 什么是好的SQL語句?
l 盡量簡單,模塊化
l 易讀、易維護
l 節省資源、內存、CPU
l 掃描的數據塊要少
l 少排序
l 不造成死鎖
## 首先要搞明白什么叫執行計划?
執行計划是數據庫根據SQL語句和相關表的統計信息作出的一個查詢方案,這個方案是由查詢優化器自動分析產生的,比如一條SQL語句如果用來從一個 10萬條記錄的表中查1條記錄,那查詢優化器會選擇“索引查找”方式,如果該表進行了歸檔,當前只剩下5000條記錄了,那查詢優化器就會改變方案,采用 “全表掃描”方式。
可見,執行計划並不是固定的,它是“個性化的”。產生一個正確的“執行計划”有兩點很重要:
(1) SQL語句是否清晰地告訴查詢優化器它想干什么?
(2)查詢優化器得到的數據庫統計信息是否是最新的、正確的?
# 什么是索引?
SQL索引有兩種,聚集索引和非聚集索引,索引主要目的是提高了SQL Server系統的性能,加快數據的查詢速度與減少系統的響應時間
下面舉兩個簡單的例子:
圖書館的例子:一個圖書館那么多書,怎么管理呢?建立一個字母開頭的目錄,例如:a開頭的書,在第一排,b開頭的在第二排,這樣在找什么書就好說了,這個就是一個聚集索引,可是很多人借書找某某作者的,不知道書名怎么辦?圖書管理員在寫一個目錄,某某作者的書分別在第幾排,第幾排,這就是一個非聚集索引
字典的例子:字典前面的目錄,可以按照拼音和部首去查詢,我們想查詢一個字,只需要根據拼音或者部首去查詢,就可以快速的定位到這個漢字了,這個就是索引的好處,拼音查詢法就是聚集索引,部首查詢就是一個非聚集索引.
看了上面的例子,下面的一句話大家就很容易理解了:聚集索引存儲記錄是物理上連續存在,而非聚集索引是邏輯上的連續,物理存儲並不連續。就像字段,聚集索引是連續的,a后面肯定是b,非聚集索引就不連續了,就像圖書館的某個作者的書,有可能在第1個貨架上和第10個貨架上。還有一個小知識點就是:聚集索引一個表只能有一個,而非聚集索引一個表可以存在多個。
索引相關介紹:
## 索引的存儲機制
首先,無索引的表,查詢時,是按照順序存續的方法掃描每個記錄來查找符合條件的記錄,這樣效率十分低下,舉個例子,如果我們將字典的漢字隨即打亂,沒有前面的按照拼音或者部首查詢,那么我們想找一個字,按照順序的方式去一頁頁的找,這樣效率有多底,大家可以想象。
聚集索引和非聚集索引的根本區別是表記錄的排列順序和與索引的排列順序是否一致,其實理解起來非常簡單,還是舉字典的例子:如果按照拼音查詢,那么都是從a-z的,是具有連續性的,a后面就是b,b后面就是c,聚集索引就是這樣的,他是和表的物理排列順序是一樣的,例如有id為聚集索引,那么1后面肯定是2,2后面肯定是3,所以說這樣的搜索順序的就是聚集索引。非聚集索引就和按照部首查詢是一樣是,可能按照偏房查詢的時候,根據偏旁‘弓’字旁,索引出兩個漢字,張和弘,但是這兩個其實一個在100頁,一個在1000頁,(這里只是舉個例子),他們的索引順序和數據庫表的排列順序是不一樣的,這個樣的就是非聚集索引。
原理明白了,那他們是怎么存儲的呢?在這里簡單的說一下,聚集索引就是在數據庫被開辟一個物理空間存放他的排列的值,例如1-100,所以當插入數據時,他會重新排列整個整個物理空間,而非聚集索引其實可以看作是一個含有聚集索引的表,他只僅包含原表中非聚集索引的列和指向實際物理表的指針。他只記錄一個指針,其實就有點和堆棧差不多的感覺了。
## 什么情況下設置索引
動作描述 |
使用聚集索引 |
使用非聚集索引 |
外鍵列 |
應 |
應 |
主鍵列 |
應 |
應 |
列經常被分組排序(order by) |
應 |
應 |
返回某范圍內的數據 |
應 |
不應 |
小數目的不同值 |
應 |
不應 |
大數目的不同值 |
不應 |
應 |
頻繁更新的列 |
不應 |
應 |
頻繁修改索引列 |
不應 |
應 |
一個或極少不同值 |
不應 |
不應 |
建立索引的原則:
1) 定義主鍵的數據列一定要建立索引。
2) 定義有外鍵的數據列一定要建立索引。
3) 對於經常查詢的數據列最好建立索引。
4) 對於需要在指定范圍內的快速或頻繁查詢的數據列;
5) 經常用在WHERE子句中的數據列。
6) 經常出現在關鍵字order by、group by、distinct后面的字段,建立索引。如果建立的是復合索引,索引的字段順序要和這些關鍵字后面的字段順序一致,否則索引不會被使用。
7) 對於那些查詢中很少涉及的列,重復值比較多的列不要建立索引。
8) 對於定義為text、image和bit的數據類型的列不要建立索引。
9) 對於經常存取的列避免建立索引
9) 限制表上的索引數目。對一個存在大量更新操作的表,所建索引的數目一般不要超過3個,最多不要超過5個。索引雖說提高了訪問速度,但太多索引會影響數據的更新操作。
10) 對復合索引,按照字段在查詢條件中出現的頻度建立索引。在復合索引中,記錄首先按照第一個字段排序。對於在第一個字段上取值相同的記錄,系統再按照第二個字段的取值排序,以此類推。因此只有復合索引的第一個字段出現在查詢條件中,該索引才可能被使用,因此將應用頻度高的字段,放置在復合索引的前面,會使系統最大可能地使用此索引,發揮索引的作用。
# SQL語句編寫注意問題
數據庫系統按着從左到右的順序來解析一個系列由 AND 連接的表達式,但是 Oracle 卻是個例外,它是從右向左地解析表達式。可以利用數據庫系統的這一特性,來將概率小的表達示放在前面,或者是如果兩個表達式可能性相同,那么可將相對不復雜的表達式放在前面。這樣做的話,如果第一個表達式為假的話,那么數據庫系統就不必再費力去解析第二個表達式了。
1.對查詢進行優化,要盡量避免全表掃描,首先應考慮在 where 及 order by 涉及的列上建立索引。
2.應盡量避免在 where 子句中對字段進行 null 值判斷,否則將導致引擎放棄使用索引而進行全表掃描,如:
select id from t where num is null
最好不要給數據庫留NULL,盡可能的使用 NOT NULL填充數據庫.
備注、描述、評論之類的可以設置為 NULL,其他的,最好不要使用NULL。
不要以為 NULL 不需要空間,比如:char(100) 型,在字段建立時,空間就固定了, 不管是否插入值(NULL也包含在內),都是占用 100個字符的空間的,如果是varchar這樣的變長字段, null 不占用空間。
可以在num上設置默認值0,確保表中num列沒有null值,然后這樣查詢:
select id from t where num = 0
3.應盡量避免在 where 子句中使用 != 或 <> 操作符,否則將引擎放棄使用索引而進行全表掃描。
4.應盡量避免在 where 子句中使用 or 來連接條件,如果一個字段有索引,一個字段沒有索引,將導致引擎放棄使用索引而進行全表掃描,如:
select id from t where num=10 or Name = 'admin'
可以這樣查詢:
select id from t where num = 10 union all select id from t where Name = 'admin'
5.in 和 not in 也要慎用,否則會導致全表掃描,如:
select id from t where num in(1,2,3)
對於連續的數值,能用 between 就不要用 in 了:
select id from t where num between 1 and 3
很多時候用 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)
6.下面的查詢也將導致全表掃描:
select id from t where name like ‘%abc%’
若要提高效率,可以考慮全文檢索。
7.如果在 where 子句中使用參數,也會導致全表掃描。因為SQL只有在運行時才會解析局部變量,但優化程序不能將訪問計划的選擇推遲到運行時;它必須在編譯時進行選擇。然而,如果在編譯時建立訪問計划,變量的值還是未知的,因而無法作為索引選擇的輸入項。如下面語句將進行全表掃描:
select id from t where num = @num
可以改為強制查詢使用索引:
select id from t with(index(索引名)) where num = @num
8.應盡量避免在 where 子句中對字段進行表達式操作,這將導致引擎放棄使用索引而進行全表掃描。如:
select id from t where num/2 = 100
應改為:
select id from t where num = 100*2
9.應盡量避免在where子句中對字段進行函數操作,這將導致引擎放棄使用索引而進行全表掃描。如:
select id from t where substring(name,1,3) = ’abc’ -–name以abc開頭的id select id from t where datediff(day,createdate,’2005-11-30′) = 0 -–‘2005-11-30’ --生成的id
應改為:
select id from t where name like 'abc%' select id from t where createdate >= '2005-11-30' and createdate < '2005-12-1'
10.不要在 where 子句中的“=”左邊進行函數、算術運算或其他表達式運算,否則系統將可能無法正確使用索引。
11.在使用索引字段作為條件時,如果該索引是復合索引,那么必須使用到該索引中的第一個字段作為條件時才能保證系統使用該索引,否則該索引將不會被使用,並且應盡可能的讓字段順序與索引順序相一致。
12.不要寫一些沒有意義的查詢,如需要生成一個空表結構:
select col1,col2 into #t from t where 1=0
這類代碼不會返回任何結果集,但是會消耗系統資源的,應改成這樣:
create table #t(…)
13.Update 語句,如果只更改1、2個字段,不要Update全部字段,否則頻繁調用會引起明顯的性能消耗,同時帶來大量日志。
14.對於多張大數據量(這里幾百條就算大了)的表JOIN,要先分頁再JOIN,否則邏輯讀會很高,性能很差。
15.select count(*) from table;這樣不帶任何條件的count會引起全表掃描,並且沒有任何業務意義,是一定要杜絕的。
16.索引並不是越多越好,索引固然可以提高相應的 select 的效率,但同時也降低了 insert 及 update 的效率,因為 insert 或 update 時有可能會重建索引,所以怎樣建索引需要慎重考慮,視具體情況而定。一個表的索引數最好不要超過6個,若太多則應考慮一些不常使用到的列上建的索引是否有 必要。
17.應盡可能的避免更新 clustered 索引數據列,因為 clustered 索引數據列的順序就是表記錄的物理存儲順序,一旦該列值改變將導致整個表記錄的順序的調整,會耗費相當大的資源。若應用系統需要頻繁更新 clustered 索引數據列,那么需要考慮是否應將該索引建為 clustered 索引。
18.盡量使用數字型字段,若只含數值信息的字段盡量不要設計為字符型,這會降低查詢和連接的性能,並會增加存儲開銷。這是因為引擎在處理查詢和連 接時會逐個比較字符串中每一個字符,而對於數字型而言只需要比較一次就夠了。
19.盡可能的使用 varchar/nvarchar 代替 char/nchar ,因為首先變長字段存儲空間小,可以節省存儲空間,其次對於查詢來說,在一個相對較小的字段內搜索效率顯然要高些。
20.任何地方都不要使用 select * from t ,用具體的字段列表代替“*”,不要返回用不到的任何字段。
21.盡量使用表變量來代替臨時表。如果表變量包含大量數據,請注意索引非常有限(只有主鍵索引)。
22. 避免頻繁創建和刪除臨時表,以減少系統表資源的消耗。臨時表並不是不可使用,適當地使用它們可以使某些例程更有效,例如,當需要重復引用大型表或常用表中的某個數據集時。但是,對於一次性事件, 最好使用導出表。
23.在新建臨時表時,如果一次性插入數據量很大,那么可以使用 select into 代替 create table,避免造成大量 log ,以提高速度;如果數據量不大,為了緩和系統表的資源,應先create table,然后insert。
24.如果使用到了臨時表,在存儲過程的最后務必將所有的臨時表顯式刪除,先 truncate table ,然后 drop table ,這樣可以避免系統表的較長時間鎖定。
25.盡量避免使用游標,因為游標的效率較差,如果游標操作的數據超過1萬行,那么就應該考慮改寫。
26.使用基於游標的方法或臨時表方法之前,應先尋找基於集的解決方案來解決問題,基於集的方法通常更有效。
27.與臨時表一樣,游標並不是不可使用。對小型數據集使用 FAST_FORWARD 游標通常要優於其他逐行處理方法,尤其是在必須引用幾個表才能獲得所需的數據時。在結果集中包括“合計”的例程通常要比使用游標執行的速度快。如果開發時 間允許,基於游標的方法和基於集的方法都可以嘗試一下,看哪一種方法的效果更好。
28.在所有的存儲過程和觸發器的開始處設置 SET NOCOUNT ON ,在結束時設置 SET NOCOUNT OFF 。無需在執行存儲過程和觸發器的每個語句后向客戶端發送 DONE_IN_PROC 消息。
29.盡量避免大事務操作,提高系統並發能力。
30.盡量避免向客戶端返回大數據量,若數據量過大,應該考慮相應需求是否合理。
實際案例分析:拆分大的 DELETE 或INSERT 語句,批量提交SQL語句
如果你需要在一個在線的網站上去執行一個大的 DELETE 或 INSERT 查詢,你需要非常小心,要避免你的操作讓你的整個網站停止相應。因為這兩個操作是會鎖表的,表一鎖住了,別的操作都進不來了。
Apache 會有很多的子進程或線程。所以,其工作起來相當有效率,而我們的服務器也不希望有太多的子進程,線程和數據庫鏈接,這是極大的占服務器資源的事情,尤其是內存。
如果你把你的表鎖上一段時間,比如30秒鍾,那么對於一個有很高訪問量的站點來說,這30秒所積累的訪問進程/線程,數據庫鏈接,打開的文件數,可能不僅僅會讓你的WEB服務崩潰,還可能會讓你的整台服務器馬上掛了。
所以,如果你有一個大的處理,你一定把其拆分,使用 LIMIT oracle(rownum),sqlserver(top)條件是一個好的方法。下面是一個mysql示例:
while(1){ //每次只做1000條 mysql_query(“delete from logs where log_date <= ’2012-11-01’ limit 1000”); if(mysql_affected_rows() == 0){
//刪除完成,退出! break; } //每次暫停一段時間,釋放表讓其他進程/線程訪問。 usleep(50000) }
SQL優化參考文檔:http://www.jfox.info/SQL-you-hua
# 針對專門操作符的調優
參考文檔:SQL 語句性能調優
與 (AND)
數據庫系統按着從左到右的順序來解析一個系列由 AND 連接的表達式,但是 Oracle 卻是個例外,它是從右向左地解析表達式。可以利用數據庫系統的這一特性,來將概率小的表達示放在前面,或者是如果兩個表達式可能性相同,那么可將相對不復雜的表達式放在前面。這樣做的話,如果第一個表達式為假的話,那么數據庫系統就不必再費力去解析第二個表達式了。例如,可以這樣轉換:
... WHERE column1 = 'A' AND column2 = 'B'
轉換成:
... WHERE column2 = 'B' AND column1 = 'A'
這里假設 column2 = 'B'的概率較低,如果是 Oracle 數據庫的話,只需將規則反過來用即可。
或 (OR)
和與 (AND) 操作符相反,在用或 (OR) 操作符寫 SQL 語句時,就應該將概率大的表達示放在左面,因為如果第一個表達示為假的話,OR 操作符意味着需要進行下一個表達示的解析。
與 + 或
按照集合的展開法則,
A AND (B OR C) 與 (A AND B) OR (A AND C) 是等價表達示。
假設有如表 3 所示的一張表,要執行一個 AND 操作符在前的表達示
SELECT * FROM Table1 WHERE (column1 = 1 AND column2 = 'A') OR (column1 = 1 AND column2 = 'B')
表 3. AND+OR 查詢
Row# | Colmun1 | Column2 |
---|---|---|
1 | 3 | A |
2 | 2 | B |
3 | 1 | C |
當數據庫系統按照查詢語進行搜索時,它按照下面的步驟執行:
- 索引查找 column1 = 1, 結果集 = {row 3}
- 索引查找 column2 = ‘ A ’ , 結果集 = {row1}
- AND 合並結果集,結果集 = {}
- 索引查找 column 1 = 1, 結果集 = {row 3}
- 索引查找 column 2 = ‘ B ’ , 結果集 = {row2}
- AND 合並結果集,結果集 = {}
- OR 合並結集,結果集 = {}
現在根據集合的展開法則,對上面的語句進行轉換:
SELECT * FROM Table1 WHERE column1 = 1 AND (column2 = 'A' OR column2 = 'B')
按照新的順序進行查搜索時,它按照下面的步驟執行:
- 索引查找 column2 = ‘ A ’ , 結果集 = {row1}
- 索引查找 column 2 = ‘ B ’ , 結果集 = {row2}
- OR 合並結集,結果集 = {}
- 索引查找 column1 = 1, 結果集 = {row 3}
- AND 合並結果集,結果集 = {}
由此可見搜索次數少了一次。雖然一些數據庫操作系統會自動的進行這樣的轉換,但是對於簡單的查詢來說,這樣的轉換還是有好處的。
非 (NOT)
讓非 (NOT) 表達示轉換成更易讀的形式。簡單的條件能通過將比較操作符進行反轉來達到轉換的目的,例如:
... WHERE NOT (column1 > 5)
轉換成:
... WHERE column1 <= 5
比較復雜的情況,根據集合的摩根定理:
NOT (A AND B) = (NOT A) OR (NOT B) 和 NOT (A OR B) = (NOT A) AND (NOT B)
根據這一定理,可以看出它可以至少二次的搜索有可能減少為一次。如下的查詢條件:
... WHERE NOT (column1 > 5 OR column2 = 7)
可以轉換成:
... WHERE column1 <= 5 AND column2 <> 7
但是,當轉換成后的表達示中有不等操作符 <>,那么性能就會下降,畢竟,在一個值平均分布的集合中,不等的值的個數要遠遠大於相等的值的個數,正因為如此,一些數據庫系統不會對非比較進行索引搜索,但是他們會為大於或小於進行索引搜索,所以可以將下面的查詢進行如下轉換:
... WHERE NOT (column1 = 0)
轉換成:
... WHERE column <0 OR column > 0
IN
很多人認為如下的兩個查詢條件沒有什么差別,因為它們返回的結果集是相同的:
條件 1:
... WHERE column1 = 5 OR column1 = 6
條件 2:
... WHERE column1 IN (5, 6)
這樣的想法並不完全正確,對於大多數的數據庫操作系統來說,IN 要比 OR 執行的快。所以如果可以的話,要將 OR 換成 IN
當 IN 操作符,是一系列密集的整型數字時,最好是查找哪些值不符合條件,而不是查找哪些值符合條件,因此,如下的查詢條件就應該進行如下的轉換:
... WHERE column1 IN (1, 3, 4, 5)
轉換成:
... WHERE column1 BETWEEN 1 AND 5 AND column1 <> 2
當一系列的離散的值轉換成算數表達示時,也可獲得同樣的性能提高。
UNION
在 SQL 中,兩個表的 UNION 就是兩個表中不重復的值的集合,即 UNION 操作符返返回的兩個或多個查詢結果中不重復行的集合。這是一個很好的合並數據的方法,但是這並不是最好的方法。
查詢 1:
SELECT * FROM Table1 WHERE column1 = 5 UNION SELECT * FROM Table1 WHERE column2 = 5
查詢 2:
SELECT DISTINCT * FROM Table1 WHERE column1 = 5 OR column2 = 5
在上面的例子中,column1 和 column2 都沒有索引。如果查詢 2 總是比查詢 1 執行的快的話,那么就可以建議總是將查詢 1 轉換成查詢 2,但是有一種情況,這樣做在一些數據庫系統中可能會帶來性能變差,這是由於兩個優化缺陷所造成的。
第一個優化缺陷就是很多優化器只優化一個 SELECT 語句中一個 WHERE 語句,所以查詢 1 的兩個 SELECT 語句都被執行。首先優化器根據查詢條件 column1 = 5 為真來查找所有符合條件的所有行,然后據查詢條件 column2 = 5 為真來查找所有符合條件的所有行,即兩次表掃描,因此,如果 column1 = 5 沒有索引的話,查詢 1 將需要 2 倍於查詢 2 所需的時間。如果 column1 = 5 有索引的話,仍然需要二次掃描,但是只有在某些數據庫系統存在一個不常見的優化缺陷卻將第一個優化缺陷給彌補了。當一些優化器發現查詢中存在 OR 操作符時,就不使用索引查詢,所以在這種情況下,並且只有在這種情況下,UNION 才比 OR 性能更高。這種情況很少見,所以仍然建議大家當待查詢的列沒有索引時使用 OR 來代替 UNION。