1、问题
项目中遇到以下写法
bash
select count(1)
from (select *
from test1 t1
where t1.stime>=?
and t1.etime<=?
and exists (select 1 from test2 t2 where t2.wpid=t1.id and t2.did=?
union all
select 1 from test3 t3 where t3.wpid=t1.id and t3.did=?) )
参数:
2026-08-28 00:00:00
2026-08-30 00:00:00
B1
B1
计划:

这里有过滤性的条件来自exists中,从计划分析,exists中union all的共同部分是t1.id即关联列,优化器会考虑提取公因子即主查询做临时结果集(HEAP TABLE)减少一次扫描。此时合并的结果集和主查询(临时结果集)做hash semi join,而无法单独利用过滤性较好的条件让计划做index join,所以我们可以拆分成单个去和主查询关联,过去的or改写中提到union all可以替代or,反之亦然,可以改写or exists
2、改写1
bash
select count(1)
from (select *
from test1 t1
where t1.stime>=to_date(?,'YYYY-MM-DD HH24:MI:SS')
and t1.etime<=to_date(?,'YYYY-MM-DD HH24:MI:SS')
and (exists (select 1 from test2 t2 where t2.wpid=t1.id and t2.did=?)
or exists (
select 1 from test3 t3 where t3.wpid=t1.id and t3.did=?) ));
参数:
2026-08-28 00:00:00
2026-08-30 00:00:00
B1
B1
计划:

执行时间从7s提升至0.017s。
另外我们看原计划它做成HEAP TABLE,优化器会认为提取临时表是减少一次扫描会提高效率,那么我们可以考虑如果把主查询和exists子查询做成同一层,让优化器根据估算去考虑做成index join,所以我们可以把union all把共同列获取后再和主查询关联。
3、改写2
bash
select count(1)
from (select *
from test1 t1
where t1.stime>=to_date(?,'YYYY-MM-DD HH24:MI:SS')
and t1.etime<=to_date(?,'YYYY-MM-DD HH24:MI:SS')
and exists (select 1
from (select t2.wpid from test2 t2 where t2.did=?
union all
select t3.wpid from test3 t3 where t3.did=?) tt
where tt.wpid=t1.id))
参数
2026-08-28 00:00:00
2026-08-30 00:00:00
B1
B1
计划:

也是符合预期,性能提升至0.013s
4、小结
exists (select 1 from union all select 1 from )写法,exists中有过滤性较好的条件,可以考虑改写成or exists 或union all合并得出关联列结果集,再与主查询做exists。
5、附加测试数据
bash
create table test1(id varchar2(36) primary key,stime timestamp,etime timestamp,pcode varchar2(4));
insert into test1 select 'A'||level,SYSDATE-INTERVAL '1' SECOND * TRUNC(DBMS_RANDOM.VALUE(-10000,10000)),SYSDATE-INTERVAL '1' SECOND * TRUNC(DBMS_RANDOM.VALUE(-10000,10000)),'03'
from dual connect by level<=800000;
commit;
insert into test1 select 'A'||(level+800000),SYSDATE-INTERVAL '1' SECOND * TRUNC(DBMS_RANDOM.VALUE(-10000,10000)),SYSDATE-INTERVAL '1' SECOND * TRUNC(DBMS_RANDOM.VALUE(-10000,10000)),'03'
from dual connect by level<=200000;
commit;
create index IDX_DM_TIME on TEST1(stime,etime);
dbms_stats.gather_table_stats(USER,'TEST1',null,100);
create table test2(id varchar2(36) primary key,stime timestamp,etime timestamp,pcode varchar2(4),did varchar2(20),wpid varchar2(20));
insert into test2 select sys_guid(),SYSDATE-INTERVAL '1' SECOND * TRUNC(DBMS_RANDOM.VALUE(-10000,10000)),SYSDATE-INTERVAL '1' SECOND * TRUNC(DBMS_RANDOM.VALUE(-10000,10000)),'03'
,'B'||to_char(round(dbms_random.value(1,30000),0)),'A'||to_char(round(dbms_random.value(1,8000),0))
from dual connect by level<=150000;
commit;
create index IDX_DM_TEST2_WID_DID on TEST2(WPID,DID);
dbms_stats.gather_table_stats(USER,'TEST2',null,100);
create table test3(id varchar2(36) primary key,stime timestamp,etime timestamp,pcode varchar2(4),did varchar2(20),wpid varchar2(20));
insert into test3 select sys_guid(),SYSDATE-INTERVAL '1' SECOND * TRUNC(DBMS_RANDOM.VALUE(-10000,10000)),SYSDATE-INTERVAL '1' SECOND * TRUNC(DBMS_RANDOM.VALUE(-10000,10000)),'03'
,'B'||to_char(round(dbms_random.value(1,12134),0)),'A'||to_char(round(dbms_random.value(1,8000),0))
from dual connect by level<=100000;
commit;
create index IDX_DM_TEST3_WID_DID on TEST3(WPID,DID);
create index IDX_DM_TEST3_DID on TEST3(DID);
create index IDX_DM_TEST2_DID on TEST2(DID);