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的訪問路徑,官方文檔作了詳細介紹:服務器
RBO Path 1: Single Row by Rowid網絡
RBO Path 2: Single Row by Cluster Joinsession
RBO Path 3: Single Row by Hash Cluster Key with Unique or Primary Keyapp
RBO Path 4: Single Row by Unique or Primary Keydom
RBO Path 5: Clustered Join分佈式
RBO Path 6: Hash Cluster Keyide
RBO Path 7: Indexed Cluster Key
RBO Path 8: Composite Index
RBO Path 9: Single-Column Indexes
RBO Path 10: Bounded Range Search on Indexed Columns
RBO Path 11: Unbounded Range Search on Indexed Columns
RBO Path 12: Sort Merge Join
RBO Path 13: MAX or MIN of Indexed Column
RBO Path 14: ORDER BY on Indexed Column
RBO Path 15: Full Table Scan
CBO是一種比RBO更加合理、可靠的優化器,它是從ORACLE 8中開始引入,但到ORACLE 9i 中才逐漸成熟,在ORACLE 10g中徹底取代RBO, CBO是計算各類可能「執行計劃」的「代價」,即COST,從中選用COST最低的執行方案,做爲實際運行方案。它依賴數據庫對象的統計信息,統計信息的準確與否會影響CBO作出最優的選擇。若是對一次執行SQL時發現涉及對象(表、索引等)沒有被分析、統計過,那麼ORACLE會採用一種叫作動態採樣的技術,動態的收集表和索引上的一些數據信息。
關於RBO與CBO,我有個形象的比喻:大數據時代到來之前,作生意或許憑藉多年累計下來的經驗(RBO)就可以很好的作出決策,跟隨市場變化。可是大數據時代,若是作生意仍是靠之前憑經驗作決策,而不是靠大數據、數據分析、數據挖掘作決策,那麼就有可能作出錯誤的決策。這也就是愈來愈多的公司對BI、數據挖掘愈來愈重視的緣故,像電商、遊戲、電信等行業都已經大規模的應用,之前在一家遊戲公司數據庫部門作BI分析,挖掘潛在消費用戶簡直無所不及。至今映像頗深。
CBO與RBO的優劣
CBO優於RBO是由於RBO是一種呆板、過期的優化器,它只認規則,對數據不敏感。畢竟規則是死的,數據是變化的,這樣生成的執行計劃每每是不可靠的,不是最優的,CBO因爲RBO能夠從不少方面體現。下面請看一個例子,此案例來自於《讓Oracle跑得更快》。
SQL> create table test as select 1 id ,object_name from dba_objects;
Table created.
SQL> create index idx_test on test(id);
Index created.
SQL> update test set id=100 where rownum =1;
1 row updated.
SQL> select id, count(1) from test group by id;
ID COUNT(1)
---------- ----------
100 1
1 50314
從上面能夠看出,該測試表的數據分佈極其不均衡,ID=100的記錄只有一條,而ID=1的記錄有50314條。咱們先看看RBO下兩條SQL的執行計劃.
SQL> select /*+ rule */ * from test where id =100;
Execution Plan
----------------------------------------------------------
Plan hash value: 2473784974
------------------------------------------------
| Id | Operation | Name |
------------------------------------------------
| 0 | SELECT STATEMENT | |
| 1 | TABLE ACCESS BY INDEX ROWID| TEST |
|* 2 | INDEX RANGE SCAN | IDX_TEST |
------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("ID"=100)
Note
-----
- rule based optimizer used (consider using cbo)
Statistics
----------------------------------------------------------
1 recursive calls
0 db block gets
3 consistent gets
0 physical reads
0 redo size
588 bytes sent via SQL*Net to client
469 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed
SQL>
SQL> select /*+ rule */ * from test where id=1;
50314 rows selected.
Execution Plan
----------------------------------------------------------
Plan hash value: 2473784974
------------------------------------------------
| Id | Operation | Name |
------------------------------------------------
| 0 | SELECT STATEMENT | |
| 1 | TABLE ACCESS BY INDEX ROWID| TEST |
|* 2 | INDEX RANGE SCAN | IDX_TEST |
------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("ID"=1)
Note
-----
- rule based optimizer used (consider using cbo)
Statistics
----------------------------------------------------------
1 recursive calls
0 db block gets
7012 consistent gets
97 physical reads
0 redo size
2243353 bytes sent via SQL*Net to client
37363 bytes received via SQL*Net from client
3356 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
50314 rows processed
從執行計劃能夠看出,RBO的執行計劃讓人有點失望,對於ID=1,幾乎全部的數據所有符合謂詞條件,走索引只能增長額外的開銷(由於ORACLE首先要訪問索引數據塊,在索引上找到了對應的鍵值,而後按照鍵值上的ROWID再去訪問表中相應數據),既然咱們幾乎要訪問全部表中的數據,那麼全表掃描天然是最優的選擇。而RBO選擇了錯誤的執行計劃。能夠對比一下CBO下SQL的執行計劃,顯然它對數據敏感,執行計劃及時的根據數據量作了調整,當查詢條件爲1時,它走全表掃描;當查詢條件爲100時,它走區間索引掃描。以下所示:
SQL> select * from test where id=1;
50314 rows selected.
Execution Plan
----------------------------------------------------------
Plan hash value: 1357081020
--------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 49075 | 3786K| 52 (2)| 00:00:01 |
|* 1 | TABLE ACCESS FULL| TEST | 49075 | 3786K| 52 (2)| 00:00:01 |
--------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter("ID"=1)
Note
-----
- dynamic sampling used for this statement
Statistics
----------------------------------------------------------
32 recursive calls
0 db block gets
3644 consistent gets
0 physical reads
0 redo size
1689175 bytes sent via SQL*Net to client
37363 bytes received via SQL*Net from client
3356 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
50314 rows processed
SQL> select * from test where id =100;
Execution Plan
----------------------------------------------------------
Plan hash value: 2473784974
----------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
----------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 79 | 2 (0)| 00:00:01 |
| 1 | TABLE ACCESS BY INDEX ROWID| TEST | 1 | 79 | 2 (0)| 00:00:01 |
|* 2 | INDEX RANGE SCAN | IDX_TEST | 1 | | 1 (0)| 00:00:01 |
----------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("ID"=100)
Note
-----
- dynamic sampling used for this statement
Statistics
----------------------------------------------------------
9 recursive calls
0 db block gets
73 consistent gets
0 physical reads
0 redo size
588 bytes sent via SQL*Net to client
469 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed
SQL>
僅此一項就能夠看出爲何ORACLE極力推薦使用CBO,從ORACLE 10g開始不支持RBO的緣故。所謂長江後浪推前浪,前浪死在沙灘上。
CBO知識點的總結
CBO優化器根據SQL語句生成一組可能被使用的執行計劃,估算出每一個執行計劃的代價,並調用計劃生成器(Plan Generator)生成執行計劃,比較執行計劃的代價,最終選擇選擇一個代價最小的執行計劃。查詢優化器由查詢轉換器(Query Transform)、代價估算器(Estimator)和計劃生成器(Plan Generator)組成。
CBO優化器組件
CBO由如下組件構成:
· 查詢轉化器(Query Transformer)
查詢轉換器的做用就是等價改變查詢語句的形式,以便產生更好的執行計劃。它決定是否重寫用戶的查詢(包括視圖合併、謂詞推動、非嵌套子查詢/子查詢反嵌套、物化視圖重寫),以生成更好的查詢計劃。
The input to the query transformer is a parsed query, which is represented by a set of
query blocks. The query blocks are nested or interrelated to each other. The form of the
query determines how the query blocks are interrelated to each other. The main
objective of the query transformer is to determine if it is advantageous to change the
form of the query so that it enables generation of a better query plan. Several different
query transformation techniques are employed by the query transformer, including:
■ View Merging
■ Predicate Pushing
■ Subquery Unnesting
■ Query Rewrite with Materialized Views
Any combination of these transformations can be applied to a given query.
· 代價評估器(Estimator)
評估器經過複雜的算法結合來統計信息的三個值來評估各個執行計劃的整體成本:選擇性(Selectivity)、基數(Cardinality)、成本(Cost)
計劃生成器會考慮可能的訪問路徑(Access Path)、關聯方法和關聯順序,生成不一樣的執行計劃,讓查詢優化器從這些計劃中選擇出執行代價最小的一個計劃。
· 計劃生成器(Plan Generator)
計劃生成器就是生成大量的執行計劃,而後選擇其整體代價或整體成本最低的一個執行計劃。
因爲不一樣的訪問路徑、鏈接方式和鏈接順序能夠組合,雖然以不一樣的方式訪問和處理數據,可是能夠產生一樣的結果
下圖是我本身爲了加深理解,用工具畫的圖
查看ORACLE優化器
SQL> show parameter optimizer_mode;
NAME TYPE VALUE
--------------------------- ----------- -----------------
optimizer_mode string ALL_ROWS
修改ORACLE優化器
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的方式。
If OPTIMIZER_MODE=CHOOSE, if statistics do not exist, and if you do not add hints to SQL statements, then SQL statements use the RBO. You can use the RBO to access both relational data and object types. If OPTIMIZER_MODE=FIRST_ROWS, FIRST_ROWS_n, or ALL_ROWS and no statistics exist, then the CBO uses default statistics. Migrate existing applications to use the cost-based approach.
First Rows:它與Choose方式是相似的,所不一樣的是當一個表有統計信息時,它將是以最快的方式返回查詢的最早的幾行,從整體上減小了響應時間。
All Rows: 10g中的默認值,也就是咱們所說的Cost的方式,當一個表有統計信息時,它將以最快的方式返回表的全部的行,從整體上提升查詢的吞吐
雖然Oracle 10g中再也不支持RBO,Oracle 10g官方文檔關於optimizer_mode參數的只有first_rows和all_rows.可是依然能夠設置 optimizer_mode爲rule或choose,估計是ORACLE爲了過渡或向下兼容考慮。以下所示。
系統級別
SQL> alter system set optimizer_mode=rule scope=both;
System altered.
SQL> show parameter optimizer_mode
NAME TYPE VALUE
-------------------------------- ----------- -----------------------
optimizer_mode string RULE
會話級別
會話級別修改優化器模式,只對當前會話有效,其它會話依然使用系統優化器模式。
SQL> alter session set optimizer_mode=first_rows_100;
Session altered.
語句級別
語句級別經過使用提示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:
在表EMP_TEMP的列MGR和DEPTNO上分別建立兩個名爲IDX_MGR_TEMP和IDX_DEPTNO_TEMP的索引:
咱們來看一下以下的範例SQL 1:
對於範例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 1:
注意到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:
執行範例SQL 2:
注意,此時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:
再從新建立上述索引IDX_MGR_TEMP:
而後再次執行範例SQL 1:
注意,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 3:
對於範例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:
1.1.1 基於規則的優化器(4)
從上面顯示的內容能夠看出,如今範例SQL 3的執行計劃走的是排序合併鏈接,且驅動表確實是表EMP_TEMP1。
注意,從嚴格意義上來講,排序合併鏈接並無驅動表和被驅動表的概念,這裏只是爲了方便闡述而人爲地給排序合併鏈接添加了上述概念。
將範例SQL 3中的表EMP_TEMP和EMP_TEMP1在該SQL的SQL文本中的位置換一下,即造成了以下形式的範例SQL 4:
按照前面介紹的知識,如今若是再執行範例SQL 4的話,那麼排序合併鏈接的驅動表應該會變成表EMP_TEMP。
咱們來驗證一下。執行範例SQL 4:
從上面顯示的內容能夠看出,如今範例SQL 4的執行計劃走的也是排序合併鏈接,且驅動表確實已經由以前的表EMP_TEMP1變爲了如今的表EMP_TEMP。這說明咱們確實使RBO改變了執行計劃,也說明當目標SQL有兩條或者兩條以上的執行路徑的等級值相同時,咱們確實能夠經過改變目標SQL中所涉及的各個對象在該SQL文本中出現的前後順序來影響RBO對於其執行計劃的選擇。
注意,這種位置的前後順序對於目標SQL執行計劃的影響是有前提條件的,那就是僅憑各條執行路徑等級值的大小RBO難以選擇執行計劃,也就是說該目標SQL必定有兩條或者兩條以上執行路徑的等級值相同。換句話說,若是RBO僅憑各條執行路徑等級值的大小就能夠選擇目標SQL的執行計劃,那麼不管怎麼調整相關對象在該SQL的SQL文本中的位置,對於該SQL最終的執行計劃都不會有任何影響。
咱們來驗證一下上述結論。看看以下的範例SQL 5:
對於範例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:
1.1.1 基於規則的優化器(5)
從上面顯示的內容能夠看出,如今範例SQL 5的執行計劃走的是嵌套循環鏈接,且驅動表是表EMP_TEMP。
咱們將範例SQL 5中的表EMP和EMP_TEMP在該SQL的SQL文本中的位置換一下,即造成了以下形式的範例SQL 6:
而後執行範例SQL 6:
從上面顯示的內容能夠看出,如今範例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:
對於範例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的公式以下:
雖然看起來可選擇率的計算公式很簡單,但實際上它的具體計算過程仍是很複雜的,每一種具體狀況都會有不一樣的計算公式。其中最簡單的狀況是對目標列作等值查詢時可選擇率的計算。在目標列上沒有直方圖且沒有NULL值的狀況下,用目標列作等值查詢的可選擇率是用以下公式來計算的:
咱們如今再回過頭來看1.1.2節中提到的範例SQL 7:
對於範例SQL 7,咱們來看一下CBO會如何計算列MGR的可選擇率和該SQL返回結果集的Cardinality。
先把列MGR修改成NOT NULL:
而後在列MGR上建立一個名爲IDX_EMP_MGR的單鍵值B樹索引:
表EMP的記錄數如今爲13:
列MGR的distinct值的數量也爲13:
如今使用DBMS_STATS包來對錶EMP、表EMP的全部列、表EMP上的全部索引收集一下統計信息(注意,這裏沒有收集直方圖統計信息,關於DBMS_STATS包的用法,咱們會在"第5章 Oracle裏的統計信息"中詳細說明):
接着執行範例SQL 7:
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 7:
從上述顯示內容能夠看出,如今範例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萬:
而後再將索引IDX_EMP_MGR對應其索引葉子塊數量的統計信息修改成10萬:
再次執行範例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 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:
在表T2的列C1上建立一個名爲IDX_T2的索引:
往表T1和T2中各插入一些數據,而後咱們來看以下的範例SQL 8:
上述範例SQL 8的where條件是"t1.c1 = t2.c1 and t1.c1 = 10",並無針對表T2的列C1的簡單謂詞條件,因此按道理講應該是不能走咱們剛纔在表T2的列C1上建的索引IDX_T2的。
但實際狀況是否如此呢?咱們來執行一下範例SQL 8:
上面顯示的內容中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改寫成了以下的等價形式:
這樣作的好處是顯而易見的--正是由於上述額外多出來的謂詞條件"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。
包含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_一、FIRST_ROWS_十、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 =一、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 transformationJPPD - join predicate push-downOJPPD - old-style (non-cost-based) JPPDFPD - filter push-downPM - predicate move-aroundCVM - complex view mergingSPJ - select-project-joinSJC - set join conversionSU - subquery unnestingOBYE - order by eliminationOST - old style star transformationST - new (cbqt) star transformationCNT - count(col) to count(*) transformationJE - Join EliminationJF - join factorizationSLP - select list pruningDP - distinct placementqb - query blockLB - leaf blocksDK - distinct keysLB/K - average number of leaf blocks per keyDB/K - average number of data blocks per keyCLUF - clustering factorNDV - number of distinct valuesResp - response costCard - cardinalityResc - resource costNL - nested loops (join)SM - sort merge (join)HA - hash (join)CPUSPEED - CPU Speed IOTFRSPEED - I/O transfer speedIOSEEKTIM - I/O seek timeSREADTIM - average single block read timeMREADTIM - average multiblock read timeMBRC - average multiblock read countMAXTHR - maximum I/O system throughputSLAVETHR - average slave I/O throughputdmeth - 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 methodsel - selectivityptn - partition