Oracle/Tuning2020. 2. 12. 14:47

JPPD에 의해 조인 순서가 이상 동작하는 사례를 살펴보자.


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

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

CREATE TABLE t1 AS SELECT ROWNUM AS c1, ROWNUM AS c2 FROM XMLTABLE ('1 to 10000');
CREATE TABLE t2 AS SELECT * FROM t1;
CREATE TABLE t3 AS SELECT * FROM t1;
CREATE TABLE t4 AS SELECT * FROM t1;

CREATE OR REPLACE VIEW v2 AS
SELECT * FROM t2 UNION ALL
SELECT * FROM t3;

CREATE INDEX t1_x1 ON t1 (c2);
CREATE INDEX t2_x1 ON t2 (c1);
CREATE INDEX t3_x1 ON t3 (c1);
CREATE INDEX t4_x1 ON t4 (c1);

아래 쿼리는 JPPD가 정상적으로 동작한다.

-- 2
SELECT *
  FROM t1 a
     , v2 b
 WHERE a.c2 = 1
   AND b.c1 = a.c1;

--------------------------------------------------------
| Id  | Operation                              | Name  |
--------------------------------------------------------
|   0 | SELECT STATEMENT                       |       |
|   1 |  NESTED LOOPS                          |       |
|   2 |   TABLE ACCESS BY INDEX ROWID BATCHED  | T1    |
|*  3 |    INDEX RANGE SCAN                    | T1_X1 |
|   4 |   VIEW                                 | V2    |
|   5 |    UNION ALL PUSHED PREDICATE          |       | -- !
|   6 |     TABLE ACCESS BY INDEX ROWID BATCHED| T2    |
|*  7 |      INDEX RANGE SCAN                  | T2_X1 |
|   8 |     TABLE ACCESS BY INDEX ROWID BATCHED| T3    |
|*  9 |      INDEX RANGE SCAN                  | T3_X1 |
--------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   3 - access("A"."C2"=1)
   7 - access("C1"="A"."C1")
   9 - access("C1"="A"."C1")

아래 쿼리는 JPPD가 수행된 뷰(v2)의 열을 t3 테이블에 조인 조건(c.c1 = b.c1)으로 사용했다. JPPD가 동작하지 않는 것을 확인할 수 있다.

-- 3
SELECT *
  FROM t1 a
     , v2 b
     , t3 c
 WHERE a.c2 = 1
   AND b.c1 = a.c1
   AND c.c1 = b.c1 -- !
;

--------------------------------------------------------
| Id  | Operation                              | Name  |
--------------------------------------------------------
|   0 | SELECT STATEMENT                       |       |
|   1 |  NESTED LOOPS                          |       |
|   2 |   NESTED LOOPS                         |       |
|*  3 |    HASH JOIN                           |       |
|   4 |     TABLE ACCESS BY INDEX ROWID BATCHED| T1    |
|*  5 |      INDEX RANGE SCAN                  | T1_X1 |
|   6 |     VIEW                               | V2    |
|   7 |      UNION-ALL                         |       | -- !!
|   8 |       TABLE ACCESS FULL                | T2    |
|   9 |       TABLE ACCESS FULL                | T3    |
|* 10 |    INDEX RANGE SCAN                    | T3_X1 |
|  11 |   TABLE ACCESS BY INDEX ROWID          | T3    |
--------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   3 - access("B"."C1"="A"."C1")
   5 - access("A"."C2"=1)
  10 - access("C"."C1"="B"."C1")

PUSH_PRED 힌트를 사용하면 t3 테이블이 먼저 조인되어 카티션 곱이 발생하는 것을 확인할 수 있다. ORDERED 힌트 마저 무시되었다.

-- 4
SELECT /*+ ORDERED USE_NL(B) PUSH_PRED(B) */
       *
  FROM t1 a
     , v2 b
     , t3 c
 WHERE a.c2 = 1
   AND b.c1 = a.c1
   AND c.c1 = b.c1;

---------------------------------------------------------
| Id  | Operation                               | Name  |
---------------------------------------------------------
|   0 | SELECT STATEMENT                        |       |
|   1 |  NESTED LOOPS                           |       |
|   2 |   MERGE JOIN CARTESIAN                  |       |
|   3 |    TABLE ACCESS BY INDEX ROWID BATCHED  | T1    |
|*  4 |     INDEX RANGE SCAN                    | T1_X1 |
|   5 |    BUFFER SORT                          |       |
|   6 |     TABLE ACCESS FULL                   | T3    | -- !!
|   7 |   VIEW                                  | V2    |
|   8 |    UNION ALL PUSHED PREDICATE           |       | -- !
|*  9 |     FILTER                              |       |
|  10 |      TABLE ACCESS BY INDEX ROWID BATCHED| T2    |
|* 11 |       INDEX RANGE SCAN                  | T2_X1 |
|* 12 |     FILTER                              |       |
|  13 |      TABLE ACCESS BY INDEX ROWID BATCHED| T3    |
|* 14 |       INDEX RANGE SCAN                  | T3_X1 |
---------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   4 - access("A"."C2"=1)
   9 - filter("C"."C1"="A"."C1")
  11 - access("C1"="A"."C1")
  12 - filter("C"."C1"="A"."C1")
  14 - access("C1"="A"."C1")

Outline Data
-------------

  /*+
      PUSH_PRED(@"SEL$1" "B"@"SEL$1" 3 2)
  */

_optimizer_push_pred_cost_based 파라미터를 false로 설정하면 이전 방식의 JPPD가 동작하는 것을 확인할 수 있다.[각주:1] 4번 쿼리의 실행 계획은 비용기반 JPPD의 오동작으로 인해 것임을 유추할 수 있다.

-- 5
SELECT /*+ ORDERED USE_NL(B) OPT_PARAM('_OPTIMIZER_PUSH_PRED_COST_BASED', 'FALSE')  */
       *
  FROM t1 a
     , v2 b
     , t3 c
 WHERE a.c2 = 1
   AND b.c1 = a.c1
   AND c.c1 = b.c1;

----------------------------------------------------------
| Id  | Operation                                | Name  |
----------------------------------------------------------
|   0 | SELECT STATEMENT                         |       |
|   1 |  NESTED LOOPS                            |       |
|   2 |   NESTED LOOPS                           |       |
|   3 |    NESTED LOOPS                          |       |
|   4 |     TABLE ACCESS BY INDEX ROWID BATCHED  | T1    |
|*  5 |      INDEX RANGE SCAN                    | T1_X1 |
|   6 |     VIEW                                 | V2    |
|   7 |      UNION-ALL PARTITION                 |       | -- !
|   8 |       TABLE ACCESS BY INDEX ROWID BATCHED| T2    |
|*  9 |        INDEX RANGE SCAN                  | T2_X1 |
|  10 |       TABLE ACCESS BY INDEX ROWID BATCHED| T3    |
|* 11 |        INDEX RANGE SCAN                  | T3_X1 |
|* 12 |    INDEX RANGE SCAN                      | T3_X1 |
|  13 |   TABLE ACCESS BY INDEX ROWID            | T3    |
----------------------------------------------------------

Outline Data
-------------

  /*+
      OLD_PUSH_PRED(@"SEL$1" "B"@"SEL$1" ("T3"."C1"))
      OPT_PARAM('_optimizer_push_pred_cost_based' 'false')
  */

Predicate Information (identified by operation id):
---------------------------------------------------

   5 - access("A"."C2"=1)
   9 - access("C1"="A"."C1")
  11 - access("C1"="A"."C1")
  12 - access("C"."C1"="B"."C1")

쿼리가 단순하다면 인라인 뷰를 통해 JPPD를 제어할 수 있다.

-- 6
SELECT *
  FROM (SELECT /*+ NO_MERGE */
               a.c1 AS ac1, a.c2 AS ac2, b.c1 AS bc1, b.c2 AS bc2
          FROM t1 a, v2 b
         WHERE a.c2 = 1
           AND b.c1 = a.c1) a
     , t3 c
 WHERE c.c1 = a.bc1;

-----------------------------------------------------------
| Id  | Operation                                 | Name  |
-----------------------------------------------------------
|   0 | SELECT STATEMENT                          |       |
|   1 |  NESTED LOOPS                             |       |
|   2 |   NESTED LOOPS                            |       |
|   3 |    VIEW                                   |       |
|   4 |     NESTED LOOPS                          |       |
|   5 |      TABLE ACCESS BY INDEX ROWID BATCHED  | T1    |
|*  6 |       INDEX RANGE SCAN                    | T1_X1 |
|   7 |      VIEW                                 | V2    |
|   8 |       UNION ALL PUSHED PREDICATE          |       | -- !
|   9 |        TABLE ACCESS BY INDEX ROWID BATCHED| T2    |
|* 10 |         INDEX RANGE SCAN                  | T2_X1 |
|  11 |        TABLE ACCESS BY INDEX ROWID BATCHED| T3    |
|* 12 |         INDEX RANGE SCAN                  | T3_X1 |
|* 13 |    INDEX RANGE SCAN                       | T3_X1 |
|  14 |   TABLE ACCESS BY INDEX ROWID             | T3    |
-----------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   6 - access("A"."C2"=1)
  10 - access("C1"="A"."C1")
  12 - access("C1"="A"."C1")
  13 - access("C"."C1"="A"."BC1")

아래 쿼리는 t3 테이블을 아우터 조인하고 NO_OUTER_JOIN_TO_INNER 힌트를 통해 아우터 조인 후 필터링하는 방식을 사용했다. JPPD가 정상적으로 동작하는 것을 확인할 수 있다.

-- 7
SELECT /*+ NO_OUTER_JOIN_TO_INNER */
       *
  FROM t1 a
     , v2 b
     , t3 c
 WHERE a.c2 = 1
   AND b.c1 = a.c1
   AND c.c1(+) = b.c1   -- !
   AND c.c1 IS NOT NULL -- !
;

----------------------------------------------------------
| Id  | Operation                                | Name  |
----------------------------------------------------------
|   0 | SELECT STATEMENT                         |       |
|*  1 |  FILTER                                  |       |
|   2 |   NESTED LOOPS OUTER                     |       |
|   3 |    NESTED LOOPS                          |       |
|   4 |     TABLE ACCESS BY INDEX ROWID BATCHED  | T1    |
|*  5 |      INDEX RANGE SCAN                    | T1_X1 |
|   6 |     VIEW                                 | V2    |
|   7 |      UNION ALL PUSHED PREDICATE          |       | -- !
|   8 |       TABLE ACCESS BY INDEX ROWID BATCHED| T2    |
|*  9 |        INDEX RANGE SCAN                  | T2_X1 |
|  10 |       TABLE ACCESS BY INDEX ROWID BATCHED| T3    |
|* 11 |        INDEX RANGE SCAN                  | T3_X1 |
|  12 |    TABLE ACCESS BY INDEX ROWID BATCHED   | T3    |
|* 13 |     INDEX RANGE SCAN                     | T3_X1 |
----------------------------------------------------------

Outline Data
-------------

  /*+
      PUSH_PRED(@"SEL$1" "B"@"SEL$1" 2)
  */

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - filter("C"."C1" IS NOT NULL)
   5 - access("A"."C2"=1)
   9 - access("C1"="A"."C1")
  11 - access("C1"="A"."C1")
  13 - access("C"."C1"(+)="B"."C1")

4번, 7번 쿼리의 Outline Data를 참조하면 아래의 힌트를 통해 JPPD를 유도할 수도 있다. 쿼리가 복잡한 경우 제대로 동작하지 않을 수 있으므로 5번 쿼리 방식을 사용하는 편이 쉽다.

-- 8
SELECT /*+ PUSH_PRED(B 2) NO_PUSH_PRED(B 3) */
       *
  FROM t1 a
     , v2 b
     , t3 c
 WHERE a.c2 = 1
   AND b.c1 = a.c1
   AND c.c1 = b.c1;

----------------------------------------------------------
| Id  | Operation                                | Name  |
----------------------------------------------------------
|   0 | SELECT STATEMENT                         |       |
|   1 |  NESTED LOOPS                            |       |
|   2 |   NESTED LOOPS                           |       |
|   3 |    NESTED LOOPS                          |       |
|   4 |     TABLE ACCESS BY INDEX ROWID          | T1    |
|*  5 |      INDEX RANGE SCAN                    | T1_X1 |
|   6 |     VIEW                                 | V2    |
|   7 |      UNION ALL PUSHED PREDICATE          |       | -- !
|   8 |       TABLE ACCESS BY INDEX ROWID BATCHED| T2    |
|*  9 |        INDEX RANGE SCAN                  | T2_X1 |
|  10 |       TABLE ACCESS BY INDEX ROWID BATCHED| T3    |
|* 11 |        INDEX RANGE SCAN                  | T3_X1 |
|* 12 |    INDEX RANGE SCAN                      | T3_X1 |
|  13 |   TABLE ACCESS BY INDEX ROWID            | T3    |
----------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   5 - access("A"."C2"=1)
   9 - access("C1"="A"."C1")
  11 - access("C1"="A"."C1")
  12 - access("C"."C1"="B"."C1")

조인 순서를 변경할 수 있다면 손쉽게 문제를 해결할 수 있다.

-- 9
SELECT *
  FROM t1 a
     , v2 b
     , t3 c
 WHERE a.c2 = 1
   AND b.c1 = a.c1
   AND c.c1 = a.c1 -- !
;

--------------------------------------------------------
| Id  | Operation                              | Name  |
--------------------------------------------------------
|   0 | SELECT STATEMENT                       |       |
|   1 |  NESTED LOOPS                          |       |
|   2 |   NESTED LOOPS                         |       |
|   3 |    TABLE ACCESS BY INDEX ROWID BATCHED | T1    |
|*  4 |     INDEX RANGE SCAN                   | T1_X1 |
|   5 |    TABLE ACCESS BY INDEX ROWID BATCHED | T3    |
|*  6 |     INDEX RANGE SCAN                   | T3_X1 |
|   7 |   VIEW                                 | V2    |
|   8 |    UNION ALL PUSHED PREDICATE          |       | -- !
|   9 |     TABLE ACCESS BY INDEX ROWID BATCHED| T2    |
|* 10 |      INDEX RANGE SCAN                  | T2_X1 |
|  11 |     TABLE ACCESS BY INDEX ROWID BATCHED| T3    |
|* 12 |      INDEX RANGE SCAN                  | T3_X1 |
--------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   4 - access("A"."C2"=1)
   6 - access("C"."C1"="A"."C1")
  10 - access("C1"="A"."C1")
  12 - access("C1"="A"."C1")


  1. _push_join_union_view 파라미터를 false로 설정해도 이전 방식의 JPPD가 동작한다. 단, 파라미터가 true로 설정된 상태로 OLD_PUSH_PRED 힌트를 사용하면 JPPD가 동작하지 않는다. [본문으로]
Posted by 정희락_