说说简单的NL连接可能面临的性能问题

来源:互联网 发布:淘宝添加客服的代码 编辑:程序博客网 时间:2024/05/01 21:20

    其实是我之前的3篇文章的精简总结版.

    Nested Loops应该是三种连接方式(nl join,hash join,sm join)中最简单的了(当然如果要算上笛卡尔积的话,nl连接比它还是要复杂些),从执行上来说很简单:对外部行源返回的每一行,查看内部行源,看是否存在匹配行,如果存在匹配行的话,输出这一个匹配行(所以first_rows_n优化器模式下,往往会使用nl连接),oracle在评估nl连接的成本时,会使用下面的公式:
Cost of acquiring data from first table + Cardinality of result from first table * Cost of single visit to second table
(引用自Jonathan Lewis的著作<<Cost-Based Oracle Fundamentals>>第十一章Nested Loops,让它有些出处,呵呵)
也就是cost=cost(outer) + card(outer) * cost(inner)
    从这个公式来反推nl连接适用的情形:外部行源返回的结果集行数要少;内部行源单次访问的成本要小,所以往往要求内表在连接列上存在索引,内表的访问路径一般是INDEX (RANGE SCAN) 或者是INDEX (UNIQUE SCAN)
    这里不讨论因为优化器统计信息陈旧甚至是缺少优化器统计信息或者是优化器统计信息收集方法不恰当,造成优化器错误的估算,错误的选择了nl连接的情况.我们假定正常的优化器统计信息的收集是没有问题的,但还是会存在一些情形,导致优化器错误的选择了nl连接:
    1.在某些情形下,oracle会估算内表单次访问代价为0,也就是cost(inner)=0,这样无论card(outer)有多大,card(outer) * cost(inner)=0,也就是说内表的访问是完全免费的,当然会导致oracle选择nl连接,但内表的单次访问代价怎么可能为0呢,虽然说这种情况下一般来说它真实的访问代价确实是极小的,但外部行源很多的话,整个内表的访问代价还是可能很大的,最终导致nl连接性能糟糕.
    2.在某些情况下,oracle会估算card(outer)为1,实际上估算值可能还要低,这种情况下,当然会导致优化器选择nl连接,但实际上外部行源集很大,最终导致nl连接性能糟糕.
下面的案例来自于实际工作环境,数据库版本都是10.2.0.4

    案例1

    这里nl连接的逻辑IO偏高,主要是因为内表访问的总的逻辑IO偏高,但内表访问的cost计算呢?从这里看,应该是单次访问代价是1,总的代价是7,明显这里的代价没有反应实际的逻辑IO,而且外部行源估算是174K,那么内表的总的访问代价应该是174K*1=174K呀?!可这里只有7而已.其实看看10053文件就会明白:
NL Join
  Outer table: Card: 176160.33  Cost: 6864.98  Resp: 6864.98  Degree: 1  Bytes: 208
  Inner table: CATALOGRELATEPRODUCT  Alias: CP1
  Access Path: TableScan
OPTIMIZER PERCENT INDEX CACHING = 60
  Access Path: index (UniqueScan)
    Index: PK_CATALOGRELATEPRODUCT
    resc_io: 0.00  resc_cpu: 1050
    ix_sel: 9.5159e-07  ix_sel_with_filters: 9.5159e-07
    NL Join: Cost: 6872.14  Resp: 6872.14  Degree: 1
      Cost_io: 6824.00  Cost_cpu: 722636557
      Resp_io: 6824.00  Resp_cpu: 722636557
  Best NL cost: 6872.14
          resc: 6872.14 resc_io: 6824.00 resc_cpu: 722636557
          resp: 6872.14 resp_io: 6824.00 resp_cpu: 722636557
    单次内表访问的IO代价为0,当然,我们通常看到的执行计划中cost,card是不会出现为0的,这时候都是显示为1的,内表访问总的代价7实际上是内表访问的cpu成本,这里整个的内表访问的IO成本是0了,为什么呢?其实是这里OPTIMIZER PERCENT INDEX CACHING = 60导致的,这时:
show parameter index
NAME                                 TYPE        VALUE
------------------------------------ ----------- ------
optimizer_index_caching              integer     60
optimizer_index_cost_adj             integer     50
    我们使用默认值:
alter session set optimizer_index_caching=0;
alter session set optimizer_index_cost_adj=100;
语句默认的执行计划已经改变了,使用提示强制选择同样的执行计划时,看到的cost计算已经不同了:

这里cost的计算已经正常了:181K=6887+174K*1 (当然,同一个sql两次执行的实际执行统计信息不同是因为它们不是同一天执行的)

这个问题,可以参考一下这个文档:
http://www.centrexcc.com/papers.html下的<<The Effects of optimizer_index_cost_adj and optimizer_index_caching on Access Plans>>
其实nl连接,内表使用索引扫描时候的IO代价可以这样计算:
nl cost= cost(outer)
+
card(outer)
*optimizer_index_cost_adj/100*
(
blevel +                                                                       
--INDEX UNIQUE SCAN的话,这一部分按0算的
(100-Optimizer_Index_Caching)/100*leaf_blocks * effective index selectivity +  
--这里可能会round或者ceil的,所以Optimizer_Index_Caching<>0时,这一部分结果可能是1甚至是0的,Optimizer_Index_Caching=0时,不会是0
clustering_factor * effective table selectivity                                
--这里可能会round或者ceil的,INDEX UNIQUE SCAN并且回访表的话,这一部分按1算的(ceil的结果)
)
    其实sql语句原来的问题就是因为黑体部分引起的:Optimizer_Index_Caching<>0,并且nl连接的内表使用唯一索引扫描的时候,索引访问的cost可能就会变成0,也就是说不管外表返回的card有多大,内表访问是完全免费的,导致优化器选择了性能很差的执行计划.(我实验的时候,optimizer_index_caching=0,1时都是正常计算的,从2开始包括更大的值时内表访问就不再有代价了,当然这个临界值可能和数据库设置,数据量等有关,并不是一个定值,但确实到某个值时,内表访问就是免费的了)
    说到这里有必要说说optimizer_index_cost_adj,Optimizer_Index_Caching这两个参数:
    optimizer_index_cost_adj是用来平衡单块读和多块读的代价的,10g的时候还是使用系统统计信息为好,使用它的mreadtim,sreadtim,mbrc.
    优化器在考虑IO成本时,都是按照物理读来考量的,也就是说按照这些数据块都不在buffer cache中来考量的,但实际情况可能不是这样的,所以需要考虑数据cache在buffer cache中的影响,所以它引入了Optimizer_Index_Caching这个参数.不过,optimizer_index_caching这个参数是在in列表和nl的内表使用索引访问的时候用到的,对于简单的单表的索引扫描不起作用的.这里只考虑了索引cache的情况,并没有考虑表数据cache的情况,而且这里并没有单独的考虑各个索引的不同情况,而是统一考虑为一个值,这显然也会带来一些问题.
    其实oracle也存在着一种机制可以由数据库统计数据(包括表数据和索引数据)的cache情况,然后在计算cost时使用这些统计数据,从而考虑cache的影响,大体的步骤是这样的:alter system set "_cache_stats_monitor"=true; 开启cache统计信息的收集,sys.cache_stats_1$这里很快就会有数据,然后dbms_stats.gather_table_stats(user,'ZSJ_DIY_USERLOG',stattype => 'cache',cascade => true);将sys.cache_stats_1$里的cache统计信息填充进tab_stats$,ind_stats$,这两个表里的数据用于cbo的cost评估.然后alter session/system set "_optimizer_cache_stats"=true;这样相应的会话里优化器计算COST时就会考虑cache统计信息了.但这样一套机制里,可能存在着一些问题,所以oracle这两个隐含参数默认都是false的,也就是说默认并没有开启这个特性.其实计算cost时要考虑cache的影响时需要考虑的东西太多了,比如说我能想到的问题:数据库刚启动的时候,没有任何的cache数据,这时cost的计算还是按照现在这样的物理IO代价模型来考虑,这样如果它因为各种原因选择了一个不恰当的执行计划的时候,无疑会强化某些cache数据,这样代价模型考虑cache信息时,无疑也会强化这个错误的执行计划的优势,这个执行计划的反复执行又会强化相应的cache信息,无疑这样形成了一个负反馈环,也许这不是你想要的. 另外就是cache信息成为了cost计算的一部分,而数据的cache信息可能在随时变化的,什么时候刷新新的cache数据呢(我是指tab_stats$,ind_stats$这里的数据),什么时候考虑新的cost呢,优化器代价模型的输入不仅包括对象统计信息,系统统计信息,优化器相关参数设置,也要包括可能随时在变化的cache信息时,无疑会带来执行计划的不稳定性的问题,这可能也不是你想要的吧.既然因为各种原因oracle默认没有开启这个模型,因为这个模型可能也存在着很多的问题,我们又要考虑某些cache的影响的时候,可能还得使用Optimizer_Index_Caching这个参数,具体设置哪个值比较合适,和你的系统有关,需要反复的测试的,不能把一个值当成一个万能灵药.另外设置Optimizer_Index_Caching<>0时,我这里说的这个问题你也需要意识到的.

    案例2:

    需要说明一下的是,这里的MIDS_BLOCK_ARTILIST_MORE_TEMP2是一个on commit preserve rows的temporary table,没有收集统计信息,使用的是动态采样机制,系统参数optimizer_dynamic_sampling是默认的2.
    这里CONCATENATION的三个部分的最外层的连接都使用了nl连接,因为它们都估算返回1行(实际上估算返回0行),实际上返回行数要多得多(特别是步骤22实际返回75W行),导致内表访问的逻辑IO过高导致了最终的性能问题.
    可为什么会出现这个问题呢?评估为什么会出现这个问题呢?还有我这里optimizer_dynamic_sampling=2,所以对于临时表MIDS_BLOCK_ARTILIST_MORE_TEMP2应该使用动态采样的呀,可为什么这里是默认的8168,表明它这里似乎没有使用动态采样呀,为什么没有使用动态采样?没有使用动态采样和这里card估算为0之间是否有关系呢?
    先说这里估算为0的情况:
create table zsj_test1(id number primary key);
create table zsj_test2(id number primary key);
create table zsj_test3(id number primary key);
insert /*+ append */into zsj_test1 select rownum from dual connect by rownum<=100;
commit;
insert /*+ append */into zsj_test2 select rownum from dual connect by rownum<=10000;
commit;
insert /*+ append */into zsj_test3 select rownum from dual connect by rownum<=1000000;
commit;
exec dbms_stats.gather_table_stats(user,'ZSJ_TEST1',CASCADE=>TRUE,METHOD_OPT=>'FOR ALL COLUMNS SIZE 1',ESTIMATE_PERCENT=>100);
exec dbms_stats.gather_table_stats(user,'ZSJ_TEST2',CASCADE=>TRUE,METHOD_OPT=>'FOR ALL COLUMNS SIZE 1',ESTIMATE_PERCENT=>100);
exec dbms_stats.gather_table_stats(user,'ZSJ_TEST3',CASCADE=>TRUE,METHOD_OPT=>'FOR ALL COLUMNS SIZE 1',ESTIMATE_PERCENT=>100);
CREATE GLOBAL TEMPORARY TABLE ZSJ_TEMP(ID NUMBER NOT NULL) on commit preserve rows;
explain plan for select * from zsj_test1 where id not in(select id from zsj_temp);
explain plan for select * from zsj_test1 where id not in(select /*+ dynamic_sampling(t 0) */id from zsj_temp t);
explain plan for select * from zsj_test2 where id not in(select id from zsj_temp);
explain plan for select * from zsj_test2 where id not in(select /*+ dynamic_sampling(t 0) */id from zsj_temp t);
explain plan for select * from zsj_test3 where id not in(select id from zsj_temp);
explain plan for select * from zsj_test3 where id not in(select /*+ dynamic_sampling(t 0) */id from zsj_temp t);
这里都可以发现使用动态采样时,可以得到正确的最终结果集的card,而不使用动态采样的时候,最终结果集的card估算都为1(实际是0)
对不做动态采样的3个sql语句做一下10053事件可以发现:
Anti Join Card:  0.00 = outer (100.00) * (1 - sel (1))
Join Card - Rounded: 1 Computed: 0.00
Anti Join Card:  0.00 = outer (10000.00) * (1 - sel (1))
Join Card - Rounded: 1 Computed: 0.00
Anti Join Card:  0.00 = outer (1000000.00) * (1 - sel (1))
Join Card - Rounded: 1 Computed: 0.00
结果集的card估算都为0.我们这里证实了not in后半部是临时表,没有收集统计信息,不使用动态采样的时候,结果集card估算为0.

    这里optimizer_dynamic_sampling=2,对临时表为什么没有使用动态采样呢?
    从那个sql语句的10053文件来看,一开始确实看到了动态采样的信息,可从之后的某个地方开始,很突兀的开始使用默认值了,不再使用动态采样信息了.我在将原来的sql语句尽量的精简,反复的实验之后,发现这似乎是和use_concat特性相关的一个问题,这个是可以通过实验证实的:

create table zsj_objs as
select 100000 id,owner,object_name,object_type,created,last_ddl_time,status
from dba_objects
where object_id is not null and rownum<=50000;
insert /*+ append*/into zsj_objs select * from zsj_objs;
commit;
insert /*+ append*/into zsj_objs select * from zsj_objs;
commit;
insert /*+ append*/into zsj_objs select * from zsj_objs where rownum<=20000;
commit;
update zsj_objs set id=rownum;
commit;
alter table zsj_objs add constraint pk_zsj_objs primary key(id);
--22W行数据,id是主键
exec dbms_stats.gather_table_stats(user,'ZSJ_OBJS',estimate_percent => 100,method_opt => 'FOR ALL COLUMNS SIZE 1');
create index ind1_zsj_objs on zsj_objs(owner);
create index ind2_zsj_objs on zsj_objs(object_type);
exec dbms_stats.gather_table_stats(user,'ZSJ_OBJS',estimate_percent => 100,method_opt => 'FOR COLUMNS SIZE 254 owner,object_type',no_invalidate=>false);
owner,object_type上的不同值个数都少于254,这样我就可以得到这两个列上的频度直方图,也就可以得到它们的准确数据行数了.
create global temporary table zsj_temp
(
id number not null,
owner varchar2(30),
OBJECT_NAME VARCHAR2(128),
OBJECT_TYPE VARCHAR2(19),
CREATED DATE,
LAST_DDL_TIME DATE,
STATUS VARCHAR2(7)
)
on commit preserve rows;

select /*+ use_concat */*
  from zsj_objs
where (owner = 'SYS' or OBJECT_TYPE = 'TABLE' or id<=1000)
   AND id not in (select id from zsj_temp);

--从note看,似乎是使用了动态采样的,可从8168可以看到其实是没有使用动态采样的,这时候concat的每个部分的card估算都为1(0)

select *
  from zsj_objs
where (owner = 'SYS' or OBJECT_TYPE = 'TABLE' or id<=1000)
   AND id not in (select id from zsj_temp);

    不使用use_concat特性时使用了动态采样,107K,这个估算是准确的
    我设置optimizer_dynamic_sampling为4,10也是这样的结果.所以似乎不是因为某些级别上的限制条件导致不能使用动态采样的,而是use_concat时就使用不了动态采样特性,所以这个似乎应该是一个bug.
    最后,简单的总结一下这个问题就是:因为动态采样的限制也好,或者是bug也好(我觉得bug的可能性还是很大的),使用use_concat时,对于not in后半部是临时表时,不使用动态采样,如果这个临时表又没有收集统计信息的话,会导致use_concat的各个部分的not in结果集估算返回0行,进而导致其它的性能上的问题.
    说到动态采样,其实用处挺大的,比如说相关列的问题,虽然说11g引入了扩展统计信息的技术,可以在一定程度上解决相关列上使用等值判断的选择性的估算,可如果在这些相关列上使用范围判断,扩展统计信息就使用不了了,这时候使用level>=4的动态采样就是一种可选的解决方案;再比如说涉及到子查询的复杂的sql语句,oracle在选择性上可能只能使用1%,5%这样的默认的选择性了,这时候使用级别较高的动态采样也是一种可选的解决方案。
    但每种方案都存在一些长处和一些短处,动态采样也是这样:硬分析时动态采样的成本,它会执行带有/* OPT_DYN_SAMP */这样注释的sql语句来执行动态采样数据动作的,特别是级别很高的动态采样的成本可能很大的,这是个问题,虽然一般来说不是一个大问题,但至少你要意识到它是存在代价的;我觉得它最主要的问题在于:如果你硬分析动态采样的时候,采样的刚好是一个非典型的情况呢,这时候可能会导致生成一个糟糕的执行计划的,也就是说它和bind peeking其实存在同样的问题:可能会导致生成对典型输入值糟糕的执行计划,也存在着执行计划不稳定的问题;当然还有我这里说到的问题:你认为它会使用动态采样,但实际上它没有使用动态采样(当然也包括因为采样级别设置不够导致不使用动态采样的情况存在),从而导致了性能问题.
    我这里想说的是:随意的使用动态采样,其实也是一种不负责任的行为(更糟糕的情况是:你根本就没有意识到动态采样其实帮助你解决了一些问题,也就是说你根本就没有意识到原来的问题的存在),对于preserve rows的临时表,我觉得还是在插入代表性数据之后手工收集统计信息的好,对于delete rows的临时表,还是手工设置统计信息为代表性数据的好,对于新建的普通表,在加载完数据后,对于新创建并加载数据的分区对象,立即手工收集统计信息或者手工设置统计信息为好.
    我上面说到存在非典型值时存在着执行计划糟糕,不稳定的情况.其实我们不能把自己的命运完全交给oracle的.因为oracle看到的只是一堆数据,它不知道它们代表什么含义,很多数据之间的关联它不会知道,而真正了解系统,了解业务逻辑,了解数据,了解应用如何使用这些数据的只有我们(oracle永远不可能了解所有的数据问题,能做到这一点的只有我们自己,当然这需要我们付出更多的精力),所以一些工作还是要由我们来做,而不是偷懒完全交给oracle去做.据说一些大型的互联网应用从不连表查询,对此我并不怀疑,因为只有他们真正了解自己的应用,了解数据的使用,了解数据,所以最好的执行路径还是只有他们知道,当然这需要更多的努力和精力,去了解数据使用,了解数据,需要框架上等多个方面更为有针对性的设计(当然也是因为这个才导致了它们系统的稳定性),单表访问似乎是简单了,实际上更复杂了.轻松的写一个多表连接的语句,然后把所有的希望寄托于oracle才是最简单的事情.这里扯远了,呵呵.

    案例3

    当然,这里逻辑IO偏高,主要是因为步骤8,9循环的次数太多了,主要的时间消耗也是发生在步骤8,9上了.一开始我关注的是步骤4实际返回了970行数据,而估算却是返回1行数据,注意这里的1很可能是代表0的:
    我们从后往前推(因为我们这里主要是关注这里为什么得到了1这个card,而不是这个执行计划是不是最优的)
Final - All Rows Plan:  Best join order: 7
  Cost: 312.3564  Degree: 1  Card: 1.0000  Bytes: 92
  Resc: 312.3564  Resc_io: 305.4525  Resc_cpu: 59150512
  Resp: 312.3564  Resp_io: 305.4525  Resc_cpu: 59150512
我们往上找连接顺序7:
Join order[7]:  CMS_ARTICLE_KEYWORDS[CMS_ARTICLE_KEYWORDS]#3  CMS_ARTICLE_COMPOSE[A6]#0  CMS_BLOCK_ARTICLE[B1]#1  VW_NSO_1[VW_NSO_1]#2
    SORT resource      Sort statistics
      Sort width:        2455 Area size:     1048576 Max Area size:   429496320
      Degree:               1
      Blocks to Sort:     159 Row size:           25 Total Rows:          51942  --这个是排序的行数
      Initial runs:         2 Merge passes:        1 IO Cost / pass:         88
      Total IO sort cost: 247      Total CPU sort cost: 49141986
      Total Temp space used: 2515000
***************
Now joining: CMS_ARTICLE_COMPOSE[A6]#0
***************
NL Join
  Outer table: Card: 1.00  Cost: 304.35  Resp: 304.35  Degree: 1  Bytes: 13
  Inner table: CMS_ARTICLE_COMPOSE  Alias: A6

Join Card:  0.03 = outer (1.00) * inner (1601.23) * sel (1.9415e-05)
Best:: JoinMethod: NestedLoop
       Cost: 305.35  Degree: 1  Resp: 305.35  Card: 0.03  Bytes: 41

Best so far: Table#: 3  cost: 304.3459  card: 1.0000  bytes: 13
             Table#: 0  cost: 305.3465  card: 0.0311  bytes: 41
             Table#: 1  cost: 307.3506  card: 0.0311  bytes: 79
             Table#: 2  cost: 312.3564  card: 0.0310  bytes: 92
    这里的顺序和执行计划中的顺序是一致的,而且代价也是一致的,注意这里第一次连接的时候,外表也就是CMS_ARTICLE_KEYWORDS表估算的card是1.
    再往前看单表访问路径处CMS_ARTICLE_KEYWORDS的card的估算:
  Table: CMS_ARTICLE_KEYWORDS  Alias: CMS_ARTICLE_KEYWORDS     
    Card: Original: 6592549  Rounded: 51942  Computed: 51942.02  Non Adjusted: 51942.02
  Best:: AccessPath: IndexRange  Index: UK_CMS_ARTICLE_KEYWORDS
         Cost: 51.61  Degree: 1  Resp: 51.61  Card: 51942.02  Bytes: 0
    注意这里,card还是对的:51942,这一点在上面的排序行数:51942处也可以得到佐证.可经过一个sort操作(sort unique)忽然就变为1了,为什么呢?感觉bug的可能性还是很大的.
    其实,这里无论是10053文件里的执行计划还是display_cursor显示的执行计划,它们的显示似乎都是有问题的,它们都是显示:sort unique后,card还是51942,cost还是52,似乎是nl后card变为了1,cost变成了305似的.但从10053文件看来,似乎不是这样的,应该是sort unique后:card=1,cost=304.35,它这里的304.35是如何计算出来的呢?就是cost(IndexRange UK_CMS_ARTICLE_KEYWORDS)+排序的io代价+排序的cpu代价: 51.61 + 247 + 49141986000/714000000/(10+ 8192/4096) = 304.3455
(
  Using NOWORKLOAD Stats
  CPUSPEED: 714 millions instruction/sec
  IOTFRSPEED: 4096 bytes per millisecond (default is 4096)
  IOSEEKTIM: 10 milliseconds (default is 10)
  使用8k大小的数据块
)
因为nl的外表返回1行,内表的访问成本是:
OPTIMIZER PERCENT INDEX CACHING = 60
  Access Path: index (UniqueScan)
    Index: PK_ID
    resc_io: 1.00  resc_cpu: 9339
    ix_sel: 4.1692e-07  ix_sel_with_filters: 4.1692e-07
    NL Join: Cost: 305.35  Resp: 305.35  Degree: 1
    所以nl连接后的总成本cost=304.35 + 1*1 = 305.35,nl连接后的card是0.03,显示为1.

    我觉得真正能反映估算情况的应该是下面这样的:

    总结一下:总的来说,就是sort unique后,card的估算忽然变成了1,实际却大得多,导致了随后的一系列问题.至于sort unique后为什么card的估算就变成1了,10053 trace文件没有给出任何的信息,也不清楚,我觉得bug的可能性还是有的.

    最近一段时间看10053文件有些多,有时候我也想,是以前没有遇到这里的问题呢,还是自己当时没有意识到问题的存在呢?前一阵子和开发人员有一个sql开发交流,最后举了一个sql优化的具体案例,实际上是很早之前的案例了,当时也写了一个总结的文档,可这次整理这个文档的时候,还是发现一个问题,sql语句默认选择了一个merge的执行计划,可添加上no_merge提示之后,执行要快速得多,而且资源消耗要少得多,最关键的问题是no_merge的执行计划的cost比merge的执行计划的cost还要小,可oracle就是默认选择了一个cost更大实际也更消耗资源的执行计划,至于这个问题的具体内容,这里就不讨论了.后来我就想,其实并不是以前没有这样的问题,而是那时候自己不够细心,没有意识到问题罢了.只要你有一颗细致入微的心,你就有可能发现一些以前可能发现不了的问题,在研究问题的过程中,也许你最终还是找不到答案(我就是这样),但你肯定会有所收获.

    关于这3个案例更详细的信息,有兴趣的可以看看我的文章:
http://blog.csdn.net/zhaosj1726/archive/2010/12/04/6054650.aspx
http://blog.csdn.net/zhaosj1726/archive/2011/01/21/6157592.aspx
http://blog.csdn.net/zhaosj1726/archive/2011/02/28/6213961.aspx

原创粉丝点击