예시
select *
from emp
where deptno in (select deptno from dept)
위 SQL문을 unnesting하지 않는다면 아래와 같이 수행할 것이다.
select *
from emp
where deptno in (select /*+no_unnest*/ deptno from dept)
--------------------------------------
| Id | Operation | Name |
--------------------------------------
| 0 | SELECT STATEMENT | |
|* 1 | FILTER | |
| 2 | TABLE ACCESS FULL| EMP |
|* 3 | INDEX UNIQUE SCAN| DEPT_PK |
--------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter( IS NOT NULL)
3 - access("DEPTNO"=:B1) -> 여기
:B1 부분을 보아 서브쿼리가 별도의 서브플랜으로 최적화되었음을 알 수 있다.
unnest 힌트를 사용하거나 옵티마이저가 unnesting한다면
select *
from (select deptno from dept) a, emp b
where b.deptno = a.deptno
위와 같이 쿼리변환이 일어나고 뷰머징 과정을 거치면
select emp.*
from dept, emp
where emp.deptno = dept.deptno
위와 같이 쿼리가 변환되고 nested loop 형태로 조인되는 것을 알 수 있다.
--------------------------------------
| Id | Operation | Name |
--------------------------------------
| 0 | SELECT STATEMENT | |
| 1 | NESTED LOOPS | |
| 2 | TABLE ACCESS FULL| EMP |
|* 3 | INDEX UNIQUE SCAN| DEPT_PK |
--------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
3 - access("EMP"."DEPTNO"="DEPT"."DEPTNO") -> 조인형태로 변환된 것을 알 수 있다.
조인순서조정
쿼리 변환이 일어났을 때 어느 테이블을 드라이빙 테이블로 지정할지 정할 수 있다.
select /*+leading(emp) */ *
from emp
where deptno in (select /*+unnest*/ deptno
from dept)
->emp가 드라이빙 테이블이 됨
select /*+leading(dept) */ *
from emp
where deptno in (select /*+unnest*/ deptno
from dept)
->dept가 드라이빙 테이블이 됨
select /*+ordered */ *
from emp
where deptno in (select /*+unnest*/ deptno
from dept)
->dept가 드라이빙 테이블이 됨
--qb_name은 10g부터 사용
select /*+leading(dept@qb1) */ *
from emp
where deptno in (select /*+unnest qb_name(qb1)*/ deptno
from dept)
->dept가 드라이빙 테이블이 됨
예시
select *
from dept
where deptno in (select deptno from emp)
위 쿼리는 아래처럼 쿼리 변형 될 것인데
select *
from (select deptno from emp) a
dept b
where a.deptno = b.deptno
서브쿼리의 deptno가 unique하지 않다면 결과 건수가 달라질 것이다.
이때 옵티마이저는
서브쿼리 쪽 테이블이 먼저 드라이빙 되면 sort unique 오퍼레이션을 수행하여 중복을 제거하고
메인쿼리 쪽 테이블이 먼저 드라이빙 되면 세미 조인 방식으로 조인한다.
테스트 : dept를 먼저 드라이빙
select /*+leading(dept)*/*
from dept
where deptno in (select deptno from emp)
-----------------------------------
| Id | Operation | Name |
-----------------------------------
| 0 | SELECT STATEMENT | |
|* 1 | HASH JOIN SEMI | | -> 세미 조인으로 풀리는 것을 알 수 있다.
| 2 | TABLE ACCESS FULL| DEPT |
| 3 | TABLE ACCESS FULL| EMP |
-----------------------------------
테스트 : emp를 먼저 드라이빙
select /*+leading(emp)*/*
from dept
where deptno in (select deptno from emp)
------------------------------------
| Id | Operation | Name |
------------------------------------
| 0 | SELECT STATEMENT | |
|* 1 | HASH JOIN | |
| 2 | SORT UNIQUE | | -> SORT UNIQUE 오퍼레이션이 일어난다.
| 3 | TABLE ACCESS FULL| EMP |
| 4 | TABLE ACCESS FULL | DEPT |
------------------------------------
내부적으로 SORT UNIQUE 오퍼레이션이 수행된 것으로 보아 쿼리 변환이 아래와 같이 일어난 것을 알 수 있다.
select b.*
from (select /*+ no_merge */ distinct deptno from dept order by deptno) a, emp b
where b.deptno = a.deptno
서브쿼리를 조인형태로 풀어내면 더 나은 실행계획을 수립할 가능성이 높아진다.
그렇다면 쿼리변환을 하지 않고 필터형태도 그대로 수행하는 것에는 장점이 없을까?
=>
서브쿼리로 필터할 때 오라클은 서브쿼리의 수행 결과를 버리지 않고 내부 캐시에 저장하고 있다가
같은 값이 서브쿼리로 입력되면 저장된 값을 리턴한다. 8i,9i에서는 256개, 10g에서는 1024개 해시 엔트리를 캐싱한다.
테스트 : 서브쿼리 필터시 캐싱 효과
create table t_emp
as
select *
from emp
,(select rownum no from dual connect by level <= 100);
select count(*) from t_emp t
where exists (select /*+ no_unnest */ 'x' from dept
where deptno = t.deptno and loc is not null);
---------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
---------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 18 |
| 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.01 | 18 |
|* 2 | FILTER | | 1 | | 1400 |00:00:00.01 | 18 |
| 3 | TABLE ACCESS FULL | T_EMP | 1 | 1400 | 1400 |00:00:00.01 | 12 |
|* 4 | TABLE ACCESS BY INDEX ROWID| DEPT | 3 | 1 | 3 |00:00:00.01 | 6 |
|* 5 | INDEX UNIQUE SCAN | DEPT_PK | 3 | 1 | 3 |00:00:00.01 | 3 |
---------------------------------------------------------------------------------------------------
t_emp가 1400 row이므로 dept테이블도 1400건 반복되어야 하지만 3번만 반복되었다.
이와 같은 캐싱 효과는 10g이후 nested loop 세미조인에서도 나타난다.
select count(*) from t_emp t
where exists (select /*+ unnest nl_sj */ 'x' from dept
where deptno = t.deptno and loc is not null);
---------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
---------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 18 |
| 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.01 | 18 |
| 2 | NESTED LOOPS SEMI | | 1 | 1400 | 1400 |00:00:00.01 | 18 |
| 3 | TABLE ACCESS FULL| T_EMP | 1 | 1400 | 1400 |00:00:00.01 | 12 |
|* 4 | TABLE ACCESS FULL| DEPT | 3 | 4 | 3 |00:00:00.01 | 6 |
---------------------------------------------------------------------------------------
테스트
not exists, not in 서브쿼리도 unnesting하지 않으면 기본적으로는 필터방식으로 동작한다.
select *
from dept d
where not exists
(select /*+ no_unnest */'x' from emp where deptno = d.deptno);
-------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 10 |
|* 1 | FILTER | | 1 | | 1 |00:00:00.01 | 10 |
| 2 | TABLE ACCESS FULL| DEPT | 1 | 4 | 4 |00:00:00.01 | 2 |
|* 3 | TABLE ACCESS FULL| EMP | 4 | 2 | 3 |00:00:00.01 | 8 |
-------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter( IS NULL)
3 - filter("DEPTNO"=:B1)
Unnesting 하면 Anti 조인 방식으로 처리된다.
select *
from dept d
where not exists
(select /*+ unnest nl_aj */'x' from emp where deptno = d.deptno);
-------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 10 |
| 1 | NESTED LOOPS ANTI | | 1 | 1 | 1 |00:00:00.01 | 10 |
| 2 | TABLE ACCESS FULL| DEPT | 1 | 4 | 4 |00:00:00.01 | 2 |
|* 3 | TABLE ACCESS FULL| EMP | 4 | 9 | 3 |00:00:00.01 | 8 |
-------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
3 - filter("DEPTNO"="D"."DEPTNO")
select *
from dept d
where not exists
(select /*+ unnest merge_aj */'x' from emp where deptno = d.deptno);
--------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
--------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 4 |
| 1 | MERGE JOIN ANTI | | 1 | 1 | 1 |00:00:00.01 | 4 |
| 2 | TABLE ACCESS BY INDEX ROWID| DEPT | 1 | 4 | 4 |00:00:00.01 | 2 |
| 3 | INDEX FULL SCAN | DEPT_PK | 1 | 4 | 4 |00:00:00.01 | 1 |
|* 4 | SORT UNIQUE | | 4 | 14 | 3 |00:00:00.01 | 2 |
| 5 | TABLE ACCESS FULL | EMP | 1 | 14 | 14 |00:00:00.01 | 2 |
--------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
4 - access("DEPTNO"="D"."DEPTNO")
filter("DEPTNO"="D"."DEPTNO")
select *
from dept d
where not exists
(select /*+ unnest hash_aj */'x' from emp where deptno = d.deptno);
-------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 4 |
|* 1 | HASH JOIN ANTI | | 1 | 1 | 1 |00:00:00.01 | 4 |
| 2 | TABLE ACCESS FULL| DEPT | 1 | 4 | 4 |00:00:00.01 | 2 |
| 3 | TABLE ACCESS FULL| EMP | 1 | 14 | 14 |00:00:00.01 | 2 |
-------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - access("DEPTNO"="D"."DEPTNO")
집계 함수(Aggregate Function)를 포함하는 서브쿼리를 Unnesting하고 이를 다시 분석함수로 대체하는 쿼리 변환이 10g에서 도입되었다.
예시
select d.deptno, d.dname, e.empno, e.ename, e.sal
from emp e, dept d
where d.deptno = e.deptno
and e.sal >= (select avg(sal) from emp where deptno = d.deptno)
1차 변환 -->
select d.deptno, d.dname, e.empno, e.ename, e.sal
from (select deptno, avg(sal) avg_sal from emp group by deptno) x, emp e, dept d
where d.deptno = e.deptno
and e.deptno = x.deptno
and e.sal >= x.avg_sal
------------------------------------------------
| Id | Operation | Name |
------------------------------------------------
| 0 | SELECT STATEMENT | |
| 1 | NESTED LOOPS | |
| 2 | NESTED LOOPS | |
|* 3 | HASH JOIN | |
| 4 | VIEW | |
| 5 | HASH GROUP BY | |
| 6 | TABLE ACCESS FULL | EMP |
| 7 | TABLE ACCESS FULL | EMP |
|* 8 | INDEX UNIQUE SCAN | DEPT_PK |
| 9 | TABLE ACCESS BY INDEX ROWID| DEPT |
------------------------------------------------
-> emp 테이블을 두번 참조하는 것을 알 수 있다.
이 쿼리는 다시 아래와 같이 변환된다.
select deptno, dname, empno, ename, sal
from (
select d.deptno, d.dname, e.empno, e.ename, e.sal
,(case when e.sal >= avg(sal) over (partition by d.deptno)
then e.rowid end) max_sal_rowid
from emp e, dept d
where d.deptno = e.deptno
)
where max_sal_rowid is not null;
--------------------------------------------------
| Id | Operation | Name |
--------------------------------------------------
| 0 | SELECT STATEMENT | |
|* 1 | VIEW | |
| 2 | WINDOW BUFFER | |
| 3 | NESTED LOOPS | |
| 4 | TABLE ACCESS BY INDEX ROWID| DEPT |
| 5 | INDEX FULL SCAN | DEPT_PK |
|* 6 | TABLE ACCESS FULL | EMP |
--------------------------------------------------
emp 테이블을 한번만 액세스했고 window buffer 오퍼레이션이 추가되었다.
해당 쿼리 변환을 사용하지 못하도록 파라미터를 변경해보면..
alter session set "_remove_aggr_subquery" = false;
select d.deptno, d.dname, e.empno, e.ename, e.sal
from dept d, emp e
where d.deptno = e.deptno
and e.sal = (select max(sal) from emp where deptno = d.deptno) ;
-----------------------------------------
| Id | Operation | Name |
-----------------------------------------
| 0 | SELECT STATEMENT | |
|* 1 | HASH JOIN | |
|* 2 | HASH JOIN | |
| 3 | VIEW | VW_SQ_1 |
| 4 | HASH GROUP BY | |
| 5 | TABLE ACCESS FULL| EMP |
| 6 | TABLE ACCESS FULL | DEPT |
| 7 | TABLE ACCESS FULL | EMP |
-----------------------------------------
emp 테이블을 다시 2번 엑세스하는 것을 알 수 있다.
서브쿼리에서 필터링을 먼저해줄 경우 다음 수행 단계로 넘어가는 row 수를 크게 줄일 수 있다면 성능이 그만큼 향상 될 것이다.
pushing 서브쿼리는 실행계획 상 가능한 앞 단계에서 서브쿼리 필터링이 처리되도록 강제하는 것으로 push_subq 힌트로 제어할 수 있다.
unnesting 되지 않은 상태에서 작동하기 때문에 항상 no_unnest 힌트와 같이 기술하는 것이 올바른 사용법이다.
push_subq 효과
사용전
select /*+leading(e1) use_nl(e2) */ sum(e1.sal), sum(e2.sal)
from emp1 e1, emp2 e2
where e1.no = e2.no
and e1.empno = e2.empno
and exists (select /*+ NO_UNNEST NO_PUSH_SUBQ */ 'x'
from dept where deptno = e1.deptno
and loc = 'NEW YORK');
-------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads |
-------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.11 | 14347 | 36 |
| 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.11 | 14347 | 36 |
|* 2 | FILTER | | 1 | | 3000 |00:00:00.09 | 14347 | 36 |
| 3 | NESTED LOOPS | | 1 | | 14000 |00:00:00.09 | 14341 | 36 |
| 4 | NESTED LOOPS | | 1 | 14000 | 14000 |00:00:00.05 | 341 | 36 |
| 5 | TABLE ACCESS FULL | EMP1 | 1 | 14000 | 14000 |00:00:00.01 | 94 | 0 |
|* 6 | INDEX UNIQUE SCAN | EMP2_PK | 14000 | 1 | 14000 |00:00:00.05 | 247 | 36 |
| 7 | TABLE ACCESS BY INDEX ROWID| EMP2 | 14000 | 1 | 14000 |00:00:00.02 | 14000 | 0 |
|* 8 | TABLE ACCESS BY INDEX ROWID | DEPT | 3 | 1 | 1 |00:00:00.01 | 6 | 0 |
|* 9 | INDEX UNIQUE SCAN | DEPT_PK | 3 | 1 | 3 |00:00:00.01 | 3 | 0 |
-------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - filter( IS NOT NULL)
6 - access("E1"."NO"="E2"."NO" AND "E1"."EMPNO"="E2"."EMPNO")
8 - filter("LOC"='NEW YORK')
9 - access("DEPTNO"=:B1)
사용후
select /*+leading(e1) use_nl(e2) */ sum(e1.sal), sum(e2.sal)
from emp1 e1, emp2 e2
where e1.no = e2.no
and e1.empno = e2.empno
and exists (select /*+ NO_UNNEST PUSH_SUBQ */ 'x'
from dept where deptno = e1.deptno
and loc = 'NEW YORK');
-----------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-----------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.02 | 3347 |
| 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.02 | 3347 |
| 2 | NESTED LOOPS | | 1 | | 3000 |00:00:00.02 | 3347 |
| 3 | NESTED LOOPS | | 1 | 700 | 3000 |00:00:00.02 | 347 |
|* 4 | TABLE ACCESS FULL | EMP1 | 1 | 700 | 3000 |00:00:00.01 | 100 |
|* 5 | TABLE ACCESS BY INDEX ROWID| DEPT | 3 | 1 | 1 |00:00:00.01 | 6 |
|* 6 | INDEX UNIQUE SCAN | DEPT_PK | 3 | 1 | 3 |00:00:00.01 | 3 |
|* 7 | INDEX UNIQUE SCAN | EMP2_PK | 3000 | 1 | 3000 |00:00:00.01 | 247 |
| 8 | TABLE ACCESS BY INDEX ROWID | EMP2 | 3000 | 1 | 3000 |00:00:00.01 | 3000 |
-----------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
4 - filter( IS NOT NULL)
5 - filter("LOC"='NEW YORK')
6 - access("DEPTNO"=:B1)
7 - access("E1"."NO"="E2"."NO" AND "E1"."EMPNO"="E2"."EMPNO")
id 6번에서 미리 'NEW YORK' 데이터를 필터해주고 조인하게 되어 조인 횟수가 14000번에서 3000번으로 줄었다.
읽은 블록 역시 14374개에서 3347개로 줄었다.