'unnest'에 해당되는 글 2건

  1. 2009.04.15 오라클에 트랜스포머가 있다? 4
  2. 2008.11.19 Access Sub Query 의 함정 4
부제목: 다단계 쿼리변환 (Muti-Phase Query Transformation)

  SF 영화 트랜스포머를 보면 자동차가 로봇으로 변환하는 과정이 있다. 자동차와 로봇간의 변환과정은 아주 현란하다 못해 활홍하여 시청자자로 하여금 넋을 놓고 빠져들게 한다. 컴퓨터그래픽(CG) 기술의 발전 덕분이다.

변환과정이 있어야 지구를 지킬수 있어
  만약 이 영화에서 자동차가 로봇으로 변환을 못한다고 상상해보자. 악한 로봇이 쳐들어와도 싸울수가 없고 격렬한 전투장면도 사라진다. 이래서는 영화가 재미없을 뿐더러 지구를 지킬수도 없다. 그럼 오라클에서 Query Transformer 가 없어진다면 어떻게 될까? 마찬가지로 Query 의 상당부분을 튜닝할수 없게 되어 전체 시스템이 느려지게된다. Query Transformer 의 목적은 성능향상에 있다.

오라클에도 트랜스포머가 있다.
  오라클 Optimizer 에서 Query Transformer 는 3대 Components 로서 아주 중요한 위치에 있다.
먼저 Query Transformer 를 이해하기 위해서 Optimizer 구조를 살펴볼 필요가 있다.
사용자 삽입 이미지
 
먼저 Query Parser 가 SQL 을 검사하여 넘겨주면 Transformer 가 SQL 을 변신시켜서 Estimator 에 넘겨준다.
이때 Estimator는 통계정보등을 참조하여 가장 낮은 cost 를 갖는 SQL 을 찾아내어 Plan Generator 에 넘겨주고 실행계획을 완성하게 된다.  사실 위의 그림은 오라클 Performance Tuning Guide 에 있는 그림 이지만 잘못된 것이 있다. Query Transformer 가 Estimator 에게 주는 SQL 은 하나이상이 될수 있으므로  Estimator 와 Plan Generator 의 관계처럼 반복적인 Loop 가 있어야 한다.

변환과정도 로봇에 따라 다양하다.
  트랜스포머에서 주인공 로봇의 변환과정은 아주 복잡하다. 하지만 소형 악당 로봇이 카세트 레코더로 변환하는 과정을 유심히 보았는가? 이 과정은 매우 간단하다. 오라클의 쿼리변환(Query Transformation) 과정도 간단한 것에서 부터 아주 복잡한 과정을 거치는 것 까지 다양하다.

구슬이 서말이라도 꿰어야 보배
  오늘은 조금 어려운 다단계 쿼리변환-(Muti-Phase-Query Transformation)에 대하여 알아보려 한다.
참고로 아래의 글이 이해하기 힘든 독자는 필자의 이전글 Using Sub query Method (Sub query Flattening ) 과 Using Sub query Method( Filter / Access sub Query ) 를 먼저 읽어보기 바란다.
그럼 각 단계별로 변환과정을 보자. 

1 단계 : 원본 쿼리
            자신이 속한 부서의 평균급여 보다 돈을 많이 받는 사원을 추출하는 예제이다.

select /*+ gather_plan_statistics */ outer.*
 from emp outer
where outer.sal > ( select /*+ NO_UNNEST */  avg(inner.sal)
                              from emp inner
                             where inner.deptno = outer.deptno
                           ); 


--------------------------------------------------------------------------------------------
| Id  | Operation                     | Name      | Starts | A-Rows |   A-Time   | Buffers |
--------------------------------------------------------------------------------------------
|*  1 |  FILTER                       |           |      1 |      5 |00:00:00.01 |      16 |
|   2 |   TABLE ACCESS FULL           | EMP       |      1 |     14 |00:00:00.01 |       8 |
|   3 |   SORT AGGREGATE              |           |      5 |      5 |00:00:00.01 |       8 |
|   4 |    TABLE ACCESS BY INDEX ROWID| EMP       |      5 |     13 |00:00:00.01 |       8 |
|*  5 |     INDEX RANGE SCAN          | IX_EMP_N3 |      5 |     13 |00:00:00.01 |       3 |
--------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
   1 - filter("OUTER"."SAL">)
   5 - access("INNER"."DEPTNO"=:B1)


전통적인 Filter Subquery(확인자 SubQuery) 이다.


2.단계 : 서브쿼리를 인라인뷰로 바꿔라.
 이 단계에서 unnest 힌트를 사용함으로서 Subquery 가 인라인뷰로 바뀌며 서브쿼리가 없어진다. 이때 메인쿼리의 건수를 유지하기 위해 인라인뷰에 group by 가 추가된다.

select /*+ gather_plan_statistics */ outer.*
 from emp outer
where outer.sal > ( select /*+ QB_NAME(SUB) UNNEST */  avg(inner.sal)
                               from emp inner
                             where inner.deptno = outer.deptno
                          );


-----------------------------------------------------------------------------------------------------
| Id  | Operation                   | Name      | Starts | A-Rows |   A-Time   | Buffers | Used-Mem |
-----------------------------------------------------------------------------------------------------
|*  1 |  TABLE ACCESS BY INDEX ROWID| EMP       |      1 |      5 |00:00:00.01 |      16 |          |
|   2 |   NESTED LOOPS              |           |      1 |     19 |00:00:00.09 |      10 |          |
|   3 |    VIEW                     | VW_SQ_1   |      1 |      5 |00:00:00.01 |       7 |          |
|   4 |     HASH GROUP BY           |           |      1 |      5 |00:00:00.01 |       7 | 1622K (0)|
|   5 |      TABLE ACCESS FULL      | EMP       |      1 |     14 |00:00:00.01 |       7 |          |
|*  6 |    INDEX RANGE SCAN         | IX_EMP_N3 |      5 |     13 |00:00:00.01 |       3 |          |
-----------------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
   1 - filter("OUTER"."SAL">"VW_COL_1")
   6 - access("DEPTNO"="OUTER"."DEPTNO")
       filter("OUTER"."DEPTNO" IS NOT NULL)  

이것은
Optimizer가 쿼리를 아래처럼 변형시킨것이다.

select /*+ gather_plan_statistics  */
       outer.*
 from emp outer,
       ( select deptno, avg(sal) AS VW_COL_1
            from emp
          group by deptno
        ) A
where outer.sal > A.VW_COL_1
   and outer.deptno = A.deptno ;

-----------------------------------------------------------------------------------------------------
| Id  | Operation                   | Name      | Starts | A-Rows |   A-Time   | Buffers | Used-Mem |
-----------------------------------------------------------------------------------------------------
|*  1 |  TABLE ACCESS BY INDEX ROWID| EMP       |      1 |      5 |00:00:00.01 |      16 |          |
|   2 |   NESTED LOOPS              |           |      1 |     19 |00:00:00.13 |      10 |          |
|   3 |    VIEW                     |           |      1 |      5 |00:00:00.01 |       7 |          |
|   4 |     HASH GROUP BY           |           |      1 |      5 |00:00:00.01 |       7 | 1622K (0)|
|   5 |      TABLE ACCESS FULL      | EMP       |      1 |     14 |00:00:00.01 |       7 |          |
|*  6 |    INDEX RANGE SCAN         | IX_EMP_N3 |      5 |     13 |00:00:00.01 |       3 |          |
-----------------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
   1 - filter("OUTER"."SAL">"A"."VW_COL_1")
   6 - access("OUTER"."DEPTNO"="A"."DEPTNO")
       filter("OUTER"."DEPTNO" IS NOT NULL)

 2단계의 원본 쿼리와 Plan 이 일치함을 알수 있다.


3단계 : 인라인뷰를 해체하라.
MERGE 힌트를 사용함으로서 2단계에서 Unnesting 된 인라인뷰를 해체하여 조인으로 바뀌었다. 이것을 View Merging 이라고 부른다.

select /*+ gather_plan_statistics MERGE(@SUB) */
       outer.*
 from emp outer
where outer.sal > ( select /*+ QB_NAME(SUB) UNNEST */  avg(inner.sal)
                               from emp inner
                            where inner.deptno = outer.deptno
                          );

다시말하면 위의 쿼리를 Optimizer가 아래처럼 재작성 한것이다.

select /*+ gather_plan_statistics */
             outer.deptno deptno,outer.sal sal,
             outer.empno empno
   from emp inner,
          emp outer
  where inner.deptno=outer.deptno
  group by inner.deptno, outer.rowid, outer.empno, outer.sal, outer.deptno
  having outer.sal > avg(inner.sal) ;

메인쿼리의 결과집합을 보존하기위하여 rowid 로 Group by 를 한것에 유의하자.
두개의 Query  Plan 은 동일하며 아래와 같다.
 
-----------------------------------------------------------------------------------------------
| Id  | Operation                      | Name      | A-Rows |   A-Time   | Buffers | Used-Mem |
-----------------------------------------------------------------------------------------------
|*  1 |  FILTER                        |           |      5 |00:00:00.01 |      12 |          |
|   2 |   HASH GROUP BY                |           |     13 |00:00:00.01 |      12 | 1103K (0)|
|   3 |    MERGE JOIN                  |           |     51 |00:00:00.01 |      12 |          |
|   4 |     TABLE ACCESS BY INDEX ROWID| EMP       |     13 |00:00:00.01 |       5 |          |
|*  5 |      INDEX FULL SCAN           | IX_EMP_N3 |     13 |00:00:00.01 |       1 |          |
|*  6 |     SORT JOIN                  |           |     51 |00:00:00.01 |       7 | 2048  (0)|
|*  7 |      TABLE ACCESS FULL         | EMP       |     13 |00:00:00.01 |       7 |          |
-----------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
   1 - filter("OUTER"."SAL">AVG("INNER"."SAL"))
   5 - filter("INNER"."DEPTNO" IS NOT NULL)
   6 - access("INNER"."DEPTNO"="OUTER"."DEPTNO")
       filter("INNER"."DEPTNO"="OUTER"."DEPTNO")
   7 - filter("OUTER"."DEPTNO" IS NOT NULL)
 
 드디어  1~3 단계에 걸친 Query Transformation 단계가 완성 되었다. 그결과는 성능면에서 대성공이다. Buffers(읽은 Block수) 가 16(원본쿼리) 에서 12 로 약 25% 감소했다.

오라클 트랜스포머는 악성쿼리와 싸워...
  오라클 Query Transformer 는 SQL 을 멋지게 변화시켰다. 이모든 과정을 개발자가 해야한다고 상상해보자.
개발자들에게 전체과정을 이해시키는 교육과정이 추가되어야 하고 개발속도는 몇배나 느려질것이다. 이는 프로젝트의 Risk 가 될것이다. 하지만 오라클 Query Transformer 가 있으므로 악당 로봇이 아닌 악성쿼리와 멋지게 싸워서 이길수 있는 것이다.

편집후기 :
  Query Transformation 을 하려면 반드시 unnesting 이나 merge 힌트를 써야 하는지 질문이 들어왔다. 대부분의 경우 Query Transformer 가 자동으로 변환과정을 수행해준다. 하지만 이것이 가끔 제대로 수행이 안될수 있으므로 이럴경우에만 명시적으로 힌트를 사용하는것이 바람직하다.  

Posted by extremedb
,

9월 달에 Using Sub query Method( Filter / Access sub Query ) 이란 글을 통하여 Access 서브쿼리를
설명한적이 있다.
오늘은 서브쿼리 사용시 함정이 몇가지 있지만 그중에서 가장 자주발생하는 Access 서브쿼리의 경우를 소개한다.

서브쿼리를 사용할때 Access sub Query plan (제공자 서브쿼리 plan)이 나오더라도 방심하면 안된다.
Plan 의 모습만 Access sub Query 일뿐 사실은 Filter sub Query(확인자 서브쿼리) 로 풀리는 경우가 많이 있기 때문이다.
 
실행환경 : Oracle 10.2.0.4
아래는 전형적인  Access sub Query plan 을 보여준다.

select small_vc
  from min_max mm1
 where mm1.id_parent = 100
   and mm1.id_child = ( select max(mm2.id_child)
                                    from min_max mm2
                                 where mm2.id_parent = 100 )  ;

         
--------------------------------------------------------------------------
| Id  | Operation                      | Name    | Rows  | Bytes | Cost  |
--------------------------------------------------------------------------
|   0 | SELECT STATEMENT               |         |     1 |   108 |     4 |
|   1 |  TABLE ACCESS BY INDEX ROWID   | MIN_MAX |     1 |   108 |     2 |
|*  2 |   INDEX UNIQUE SCAN            | MM_PK   |     1 |       |     1 |
|   3 |    SORT AGGREGATE              |         |     1 |     8 |       |
|   4 |     FIRST ROW                  |         |    10 |    80 |     2 |
|*  5 |      INDEX RANGE SCAN (MIN/MAX)| MM_PK   |    10 |    80 |     2 |
--------------------------------------------------------------------------

위 쿼리의 실행순서는 id 기준으로 5 -> 4 -> 3 -> 2 -> 1 이라고 설명 하였다.
하지만 항상 그렇게 되는 것은 아니다.
아래 스크립트를 보자  

인덱스 현황 :
1)EMP 테이블  : EMP_IX04  (JOB, DEPTNO, HIREDATE)
2)DEPT 테이블 : DEPT_IDX1 (LOC)

SELECT /*+ GATHER_PLAN_STATISTICS INDEX_RS(A EMP_IX04) */ *
  FROM EMP A
 WHERE A.JOB = 'CLERK'
   AND A.DEPTNO IN (SELECT /*+ NO_UNNEST INDEX(B DEPT_IDX1) */  B.DEPTNO
                                  FROM DEPT B
                                WHERE B.LOC = 'CHICAGO')
   AND A.HIREDATE BETWEEN '19801010' AND '19820101';


SELECT *
FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(NULL,NULL,'ALLSTATS LAST'));

---------------------------------------------------------------------------------------------
| Id  | Operation                      | Name      | Starts | A-Rows |   A-Time   | Buffers |
---------------------------------------------------------------------------------------------
|*  1 |  FILTER                        |           |      1 |      1 |00:00:00.01 |       7 |
|   2 |   TABLE ACCESS BY INDEX ROWID  | EMP       |      1 |      1 |00:00:00.01 |       7 |
|*  3 |    INDEX RANGE SCAN            | EMP_IX04  |      1 |      1 |00:00:00.01 |       6 |
|*  4 |     TABLE ACCESS BY INDEX ROWID| DEPT      |      2 |      1 |00:00:00.01 |       4 |
|*  5 |      INDEX RANGE SCAN          | DEPT_IDX1 |      2 |      2 |00:00:00.01 |       2 |
---------------------------------------------------------------------------------------------


위 SQL 의 의도는 부서 테이블에서 위치가 CHICAGO 인 부서를 찾아서 EMP 테이블에 제공 하는것이다.
따라서 의도적으로 서브쿼리에 NO_UNNEST 힌트를 사용하였다.
PLAN 을 보면 정상적 이라고 생각할 수 있다.
그런데 Starts 컬럼(빨강색)을 보면 서브쿼리가 2번 실행된걸 알수 있다.
뭔가 이상하다.
제공자 서브쿼리는 단 한번만 실행 되어야 하는데...
아래의 Predicate Information 을 보면 더욱 이상한것을 발견할수 있다.

Predicate Information (identified by operation id):
---------------------------------------------------
 
   1 - filter(TO_DATE('19801010')<=TO_DATE('19820101'))
   3 - access("A"."JOB"='CLERK' AND "A"."HIREDATE">='19801010' AND "A"."HIREDATE"<='19820101')
       filter(("A"."HIREDATE"<='19820101' AND "A"."HIREDATE">='19801010' AND  IS NOT NULL))

   4 - filter("B"."DEPTNO"=:B1)
   5 - access("B"."LOC"='CHICAGO')
 
id 3에서  access 정보를 보면 DEPTNO 가 빠져있다.
인덱스가 (JOB, DEPTNO, HIREDATE) 인 상황에서 JOB, HIREDATE 만 사용하였다.
또한 id 4에서 filter("B"."DEPTNO"=:B1) 이 발생하였다.
이것은 확인자 서브쿼리에서만 나올수 있는 정보이다.
뭔가 이상하지 않은가?

이것을 결정적으로 확인할수 있는것은
DBMS_XPLAN.DISPLAY_CURSOR 가 아니라
DBMS_XPLAN.DISPLAY 이다.

SELECT *
FROM TABLE(DBMS_XPLAN.DISPLAY);


---------------------------------------------------------------------------------
| Id  | Operation                      | Name      | Rows  | Bytes | Cost (%CPU)|
---------------------------------------------------------------------------------
|   0 | SELECT STATEMENT               |           |     1 |    32 |     3   (0)|
|*  1 |  FILTER                        |           |       |       |            |
|   2 |   TABLE ACCESS BY INDEX ROWID  | EMP       |     1 |    32 |     1   (0)|
|*  3 |    INDEX RANGE SCAN            | EMP_IX04  |     1 |       |     1   (0)|
|*  4 |     TABLE ACCESS BY INDEX ROWID| DEPT      |     1 |     9 |     1   (0)|
|*  5 |      INDEX RANGE SCAN          | DEPT_IDX1 |     1 |       |     1   (0)|
---------------------------------------------------------------------------------

 
Predicate Information (identified by operation id):
---------------------------------------------------
 
   1 - filter(TO_DATE('19801010')<=TO_DATE('19820101'))
   3 - access("A"."JOB"='CLERK' AND "A"."HIREDATE">='19801010' AND
              "A"."HIREDATE"<='19820101')
       filter("A"."HIREDATE"<='19820101' AND "A"."HIREDATE">='19801010'
              AND  EXISTS (SELECT /*+ NO_UNNEST INDEX ("B" "DEPT_IDX1") */ 0 FROM  --> EXISTS 발생
              "DEPT" "B" WHERE "B"."LOC"='CHICAGO' AND "B"."DEPTNO"=:B1))
   4 - filter("B"."DEPTNO"=:B1)
   5 - access("B"."LOC"='CHICAGO')

Predicate Information 을 보면 분명히 Filter sub Query(확인자 서브쿼리) 로 풀리는걸 확인할수 있다.
많은 튜닝책에서 이런식으로 결합인덱스를 만들면 제공자 서브쿼리로 풀린다고 가이드 하고 있지만
불행히도 버젼 9i 이후부터는 그렇지못하다.
Access sub Query 으로 예상 했지만  Filter sub Query 로 풀리면 심각한 성능저하 현상이 발생할수 있다.
그렇다면 어떻게  Filter sub Query 를 Access sub Query(제공자 서브쿼리) 로 만들것인가?
오라클은 옵티마이져는 이러한 경우에 왠만 해서는 Access sub Query 를 선택하지 않는다.
이때 간단한 트릭을 생각할수 있다.
몇가지 방법이 있지만 가장 간단한 두가지의 방법을 소개한다.
서브쿼리가 항상 1건만 Return 한다는 정보를 옵티마이져에게 주면된다.

1.min/max 함수 사용하기

SELECT /*+ GATHER_PLAN_STATISTICS INDEX(A EMP_IX04) */ *
  FROM EMP A
 WHERE A.JOB = 'CLERK'
   AND A.DEPTNO IN (SELECT /*+ NO_UNNEST INDEX(B DEPT_IDX1) */  MAX(B.DEPTNO)
                                  FROM DEPT B
                                 WHERE B.LOC = 'CHICAGO')
   AND A.HIREDATE BETWEEN '19801010' AND '19820101' ;

----------------------------------------------------------------------------------------------
| Id  | Operation                       | Name      | Starts | A-Rows |   A-Time   | Buffers |
----------------------------------------------------------------------------------------------
|*  1 |  FILTER                         |           |      1 |      1 |00:00:00.01 |       5 |
|   2 |   TABLE ACCESS BY INDEX ROWID   | EMP       |      1 |      1 |00:00:00.01 |       5 |
|*  3 |    INDEX RANGE SCAN             | EMP_IX04  |      1 |      1 |00:00:00.01 |       4 |
|   4 |     SORT AGGREGATE              |           |      1 |      1 |00:00:00.01 |       2 |
|   5 |      TABLE ACCESS BY INDEX ROWID| DEPT      |      1 |      1 |00:00:00.01 |       2 |
|*  6 |       INDEX RANGE SCAN          | DEPT_IDX1 |      1 |      1 |00:00:00.01 |       1 |
----------------------------------------------------------------------------------------------

 
Predicate Information (identified by operation id):
---------------------------------------------------
 
   1 - filter(TO_DATE('19801010')<=TO_DATE('19820101'))
   3 - access("A"."JOB"='CLERK' AND "A"."DEPTNO"= AND "A"."HIREDATE">='19801010' AND
              "A"."HIREDATE"<='19820101')
   6 - access("B"."LOC"='CHICAGO')

정상적으로 Access sub Query plan 이 나온것을 확인할수 있다.

2. IN 서브쿼리 대신에 = 서브쿼리로 바꾸기

SELECT /*+ GATHER_PLAN_STATISTICS INDEX(A EMP_IX04) */ *
  FROM EMP A
 WHERE A.JOB = 'CLERK'
   AND A.DEPTNO = (SELECT /*+ NO_UNNEST INDEX(B DEPT_IDX1) */  B.DEPTNO
                                 FROM DEPT B
                               WHERE B.LOC = 'CHICAGO')
   AND A.HIREDATE BETWEEN '19801010' AND '19820101';

---------------------------------------------------------------------------------------------
| Id  | Operation                      | Name      | Starts | A-Rows |   A-Time   | Buffers |
---------------------------------------------------------------------------------------------
|*  1 |  FILTER                        |           |      1 |      1 |00:00:00.01 |       5 |
|   2 |   TABLE ACCESS BY INDEX ROWID  | EMP       |      1 |      1 |00:00:00.01 |       5 |
|*  3 |    INDEX RANGE SCAN            | EMP_IX04  |      1 |      1 |00:00:00.01 |       4 |
|   4 |     TABLE ACCESS BY INDEX ROWID| DEPT      |      1 |      1 |00:00:00.01 |       2 |
|*  5 |      INDEX RANGE SCAN          | DEPT_IDX1 |      1 |      1 |00:00:00.01 |       1 |
---------------------------------------------------------------------------------------------

 
Predicate Information (identified by operation id):
---------------------------------------------------
 
   1 - filter(TO_DATE('19801010')<=TO_DATE('19820101'))
  3 - access("A"."JOB"='CLERK' AND "A"."DEPTNO"= AND "A"."HIREDATE">='19801010' AND
              "A"."HIREDATE"<='19820101')
   5 - access("B"."LOC"='CHICAGO')

두방법 모두 성공적으로 Access sub Query plan 으로 풀린다.
Buffers 도 7 에서 5 로 줄어들었다.
하지만 서브쿼리에서 2건 이상 RETURN 되는 경우는 어떻게 할것인가?
필자의 생각은 Access sub Query plan 을 포기하고 UNNEST 서브쿼리를 선택하라는 것이다.

SELECT /*+ GATHER_PLAN_STATISTICS INDEX(A EMP_IX04) */ *
  FROM EMP A
 WHERE A.JOB = 'CLERK'
   AND A.DEPTNO IN (SELECT /*+ UNNEST INDEX(B DEPT_IDX1) */  B.DEPTNO
                                  FROM DEPT B
                                WHERE B.LOC = 'CHICAGO')
   AND A.HIREDATE BETWEEN '19801010' AND '19820101';

---------------------------------------------------------------------------------------------
| Id  | Operation                      | Name      | Starts | A-Rows |   A-Time   | Buffers |
---------------------------------------------------------------------------------------------
|*  1 |  FILTER                        |           |      1 |      1 |00:00:00.01 |       6 |
|   2 |   TABLE ACCESS BY INDEX ROWID  | EMP       |      1 |      1 |00:00:00.01 |       6 |
|   3 |    NESTED LOOPS                |           |      1 |      3 |00:00:00.01 |       5 |
|   4 |     TABLE ACCESS BY INDEX ROWID| DEPT      |      1 |      1 |00:00:00.01 |       3 |
|*  5 |      INDEX RANGE SCAN          | DEPT_IDX1 |      1 |      1 |00:00:00.01 |       2 |
|*  6 |     INDEX RANGE SCAN           | EMP_IX04  |      1 |      1 |00:00:00.01 |       2 |
---------------------------------------------------------------------------------------------


Predicate Information (identified by operation id):
---------------------------------------------------
 
   1 - filter(TO_DATE('19801010')<=TO_DATE('19820101'))
   5 - access("B"."LOC"='CHICAGO')
   6 - access("A"."JOB"='CLERK' AND "A"."DEPTNO"="B"."DEPTNO" AND "A"."HIREDATE">='19801010'
              AND "A"."HIREDATE"<='19820101')

서브쿼리가 조인으로 바뀌었고 정상적으로  Access sub Query plan 과 같이
(JOB, DEPTNO, HIREDATE) 인덱스를 사용하였다.
위와 같은 상황에서는 Unnesting 을 적극 추천한다.
혹자는 "_unnest_subquery 파라미터 등을 세션단위에서 수정하여 제공자 서브쿼리로 만들면 되지않냐?"
고 말한다.
맞는 말이지만 연구소에서나 사용하여야 한다.
Production 시스템에서는 위와같이 히든 파라미터를 수정하는 것은 아주 위험하다.
현재는 버젼이 10g 이지만 Oracle을 Upgrade를 할때는 아마도 Oracle 버젼 13 혹은 14버젼을
사용할것이다.
그때에도 위와 같은 히든 파라미터가 있다고 보장할수 없다.
Deprecate 된 힌트도 마찬가지 일것이다.(대부분 3~4 버젼이 지나면 힌트가 없어짐)
물론 미래(Oralce Upgrade 시)에 모든 AS-IS 의 소스코드를 모조리 체크해서 새로운 힌트및 파라미터로 바꿀 각오가 되어 있다면 가능하다.

결론 :
마지막 예제와 같이 서브쿼리의 집합이 PK 컬럼이나 Unique 인덱스의 컬럼을 모두 RETURN 하는 경우는
Unnesting 될때 UNQUE 가 보장되므로 별도의 SORT UNIQUE 작업이나 HASH UNIQUE 작업이 일어나지 않는다.
따라서 서브쿼리의 결과 건수가 작고 별도의 Unique 작업이 없을경우 거의 Access sub Query 와
같은 성능을 보장하므로 굳이 Access sub Query plan (제공자 plan) 을 고집할 필요가  없다는 점을
기억 해야 한다.
서브쿼리 Unnesting 은 Using Sub query Method (Sub query Flattening ) 글을 참고하기 바란다.

Posted by extremedb
,