Oracle/Tuning2019. 8. 28. 02:44

이번 글에서는 고유 값에 대한 상관 서브 쿼리의 성능 개선 방안을 살펴보자.


테스트를 위해 아래와 같이 테이블을 생성하자.

-- 1
DROP TABLE t1 PURGE;
DROP TABLE t2 PURGE;

CREATE TABLE t1 AS SELECT ROWNUM AS c1, LPAD ('X', 100, 'X') AS c2 FROM XMLTABLE ('1 to 10000');
CREATE TABLE t2 AS SELECT c1 + 1 AS c1, c2 FROM t1;

CREATE UNIQUE INDEX t1_u1 ON t1 (c1);
CREATE INDEX t2_x1 ON t2 (c1);

아래는 전형적인 상관 서브 쿼리다. t1 테이블을 랜덤 액세스하는 과정(2번 오퍼레이션)에서 155개의 블록 I/O가 발생했다.

-- 2
SELECT /*+ INDEX(A) */
       a.*
  FROM t1 a
 WHERE a.c1 > 0
   AND NOT EXISTS (SELECT /*+ UNNEST NL_AJ */
                          1
                     FROM t2 x
                    WHERE x.c1 = a.c1);

--------------------------------------------------------------------------
| Id  | Operation                    | Name  | Starts | A-Rows | Buffers |
--------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |       |      1 |      1 |     305 |
|   1 |  NESTED LOOPS ANTI           |       |      1 |      1 |     305 |
|   2 |   TABLE ACCESS BY INDEX ROWID| T1    |      1 |  10000 |     177 | -- !
|*  3 |    INDEX RANGE SCAN          | T1_U1 |      1 |  10000 |      22 |
|*  4 |   INDEX RANGE SCAN           | T2_X1 |  10000 |   9999 |     128 |
--------------------------------------------------------------------------

랜덤 액세스를 줄이기 위해 t1 테이블 셀프 조인하는 기법을 사용할 수 있다. 2번 쿼리에 비해 블록 I/O가 감소한 것을 확인할 수 있다.

-- 3
SELECT /*+ INDEX(A) ROWID(B) NO_ELIMINATE_JOIN(A) NO_ELIMINATE_JOIN(B) */
       b.*
  FROM t1 a, t1 b
 WHERE a.c1 > 0
   AND NOT EXISTS (SELECT /*+ UNNEST NL_AJ */
                          1
                     FROM t2 x
                    WHERE x.c1 = a.c1)
   AND b.ROWID = a.ROWID;

-------------------------------------------------------------------------
| Id  | Operation                   | Name  | Starts | A-Rows | Buffers |
-------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |       |      1 |      1 |     151 |
|   1 |  NESTED LOOPS               |       |      1 |      1 |     151 |
|   2 |   NESTED LOOPS ANTI         |       |      1 |      1 |     150 |
|*  3 |    INDEX RANGE SCAN         | T1_U1 |      1 |  10000 |      22 |
|*  4 |    INDEX RANGE SCAN         | T2_X1 |  10000 |   9999 |     128 |
|   5 |   TABLE ACCESS BY USER ROWID| T1    |      1 |      1 |       1 | -- !
-------------------------------------------------------------------------

t1 테이블의 c1 열이 고유하므로 아래처럼 MINUS 연산자를 사용할 수도 있다. 블록 I/O가 46개로 감소했지만, 소트 과정에서 848K의 PGA를 사용했다. 테이블 크기가 작고, 결과 건수가 적을 경우에 사용할 수 있는 방식이다. 

-- 4
SELECT /*+ LEADING(A) USE_NL(B) */
       b.*
  FROM (SELECT c1 FROM t1 WHERE c1 > 0
        MINUS
        SELECT c1 FROM t2 WHERE c1 > 0) a
     , t1 b
 WHERE b.c1 = a.c1;

-------------------------------------------------------------------------------------
| Id  | Operation                    | Name  | Starts | A-Rows | Buffers | Used-Mem |
-------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |       |      1 |      1 |      46 |          |
|   1 |  NESTED LOOPS                |       |      1 |      1 |      46 |          |
|   2 |   NESTED LOOPS               |       |      1 |      1 |      45 |          |
|   3 |    VIEW                      |       |      1 |      1 |      43 |          |
|   4 |     MINUS                    |       |      1 |      1 |      43 |          |
|   5 |      SORT UNIQUE             |       |      1 |  10000 |      21 |  424K (0)| -- !
|*  6 |       INDEX RANGE SCAN       | T1_U1 |      1 |  10000 |      21 |          |
|   7 |      SORT UNIQUE             |       |      1 |  10000 |      22 |  424K (0)| -- !
|*  8 |       INDEX RANGE SCAN       | T2_X1 |      1 |  10000 |      22 |          |
|*  9 |    INDEX UNIQUE SCAN         | T1_U1 |      1 |      1 |       2 |          |
|  10 |   TABLE ACCESS BY INDEX ROWID| T1    |      1 |      1 |       1 |          |
-------------------------------------------------------------------------------------

아래 쿼리의 MINUS 연산자는 SORT UNIQUE NOSORT 오퍼레이션으로 동작했다. 인덱스를 사용했기 때문이다. 직전 쿼리의 SORT UNIQUE 오퍼레이션은 최적화되지 못한 동작으로 판단된다.

-- 5
SELECT c1 FROM t1 WHERE c1 > 0
MINUS
SELECT c1 FROM t2 WHERE c1 > 0;

-----------------------------------------------------------------
| Id  | Operation           | Name  | Starts | A-Rows | Buffers |
-----------------------------------------------------------------
|   0 | SELECT STATEMENT    |       |      1 |      1 |      45 |
|   1 |  MINUS              |       |      1 |      1 |      45 |
|   2 |   SORT UNIQUE NOSORT|       |      1 |  10000 |      22 | -- !
|*  3 |    INDEX RANGE SCAN | T1_U1 |      1 |  10000 |      22 |
|   4 |   SORT UNIQUE NOSORT|       |      1 |  10000 |      23 | -- !
|*  5 |    INDEX RANGE SCAN | T2_X1 |      1 |  10000 |      23 |
-----------------------------------------------------------------

소트를 제거할 수 없으므로 인덱스 스캔 범위가 넓은 경우라면 INDEX_FFS 힌트를 고려할 수 있다. 블록 I/O는 다소 증가하지만 Multiblock I/O를 통해 수행 시간을 단축할 수 있다.

-- 6
SELECT /*+ LEADING(A) USE_NL(B) */
       b.*
  FROM (SELECT /*+ INDEX_FFS(T1) */ c1 FROM t1 WHERE c1 > 0
        MINUS
        SELECT /*+ INDEX_FFS(T2) */ c1 FROM t2 WHERE c1 > 0) a
     , t1 b
 WHERE b.c1 = a.c1;

------------------------------------------------------------------------------------
| Id  | Operation                    | Name  | Starts | A-Rows | Buffers |Used-Mem |
------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |       |      1 |      1 |      56 |         |
|   1 |  NESTED LOOPS                |       |      1 |      1 |      56 |         |
|   2 |   NESTED LOOPS               |       |      1 |      1 |      55 |         |
|   3 |    VIEW                      |       |      1 |      1 |      53 |         |
|   4 |     MINUS                    |       |      1 |      1 |      53 |         |
|   5 |      SORT UNIQUE             |       |      1 |  10000 |      26 | 424K (0)|
|*  6 |       INDEX FAST FULL SCAN   | T1_U1 |      1 |  10000 |      26 |         | -- !
|   7 |      SORT UNIQUE             |       |      1 |  10000 |      27 | 424K (0)|
|*  8 |       INDEX FAST FULL SCAN   | T2_X1 |      1 |  10000 |      27 |         | -- !
|*  9 |    INDEX UNIQUE SCAN         | T1_U1 |      1 |      1 |       2 |         |
|  10 |   TABLE ACCESS BY INDEX ROWID| T1    |      1 |      1 |       1 |         |
------------------------------------------------------------------------------------


'Oracle > Tuning' 카테고리의 다른 글

UPDATE 문 성능 개선 사례  (0) 2019.09.03
NO FPD 뷰 성능 개선 방안  (0) 2019.09.02
Plan Hash Value  (0) 2019.08.26
MERGE JOIN CARTESIAN  (0) 2019.08.23
날짜 조회 안티 패턴 #3  (0) 2019.08.16
Posted by 정희락_