前陣子總結了這篇“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次(如果不理解的話,多補一下基礎知識)
如果你對這種方式存在質疑的話,也可以使用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
刪除這個表,然后我們構造一個擁有從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次
然后插入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;
然后我們清空表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>
其實這里出現這個問題,是因為1-255中,有些數因為HASH沖突,導致無法緩存到哈希表中,我們來驗證測試一下,如下所示,9和16出現HASH沖突(為什么會出現HASH沖突,這個不清楚,因為我們不清楚它的HASH算法),由於9和16出現HASH 沖突,從而導致16無法緩存到哈希表,從而導致兩條16的記錄調用了兩次,所以標量函數被調用了3次。但是如果出現沖突的記錄,兩次重復出現,那么它會重用上一次的調用函數的結果。如下測試所示:
我們繼續往表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
如果我們接下來繼續插入兩條記錄,一條為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.
那么我們接下來分析一下,標量子查詢緩存中生成的哈希表到底能緩存多少條記錄呢?
推理如下 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;
/
接着分析, 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