1、问题
这周在项目中遇到这样的sql
bash
select count(1)
from (select t2.*,
row_number()over(
partition by t2.pid
order by null) rn
from t2
left join (select *
from t1
where t1.flag=1
and (t1.nname like 'ABC10%'
or t1.nname like 'ABC2%'
or t1.nname like 'ABC3%')) tt
on t2.pid=tt.pid
where t2.flag in (1,2,3,4) and (exists (select 1 from t3 where t3.flag=1 and t3.uid='U1' and t3.pid=t2.pid)
or exists (select 1
from t4
where t4.flag=1
and t4.uid='U1'
and t2.udcode like t4.dcode||'%') ))
where rn=1
计划:

从计划中可以看到

(1)生成HEAP TABLE,即是临时表用来临时存放中间结果集(可以理解为with 表),优化器会将公共部分提取处理生成HEAP TABLE,为了减少扫描次数而做的优化
HEAP TABLE主要是T1和T2表关联,即主查询,or条件会让公共部分多次扫描,所以优化器考虑生成HEAP TABLE减少扫描次数,但HEAP TABLE会造成条件没有提前过滤引起性能问题,我们可以看到都是全表扫(CSCN)
而从语句中得知exists中存在UID过滤性较好的条件,可以考虑优化先做exists后,t2表再与t3表关联,or 用union all合并
值得注意一点union all是不会去重,所以结果可能出现重复结果,但是RN = 1做了去重操作,所以最终改写结果符合预期。
(2)另外like关联再inner join被驱动表才有可能利用上索引,可以理解为t2.udcode like t4.dcode||'%' 是将t4.dcode当成var(参数),过去分享中可以知道like模糊匹配,通配符在后面可以利用索引优化。所以可以从这点考虑将exists改写成inner join
简单介绍exists和inner join转换

exists是判断主查询(左表)是否存在满足子查询关联条件的数据,是则返回该行结果,只是判断真假,重复也只返回主查询的数据,不受子查询重复数据影响
那么其实就是内连接右表去重后的结果

最终结果与exists一样
所以得知exists子查询转换成inner join需要将子查询结果去重后作为inner join右表进行关联。
2、改写
bash
select count(1)
from (select * ,
row_number()over(
partition by pid
order by null) rn
from (select t2.*
from t2
left join (select *
from t1
where t1.flag=1
and (t1.nname like 'ABC1%'
or t1.nname like 'ABC2%'
or t1.nname like 'ABC3%')) tt
on t2.pid=tt.pid
where t2.flag in (1,2,3,4) and exists (select 1 from t3 where t3.flag=1 and t3.uid='U1' and t3.pid=t2.pid)
union all
select t2.*
from t2
left join (select *
from t1
where t1.flag=1
and (t1.nname like 'ABC1%'
or t1.nname like 'ABC2%'
or t1.nname like 'ABC3%')) tt
on t2.pid=tt.pid
inner join (select distinct t4.dcode
from t4 where t4.flag=1
and t4.uid='U1'
) t4 on t2.udcode like t4.dcode||'%'
where t2.flag in (1,2,3,4) ) tt )
where rn=1
计划:

性能提升几倍。
3、小结
(1) Left join+or exists,以及多个or条件存在,优化器会认为生成HEAP TABLE可以优化,当exists存在过滤性较好的条件,可以考虑将or exitst改成union all来合并,但值得注意union all与or并非完全等价,需要考虑去重。
(2) Like关联,通配符在后,相当于模糊匹配通配符在后可以用索引优化,如果是关联结果数据较少,可以考虑exists转换成inner join去优化。
(3) 项目中遇到的例子是各种性能问题综合起来的,需要我们逐一用过去所学到的优化去灵活处理,并非所有的都是单向优化。
4、附加测试数据
bash
create table t1 (id varchar2(36) primary key,pid varchar2(20),nname varchar2(20),flag int);
insert into t1 select sys_guid(),'A'||to_char(round(dbms_random.value(1,10000),0)),'ABC'||to_char(round(dbms_random.value(1,500),0)),
round(dbms_random.value(1,5),0) from dual connect by level<=100000;
commit;
create table t2 (id varchar2(36) primary key,pid varchar2(20),udcode varchar2(10),flag int);
insert into t2 select sys_guid(),'A'||to_char(round(dbms_random.value(1,10000),0)),'0'||to_char(round(dbms_random.value(301,321),0)),
round(dbms_random.value(1,5),0) from dual connect by level<=200000;
commit;
create table t3 (id varchar2(36) primary key,pid varchar2(20),uid varchar2(20),flag int);
insert into t3 select sys_guid(),'A'||to_char(round(dbms_random.value(1,50000),0)),'U'||to_char(round(dbms_random.value(1,5000),0)),
round(dbms_random.value(1,5),0) from dual connect by level<=100000;
commit;
create table t4 (id varchar2(36) primary key,pid varchar2(20),dcode varchar2(10),uid varchar2(20),flag int);
insert into t4 select sys_guid(),'A'||to_char(round(dbms_random.value(1,20000),0)),'0'||to_char(round(dbms_random.value(301,321),0)),'U'||to_char(round(dbms_random.value(1,5000),0)),
round(dbms_random.value(1,5),0) from dual connect by level<=100000;
commit;
dbms_stats.gather_table_stats(USER,'T1',null,100);
dbms_stats.gather_table_stats(USER,'T2',null,100);
dbms_stats.gather_table_stats(USER,'T3',null,100);
dbms_stats.gather_table_stats(USER,'T4',null,100);
create index IDX_DM_T3_uid on t3(uid);
create index IDX_DM_T4_uid on t4(uid);
create index IDX_DM_T2_pid on t2(pid);
create index IDX_DM_T2_udcode on t2(udcode);
create index IDX_DM_T1_pid on t1(pid);
create index IDX_DM_T1_nname on t1(nname);