目錄
一、什么是執行計划
二、如何查看執行計划
三、如何讀懂執行計划
1. 執行順序的原則
2. 執行計划中字段解釋
3. 謂詞說明
4. JOIN方式
4.1 HASH JOIN(散列連接)
4.2 SORT MERGE JOIN(排序合並連接)
4.3 NESTED LOOP(嵌套循環連接)
5. 表訪問方式
5.1 全表掃描(Full Table Scans, FTS)
5.2 通過ROWID訪問表(table access by ROWID)
5.3 索引掃描
一、什么是執行計划
執行計划是一條查詢語句在Oracle中的執行過程或訪問路徑的描述。即是對一個查詢任務,做出一份怎樣去完成任務的詳細方案。
如果要分析某條SQL的性能問題,通常我們要先看SQL的執行計划,看看SQL的每一步執行是否存在問題。 看懂執行計划也就成了SQL優化的先決條件。 通過執行計划定位性能問題,定位后就通過建立索引、修改SQL等解決問題。
二、如何查看執行計划
查看Oracle執行計划的方式有很多,常見的客戶端工具如PL/SQL Developer,Navicat,Toad都支持查看解釋計划,接下來就通過PL/SQL來學習如何查看執行計划。
在PL/SQL上按F5即可查看執行計划。
PL/SQL默認的設置只能查看耗費、基礎、字節等基本指標,若要更深層次地分析執行計划,需要更多的數據支持,幸好PL/SQL提供了豐富的分析指標,我們可以在PL/SQL的“首選項”里面進行設置。

三、如何讀懂執行計划
SELECT T1.CODE, T1.EVENTTYPE, T1.STATUS AS EXC_STATUS, T1.EVENTSTARTTIME, T1.EVENTENDTIME, T1.EXCEPTION_TIMES, T2.STATUS AS E_STATUS FROM E_EXCEPTION T1 INNER JOIN E_INFO T2 ON T1.CODE = T2.CODE WHERE T1.STATUS = 0 AND T1.EVENTTYPE = 'LOST_S' AND T1.EVENTENDTIME IS NULL AND T2.STATUS = 1 ORDER BY T1.EVENTSTARTTIME


上圖是某條SQL語句的執行計划示意圖,圖中的左右箭頭可以演示該SQL的執行順序,當然,這是PL/SQL提供的便利操作,我們還是應該熟悉SQL執行順序的原則。
1. 執行順序的原則
執行順序的原則是:由上至下,從右向左。
由上至下:在執行計划中一般含有多個節點,相同級別(或並列)的節點,靠上的優先執行,靠下的后執行。
從右向左:在某個節點下還存在多個子節點,先從最靠右的子節點開始執行。
一般按縮進長度來判斷,縮進最大的最先執行,如果有2行縮進一樣,那么就先執行上面的。
2. 執行計划中字段解釋
| Description | 操作描述,即當前操作的內容 |
| 對象所有者(Object Owner) | 當前操作對象的所有者 |
| 對象名稱(Object Name) | 當前操作的對象,對象可能是一個表,一個索引,也可能是一個子查詢 |
| 耗費(Cost) | 沒有單位,是一個相對值,是SQL以CBO方式解析執行計划時,供ORACLE來評估CBO成本,選擇執行計划用的。沒有明確的含義,但是在對比是就非常有用 公式:COST=(Single Block I/O COST + MultiBlock I/O Cost + CPU Cost)/ Sreadtim |
| 基數(Cardinality) | Oracle估計當前操作的返回結果集,10g版本以前叫做基數(Cardinality),10g版本以后叫做行數(Rows) |
| 字節(Bytes) | 執行該步驟后返回的字節數 |
| CPU耗費(Cost(CPU)) | 執行到該步驟的一個執行成本,用於說明SQL執行的代價 |
| Id | 一個序號,但不是執行的先后順序。執行的先后根據縮進來判斷 |
| IO耗費(Cost(IO)) | IO層面的執行成本 |
| 時間(Time) | Oracle 估計當前操作的時間 |
在看執行計划的時候,除了看執行計划本身,還需要看謂詞和統計信息。 通過整體信息來判斷SQL效率。
3. 謂詞說明
訪問謂詞(Access predicate)
- 通過某種方式定位了需要的數據,然后讀取出這些結果集,叫做Access
- 表示這個謂詞條件的值將會影響數據的訪問路勁(表還是索引)
過濾器謂詞(Filter predicate)
- 把所有的數據都訪問了,然后過濾掉不需要的數據,這種方式叫做Filter
- 表示謂詞條件的值不會影響數據的訪問路勁,只起過濾的作用
在謂詞中主要注意Access,要考慮謂詞的條件,使用的訪問路徑是否正確。
4. JOIN方式
4.1 HASH JOIN(散列連接)
Hash join散列連接是CBO 做大數據集連接時的常用方式,優化器使用兩個表中較小的表(通常是小一點的那個表或數據源)利用連接鍵(JOIN KEY)在內存中建立散列表,將列數據存儲到hash列表中,然后掃描較大的表,同樣對JOIN KEY進行HASH后探測散列表,找出與散列表匹配的行。需要注意的是:如果HASH表太大,無法一次構造在內存中,則分成若干個partition,寫入磁盤的temporary segment,則會多一個寫的代價,會降低效率。
這種方式適用於較小的表完全可以放於內存中的情況,這樣總成本就是訪問兩個表的成本之和。但是在表很大的情況下並不能完全放入內存,這時優化器會將它分割成若干不同的分區,不能放入內存的部分就把該分區寫入磁盤的臨時段,此時要有較大的臨時段從而盡量提高I/O 的性能。
可以用USE_HASH(table_name1 table_name2)提示來強制使用散列連接。
適用情況:
- Hash join在兩個表的數據量差別很大的時候。
4.2 SORT MERGE JOIN(排序合並連接)
Merge Join 是先將關聯表的關聯列各自做排序,然后從各自的排序表中抽取數據,到另一個排序表中做匹配。
因為merge join需要做更多的排序,所以消耗的資源更多。 通常來講,能夠使用merge join的地方,hash join都可以發揮更好的性能,即散列連接的效果都比排序合並連接要好。然而如果行源已經被排過序,在執行排序合並連接時不需要再排序了,這時排序合並連接的性能會優於散列連接。
可以使用USE_MERGE(table_name1 table_name2)來強制使用排序合並連接.
適用情況:
- RBO模式;
- 不等價關聯(>,<,>=,<=,<>);
- HASH_JOIN_ENABLED=false;
- 用在沒有索引,並且數據已經排序的情況。
4.3 NESTED LOOP(嵌套循環連接)
Nested loops 工作方式是循環從一張表中讀取數據(驅動表outer table),然后訪問另一張表(被查找表 inner table,通常有索引)。驅動表中的每一行與inner表中的相應記錄JOIN。類似一個嵌套的循環。
對於被連接的數據子集較小的情況,嵌套循環連接是個較好的選擇。在嵌套循環中,內表被外表驅動,外表返回的每一行都要在內表中檢索找到與它匹配的行,因此整個查詢返回的結果集不能太大(大於1 萬不適合),要把返回子集較小表的作為外表(CBO 默認外表是驅動表),而且在內表的連接字段上一定要有索引。當然也可以用ORDERED 提示來改變CBO默認的驅動表。
使用USE_NL(table_name1 table_name2)可是強制CBO 執行嵌套循環連接。
適用情況:
- 驅動表的記錄集比較小(<10000)而且inner表需要有有效的訪問方法(Index),並且索引選擇性較好的時候;
- JOIN的順序很重要,驅動表的記錄集一定要小,返回結果集的響應時間是最快的。
5. 表訪問方式
5.1 全表掃描(Full Table Scans, FTS)
全表掃描(Full Table Scans,又叫Table Access Full)是指Oracle在訪問目標表里的數據時,會從該表所占用的第一個區(EXTENT)的第一個塊(BLOCK)開始掃描,一直掃描到該表的高水位線(HWM,High Water Mark),Oracle會對這期間讀到的所有數據施加目標SQL的where條件中指定的過濾條件,最后只返回那些滿足過濾條件的數據。
不是說全表掃描不好,事實上Oracle在做全表掃描操作時會使用多塊讀,ORACLE采用一次讀入多個數據塊 (database block)的方式優化全表掃描,而不是只讀取一個數據塊,這極大的減少了I/O總次數,提高了系統的吞吐量,所以利用多塊讀的方法可以十分高效地實現全表掃描。這在目標表的數據量不大時執行效率是非常高的,但全表掃描最大的問題就在於走全表掃描的目標SQL的執行時間會不穩定、不可控,這個執行時間一定會隨着目標表數據量的遞增而遞增。因為隨着目標表數據量的遞增,它的高水位線會一直不斷往上漲,所以全表掃描該表時所需要讀取的數據塊的數量也會不斷增加,這意味着全表掃描該表時所需要耗費的I/O資源會隨之不斷增加,當然完成對該表的全表掃描操作所需要耗費的時間也會隨之增加。
在Oracle中,如果對目標表不停地插入數據,當分配給該表的現有空間不足時高水位線就會向上移動,但如果你用DELETE語句從該表刪除數據, 則高水位線並不會隨之往下移動(這在某種程度上契合了"高水位線"的定義,就好比水庫的水位,當水庫漲水時,水位會往上移,當水庫放水后,曾經的最高水位 的痕跡還是會清晰可見)。高水位線的這種特性所帶來的副作用是,即使使用DELETE語句刪光了目標表中的所有數據,高水位線還是會在原來的位置,這意味着全表掃描該表時Oracle還是需要掃描該表高水位線下的所有數據塊,所以此時對該表的全表掃描操作所耗費的時間與之前相比並不會有明顯的改觀。
使用FTS的前提條件:在較大的表上不建議使用全表掃描,除非取出數據的比較多,超過總量的5% -- 10%,或你想使用並行查詢功能時。
5.2 通過ROWID訪問表(table access by ROWID)
ROWID是一個偽列,即是一個非用戶定義的列,而又實際存儲於數據庫之中。每一個表都有一個ROWID列,一個ROWID值用於唯一確定數據庫表中的的一條記錄。因此通過ROWID 方式來訪問數據也是 Oracle 數據庫訪問數據的實現方式之一。一般情況下,ROWID方式的訪問一定以索引訪問或用戶指定ROWID作為先決條件,因為所有的索引訪問方式最終都會轉換為通過ROWID來訪問數據記錄。(注:index full scan 與index fast full scan除外)由於Oracle ROWID能夠直接定位一條記錄,因此使用ROWID方式來訪問數據,極大提高數據的訪問效率
ROWID掃描是指Oracle在訪問目標表里的數據時,直接通過數據所在的ROWID去定位並訪問這些數據。
從嚴格意義上來說,Oracle中的ROWID掃描有兩層含義:一種是根據用戶在SQL語句中輸入的ROWID的值直接去訪問對應的數據行記錄;另外一種是先去訪問相關的索引,然后根據訪問索引后得到的ROWID再回表去訪問對應的數據行記錄。
對Oracle中的堆表而言,我們可以通過Oracle內置的ROWID偽列得到對應行記錄所在的ROWID的值(注意,這個ROWID只是一個偽 列,在實際的表塊中並不存在該列),然后我們還可以通過DBMS_ROWID包中的相關方法(dbms_rowid.rowid_object,dbms_rowid.rowid_relative_fno、dbms_rowid.rowid_block_number和 dbms_rowid.rowid_row_number)將上述ROWID偽列的值翻譯成對應數據行的實際物理存儲地址。
5.3 索引掃描
索引范圍掃描(INDEX RANGE SCAN)
索引范圍掃描(INDEX RANGE SCAN)適用於所有類型的B樹索引,當掃描的對象是唯一性索引時,此時目標SQL的where條件一定是范圍查詢(謂詞條件為 BETWEEN、<、>等);當掃描的對象是非唯一性索引時,對目標SQL的where條件沒有限制(可以是等值查詢,也可以是范圍查詢)。 索引范圍掃描的結果可能會返回多條記錄,其實這就是索引范圍掃描中"范圍"二字的本質含義。
索引唯一性掃描(INDEX UNIQUE SCAN)
索引唯一性掃描(INDEX UNIQUE SCAN)是針對唯一性索引(UNIQUE INDEX)的掃描,它僅僅適用於where條件里是等值查詢的目標SQL。因為掃描的對象是唯一性索引,所以索引唯一性掃描的結果至多只會返回一條記錄。
索引全掃描(INDEX FULL SCAN)
所謂的索引全掃描(INDEX FULL SCAN)就是指要掃描目標索引所有葉子塊的所有索引行。這里需要注意的是,索引全掃描需要掃描目標索引的所有葉子塊,但這並不意味着需要掃描該索引的所有分支塊。在默認情況下,Oracle在做索引全掃描時只需要通過訪問必要的分支塊定位到位於該索引最左邊的葉子塊的第一行索引行,就可以利用該索引葉子塊之間的雙向指針鏈表,從左至右依次順序掃描該索引所有葉子塊的所有索引行了。
索引快速全掃描(INDEX FAST FULL SCAN)
索引快速全掃描(INDEX FAST FULL SCAN)和索引全掃描(INDEX FULL SCAN)極為類似,它也適用於所有類型的B樹索引(包括唯一性索引和非唯一性索引)。和索引全掃描一樣,索引快速全掃描也需要掃描目標索引所有葉子塊的所有索引行。
索引快速全掃描與索引全掃描相比有如下三點區別。
(1)索引快速全掃描只適用於CBO。
(2)索引快速全掃描可以使用多塊讀,也可以並行執行。
(3)索引快速全掃描的執行結果不一定是有序的。這是因為索引快速全掃描時Oracle是根據索引行在磁盤上的物理存儲順序來掃描,而不是根據索引行的邏輯順序來掃描的,所以掃描結果才不一定有序(對於單個索引葉子塊中的索引行而言,其物理存儲順序和邏輯存儲順序一致;但對於物理存儲位置相鄰的索引葉子塊而言,塊與塊之間索引行的物理存儲順序則不一定在邏輯上有序)。
索引跳躍式掃描(INDEX SKIP SCAN)
索引跳躍式掃描(INDEX SKIP SCAN)適用於所有類型的復合B樹索引(包括唯一性索引和非唯一性索引),它使那些在where條件中沒有對目標索引的前導列指定查詢條件但同時又對該 索引的非前導列指定了查詢條件的目標SQL依然可以用上該索引,這就像是在掃描該索引時跳過了它的前導列,直接從該索引的非前導列開始掃描一樣(實際的執行過程並非如此),這也是索引跳躍式掃描中"跳躍"(SKIP)一詞的含義。
為什么在where條件中沒有對目標索引的前導列指定查詢條件但Oracle依然可以用上該索引呢?這是因為Oracle幫你對該索引的前導列的所有distinct值做了遍歷。
參考:
