서브쿼리 변환이 필요한 경우

서브쿼리와 조인

필터 오퍼레이션

select c.고객번호, c.고객명
from   고객 c
where  c.가입일시 >= trunc(add_months(sysdate, -1), 'mm')
and    exists (
       select /*+ no_unnest */ 'x'
       from   거래
       where  고객번호 = c.고객번호
       and    거래일시 >= trunc(sysdate, 'mm') )

---------------------------------------------------------------------------
0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=289 Card=1 Bytes=39)
1    0   FILTER
2    1     TABLE ACCESS (BY INDEX ROWID) OF '고객' (TABLE) (Cost=4 Card=190 ...)
3    2       INDEX (RANGE SCAN) OF '고객_X01' (INDEX) (Cost=2 Card=190)
4    1     INDEX (RANGE SCAN) OF '거래_X01' (INDEX) (Cost=3 Card=4K Bytes=92K)
---------------------------------------------------------------------------
begin
  for outer in (select 고객번호, 고객명 from 고객 where … )
  loop
    for inner in (select 'x' from 거래 where 고객번호 = outer.고객번호 and … )
    loop
      dbms_output.put_line(outer.고객번호 || ',' || outer.고객명);
      exit; -- 조인에 성공하면 inner loop exit
    end loop;
  end loop;
end;

서브쿼리 Unnesting

select c.고객번호, c.고객명
from   고객 c

where  c.가입일시 >= trunc(add_months(sysdate, -1), 'mm')
and    exists (
       select /*+ unnest nl_sj */ 'x'
       from   거래
       where  고객번호 = c.고객번호
       and    거래일시 >= trunc(sysdate, 'mm') )

Execution Plan
---------------------------------------------------------------------------
0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=384 Card=190 Bytes=11K)
1    0   NESTED LOOPS (SEMI) (Cost=384 Card=190 Bytes=11K)
2    1     TABLE ACCESS (BY INDEX ROWID) OF '고객' (TABLE) (Cost=4 Card=190 …)
3    2       INDEX (RANGE SCAN) OF '고객_X01' (INDEX) (Cost=2 Card=190)
4    1     INDEX (RANGE SCAN) OF '거래_X01' (INDEX) (Cost=2 Card=427K Bytes=9M)
---------------------------------------------------------------------------
select /*+ leading(거래@subq) use_nl(c) */ c.고객번호, c.고객명
from   고객 c
where  c.가입일시 >= trunc(add_months(sysdate, -1), 'mm')
and    exists (
       select /*+ qb_name(subq) unnest */ 'x'
       from   거래
       where  고객번호 = c.고객번호
       and    거래일시 >= trunc(sysdate, 'mm') )

Execution Plan
---------------------------------------------------------------------------
0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=253K Card=190 Bytes=11K)
1    0   NESTED LOOPS
2    1     NESTED LOOPS (Cost=253K Card=190 Bytes=11K)
3    2       SORT (UNIQUE) (Cost=2K Card=427K Bytes=9M)
4    3         TABLE ACCESS (BY INDEX ROWID) OF '거래' (TABLE) (Cost=2K … )
5    4           INDEX (RANGE SCAN) OF '거래_X02' (INDEX) (Cost=988 Card=427K)
6    2       INDEX (RANGE SCAN) OF '고객_X01' (INDEX) (Cost=1 Card=190)
7    1     TABLE ACCESS (BY INDEX ROWID) OF '고객' (TABLE) (Cost=3 Card=1 … )
---------------------------------------------------------------------------
-- 서브쿼리 집합에서 고객번호 중복을 제거하기 위해 쿼리를 아래와 같이 변환한 것임
select /*+ no_merge(t) leading(t) use_nl(c) */ c.고객번호, c.고객명
from   (select distinct 고객번호
        from   거래
        where  거래일시 >= trunc(sysdate, 'mm')) t, 고객 c
where  c.가입일시 >= trunc(add_months(sysdate, -1), 'mm')
and    c.고객번호 = t.고객번호
-- 서브쿼리를 Unnesting하고 해시 세미 조인 방식으로 실행한 예시
select c.고객번호, c.고객명
from   고객 c
where  c.가입일시 >= trunc(add_months(sysdate, -1), 'mm')
and    exists (
       select /*+ unnest hash_sj */ 'x'
       from   거래
       where  고객번호 = c.고객번호
       and    거래일시 >= trunc(sysdate, 'mm') )

Execution Plan
---------------------------------------------------------------------------
0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=2K Card=38 Bytes=2K)
1    0   FILTER
2    1     HASH JOIN (SEMI) (Cost=2K Card=38 Bytes=2K)
3    2       TABLE ACCESS (BY INDEX ROWID) OF '고객' (TABLE) (Cost=3 Card=38 … )
4    3         INDEX (RANGE SCAN) OF '고객_X01' (INDEX) (Cost=2 Card=38)
5    2       TABLE ACCESS (BY INDEX ROWID) OF '거래' (TABLE) (Cost=2K … )
6    5         INDEX (RANGE SCAN) OF '거래_X02' (INDEX) (Cost=988 Card=427K)
---------------------------------------------------------------------------

서브쿼리에서 ROWNUM

select 글번호, 제목, 작성자, 등록일시
from   게시판 b
where  게시판구분 = '공지'
and    등록일시 >= trunc(sysdate-1)
and    exists (select 'x'
               from   수신대상자
               where  글번호 = b.글번호
               and    수신자 = :memb_no
               and    rownum <= 1)

서브쿼리 Pushing

select /*+ leading(p) use_nl(t) */ count(distinct p.상품번호), sum(t.주문금액)
from   상품 p, 주문 t
where  p.상품번호 = t.상품번호
and    p.등록일시 >= trunc(add_months(sysdate, -3), 'mm')
and    t.주문일시 >= trunc(sysdate - 7)
and    exists (select 'x' from 상품분류
               where 상품분류코드 = p.상품분류코드
               and 상위분류코드 = 'AK' )


Call     Count  CPU Time  Elapsed Time       Disk      Query    Current       Rows
-------  -----  --------  ------------  ---------  ---------  ---------  ---------
Parse        1     0.000         0.000          0          0          0          0
Execute      1     0.000         0.000          0          0          0          0
Fetch        2     0.484         3.493        650      38103          0          1
-------  -----  --------  ------------  ---------  ---------  ---------  ---------
Total        4     0.484         3.493        650      38103          0          1


Rows     Row Source Operation
-------  -----------------------------------------------------------------------
      0  STATEMENT
      1    SORT AGGREGATE (cr=38103 pr=650 pw=0 time=3493306 us)
   3000      FILTER (cr=38103 pr=650 pw=0 time=3486253 us)
  60000        NESTED LOOPS (cr=38097 pr=650 pw=0 time=3602032 us)
   1000          TABLE ACCESS FULL 상품 (cr=95 pr=0 pw=0 time=342023 us)
  60000          TABLE ACCESS BY INDEX ROWID 주문 (cr=38002 pr=650 pw=0 time= … )
  60000            INDEX RANGE SCAN 주문_PK (cr=2002 pr=90 pw=0 time=964606 us)
      1        TABLE ACCESS BY INDEX ROWID 상품분류 (cr=6 pr=0 pw=0 time=78 us)
      3          INDEX UNIQUE SCAN 상품분류_PK (cr=3 pr=0 pw=0 time=36 us)
-- PUSH_SUBQ 힌트 사용
select /*+ leading(p) use_nl(t) */ count(distinct p.상품번호), sum(t.주문금액)
from   상품 p, 주문 t
where  p.상품번호 = t.상품번호
and    p.등록일시 >= trunc(add_months(sysdate, -3), 'mm')
and    t.주문일시 >= trunc(sysdate - 7)
and    exists (select /*+ NO_UNNEST PUSH_SUBQ */ 'x' from 상품분류
               where 상품분류코드 = p.상품분류코드
               and 상위분류코드 = 'AK' )


Call     Count  CPU Time  Elapsed Time       Disk      Query    Current       Rows
-------  -----  --------  ------------  ---------  ---------  ---------  ---------
Parse        1     0.000         0.000          0          0          0          0
Execute      1     0.000         0.000          0          0          0          0
Fetch        2     0.125         0.129          0       1903          0          1
-------  -----  --------  ------------  ---------  ---------  ---------  ---------
Total        4     0.125         0.129          0       1903          0          1


Rows     Row Source Operation
-------  -----------------------------------------------------------------------
      0  STATEMENT
      1    SORT AGGREGATE (cr=1903 pr=0 pw=0 time=128934 us)
   3000      NESTED LOOPS (cr=1903 pr=0 pw=0 time=153252 us)
    150        TABLE ACCESS FULL 상품 (cr=101 pr=0 pw=0 time=18230 us)
      1        TABLE ACCESS BY INDEX ROWID 상품분류 (cr=6 pr=0 pw=0 time=135 us)
      3          INDEX UNIQUE SCAN 상품분류_PK (cr=3 pr=0 pw=0 time=63 us)
   3000        TABLE ACCESS BY INDEX ROWID 주문 (cr=1802 pr=0 pw=0 time=100092 us)
   3000          INDEX RANGE SCAN 주문_PK (cr=302 pr=0 pw=0 time=41733 us)

뷰(View)와 조인

select c.고객번호, c.고객명, t.평균거래, t.최소거래, t.최대거래
from   고객 c
     , (select 고객번호, avg(거래금액) 평균거래
             , min(거래금액) 최소거래, max(거래금액) 최대거래
        from   거래
        where  거래일시 >= trunc(sysdate, 'mm')          -- 당월 발생한 거래
        group by 고객번호) t
where  c.가입일시 >= trunc(add_months(sysdate, -1), 'mm') -- 전월 이후 가입 고객
and    t.고객번호 = c.고객번호

Execution Plan
---------------------------------------------------------------------------
0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=1M Card=1K Bytes=112K)
1    0   NESTED LOOPS
2    1     NESTED LOOPS (Cost=1M Card=1K Bytes=112K)
3    2       VIEW (Cost=2K Card=427K Bytes=21M)
4    3         HASH (GROUP BY) (Cost=2K Card=427K Bytes=14M)
5    4           TABLE ACCESS (BY INDEX ROWID) OF '거래' (TABLE) (Cost=2K … )
6    5             INDEX (RANGE SCAN) OF '거래_X01' (INDEX) (Cost=988 Card=427K)
7    2       INDEX (RANGE SCAN) OF '고객_X01' (INDEX) (Cost=1 Card=190)
8    1     TABLE ACCESS (BY INDEX ROWID) OF '고객' (TABLE) (Cost=3 Card=1 … )
---------------------------------------------------------------------------
-- merge 힌트를 이용해 뷰를 메인 쿼리와 머징(Merging)
select c.고객번호, c.고객명, t.평균거래, t.최소거래, t.최대거래
from   고객 c
     ,(select /*+ merge */ 고객번호, avg(거래금액) 평균거래
             , min(거래금액) 최소거래, max(거래금액) 최대거래
        from   거래
        where  거래일시 >= trunc(sysdate, 'mm')
        group by 고객번호) t
where  c.가입일시 >= trunc(add_months(sysdate, -1), 'mm')
and    t.고객번호 = c.고객번호

Execution Plan
---------------------------------------------------------------------------
0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=4 Card=1 Bytes=27)
1    0   HASH (GROUP BY) (Cost=4 Card=1 Bytes=27)
2    1     NESTED LOOPS (Cost=3 Card=5 Bytes=135)
3    2       TABLE ACCESS (BY INDEX ROWID) OF '고객' (TABLE) (Cost=2 Card=1 … )
4    3         INDEX (RANGE SCAN) OF '고객_X01' (INDEX) (Cost=1 Card=1)
5    2       TABLE ACCESS (BY INDEX ROWID) OF '거래' (TABLE) (Cost=1 Card=5 … )
6    5         INDEX (RANGE SCAN) OF '거래_X02' (INDEX) (Cost=0 Card=5)
---------------------------------------------------------------------------

-- 쿼리는 아래와 같이 변환되었음
select c.고객번호, c.고객명
     , avg(t.거래금액) 평균거래, min(t.거래금액) 최소거래, max(t.거래금액) 최대거래
from   고객 c, 거래 t
where  c.가입일시 >= trunc(add_months(sysdate, -1), 'mm')
and    t.고객번호 = c.고객번호
and    t.거래일시 >= trunc(sysdate, 'mm')
group by c.고객번호, c.고객명
Execution Plan
---------------------------------------------------------------------------
0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=8 Card=1 Bytes=27)
1    0   HASH (GROUP BY) (Cost=8 Card=1 Bytes=27)
2    1     HASH JOIN (Cost=7 Card=5 Bytes=135)
3    2       TABLE ACCESS (FULL) OF '고객' (TABLE) (Cost=3 Card=1 Bytes=20)
4    2       TABLE ACCESS (FULL) OF '거래' (TABLE) (Cost=3 Card=14 Bytes=98)
---------------------------------------------------------------------------

조인 조건 Pushdown

select c.고객번호, c.고객명, t.평균거래, t.최소거래, t.최대거래
from   고객 c
     , (select /*+ no_merge push_pred */
               고객번호, avg(거래금액) 평균거래
             , min(거래금액) 최소거래, max(거래금액) 최대거래
        from   거래
        where  거래일시 >= trunc(sysdate, 'mm')
        group by 고객번호) t
where  c.가입일시 >= trunc(add_months(sysdate, -1), 'mm')
and    t.고객번호 = c.고객번호

Execution Plan
---------------------------------------------------------------------------
0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=4 Card=1 Bytes=61)
1    0   NESTED LOOPS (Cost=4 Card=1 Bytes=61)
2    1     TABLE ACCESS (BY INDEX ROWID BATCHED) OF '고객' (TABLE) (Cost=2 … )
3    2       INDEX (RANGE SCAN) OF '고객_X01' (INDEX) (Cost=1 Card=1)
4    1     VIEW PUSHED PREDICATE (Cost=2 Card=1 Bytes=41)
5    4       SORT (GROUP BY) (Cost=2 Card=1 Bytes=7)
6    5         TABLE ACCESS (BY INDEX ROWID BATCHED) OF '거래' (TABLE) (Cost=2 … )
7    6           INDEX (RANGE SCAN) OF '거래_X02' (INDEX) (Cost=1 Card=5)
---------------------------------------------------------------------------

-- 옵티마이저가 쿼리를 아래처럼 변환해 최적화했다고 이해하면 쉬움
select c.고객번호, c.고객명, t.평균거래, t.최소거래, t.최대거래
from   고객 c
     ,(select 고객번호, avg(거래금액) 평균거래
             , min(거래금액) 최소거래, max(거래금액) 최대거래
        from   거래
        where  거래일시 >= trunc(sysdate, 'mm')
        and    고객번호 = c.고객번호
        group by 고객번호) t
where  c.가입일시 >= trunc(add_months(sysdate, -1), 'mm')

Lateral 인라인 뷰, Cross/Outer Apply 조인

select * from 사원 e,
       LATERAL (select *
                from 조직
                where 조직코드 = e.조직코드)

-- Lateral 인라인 뷰와 Outer 조인
select * from 사원 e,
       LATERAL (select *
                from 조직
                where 조직코드 = e.조직코드)(+)
select * from 사원 e
       OUTER APPLY (select *
                    from 조직
                    where 조직코드 = e.조직코드)
select * from 사원 e
       CROSS APPLY (select *
                    from 조직
                    where 조직코드 = e.조직코드);

스칼라 서브쿼리 조인

스칼라 서브쿼리의 특징

-- GET_DNAME 함수
create or replace function GET_DNAME(p_deptno number) return varchar2
is
  l_dname dept.dname%TYPE;
begin
  select dname into l_dname from dept where deptno = p_deptno;
  return l_dname ;
exception
  when others then
    return null;
end;
/

select empno, ename, sal, hiredate
     , GET_DNAME(e.deptno) as dname
from   emp e
where  sal >= 2000
select empno, ename, sal, hiredate
     , (select d.dname from dept d where d.deptno = e.deptno) as dname
from   emp e
where  sal >= 2000

-- 마치 outer 조인문처럼 하나의 문장으로 이해하자
select /*+ ordered use_nl(d) */ e.empno, e.ename, e.sal, e.hiredate, d.dname
from   emp e, dept d
where  d.deptno(+) = e.deptno
and    e.sal >= 2000

스칼라 서브쿼리 캐싱 효과

select empno, ename, sal, hiredate
     , (
       select d.dname        -- 출력 값 : d.dname
       from   dept d
       where  d.deptno = e.deptno   -- 입력 값 : e.deptno
       )
from   emp e
where  sal >= 2000
select empno, ename, sal, hiredate
     , (select GET_DNAME(e.deptno) from dual) dname
from   emp e
where  sal >= 2000

스칼러 서브쿼리 캐싱의 부작용

select 거래번호, 고객번호, 영업조직ID, 거래구분코드
     , (select 거래구분명 from 거래구분 where 거래구분코드 = t.거래구분코드) 거래구분명
from   거래 t
where  거래일자 >= to_char(add_months(sysdate, -3), 'yyyymmdd') -- 50,000건
select 거래번호, 고객번호, 영업조직ID, 거래구분코드
     , (select 고객명 from 고객 where 고객번호 = t.고객번호) 고객명
from   거래 t
where  거래일자 >= to_char(add_months(sysdate, -3), 'yyyymmdd') -- 50,000건
select 매도회원번호
     , 매수회원번호
     , 매도투자자구분코드
     , 매수투자자구분코드
     , 체결유형코드
     , 매도계좌번호, (select acnt_nm(매도계좌번호) from dual) 매도계좌명
     , 매수계좌번호, (select acnt_nm(매수계좌번호) from dual) 매수계좌명
     , 체결시각
     , 체결수량
     , 체결가
     , 체결수량 * 체결가 체결금액
from   체결
where  종목코드 = :종목코드
and    체결일자 = :체결일자
and    체결시각 between sysdate-10/24/60 and sysdate
select 계좌번호, 계좌명, 고객번호, 개설일자, 계좌종류구분코드, 은행개설여부, 은행연계여부
     , (select brch_nm(관리지점코드) from dual) 관리지점명
     , (select brch_nm(개설지점코드) from dual) 개설지점명
from   계좌
where  고객번호 = :고객번호

두개 이상의 값 반환

select c.고객번호, c.고객명
     ,(select round(avg(거래금액), 2) 평균거래금액
       from   거래
       where  거래일시 >= trunc(sysdate, 'mm')
       and    고객번호 = c.고객번호)
from   고객 c
where  c.가입일시 >= trunc(add_months(sysdate, -1), 'mm')

Execution Plan
---------------------------------------------------------------------------
0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=7 Card=4 Bytes=80)
1    0   SORT (AGGREGATE) (Card=1 Bytes=7)
2    1     TABLE ACCESS (BY INDEX ROWID BATCHED) OF '거래' (TABLE) (Cost=2 … )
3    2       INDEX (RANGE SCAN) OF '거래_X02' (INDEX) (Cost=1 Card=5)
4    0   TABLE ACCESS (FULL) OF '고객' (TABLE) (Cost=3 Card=4 Bytes=80)
5    4     INDEX (RANGE SCAN) OF '고객_X01' (INDEX) (Cost=1 Card=1)
---------------------------------------------------------------------------
-- 이렇게 SQL을 작성할 수 없음
select c.고객번호, c.고객명
     ,(select avg(거래금액), min(거래금액), max(거래금액)
       from   거래
       where  거래일시 >= trunc(sysdate, 'mm')
       and    고객번호 = c.고객번호)
from   고객 c
where  c.가입일시 >= trunc(add_months(sysdate, -1), 'mm')
select c.고객번호, c.고객명
     ,(select avg(거래금액) from 거래
       where 거래일시 >= trunc(sysdate, 'mm') and 고객번호 = c.고객번호)
     ,(select min(거래금액) from 거래
       where 거래일시 >= trunc(sysdate, 'mm') and 고객번호 = c.고객번호)
     ,(select max(거래금액) from 거래
       where 거래일시 >= trunc(sysdate, 'mm') and 고객번호 = c.고객번호)
from   고객 c
where  c.가입일시 >= trunc(add_months(sysdate, -1), 'mm')
-- 구하는 값들을 문자열로 모두 결합하고, 바깥쪽 엑세스 쿼리에서 substr로 분리
select 고객번호, 고객명
     , to_number(substr(거래금액, 1, 10)) 평균거래금액
     , to_number(substr(거래금액, 11, 10)) 최소거래금액
     , to_number(substr(거래금액, 21)) 최대거래금액
from (
  select c.고객번호, c.고객명
       , (select lpad(avg(거래금액), 10) || lpad(min(거래금액), 10) || max(거래금액)
          from 거래
          where 거래일시 >= trunc(sysdate, 'mm')
          and 고객번호 = c.고객번호) 거래금액
  from 고객 c
  where c.가입일시 >= trunc(add_months(sysdate, -1), 'mm')
)

-- 오브젝트 TYPE을 사용하는 방법도 있으나, TYPE을 미리 선언해야 하는 불편함이 존재
create or replace type 거래금액_T as object
( 평균거래금액 number, 최소거래금액 number, 최대거래금액 number )
/

select 고객번호, 고객명
     , 거래.금액.평균거래금액, 거래.금액.최소거래금액, 거래.금액.최대거래금액
from (
  select c.고객번호, c.고객명
       , (select 거래금액_T( avg(거래금액), min(거래금액), max(거래금액) ) 금액
          from   거래
          where  거래일시 >= trunc(sysdate, 'mm')
          and    고객번호 = c.고객번호) 거래
  from   고객 c
  where  c.가입일시 >= trunc(add_months(sysdate, -1), 'mm')
)
select c.고객번호, c.고객명, t.평균거래, t.최소거래, t.최대거래
from 고객 c
   , (select 고객번호, avg(거래금액) 평균거래
           , min(거래금액) 최소거래, max(거래금액) 최대거래
      from 거래
      where 거래일시 >= trunc(sysdate, 'mm')
      group by 고객번호) t
where c.가입일시 >= trunc(add_months(sysdate, -1), 'mm')
and t.고객번호(+) = c.고객번호

Execution Plan
---------------------------------------------------------------------------------------
 0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=4 Card=1 Bytes=27)
 1   0    HASH (GROUP BY) (Cost=4 Card=1 Bytes=27)
 2   1      NESTED LOOPS (OUTER) (Cost=3 Card=5 Bytes=135)
 3   2        TABLE ACCESS (BY INDEX ROWID) OF '고객' (TABLE) (Cost=2 Card=1 ...)
 4   3          INDEX (RANGE SCAN) OF '고객_X01' (INDEX) (Cost=1 Card=1)
 5   2        TABLE ACCESS (BY INDEX ROWID) OF '거래' (TABLE) (Cost=1 Card=5 ...)
 6   5          INDEX (RANGE SCAN) OF '거래_X02' (INDEX) (Cost=0 Card=5)
---------------------------------------------------------------------------------------
select c.고객번호, c.고객명, t.평균거래, t.최소거래, t.최대거래
from 고객 c
   , (select /*+ no_merge push_pred */
             고객번호, avg(거래금액) 평균거래
           , min(거래금액) 최소거래, max(거래금액) 최대거래
      from 거래
      where 거래일시 >= trunc(sysdate, 'mm')
      group by 고객번호) t
where c.가입일시 >= trunc(add_months(sysdate, -1), 'mm')
and t.고객번호(+) = c.고객번호

-- Execution Plan
---------------------------------------------------------------------------------------
 0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=4 Card=1 Bytes=61)
 1   0    NESTED LOOPS (OUTER) (Cost=4 Card=1 Bytes=61)
 2   1      TABLE ACCESS (BY INDEX ROWID BATCHED) OF '고객' (TABLE) (Cost=2 ...)
 3   2        INDEX (RANGE SCAN) OF '고객_X01' (INDEX) (Cost=1 Card=1)
 4   1      VIEW PUSHED PREDICATE (Cost=2 Card=1 Bytes=41)
 5   4        SORT (GROUP BY) (Cost=2 Card=1 Bytes=7)
 6   5          TABLE ACCESS (BY INDEX ROWID BATCHED) OF '거래' (TABLE) (Cost=2 ...)
 7   6            INDEX (RANGE SCAN) OF '거래_X02' (INDEX) (Cost=1 Card=5)
---------------------------------------------------------------------------------------

스칼러 서브쿼리 Unnesting

select c.고객번호, c.고객명
      ,(select /*+ unnest */ round(avg(거래금액), 2) 평균거래금액
        from   거래
        where  거래일시 >= trunc(sysdate, 'mm')
        and    고객번호 = c.고객번호)
from   고객 c
where  c.가입일시 >= trunc(add_months(sysdate, -1), 'mm')

Execution Plan
---------------------------------------------------------------------------
0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=7 Card=4 Bytes=184)
1    0   HASH JOIN (OUTER) (Cost=7 Card=4 Bytes=184)
2    1     TABLE ACCESS (FULL) OF '고객' (TABLE) (Cost=3 Card=4 Bytes=80)
3    1     VIEW OF 'SYS.VW_SSQ_1' (VIEW) (Cost=4 Card=3 Bytes=78)
4    3       HASH (GROUP BY) (Cost=4 Card=3 Bytes=21)
5    4         TABLE ACCESS (FULL) OF '거래' (TABLE) (Cost=3 Card=14 Bytes=98)

-- unnest와 merge 힌트를 같이 사용했을 때
Execution Plan
---------------------------------------------------------------------------
0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=7 Card=15 Bytes=405)
1    0   HASH (GROUP BY) (Cost=7 Card=15 Bytes=405)
2    1     HASH JOIN (OUTER) (Cost=6 Card=15 Bytes=405)
3    2       TABLE ACCESS (FULL) OF '고객' (TABLE) (Cost=3 Card=4 Bytes=80)
4    2       TABLE ACCESS (FULL) OF '거래' (TABLE) (Cost=3 Card=14 Bytes=98)
select c.고객번호, c.고객명
      ,(select /*+ no_unnest */ round(avg(거래금액), 2) 평균거래금액
        from   거래
        where  거래일시 >= trunc(sysdate, 'mm')
        and    고객번호 = c.고객번호)
from   고객 c
where  c.가입일시 >= trunc(add_months(sysdate, -1), 'mm')

Execution Plan
---------------------------------------------------------------------------
0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=7 Card=4 Bytes=80)
1    0   SORT (AGGREGATE) (Card=1 Bytes=7)
2    1     TABLE ACCESS (BY INDEX ROWID) OF '거래' (TABLE) (Cost=2 ...)
3    2       INDEX (RANGE SCAN) OF '거래_X02' (INDEX) (Cost=1 Card=5)
4    0   TABLE ACCESS (BY INDEX ROWID) OF '고객' (TABLE) (Cost=3 Card=4 Bytes=80)
5    4     INDEX (RANGE SCAN) OF '고객_X01' (INDEX) (Cost=1 Card=1)