程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 數據庫知識 >> Oracle數據庫 >> Oracle教程 >> Oracle11g新特性之動態變量窺視

Oracle11g新特性之動態變量窺視

編輯:Oracle教程

Oracle11g新特性之動態變量窺視


1. 11g之前的綁定變量窺視
我們都知道,為了能夠讓SQL語句共享執行計劃,oracle始終都是強調在進行應用系統的設計時,必須使用綁定變量,也就是用一個變量來代替原來出現在SQL語句裡的字面值。比如,對於下面三條SQL語句來說:
select col1 from t where col2 = 1;
select col1 from t where col2 = 2;
select col1 from t where col2 = 3;

我們可以看到,這三條SQL語句幾乎一樣,只有最後where條件裡的字面值(分別是1、2、3)不同而已。但是如果寫成這個樣子,則oracle是不知道這三條SQL語句是一樣的,仍然把它們當作三條完全不同的SQL語句,從而在shared pool裡進行硬解析,並生成最終的執行計劃。但是我們會發現,這三個執行計劃可能都是一樣的,因此後面兩次生成執行計劃的工作可能是完全不必要的,這在典型的OLTP環境中更是如此。由於解析本身屬於CPU密集型操作,因此為了降低對CPU的消耗,oracle建議將這樣的SQL寫成:
select col1 from t where col2 = :v1;

然後,分別將1、2、3傳遞給v1,這樣的話,只需要第一次傳入1時進行解析即可。而後面執行2、3時,由於SQL文本本身沒有變化,因此直接把執行計劃拿來使用即可,不需要再次生成執行計劃。

但是,生成執行計劃本身是基於概率的理論,在不訪問具體表裡的數據的前提下,根據你的where條件,來猜測返回的記錄數大概是多少,從而判斷應該采用怎樣的訪問路徑。很明顯,這是一定要參照具體的where條件裡的值才能進行猜測的。這樣就與節省CPU的初衷產生了矛盾,因為節省CPU的關鍵是使用綁定變量,你一旦使用了綁定變量,則oracle豈不是不知道你具體的字面值了嗎?

為了解決這一問題,oracle引入了綁定變量窺視。所謂綁定變量窺視,就是指oracle在第一次解析SQL語句的時候(也就是說該SQL第一次傳入shared pool),會將你輸入的綁定變量的值帶入SQL語句裡,從而參考你的字面值來猜測該SQL大概會返回多少條記錄,從而得到優化的執行計劃。然後,以後再次執行相同的SQL語句時,不再考慮你所輸入的綁定變量的值,直接取出第一次生成的綁定變量。

但是,很可惜的是,使用綁定變量從而共享游標與SQL優化是兩個矛盾的目標。Oracle使用綁定變量的前提,是oracle認為大部分的列的數據都是分布比較均勻的。從而,使用第一次的綁定變量的值所得到的執行計劃,大多數情況下都能適用於該綁定變量的其他的值。很明顯,如果第一次傳入的綁定變量的值恰好占整個數據量的百分比較高,從而導致全表掃描的執行計劃。而後來傳入的綁定變量的值都占整個數據量的百分比都很低,則應該走索引掃描會更好的,但是由於使用了綁定變量,從而oracle並不會再去看你的綁定變量的值,而是直接拿全表掃描的執行計劃來用。這時,由於使用了綁定變量,雖然我們達到了游標共享,從而節省CPU的目的,但是SQL的執行計劃卻不夠優化了。

那麼我們如何在綁定變量和SQL優化之間進行取捨呢?在OLTP應用中,由於並發性較高,CPU上的爭用會比較嚴重,同時SQL本身執行時間較短,涉及到的數據量較少,解析所占的時間在整個SQL執行時間中占的比例較高,而花在I/O上的時間占的比例較低。因此盡管綁定變量會有SQL不夠優化的問題,還是建議使用綁定變量。但是在DSS應用和數據倉庫應用中,由於並發性較低,CPU上的爭用較輕,同時SQL語句的執行時間都很長,而且主要時間花在等待I/O上,而解析占的比重較低,這時優化SQL執行計劃的重要性就體現出來了。因此,建議不要使用綁定變量,而直接使用字面值。但是大多數的情況都是混合應用,既有OLTP又有數據倉庫,這時就很難完美的解決該問題了。

我們先來看一下11g之前的綁定變量窺視是如何工作的,以10g為例。
我們先創建一個表,使得其含有的數據分布不均勻,並在該表上創建一個索引。
hr@ora10g > create table t1 as select object_id as id,object_name from dba_objects;
hr@ora10g > update t1 set id=1 where rownum<=10000;
hr@ora10g > commit;
hr@ora10g > create index idx_t1 on t1(id); 

這樣,該表裡id為的1記錄有一萬條,而id為其他值的記錄都只有一條。從而,我們構建出一個分布不均勻的測試用表。然後,我們收集一下統計信息。注意,這裡要收集直方圖,為的是要讓CBO知道id列上的數據分布不均勻。

hr@ora10g> begin
2 dbms_stats.gather_table_stats(
3 user,
4 't1',
5 cascade => true,
6 method_opt => 'for columns id size 254'
7 );
8 end;
9 /

我們找到表t1裡最大的id,然後以該id作為第一個綁定變量傳入,可以想象,該綁定變量將導致走索引。注意,我們這裡設定的優化器目標為all_rows。
\hr@ora11g > select max(id) from t1; \MAX(ID) \---------- \13871 \hr@ora10g> alter system flush shared_pool; \hr@ora10g> var v_id number; \hr@ora10g> var v_sql_id varchar2(20); \hr@ora10g> exec :v_id := 13871; \hr@ora10g> select * from t1 where id=:v_id; \此處省略查詢結果 \hr@ora10g > begin \2 select sql_id into :v_sql_id from v$sql \3 where sql_text like 'select * from t1 where id=:v_id%'; \4 end; \5 / \hr@ora10g > select * from table(dbms_xplan.display_cursor(:v_sql_id)); \PLAN_TABLE_OUTPUT \-------------------------------------------------------------------------------- \SQL_ID djwq30cpbcz7k, child number 0 \------------------------------------- \select * from t1 where id=:v_id \Plan hash value: 50753647 \-------------------------------------------------------------------------------- \| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time \-------------------------------------------------------------------------------- \| 0 | SELECT STATEMENT | | | | 11 (100) | \| 1 | TABLE ACCESS BY INDEX ROWID| T1 | 1365 | 28665| 11 (0) | 00:00:01 \|* 2 | INDEX RANGE SCAN | IDX_T1 | 1365 | | 3 (0) | 00:00:01 \-------------------------------------------------------------------------------- \...... \hr@ora10g> exec :v_id := 1; \hr@ora10g> select * from t1 where id=:v_id; \此處省略查詢結果 \hr@ora10g > begin \2 select sql_id into :v_sql_id from v$sql \3 where sql_text like 'select * from t1 where id=:v_id%'; \4 end; \5 / \hr@ora10g > select * from table(dbms_xplan.display_cursor(:v_sql_id)); \PLAN_TABLE_OUTPUT \-------------------------------------------------------------------------------- \SQL_ID djwq30cpbcz7k, child number 0 \------------------------------------- \select * from t1 where id=:v_id \Plan hash value: 50753647 \-------------------------------------------------------------------------------- \| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time \-------------------------------------------------------------------------------- \| 0 | SELECT STATEMENT | | | | 11 (100) | \| 1 | TABLE ACCESS BY INDEX ROWID| T1 | 1365 | 28665| 11 (0) | 00:00:01 \|* 2 | INDEX RANGE SCAN | IDX_T1 | 1365 | | 3 (0) | 00:00:01 \-------------------------------------------------------------------------------- \

從上面結果可以看出,在為綁定變量傳入第一個值為13871時,由於返回的記錄條數較少,導致走索引掃描。當我們第二次傳入綁定變量值1時,oracle不再生成新的執行計劃,而直接拿索引掃描的執行路徑來用。

但是,如果先傳入1的綁定變量值,然後再傳入13871的綁定變量值時,會怎樣?我們繼續測試。

hr@ora10g> alter system flush shared_pool;
hr@ora10g> set autotrace traceonly exp stat;
hr@ora10g> exec :v_id := 1;
hr@ora10g> select * from t1 where id=:v_id;
hr@ora10g > begin
2 select sql_id into :v_sql_id from v$sql
3 where sql_text like 'select * from t1 where id=:v_id%';
4 end;
5 /
hr@ora10g > select * from table(dbms_xplan.display_cursor(:v_sql_id));
PLAN_TABLE_OUTPUT
-------------------------------------------------------------------------------- 
SQL_ID djwq30cpbcz7k, child number 0
------------------------------------- 
select * from t1 where id=:v_id
Plan hash value: 3617692013
-------------------------------------------------------------------------- 
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------- 
| 0 | SELECT STATEMENT | | | | 13 (100)| |
|* 1 | TABLE ACCESS FULL | T1 | 8738 | 179K | 13 (0) | 00:00:01 |
-------------------------------------------------------------------------- 
......
hr@ora10g > exec :v_id := 13871;
hr@ora10g > select * from t1 where id=:v_id;
hr@ora10g > begin
2 select sql_id into :v_sql_id from v$sql
3 where sql_text like 'select * from t1 where id=:v_id%';
4 end;
5 /
hr@ora10g > select * from table(dbms_xplan.display_cursor(:v_sql_id));
PLAN_TABLE_OUTPUT
-------------------------------------------------------------------------------- 
SQL_ID djwq30cpbcz7k, child number 0
------------------------------------- 
select * from t1 where id=:v_id
Plan hash value: 3617692013
-------------------------------------------------------------------------- 
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------- 
| 0 | SELECT STATEMENT | | | | 13 (100)| |
|* 1 | TABLE ACCESS FULL | T1 | 8738 | 179K | 13 (0) | 00:00:01 |
-------------------------------------------------------------------------- 

很明顯,先傳入1的綁定變量時將導致生成的執行計劃走全表掃描。後面傳入的13871的綁定變量的最佳執行路徑應該是索引掃描,但是由於CBO並不知道這一點,而是直接拿第一次生成的執行計劃來用了,於是也走全表掃描了。

2. 11g之後的動態綁定變量窺視
而從11g開始,這個尴尬的問題開始得到了改善。因此從11g開始,引入了所謂的自適應游標共享(Adaptive Cursor Sharing)。該特性是一個非常復雜的技術,用來平衡游標共享和SQL優化這兩個矛盾的目標。11g裡不會盲目的共享游標,而是會去查看每個綁定變量,並為不同的綁定變量來產生不同的執行計劃。而oracle這麼做的前提是,使用多個執行計劃的所帶來的收益,要比產生多個執行計劃所引起的CPU開銷要更大。

使用自適應游標共享時,會遵循下面的步驟:
1) 一條新的SQL語句第一次傳入shared pool時,還是和以前一樣,進行硬解析。而且進行綁定變量窺視,計算where條件各個列的selectivity,同時如果綁定變量所在的列上存在直方圖的話,也會去參考該直方圖來計算selectivity。該游標會被標記為是一個綁定敏感的游標(bind-sensitive cursor)。同時,oracle還會保留包含綁定變量的where條件的其他信息,比如selectivity等。Oracle會為該謂詞的selectivity維持一個范圍,oracle叫做立方體(cube)。只要傳入的綁定變量所產生的selectivity落在該范圍裡面,也就是落在該cube裡面,就不產生新的執行計劃,而直接拿該cube所對應的執行計劃來用。

2) 下次再次執行相同的SQL時,傳入了新的綁定變量,假設使用新的綁定變量的謂詞的selectivity落在已經存在的cube范圍裡,於是這次SQL的執行會使用該cube所對應的執行計劃。

3) 相同的查詢再次執行時,假設所使用的新的綁定變量導致這時候的selectivity不再落在已經存在的cube裡了,於是也就找不到對應的執行計劃。於是系統會進行一個硬解析,這將產生第二個新的執行計劃。而且新的selectivity以及對應的cube也會保存下來。也就是說,這時,我們分別有兩個cube以及兩個執行計劃。

4) 相同的查詢再次執行時,假設所使用的新的綁定變量導致這時候的selectivity不落在現存的兩個cube中的任何一個,所以系統又會進行硬解析。假設這時硬解析所產生的執行計劃與第一次產生執行計劃一樣,也就是說,在第一次評估selectivity的cube時過於保守,導致cube過小,進而導致了這一次的不必要的硬解析。於是,oracle會將第一次產生的cube與這次產生的cube合並成一個新的更大的cube。那麼,下次再次進行軟解析的時候,如果selectivity落在新的cube裡,則會使用第一次所產生的執行計劃。

我們從這裡可以看到,11g對這個問題的處理非常精彩。這樣做的結果是,系統開始運行時,CPU消耗可能會比較嚴重,但是隨著系統不斷運行,cube的不斷合並從而不斷擴大,於是系統的CPU消耗會不斷下降,同時執行計劃也會更加的合理。
我們來做個試驗進行驗證。我們采用11g新引入的執行計劃管理特性來驗證該特性。

與10g中的測試一樣,創建一個數據分布不均勻的表,在數據分布不均勻的列上創建索引,並收集統計信息,收集時注意要收集直方圖,從而讓CBO知道該列上的數據分布不均勻。

hr@ora11g > create table t1 as select object_id as id,object_name from dba_objects;
hr@ora11g > select count(*) from t1;
COUNT(*)
---------- 
12064
hr@ora11g > update t1 set id=1 where rownum<=10000;
hr@ora11g > commit;
hr@ora11g > create index idx_t1 on t1(id);
hr@ora11g > begin
2 dbms_stats.gather_table_stats(
3 user,
4 't1',
5 cascade => true,
6 method_opt => 'for columns id size 254'
7 );
8 end;
9 / 

我們找到表t1裡最大的id,然後以該id作為第一個綁定變量傳入,可以想象,該綁定變量將導致走索引。

hr@ora11g > select max(id) from t1;
MAX(ID)
---------- 
12462
我們將optimizer_capture_plan_baselines設置為true,從而讓oracle自動獲取plan baseline。
hr@ora11g > alter system set OPTIMIZER_CAPTURE_PLAN_BASELINES=true;
hr@ora11g > alter system flush shared_pool;
hr@ora11g > var v_id number;
hr@ora11g > exec :v_id := 12462;
hr@ora11g > select * from t1 where id=:v_id;
hr@ora11g > select * from t1 where id=:v_id; 

我們運行兩遍select * from t1 where id=:v_id,從而讓oracle捕獲plan baseline。我們知道id為12462的記錄只有一條,因此該SQL應該使用索引掃描。然後我們再為綁定變量傳入1,我們知道id為1的記錄有一萬條,所以較好的執行計劃不應該走已經生成的執行計劃,而應該走全表掃描。

hr@ora11g > exec :v_id := 1;
hr@ora11g > set autotrace traceonly stat;
--之所以設置stat是為了讓該sql實際執行,但不要返回所有記錄, 
hr@ora11g > select * from t1 where id=:v_id;
hr@ora11g > select sql_handle,plan_name,origin,enabled,accepted
2 from dba_sql_plan_baselines where sql_text like 'select * from t1%';
SQL_HANDLE PLAN_NAME ORIGIN ENA ACC
----------------------- ----------------------------- -------------- --- --- 
SYS_SQL_ea05bbed6f2f670c SYS_SQL_PLAN_6f2f670c844cb98a AUTO-CAPTURE YES YES
SYS_SQL_ea05bbed6f2f670c SYS_SQL_PLAN_6f2f670cdbd90e8e AUTO-CAPTURE YES NO 

我們可以發現,現在該SQL語句存在兩個執行計劃了,其中第一個執行計劃,也就是accepted為YES的執行計劃為v_id等於12462得到的,而第二個執行計劃,也就是accepted為NO的是由v_id等於1得到的。第二個執行計劃還沒有被加入plan baseline,所以優化器不會使用該執行計劃。我們將第二個執行計劃的accepted改為YES,從而讓oracle考慮使用該計劃。

hr@ora11g > var cnt number;
hr@ora11g > begin
2 :cnt := dbms_spm.alter_sql_plan_baseline(
3 sql_handle => 'SYS_SQL_ea05bbed6f2f670c',
4 plan_name => 'SYS_SQL_PLAN_6f2f670cdbd90e8e',
5 attribute_name => 'ACCEPTED', attribute_value => 'YES');
6 end;
7 /

我們來看一下這兩個執行計劃分別是怎樣的。
注意:在這裡我們要驗證oracle會為不同綁定變量生成不同的執行計劃時,不能使用set autotrace traceonly exp stat等其他方式。因為set autotrace得出的執行計劃始終都是第一次生成的執行計劃。我們通過plan baseline從側面來驗證它。當然,我們也可以通過設置sql_trace=true從而將執行計劃轉儲出來進行驗證。

SQL> select * from table(dbms_xplan.display_sql_plan_baseline
2 ('SYS_SQL_ea05bbed6f2f670c','SYS_SQL_PLAN_6f2f670c844cb98a'));
......
-------------------------------------------------------------------------------- 
Plan hash value: 50753647
-------------------------------------------------------------------------------- 
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------------- 
| 0 | SELECT STATEMENT | | 6 | 126 | 2 (0)| 00:00:01 |
| 1 | TABLE ACCESS BY INDEX ROWID| T1 | 6 | 126 | 2 (0)| 00:00:01 |
|* 2 | INDEX RANGE SCAN | IDX_T1 | 6 | | 1 (0)| 00:00:01 |
-----------------------------------------------------------------------------------
......
SQL> select * from table(dbms_xplan.display_sql_plan_baseline
2 ('SYS_SQL_ea05bbed6f2f670c','SYS_SQL_PLAN_6f2f670cdbd90e8e'));
......
-------------------------------------------------------------------------------- 
Plan hash value: 3617692013
-------------------------------------------------------------------------- 
| Id | Operation | Name | Rows | Bytes | Cost (%CPU) | Time |
-------------------------------------------------------------------------- 
| 0 | SELECT STATEMENT | | 6 | 126 | 16 (0) | 00:00:01 |
|* 1 | TABLE ACCESS FULL | T1 | 6 | 126 | 16 (0) | 00:00:01 |
-------------------------------------------------------------------------- 
......

很明顯,第一個是索引掃描,第二個是全表掃描。同樣,我們來看一下v$sql裡該sql語句有幾條記錄。
hr@ora11g > select sql_text,sql_id,child_number,plan_hash_value
2 from v$sql where sql_text like 'select * from t1 where%';
SQL_TEXT SQL_ID CHILD_NUMBER PLAN_HASH_VALUE
--------------------------------- ------------- ------------ ---------------- 
select * from t1 where id=:v_id 7y7tt6xyhas1g 0 50753647 

可以看到,該SQL語句目前在內存裡只存在一個執行計劃,其plan hash value就等於我們在前面plan baseline裡看到的第一個走索引的執行計劃的hash value。我們把該執行計劃顯示出來進行確認。

hr@ora11g > select * from table(dbms_xplan.display_cursor('7y7tt6xyhas1g',0));
PLAN_TABLE_OUTPUT
-------------------------------------------------------------------------------- 
SQL_ID 7y7tt6xyhas1g, child number 0
------------------------------------- 
select * from t1 where id=:v_id
Plan hash value: 50753647
--------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
| 0 | SELECT STATEMENT | | | | 2 (100) | |
| 1 | TABLE ACCESS BY INDEX ROWID| T1 | 1 | 21 | 2 (0) | 00:00:01 |
|* 2 | INDEX RANGE SCAN | IDX_T1 | 1 | | 1 (0) | 00:00:01 |
...... 

結果很明顯,正是走索引的執行計劃。然後我們繼續為幫定變量傳入1,多執行幾次。
hr@ora11g > exec :v_id := 1;
hr@ora11g > set autotrace traceonly stat;
hr@ora11g > select * from t1 where id=:v_id;
hr@ora11g > select * from t1 where id=:v_id;
hr@ora11g > select * from t1 where id=:v_id;

注意:這裡,我們之所以要多執行幾次,主要是因為如果只是執行一次或兩次,oracle能夠認識到你傳入的綁定變量落在了第一次的綁定變量(12462)所在的cube之外,但是oracle認為你可能只是偶爾執行該綁定變量,所以並不一定會使用另外那個全表掃描的執行計劃。多執行幾次以後,你會發現consistent gets突然從1390直線下降到了715,這時就說明oracle開始使用新的全表掃描的執行計劃了。
然後,這時我們再去查看v$sql裡該sql語句有幾條記錄。

hr@ora11g > select sql_text,sql_id,child_number,plan_hash_value
2 from v$sql where sql_text like 'select * from t1 where%';
SQL_TEXT SQL_ID CHILD_NUMBER PLAN_HASH_VALUE
--------------------------------- ------------- ------------ ---------------- 
select * from t1 where id=:v_id 7y7tt6xyhas1g 0 50753647
select * from t1 where id=:v_id 7y7tt6xyhas1g 1 3617692013 

我們發現,該SQL語句在內存裡存在兩條記錄了,也就是存在兩個子游標了,分別對應了不同的執行計劃。同樣,我們來看一下新產生的子游標,也就是child_number為1的執行計劃是怎樣的。
SQL> select * from table(dbms_xplan.display_cursor('7y7tt6xyhas1g',1));
PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------
SQL_ID 7y7tt6xyhas1g, child number 1
-------------------------------------
select * from t1 where id=:v_id
Plan hash value: 3617692013
--------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | | | 16 (100) | |
|* 1 | TABLE ACCESS FULL| T1 | 9974 | 204K | 16 (0) | 00:00:01 |
......
我們還可以從另外的角度來驗證11g裡的動態綁定變量窺視,也就是設置sql_trace的方式。這個方式比較簡單,只要先發出:alter session set sql_trace=true以後,傳入兩個不同的綁定變量,然後分別就不同的綁定變量多執行幾次。最後調用tkprof對跟蹤文件進行分析。這裡注意兩個地方,第一是跟蹤文件位於ADR中,不再位於user_dump_dest參數所指定的目錄裡了。就這裡的跟蹤文件而言,其所在位置缺省為:$ORACLE_HOME/diag/rdbms/<DB name>/<SID>/trace目錄下;第二個要注意的是使用tkprof時,添加aggregate=no選項,缺省會將相同SQL語句合並,這樣你就發現不到對於相同SQL語句的不同的執行計劃了。
這裡節選部分使用tkprof得到的文件內容,如下所示。

......
SQL ID : 7y7tt6xyhas1g
select *
from
t1 where id=:v_id
......
Rows Row Source Operation
------- --------------------------------------------------- 
10000 TABLE ACCESS BY INDEX ROWID T1 (cr=1390 pr=0 pw=0 time=446 us cost=2 size=21 card=1)
10000 INDEX RANGE SCAN IDX_T1 (cr=687 pr=0 pw=0 time=228 us cost=1 size=0 card=1)(object id 12463)
......
SQL ID : 7y7tt6xyhas1g
select *
from
t1 where id=:v_id
......
Rows Row Source Operation
------- --------------------------------------------------- 
10000 TABLE ACCESS FULL T1 (cr=715 pr=0 pw=0 time=142 us cost=16 size=209454 card=9974)
...... 

從這裡也可以很清楚的看到,對於不同的綁定變量,oracle能夠自行選擇是否應該生成更好的執行計劃並使用該執行計劃。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved