ORACLE中Scalar subquery Caching的hash table大小測試淺析


 

前陣子總結了這篇ORACLE當中自定義函數性優化淺析博客,里面介紹了標量子查詢緩存(scalar subquery caching),如果使用標量子查詢緩存,ORACLE會將子查詢結果緩存在哈希表中,如果后續的記錄出現同樣的值,優化器通過緩存在哈希表中的值,判斷重復值不用重復調用函數,直接使用上次計算結果即可。從而減少調用函數次數,從而達到優化性能的效果。另外在ORACLE 10和11中, 哈希表只包含了255個Buckets,也就是說它能存儲255個不同值,如果超過這個范圍,就會出現散列沖突。 更多詳細新可以參考我那篇博客

 

當然,哈希表只包含了255個Buckets是怎么來的呢?這個是Tom大神推算而來,我也沒有測試過,后面網友lfree反饋他的測試結果跟這個結果不同。他反饋在ORACLE 10g下,測試結果實際上是512, ORACLE 11g為1024。由於前陣子比較忙,拖延症犯了,另外也跟他缺少溝通,不過有個志同道合的人討論感興趣的技術話題是一件幸事。最近有時間,看完了他的關於這個問題的多篇文章,學到了不少東西,也咨詢了一下他一下具體細節,具體測試了一下,感覺他的測試方法有點復雜,部分結論過早給出定論了! 但是自己也沒有一個合理的測試驗證方法。遂啃了一下Tom大神的On Caching and Evangelizing SQL這篇雄文。在這里結合自己的理解,重新演示一下,下面測試環境為Oracle 11g,關於Hash Table,估計有些人會比較懵,借用Tom大神的述說:

 

 

You cannot 'see' the hash table anywhere, it is an internal data structure that lives in your session memory for the duration of the query. Once the query is finished - it goes away.

 

It is a cache associated with your query - nothing more, nothing less.

 

You can "see" it in action by measuring how many times your function is called, for example: 

 

 

   

首先,創建這個自定義函數,這個函數是用來驗證哈希表大小的關鍵所在(確實是一個構造很巧妙,而且又簡單的函數。大神真不是蓋的)。如果對函數dbms_application_info.set_client_info不了解的,自行搜索、學習這個知識點!

 

create or replace function f( x in varchar2 ) return number
as
begin
        dbms_application_info.set_client_info(userenv('client_info')+1 );
        return length(x);
end

 

然后創建測試表,插入測試數據。然后就可以開始我們的測試,

 

 

CREATE TABLE TEST(ID NUMBER);
INSERT INTO TEST
SELECT 1 FROM DUAL UNION ALL
SELECT 1 FROM DUAL UNION ALL
SELECT 1 FROM DUAL UNION ALL
SELECT 2 FROM DUAL UNION ALL
SELECT 2 FROM DUAL UNION ALL
SELECT 2 FROM DUAL UNION ALL
SELECT 3 FROM DUAL UNION ALL
SELECT 3 FROM DUAL;
COMMIT;

 

准備好上述測試環境,我們就可以用下面腳本來測試、驗證標量函數被調用了多少次(注意下面這段腳本會被多次使用,下面測試部分會多次使用,后續可能直接稱呼其為test.sql,而不會每次貼出這段腳本

 

variable cpu number;
begin
   :cpu := dbms_utility.get_cpu_time; 
      dbms_application_info.set_client_info(0);
end;
select id,(select f(id) from dual) as client_info from test;
select dbms_utility.get_cpu_time- :cpu cpu_hsecs, 
             userenv('client_info') 
from dual;

 

我們可以看到測試結果userenv('client_info')的值為3, 這意味着標量函數被遞歸調用了3次(如果不理解的話,多補一下基礎知識)

 

 

clip_image001[1]

 

如果你對這種方式存在質疑的話,也可以使用10046 trace找到SQL的真實執行計划。具體SQL如下所

 

alter session set events '10046 trace name context  forever,level 12'; 
 
select id,(select f(id) from dual) as client_info from test;
 
alter session set events '10046 trace name context off'
 
SELECT T.value 
       || '/' 
       || Lower(Rtrim(I.INSTANCE, Chr(0))) 
       || '_ora_' 
       || P.spid 
       || '.trc' TRACE_FILE_NAME 
FROM   (SELECT P.spid 
        FROM   v$mystat M, 
               v$session S, 
               v$process P 
        WHERE  M.statistic# = 1 
               AND S.sid = M.sid 
               AND P.addr = S.paddr) P, 
       (SELECT T.INSTANCE 
        FROM   v$thread T, 
               v$parameter V 
        WHERE  V.name = 'thread' 
               AND ( V.value = 0 
                      OR T.thread# = To_number(V.value) )) I, 
       (SELECT value 
        FROM   v$parameter 
        WHERE  name = 'user_dump_dest') T;

 

找到測試生成的trace文件,格式化后,如下截圖所示,FAST DUAL表示執行子查詢的次數,也就是遞歸調用次數。

 

 

[oracle@DB-Server trace]$ tkprof gsp_ora_11336.trc klb_out.txt

 

clip_image002[1]

 

 

刪除這個表,然后我們構造一個擁有從1到255的新表,然后執行test.sql,測試看看標量函數會調用多少次,如下所示:

 

 

SQL> drop table test purge;
 
Table dropped.
 
SQL> create table test as select rownum id from dual connect by level<=255;
 
Table created.

 

 

如下所示,可以看到當前情況下,標量函數執行了255次

 

clip_image003[1]

 

 

然后插入1、2、 3 三個值,我們再執行一下test.sql,看看優化器是否使用哈希表中緩存的記錄,減少函數調用次數。如下所示,函數還是只調用了255次。

 

 

INSERT INTO TEST
SELECT 1 FROM DUAL UNION ALL
SELECT 2 FROM DUAL UNION ALL
SELECT 3 FROM DUAL;
COMMIT;

 

 

clip_image004[1]

 

 

 

然后我們清空表TEST中的數據,然后使用下面腳本構造相關數據后, 執行test.sql繼續我們的測試。

 

SQL> TRUNCATE TABLE TEST;
 
Table truncated.
 
SQL> DECLARE RowIndex NUMBER;
  2  BEGIN
  3  RowIndex :=1;
  4  WHILE RowIndex <= 255 LOOP
  5      INSERT INTO TEST
  6      SELECT RowIndex  FROM DUAL;
  7      
  8       RowIndex := RowIndex +1;
  9  END LOOP;
 10  COMMIT;
 11  END;
 12  /
 
PL/SQL procedure successfully completed.
 
SQL> DECLARE RowIndex NUMBER;
  2  BEGIN
  3  RowIndex :=1;
  4  WHILE RowIndex <= 255 LOOP
  5      INSERT INTO TEST
  6      SELECT RowIndex  FROM DUAL;
  7      
  8       RowIndex := RowIndex +1;
  9  END LOOP;
 10  COMMIT;
 11  END;
 12  /
 
PL/SQL procedure successfully completed.
 
SQL> 

 

 

clip_image005[1]

 

clip_image006[1]

 

 

其實這里出現這個問題,是因為1-255中,有些數因為HASH沖突,導致無法緩存到哈希表中,我們來驗證測試一下,如下所示,9和16出現HASH沖突(為什么會出現HASH沖突,這個不清楚,因為我們不清楚它的HASH算法),由於9和16出現HASH 沖突,從而導致16無法緩存到哈希表,從而導致兩條16的記錄調用了兩次,所以標量函數被調用了3次。但是如果出現沖突的記錄,兩次重復出現,那么它會重用上一次的調用函數的結果。如下測試所示:

 

clip_image007[1]

 

我們繼續往表TEST里面插入一條ID=16的記錄, 我們開始測試

 

SQL> INSERT INTO TEST VALUES(16);
 
1 row created.
 
SQL> COMMIT;
 
SQL> select id,(select f(id) from dual) from test where id in (9,16);
 
        ID (SELECTF(ID)FROMDUAL)
---------- ---------------------
         9                     9
        16                    16
         9                     9
        16                    16
        16                    16
 
SQL> select dbms_utility.get_cpu_time- :cpu cpu_hsecs, userenv('client_info') from dual;
 
 CPU_HSECS USERENV('CLIENT_INFO')
---------- ----------------------------------------------------------------
         1 3

 

如上所示,自定義函數調用的次數還是3, 按照推理:ID=9的記錄調用一次自定義函數,然后ID=16的記錄出現HASH沖突,調用一次自定義函數,然后到記錄ID=9,發現可以從內存中的哈希表取值,跳過調用自定義函數,接着到ID=16,由於哈希沖突,哈希表沒有緩存相關記錄,那么還會調用一次自定義函數,再接下來ID=16的記錄,由於兩次重復出現,那么它會重用上一次的調用函數的結果。所以調用次數為3

 

clip_image008[1]

 

如果我們接下來繼續插入兩條記錄,一條為9,一條為16,那么調用自定義函數的次數就會變為4,如下所示:

 

SQL> insert into test values(9);
 
1 row created.
 
SQL> insert into test values(16);
 
1 row created.
 
SQL> commit;
 
Commit complete.
 
SQL> variable cpu number;
SQL> begin
  2     :cpu := dbms_utility.get_cpu_time; 
  3       dbms_application_info.set_client_info(0);
  4  end;
  5  /
 
PL/SQL procedure successfully completed.
 
SQL>    
SQL> select id,(select f(id) from dual) from test where id in(9,16);
 
 
        ID (SELECTF(ID)FROMDUAL)
---------- ---------------------
         9                     9
        16                    16
         9                     9
        16                    16
        16                    16
         9                     9
        16                    16
 
7 rows selected.
 
SQL> SQL> select dbms_utility.get_cpu_time- :cpu cpu_hsecs, userenv('client_info') from dual;
 
 CPU_HSECS USERENV('CLIENT_INFO')
---------- ----------------------------------------------------------------
         1 4
 
SQL> 

 

 

如果我們插入數據的順序修改一下,如下所示,此時的測試結果就能理解了(之前我一直沒有理解清楚,注意之前的截圖,你就能理解一二了,如果插入1~255  然后插入 1~255 這里函數的調用次數為306, 如果插入的記錄為1、1、2、2 ....255、255 函數調用次數為255)

 

SQL> TRUNCATE TABLE TEST;
 
Table truncated.
 
SQL> DECLARE RowIndex NUMBER;
  2  BEGIN
  3  RowIndex :=1;
  4  WHILE RowIndex <= 255 LOOP
  5      INSERT INTO TEST
  6      SELECT RowIndex  FROM DUAL UNION ALL
  7      SELECT RowIndex  FROM DUAL;
  8      
  9       RowIndex := RowIndex +1;
 10  END LOOP;
 11  COMMIT;
 12  END;
 13  /
 
PL/SQL procedure successfully completed.

 

clip_image009[1]

 

 

那么我們接下來分析一下,標量子查詢緩存中生成的哈希表到底能緩存多少條記錄呢? 

 

推理如下 306-255 =51  表示1-255 記錄里面,有51個記錄跟其它記錄存在哈希沖突,那么哈希表中實際緩存255-51=204條記錄,然后我們將上面實驗的值放大到510,繼續測試

 

TRUNCATE TABLE TEST;
 
DECLARE RowIndex NUMBER;
BEGIN
RowIndex :=1;
WHILE RowIndex <= 510 LOOP
    INSERT INTO TEST
    SELECT RowIndex  FROM DUAL;
    
     RowIndex := RowIndex +1;
END LOOP;
COMMIT;
END;
/
 
DECLARE RowIndex NUMBER;
BEGIN
RowIndex :=1;
WHILE RowIndex <= 510 LOOP
    INSERT INTO TEST
    SELECT RowIndex  FROM DUAL;
    
     RowIndex := RowIndex +1;
END LOOP;
COMMIT;
END;
/

 

clip_image010[1]

 

接着分析, 707- 510 = 197  這意味着197個數據存在哈希沖突, 假設內存中的哈希表緩存了510-197=313條記錄, 那么313 + 197 + 197 = 707。 假設這個哈希表只能緩存255 bucket的話, 那么按照推理,函數調用次數應該為255 +(510-255)*2 = 765次,顯然跟實際次數有出入,那么說明這個值應該大於255。

 

 

SQL> select 313 +197 from dual;

 

   313+197

----------

       510

 

SQL> select 313 + 197 + 197 from dual;

 

313+197+197

-----------

        707

 

我們繼續放大插入的值,繼續后面測試,后面測試其實我已經無法繼續推理,例如,插入2048連續記錄,然后插入2048條連續記錄,測試發現函數的調用次數為3592

 

假設哈希表只能緩存1024條記錄, 那么 1024+ (2048-1024)*2 = 3072  < 3592 ,這是否意味着哈希表不止緩存1024條記錄,其實,到目前為止,我們只發現了部分記錄存在HASH沖突,上述測試也是存在假設前提的,例如9 跟 16 存在HAST沖突,那么是否還存在其它值跟它們HASH 沖突呢? 測試越來越復雜,個人在這上面花費了大量的時間,其實是有點不划算的。

 

透過現象看本質,有時候,局限於知識、認知、眼界,可能並不能透過現象看到本質,更何況這個也是封閉的,官方沒有相關解釋。所以我們只能透過現象做出一些推理和論證,而很難跨過現象直至本質。

 

 

 

結論:

 

    網友lfree的反饋是對的。標量子查詢緩存(scalar subquery caching)中的哈希表緩存的buckets,在ORACLE 10g / 11g 下面確實不止255, 但是這個值到底是多少,這篇博文無法給出一個確切值!

 

       

 

 

參考資料:

 

https://asktom.oracle.com/pls/apex/f?p=100:11:0::::P11_QUESTION_ID:2683853500346598211

https://blogs.oracle.com/oraclemagazine/on-caching-and-evangelizing-sql


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM