前阵子总结了这篇“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不了解的,自行搜索、学习这个知识点!

  1. create or replace function f( x in varchar2 ) return number

  1. as

  1. begin

  1.         dbms_application_info.set_client_info(userenv('client_info')+1 );

  1.         return length(x);

  1. end

然后创建测试表,插入测试数据。然后就可以开始我们的测试,

  1. CREATE TABLE TEST(ID NUMBER);

  1. INSERT INTO TEST

  1. SELECT 1 FROM DUAL UNION ALL

  1. SELECT 1 FROM DUAL UNION ALL

  1. SELECT 1 FROM DUAL UNION ALL

  1. SELECT 2 FROM DUAL UNION ALL

  1. SELECT 2 FROM DUAL UNION ALL

  1. SELECT 2 FROM DUAL UNION ALL

  1. SELECT 3 FROM DUAL UNION ALL

  1. SELECT 3 FROM DUAL;

  1. COMMIT;

准备好上述测试环境,我们就可以用下面脚本来测试、验证标量函数被调用了多少次(注意下面这段脚本会被多次使用,下面测试部分会多次使用,后续可能直接称呼其为test.sql,而不会每次贴出这段脚本)

  1. variable cpu number;

  1. begin

  1.    :cpu := dbms_utility.get_cpu_time;

  1.       dbms_application_info.set_client_info(0);

  1. end;

  1. / 

  1. select id,(select f(id) from dual) as client_info from test;

  1. select dbms_utility.get_cpu_time- :cpu cpu_hsecs,

  1.              userenv('client_info')

  1. from dual;

我们可以看到测试结果userenv('client_info')的值为3, 这意味着标量函数被递归调用了3次(如果不理解的话,多补一下基础知识)

如果你对这种方式存在质疑的话,也可以使用10046 trace找到SQL的真实执行计划。具体SQL如下所

  1. alter session set events '10046 trace name context  forever,level 12';

  1.  

  1. select id,(select f(id) from dual) as client_info from test;

  1.  

  1. alter session set events '10046 trace name context off'; 

  1.  

  1. SELECT T.value 

  1.        || '/' 

  1.        || Lower(Rtrim(I.INSTANCE, Chr(0)))

  1.        || '_ora_' 

  1.        || P.spid

  1.        || '.trc' TRACE_FILE_NAME

  1. FROM   (SELECT P.spid

  1.         FROM   v$mystat M,

  1.                v$session S,

  1.                v$process P

  1.         WHERE  M.statistic# = 1

  1.                AND S.sid = M.sid

  1.                AND P.addr = S.paddr) P,

  1.        (SELECT T.INSTANCE

  1.         FROM   v$thread T,

  1.                v$parameter V

  1.         WHERE  V.name = 'thread' 

  1.                AND ( V.value = 0

  1.                       OR T.thread# = To_number(V.value) )) I,

  1.        (SELECT value 

  1.         FROM   v$parameter 

  1.         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,测试看看标量函数会调用多少次,如下所示:

  1. SQL> drop table test purge;

  1.  

  1. Table dropped.

  1.  

  1. SQL> create table test as select rownum id from dual connect by level<=255;

  1.  

  1. Table created.

如下所示,可以看到当前情况下,标量函数执行了255次

然后插入1、2、 3 三个值,我们再执行一下test.sql,看看优化器是否使用哈希表中缓存的记录,减少函数调用次数。如下所示,函数还是只调用了255次。

  1. INSERT INTO TEST

  1. SELECT 1 FROM DUAL UNION ALL

  1. SELECT 2 FROM DUAL UNION ALL

  1. SELECT 3 FROM DUAL;

  1. COMMIT;

然后我们清空表TEST中的数据,然后使用下面脚本构造相关数据后, 执行test.sql继续我们的测试。

  1. SQL> TRUNCATE TABLE TEST;

  1.  

  1. Table truncated.

  1.  

  1. SQL> DECLARE RowIndex NUMBER;

  1.   2  BEGIN

  1.   3  RowIndex :=1;

  1.   4  WHILE RowIndex <= 255 LOOP

  1.   5      INSERT INTO TEST

  1.   6      SELECT RowIndex  FROM DUAL;

  1.   7     

  1.   8       RowIndex := RowIndex +1;

  1.   9  END LOOP;

  1. 10  COMMIT;

  1. 11  END;

  1. 12  /

  1.  

  1. PL/SQL procedure successfully completed.

  1.  

  1. SQL> DECLARE RowIndex NUMBER;

  1.   2  BEGIN

  1.   3  RowIndex :=1;

  1.   4  WHILE RowIndex <= 255 LOOP

  1.   5      INSERT INTO TEST

  1.   6      SELECT RowIndex  FROM DUAL;

  1.   7     

  1.   8       RowIndex := RowIndex +1;

  1.   9  END LOOP;

  1. 10  COMMIT;

  1. 11  END;

  1. 12  /

  1.  

  1. PL/SQL procedure successfully completed.

  1.  

  1. SQL>

其实这里出现这个问题,是因为1-255中,有些数因为HASH冲突,导致无法缓存到哈希表中,我们来验证测试一下,如下所示,9和16出现HASH冲突(为什么会出现HASH冲突,这个不清楚,因为我们不清楚它的HASH算法),由于9和16出现HASH 冲突,从而导致16无法缓存到哈希表,从而导致两条16的记录调用了两次,所以标量函数被调用了3次。但是如果出现冲突的记录,两次重复出现,那么它会重用上一次的调用函数的结果。如下测试所示:

我们继续往表TEST里面插入一条ID=16的记录, 我们开始测试

  1. SQL> INSERT INTO TEST VALUES(16);

  1.  

  1. 1 row created.

  1.  

  1. SQL> COMMIT;

  1.  

  1. SQL> select id,(select f(id) from dual) from test where id in (9,16);

  1.  

  1.         ID (SELECTF(ID)FROMDUAL)

  1. ---------- ---------------------

  1.          9                     9

  1.         16                    16

  1.          9                     9

  1.         16                    16

  1.         16                    16

  1.  

  1. SQL> select dbms_utility.get_cpu_time- :cpu cpu_hsecs, userenv('client_info') from dual;

  1.  

  1. CPU_HSECS USERENV('CLIENT_INFO')

  1. ---------- ----------------------------------------------------------------

  1.          1 3

如上所示,自定义函数调用的次数还是3, 按照推理:ID=9的记录调用一次自定义函数,然后ID=16的记录出现HASH冲突,调用一次自定义函数,然后到记录ID=9,发现可以从内存中的哈希表取值,跳过调用自定义函数,接着到ID=16,由于哈希冲突,哈希表没有缓存相关记录,那么还会调用一次自定义函数,再接下来ID=16的记录,由于两次重复出现,那么它会重用上一次的调用函数的结果。所以调用次数为3

如果我们接下来继续插入两条记录,一条为9,一条为16,那么调用自定义函数的次数就会变为4,如下所示:

  1. SQL> insert into test values(9);

  1.  

  1. 1 row created.

  1.  

  1. SQL> insert into test values(16);

  1.  

  1. 1 row created.

  1.  

  1. SQL> commit;

  1.  

  1. Commit complete.

  1.  

  1. SQL> variable cpu number;

  1. SQL> begin

  1.   2     :cpu := dbms_utility.get_cpu_time;

  1.   3       dbms_application_info.set_client_info(0);

  1.   4  end;

  1.   5  /

  1.  

  1. PL/SQL procedure successfully completed.

  1.  

  1. SQL>   

  1. SQL> select id,(select f(id) from dual) from test where id in(9,16);

  1.  

  1.  

  1.         ID (SELECTF(ID)FROMDUAL)

  1. ---------- ---------------------

  1.          9                     9

  1.         16                    16

  1.          9                     9

  1.         16                    16

  1.         16                    16

  1.          9                     9

  1.         16                    16

  1.  

  1. 7 rows selected.

  1.  

  1. SQL> SQL> select dbms_utility.get_cpu_time- :cpu cpu_hsecs, userenv('client_info') from dual;

  1.  

  1. CPU_HSECS USERENV('CLIENT_INFO')

  1. ---------- ----------------------------------------------------------------

  1.          1 4

  1.  

  1. SQL>

如果我们插入数据的顺序修改一下,如下所示,此时的测试结果就能理解了(之前我一直没有理解清楚,注意之前的截图,你就能理解一二了,如果插入1~255  然后插入 1~255 这里函数的调用次数为306, 如果插入的记录为1、1、2、2 ....255、255 函数调用次数为255)

  1. SQL> TRUNCATE TABLE TEST;

  1.  

  1. Table truncated.

  1.  

  1. SQL> DECLARE RowIndex NUMBER;

  1.   2  BEGIN

  1.   3  RowIndex :=1;

  1.   4  WHILE RowIndex <= 255 LOOP

  1.   5      INSERT INTO TEST

  1.   6      SELECT RowIndex  FROM DUAL UNION ALL

  1.   7      SELECT RowIndex  FROM DUAL;

  1.   8     

  1.   9       RowIndex := RowIndex +1;

  1. 10  END LOOP;

  1. 11  COMMIT;

  1. 12  END;

  1. 13  /

  1.  

  1. PL/SQL procedure successfully completed.

那么我们接下来分析一下,标量子查询缓存中生成的哈希表到底能缓存多少条记录呢?

推理如下 306-255 =51  表示1-255 记录里面,有51个记录跟其它记录存在哈希冲突,那么哈希表中实际缓存255-51=204条记录,然后我们将上面实验的值放大到510,继续测试

  1. TRUNCATE TABLE TEST;

  1.  

  1. DECLARE RowIndex NUMBER;

  1. BEGIN

  1. RowIndex :=1;

  1. WHILE RowIndex <= 510 LOOP

  1.     INSERT INTO TEST

  1.     SELECT RowIndex  FROM DUAL;

  1.    

  1.      RowIndex := RowIndex +1;

  1. END LOOP;

  1. COMMIT;

  1. END;

  1. /

  1.  

  1. DECLARE RowIndex NUMBER;

  1. BEGIN

  1. RowIndex :=1;

  1. WHILE RowIndex <= 510 LOOP

  1.     INSERT INTO TEST

  1.     SELECT RowIndex  FROM DUAL;

  1.    

  1.      RowIndex := RowIndex +1;

  1. END LOOP;

  1. COMMIT;

  1. END;

  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

ORACLE中Scalar subquery Caching的hash table大小测试浅析的更多相关文章

  1. 【Oracle】Oracle中复合数据类型

    1,常见的操作数据库的技术有那些 jdbc     使用java 访问数据库的技术    PLSQL  (procedure  过程化sql) 在数据库内部操作数据的技术    proc/c++    ...

  2. [转载] 散列表(Hash Table)从理论到实用(中)

    转载自:白话算法(6) 散列表(Hash Table)从理论到实用(中) 不用链接法,还有别的方法能处理碰撞吗?扪心自问,我不敢问这个问题.链接法如此的自然.直接,以至于我不敢相信还有别的(甚至是更好 ...

  3. ORACLE中RECORD、VARRAY、TABLE的使用详解(转)

    原文地址:ORACLE中RECORD.VARRAY.TABLE的使用详解

  4. ORACLE中RECORD、VARRAY、TABLE、IS REF CURSOR 的使用及实例详解

    ORACLE中RECORD.VARRAY.TAB.IS REF CURSOR LE的使用及实例详解 create or replaceprocedure PRO_RECORD_ROW_TAB_EXAM ...

  5. Bullet:关于ORACLE中的HASH JOIN的参数变化

    Oracle在7.3引入了hash join. 但是在Oracle 10g及其以后的Oracle数据库版本中,优化器,实际是CBO,也是因为HASH JOIN仅适用于CBO,在解析目标SQL时是否考虑 ...

  6. Oracle中使用Table()函数解决For循环中不写成 in (l_idlist)形式的问题

    转: Oracle中使用Table()函数解决For循环中不写成 in (l_idlist)形式的问题 在实际PL/SQL编程中,我们要对动态取出来的一组数据,进行For循环处理,其基本程序逻辑为: ...

  7. [转]Oracle中Hint深入理解

    原文地址:http://czmmiao.iteye.com/blog/1478465 Hint概述 基于代价的优化器是很聪明的,在绝大多数情况下它会选择正确的优化器,减轻了DBA的负担.但有时它也聪明 ...

  8. Oracle中Hint深入理解(原创)

    http://czmmiao.iteye.com/blog/1478465 Hint概述  基于代价的优化器是很聪明的,在绝大多数情况下它会选择正确的优化器,减轻了DBA的负担.但有时它也聪明反被聪明 ...

  9. Oracle中Hint深入理解

    Hint概述 基于代价的优化器是很聪明的,在绝大多数情况下它会选择正确的优化器,减轻了DBA的负担.但有时它也聪明反被聪明误,选择了很差的执行计划,使某个语句的执行变得奇慢无比. 此时就需要DBA进行 ...

随机推荐

  1. sql server 索引阐述系列一索引概述

    一. 索引概述 关于介绍索引,有一种“文章太守,挥毫万字,一饮千钟”的豪迈感觉,因为索引需要讲的知识点太多.在每个关系型数据库里都会作为重点介绍,因为索引关系着数据库的整体性能, 它在数据库性能优化里 ...

  2. Jenkins技巧:如何更新Jenkins到最新版本

    ----------------------------------------------------------------- 原创博文,未经作者允许禁止转载. 博主:疲惫的豆豆 链接:http: ...

  3. SLG手游Java服务器的设计与开发——架构分析

    微信公众号[程序员江湖] 作者黄小斜,斜杠青年,某985硕士,阿里 Java 研发工程师,于 2018 年秋招拿到 BAT 头条.网易.滴滴等 8 个大厂 offer,目前致力于分享这几年的学习经验. ...

  4. 新生命团队netcore服务器免费开放计划

    为了便于大家学习测试netcore,我们计划提供1~3台公网Linux服务器(CentOS/Ubuntu),1vCPU+1G内存+100Mbps,为期1年,每周重置系统修改一次密码.对使用者要求如下: ...

  5. Go基础系列:读取标准输入

    fmt包中提供了3类读取输入的函数: Scan家族:从标准输入os.Stdin中读取数据,包括Scan().Scanf().Scanln() SScan家族:从字符串中读取数据,包括Sscan().S ...

  6. 翻译:last_value()函数(已提交到MariaDB官方手册)

    本文为mariadb官方手册:LAST_VALUE()的译文. 原文:https://mariadb.com/kb/en/last_value/我提交到MariaDB官方手册的译文:https://m ...

  7. Docker容器Tomcat部署war包

    在docker容器中使用tomcat部署war包主要包括四个步骤,创建tomcat容器.上传war包到容器.重启容器.访问应用. 1.创建tomcat容器 使用docker run  -d --nam ...

  8. OJ:重载 << 运算符

    Description 补足程序,使得下面程序输出的结果是: ****100 #include <iostream> #include <string> using names ...

  9. 分布式系统监视zabbix讲解十一之zabbix升级--技术流ken

    思考 现在有这样一个需求,业务场景想要使用的监控模版没有3.0版本的,只有2.0,我们都知道2.0的模版无法导入进3.0版本的zabbix中,这个时候应该怎么获得3.0的监控模版哪?本篇博客将详细演示 ...

  10. 【golang-GUI开发】QSS的使用(一)———QSS入门指南

    在这篇文章中我们将初步体验对qss的使用.并对在goqt中使用qss时的注意事项进行说明. 那么事不宜迟,现在开始我们的qss之旅吧. QSS语法入门 qss是一种与css3相似的控制Qt组件的样式表 ...