Oracle优化05-执行计划
思維導(dǎo)圖
系列文章
Oracle-SQL Explain Plan解讀
概述
如果要分析某條SQL的性能問題,通常來講,我們首先要看SQL的執(zhí)行計劃,看看SQL的每一步執(zhí)行計劃是否存在問題。
如果某一條SQL平常執(zhí)行的都很好,卻有一天突然性能很差,如果排除了系統(tǒng)資源和阻塞的原因,那么基本上可以判斷是執(zhí)行計劃出現(xiàn)了問題。
看懂執(zhí)行計劃變成了SQL優(yōu)化(其實在大多數(shù)的情況下,SQL優(yōu)化指的是SQL的性能問題定位)的先決條件。
在討論SQL執(zhí)行計劃之前,我們需要知道執(zhí)行計劃當(dāng)中一個非常重要的概念–Cardinality基數(shù)。
Cardinality基數(shù)
在我們看執(zhí)行計劃的每一步操作的時候,當(dāng)前操作的Cardinality值表示CBO預(yù)期從一個行源(row source)返回的記錄數(shù)。
一個行源可能是一個表、一個索引、也可能是一個子查詢。
比如:
當(dāng)CBO無法準(zhǔn)確的獲取到Cardinality時,將會發(fā)生什么?
在執(zhí)行計劃中, card 就是Cardinality的縮寫,它表示CBO估算當(dāng)前操作預(yù)期獲取的記錄數(shù)。
Cardinality的值對于CBO做出正確的執(zhí)行計劃來說至關(guān)重要,如果CBO獲得的Cardinality值不夠準(zhǔn)確(通常是沒有做分析或者分析數(shù)據(jù)過舊導(dǎo)致),在執(zhí)行成本計算上就會出現(xiàn)偏差,從而導(dǎo)致CBO錯誤的制定出執(zhí)行計劃。
下面演示下當(dāng)CBO無法準(zhǔn)確的獲取到Cardinality時,將會發(fā)生什么?
創(chuàng)建一個數(shù)據(jù)分布非常不均勻的表T
Connected to Oracle Database 11g Enterprise Edition Release 11.2.0.4.0 Connected as xxx@xgjSQL> drop table t;Table droppedSQL> create table t as select 1 id , object_name from dba_objects;Table createdSQL> update t set t.id=99 where rownum=1;1 row updatedSQL> commit;Commit completeSQL> create index t_ind on t(id);Index createdSQL> select id ,count(*) from t group by id ;ID COUNT(*) ---------- ----------1 3525199 1查看執(zhí)行計劃
##執(zhí)行SQL SQL> select /*+ dynamic_sampling(t 0) */ * from t where id=1;SQL> SELECT sql_id, child_number, a.* from v$sql a where a.SQL_TEXT like '%select /*+ dynamic_sampling(t 0) */ * from t where id=1%';SQL> select * from table(DBMS_XPLAN.display_cursor('bb22rwn4604yj',0));說明:
- /+ dynamic_sampling(t 0) / :目的是讓CBO無法通過動態(tài)采樣獲取表中實際數(shù)據(jù)的情況,此時CBO只能根據(jù)數(shù)據(jù)字典中標(biāo)T的非常有限的信息(比如表的extends數(shù)量,數(shù)據(jù)塊的數(shù)量)來猜測表中的數(shù)據(jù)
- 從結(jié)果中可以看出,CBO猜測出id=1的數(shù)據(jù)為118條,而與實際上的數(shù)量 35251,相差甚遠,所以CBO選擇了索引而不是全表掃描
讓我們看下實際的執(zhí)行情況:
select * from t where id=1;SELECT sql_id, child_number, a.* from v$sql a where a.SQL_TEXT like '%select * from t where id=1%';select * from table(DBMS_XPLAN.display_cursor('3k9f6qpq9sbmn',0));通過動態(tài)采樣(10g及以上版本,如果表沒有做過分析,ORACLE會自動通過動態(tài)采樣的方式來收集分析數(shù)據(jù)),CBO估算出來的表中數(shù)量為39257 (Cardinality) 和實際的數(shù)量 35251非常接近,CBO判斷ID=1的數(shù)據(jù)基本上等同于表中的數(shù)據(jù),所以選擇了全表掃描。
下面我們做一下表和索引分析
##先查看下 SQL> select num_rows, avg_row_len, blocks, last_analyzed from user_tables where table_name = 'T';NUM_ROWS AVG_ROW_LEN BLOCKS LAST_ANALYZED ---------- ----------- ---------- -------------SQL> select blevel,leaf_blocks,distinct_keys,last_analyzed from user_indexes where table_name='T';BLEVEL LEAF_BLOCKS DISTINCT_KEYS LAST_ANALYZED ---------- ----------- ------------- -------------##對表和索引做分析 SQL> exec dbms_stats.gather_table_stats(user,'t',cascade => true);PL/SQL procedure successfully completed##重新查詢信息 SQL> select num_rows, avg_row_len, blocks, last_analyzed from user_tables where table_name = 'T';NUM_ROWS AVG_ROW_LEN BLOCKS LAST_ANALYZED ---------- ----------- ---------- -------------35252 22 144 2016-12-30 0:SQL> select blevel,leaf_blocks,distinct_keys,last_analyzed from user_indexes where table_name='T';BLEVEL LEAF_BLOCKS DISTINCT_KEYS LAST_ANALYZED ---------- ----------- ------------- -------------1 69 2 2016-12-30 0:SQL>查看執(zhí)行計劃
SQL> select * from t a where a.id=99;SQL> SELECT sql_id, child_number, a.* from v$sql a where a.SQL_TEXT like '%select * from t a where a.id=99%';SQL> select * from table(DBMS_XPLAN.display_cursor('fa0r3kc8y5239',0));通過對表的分析,CBO就可以獲取到T表和索引的充足的信息。
上面的截圖,CBO從分析數(shù)據(jù)中,獲取到了id=99的數(shù)據(jù)6 rows , 所以選擇了索引。
我們更新下數(shù)據(jù)
SQL>update t set id=99; SQL>commit;將id 全部更新為99 , 因為沒有對表進行分析,所以CBO知道的信息還是舊的,重新查詢 下 我們可以看到 CBO依然認(rèn)為表T中的數(shù)據(jù)很少,依然選擇的是索引。
SQL> exec dbms_stats.gather_table_stats(user,'t',cascade => true);PL/SQL procedure successfully completed ## 如果使用 DBMS_XPLAN.display_cursor 查詢執(zhí)行計劃的話,需要清空shared_pool ,或者換個SQL(目的是不匹配到shared_pool中的緩存) SQL> alter system flush shared_pool;System altered##重新執(zhí)行SQL SQL> select * from t a where a.id=99;##獲取SQL_ID等 SQL> SELECT sql_id, child_number, a.* from v$sql a where a.SQL_TEXT like '%select * from t a where a.id=99%';##查看執(zhí)行計劃 SQL> select * from table(DBMS_XPLAN.display_cursor('fa0r3kc8y5239',0));重新對表分析后,CBO獲取了正確的Cardinality值。T表中id=99的數(shù)據(jù)3w+,所以全表掃描是最佳執(zhí)行計劃。
Cardinality是如何影響多表查詢的?
在多表關(guān)聯(lián)查詢或者SQL中有子查詢時,每個關(guān)聯(lián)表或者子查詢的Cardinality的值對主查詢的影響非常大,甚至可以說,CBO就是依賴于各個關(guān)聯(lián)表或者子查詢Cardinality值來計算出最后的執(zhí)行計劃。
對于多表查詢,CBO使用每個關(guān)聯(lián)表返回的行數(shù)(Cardinality)決定使用設(shè)么樣的方式來做表關(guān)聯(lián)(比如Nested loops ,sm 或者 hash join),
對于子查詢,它的Cardinality將決定子查詢是使用索引還是使用全表掃描的方式訪問數(shù)據(jù)。
舉例說明:
數(shù)據(jù)如下:
Connected to Oracle Database 11g Enterprise Edition Release 11.2.0.4.0 Connected as xxx@xgjSQL> create table t1 (id int , name varchar2(1000));Table createdSQL> create table t2 (id int , name varchar2(1000));Table createdSQL> create index ind_t1 on t1(id);Index createdSQL> create index ind_t2 on t2(id);Index createdSQL> create index ind_t2_name on t2(name);Index createdSQL> insert into t1 select object_id ,object_name from dba_objects ;35258 rows insertedSQL> commit;Commit completeSQL> insert into t2 values(1,'XGJ');1 row insertedSQL> commit;Commit complete##僅對t1的表和索引進行分析 SQL> exec dbms_stats.gather_table_stats(user,'t1',cascade => true,method_opt => 'for all indexed columns');PL/SQL procedure successfully completedSQL> select * from t1 where id in (select /*+ dynamic_sampling(t2 0) cardinality(t2 10000) */ id from t2 where name = 'XGJ');ID NAME ------------- ----------------------SQL>select * from v$sql a where a.SQL_TEXT like '% /*+ dynamic_sampling(t2 0) cardinality(t2 10000) */ %';##DBMS_XPLAN.display_cursor中查看執(zhí)行計劃 SQL>select * from table(dbms_xplan.display_cursor('2c7hx20a3bhxx',0));解析:
我們發(fā)出的子查詢的SQL,同時使用了hint
/*+ dynamic_sampling(t2 0) cardinality(t2 10000) */- dynamic_sampling(t2 0):禁止動態(tài)采用
- cardinality(t2 10000):告訴CBO從T2表中取10000條記錄
通過這種方式,我們模擬子查詢中返回的結(jié)果數(shù),同時為了讓CBO完全依賴這個信息生成執(zhí)行計劃,我們禁止了子查詢使用動態(tài)采樣(dynamic_sampleing 設(shè)置為0)。
可以看到,當(dāng)CBO得到來自于子查詢的結(jié)果集(Rows )的記錄為10000條時,采用了hash join semi的執(zhí)行計劃,hash join通常適用于兩張關(guān)聯(lián)的表都比較大的時候
如果我們把子查詢的結(jié)果集變得很小會怎樣呢?
來看我們下面的栗子:
SQL> select * from t1 where id in (select /*+ dynamic_sampling(t2 0) cardinality(t2 1) */ id from t2 where name = 'XGJ');ID NAME ------ ----------同樣的 我們來看下執(zhí)行計劃:
我們在查詢中將子查詢的返回值設(shè)置為1,即
Cardinality(t2,1)此時CBO選擇了兩個表通過nested loop join 進行關(guān)聯(lián)的執(zhí)行計劃,因為子查詢只有1條記錄,這個時候CBO會選擇最適合擇偶張情況的netsted loops join關(guān)聯(lián)方式。
從這個試驗中我們可以得到如下結(jié)論:
子查詢的Cardinality的值,直接影響了主查詢的執(zhí)行計劃,如果CBO對子查詢的Cardinality判斷有誤,那么餓主查詢的執(zhí)行計劃很有可能是錯誤的。
再看量表關(guān)聯(lián)的查詢情況,還是用上面的栗子
##上面執(zhí)行后 user_index有值了...清掉先 select num_rows, avg_row_len, blocks, last_analyzed from user_tables where table_name = 'T2';select blevel,leaf_blocks,distinct_keys,last_analyzed from user_indexes where table_name='T2';SQL> analyze table t2 delete statistic2 ;Table analyzedSQL> alter system flush shared_pool;System alteredSQL> select /*+ dynamic_sampling(t2 0) cardinality(t2 10000) */ * from t1,t2 where t1.id=t2.id;....省略輸出查看執(zhí)行計劃
這個栗子中,CBO認(rèn)為T2關(guān)聯(lián)的數(shù)據(jù)足夠多,而且T1又足夠大,所以在這種情況下,hash join 是最合適的。
SQL>select /*+ dynamic_sampling(t2 0) cardinality(t2 1) */ * from t1,t2 where t1.id=t2.id;....省略輸出查看執(zhí)行計劃:
這時候因為T2給CBO提供的信息,只有1條記錄做關(guān)聯(lián)查詢,所以CBO選擇了nested loop join .
總結(jié)
以上的例子主要說明Cardinality對CBO生成執(zhí)行計劃的影響,所以我們在看多表查詢的時候,一定要注意每個操作返回的Cardinality的值,如果這個值明顯的不對,那么很有可能操作的表的分析數(shù)據(jù)出了問題,或者沒有分析。
比如在上面的栗子中,我們確切的知道T2表的數(shù)據(jù)很小,而在執(zhí)行計劃中卻顯示10000條,這顯然不對了,這個時候叫就要檢查問題所在,看看T2表是不是曾經(jīng)有很多數(shù)據(jù),刪除之后沒有做重新分析等等。
SQL的執(zhí)行計劃
如果一條SQL的性能出現(xiàn)了問題,首先應(yīng)該看一下它的執(zhí)行計劃,以便確定(或者猜測)問題的所在。
生成SQL的執(zhí)行計劃時Oracle在對SQL做硬分析時的一個非常重要的步驟,它制定出一個方案告訴Oracle在執(zhí)行這條SQL時以什么樣的方式訪問數(shù)據(jù): 索引掃描? 全表掃描? , 是hash join 還是 netsted loops join 等。
如何得到一個SQL的執(zhí)行計劃
- explain plan for
- SQLPLUS命令 set autotrace on
- 第三方軟件提供的GUI工具,比如Toad ,PL/SQL Developer
具體看: Oracle-SQL Explain Plan解讀
如何看懂一個SQL的執(zhí)行計劃
首先得到一個SQL的執(zhí)行計劃
我們使用select * from table(DBMS_XPLAN.display_cursor(.......))的方式獲取select * from t1 ,t2 where t1.id = t2.id如下的執(zhí)行計劃:
其中,rows列 就是我們上面說到的 Card(Cardinality) 9i以前的版本使用的是Card.
如何閱讀呢?
我們首先從說縮進度最大的行讀取,它是最先被執(zhí)行的步驟
ID=3和ID=4是最先被執(zhí)行的。
當(dāng)兩行縮進一樣時,最上面的最先被執(zhí)行,在這里就是 ID=3的先執(zhí)行,然后是ID=4的。
然后是縮進次之的,
這里就是
緊接著鎖進再次之的,以此類推
在這里就是是ID=1的 ,最后是ID=0的。
我們也可在PL/SQL中F5查看執(zhí)行計劃后,一步步的跟蹤
這就是這個SQL的執(zhí)行過程。
大致意思是:
從T2表讀取第一行數(shù)據(jù) 是否符合條件
如果符合就拿出一行來,然后到索引IND_T1 中找到對應(yīng)的值,然后重復(fù),直到把整個T2表全表掃描完,這個過程就叫NESTED LOOPS .
當(dāng)T2表被掃描完之后,會產(chǎn)生一個結(jié)果集,這個結(jié)果集是 IND_T1的一個索引集,然后ORACLE根據(jù)說印鍵值上的rowid 去T1表找對應(yīng)的記錄,就是這一步: Operation TABLE ACCESS BY INDEX ROWID
然后還有個NESTED LOOPS .(疑惑待思考)
最后將結(jié)果返回: Operation SELECT STATEMENT
執(zhí)行計劃中的值說明
ID列: 是一個序號,注意,它的大小并不是執(zhí)行的先后順序。
Operation列: 是當(dāng)前的操作內(nèi)容。
Rows 列: 就是當(dāng)前操作的cardinality,Oracle估算當(dāng)前操作的返回結(jié)果集
Cost(cpu): Oracle計算出來的一個數(shù)值(代價),用于說明SQL執(zhí)行的代價
Time列: Oracle估算當(dāng)前操作的時間。
還有些重要信息。比如
Predicate Information (identified by operation id): --------------------------------------------------- 4 - access("T1"."ID"="T2"."ID")這一段是來說明謂詞信息和數(shù)據(jù)獲取的方式,它的意思在ID=4的那一列,通過訪問索引尋找數(shù)據(jù),而不是訪問原表數(shù)據(jù)。
還有個常見的方式 filter ,我們這里來解釋下access和filter的區(qū)別
如果執(zhí)行計劃顯示access, 就表示這個謂詞條件的值將會影響數(shù)據(jù)的訪問路徑(全表還是索引,這里是索引)
如果執(zhí)行計劃顯示filter,表示謂詞條件的值并不會影響數(shù)據(jù)的訪問路徑,只起到過濾的作用。
執(zhí)行計劃的最后一步是
這一步提示用戶CBO當(dāng)前使用的技術(shù),需要用戶咋分析執(zhí)行計劃時考慮到這些因素,比如現(xiàn)在提示這些信息時,當(dāng)前表使用了動態(tài)采樣,通過這個提示,我們就知道這個表可能沒有做過分析。
舉例:
Connected to Oracle Database 11g Enterprise Edition Release 11.2.0.4.0 Connected as xxx@xgjSQL> create table t(x int);Table createdSQL> begin for i in 1 .. 1000 loop insert into t values (i); end loop; commit; end; /SQL> select a.SQL_ID ,a.CHILD_NUMBER from v$sql a where a.SQL_TEXT like 'select * from t';SQL_ID CHILD_NUMBER ------------- ------------ 89km4qj1thh13 0 SQL> select * from table(dbms_xplan.display_cursor('89km4qj1thh13',0));PLAN_TABLE_OUTPUT -------------------------------------------------------------------------------- SQL_ID 89km4qj1thh13, child number 0 ------------------------------------- select * from t Plan hash value: 1601196873 -------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | -------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | | | 2 (100)| | | 1 | TABLE ACCESS FULL| T | 1 | 13 | 2 (0)| 00:00:01 | -------------------------------------------------------------------------- Note ----- - dynamic sampling used for this statement (level=2)17 rows selectedSQL>我們手工執(zhí)行進行表分析后,重新查看下
##表分析 SQL> exec dbms_stats.gather_table_stats(user,'t');PL/SQL procedure successfully completed##因為是從shared_pool中加載數(shù)據(jù),如果不清空,會影響執(zhí)行計劃 SQL> alter system flush shared_pool;System altered SQL>select * from t; .... SQL>select a.SQL_ID ,a.CHILD_NUMBER,a.SQL_TEXT from v$sql a where a.SQL_TEXT like 'select * from t%'; .... SQL> select * from table(dbms_xplan.display_cursor('ckzaraqcmkr2f',0));PLAN_TABLE_OUTPUT -------------------------------------------------------------------------------- SQL_ID ckzaraqcmkr2f, child number 0 ------------------------------------- select * from t Plan hash value: 1601196873 -------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | -------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | | | 3 (100)| | | 1 | TABLE ACCESS FULL| T | 1000 | 4000 | 3 (0)| 00:00:01 | --------------------------------------------------------------------------13 rows selectedSQL>執(zhí)行計劃中已經(jīng)沒有動態(tài)采樣的提示信息了。
第一次執(zhí)行SQL,CBO發(fā)現(xiàn)表沒有做表分析,于是使用動態(tài)采樣的方式獲取數(shù)據(jù)信息。
SQL第二次執(zhí)行時,CBO發(fā)現(xiàn)表已經(jīng)分析過了,于是就不會再使用動態(tài)分析,而是直接使用分析數(shù)據(jù)。
這里會出現(xiàn)兩種情況:
- 如果沒做表分析,CBO可以通過動態(tài)采樣的方式來分析數(shù)據(jù),也可以獲取到正確的執(zhí)行計劃
- 如果分析過,但是分析信息過舊,這時候CBO不會再使用動態(tài)采樣,而是使用這些舊的分析數(shù)據(jù),有可能導(dǎo)致錯誤的執(zhí)行信息
總結(jié)
以上我們闡述了執(zhí)行計劃輸出的全部內(nèi)容。 當(dāng)我們在看執(zhí)行計劃時,不能只看執(zhí)行計劃的本身,還要看下面的謂詞和提示信息,這都非常有幫助。
總結(jié)
以上是生活随笔為你收集整理的Oracle优化05-执行计划的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Oracle优化04-Optimizer
- 下一篇: Oracle优化07-分析及动态采样-直