oracle connect by优化小探

前一篇介绍了connect by的用法
https://blog.csdn.net/u011165335/article/details/82822224

这里在看看如何优化:
环境跟上一篇的一样:11g

-- 表结构
drop table   menu;
create table menu(
 mid varchar2(64) not null,
 parent_id varchar2(64) not null,
 mname varchar2(100) not null,
 mdepth number(2) not null,
 primary key (mid)
);

-- 初始化数据
-- 顶级菜单
insert into menu values ('100000', '0', '顶级菜单1', 1);
insert into menu values ('200000', '0', '顶级菜单2', 1);
insert into menu values ('300000', '0', '顶级菜单3', 1); 

-- 父级菜单
-- 顶级菜单1 直接子菜单
insert into menu values ('110000', '100000', '菜单11', 2);
insert into menu values ('120000', '100000', '菜单12', 2);
insert into menu values ('130000', '100000', '菜单13', 2);
insert into menu values ('140000', '100000', '菜单14', 2); 
-- 顶级菜单2 直接子菜单
insert into menu values ('210000', '200000', '菜单21', 2);
insert into menu values ('220000', '200000', '菜单22', 2);
insert into menu values ('230000', '200000', '菜单23', 2); 
-- 顶级菜单3 直接子菜单
insert into menu values ('310000', '300000', '菜单31', 2); 

-- 菜单13 直接子菜单
insert into menu values ('131000', '130000', '菜单131', 3);
insert into menu values ('132000', '130000', '菜单132', 3);
insert into menu values ('133000', '130000', '菜单133', 3);

-- 菜单132 直接子菜单
insert into menu values ('132100', '132000', '菜单1321', 4);
insert into menu values ('132200', '132000', '菜单1332', 4);

在10.2.0.2之后对于递归查询,CBO可以有2种选择:
connect_by_filtering和no_connect_by_filtering(10.2.0.2版本引入),另外还有一些隐藏参数也影响着递归查询的执行计划。
参数
KSPPINM KSPPSTVL KSPPDESC


_optimizer_connect_by_cost_based TRUE use cost-based transformation for connect by
_optimizer_connect_by_combine_sw TRUE combine no filtering connect by and start with
_optimizer_connect_by_elim_dups TRUE allow connect by to eliminate duplicates from input
_optimizer_connect_by_cb_whr_only FALSE use cost-based transformation for whr clause in connect

hint:NO_CONNECT_BY_FILTERING/CONNECT_BY_FILTERING
在原来的算法中(CONNECT BY WITH FILTERING),会在每次循环中作行过滤操作,
然后只读取需要的行.
而在新的算法中(CONNECT BY NO FILTERING WITH SW),则会把所有需要的数据读入内存,
然后统一作内存排序来过滤掉不需要的列.
猜想:(未经验证)
前一种算法适用于从数据集中取很小一部分数据的场景,
而后一种算法则适用于从大数据量中取大量数据的操作.
但由于oracle对connect by语句cardinality估算的不准确性,
关于不准确性这里可以看之前我转载的一篇文章:
https://blog.csdn.net/u011165335/article/details/82819455
导致oracle并不能准确地在两种算法间作出合理的抉择.

猜想:cbo在选择2种算法时,是基于成本考虑的;
如果不想让执行计划走CONNECT BY NO FILTERING WITH START-WITH,
可以:
1.使用hint:connect_by_filtering
2.11g中新增的隐含参数’_optimizer_connect_by_elim_dups’和’_connect_by_use_union_all’;
禁用即可:也可以在session级别设置
alter system set “_optimizer_connect_by_elim_dups” = false;
alter system set “_connect_by_use_union_all” = “old_plan_mode”;
禁用之后,只会走connect by filtering

分2个部分:
第一个部分:看看connect by 的执行过程
第二个部分:看看start with涉及子查询以及视图合并的场景

下面开始第一个部分:

drop table  menu_temp;
create table menu_temp
as
select * from menu;

--1.不加任何索引
select /*+ connect_by_filtering*/ aa.*,level
  from menu_temp aa
 start with aa.mid = '130000'
connect by  prior aa.mid = aa.parent_id; 
查看对应的预估执行计划和高级执行计划

在这里插入图片描述
在这里插入图片描述
此时走的时候connect by with filtering
执行过程:
id=2,把开始要循环的数据取出来;猜想每次是走的filter;
select * from menu_temp aa where aa.mid=‘130000’;

这里得到的mid为10000;
然后CONNECT BY PUMP接收第一步扫描出的结果parent_id=100000的数据,用临时表 0 02 m e n u t e m p h a s h ( p a r e n t i d ) , m i d , _002跟menu_temp做hash连接(连接列parent_id),把得到的结果mid,放入 _002,再跟menu_temp做hash连接,直到最后的parent_id没有对应的mid时,停止循环,直到达到最后一个层级。
menu_temp的访问次数为3+1=4次;
可以发现,此时的表访问次数为:max(level)+1;
这里走的全表,如果表很大,层级很多,那么这种方式的全表范问,必然造成效率低下;
而且此时的预估数是16(全表数),实际上是6;
这里cbo无法估算准确的结果数,也是一个问题;

再看走no filtering的
select *
  from menu_temp aa
 start with aa.mid = '130000'
connect by  prior aa.mid = aa.parent_id;

在这里插入图片描述
默认此时CBO走的是connect by no filtering with start-with算法;
逻辑读为15;

再看看真实的执行计划:
alter session set statistics_level=all;
select * from table(dbms_xplan.display_cursor(null,null,‘advanced allstats last -projection’));
在这里插入图片描述
这种算法下:表的访问次数为1;具体如何实现,目前不太清楚…
但是从outline来看,内部处理似乎还是用到了临时表,只不过在普通的执行计划里面看不出来;
对比发现,此时走connect by no filtering with start-with效率更高;
逻辑读以及代价都比第一种少;

上面的都是没建索引的
--2建立索引看看:
create index   menu_mid_ind on menu_temp(mid);
create index   min_parent_id_ind on menu_temp(parent_id);

select aa.*,level
  from menu_temp aa
 start with aa.mid = '130000'
connect by  prior aa.mid = aa.parent_id;

在这里插入图片描述
在这里插入图片描述
放大部分真实的执行计划:可以发现有索引的情况,估算的返回值还相对准确的;
为总数/列基数
在这里插入图片描述
此时:NL走了3次;menu_temp的索引一共访问了6+1=7次;
而如果全表的是4次;这就需要权衡了,走索引不一定比全表快;(这里先不讨论这个…)
在这里插入图片描述
第一次访问索引: select * from menu_temp aa where aa.mid=‘130000’;
然后再看NL的3次:
首先第一次NL:pump的临时表把parent_id=100000的结果集:1行;
传递给ment_temp的索引,走了1次;
第二次NL:pump的临时表的parent_id=130000的结果集:3行;
传递给ment_temp的索引:重复走3次;
第三次NL:
pump的临时表的
parent_id=133000的结果集为0.在ment_temp索引没有记录,次数为0;
parent_id=132000的结果集为2,在ment_temp索引被驱动2次;
parent_id=133000的结果集为0.在ment_temp索引没有记录,次数为0;

然后是parent_id=132200,132100,均没有记录,在ment_temp索引范问次数为0;
于是这里ment_temp的索引访问次数为:1+3+2=6;

通过执行计划可以看到这里虽然在子节点和父节点都建立了索引,但是如果我只查mid
如:
select aa.mid
from menu_temp aa
start with aa.mid = ‘130000’
connect by prior aa.mid = aa.parent_id;
在这里插入图片描述
可以发现依然有回表现象,那么如何解决回表呢?
继续看走no filtering的执行计划

select /*+ NO_CONNECT_BY_FILTERING */*
  from menu_temp aa
 start with aa.mid = '130000'
connect by  prior aa.mid = aa.parent_id;
第二次跑,有缓存,所以此时的逻辑为3;

在这里插入图片描述

从代价和逻辑读来看,效率比上面的走索引好;
此时走的还是全表扫,为啥不走索引呢?
是不是因为预估的返回记录数为全表的16,所以cbo认为时走全表呢?
select /*+ NO_CONNECT_BY_FILTERING  cardinality(aa 1) */*
  from menu_temp aa
 start with aa.mid = '130000'
connect by  prior aa.mid = aa.parent_id;
等价:
select /*+ NO_CONNECT_BY_FILTERING  cardinality(AA@SEL$2 1) */*
  from menu_temp aa
 start with aa.mid = '130000'
connect by  prior aa.mid = aa.parent_id;

在这里插入图片描述
可以发现,即使为1,cbo选择的还是全表扫;
为啥此时还是走的全表扫呢? 稍等…
看下面的强制走索引
可不可以强制走索引?
select /*+ NO_CONNECT_BY_FILTERING index(aa) */aa.mid
from menu_temp aa
start with aa.mid = ‘130000’
connect by prior aa.mid = aa.parent_id;
在这里插入图片描述
可以发现,虽然此时强制走的是索引,但是走的是索引全扫描,单块读,且回表,在返回数据量多的情况下,效率并不高;而且我的返回值只有aa.mid,按道理此时不应该会有回表啊,于是猜想在内部的表连接时产生了回表:
内部应该存在 temp1.mid=某个值 and temp1.parent_id=某个值
这种关联关系;
于是再建立一个组合索引就可以去掉内部回表,此时查询CBO就可以自动走索引了:
drop index MIN_PARENT_ID_IND ;
drop index MENU_MID_IND;
删除这2个之前建立的索引,防止干扰;

drop index mid_parent_ind;
create index mid_parent_ind on menu_temp(mid,parent_id);
select /+ NO_CONNECT_BY_FILTERING index_ffs(aa)/aa.mid,aa.parent_id
from menu_temp aa
start with aa.mid = ‘130000’
connect by prior aa.mid = aa.parent_id;

在这里插入图片描述
在这里插入图片描述
可以发现,建立了组合索引后,可以成功的走ffs索引;说明回表已经被消除了!
逻辑读为4,代价为5,比全表扫的7小;
好,不加hint呢?
select aa.mid,aa.parent_id
from menu_temp aa
start with aa.mid = ‘130000’
connect by prior aa.mid = aa.parent_id;
在这里插入图片描述
可以发现,此时cbo走的是索引全扫描,且逻辑读极小为1,代价也最小为3;
比指定的ffs效率还好!
如果查询的是所有列呢?
select /+ cardinality(aa 2)/ aa.*
from menu_temp aa
start with aa.mid = ‘130000’
connect by prior aa.mid = aa.parent_id;
在这里插入图片描述
此时因为需要回表,于是cbo选择走了全表扫;
而且这里我故意指定了menu_temp的返回结果数为2,但是cbo依然选择了走全表扫;
在NO_CONNECT_BY_FILTERING这种执行计划下,cbo似乎很害怕回表,不管返回的记录数是多少,它见到回表操作,就决然的选择了走全表扫;难道cbo认为 只要有回表,代价就比全表低?这点不是很理解;

那么在有回表的情况走索引,真的效率就低吗?
select /+ index(aa)/ aa.*
from menu_temp aa
start with aa.mid = ‘130000’
connect by prior aa.mid = aa.parent_id;
在这里插入图片描述
在强制走索引下,可以看到,这里的逻辑读和代价都比上面的全表的效率要好;
cbo为啥就不考虑一下呢?
说明了cbo在这种场景下是需要改进的;

关于第一个部分做个小结:
在10.2.0.2版本会,connect by查询执行计划会有2种算法可以选择:
1.connect_by_filtering
这种类似filter操作,只不过filter的第二个部分为pump和menu_temp的递归连接查询,可以有嵌套或者hash join;
1.1在返回数据量比较小的时候,适合走NL,此时可以在要递归的子节点和父节点列建立组合索引(不建议分别建索引);
这样在nl的时候,都可以走同一个索引;
但是要注意回表消耗和索引的查询次数;
索引的查询次数应该近似为表的查询结果数;
所以在返回量大的情况下,如果不能避免回表,不如走全表;
1.2在返回的数据量大的时候,递归部分可以走hash,此时menu_temp为全表扫,
表的访问次数为max(level)+1;
所以,如果一定要走connect_by_filtering的话,此时只能尽量减少表menu_temp的访问体积,即在访问的时候,用过滤后的with临时表替代;

2.no_connect_by_filtering
从刚才上面的测试来看,效率还不错,但是场景有限,
在实际的问题分析中:
还是要比较2种算法谁的效率更高;
如何简单的比较呢?先建立组合索引,再分别比较这2种算法啦;

在no_connect_by_filtering场景下,表只会被访问次一次,似乎告诉我们,这个算法适合返回大量数据的场景;具体看以后的实际问题测试吧…
为了避免内部回表,此时需要建立一个组合索引(子节点列,父节点列);
1.1 当查询的结果列为 子节点或者父节点列时,
此时可以走fs或者ffs;效率最佳;因为不会回表;
fs内部走的NL,ffs内部走的hash
1.2 当查询的结果列不仅仅是子节点或者父节点列时,
此时CBO默认是会走全表扫的;但是可以强制走索引,具体走不走索引;以实际场景分析为准;

好,再看第二部分:关于start with的子查询以及视图
1.子查询

menu_temp还是上面的表;
有一个组合索引
drop index  mid_parent_ind;
create index mid_parent_ind on menu_temp(mid,parent_id);

--先模拟一个最简单的子查询
select aa.*
  from menu_temp aa
 start with aa.mid in(select  '130000' from dual)
connect by  prior aa.mid = aa.parent_id;

在这里插入图片描述

这里CBO主动选择了no filtering with SW;
此时的代价为9;逻辑读为3;
并且是在最后一步来过滤开始的条件;

–不回表呢
select aa.mid
from menu_temp aa
start with aa.mid in(select ‘130000’ from dual)
connect by prior aa.mid = aa.parent_id;
在这里插入图片描述
此时走的FS,代价为5;逻辑读为1,近乎减少了1倍;
也可以知道主要的消耗还是在于递归查询;

--再看看走filtering的
select /*+ connect_by_filtering*/ aa.*
  from menu_temp aa
 start with aa.mid in(select '130000' from dual)
connect by  prior aa.mid = aa.parent_id;

在这里插入图片描述
可以发现,效率显然没有no_filtering的好;
这里的子查询展开了(二级形态),跟menu_temp走nl内连接;没问题;
此时主要的消耗在于递归查询;
另外,这里的start with部分做的这个查询转换,是内部的;
所以无法显式的等价改写…
只能控制展开与不展开;
小结一下:
这里如果存在性能问题:
1.子查询跟主表的连接方式是否存在问题,有问题就先优化子查询问题;
2如果没问题那么就看看递归查询的效率;

再看看视图合并的

select * from (
 select e.empno,e.mgr from emp e,dept d
 where e.deptno=d.deptno
 and   d.dname like '%A%'  
) tt
start with tt.empno=7369
connect by prior tt.empno=tt.mgr;

在这里插入图片描述
这个明显是进行了视图合并;

等价下面的sql:
select e.empno, e.mgr
from emp e, (select * from dept d where d.dname like ‘%A%’) d
where e.deptno = d.deptno
start with e.empno in (7369)
connect by prior e.empno = e.mgr;

在这里插入图片描述

能等价改写,再优化相关的问题就方便了;
(比如刚在网上看一个一个connect by查询问题:http://www.itpub.net/thread-1903074-1-1.html
第三页有我的回复;)
这里改写的关键在于不能出现and=‘常量’;

再比如:
select *
  from (select e.empno, e.mgr
          from emp e, dept d, salgrade ss
         where e.deptno = d.deptno
           and e.ename = d.dname
           and ss.losal = e.sal
           and d.dname like '%A%') tt
 start with tt.empno = 7369
connect by prior tt.empno = tt.mgr;
等价:
select e.empno, e.mgr
  from emp e,
       (select * from dept d where d.dname like '%A%') d,
       salgrade ss
 where e.deptno = d.deptno
   and e.ename = d.dname
   and ss.losal = e.sal
 start with e.empno = 7369
connect by prior e.empno = e.mgr; 

小结一下:
关于这种视图合并的,先要看懂执行计划,是哪一块出了问题,如果cbo进行了视图合并,那么你就要知道如何合并的,并能自已改写出来,这样你就能针对性的控制你想控制的部分;
好了,就先写到这了,有不对的地方还请指正,谢谢(∩_∩)

2018年9月23日21:36:32 by ysy

猜你喜欢

转载自blog.csdn.net/u011165335/article/details/82825169