Oracle資料庫中的優化器又叫查詢優化器(Query Optimizer)。它是SQL分析和執行的優化工具,它負責生成、制定SQL的執行計劃。Oracle的優化器有兩種,基于規則的優化器(RBO)與基于代價的優化器(CBO)
RBO: Rule-Based Optimization 基于規則的優化器
CBO: Cost-Based Optimization 基于代價的優化器
RBO自ORACLE 6以來被采用,一直沿用至ORACLE 9i. ORACLE 10g開始,ORACLE已經徹底丢棄了RBO,它有着一套嚴格的使用規則,隻要你按照它去寫SQL語句,無論資料表中的内容怎樣,也不會影響到你的“執行計劃”,也就是說RBO對資料不“敏感”;它根據ORACLE指定的優先順序規則,對指定的表進行執行計劃的選擇。比如在規則中,索引的優先級大于全表掃描;RBO是根據可用的通路路徑以及通路路徑等級來選擇執行計劃,在RBO中,SQL的寫法往往會影響執行計劃,它要求開發人員非常了解RBO的各項細則,菜鳥寫出來的SQL腳本性能可能非常差。随着RBO的被遺棄,漸漸不為人所知。也許隻有老一輩的DBA對其了解得比較深入。關于RBO的通路路徑,官方文檔做了詳細介紹:
<a href="http://docs.oracle.com/cd/B10501_01/server.920/a96533/rbo.htm#38960">RBO Path 1: Single Row by Rowid</a>
<a href="http://docs.oracle.com/cd/B10501_01/server.920/a96533/rbo.htm#38979">RBO Path 2: Single Row by Cluster Join</a>
<a href="http://docs.oracle.com/cd/B10501_01/server.920/a96533/rbo.htm#39019">RBO Path 3: Single Row by Hash Cluster Key with Unique or Primary Key</a>
<a href="http://docs.oracle.com/cd/B10501_01/server.920/a96533/rbo.htm#39041">RBO Path 4: Single Row by Unique or Primary Key</a>
<a href="http://docs.oracle.com/cd/B10501_01/server.920/a96533/rbo.htm#39063">RBO Path 5: Clustered Join</a>
<a href="http://docs.oracle.com/cd/B10501_01/server.920/a96533/rbo.htm#39095">RBO Path 6: Hash Cluster Key</a>
<a href="http://docs.oracle.com/cd/B10501_01/server.920/a96533/rbo.htm#39114">RBO Path 7: Indexed Cluster Key</a>
<a href="http://docs.oracle.com/cd/B10501_01/server.920/a96533/rbo.htm#39135">RBO Path 8: Composite Index</a>
<a href="http://docs.oracle.com/cd/B10501_01/server.920/a96533/rbo.htm#39156">RBO Path 9: Single-Column Indexes</a>
<a href="http://docs.oracle.com/cd/B10501_01/server.920/a96533/rbo.htm#39197">RBO Path 10: Bounded Range Search on Indexed Columns</a>
<a href="http://docs.oracle.com/cd/B10501_01/server.920/a96533/rbo.htm#39232">RBO Path 11: Unbounded Range Search on Indexed Columns</a>
<a href="http://docs.oracle.com/cd/B10501_01/server.920/a96533/rbo.htm#39269">RBO Path 12: Sort Merge Join</a>
<a href="http://docs.oracle.com/cd/B10501_01/server.920/a96533/rbo.htm#39301">RBO Path 13: MAX or MIN of Indexed Column</a>
<a href="http://docs.oracle.com/cd/B10501_01/server.920/a96533/rbo.htm#39321">RBO Path 14: ORDER BY on Indexed Column</a>
<a href="http://docs.oracle.com/cd/B10501_01/server.920/a96533/rbo.htm#39346">RBO Path 15: Full Table Scan</a>
<b></b>
CBO是一種比RBO更加合理、可靠的優化器,它是從ORACLE 8中開始引入,但到ORACLE 9i 中才逐漸成熟,在ORACLE 10g中完全取代RBO, CBO是計算各種可能“執行計劃”的“代價”,即COST,從中選用COST最低的執行方案,作為實際運作方案。它依賴資料庫對象的統計資訊,統計資訊的準确與否會影響CBO做出最優的選擇。如果對一次執行SQL時發現涉及對象(表、索引等)沒有被分析、統計過,那麼ORACLE會采用一種叫做動态采樣的技術,動态的收集表和索引上的一些資料資訊。
關于RBO與CBO,我有個形象的比喻:大資料時代到來以前,做生意或許憑借多年累計下來的經驗(RBO)就能夠很好的做出決策,跟随市場變化。但是大資料時代,如果做生意還是靠以前憑經驗做決策,而不是靠大資料、資料分析、資料挖掘做決策,那麼就有可能做出錯誤的決策。這也就是越來越多的公司對BI、資料挖掘越來越重視的緣故,像電商、遊戲、電信等行業都已經大規模的應用,以前在一家遊戲公司資料庫部門做BI分析,挖掘潛在消費使用者簡直無所不及。至今映像頗深。
<b>CBO</b><b>與RBO的優劣</b>
CBO優于RBO是因為RBO是一種呆闆、過時的優化器,它隻認規則,對資料不敏感。畢竟規則是死的,資料是變化的,這樣生成的執行計劃往往是不可靠的,不是最優的,CBO由于RBO可以從很多方面展現。下面請看一個例子,此案例來自于《讓Oracle跑得更快》。
從上面可以看出,該測試表的資料分布極其不均衡,ID=100的記錄隻有一條,而ID=1的記錄有50314條。我們先看看RBO下兩條SQL的執行計劃.
從執行計劃可以看出,RBO的執行計劃讓人有點失望,對于ID=1,幾乎所有的資料全部符合謂詞條件,走索引隻能增加額外的開銷(因為ORACLE首先要通路索引資料塊,在索引上找到了對應的鍵值,然後按照鍵值上的ROWID再去通路表中相應資料),既然我們幾乎要通路所有表中的資料,那麼全表掃描自然是最優的選擇。而RBO選擇了錯誤的執行計劃。可以對比一下CBO下SQL的執行計劃,顯然它對資料敏感,執行計劃及時的根據資料量做了調整,當查詢條件為1時,它走全表掃描;當查詢條件為100時,它走區間索引掃描。如下所示:
僅此一項就可以看出為什麼ORACLE極力推薦使用CBO,從ORACLE 10g開始不支援RBO的緣故。所謂長江後浪推前浪,前浪死在沙灘上。
<b>CBO</b><b>知識點的總結</b>
CBO優化器根據SQL語句生成一組可能被使用的執行計劃,估算出每個執行計劃的代價,并調用計劃生成器(Plan Generator)生成執行計劃,比較執行計劃的代價,最終選擇選擇一個代價最小的執行計劃。查詢優化器由查詢轉換器(Query Transform)、代價估算器(Estimator)和計劃生成器(Plan Generator)組成。
<b>CBO</b><b>優化器元件</b>
CBO由以下元件構成:
· 查詢轉化器(Query Transformer)
查詢轉換器的作用就是等價改變查詢語句的形式,以便産生更好的執行計劃。它決定是否重寫使用者的查詢(包括視圖合并、謂詞推進、非嵌套子查詢/子查詢反嵌套、物化視圖重寫),以生成更好的查詢計劃。
· 代價評估器(Estimator)
評估器通過複雜的算法結合來統計資訊的三個值來評估各個執行計劃的總體成本:選擇性(Selectivity)、基數(Cardinality)、成本(Cost)
計劃生成器會考慮可能的通路路徑(Access Path)、關聯方法和關聯順序,生成不同的執行計劃,讓查詢優化器從這些計劃中選擇出執行代價最小的一個計劃。
· 計劃生成器(Plan Generator)
計劃生成器就是生成大量的執行計劃,然後選擇其總體代價或總體成本最低的一個執行計劃。
由于不同的通路路徑、連接配接方式和連接配接順序可以組合,雖然以不同的方式通路和處理資料,但是可以産生同樣的結果
下圖是我自己為了加深了解,用工具畫的圖
<b></b>
<b>檢視ORACLE優化器</b>
<b>修改ORACLE優化器</b>
ORACLE 10g 優化器可以從系統級别、會話級别、語句級别三種方式修改優化器模式,非常友善靈活。
其中optimizer_mode可以選擇的值有: first_rows_n,all_rows. 其中first_rows_n又有first_rows_1000, first_rows_100, first_rows_10, first_rows_1
在Oracle 9i中,優化器模式可以選擇first_rows_n,all_rows, choose, rule 等模式:
Rule: 基于規則的方式。
Choolse:指的是當一個表或或索引有統計資訊,則走CBO的方式,如果表或索引沒統計資訊,表又不是特别的小,而且相應的列有索引時,那麼就走索引,走RBO的方式。
First Rows:它與Choose方式是類似的,所不同的是當一個表有統計資訊時,它将是以最快的方式傳回查詢的最先的幾行,從總體上減少了響應時間。
All Rows: 10g中的預設值,也就是我們所說的Cost的方式,當一個表有統計資訊時,它将以最快的方式傳回表的所有的行,從總體上提高查詢的吞吐
雖然Oracle 10g中不再支援RBO,Oracle 10g官方文檔關于optimizer_mode參數的隻有first_rows和all_rows.但是依然可以設定 optimizer_mode為rule或choose,估計是ORACLE為了過渡或向下相容考慮。如下所示。
<b>系統級别</b>
<b>會話級别</b>
會話級别修改優化器模式,隻對目前會話有效,其它會話依然使用系統優化器模式。
SQL> alter session set optimizer_mode=first_rows_100;
Session altered.
<b>語句級别</b>
語句級别通過使用提示hints來實作。
SQL> select /*+ rule */ * from dba_objects where rownum <= 10;
第1章 Oracle裡的優化器
到目前為止,Oracle資料庫是市場占有率最高(接近50%),使用範圍最廣的關系型資料庫(RDBMS),這意味着有太多太多的系統都是建構在Oracle資料庫上的。而我們大家都知道,對于使用關系型資料庫的應用系統而言,SQL語句的好壞會直接影響系統的性能,很多系統性能很差最後發現都是因為SQL寫得很爛的緣故。實際上,一條寫得很爛的SQL語句就能拖垮整個應用,極端情況下,一條寫得很爛的SQL語句甚至會導緻資料庫伺服器失去響應或者使整個資料庫Hang住,去Google一下吧,這樣的例子有很多!
怎樣避免在Oracle資料庫中寫出很爛的SQL?或者說應該如何在Oracle資料庫中做SQL優化?這個問題真的很不好回答,且容我慢慢道來。
對所有的關系型資料庫而言,優化器無疑是其中最核心的部分,因為優化器負責解析SQL,而我們又都是通過SQL來通路存儲在關系型資料庫中的資料的,是以優化器的好壞會直接決定該關系型資料庫的強弱。從另外一個方面來說,正是因為優化器負責解析SQL,是以要想做好SQL優化就必須了解優化器,而且最好是能全面、深入的了解,這是做好SQL優化基礎中的基礎。
Oracle資料庫裡的優化器以其複雜、強悍而聞名于世,本章會詳細介紹與Oracle資料庫裡優化器相關的基礎知識,目的是希望通過這一章的介紹,讓大家對Oracle資料庫裡的優化器有一個全局、概要性的認識,打好基礎,為閱讀後續章節掃清障礙。
1.1 什麼是Oracle裡的優化器
優化器(Optimizer)是Oracle資料庫中内置的一個核心子系統,你也可以把它了解成是Oracle資料庫中的一個核心子產品或者一個核心功能元件。優化器的目的是按照一定的判斷原則來得到它認為的目标SQL在目前情形下最高效的執行路徑(Access Path),也就是說,優化器的目的就是為了得到目标SQL的執行計劃(關于執行計劃,會在"第2章 Oracle裡的執行計劃"中較長的描述)。
依據選擇執行計劃時所用的判斷原則,Oracle資料庫裡的優化器又分為RBO和CBO這兩種類型。RBO是Rule-Based Optimizer的縮寫,直譯過來就是"基于規則的優化器";相對應的,CBO是Cost-Based Optimizer的縮寫,直譯過來就是"基于成本的優化器"。
在得到目标SQL的執行計劃時,RBO所用的判斷原則為一組内置的規則,這些規則是寫死在Oracle資料庫的代碼中的,RBO會根據這些規則從目标SQL諸多可能的執行路徑中選擇一條來作為其執行計劃;而CBO所用的判斷原則為成本,CBO會從目标SQL諸多可能的執行路徑中選擇成本值最小的一條來作為其執行計劃,各個執行路徑的成本值是根據目标SQL語句所涉及的表、索引、列等相關對象的統計資訊計算出來的(關于統計資訊,會在"第5章 Oracle裡的統計資訊"中較長的描述)。
Oracle資料庫裡SQL語句的執行過程可以用圖1-1來表示。
關于圖1-1,會在"第4章 Oracle裡的查詢轉換"中詳細說明,這裡隻需要知道Oracle裡優化器的輸入是經過解析後(在這個解析過程中,Oracle會執行對目标SQL的文法、語義和權限檢查)的目标SQL,輸出是該目标SQL的執行計劃就好了。
接下來,分别介紹RBO和CBO。
1.1.1 基于規則的優化器(1)
之前已經提到,基于規則的優化器(RBO)通過寫死在Oracle資料庫代碼中的一系列固定的規則,來決定目标SQL的執行計劃。具體來說就是這樣:Oracle會在代碼裡事先給各種類型的執行路徑定一個等級,一共有15個等級,從等級1到等級15。并且Oracle會認為等級值低的執行路徑的執行效率會比等級值高的執行效率要高,也就是說在RBO的眼裡,等級1所對應的執行路徑的執行效率最高,等級15所對應的執行路徑的執行效率最低。在決定目标SQL的執行計劃時,如果可能的執行路徑不止一條,則RBO就會從該SQL諸多可能的執行路徑中選擇一條等級值最低的執行路徑來作為其執行計劃。
RBO是一種适用于OLTP類型SQL語句的優化器,在這樣的前提條件下,大家來猜一猜RBO的等級1和等級15所對應的執行路徑分别是什麼?
在Oracle資料庫裡,對于OLTP類型的SQL語句而言,顯然通過ROWID來通路是效率最高的方式,而通過全表掃描來通路則是效率最低的方式。與之相對應的,RBO内置的等級1所對應的執行路徑就是"single row by rowid(通過rowid來通路單行資料)",而等級15所對應的執行路徑則是"full table scan(全表掃描)"。
RBO在Oracle中由來已久,雖然從Oracle 10g開始,RBO已不再被Oracle支援,但RBO的相關實作代碼并沒有從Oracle資料庫的代碼中移除,這意味着即使是在Oracle 11gR2中,我們依然可以通過修改優化器模式或使用RULE Hint來繼續使用RBO。
和CBO相比,RBO是有其明顯缺陷的。在使用RBO的情況下,執行計劃一旦出了問題,很難對其做調整;另外,如果使用了RBO,則目标SQL的寫法,甚至是目标SQL中所涉及的各個對象在該SQL文本中出現的先後順序,都可能會影響RBO對于該SQL執行計劃的選擇。更糟糕的是,Oracle資料庫中很多很好的特性、功能均不能在RBO下使用,因為它們均不被RBO所支援。
隻要出現了如下的情形之一(包括但不限于這些情形),那麼即便你修改了優化器模式或者使用了RULE Hint,Oracle依然不會使用RBO(而是強制使用CBO):
目标SQL中涉及的對象有IOT(Index Organized Table)。
目标SQL中涉及的對象有分區表。
使用了并行查詢或者并行DML。
使用了星型連接配接。
使用了哈希連接配接。
使用了索引快速全掃描。
使用了函數索引。
……
在使用RBO的情況下,一旦RBO選擇的執行計劃并不是目前情形下最優的執行計劃,應該如何對其做調整呢?
這種情況下我們是很難對RBO選擇的執行計劃做調整的,其中非常關鍵的一個原因就是不能使用Hint,因為如果在目标SQL中使用了Hint,就意味着自動啟用了CBO,即Oracle會以CBO來解析含Hint的目标SQL。這裡僅有兩個例外,就是RULE Hint和DRIVING_SITE Hint,它們可以在RBO下使用并且不自動啟用CBO(關于Oracle中的Hint,會在"第6章 Oracle裡的Hint"詳細說明)。
那麼,是不是在使用RBO的情況下就沒辦法對執行計劃做調整了?
當然不是這樣,隻是這種情況下我們的調整手段會非常有限。其中的一種可行的方法就是等價改寫目标SQL,比如在目标SQL的where條件中對NUMBER或DATE類型的列加上0(如果是VARCHAR2或CHAR類型,可以加上一個空字元,例如 || ''),這樣就可以讓原本可以走的索引現在走不了。對于包含多表連接配接的目标SQL而言,這種改變甚至可以影響表連接配接的順序,進而就可以實作在使用RBO的情況下對該目标SQL的執行計劃做調整的目的。
之前已經提到:RBO會從目标SQL諸多可能的執行路徑中選擇一條等級值最低的作為其執行計劃,但如果出現了兩條或者兩條以上等級值相同的執行路徑的情況,那麼此時RBO會如何選擇呢?很簡單,此時RBO會依據目标SQL中所涉及的相關對象在資料字典緩存(Data Dictionary Cache)中的緩存順序和目标SQL中所涉及的各個對象在目标SQL文本中出現的先後順序來綜合判斷。這也就意味着我們還可以通過調整相關對象在資料字典緩存中的緩存順序,改變目标SQL中所涉及的各個對象在該SQL文本中出現的先後順序來調整其執行計劃。
我們來看一個在使用RBO的情況下對目标SQL的執行計劃做調整的執行個體。建立一個測試表EMP_TEMP:
SQL> create table emp_temp as select * from emp;
Table created
在表EMP_TEMP的列MGR和DEPTNO上分别建立兩個名為IDX_MGR_TEMP和IDX_DEPTNO_TEMP的索引:
SQL> create index idx_mgr_temp on emp_temp(mgr);
Index created
SQL> create index idx_deptno_temp on emp_temp(deptno);
我們來看一下如下的範例SQL 1:
select * from emp_temp
where mgr > 100 and deptno > 100;
對于範例SQL 1而言,其where條件中出現了列MGR和DEPTNO,而在列MGR和DEPTNO上分别存在着索引IDX_MGR_TEMP和IDX_DEPTNO_TEMP。
現在的問題是,如果在啟用RBO的情形下執行範例SQL 1,則Oracle會選擇走上述兩個索引中的哪一個?
1.1.1 基于規則的優化器(2)
我們來實際驗證一下。在目前Session中将優化器模式修改為RULE,表示在目前Session中啟用RBO:
SQL> alter session set optimizer_mode='RULE';
Session altered
然後執行範例SQL 1:
SQL> set autotrace traceonly explain
SQL> select * from emp_temp where mgr>100 and deptno>100;
注意到Id = 2的執行步驟為"INDEX RANGE SCAN | IDX_DEPTNO_TEMP",Note部分有關鍵字"rule based optimizer used (consider using cbo)",這說明Oracle在執行上述範例SQL 1時使用的是RBO,且選擇的是走對索引IDX_DEPTNO_TEMP的索引範圍掃描。
範例SQL 1的where條件中有"mgr>100",是以RBO實際上是可以選擇走列MGR上的索引IDX_MGR_TEMP的,隻不過RBO這裡并沒有選擇走該索引,而是選擇走列DEPTNO上的索引IDX_DEPTNO_TEMP。
假如我們發現走索引IDX_DEPTNO_TEMP不如走索引IDX_MGR_TEMP的執行效率高,或者說我們就想讓RBO選擇走索引IDX_MGR_TEMP,那麼應該如何做呢?
之前已經提到過:在使用RBO的情況下,可以通過等價改寫目标SQL(加0或者空字元串的方式)來調整該SQL的執行計劃。列DEPTNO的類型為NUMBER,是以我們可以在列DEPTNO上加0,來達到不讓RBO選擇走其上的索引IDX_DEPTNO_TEMP的目的。在列DEPTNO上加0後即形成了如下形式的範例SQL 2:
select * from emp_temp
where mgr>100 and deptno+0>100;
執行範例SQL 2:
SQL> select * from emp_temp where mgr>100 and deptno+0>100;
注意,此時Id = 2的執行步驟已經從之前的"INDEX RANGE SCAN | IDX_DEPTNO_TEMP"變為了現在的"INDEX RANGE SCAN | IDX_MGR_TEMP",這說明我們确實迫使RBO改變了執行計劃,即我們的調整已經生效了。
之前已經提到:如果目标SQL出現了有兩條或者兩條以上的執行路徑的等級值相同的情況,我們可以通過調整相關對象在資料字典緩存中的緩存順序來影響RBO對于其執行計劃的選擇。對于範例SQL 1而言,對索引IDX_DEPTNO_TEMP走索引範圍掃描和對索引IDX_MGR_TEMP走索引範圍掃描的等級值顯然是相同的,是以我們就可以通過調整這兩個索引在資料字典緩存中的緩存順序來改變執行計劃。
剛才我們先建立索引IDX_MGR_TEMP,再建立索引IDX_DEPTNO_TEMP,是以索引IDX_MGR_TEMP和IDX_DEPTNO_TEMP在資料字典緩存中的緩存順序是,先緩存IDX_MGR_TEMP,再緩存IDX_DEPTNO_TEMP。這種情形下RBO選擇的是走對索引IDX_DEPTNO_TEMP的索引範圍掃描,如果我們現在把索引IDX_MGR_TEMP先Drop掉再重新建立一次,那麼就相當于是先建立索引IDX_DEPTNO_TEMP,再建立索引IDX_MGR_TEMP,也就是說此時這兩個索引在資料字典緩存中的緩存順序就剛好颠倒過來了。按照此前介紹的知識,此時RBO應該就會選擇走對索引IDX_MGR_TEMP的索引範圍掃描。
1.1.1 基于規則的優化器(3)
現在驗證一下:
先Drop掉索引IDX_MGR_TEMP:
SQL> drop index idx_mgr_temp;
Index dropped
再重新建立上述索引IDX_MGR_TEMP:
然後再次執行範例SQL 1:
SQL> select * from emp_temp where mgr>100 and deptno>100;
注意,Id = 2的執行步驟已經從之前的"INDEX RANGE SCAN | IDX_DEPTNO_TEMP"變為了現在的"INDEX RANGE SCAN | IDX_MGR_TEMP",說明我們确實迫使RBO改變了執行計劃,這也說明當目标SQL有兩條或者兩條以上的執行路徑的等級值相同時,我們确實可以通過調整相關對象在資料字典緩存中的緩存順序來影響RBO對于其執行計劃的選擇。
我們之前還提到過:如果目标SQL出現了有兩條或者兩條以上的執行路徑的等級值相同的情況,可以通過改變目标SQL中所涉及的各個對象在該SQL文本中出現的先後順序來調整該目标SQL的執行計劃。這通常适用于目标SQL中出現了多表連接配接的情形,在目标SQL出現了有兩條或者兩條以上的執行路徑的等級值相同的前提條件下,RBO會按照從右到左的順序來決定誰是驅動表,誰是被驅動表,進而會據此來選擇執行計劃,是以如果我們改變了目标SQL中所涉及的各個對象在該SQL文本中出現的先後順序,也就改變了表連接配接的驅動表和被驅動表,進而就調整了該SQL的執行計劃。
我們來驗證一下上述結論。再建立一個測試表EMP_TEMP1:
SQL> create table emp_temp1 as select * from emp;
我們來看如下的範例SQL 3:
select t1.mgr, t2.deptno
from emp_temp t1, emp_temp1 t2
where t1.empno = t2.empno;
對于範例SQL 3而言,表EMP_TEMP和EMP_TEMP1唯一的表連接配接條件為"t1.empno = t2.empno",而在表EMP_TEMP和EMP_TEMP1的字段EMPNO上均沒有任何索引,按照前面介紹的知識,表EMP_TEMP1在SQL文本中的位置是在表EMP_TEMP的右邊,是以此時RBO會将表EMP_TEMP1作為表連接配接的驅動表,而将表EMP_TEMP作為表連接配接的被驅動表。
執行一下範例SQL 3:
SQL> select t1.mgr,t2.deptno from emp_temp t1,emp_temp1 t2 where t1.empno=t2.empno;
1.1.1 基于規則的優化器(4)
從上面顯示的内容可以看出,現在範例SQL 3的執行計劃走的是排序合并連接配接,且驅動表确實是表EMP_TEMP1。
注意,從嚴格意義上來說,排序合并連接配接并沒有驅動表和被驅動表的概念,這裡隻是為了友善闡述而人為地給排序合并連接配接添加了上述概念。
将範例SQL 3中的表EMP_TEMP和EMP_TEMP1在該SQL的SQL文本中的位置換一下,即形成了如下形式的範例SQL 4:
from emp_temp1 t2, emp_temp t1
按照前面介紹的知識,現在如果再執行範例SQL 4的話,那麼排序合并連接配接的驅動表應該會變成表EMP_TEMP。
我們來驗證一下。執行範例SQL 4:
SQL> select t1.mgr,t2.deptno from emp_temp1 t2,emp_temp t1 where t1.empno=t2.empno;
從上面顯示的内容可以看出,現在範例SQL 4的執行計劃走的也是排序合并連接配接,且驅動表确實已經由之前的表EMP_TEMP1變為了現在的表EMP_TEMP。這說明我們确實使RBO改變了執行計劃,也說明當目标SQL有兩條或者兩條以上的執行路徑的等級值相同時,我們确實可以通過改變目标SQL中所涉及的各個對象在該SQL文本中出現的先後順序來影響RBO對于其執行計劃的選擇。
注意,這種位置的先後順序對于目标SQL執行計劃的影響是有前提條件的,那就是僅憑各條執行路徑等級值的大小RBO難以選擇執行計劃,也就是說該目标SQL一定有兩條或者兩條以上執行路徑的等級值相同。換句話說,如果RBO僅憑各條執行路徑等級值的大小就可以選擇目标SQL的執行計劃,那麼無論怎麼調整相關對象在該SQL的SQL文本中的位置,對于該SQL最終的執行計劃都不會有任何影響。
我們來驗證一下上述結論。看看如下的範例SQL 5:
from emp t1, emp_temp t2
對于範例SQL 5而言,表EMP和EMP_TEMP唯一的表連接配接條件為"t1.empno = t2.empno"。對于表EMP而言,列EMPNO上存在主鍵索引PK_EMP,而對于表EMP_TEMP而言,列EMPNO上不存在任何索引。是以在使用RBO的情況下,範例SQL 5的執行路徑将不再僅限于排序合并連接配接(RBO不支援哈希連接配接),也就是說RBO此時有可能可以僅憑各條執行路徑等級值的大小就選擇出範例SQL 5的執行計劃。
執行一下範例SQL 5:
SQL> select t1.mgr,t2.deptno from emp t1,emp_temp t2 where t1.empno=t2.empno;
1.1.1 基于規則的優化器(5)
從上面顯示的内容可以看出,現在範例SQL 5的執行計劃走的是嵌套循環連接配接,且驅動表是表EMP_TEMP。
我們将範例SQL 5中的表EMP和EMP_TEMP在該SQL的SQL文本中的位置換一下,即形成了如下形式的範例SQL 6:
from emp_temp t2, emp t1
然後執行範例SQL 6:
SQL> select t1.mgr,t2.deptno from emp_temp t2,emp t1 where t1.empno=t2.empno;
從上面顯示的内容可以看出,現在範例SQL 6的執行計劃走的還是嵌套循環連接配接,且驅動表依然是表EMP_TEMP。這就驗證了我們之前提到的觀點:如果RBO僅憑目标SQL各條執行路徑等級值的大小就可以選擇出執行計劃,那麼無論怎麼調整相關對象在該SQL的SQL文本中的位置,對于該SQL最終的執行計劃都不會有任何影響。
1.1.2 基于成本的優化器
我們在1.1.1節中已經提到:RBO是有明顯缺陷的,比如Oracle資料庫中很多很好的功能、特性,RBO均不支援,RBO産生的執行計劃很難調整等,但這些還不是最要命的,RBO最大的問題在于它是靠寫死在Oracle資料庫代碼中的一系列固定的規則來決定目标SQL的執行計劃的,而并沒有考慮目标SQL中所涉及的對象的實際資料量、實際資料分布等情況,這樣一旦固定的規則并不适用于該SQL中所涉及的實際對象時,RBO根據固定規則産生的執行計劃就很可能不是目前情況下的最優執行計劃了。
我們來看如下的範例SQL 7:
select * from emp
where mgr=7902;
對于範例SQL 7而言,假設在表EMP的列MGR上事先存在一個名為IDX_EMP_MGR的單鍵值B樹索引,如果我們使用RBO,則不管表EMP的資料量有多大,也不管列MGR的資料分布情況如何,Oracle在執行範例SQL 7時始終會選擇走對索引IDX_EMP_MGR的索引範圍掃描,并回表取得表EMP中的記錄。Oracle此時是不會選擇全表掃描表EMP的,因為對于RBO而言,全表掃描的等級值要高于索引範圍掃描的等級值。
RBO的這種選擇在表EMP的資料量不大,或者雖然表EMP的資料量很大,但滿足條件"mgr=7902"的記錄數很少時是沒問題的。如果出現了極端的情況(比如表EMP的資料量很大,有1000萬行記錄,且這1000萬行記錄的列MGR的值均等于7902),當出現這種極端情況時,如果使用RBO,則RBO還是會選擇走對索引IDX_EMP_MGR的索引範圍掃描,那就有問題了!因為這相當于要以單塊讀順序掃描所有的1000萬行索引,然後再回表1000萬次,而這顯然是沒有使用多塊讀以全表掃描方式直接掃描表EMP的執行效率高的(這裡的1000萬隻是一個理論值,實際情況并不完全是這樣,因為這裡并沒有考慮Index Prefetch所帶來的掃描索引時可能會使用的多塊讀。不考慮Index Prefetch的原因是因為它的存在與否對這裡的結論并不會産生本質的影響)。這裡RBO會選錯執行計劃就是因為它并沒有考慮目标SQL中所涉及的對象的實際資料量、實際資料分布等情況,是以RBO确實是有先天缺陷的。
為了解決RBO的上述先天缺陷,從Oracle 7開始,Oracle就引入了CBO。之前已經提到過,CBO在選擇目标SQL的執行計劃時,所用的判斷原則為成本,CBO會從目标SQL諸多可能的執行路徑中選擇一條成本值最小的執行路徑來作為其執行計劃,各條執行路徑的成本值是根據目标SQL語句所涉及的表、索引、列等相關對象的統計資訊計算出來的。
這裡的統計資訊是這樣的一組資料:它們存儲在Oracle資料庫的資料字典裡,且從多個次元描述了Oracle資料庫裡相關對象的實際資料量、實際資料分布等詳細資訊(關于統計資訊,會在"第5章 Oracle裡的統計資訊"中較長的描述)。
這裡的成本是指Oracle根據相關對象的統計資訊計算出來的一個值,它實際上代表了Oracle根據相關統計資訊估算出來的目标SQL的對應執行步驟的I/O、CPU和網絡資源的消耗量,這也就意味着Oracle資料庫裡的成本實際上就是對執行目标SQL所要耗費的I/O、CPU和網絡資源的一個估算值。
Oracle在執行目标SQL時需要耗費I/O和CPU,這很容易了解,但這裡的網絡資源消耗是指什麼?實際上,這裡的網絡資源消耗适用于那些使用了dblink的分布式目标SQL,CBO在解析該類SQL時知道在實際執行它們時所需要的資料并不全部在本地資料庫中(需要去遠端資料庫中取資料),是以此時的網絡資源消耗就會被CBO考慮在内。這裡需要注意的是,Oracle會把解析這種分布式目标SQL所需要考慮的網絡資源消耗折算成對等的I/O資源消耗,是以實際上你可以認為Oracle資料庫裡的成本僅僅依賴于執行目标SQL時所需要耗費的I/O和CPU資源。另外需要注意的是,在Oracle未引入系統統計資訊之前,CBO所計算的成本值實際上全部是基于I/O來估算的,隻有在Oracle引入了系統統計資訊之後,CBO所計算的成本值才真正依賴于目标SQL的I/O和CPU消耗(關于系統統計資訊,會在"第5章 Oracle裡的統計資訊"中較長的描述)。
從上述對CBO的介紹中我們可以看出:CBO會從目标SQL諸多可能的執行路徑中選擇一條成本值最小的執行路徑來作為其執行計劃,這也就意味着CBO會認為那些消耗系統I/O和CPU資源最少的執行路徑就是目前情況下的最佳選擇。注意,這裡的"消耗系統I/O和CPU資源"(即成本)的計算方法會随着優化器模式的不同而不同,這一點在"1.2.1 優化器的模式"中會詳細說明。
CBO在解析目标SQL時,首先會對目标SQL執行查詢轉換(關于查詢轉換,我們會在"第4章 Oracle裡的查詢轉換"中詳細說明);接下來,CBO會計算執行完查詢轉換這一步後得到的等價改寫SQL的諸多可能的執行路徑的成本,然後從上述諸多可能的執行路徑中選擇成本值最小的一條來作為原目标SQL的執行計劃;在得到了目标SQL的執行計劃後,接下來Oracle就會根據此執行計劃去實際執行該SQL,并将執行結果傳回給使用者。這裡需要說明的是,Oracle在對一條執行路徑計算成本時,并不一定會從頭到尾完整計算完,隻要Oracle在計算過程中發現算出來的部分成本值已經大于之前儲存下來的到目前為止的最小成本值,就會馬上中止對目前執行路徑成本值的計算,并轉而開始計算下一條新的執行路徑的成本。這個過程會一直持續下去,直到目标SQL的各個可能的執行路徑全部計算完畢或已達到預先定義好的待計算的執行路徑數量的門檻值。
接下來,介紹與CBO相關的一些基本概念。
1.1.2.1 集的勢
Cardinality是CBO特有的概念,直譯過來就是"集的勢",它是指指定集合所包含的記錄數,說白了就是指定結果集的行數。這個指定結果集是與目标SQL執行計劃的某個具體執行步驟相對應的,也就是說Cardinality實際上表示對目标SQL的某個具體執行步驟的執行結果所包含記錄數的估算。當然,如果是針對整個目标SQL,那麼此時的Cardinality就表示對該SQL最終執行結果所包含記錄數的估算。
Cardinality和成本值的估算是息息相關的,因為Oracle得到指定結果集所需要耗費的I/O資源可以近似看作随着該結果集所包含記錄數的遞增而遞增,是以某個執行步驟所對應的Cardinality的值越大,那麼它所對應的成本值往往也就越大,這個執行步驟所在執行路徑的總成本值也就會越大。
1.1.2.2 可選擇率(1)
可選擇率(Selectivity)也是CBO特有的概念,它是指施加指定謂詞條件後傳回結果集的記錄數占未施加任何謂詞條件的原始結果集的記錄數的比率。
可選擇率可以用如下的公式來表示:
從上述計算可選擇率的公式可以看出,可選擇率的取值範圍顯然是0~1,它的值越小,就表明可選擇性越好。毫無疑問,可選擇率為1時的可選擇性是最差的。
可選擇率和成本值的估算也是息息相關的,因為可選擇率的值越大,就意味着傳回結果集的Cardinality的值就越大,是以估算出來的成本值也就會越大。
實際上,CBO就是用可選擇率來估算對應結果集的Cardinality的,上述關于可選擇率的計算公式等價轉換後就可以用來估算Cardinality的值。這裡我們用"Original Cardinality"來表示未施加任何謂詞條件的原始結果集的記錄數,用"Computed Cardinality"來表示施加指定謂詞條件後傳回結果集的記錄數,CBO用來估算Cardinality的公式如下:
Computed Cardinality = Original Cardinality * Selectivity
雖然看起來可選擇率的計算公式很簡單,但實際上它的具體計算過程還是很複雜的,每一種具體情況都會有不同的計算公式。其中最簡單的情況是對目标列做等值查詢時可選擇率的計算。在目标列上沒有直方圖且沒有NULL值的情況下,用目标列做等值查詢的可選擇率是用如下公式來計算的:
我們現在再回過頭來看1.1.2節中提到的範例SQL 7:
對于範例SQL 7,我們來看一下CBO會如何計算列MGR的可選擇率和該SQL傳回結果集的Cardinality。
先把列MGR修改為NOT NULL:
SQL> alter table emp modify (mgr not null);
Table altered
然後在列MGR上建立一個名為IDX_EMP_MGR的單鍵值B樹索引:
SQL> create index idx_emp_mgr on emp(mgr);
表EMP的記錄數現在為13:
SQL> select count(*) from emp;
COUNT(*)
----------
13
列MGR的distinct值的數量也為13:
SQL> select count(distinct mgr) from emp;
COUNT(DISTINCTMGR)
------------------
13
現在使用DBMS_STATS包來對表EMP、表EMP的所有列、表EMP上的所有索引收集一下統計資訊(注意,這裡沒有收集直方圖統計資訊,關于DBMS_STATS包的用法,我們會在"第5章 Oracle裡的統計資訊"中詳細說明):
SQL> exec dbms_stats.gather_table_stats(ownname => 'SCOTT',tabname => 'EMP',estimate_percent => 100,cascade => true, method_opt=>'for all columns size 1',no_invalidate => false);
PL/SQL procedure successfully completed
接着執行範例SQL 7:
SQL> set linesize 800
SQL> set pagesize 900
SQL> set autotrace traceonly
SQL> select * from emp where mgr=7902;
1.1.2.2 可選擇率(2)
從Oracle 10g開始,Oracle在解析目标SQL時就會預設使用CBO。注意到上述執行計劃的顯示内容中有列Rows和列Cost (%CPU),這說明Oracle在解析範例SQL 7時确實使用的是CBO。這裡列Rows記錄的就是上述執行計劃中的每一個執行步驟所對應的Cardinality的值,列Cost (%CPU) 記錄的就是上述執行計劃中的每一個執行步驟所對應的成本值。
從上面顯示的内容可以看出,現在範例SQL 7的執行計劃走的是對索引IDX_EMP_MGR的索引範圍掃描。注意,Id = 2的執行步驟所對應的列Rows的值為1,這說明CBO評估出來以驅動查詢條件"access("MGR"=7902)"去通路索引IDX_EMP_MGR時傳回結果集的Cardinality的值是1;另外,Id = 0的執行步驟所對應的列Rows的值也為1,這說明CBO評估出來的範例SQL 7的最終執行結果所對應的Cardinality的值也是1。
這兩個值CBO是如何算出來的呢?
之前提到過:在目标列上沒有直方圖且沒有NULL值的情況下,用目标列做等值查詢的可選擇率的計算公式為Selectivity = ( 1 / NUM_DISTINCT )。現在列MGR沒有NULL值也沒有直方圖統計資訊,範例SQL 7的where條件是針對列MGR的等值查詢(等值查詢條件為"mgr=7902"),而列MGR的distinct值的數量是13,是以此時針對列MGR做等值查詢的可選擇率就是1/13。另外,之前也提到Cardinality的計算公式為Computed Cardinality = Original Cardinality * Selectivity,表EMP的記錄數為13,即此時Original Cardinality的值為13,那麼根據Cardinality的計算公式,上述針對列MGR做等值查詢的執行步驟所對應的Cardinality的值就是13 * 1/13 = 1,是以這就是CBO評估出來以驅動查詢條件"access("MGR"=7902)"去通路索引IDX_EMP_MGR時傳回結果集的Cardinality的值為1的原因。又因為where條件"mgr=7902"是範例SQL 7的唯一查詢條件,是以範例SQL 7的最終執行結果所對應的Cardinality的值也會是1。
我們現在把列MGR的值全部修改為7,902:
SQL> update emp set mgr=7902;
13 rows updated
SQL> commit;
Commit complete
然後重新收集一下統計資訊:
接着重新執行範例SQL 7:
SQL> select * from emp where mgr=7902;
從上述顯示内容可以看出,現在範例SQL 7的執行計劃走的依然是對索引IDX_EMP_MGR的索引範圍掃描,隻不過現在CBO評估出來以驅動查詢條件"access("MGR"=7902)"去通路索引IDX_EMP_MGR時傳回結果集的Cardinality和最終執行結果所對應的Cardinality的值均已從之前的1變為了現在的13。
這是很容易了解的。現在表EMP總的記錄數還是13,但列MGR的distinct值的數量已經從之前的13變為了1(即針對列MGR做等值查詢的可選擇率已經從之前的1/13變為了1),是以現在針對列MGR做等值查詢的執行步驟所對應的Cardinality和最終執行結果所對應的Cardinality的值就都會是13 * 1/1 = 13。
我們現在來構造之前在1.1.2節中提到的那種極端情況(表EMP的資料量為1000萬行,且這1000萬行記錄的列MGR的值均等于7,902)。注意,這裡并不用真正往表EMP裡插入1000萬行記錄,隻需要讓CBO認為表EMP的資料量為1000萬行就可以了(因為CBO計算成本時完全基于目标SQL的相關對象的統計資訊,是以這裡我們隻需要改一下表EMP和索引IDX_EMP_MGR的統計資訊,就可以讓CBO認為表EMP的資料量是1000萬行了):
1.1.2.2 可選擇率(3)
使用DBMS_STATS包将表EMP對應其資料量的統計資訊修改為1000萬:
SQL> exec dbms_stats.set_table_stats(ownname => 'SCOTT',tabname => 'EMP',numrows => 10000000,no_invalidate => false);
然後再将索引IDX_EMP_MGR對應其索引葉子塊數量的統計資訊修改為10萬:
SQL> exec dbms_stats.set_index_stats(ownname => 'SCOTT',indname => 'IDX_EMP_MGR',numlblks => 100000,no_invalidate => false);
再次執行範例SQL 7:
從上面顯示的内容中我們可以看出,範例SQL 7的執行計劃已經從之前的走對索引IDX_EMP_MGR的索引範圍掃描變為了現在的對表EMP的全表掃描,并且針對列MGR做等值查詢的執行步驟所對應的Cardinality和最終執行結果所對應的Cardinality的值已經從之前的13變為了現在的"10M"(即1000萬)。這就契合了我們之前提到的觀點:如果出現了上述這種極端的情況,CBO肯定會選擇全表掃描。
這裡為什麼Cardinality的值會變成1000萬呢?因為表EMP的記錄數(即Original Cardinality)在CBO的眼裡由之前的13變為了現在的1000萬,而Selectivity的值還是1,是以最後CBO估算出來的Cardinality的值就從之前的13變為了現在的1000萬(這裡用到的計算公式還是之前提到的Computed Cardinality = Original Cardinality * Selectivity)。
現在我們再來看一下在上述這種極端情況下RBO的選擇。在目前Session中将優化器模式修改為RULE,這表示在目前Session中啟用RBO:
SQL> alter session set optimizer_mode=rule;
然後再次執行範例SQL 7:
從上面顯示的内容中我們可以看出,範例SQL 7的執行計劃走的還是對索引IDX_EMP_MGR的索引範圍掃描,這也契合了我們之前提到的觀點:如果出現了上述這種極端的情況,RBO還是會選擇走對索引IDX_EMP_MGR的索引範圍掃描。
從對範例SQL 7的實際執行過程我們可以得到如下結論。
(1)RBO确實是靠寫死在Oracle資料庫代碼中的一系列固定的規則來決定目标SQL的執行計劃的,并沒有考慮目标SQL中所涉及的對象的實際資料量、實際資料分布等情況。而CBO則恰恰相反,CBO會根據反映目标SQL中相關對象的實際資料量、實際資料分布等情況的統計資訊來決定其執行計劃,這就意味着CBO選擇的執行計劃可能會随着目标SQL中所涉及的對象的統計資訊的變化而變化。CBO的這種變化是颠覆性的,這意味着隻要統計資訊相對準确,則用CBO來解析目标SQL會比在同等條件下用RBO來解析得到正确執行計劃的機率要高。
(2)Cardinality和Selectivity的值會直接影響CBO對于相關執行步驟成本值的估算,進而影響CBO對于目标SQL執行計劃的選擇。
1.1.2.3 可傳遞性(1)
可傳遞性(Transitivity)也是CBO特有的概念,它是CBO在圖1-1的查詢轉換中所做的第一件事情,其含義是指CBO可能會對原目标SQL做簡單的等價改寫,即在原目标SQL中加上根據該SQL現有的謂詞條件推算出來的新的謂詞條件,這麼做的目的是提供更多的執行路徑給CBO做選擇,進而增加得到更高效執行計劃的可能性。這裡需要注意的是,利用可傳遞性對目标SQL做簡單的等價改寫僅僅适用于CBO,RBO不會做這樣的事情。
在Oracle裡,可傳遞性又分為如下這三種情形。
1.簡單謂詞傳遞
比如原目标SQL中的謂詞條件是"t1.c1=t2.c1 and t1.c1=10",則CBO可能會在這個謂詞條件中額外地加上"t2.c1=10",即CBO可能會将原謂詞條件"t1.c1=t2.c1 and t1.c1=10"修改為"t1.c1=t2.c1 and t1.c1=10 and t2.c1=10"。改寫前後的謂詞條件顯然是等價的,因為如果t1.c1=t2.c1且t1.c1=10,那麼我們就可以推算出t2.c1也等于10。
2.連接配接謂詞傳遞
比如原目标SQL中的謂詞條件是"t1.c1=t2.c1 and t2.c1=t3.c1",則CBO可能會在這個謂詞條件中額外地加上"t1.c1=t3.c1",即CBO可能會将原謂詞條件"t1.c1=t2.c1 and t2.c1=t3.c1"修改為"t1.c1=t2.c1 and t2.c1=t3.c1 and t1.c1=t3.c1",同理,這裡改寫前後的謂詞條件也是等價的。
3.外連接配接謂詞傳遞
比如原目标SQL中的謂詞條件是"t1.c1=t2.c1(+) and t1.c1=10",則CBO可能會在這個謂詞條件中額外加上"t2.c1(+)=10",即CBO可能會将原謂詞條件"t1.c1=t2.c1(+) and t1.c1=10"修改為"t1.c1=t2.c1(+) and t1.c1=10 and t2.c1(+)=10"。關于外連接配接及上述SQL中關鍵字"(+)"的含義,我們會在"1.2.4.1.2 外連接配接"中較長的描述。
之前已經提到過:Oracle利用可傳遞性對目标SQL做簡單的等價改寫的目的是為了提供更多的執行路徑給CBO做選擇,進而增加得到更高效執行計劃的可能性。我們現在來看一個CBO利用可傳遞性對目标SQL做簡單等價改寫的執行個體:
建立兩個測試表T1和T2:
SQL> create table t1(c1 number,c2 varchar2(10));
SQL> create table t2(c1 number,c2 varchar2(10));
在表T2的列C1上建立一個名為IDX_T2的索引:
SQL> create index idx_t2 on t2(c1);
往表T1和T2中各插入一些資料,然後我們來看如下的範例SQL 8:
select t1.c1,t2.c2 from t1, t2
where t1.c1 = t2.c1 and t1.c1 = 10;
上述範例SQL 8的where條件是"t1.c1 = t2.c1 and t1.c1 = 10",并沒有針對表T2的列C1的簡單謂詞條件,是以按道理講應該是不能走我們剛才在表T2的列C1上建的索引IDX_T2的。
但實際情況是否如此呢?我們來執行一下範例SQL 8:
SQL> select t1.c1,t2.c2 from t1, t2
2 where t1.c1 = t2.c1 and t1.c1 = 10;
上面顯示的内容中Id = 5的執行步驟為"INDEX RANGE SCAN | IDX_T2",這說明Oracle現在還是走了對索引IDX_T2的索引範圍掃描。為什麼Oracle能夠這樣做?
注意到Id = 5的執行步驟所對應的驅動查詢條件為"access("T2"."C1"=10)",這說明Oracle在通路索引IDX_T2時用的驅動查詢條件是"t2.c1=10",但這個"t2.c1=10 "在範例SQL 8的原始SQL文本中并不存在。這就說明CBO此時确實利用可傳遞性對範例SQL 8做了簡單等價改寫,即CBO此時已經将範例SQL 8改寫成了如下的等價形式:
where t1.c1 = t2.c1 and t1.c1 = 10 and t2.c1 = 10;
這樣做的好處是顯而易見的--正是因為上述額外多出來的謂詞條件"and t2.c1 = 10",CBO在解析範例SQL 8時就多出了走索引IDX_T2和對應的執行路徑這種選擇,進而就增加了得到更高效執行計劃的可能性。
1.1.2.4 CBO的局限性
CBO誕生的初衷是為了解決RBO的先天缺陷,并且随着Oracle資料庫版本的不斷進化,CBO也越來越智能,越來越強悍,但這并不意味着CBO就完美無瑕,沒有任何缺陷了。這個世界上并沒有完美的事情,CBO同樣如此。
實際上,CBO的缺陷(或者說局限性)至少表現在如下幾個方面。
1.CBO會預設目标SQL語句where條件中出現的各個列之間是獨立的,沒有關聯關系
CBO會預設目标SQL語句where條件中出現的各個列之間是獨立的,沒有關聯關系,并且CBO會依據這個前提條件來計算組合可選擇率、Cardinality,進而來估算成本并選擇執行計劃。但這種前提條件并不總是正确的,在實際的應用中,目标SQL的各列之間有關聯關系的情況實際上并不罕見。在這種各列之間有關聯關系的情況下,如果還用之前的計算方法來計算目标SQL語句整個where條件的組合可選擇率,并用它來估算傳回結果集的Cardinality的話,那麼估算結果可能就會和實際結果有較大的偏差,導緻CBO選錯執行計劃。
目前可以用來緩解上述問題所帶來負面影響的方法是使用動态采樣或者多列統計資訊,但動态采樣的準确性取決于采樣資料的品質和采樣資料的數量,而多列統計資訊并不适用于多表之間有關聯關系的情形,是以這兩種解決方法都不能算是完美的解決方案。關于動态采樣和多列統計資訊,我們會在的"5.7 動态采樣"和"5.8 多列統計資訊"中分别予以詳細說明。
2.CBO會假設所有的目标SQL都是單獨執行的,并且互不幹擾
CBO會假設所有的目标SQL都是單獨執行、并且是互不幹擾的,但實際情況卻完全不是這樣。我們執行目标SQL時所需要通路的索引葉子塊、資料塊等可能由于之前執行的SQL而已經被緩存在Buffer Cache中,是以這次執行時也許不需要耗費實體I/O去相關的存儲上讀要通路的索引葉子塊、資料塊等,而隻需要去Buffer Cache中讀相關的緩存塊就可以了。是以,如果此時CBO還是按照目标SQL是單獨執行,不考慮緩存的方式去計算相關成本值的話,就可能會高估走相關索引的成本,進而可能會導緻選錯執行計劃。
3.CBO對直方圖統計資訊有諸多限制
CBO對直方圖統計資訊的限制展現在如下兩個方面。
(1)在Oracle 12c之前,Frequency類型的直方圖所對應的Bucket的數量不能超過254,這樣如果目标列的distinct值的數量超過254,Oracle就會使用Height Balanced類型的直方圖。對于Height Balanced類型的直方圖而言,因為Oracle不會記錄所有的nonpopular value的值,是以在此情況下CBO選錯執行計劃的機率會比對應的直方圖統計資訊是Frequency類型的情形要高。
(2)在Oracle資料庫裡,如果針對文本型的字段收集直方圖統計資訊,則Oracle隻會将該文本型字段的文本值的頭32位元組給取出來(實際上隻取頭15位元組)并将其轉換成一個浮點數,然後将該浮點數作為上述文本型字段的直方圖統計資訊存儲在資料字典裡。這種處理機制的先天缺陷就在于,對于那些超過32位元組的文本型字段,隻要對應記錄的文本值的頭32位元組相同,Oracle在收集直方圖統計資訊的時候就會認為這些記錄該字段的文本值是相同的,即使實際上它們并不相同。這種先天性的缺陷會直接影響CBO對相關文本型字段的可選擇率及傳回結果集的Cardinality的估算,進而就可能導緻CBO選錯執行計劃。
我們會在第5章的"5.5.3 直方圖"中對上述兩個限制予以詳細說明,這裡不再贅述。
4.CBO在解析多表關聯的目标SQL時,可能會漏選正确的執行計劃
在解析多表關聯的目标SQL時,雖然CBO會采取多種手段來避免漏選正确的執行計劃,但是這種漏選往往難以完全避免。因為随着多表關聯的目标SQL所包含表的數量的遞增,各表之間可能的連接配接順序會呈幾何級數增長,即該SQL各種可能的執行路徑的總數也會随之呈幾何級數增長。
假設多表關聯的目标SQL所包含表的數量為n,則該SQL各表之間可能的連接配接順序的總數就是n!(n的階乘)。這意味着包含10個表的目标SQL各表之間可能的連接配接順序總數為3,628,800,包含15個表的目标SQL各表之間可能的連接配接順序總數為1,307,674,368,000。
SQL> select 10*9*8*7*6*5*4*3*2*1 from dual;
10*9*8*7*6*5*4*3*2*1
--------------------
3628800
SQL> select 15*14*13*12*11*10*9*8*7*6*5*4*3*2*1 from dual;
15*14*13*12*11*10*9*8*7*6*5*4*
------------------------------
1307674368000
包含15個表的多表關聯的目标SQL在實際的應用系統中并不罕見,顯然CBO在處理這種類型的目标SQL時是不可能周遊其所有可能的情形的,否則解析該SQL的時間将會變得不可接受。
在Oracle 11gR2中,CBO在解析這種多表關聯的目标SQL時,所考慮的各個表連接配接順序的總和會受隐含參數_OPTIMIZER_MAX_PERMUTATIONS的限制,這意味着不管目标SQL在理論上有多少種可能的連接配接順序,CBO至多隻會考慮其中根據_OPTIMIZER_MAX_PERMUTATIONS計算出來的有限種可能。這同時也意味着隻要該目标SQL正确的執行計劃并不在上述有限種可能之中,則CBO一定會漏選正确的執行計劃。
雖然有上述這些局限性,但是瑕不掩瑜,CBO毫無疑問是目前情形下Oracle中解析目标SQL的不二選擇,并且我們完全有理由相信随着Oracle資料庫版本不斷的進化,CBO也會越來越完善。
1.2 優化器的基礎知識
接下來,介紹一些優化器的基礎知識,這些基礎知識中的絕大部分内容與優化器的類型是沒有關系的,也就是說它們中的絕大部分内容不僅适用于CBO,同樣也适用于RBO。
1.2.1 優化器的模式
優化器的模式用于決定在Oracle中解析目标SQL時所用優化器的類型,以及決定當使用CBO時計算成本值的側重點。這裡的"側重點"是指當使用CBO來計算目标SQL各條執行路徑的成本值時,計算成本值的方法會随着優化器模式的不同而不同。
在Oracle資料庫中,優化器的模式是由參數OPTIMIZER_MODE的值來決定的,OPTIMIZER_MODE的值可能是RULE、CHOOSE、FIRST_ROWS_n(n = 1, 10, 100, 1000)、FIRST_ROWS或ALL_ROWS。
OPTIMIZER_MODE的各個可能的值的含義為如下所示。
1.RULE
RULE表示Oracle将使用RBO來解析目标SQL,此時目标SQL中所涉及的各個對象的統計資訊對于RBO來說将沒有任何作用。
2.CHOOSE
CHOOSE是Oracle 9i中OPTIMIZER_MODE的預設值,它表示Oracle在解析目标SQL時到底是使用RBO還是使用CBO取決于該SQL中所涉及的表對象是否有統計資訊。具體來說就是:隻要該SQL中所涉及的表對象中有一個有統計資訊,那麼Oracle在解析該SQL時就會使用CBO;如果該SQL中所涉及的所有表對象均沒有統計資訊,那麼此時Oracle就會使用RBO。
3.FIRST_ROWS_n(n = 1, 10, 100, 1000)
這裡FIRST_ROWS_n(n = 1, 10, 100, 1000)可以是FIRST_ROWS_1、FIRST_ROWS_10、FIRST_ROWS_100和FIRST_ROWS_1000中的任意一個值,其含義是指當OPTIMIZER_MODE的值為FIRST_ROWS_n(n = 1, 10, 100, 1000)時,Oracle會使用CBO來解析目标SQL,且此時CBO在計算該SQL的各條執行路徑的成本值時的側重點在于以最快的響應速度傳回頭n(n = 1, 10, 100, 1000)條記錄。
我們在1.1.2節中提到過:CBO會從目标SQL諸多可能的執行路徑中選擇一條成本值最小的執行路徑來作為其執行計劃,這也就意味着CBO會認為那些消耗系統I/O和CPU資源最少的執行路徑就是目前情形下的最佳選擇。
那麼當OPTIMIZER_MODE的值為FIRST_ROWS_n(n = 1, 10, 100, 1000)時,是否意味着CBO在選擇執行計劃時所采用的原則将不再是選擇成本值最小的執行路徑(即消耗系統I/O和CPU資源最少的執行路徑),而是選擇那些能夠以最快的響應速度傳回頭n(n = 1, 10, 100, 1000)條記錄所對應的執行路徑?
表面上看确實是這樣,但實際上Oracle采用了一種變通的辦法使得CBO在選擇執行計劃時所采用的總原則(成本值最小)依然沒有發生變化。這種變通的辦法是什麼呢?很簡單,當OPTIMIZER_MODE的值為FIRST_ROWS_n(n = 1, 10, 100, 1000)時,Oracle會把那些能夠以最快的響應速度傳回頭n(n = 1, 10, 100, 1000)條記錄所對應的執行步驟的成本值修改成一個很小的值(遠遠小于預設情況下CBO對同樣執行步驟所計算出的成本值)。這樣Oracle就既沒有違背CBO選取執行計劃的總原則(成本值最小),同時又兼顧了FIRST_ROWS_n(n = 1, 10, 100, 1000)的含義。
4.FIRST_ROWS
FIRST_ROWS是一個在Oracle 9i中就已經過時的參數,它表示Oracle在解析目标SQL時會聯合使用CBO和RBO。這裡聯合使用CBO和RBO的含義是指在大多數情況下,FIRST_ROWS還是會使用CBO來解析目标SQL,且此時CBO在計算該SQL的各條執行路徑的成本值時的側重點在于以最快的響應速度傳回頭幾條記錄(類似于FIRST_ROWS_n);但是,當出現了一些特定情況時,FIRST_ROWS轉而會使用RBO中的一些内置的規則來選取執行計劃而不再考慮成本。比如當OPTIMIZER_MODE的值為FIRST_ROWS時有一個内置的規則,就是如果Oracle發現能用相關的索引來避免排序,則Oracle就會選擇該索引所對應的執行路徑而不再考慮成本,這顯然是不合理的。與之相對應的,在OPTIMIZER_MODE的值為FIRST_ROWS的情形下,你會發現索引全掃描出現的機率會比之前有所增加,這是因為走索引全掃描能夠避免排序的緣故。
5.ALL_ROWS
ALL_ROWS是Oracle 10g以及後續Oracle資料庫版本中OPTIMIZER_MODE的預設值,它表示Oracle會使用CBO來解析目标SQL,且此時CBO在計算該SQL的各條執行路徑的成本值時的側重點在于最佳的吞吐量(即最小的系統I/O和CPU資源的消耗量)。
之前我們在1.1.2節中已經提到過:"消耗系統I/O和CPU資源"(即成本)的計算方法會随着優化器模式的不同而不同。這裡我們怎麼來了解成本的計算方法會随着優化器模式的不同而不同?
實際上,成本的計算方法随着優化器模式的不同而不同,主要展現在ALL_ROWS和FIRST_ROWS_n(n = 1, 10, 100, 1000)對成本值計算方法的影響上。當優化器模式為ALL_ROWS時,CBO計算成本的側重點在于最佳的吞吐量;而當優化器模式為FIRST_ROWS_n(n = 1, 10, 100, 1000)時,CBO計算成本的側重點會變為以最快的響應速度傳回頭n(n = 1, 10, 100, 1000)條記錄。這意味着同樣的執行步驟,在優化器模式為ALL_ROWS時和FIRST_ROWS_n(n = 1, 10, 100, 1000)時CBO分别計算出來的成本值會存在巨大的差異,這也就意味着優化器的模式對CBO計算成本(進而對CBO選擇執行計劃)有着決定性的影響!我們在"1.3 優化器模式對CBO計算成本帶來巨大影響的執行個體"中會介紹一個由于優化器模式的不當設定而導緻CBO認為全表掃描一個700多萬行資料的大表的成本值僅為2,進而直接導緻CBO選錯執行計劃的執行個體。
1.2.2 結果集
結果集(Row Source)是指包含指定執行結果的集合。對于優化器而言(無論是RBO還是CBO),結果集和目标SQL執行計劃的執行步驟相對應,一個執行步驟所産生的執行結果就是該執行步驟所對應的輸出結果集。
對于目标SQL的執行計劃而言,其中某個執行步驟的輸出結果就是該執行步驟所對應的輸出結果集,同時,該執行步驟所對應的輸出結果集可能就是下一個執行步驟的輸入結果集。這樣一步一步執行下來,伴随的就是結果集在各個執行步驟之間的傳遞,等目标SQL執行計劃的各個執行步驟全部執行完畢後,最後的輸出結果集就是該SQL最終的執行結果。
對于RBO而言,我們在對應的執行計劃中看不到對相關執行步驟所對應的結果集的描述,雖然結果集的概念對于RBO來說也同樣适用。
對于CBO而言,對應執行計劃中的列(Rows)反映的就是CBO對于相關執行步驟所對應輸出結果集的記錄數(即Cardinality)的估算值。
我們來看如下使用CBO的執行計劃範例:
對于上述使用CBO的執行計劃而言,我們将Id =1、2的執行步驟所對應的輸出結果集分别記為輸出結果集1和輸出結果集2。這裡Oracle會先執行Id = 2的執行步驟。注意到上述Id = 2的執行步驟所對應的列Rows的值為13,這說明CBO對輸出結果集2的Cardinality的估算值為13。同時,輸出結果集2又會作為Id = 1的執行步驟的輸入結果集,注意到上述Id = 1的執行步驟所對應的列Rows的值也為13,這說明CBO對輸出結果集1的Cardinality的估算值也為13。同時我們可以看到Id = 0的執行步驟為"SELECT STATEMENT",這說明輸出結果集1就是上述整個目标SQL的最終執行結果。
The following abbreviations are used by optimizer trace.
CBQT - cost-based query transformation
JPPD - join predicate push-down
OJPPD - old-style (non-cost-based) JPPD
FPD - filter push-down
PM - predicate move-around
CVM - complex view merging
SPJ - select-project-join
SJC - set join conversion
SU - subquery unnesting
OBYE - order by elimination
OST - old style star transformation
ST - new (cbqt) star transformation
CNT - count(col) to count(*) transformation
JE - Join Elimination
JF - join factorization
SLP - select list pruning
DP - distinct placement
qb - query block
LB - leaf blocks
DK - distinct keys
LB/K - average number of leaf blocks per key
DB/K - average number of data blocks per key
CLUF - clustering factor
NDV - number of distinct values
Resp - response cost
Card - cardinality
Resc - resource cost
NL - nested loops (join)
SM - sort merge (join)
HA - hash (join)
CPUSPEED - CPU Speed
IOTFRSPEED - I/O transfer speed
IOSEEKTIM - I/O seek time
SREADTIM - average single block read time
MREADTIM - average multiblock read time
MBRC - average multiblock read count
MAXTHR - maximum I/O system throughput
SLAVETHR - average slave I/O throughput
dmeth - distribution method
1: no partitioning required
2: value partitioned
4: right is random (round-robin)
128: left is random (round-robin)
8: broadcast right and partition left
16: broadcast left and partition right
32: partition left using partitioning of right
64: partition right using partitioning of left
256: run the join in serial
0: invalid distribution method
sel - selectivity
ptn - partition
About Me ............................................................................................................................... ● 本文整理自網絡 ● QQ群:230161599 微信群:私聊 ● 聯系我請加QQ好友(646634621),注明添加緣由 ● 于 2017-06-02 09:00 ~ 2017-06-30 22:00 在魔都完成 ● 文章内容來源于小麥苗的學習筆記,部分整理自網絡,若有侵權或不當之處還請諒解 ● 版權所有,歡迎分享本文,轉載請保留出處 拿起手機使用微信用戶端掃描下邊的左邊圖檔來關注小麥苗的微信公衆号:xiaomaimiaolhr,掃描右邊的二維碼加入小麥苗的QQ群,學習最實用的資料庫技術。 <a target="_blank" href="http://wpa.qq.com/msgrd?v=3&uin=646634621&site=qq&menu=yes"></a>