關於繫結變數

foxmile發表於2008-01-24
關於繫結變數一直理解的不是很清晰,最近看文件,基本瞭解。

什麼是繫結變數?  
    查詢通常只是因為改變where子句中的內容而產生不同的結果。為了在這種情況下避免硬解析,需要使用繫結變數(bind variable)。它是使用者放入查詢中的佔位符,它會告訴Oracle"我會隨後為這個變數提供一個值,現在需要生成一個方案,但我實際執行語句的時 候,我會為您提供應該使用的實際值"。
    select * from emp where ename='KING'; //不使用繫結變數
    select * from emp where ename=:bv     //使用繫結變數
一般在 procedure or function 中使用,可以最佳化共享池的使用。

一、Oracle SQL語句共享區的機制
1、SGA 區結構:
Oracle 資料庫啟動時,在記憶體中分配了一大片空間,為系統全域性區(System Global Area),其中包含 Sql 共享池及資料快取器(Data Buffer Cache)。SGA 區的共享池部分主要由三個區域組成: 庫快取, 字典快取, 控制結構。庫快取包括共享 SQL 區,私有SQL區,PL/SQL 過程及包, 及控制結構,如鎖及庫快取handles。使用者執行過的 Sql 語句存放於 Sql 共享池中,以便可以重用,提高其效率。
 
2、SQL語句在記憶體中的分佈:
Oracle 將其執行的每一條 SQL 語句存於共享SQL 區及私有 SQL 區中。當Oracle 發現兩個使用者執行相同的SQL語句時,則為這些使用者重用SQL共享區。但是,每一使用者必須在私有SQL區中擁有該語句的一份獨立複製。共享SQL 區包含單一SQL語句或相同的SQL語句的解析樹及執行計劃。透過為多個相同的DML語句使用一個共享SQL區,Oracle 節省了記憶體的使用, 特別是當許多使用者使用同一應用時。 共享SQL區永遠駐留在共享池中。
 
3、SQL語句解析時進行的記憶體分配操作:
當一個SQL語句被提交至Oracle 去執行時,Oracle 自動地執行以下記憶體分配步驟:
Oracle 檢查共享池,看是否在共享SQL區中已存在相同的語句。 若有,則該共享SQL區被用於執行該語句的新例項的後續操作。 相應地,若在共享池中無該語句,則Oracle在共享池中分配一新的共享SQL區,其尺寸決定於該語句的複雜性。 若一個SQL語句要求新的共享SQL區而整個共享池已被分配完畢,則 Oracle 可透過一個最近最少修改機理從共享池中釋放部分專案,直至可為新語句的共享SQL區提供足夠的空間。 若 Oracle釋放了一個共享 SQL 區,則與該區相關聯的SQL語句在下次重執行時,須重新解析並重新分配至另一共享SQL區。 在兩種情況下,使用者專用SQL區與包含該語句的共享SQL區相關聯。
因而,若能使語句得到共享,則其將減少記憶體的佔用,同時,減少了cpu 的佔用,加快了語句執行的速度。
即使一個游標仍處於開啟狀態,若其很久未被使用了,則其共享區也可能被從共享池中移出。若該游標以後又被用於執行其語句,則Oracle重解析該語句並且在共享池中分配一新的共享SQL區。
 
4、私有SQL區
私有SQL區包含繫結資訊及執行時緩衝等資料。 每一個提交一個SQL語句的會話均有一個私有SQL區。 每一提交相同SQL語句的使用者有其使用單一共享SQL區的私有SQL區。許多私有SQL區可以與同一共享SQL區相關聯
一個私有SQL區包括一個永久區和一個執行時區:
一個永久區包含在執行過程中保持的繫結資訊,資料型別轉換的程式碼(在定義的資料型別與查詢列的資料型別不一致時), 及其它狀態資訊(比如遞迴或遠端游標數或並行查詢的狀態)。 永久區的尺寸決定於繫結變數的數目及語句中指定的列數。 例如, 若一個查詢中指定了很多列,則永久區要大一些。
執行時區包含SQL語句被執行時使用的一些資訊。 執行時區的尺寸信賴於被執行的SQL語句的型別及其複雜性及被該語句處理的行的尺寸。 一般而言, 用於INSERT, UPDATE, 及 DELETE 的語句其執行區要比 SELECT 語句所需的執行區尺寸要小。
 
 繫結變數和BIND PEEKING

使用繫結變數可以減少SQL PARSE,但是使用繫結變數有一個不好的地方,就是對於訪問具有傾斜的列,可能使用錯誤的執行計劃。在Oracle 9i之前,如果WHERE 條件裡面全部使用繫結變數,那麼只能使用固定的選擇性引數來確定執行計劃。

=操作和>=操作的選擇性為5%,範圍掃描的選擇性為25%。預設值的方式可能生成不好的執行計劃。所以 Oracle 9i就出現了一個新的技術,bind peeking。什麼是bind peeking呢?當SQL第一次執行的時候,最佳化器會根據繫結變數來確定執行計劃(如果存在柱狀圖)。BIND PEEKING只有當該SQL第一次執行的時候,進行HARD PARSE的時候才進行,第二次呼叫該SQL,就不會再次進行BIND PEEKING。這種情況下,就存在另外一個風險,如果某個列的傾斜性很厲害,那麼使用BIND PEEKING就是不安全的,因為不同的引數代入,只能走第一次執行時的執行計劃,那麼執行計劃就像擲色子一樣,要靠運氣了。碰到這種情況,應用就不應該使用繫結變數,而應該改為直接值了。

下面是一個實驗:

1、建立一個表create table tt2 as select *  from dba_objects; 也可以再執行幾次,把記錄數搞大一些

2、update tt2 set status='INVALID' where rownum<100;

3、update tt2 set status='IND' where status='VALID' and rownum<3;

4、經過上述操作,就會產生幾萬valid,89條 invalid和2條ind的記錄。下面進行分析

5、create index ind_tt2_status on tt2 (status);

SQL> EXEC DBMS_STATS.GATHER_TABLE_STATS('SCOTT','TT2',method_opt => 'for all indexed columns  size skewonly');

PL/SQL 過程已成功完成。

SQL> select table_name,COLUMN_NAME,endpoint_number,ENDPOINT_VALUE,ENDPOINT_ACTUAL_VALUE from user_tab_histograms where table_name='T
T2' ORDER BY COLUMN_nAME;

TABLE_NAME                     COLUMN_NAME                    ENDPOINT_NUMBER ENDPOINT_VALUE ENDPOINT_ACTUAL_VALU
------------------------------ ------------------------------ --------------- -------------- --------------------
TT2                            STATUS                                      21     3.8063E+35
TT2                            STATUS                                   29819     4.4786E+35
TT2                            STATUS                                       2     3.8063E+35

 

6、執行:

var a varchar2(10);

exec :a:='VALID';

select object_id,status from tt2  where status=:a;

7、檢視執行計劃(這個時候最好不要用AUTOTRACE,由於BUG,AUTOTRACE顯示的執行計劃是錯誤的),可以透過v$sql_plan,或者乾脆用sql_trace

  檢視sql_trace的時候,如果看裸檔案,那麼parse #1 。。。mis=1說明是硬解析,否則就是軟解析

  我們使用v$_sql_plan來看:

Plan Table
--------------------------------------------------------------------------------------------------------------------------------
--------
| Operation | Name | Starts | E-Rows | A-Rows | Buffers | Reads | Writes | E-Time |
------------------------------------------------------------------------------------------------------------------------
|  TABLE ACCESS FULL              |TT2                 |      0 |    123K|    123K|       0 |      0 |      0 |      0 |
| SELECT STATEMENT                |                    |      0 |        |    123K|       0 |      0 |      0 |      0 |
------------------------------------------------------------------------------------------------------------------------

8、然後在執行:

exec :a:='INVALID';

select object_id,status from tt2  where status=:a;

檢視執行計劃和上面的相同

Plan Table
--------------------------------------------------------------------------------------------------------------------------------
--------
| Operation | Name | Starts | E-Rows | A-Rows | Buffers | Reads | Writes | E-Time |
------------------------------------------------------------------------------------------------------------------------
|  TABLE ACCESS FULL              |TT2                 |      0 |    123K|    123K|       0 |      0 |      0 |      0 |
| SELECT STATEMENT                |                    |      0 |        |    123K|       0 |      0 |      0 |      0 |
------------------------------------------------------------------------------------------------------------------------
如果生成了TRACE檔案,可以看到:

SELECT OBJECT_NAME,STATUS
FROM
 TT2 WHERE STATUS=:A


call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        2      0.00       0.01          0          0          0           0
Execute      2      0.00       0.00          0          0          0           0
Fetch     8213      0.56       0.59          0      18352          0      123154
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total     8217      0.56       0.60          0      18352          0      123154

Misses in library cache during parse: 1
Optimizer goal: CHOOSE
Parsing user id: 59

Rows     Row Source Operation
-------  ---------------------------------------------------
  4      TABLE ACCESS FULL TT2

從這裡可以看出執行了2次parse,其中misses in library cache during parse:1,說明其中一次是硬分析(從裸檔案也可以看出mis=1的有一次)

9、重新整理一下共享池alter system flush shared_pool;

10、然後再執行

exec :a:='INVALID';

select object_id,status from tt2  where status=:a;

看到的執行計劃如下:

Plan Table
-------------------------------------------------------------------------------------------------------------------------------
--------
| Operation | Name | Starts | E-Rows | A-Rows | Buffers | Reads | Writes | E-Time |
------------------------------------------------------------------------------------------------------------------------
|   INDEX RANGE SCAN              |IDX_TT2_STATUS      |      0 |    178 |    123K|       0 |      0 |      0 |      0 |
|  TABLE ACCESS BY INDEX ROWID    |TT2                 |      0 |    178 |    123K|       0 |      0 |      0 |      0 |
| SELECT STATEMENT                |                    |      0 |        |    123K|       0 |      0 |      0 |      0 |
------------------------------------------------------------------------------------------------------------------------

11、再執行:

exec :a:='VALID';

select object_id,status from tt2  where status=:a;

看到的執行計劃還是和上面的相同,走索引範圍掃描。

12、這就是bind peeking的含義,使用繫結變數的時候,能夠共享SQL,其執行計劃是在第一次parse的時候做peeking生成的。根據當前的繫結變數的值。今後再次執行,就認為SQL是安全的,不再進行peeking,也不會生成新的執行計劃

13、如果刪除柱狀圖,那麼還是否會產生PEEKING呢?

SQL> exec dbms_stats.delete_column_stats(ownname=>'SCOTT',TABNAME=>'TT2',COLNAME=>'STATUS');

PL/SQL 過程已成功完成。

 

SQL> exec :a:='VALID';

PL/SQL 過程已成功完成。

SQL> select object_id,status from tt2 where status=:a;

已選擇123150行。


Execution Plan
----------------------------------------------------------
   0      SELECT STATEMENT ptimizer=CHOOSE (Cost=2 Card=1233 Bytes=22
          194)

   1    0   TABLE ACCESS (BY INDEX ROWID) OF 'TT2' (Cost=2 Card=1233 B
          ytes=22194)

   2    1     INDEX (RANGE SCAN) OF 'IDX_TT2_STATUS' (NON-UNIQUE) (Cos
          t=1 Card=493)

Statistics
----------------------------------------------------------
        303  recursive calls
          0  db block gets
      18388  consistent gets
       1955  physical reads
          0  redo size
    2185342  bytes sent via SQL*Net to client
      90802  bytes received via SQL*Net from client
       8211  SQL*Net roundtrips to/from client
          6  sorts (memory)
          0  sorts (disk)
     123150  rows processed

Plan Table
---------------------------------------------------------------------------------------------------------------------------
--------
| Operation | Name | Starts | E-Rows | A-Rows | Buffers | Reads | Writes | E-Time |
------------------------------------------------------------------------------------------------------------------------
|   INDEX RANGE SCAN              |IDX_TT2_STATUS      |        |    493 |        |         |        |        |        |
|  TABLE ACCESS BY INDEX ROWID    |TT2                 |        |      1K|        |         |        |        |        |
| SELECT STATEMENT                |                    |        |        |        |         |        |        |        |
------------------------------------------------------------------------------------------------------------------------

在沒有柱狀圖的情況下,最佳化器採用了預設值,認為選擇性是5%,毫不猶豫就走了索引。

 

14、以上測試是在CURSOR_SHARING=EXACT情況的,再來看看在CURSOR_SHARING=SIMILAR的情況下是什麼樣的?

SQL> exec :a:='VALID';

PL/SQL 過程已成功完成。

SQL> select object_id,status from tt2 where status=:a;

已選擇123150行。

檢視執行計劃,發現是走全表掃描,再執行:

SQL> exec :a:='INVALID';

PL/SQL 過程已成功完成。

SQL> select object_id,status from tt2 where status=:a;

已選擇123150行。

設定:a='INVALID';

執行還是全表掃描,說明第二次執行的時候BIND PEEKING沒有出現。而第一次執行出現了bind peeking,再次確認:

Plan Table
------------------------------------------------------------------------------------------------------------------------------
--------
| Operation | Name | Starts | E-Rows | A-Rows | Buffers | Reads | Writes | E-Time |
------------------------------------------------------------------------------------------------------------------------
|   INDEX RANGE SCAN              |IDX_TT2_STATUS      |        |    178 |        |         |        |        |        |
|  TABLE ACCESS BY INDEX ROWID    |TT2                 |        |    178 |        |         |        |        |        |
| SELECT STATEMENT                |                    |        |        |        |         |        |        |        |
------------------------------------------------------------------------------------------------------------------------
走了索引,確認了第一次執行是進行了bind peeking的。

15、下面測試cursor_sharing=force的情況

SQL> alter session set cursor_sharing=force;

會話已更改。

SQL> alter system flush shared_pool;

系統已更改。

SQL> exec :a:='INVALID';

PL/SQL 過程已成功完成。

SQL> select object_id,status from tt2 where status=:a;

已選擇178行。


Plan Table
-----------------------------------------------------------------------------------------------------------------------------
--------
| Operation | Name | Starts | E-Rows | A-Rows | Buffers | Reads | Writes | E-Time |
------------------------------------------------------------------------------------------------------------------------
|   INDEX RANGE SCAN              |IDX_TT2_STATUS      |        |    178 |        |         |        |        |        |
|  TABLE ACCESS BY INDEX ROWID    |TT2                 |        |    178 |        |         |        |        |        |
| SELECT STATEMENT                |                    |        |        |        |         |        |        |        |
------------------------------------------------------------------------------------------------------------------------
第一次執行也是做了BIND PEEKING的,

 

 

16、結論:

    * BIND PEEKING對於使用繫結變數的情況下,選擇較優的執行計劃有一定的作用
    * BIND PEEKING只有在存在柱狀圖的情況下才能工作
    * BIND PEEKING只在做HARD PARSE的時候才產生,隨後的執行如果不需要HARD PARSE就不會進行BIND PEEKING,這種情況和CURSOR_SHARING無關
    * 由於以上原因,使用繫結變數的時候可以有效的減少PARSE
    * 對於使用不同繫結變數執行計劃應該不同的情況,建議不要使用繫結變數,否則可能會產生隨機的執行計劃(硬分析後的所有執行都使用第一個執行計劃,執行計劃和第一次執行的引數有關)

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/79499/viewspace-159313/,如需轉載,請註明出處,否則將追究法律責任。

相關文章