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至少2048。由于前阵子比较忙,拖延症犯了,另外也跟他缺少沟通,不过有个志同道合的人讨论感兴趣的技术话题是一件幸事。最近有时间,看完了他的关于这个问题的多篇文章,学到了不少东西,也咨询了一下他一下具体细节,具体测试了一下,感觉他的测试方法有点复杂,部分结论过早给出定论了! 在这里结合自己的理解,重新测试、演示一下,仅供参考,如有不足和错误的地方,敬请指正。下面测试环境为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

 

如果你对这种方式存在质疑的话,也可以使用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到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、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

 

 

然后我们插入一个值256,继续测试发现,此时出现hash 冲突了, 标量函数调用的次数变成了256了。我想这也是Tom大神给出推断的原因。当然你可以多插入一些值,继续深入测试。所以从这个也可以推翻网友lfree的测试结论。

 

INSERT INTO TEST

SELECT 256 FROM DUAL

COMMIT;

 

 

clip_image005

 

 

clip_image006

 

 

然后我们清空表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_image007

 

clip_image008

 

如果我们插入数据的顺序修改一下,如下所示,此时的测试结果又让我纳闷了

 


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

 

 

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

 

clip_image010

 

 

这里究竟是怎么回事呢? 思来想去,没有一个很合理的解释,个人猜测,是否这个内部哈希表也有一些内部算法,会将之前一些缓存的数据移除哈希表,插入一些新的hash值,类似于堆栈的数据进出,当然算法肯定要比这个假设复杂一些。透过现象看本质,有时候,局限于知识、认知、眼界等多方面因素,可能并不能透过现象看到本质,更何况这些是封闭的,官方没有相关解释。所以我们只能透过现象做出一些推理和论证,而很难跨过现象直至本质。

 

 

 

 

参考资料:

 

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

猜你喜欢

转载自www.cnblogs.com/kerrycode/p/9223093.html