在應用系統開發初期,由於開發數據庫數據比較少,對於查詢sql語句,復雜試圖的編寫等體會不出sql語句各種寫法的性能優劣,但是如果將應用系統提交實際應用后,隨着數據庫中數據的增加,系統的響應速度就成為目前系統需要解決的最主要問題之一。系統優化中一個很重要的方面就是sql語句的優化。對於海量數據,劣質sql語句和優質sql語句之間的速度差別可以達到上百倍,可見對於一個系統不是簡單地能實現其功能就行,而是要寫出高質量的sql語句,提高系統的可用性。
Oracle的sql調優第一個復雜的主題,甚至需要長篇概論來介紹OracleSQL調優的細微差別。不過有一些基本的規則是每個OracleDBA都需要遵從的,這些規則可以改善他們系統的性能。
sql調優的目標是簡單的:消除不必要的大表全表搜索。不必要的全表搜索導致大量不必要的磁盤I/O,從而拖慢整個數據庫的性能,對於不必要的全表搜索來說,最常見的調優方法是增加索引,可以在表中加入標准的B樹索引,也可以加入位圖索引和基於函數的索引。要決定是否消除一個全表搜索,你可以仔細檢查索引搜索的I/O開銷和全表搜索的開銷,它們的開銷和數據塊的讀取和可能的並行執行有關,並將兩者作對比。
另外,在全表搜索是一個最快的訪問方法時,將小表的全表搜索放到緩存(內存)中,也是一個非常明智的選擇。我們會發現現在誕生了很多基於內存的數據庫管理系統,將整個數據庫置於內存之中,性能將得到質的飛躍。
一、與索引相關的性能優化
在多數情況下,Oracle使用索引來更快地遍歷表,優化器主要根據定義的索引來提高性能。但是,如果在sql語句的where子句中寫的sql代碼不合理,就會造成優化器刪去索引而使用全表掃描,一般這種sql語句就是所謂的劣質sql語句。在編寫sql語句時我們應清楚優化器根據何種原則來刪除索引,這有助於寫出高性能的sql語句。
1.IS NULL 與 IS NOT NULL
不能用null做索引,任何包含null值的列都將不會被包含在索引中,即使索引有多列這樣的情況下,只要這些列中有一列含有null,該列就會從索引中排除。也就是說某列存在空值,即使對該列建立索引也不會提高性能。任何在where子句中使用is null或者is nou null的語句優化器是不允許使用索引的。
2.聯接列
對於有聯接的列,即使最后的聯接列為一個靜態值,優化器是不會使用索引的。來看個例子,假定有一個職工表(employee),對於一個職工的姓和名分成兩列存放(FIRST_NAME和LAST_NAME),現在要查詢一個叫Beill Cliton的職工。
下面是一個采用聯接查詢的sql語句:
select * from employee where first_name ||''|| last_name = 'Beill Cliton';
上面這條語句完全可以查詢出是否有Beill Cliton這個員工,但是這里需要注意,系統優化器對基於LAST_NAME創建的索引沒有使用,當采用下面這種sql語句的編寫,Oracle系統就可以采用基於LAST_NAME創建的索引:
select * from employee where first_name = 'Beill' and last_name = 'Cliton';
3.帶通配符(%)的like語句
同樣拿上面的例子,目前的需求是這樣的,要求在職工表中查詢名字中包含Cliton的人,可以采用如下的查詢sql語句:
select * from employee where last_name like '%Cliton%';
這里由於通配符(%)在搜尋詞首出現,所以Oracle系統無法使用last_name的索引。在很多情況下可能無法避免這種情況,但是一定要心里有底,通配符這樣使用會降低查詢速度。然而當通配符出現在字符串其它位置時,優化器就能利用索引,在下面的查詢中索引就得到了使用:
select * from employee where last_name like 'C%';
該語句查詢所有姓名以C開頭的,這完全滿足索引的要求,因為索引本身就是一個排序的列。
4.ORDER BY 子句
ORDER BY子句決定了Oracle如何將返回的查詢結果排序。該子句對要排序的列沒有什么特別的限制,也可以將函數加入到列中(象聯接或者附加等)。任何在該子句的非索引項或者有計算表達式都將降低查詢速度。
仔細檢查order by子語句找出非索引項或者表達式,它們會降低性能。解決這個問題的辦法就是重寫order by子句以使用索引,也可以為所使用的列建立另外一個索引,同時應絕對避免在order by子句中使用表達式。
5.NOT 關鍵字
我們在查詢時經常在where子句使用一些邏輯表達式,如大於、小於、等於以及不等於等等,也可以使用and(與)、or(或)以及not(非)。NOT可用來對任何邏輯運算取反。下面是一個NOT子句的例子:
... where not(status = 'VALID');
如果要使用NOT,則應在取反的短語前面加上括號,並在短語前面加上NOT運算符。NOT運算符包含在另外一個邏輯運算符中,這就是不等於(<>)運算符。換句話說,即使不在查詢where子句中加入NOT關鍵字,NOT仍在運算符中,見下例:
... where status <> 'VALID';
再看下面這個例子:
select * from employee where salary <> 3000;
對這個查詢,可以改寫為不使用NOT:
select * from employee where salary < 3000 or salary > 3000;
雖然這兩種查詢的結果是一樣的,但是第二種查詢方案會比第一種查詢方案更快些。第二種查詢允許Oralce對salary列使用索引,而第一種查詢不能使用索引。
6.IN 和 EXISTS
有時候會將一列和一系列值相比較,最簡單的辦法就是在where子句中使用子查詢。在where子句中可以使用兩種格式的子查詢。
第一種格式是使用IN操作符:
... where column in (select * from ... where ...);
第二種格式是使用EXISTS操作符:
... where exists (select 'X' from ... where ...);
相信絕大多數人會使用第一種格式,因為它比較容易編寫,而實際上第二種格式要遠比第一種格式的效率高。在Oracle中可以幾乎將所有的IN操作符子查詢改寫為使用EXISTS的子查詢。
第二種格式中,子查詢以“select 'X'”開始,運用EXISTS子句,不管子查詢從表中抽取什么數據,它只查看where子句,這樣優化器就不必遍歷整個表而僅根據索引就可完成工作(這里假定在where語句中使用的列存在索引)。相對於IN子句來說,EXISTS使用相連子查詢構造起來要比IN子查詢困難一些。
通過使用EXISTS,Oracle系統會首先檢查主查詢,然后運行子查詢直到它找到第一個匹配項,這就節省了時間。Oralce系統在執行IN子查詢時,首先執行子查詢,並將獲得的結果列表放在一個加了索引的臨時表中。在執行子查詢之前,系統先將主查詢掛起,待子查詢執行完畢,存放在臨時表中以后再執行主查詢。這也就是使用EXISTS比使用IN通常查詢速度快的原因。
同時應盡可能使用NOT EXISTS來代替NOT IN,盡管二者都使用了NOT(不能使用索引而降低速度),NOT EXISTS要比NOT IN查詢效率更高。
7.<> 不等於符號
不等於操作符是永遠不會用到索引的,因為對它的處理只會產生全表掃描。
推薦方案:用其它相同功能的操作運算符代替,如
a<>0 改為 a>0 or a<0, a<>'' 改為 a >''
8.避免在索引列上使用計算
WHERE子句中,如果索引列是函數的一部分,優化器將不使用索引而使用全表掃描
低效:
select ... from dept where SAL * 12 > 25000;
高效:
select ... from dept where SAL > 25000/12;
9.總是使用索引的第一個列
如果索引是建立在多個列上,只有在它的第一個列(leading column)被where子句引用時,優化器才會選擇使用該索引。這也是一條簡單而重要的規則,當僅引用索引的第二個列時,優化器使用了全表掃描而忽略了索引。
10.避免改變索引列的類型
當比較不同數據類型的數據時,Oralce自動對列進行簡單的類型轉換。
假設empno是一個數值類型的索引列:
select ... from emp where empno = '123';
實際上,經過了Oracle類型轉換,語句轉化為:
select ... from emp where empno = TO_NUMBER('123');
幸運的是,類型轉換沒有發生在索引列上,索引的用途沒有被改變。
現在,假設emp_type是一個字符類型的索引列:
select ... from emp where emp_type = 123;
這個語句被Oracle轉換為:
select ... from emp where TO_NUMBER(emp_type) = 123;
因為內部發生的類型轉換,這個索引將不會被用到。為了避免Oracle對sql進行隱式的類型轉換,最好把類型轉換用顯示表現出來。注意當字符和數值比較時,Oracle會優先轉換數值類型到字符類型。
11.需要當心的 WHERE 子句
某些select語句中的where子句不使用索引:
- '!='將不使用索引,索引只能告訴你什么存在於表中,而不能告訴你什么不存在於表中
- '||'是字符連接函數,就像其它函數那樣,停用了索引
- '+'是數學函數,就像其它數學函數那樣,停用了索引
- 相同的索引列不能互相比較,這將會啟動全表掃描
12.其它一些規則
- 如果檢索數據量超過30%的表中記錄數,使用索引將沒有顯著的效率提高
- 在特定情況下,使用索引也許會比全盤掃描慢,但這是同一個數量級上的區別。而通常情況下,使用索引比全表掃描要快幾倍乃至幾千倍
- 避免在索引列上使用IS NULL 和IS NOT NULL
- 避免在索引列上使用NOT
- 用EXISTS替代IN、用NOT EXISTS替代NOT IN
- 通過內部函數提高sql效率
- 選擇最有效的表名順序:Oracle的解析器按照從右到左的順序處理from子句中的表名,from子句中寫在最后的表(基礎表)將被最先處理,在from子句中包含多個表的情況下,你必須選擇記錄條數最少的表作為基礎表。如果有三個以上的表連接查詢,那就需要選擇交叉表作為基礎表,交叉表是指被其它表所引用的表
- WHERE子句中的連接順序:Oracle采用自下而上的順序解析where子句,根據這個原理,表之間的連接必須寫在其它where條件之前,那些可以過濾掉最大數量記錄的條件必須寫在where子句的末尾
二、與內存相關的優化
1.UNION 操作符
UNION在進行表鏈接后會篩選掉重復的記錄,所以在表鏈接后會對所產生的結果集進行排序運算,刪除重復的記錄再返回結果。實際大部分應用中是不會產生重復的記錄。最常見的是過程表與歷史表UNION。如:
select * from A union select * from B;
這個sql在運行時先取出兩個表的結果,再用排序空間進行排序刪除重復的記錄,最后返回結果集,如果表數據量大的話可能會導致用磁盤進行排序。
推薦方案:采用UNION ALL操作符替代UNION,因為UNION ALL操作只是簡單的將兩個結果合並后就返回:
select * from A union all select * from B;
2.SQL書寫的影響
同一功能同一性能不同寫法sql的影響
如,一個sql在A程序員寫的為:
select * from employee;
B程序員寫為:
select * from scott.employee; (帶表所有者的前綴)
C程序員寫為:
select * from EMPLOYEE; (大寫表名)
D程序員寫為:
select * from employee; (中間多了空格)
以上四個sql在Oracle分析整理之后產生的結果及執行的時間是一樣的,但是從Oracle共享內存SGA的原理,可以得出Oracle對每個sql都會對其進行一次分析,並且占用共享內存,如果將sql的字符串及格式寫得完全相同則Oracle只會分析一次,共享內存也只會留下一次的分析結果,這不僅可以減少分析sql的時間,而且也可以減少共享內存重復的信息,oracle也可以准確統計sql的執行頻率。
3.避免在磁盤中排序
當與Oracle建立起一個session時,在內存中就會為該session分配一個私有的排序區域。如果該連接是一個專用的連接(dedicated connection),那么就會根據init.ora中sort_area_size參數的大小在內存中分配一個Program Global Area(PGA)。如果連接是通過多線程服務器建立的,那么排序的空間就在large_pool中分配。不幸的是,對於所有的session,用作排序的內存量都必須是一樣的,我們不能為需要更大排序的操作分配額外的排序區域。因此,設計者必須做出一個平衡,在分配足夠的排序區域以避免發生大的排序任務時出現磁盤排序(disksorts)的同時,對於那些並不需要進行很大排序的任務,就會出現一些浪費。當然,當排序的空間需求超出了sort_area_size的大小時,這時將會在TEMP表空間中分頁進行磁盤排序。磁盤排序要比內存排序大概慢14000倍。
上面我們已經提到,私有排序區域的大小是由init.ora中的sort_area_size參數決定的。每個排序所占用的大小由init.ora中的sort_area_size參數決定。當排序不能在分配的空間中完成時,就會使用磁盤排序的方式,即在Oracle實例中的臨時表空間中進行。
磁盤排序的開銷是很大的,有幾個方面的原因。首先,和內存排序相比較,它們特別慢;而且,磁盤排序會消耗臨時表空間中的資源。Oracle還必須分配緩沖池塊來保持臨時表空間中的塊。無論什么時候,內存排序都比磁盤排序好,磁盤排序將會令任務變慢,並且會影響Oracle實例的當前任務的執行。還有,過多的磁盤排序將會令freebufferwaits的值特別高,從而令其它任務的數據塊由緩沖中移走。
4.避免使用耗費資源的操作
帶有 DISTINCT,UNION,MINUS,INTERSECT,ORDER BY的sql語句回啟動sql引擎執行耗費資源的排序(SORT)功能。DISTINCT需要一次排序操作,而其它的至少需要執行兩次排序。通常,帶有UNION,MINUS,INTERSECT的sql語句都可以用其它方式重寫。如果你的數據庫的sort_area_size調配的好,使用UNION,MINUS,INTERSECT也是可以考慮的,畢竟它們的可讀性很強。
三、其它性能優化相關技巧
1.刪除重復記錄
最高效的刪除重復記錄方法(因使用了ROWID)例子:
delete from emp E where E.ROWID > (select MIN(X.ROWID) from emp X where X.emp_no = E.emp_no);
2.用 TRUNCATE 替代 DELETE
當刪除表中的記錄時,在通常情況下,回滾段(rollback segments)用來存放可以被恢復的信息。如果你沒有COMMIT事務,Oracle會將數據恢復到刪除之前的狀態(准確地說是恢復到執行刪除之前的狀態),而當運用TRUNCATE時,回滾段不再存放任何可被恢復的信息。當命令運行后,數據不能被恢復,因此很少的資源被調用,執行時間也會很短(TRUNCATE只在清空全表適用,TRUNCATE是DDL而不是DML)。
3.SELECT 子句中避免使用 *
Oracle在解析的過程中,會將 * 依次轉換成所有的列名,這個工作是通過查詢數據字典完成的,這意味着將耗費更多的時間。
4.用 WHERE 子句替換 HAVING 子句
避免使用having子句,having只會在檢索出所有記錄之后才對結果集進行過濾,這個處理需要排序、總計等操作。如果能通過where子句限制記錄的數目,那就能減少這方面的開銷。sql語句中on、where、having這三個都可以加條件的子句中,on是最先執行,where次之,having最后,因為on是先把不符合條件的記錄過濾后才進行統計,它就可以減少中間運算要處理的數據,按理說應該是速度最快的,where也應該比having快點的,因為它過濾數據后才進行sum,在兩個表連接時采用on,所以在一個表的時候,就剩下where跟having比較了。在單表查詢統計的情況下,如果要過濾的條件沒有涉及到要計算字段,那它們的結果是一樣的,只是where可以使用rushmore技術,而having就不能,在速度上后者要慢如果要涉及到計算的字段,就表示在沒計算之前,這個字段的值是不確定的,where的作用時間是在計算之前就完成的,而having就是在計算之后才起作用的,所以在這種情況下,兩者的結果會不同。在多表聯接查詢時,on比where更早起作用,系統首先根據各個表之間的連接條件,把多個表合成一個臨時表后,再由where進行過濾,然后再計算,計算完成后再由having進行過濾。由此可見,要想過濾條件起到正確的作用,首先要明白這個條件應該在什么時候起作用,然后再決定放在哪里。
5.使用表的別名(Alias)
當在sql語句中連接多個表時,請使用表的別名並把別名前綴於每個column上。這樣一來,就可以減少解析的時間並減少那些由column歧義引起的語法錯誤。
6.用 EXISTS 替代 IN、用 NOT EXISTS 替代 NOT IN
在許多基於基礎表的查詢中,為了滿足一個條件,往往需要對另一個表進行聯接。在這種情況下,使用EXISTS(或NOT EXISTS)通常將提高查詢的效率。在子查詢中NOT IN子句將執行一個內部的排序和合並。無論在哪種情況下,NOT IN都是最低效的(因為它對子查詢中的表執行了一個全表遍歷)。為了避免使用NOT IN,我們可以把它改寫成外聯接(OUTER JOIN)或NOT EXISTS。
高效:
select * from emp where empno > 0 and EXISTS (select 'X' from dept where dept.deptno = emp.deptno and loc = 'MELB');
低效:
select * from emp where empno > 0 and deptno IN (select deptno from dept where loc = 'MELB');
7.用 EXISTS 替換 DISTINCT
提交一個對多表信息(比如部門表和雇員表)進行查詢的語句時,避免在select子句中使用distinct。一般可以考慮用EXISTS替換,EXISTS使查詢更為迅速,因為RDBMS核心模塊將在子查詢的條件一但滿足后,立刻返回結果。
高效:
select dept_no, dept_name from dept D where EXISTS (select 'X' from emp E where E.deptno = D.deptno);
低效:
select DESTINCT dept_no, dept_name from dept D, emp E where D.deptno = E.deptno;
8.SQL語句使用大寫
因為Oracle總是先解析sql語句,把小寫的字母轉換成大寫的再執行
9.用 >= 替代 >
高效:
select * from emp where deptno >= 4;
低效:
select * from emp where deptno > 3;
兩者的區別在於,前者DBMS將直接跳到第一個deptno等於4的記錄,而后者將首先定位到deptno=3的記錄並且向前掃描到第一個deptno大於3的記錄。
10.優化 GROUP BY
提高group by語句的效率,可以通過將不需要的記錄在group by之前過濾掉。下面兩個查詢返回相同結果,但第二個就明顯快了許多。
低效:
select job, AVG(SAL) from emp group by job having job = 'PRESIDENT' or job = 'MANAGER';
高效:
select job, AVG(SAL) from emp where job = 'PRESIDENT' or job = 'MANAGER' group by job;