출처 :  URL : http://www.gurubee.net/lecture/2148





튜닝 전

  이번 예제 또한 Query Block Name을 이용한 튜닝방법으로, 서브쿼리에 있는 테이블을 원하는 액세스 순서에 위치시키는 내용이다.

  서브쿼리 또한 위에서 살펴본 내용처럼 Pointer가 없기 때문에 기존에 사용하는 힌트(PUSH_SUBQ)로는 제어가 불가능 하였지만 Query Block Name을 사용할 경우에는 이 또한 가능하다.

  이 튜닝방법의 핵심은 서브쿼리가 주요 필터링 조건임에도 불구하고 원하는 액세스 순서에 위치 시킬 수 없어 여러 SQL 변형을 통해야 했지만 단 한번의 글로벌 힌트로 제어가 가능하다는 점이다.

 
ALTER SESSION SET STATISTICS_LEVEL = ALL ;
SET SERVEROUTPUT OFF;
SET LINESIZE 2000;

VAR B1 VARCHAR2(100);
VAR B2 VARCHAR2(100);
VAR B3 NUMBER;

EXEC :B1 := '00010101';
EXEC :B2 := '00010102';
EXEC :B3 := 10;

SELECT *
FROM   (SELECT E_1.EMPNO,
               E_1.JOB,
               E_1.HIREDATE,
               D.DEPTNO
        FROM   EMP  E_1,
               DEPT D
        WHERE  E_1.DEPTNO = D.DEPTNO
        AND    EXISTS (SELECT 1
                       FROM   EMP E_2
                       WHERE  E_2.DEPTNO = E_1.DEPTNO
                       AND    E_2.HIREDATE BETWEEN TO_DATE(:B1, 'YYYYMMDD')  
                                           AND     TO_DATE(:B2, 'YYYYMMDD') 
                       )
       ) EMP_V
WHERE  EMP_V.DEPTNO = :B3;

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

----------------------------------------------------------------------------------------------------
| Id  | Operation                      | Name    | Starts | A-Rows |   A-Time   | Buffers | Reads  |
----------------------------------------------------------------------------------------------------
|*  1 |  FILTER                        |         |      1 |      9 |00:00:08.74 |   44247 |  43940 |
|   2 |   MERGE JOIN SEMI              |         |      1 |      9 |00:00:08.74 |   44247 |  43940 |
|   3 |    NESTED LOOPS                |         |      1 |      9 |00:00:08.72 |   44243 |  43938 |
|*  4 |     INDEX UNIQUE SCAN          | DEPT_U1 |      1 |      1 |00:00:00.01 |       1 |      0 |
|*  5 |     TABLE ACCESS FULL          | EMP     |      1 |      9 |00:00:08.72 |   44242 |  43938 |
|*  6 |    SORT UNIQUE                 |         |      9 |      9 |00:00:00.02 |       4 |      2 |
|*  7 |     TABLE ACCESS BY INDEX ROWID| EMP     |      1 |      9 |00:00:00.02 |       4 |      2 |
|*  8 |      INDEX RANGE SCAN          | EMP_N2  |      1 |     20 |00:00:00.02 |       3 |      2 |
----------------------------------------------------------------------------------------------------

Query Block Name / Object Alias (identified by operation id):
-------------------------------------------------------------
   1 - SEL$1F949E82
   4 - SEL$1F949E82 / D@SEL$2
   5 - SEL$1F949E82 / E_1@SEL$2
   7 - SEL$1F949E82 / E_2@SEL$3
   8 - SEL$1F949E82 / E_2@SEL$3

Outline Data 
-------------
  /*+
      INDEX(@"SEL$1F949E82" "D"@"SEL$2" ("DEPT"."DEPTNO"))
      FULL(@"SEL$1F949E82" "E_1"@"SEL$2")
      INDEX_RS_ASC(@"SEL$1F949E82" "E_2"@"SEL$3" ("EMP"."HIREDATE"))
      LEADING(@"SEL$1F949E82" "D"@"SEL$2" "E_1"@"SEL$2" "E_2"@"SEL$3")
      USE_NL(@"SEL$1F949E82" "E_1"@"SEL$2")
      USE_MERGE(@"SEL$1F949E82" "E_2"@"SEL$3")
      END_OUTLINE_DATA
  */
    

  위의 예제는 EMP_V 인라인뷰에서 E_1와 D가 조인이 되어 있고, 서브쿼리로 E_2가 E_1과 연결이 되어 있다.

  또한 액세스 순서는 Outline Data(LEADING(@"SEL$1F949E82" "D"@"SEL$2" "E_1"@"SEL$2" "E_2"@"SEL$3"))를 통해 D -> E_1 -> E_2 순서로 진행되는 것을 알 수 있다.

  우리는 이 액세스 순서를 E_2(서브쿼리)가 마지막에서 2번째로 위치시키고자 하며, E_1은 맨 마지막에 수행되도록 하고자 한다.

1차 튜닝, Query Block Name을 이용하여 액세스 순서 조정

  방법은 간단하다. Query Block Name을 이용하여 D -> E_2 -> E_1이 되도록 LEADING 힌트를 사용하면 된다.

 
SELECT /*+ LEADING(@SEL$1F949E82 D@SEL$2 E_2@SEL$3 E_1@SEL$2) */
       *
FROM   (SELECT E_1.EMPNO,
               E_1.JOB,
               E_1.HIREDATE,
               D.DEPTNO
        FROM   EMP  E_1,
               DEPT D
        WHERE  E_1.DEPTNO = D.DEPTNO
        AND    EXISTS (SELECT 1
                       FROM   EMP E_2
                       WHERE  E_2.DEPTNO = E_1.DEPTNO
                       AND    E_2.HIREDATE BETWEEN TO_DATE(:B1, 'YYYYMMDD')  
                                           AND     TO_DATE(:B2, 'YYYYMMDD') 
                       )
       ) EMP_V
WHERE  EMP_V.DEPTNO = :B3;

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

--------------------------------------------------------------------------------------------------------------
| Id  | Operation                       | Name    | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads  |
--------------------------------------------------------------------------------------------------------------
|*  1 |  FILTER                         |         |      1 |        |      9 |00:00:06.92 |   44247 |  43959 |
|*  2 |   HASH JOIN                     |         |      1 |   3899M|      9 |00:00:06.92 |   44247 |  43959 |
|   3 |    NESTED LOOPS                 |         |      1 |   6245 |      1 |00:00:00.05 |       5 |      3 |
|*  4 |     INDEX UNIQUE SCAN           | DEPT_U1 |      1 |      1 |      1 |00:00:00.01 |       1 |      0 |
|   5 |     SORT UNIQUE                 |         |      1 |   6245 |      1 |00:00:00.05 |       4 |      3 |
|*  6 |      TABLE ACCESS BY INDEX ROWID| EMP     |      1 |   6245 |      9 |00:00:00.05 |       4 |      3 |
|*  7 |       INDEX RANGE SCAN          | EMP_N2  |      1 |  44962 |     20 |00:00:00.05 |       3 |      3 |
|*  8 |    TABLE ACCESS FULL            | EMP     |      1 |   2497K|      9 |00:00:06.87 |   44242 |  43956 |
--------------------------------------------------------------------------------------------------------------
    

  위의 결과처럼 액세스 순서가 D -> E_2 -> E_1이 된 것을 확인할 수 있다.

  하지만 E_1을 액세스 할 때 'TABLE ACCESS FULL'이 일어나 여전히 Buffers를 44242만큼 읽고 있어 비효율이므로 이 부분을 인덱스를 사용하도록 힌트를 추가해보자.

2차 튜닝, Query Block Name을 이용하여 인덱스 유도

  방법은 E_1의 Query Block Name을 이용하여 INDEX 힌트를 사용하면 된다.

 
SELECT /*+ LEADING(@SEL$1F949E82 D@SEL$2 E_2@SEL$3 E_1@SEL$2) USE_NL(@SEL$1F949E82 E_1@SEL$2) */
       *
FROM   (SELECT E_1.EMPNO,
               E_1.JOB,
               E_1.HIREDATE,
               D.DEPTNO
        FROM   EMP  E_1,
               DEPT D
        WHERE  E_1.DEPTNO = D.DEPTNO
        AND    EXISTS (SELECT 1
                       FROM   EMP E_2
                       WHERE  E_2.DEPTNO = E_1.DEPTNO
                       AND    E_2.HIREDATE BETWEEN TO_DATE(:B1, 'YYYYMMDD')  
                                           AND     TO_DATE(:B2, 'YYYYMMDD') 
                       )
       ) EMP_V
WHERE  EMP_V.DEPTNO = :B3;

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

---------------------------------------------------------------------------------------------------------------
| Id  | Operation                        | Name    | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads  |
---------------------------------------------------------------------------------------------------------------
|*  1 |  FILTER                          |         |      1 |        |      9 |00:00:00.01 |      11 |      3 |
|   2 |   TABLE ACCESS BY INDEX ROWID    | EMP     |      1 |    624K|      9 |00:00:00.01 |      11 |      3 |
|   3 |    NESTED LOOPS                  |         |      1 |   3899M|     11 |00:00:00.01 |       9 |      3 |
|   4 |     NESTED LOOPS                 |         |      1 |   6245 |      1 |00:00:00.01 |       5 |      0 |
|*  5 |      INDEX UNIQUE SCAN           | DEPT_U1 |      1 |      1 |      1 |00:00:00.01 |       1 |      0 |
|   6 |      SORT UNIQUE                 |         |      1 |   6245 |      1 |00:00:00.01 |       4 |      0 |
|*  7 |       TABLE ACCESS BY INDEX ROWID| EMP     |      1 |   6245 |      9 |00:00:00.01 |       4 |      0 |
|*  8 |        INDEX RANGE SCAN          | EMP_N2  |      1 |  44962 |     20 |00:00:00.01 |       3 |      0 |
|*  9 |     INDEX RANGE SCAN             | EMP_N1  |      1 |    624K|      9 |00:00:00.01 |       4 |      3 |
---------------------------------------------------------------------------------------------------------------
    

  이제 원하는 실행계획이 완성되었으며, 수행시간은 기존 8.7초에서 0.01초로 줄어들었고 Buffers 또한 44247에서 11로 줄어든것을 알 수 있다.

  이렇게 2가지 Outline Date를 이용하여 튜닝하는 Case를 보았는데, 다시한번 강조하지만 가급적 이 방법은 사용을 지양하고 기존 힌트 사용방법으로 하는 것이 좋으며, 빠른 시간내에 대응을 해야할 경우 사용하는 것이 좋다.

- 강좌 URL : http://www.gurubee.net/lecture/2147


단순하지만 여러가지 복합적인 내용이 숨어 있음. 

- Predicate Information - Filter

옵티마이저가 Internal Query Block Name - QB_NAME




튜닝 전

  이번 예제는 OR조건이 사용될 경우 조건절 컬럼이 인덱스가 있음에도 불구하고 FULL SCAN 하는 것을 인덱스를 사용하도록 유도하는 예제이다.

  아래의 내용에서 보는 것처럼 EMP 테이블에 EMPNO와 HIREDATE 컬럼으로 구성된 인덱스가 있음에도 불구하고, FULL SCAN을 하여 A-Time에 8.84초가 걸린것을 확인할 수 있다.

 
ALTER SESSION SET STATISTICS_LEVEL = ALL ;
SET SERVEROUTPUT OFF;
SET LINESIZE 2000;

VAR B1 NUMBER;
VAR B2 VARCHAR2(100);
VAR B3 VARCHAR2(100);

EXEC :B1 := 10;
EXEC :B2 := '20090401';
EXEC :B3 := '20090402';

SELECT *
FROM   EMP E
WHERE  (E.DEPTNO = :B1  
        OR 
        E.HIREDATE BETWEEN TO_DATE(:B2, 'YYYYMMDD')  
                   AND     TO_DATE(:B3, 'YYYYMMDD')  
       );

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

------------------------------------------------------------------------------------
| Id  | Operation         | Name | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
------------------------------------------------------------------------------------
|*  1 |  TABLE ACCESS FULL| EMP  |      1 |   2021K|     29 |00:00:08.84 |   44242 |
------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   1 - filter(("E"."DEPTNO"=:B1 OR ("E"."HIREDATE">=TO_DATE(:B2,'YYYYMMDD')
              AND "E"."HIREDATE"<=TO_DATE(:B3,'YYYYMMDD'))))
    

1차 튜닝, UNION ALL 분리

  이를 튜닝하는 1차 방법은 각 조건절이 독립적으로 사용될 수 있도록 SQL을 UNION ALL을 사용하여 분리하는 것이다.

  OR 조건의 특징은 'A OR B'일 경우 A가 True이면 B를 수행하지 않고 A가 False일 경우 B를 수행한다. 그러므로 UNION ALL로 분리할 때 상단은 True가 되게 하고 하단은 False가 되게 구성하면 된다.

  여기서는 DEPTNO 조건을 True와 False로 나누면 되며 UNION ALL 상단에 'DEPTNO = :B1', 하단에 'DEPTNO :B1'를 위치시켜 분리를 하였다.

 
SElECT *
FROM   EMP E
WHERE  E.DEPTNO = :B1   
UNION ALL
SElECT *
FROM   EMP E
WHERE  E.DEPTNO  :B1
AND    E.HIREDATE BETWEEN TO_DATE(:B2, 'YYYYMMDD')  
                  AND     TO_DATE(:B3, 'YYYYMMDD') 
;


-----------------------------------------------------------------------------------------------------------
| Id  | Operation                     | Name   | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads  |
-----------------------------------------------------------------------------------------------------------
|   1 |  UNION-ALL                    |        |      1 |        |     29 |00:00:06.14 |   44248 |  43957 |
|*  2 |   TABLE ACCESS FULL           | EMP    |      1 |   1669K|      9 |00:00:06.14 |   44242 |  43957 |
|*  3 |   FILTER                      |        |      1 |        |     20 |00:00:00.01 |       6 |      0 |
|*  4 |    TABLE ACCESS BY INDEX ROWID| EMP    |      1 |  20865 |     20 |00:00:00.01 |       6 |      0 |
|*  5 |     INDEX RANGE SCAN          | EMP_N2 |      1 |  45067 |     20 |00:00:00.01 |       4 |      0 |
-----------------------------------------------------------------------------------------------------------
                                                                                                           
Predicate Information (identified by operation id):                                                        
---------------------------------------------------                                                        
                                                                                                           
   2 - filter("E"."DEPTNO"=:B1)                                                                            
   3 - filter(TO_DATE(:B2,'YYYYMMDD')<=TO_DATE(:B3,'YYYYMMDD'))                                            
   4 - filter("E"."DEPTNO"<>:B1)                                                                           
   5 - access("E"."HIREDATE">=TO_DATE(:B2,'YYYYMMDD') AND "E"."HIREDATE"<=TO_DATE(:B3,'YYYYMMDD'))         
    

1차 튜닝, UNION ALL 분리 - 힌트 사용

  위의 결과에서 보듯이 결과는 29건으로 동일하며 수행시간은 8.84초에서 6.14초로 줄어들었다.

  하지만 E.HIREDATE 조건절을 사용한 UNINO ALL 하단은 EMP_N2 인덱스를 사용하여 0.01초만에 추출되었는데, E.DEPTNO 조건절을 사용한 UNION ALL 상단은 여전히 FULL SCAN을 하고 있다.

  그러므로 이 부분을 해소하기 위해 힌트를 사용해 보도록 하자.

 
SElECT /*+ INDEX(E EMP_N1) */
       *
FROM   EMP E
WHERE  E.DEPTNO = :B1   
UNION ALL
SElECT *
FROM   EMP E
WHERE  E.DEPTNO  :B1
AND    E.HIREDATE BETWEEN TO_DATE(:B2, 'YYYYMMDD')  
                  AND     TO_DATE(:B3, 'YYYYMMDD') 
;

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

-----------------------------------------------------------------------------------------------------------
| Id  | Operation                     | Name   | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads  |
-----------------------------------------------------------------------------------------------------------
|   1 |  UNION-ALL                    |        |      1 |        |     29 |00:00:00.02 |      12 |      5 |
|   2 |   TABLE ACCESS BY INDEX ROWID | EMP    |      1 |   1669K|      9 |00:00:00.02 |       6 |      3 |
|*  3 |    INDEX RANGE SCAN           | EMP_N1 |      1 |   1669K|      9 |00:00:00.02 |       4 |      2 |
|*  4 |   FILTER                      |        |      1 |        |     20 |00:00:00.01 |       6 |      2 |
|*  5 |    TABLE ACCESS BY INDEX ROWID| EMP    |      1 |  20865 |     20 |00:00:00.01 |       6 |      2 |
|*  6 |     INDEX RANGE SCAN          | EMP_N2 |      1 |  45067 |     20 |00:00:00.01 |       4 |      0 |
-----------------------------------------------------------------------------------------------------------
                                                                                                           
Predicate Information (identified by operation id):                                                        
---------------------------------------------------                                                        
                                                                                                           
   3 - access("E"."DEPTNO"=:B1)                                                                            
   4 - filter(TO_DATE(:B2,'YYYYMMDD')<=TO_DATE(:B3,'YYYYMMDD'))                                            
   5 - filter("E"."DEPTNO"<>:B1)                                                                           
   6 - access("E"."HIREDATE">=TO_DATE(:B2,'YYYYMMDD') AND "E"."HIREDATE"<=TO_DATE(:B3,'YYYYMMDD'))         
    

  위의 결과에서 보듯이 UNION ALL 상단에 힌트를 사용한 결과 0.02초로 줄어들었고 전체 시간 또한 감소된 걸 확인할 수 있다.

  이처럼 OR 조건을 사용하여 인덱스가 있음에도 불구하고 FULL SCAN을 하고 있는 SQL들이 있다면 우선 OR 조건으로 분리할 수 있는지 검토하는 것이 좋다.

2차 튜닝, USE_CONCAT 힌트 사용

  이번에는 SQL을 분리하지 않고 힌트만을 사용하여 각각 조건에 맞는 인덱스가 사용되도록 힌트를 사용하는 예제이다.

  1차 튜닝에서는 OR조건을 기준으로 UNION을 사용하여 2개의 SQL 분리하였지만, 이번에는 USE_CONCAT이라는 힌트를 사용하여 실행계획이 2개로 분리하도록 유도하였다.

 
SElECT /*+ USE_CONCAT */  
       *
FROM   EMP E
WHERE  (E.DEPTNO = :B1          
        OR 
        E.HIREDATE BETWEEN TO_DATE(:B2, 'YYYYMMDD')  
                   AND     TO_DATE(:B3, 'YYYYMMDD') 
       );

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

----------------------------------------------------------------------------------------------------------
| Id  | Operation                    | Name   | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads  |
----------------------------------------------------------------------------------------------------------
|   1 |  CONCATENATION               |        |      1 |        |     29 |00:00:01.96 |   44250 |      2 |
|   2 |   TABLE ACCESS BY INDEX ROWID| EMP    |      1 |  25065 |     20 |00:00:00.03 |       9 |      2 |
|*  3 |    INDEX RANGE SCAN          | EMP_N2 |      1 |  45525 |     20 |00:00:00.03 |       5 |      2 |
|*  4 |   TABLE ACCESS FULL          | EMP    |      1 |   2500K|      9 |00:00:07.93 |   44241 |      0 |
----------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------                                                       
   3 - access("E"."HIREDATE">=TO_DATE(:B2,'YYYYMMDD') AND "E"."HIREDATE"<=TO_DATE(:B3,'YYYYMMDD'))
   4 - filter(("E"."DEPTNO"=:B1 AND (LNNVL("E"."HIREDATE">=TO_DATE(:B2,'YYYYMMDD')) OR
              LNNVL("E"."HIREDATE"<=TO_DATE(:B3,'YYYYMMDD')))))
                                         

    

  위에서 보는 것처럼 USE_CONCAT 힌트를 사용하면 실행계획에서 CONCATENATION이라는 구문을 볼 수 있으며, Predicate Information절에서 어떤 조건들이 매칭되는지를 확인할 수 있다.

  Predicate Information절을 기준으로 보았을 때 Id 2~3번절은 HIREDATE 조건에 의한 절이라는 것을 알 수 있고 Id 4번절은 DEPTNO 조건에 의한 것이라는 것을 알 수 있다.

  그런데, 1차 튜닝때와 마찬가지로 옵티마이저가 Id 1번절을 FULL SCAN 하고 있어 이 부분도 인덱스를 사용하도록 유도하고자 한다.

  그런데 인덱스를 사용하도록 유도하려면 1차 튜닝때 처럼 테이블이름 또는 테이블의 Alias처럼 인식할 수 있는 Pointer가 있어야 하는데 여기서는 그런 Pointer를 찾을 수가 없다. 이럴 경우 어떻게 인덱스를 유도할 수 있을까?

  방법은 바로 강좌란 '1.2.4. OUTLINE Format'에서 공부한 내용으로, 옵티마이저가 Internal하게 구성하고 있는 Query Block Name을 이용하여 Pointer를 얻는 방법이다.

2차 튜닝, USE_CONCAT 힌트 사용 - Query Block Name Hint 확인

  Query Block Name이란 옵티마이저가 SQL을 Parsing할 때 각각의 SQL을 구간별로 따로 인식하여 이름을 부여하고 이를 통해 실행계획을 만드는데, 이 때 각각의 구간을 Query Block Name에서 확인할 수 있다.

  Query Block Name은 QB_NAME이라는 힌트를 통해 임의로 부여를 할 수 있으며, 부여하지 않을 경우 옵티마이저가 'SEL$1'과 같은 Internal한 이름을 부여한다.

 
SElECT /*+ USE_CONCAT */  
       *
FROM   EMP E
WHERE  (E.DEPTNO = :B1          
        OR 
        E.HIREDATE BETWEEN TO_DATE(:B2, 'YYYYMMDD')  
                   AND     TO_DATE(:B3, 'YYYYMMDD') 
       );

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

----------------------------------------------------------------------------------------------------------
| Id  | Operation                    | Name   | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads  |
----------------------------------------------------------------------------------------------------------
|   1 |  CONCATENATION               |        |      1 |        |     29 |00:00:05.30 |   44250 |   4174 |
|   2 |   TABLE ACCESS BY INDEX ROWID| EMP    |      1 |  25065 |     20 |00:00:00.01 |       9 |      0 |
|*  3 |    INDEX RANGE SCAN          | EMP_N2 |      1 |  45525 |     20 |00:00:00.01 |       5 |      0 |
|*  4 |   TABLE ACCESS FULL          | EMP    |      1 |   2500K|      9 |00:00:05.30 |   44241 |   4174 |
----------------------------------------------------------------------------------------------------------

Query Block Name / Object Alias (identified by operation id):
-------------------------------------------------------------
   1 - SEL$1
   2 - SEL$1_1 / E@SEL$1
   3 - SEL$1_1 / E@SEL$1
   4 - SEL$1_2 / E@SEL$1_2
   
Outline Data
-------------
  /*+
      BEGIN_OUTLINE_DATA
      IGNORE_OPTIM_EMBEDDED_HINTS
      OPTIMIZER_FEATURES_ENABLE('10.2.0.3')
      OPT_PARAM('_b_tree_bitmap_plans' 'false')
      OPT_PARAM('_fast_full_scan_enabled' 'false')
      OPT_PARAM('_optim_peek_user_binds' 'false')
      ALL_ROWS
      OUTLINE_LEAF(@"SEL$1")
      OUTLINE_LEAF(@"SEL$1_1")
      USE_CONCAT(@"SEL$1" 8)
      OUTLINE_LEAF(@"SEL$1_2")
      OUTLINE(@"SEL$1")
      INDEX_RS_ASC(@"SEL$1_1" "E"@"SEL$1" ("EMP"."HIREDATE"))
      FULL(@"SEL$1_2" "E"@"SEL$1_2")
      END_OUTLINE_DATA
  */ 
    

  위의 내용에서 'Query Block Name / Object Alias'를 보면 실행계획에서 Id-1 ~ 4의 내용에 대해 Query Block Name과 Alias 이름이 어떻게 부여되었는지 알 수 있다.

  즉 이 SQL은 Id-1번절에서 'SEL$1' 이름으로 하나의 집합 구간에 대해 'Query Block Name'을 주었고, 'USE_CONCAT' 힌트를 통해 2개의 실행계획으로 분리되면서 HIREDATE 조건절은 'SEL$1_1' 이름이, 'DEPTNO' 조건절은 'SEL$1_2' 이름이 부여된 걸 알 수 있다 .

  그리고 'Query Block Name' 이외에 각 Object(테이블 또는 View)를 식별할 수 있는 Alias 이름을 'Object Alias'에서 확인할 수 있는데, HIREDATE 조건으로 액세스하는 EMP 테이블 Alias는 'E@SEL$1'으로, DEPTNO 조건으로 액세스하는 EMP 테이블 Alias는 'E@SEL$1_2'인 것을 알 수 있다.

  이처럼 힌트를 사용하기 위한 Pointer를 찾을 수 없을 때 XPLAN의 Outline 정보를 이용하면 Pointer를 찾을 수 있으며 이를 통해 힌트를 사용할 수 있다.

2차 튜닝, USE_CONCAT 힌트 사용 - Query Block Name Hint 사용

  Outline을 통해 힌트를 사용하는 방법은 다음과 같다.

 
1. Object Alias가 있는 경우
    - /*+ INDEX(Alias 인덱스명) */
    - /*+ INDEX(E EMP_N1) */

2. Object Alias가 없는 경우
    - /*+ INDEX(Query_Block_Name Query_Alias 인덱스명) */
    - /*+ INDEX(SEL$1_2 E@SEL$1_2 EMP_N1) */

Outline에서 얻은 Pointer는 반드시 Query_Alias만 사용하면 안되고 Query_Block_Name도 같이 사용해야 함.
    

  여기서 우리가 원하는 것은 DEPTNO 조건에 의해 FULL SCAN하는 것을 인덱스를 사용하도록 유도하기 위해 힌트를 사용하는 것이며, 이를 위해 Pointer 값도 얻었기 때문에 이를 이용하여 힌트를 사용해보자.

 
SElECT /*+ USE_CONCAT 
           INDEX(@SEL$1_2 E@SEL$1_2 EMP_N1) */
       *
FROM   EMP E
WHERE  (E.DEPTNO = :B1         
        OR 
        E.HIREDATE BETWEEN TO_DATE(:B2, 'YYYYMMDD')  
                   AND     TO_DATE(:B3, 'YYYYMMDD') 
       );

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

----------------------------------------------------------------------------------------------------------
| Id  | Operation                    | Name   | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads  |
----------------------------------------------------------------------------------------------------------
|   1 |  CONCATENATION               |        |      1 |        |     29 |00:00:00.01 |      13 |      8 |
|   2 |   TABLE ACCESS BY INDEX ROWID| EMP    |      1 |  24979 |     20 |00:00:00.01 |       9 |      5 |
|*  3 |    INDEX RANGE SCAN          | EMP_N2 |      1 |  44962 |     20 |00:00:00.01 |       5 |      3 |
|*  4 |   TABLE ACCESS BY INDEX ROWID| EMP    |      1 |   2491K|      9 |00:00:00.01 |       4 |      3 |
|*  5 |    INDEX RANGE SCAN          | EMP_N1 |      1 |   2497K|      9 |00:00:00.01 |       3 |      3 |
----------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   3 - access("E"."HIREDATE">=TO_DATE(:B2,'YYYYMMDD') AND "E"."HIREDATE"<=TO_DATE(:B3,'YYYYMMDD'))
   4 - filter((LNNVL("E"."HIREDATE">=TO_DATE(:B2,'YYYYMMDD')) OR
              LNNVL("E"."HIREDATE"<=TO_DATE(:B3,'YYYYMMDD'))))
   5 - access("E"."DEPTNO"=:B1)
    

  드디어 Outline Pointer를 통해 인덱스를 사용하도록 유도하였다.

  이처럼 Alias Pointer가 없을 때에도 Outline을 이용하여 힌트를 사용할 수 있으므로 하나의 튜닝방법론으로 숙지하면 좋을 것으로 보인다.

  다만 이 방법론을 범용적으로 사용하는 것은 지양해야 한다. 그 이유는 Internal하게 부여되는 Query_Block_Name이나 Query_Alias가 Object 또는 SQL이 변경됨에 따라 값들도 변경될 수 있으므로 기존 힌트가 무용지물이 될 수 있다. 그러므로 최후의 보루로 튜닝방법이 없을 때 사용하는 것이 좋으며 가능하다면 QB_NAME 힌트를 사용하여 Query_Alias 값을 직접 부여하여 사용하는 것이 좋다.

ROWNUM이 존재하게 되면 오라클의 특성 상 ROWNUM이 존재하는 집합을 먼저 만들고 인라인 뷰 밖의 조건(DEPTNO 조인키)을 필터시키기 때문에 문제가 된다.

  문제가 되는 이유는 EMP 테이블에 천만건의 데이터가 있는데 이 천만건을 모두 액세스한 뒤 9건밖에 존재하지 않는 'DEPTNO = 10' 데이터를 필터링 시키기 때문이며, 실행계획에서도 이러한 문제를 Id 3번를 보면 access가 아닌 filter로 처리된다는 것을 확인할 수 있다.

 
ALTER SESSION SET STATISTICS_LEVEL = ALL;

SELECT EMP_V.*
FROM   (SELECT ROWNUM,
               EMPNO,
               JOB,
               HIREDATE,
               DEPTNO       
        FROM   EMP) EMP_V,
       DEPT D
WHERE  EMP_V.DEPTNO = D.DEPTNO
AND    D.DEPTNO     = 10;

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

---------------------------------------------------------------------------------------------------
| Id  | Operation            | Name    | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads  |
---------------------------------------------------------------------------------------------------
|   1 |  NESTED LOOPS        |         |      1 |   9991K|      9 |00:00:25.47 |   44243 |   7482 |
|*  2 |   INDEX UNIQUE SCAN  | DEPT_U1 |      1 |      1 |      1 |00:00:00.01 |       1 |      0 |
|*  3 |   VIEW               |         |      1 |   9991K|      9 |00:00:25.47 |   44242 |   7482 |
|   4 |    COUNT             |         |      1 |        |     10M|00:00:10.00 |   44242 |   7482 |
|   5 |     TABLE ACCESS FULL| EMP     |      1 |   9991K|     10M|00:00:10.00 |   44242 |   7482 |
---------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------                                                
   2 - access("D"."DEPTNO"=10)
   3 - filter("EMP_V"."DEPTNO"=10)
    

튜닝 후

  이를 해결하기 위해서는 결국 문제가 되는 ROWNUM을 인라인 뷰 밖으로 가져오면 되며, 실제 그렇게 변경할 경우 아래처럼 조인조건이 인라인 뷰 안으로 침투가 되어 인덱스를 사용하는 것을 확인할 수 있다.

 
SELECT ROWNUM,
       EMP_V.*
FROM   (SELECT EMPNO,
               JOB,
               HIREDATE,
               DEPTNO       
        FROM   EMP) EMP_V,
       DEPT D
WHERE  EMP_V.DEPTNO = D.DEPTNO
AND    D.DEPTNO     = 10;

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

---------------------------------------------------------------------------------------------------
| Id  | Operation                     | Name    | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
---------------------------------------------------------------------------------------------------
|   1 |  COUNT                        |         |      1 |        |      9 |00:00:00.01 |       7 |
|   2 |   NESTED LOOPS                |         |      1 |      1 |      9 |00:00:00.01 |       7 |
|*  3 |    INDEX UNIQUE SCAN          | DEPT_U1 |      1 |      1 |      1 |00:00:00.01 |       1 |
|   4 |    TABLE ACCESS BY INDEX ROWID| EMP     |      1 |      1 |      9 |00:00:00.01 |       6 |
|*  5 |     INDEX RANGE SCAN          | EMP_N1  |      1 |      1 |      9 |00:00:00.01 |       4 |
---------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   3 - access("D"."DEPTNO"=10)                                                                     
   5 - access("DEPTNO"=10) 
    



출처: http://clipper0317.tistory.com/2 [오라클의 세계]




DBMS_XPLAN.DISPLAY_CURSOR 사용방법


아래 객체에 SELECT 권한이 있어야 한다
  • - V$SESSION
  • - V$SQL_PLAN
  • - V$SQL(optional)
  • V$SQL_PLAN_STATISTICS_ALL

 


PLAN_STATISTICS 정보는 다음 조건 중 하나를 만족해야 기록된다.
  • - 'STATISTICS_LEVEL' Parameter 값을 ALL로 변경한 경우
  • - '_ROWSOURCE_EXECUTION_STATISTICS' Parameter 값을 TRUE로 변경한 경우
  • GATHER_PLAN_STATISTICS HINT를 사용할 때, 10g부터 지원

 


WINDOW 환경에서 SQL*PLUS를 사용할 때
  • - 이 경우에 DBMS_XPLAN.DISPLAY_CURSOR 정보를 보려면 다음의 구문 사용해야
  • - SET SERVEROUTPUT OFF;

 

Runtime Execution Plan 확인방법

  DBMS_XPLAN.DISPLAY_CURSOR는 이미 수행된 SQL의 PLAN을 확인할 수 있는 장점이 있다.

  이미 수행된 SQL은 V$SQL을 조회하여 확인할 수 있다

 


1. SQL 실행
 
SQL> SELECT *
     FROM EMP E
     WHERE E.EMPNO = 9999999
     AND E.DEPTNO = 10

no rows selected
    

 

2. V$SQL에서 SQL_ID 확인
 
SQL> SELECT SUBSTR(SQL_TEXT, 1, 30) SQL_TEXT,
            SQL_ID, CHILD_NUMBER
     FROM V$SQL
     WHERE SQL_TEXT LIKE 'SELECT * FROM EMP E%';

SQL_TEXT                        SQL_ID          CHILD_NUMBER
------------------------------  -------------   ------------
SELECT * FROM EMP E WHERE E     ak7vxhj055996   0
    

 

3. DBMS_XPLAN.DISPLAY_CURSOR 조회
 
-- V$SQL에서 조회 한 SQL_ID, CHILD_NUMBER를 입력한다.    
SQL> SELECT * 
     FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('ak7vxhj055996', 0, 'ALLSTATS LAST'));

--------------------------------------------------------------------------------------
| Id  | Operation                   | Name   | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |        |     1 |    26 |     3   (0)| 00:00:01 |
|*  1 |  TABLE ACCESS BY INDEX ROWID| EMP    |     1 |    26 |     3   (0)| 00:00:01 |
|*  2 |   INDEX UNIQUE SCAN         | EMP_U1 |     1 |       |     2   (0)| 00:00:01 |
--------------------------------------------------------------------------------------
                                                                                      
Predicate Information (identified by operation id):                                   
---------------------------------------------------                                   
                                                                                      
   1 - filter("E"."DEPTNO"=10)                                                        
   2 - access("E"."EMPNO"=9999999)                                                    


Note
-----
- Warning: basic plan statistics not available. These are only collected when:
* hint 'gather_plan_statistics' is used for the statement or
* parameter 'statistics_level' is set to 'ALL', at session or system level

[수행한 SQL이 다음의 조건을 충족하지 않았기 때문에 기록이 안됨]

  • - 'STATISTICS_LEVEL' Parameter 값을 ALL로 변경한 경우
  • - '_ROWSOURCE_EXECUTION_STATISTICS' Parameter 값을 TRUE로 변경한 경우
  • - GATHER_PLAN_STATISTICS HINT를 사용할 때, 10g부터 지원

 



4. STATISTICS_LEVEL 파라미터 값 변경 이후 추가 수행
 
-- 세션 레벨의 파라미터 변경    
SQL> ALTER SESSION SET STATISTICS_LEVEL = ALL;

-- 1번 단계의 SQL문장을 다시 실행
SQL> SELECT *
     FROM EMP E
     WHERE E.EMPNO = 9999999
       AND E.DEPTNO = 10;
       
no rows selected

-- 2번 단계의 SQL_ID 확인
SQL> SELECT SUBSTR(SQL_TEXT, 1, 30) SQL_TEXT, 
            SQL_ID, CHILD_NUMBER
     FROM V$SQL
     WHERE SQL_TEXT LIKE 'SELECT * FROM EMP E%';

SQL_TEXT                        SQL_ID          CHILD_NUMBER
------------------------------  -------------   ------------
SELECT * FROM EMP E WHERE E     ak7vxhj055996   0 
SELECT * FROM EMP E WHERE E     ak7vxhj055996   1

-- 1번의 CHILD_NUMBER 추가 확인 됨.
-- 동일한 SQL을 ‘STATISTICS_LEVEL' 값을 변경하여 수행하였기 때문에
   CHILD_NUMBER 값으로 구분
    

 

5. DBMS_XPLAN.DISPLAY_CURSOR 다시 조회
 
SQL> SELECT * 
     FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('ak7vxhj055996', 1, 'ALLSTATS LAST'));

-- 실제 수행한 결과
---------------------------------------------------------------------------------------
|Id  | Operation                  | Name   | E-Rows | A-Rows | A-Time     | Buffers |
---------------------------------------------------------------------------------------
|* 1 | TABLE ACCESS BY INDEX ROWID| EMP    | 1      | 0      |00:00:00.01 | 4       |
|* 2 | INDEX UNIQUE SCAN          | EMP_U1 | 1      | 1      |00:00:00.01 | 3       |
---------------------------------------------------------------------------------------

-- 예측 결과
-------------------------------------------------------
|Id  | Operation                  | Name   | E-Rows |
-------------------------------------------------------
|* 1 | TABLE ACCESS BY INDEX ROWID| EMP    | 1      |
|* 2 | INDEX UNIQUE SCAN          | EMP_U1 | 1      |
-------------------------------------------------------

-- 예측 실행계획과 실제 실행계획 비교
 - A-Rows, A-Time, Buffer 추가 정보 보여줌
 - E : Estimate, A : Actual
   




Range Partition 

① 파티션 테이블 생성

  PARTITION BY RANGE 절에 어떤 column들을 기준으로 하여 Partition을 나눌 것인지 지정을 하고, 각 Partition이 나누어 지는 범위는 VALUES LESS THAN 절에서 지정해 주면 됩니다.

 
SQL> CREATE TABLE sales
        (sales_no NUMBER,
         sale_year INT NOT NULL,
         sale_month INT NOT NULL,
         sale_day INT NOT NULL,
         customer_name  VARCHAR2(30),
         price NUMBER)
       PARTITION BY RANGE (sale_year, sale_month, sale_day)
       (PARTITION sales_q1 VALUES LESS THAN (2005, 01, 01) TABLESPACE ASSM_TBS1,
        PARTITION sales_q2 VALUES LESS THAN (2005, 07, 01) TABLESPACE ASSM_TBS2,
        PARTITION sales_q3 VALUES LESS THAN (2006, 01, 01) TABLESPACE ASSM_TBS3,
        PARTITION sales_q4 VALUES LESS THAN (2006, 07, 01) TABLESPACE ASSM_TBS4 );
    

② 데이터 조작

  아래와 같이 INSERT 문장을 실행 하면 파티션 테이블에서 지정한 범위에 따라서 자동으로 파티션이 지정 됩니다.

 
-- Range 파티션 INSERt 예제
INSERT INTO sales VALUES(1, 2004, 06, 12, 'scott', 2500);
INSERT INTO sales VALUES(2, 2005, 06, 17, 'jones', 4300);
INSERT INTO sales VALUES(3, 2005, 12, 12, 'miller', 1200);
INSERT INTO sales VALUES(4, 2006, 06, 22, 'ford', 5200);
INSERT INTO sales VALUES(5, 2005, 01, 01, 'lion', 2200); 
INSERT INTO sales VALUES(6, 2006, 12, 22, 'tiger', 3300);
COMMIT;

--> 범위 초과로 ORA-14400 에러 발생
INSERT INTO sales VALUES(6, 2006, 12, 22, 'tiger', 3300); 
1행에 오류:
ORA-14400: 삽입된 분할 영역 키와 매핑되는 분할 영역이 없음  


-- 각 파티션 마다 데이터가 INSERT 되었는지는 
-- 직접 SELECT 문으로 확인 해 보세요.
SELECT sales_no FROM sales PARTITION (sales_q1); --> 1
SELECT sales_no FROM sales PARTITION (sales_q2); --> 2, 5
SELECT sales_no FROM sales PARTITION (sales_q3); --> 3
SELECT sales_no FROM sales PARTITION (sales_q4); --> 4
    

③ 파티션 추가

  MAXVALUE partition 이 존재하면 추가가 불가능 합니다.

 
-- sales 파티션 테이블에 새로운 파티션 sales_q5를 추가하는 예제 입니다.
SQL> ALTER TABLE sales
     ADD PARTITION sales_q5 VALUES LESS THAN (MAXVALUE, MAXVALUE, MAXVALUE )
     TABLESPACE ASSM_TBS5;
    

④ 파티션 삭제

  Range, List 파티션만 가능 합니다.

  하나의 파티션은 반드시 남아 있어야 합니다.

  한번의 하나의 파티션만 삭제 가능 합니다. 여러 개의 파티션을 삭제하고자 할 때는 삭제 문장을 여러 번 실행 시켜야 합니다.

 
-- sales 테이블의 sales_q5 파티션을 삭제
SQL> ALTER TABLE sales DROP PARTITION sales_q5;
    

⑤ 파티션 이름 변경

  sales 테이블의 sales_q4 파티션 이름을 sales_four로 변경하는 예제 입니다.

 
SQL> ALTER TABLE sales RENAME PARTITION sales_q4 TO sales_four;
    

⑥ 파티션의 병합(MERGE)

  파티션 병합은 두 파티션의 데이터를 합치고, 하나의 파티션을 DROP 합니다.

  Hash Partition, Subpartition은 MERGE 작업을 할 수 없습니다.

 
--  sales_q1 파티션과 sales_q2 파티션을 sales_q2 파티션으로 병합
SQL> ALTER TABLE sales
     MERGE PARTITIONS sales_q1, sales_q2 INTO PARTITION sales_q2
     UPDATE INDEXES; --> Local Index를 갱신
    

⑦ 파티션의 분할(SPLIT)

  SPLIT 작업은 하나의 파티션을 두 개의 새로운 파티션으로 분할 합니다.

  Hash Partition, Subpartition은 SPLIT 작업을 할 수 없습니다

  아래는 sales 파티션 테이블의 sales_q2 파티션을 (2005,01,01) 값을 기준으로 sales_q1 와 sales_q2로 파티션을 분할하는 예제 입니다.

 
SQL> ALTER TABLE sales
     SPLIT PARTITION sales_q2 AT (2005, 01, 01)
     INTO (PARTITION sales_q1 TABLESPACE ASSM_TBS1,
           PARTITION sales_q2 TABLESPACE ASSM_TBS2)
    

⑧ 파티션의 변경(EXCHANGE)

  파티션의 EXCHAGEN는 파티션 데이터를 일반테이블로 생성하는 작업입니다. 물론 일반 테이블의 데이터를 파티션 테이블의 데이터로 생성 할 수도 있습니다.

  아래 예제는 파티션 테이블의 데이터를 일반 테이블로 생성하는 예제 입니다.

 
-- 파티션 데이터를 일반테이블로 이동하기 위한 테이블을 생성 합니다.
SQL> CREATE TABLE sales_ex
       (sales_no NUMBER,
        sale_year INT NOT NULL,
        sale_month INT NOT NULL,
        sale_day INT NOT NULL,
        customer_name  VARCHAR2(30),
        price NUMBER)
     TABLESPACE ASSM_TBS1;


-- 파티션 데이터를 일반 테이블로 변경 합니다.
SQL> ALTER TABLE sales
     EXCHANGE PARTITION sales_q1 
     WITH TABLE sales_ex;


-- 파티션 테이블의 데이터를 조회 해봅니다.
SQL> SELECT sales_no 
     FROM sales PARTITION (sales_q1); 
선택된 레코드가 없습니다.


-- 파티션 데이터를 이동한 일반 테이블의 데이터를 조회 해봅니다.
SQL> SELECT sales_no FROM sales_ex;  --> 1
    

⑨ 파티션의 테이블스페이스 변경

  sales 테이블의 sales_q3 파티션의 테이블스페이스를 ASSM_TBS5로 변경하는 예제 입니다.

 
SQL> ALTER TABLE sales 
     MOVE PARTITION sales_q3 
     TABLESPACE ASSM_TBS5;
    

⑩ 파티션 데이터 TRUNCATE

  sales 테이블의 sales_q3 파티션을 TRUNCATE 하는 예제 입니다.

 
SQL> ALTER TABLE sales TRUNCATE PARTITION sales_q3;
    


----------------------------------------------------------------------------------------------------------------------------------------------------------------

원문 : http://www.zdnet.co.kr/news/news_view.asp?artice_id=00000039137247&type=det

----------------------------------------------------------------------------------------------------------------------------------------------------------------



파티셔닝 세계 입문


대용량 테이블이나 인덱스를 파티셔닝한다는 것은 하나의 Object를 여러 개의 세그먼트로 나눈다는 의미이다. 즉 하나의 테이블이나 인덱스가 동일한 논리적 속성을 가진 여러 개의 단위(partition)로 나누어져 각각이 PCTFREE, PCTUSED, INITRANS, MAXTRANS, TABLESPACE, STORAGE PARAMETER 등 별도의 물리적 속성을 갖는 것이다. 

특히 관리해야 할 데이터가 늘어나면 성능과 스토리지 관점에서 문제가 생길 수 있는데, 이를 해결할 수 있는 효율적인 방법 가운데 하나가 곧 파티셔닝이다. 파티셔닝은 보통 다음과 같은 장점을 갖고 있다.


◆ 데이터 액세스시(특히 풀 스캔시) 액세스의 범위를 줄여 성능을 향상시킨다. 
◆ 물리적으로 여러 영역으로 파티셔닝해 전체 데이터의 훼손 가능성이 줄어들고 데이터 가용성이 향상된다. 
◆ 각 파티션별로 백업, 복구 작업을 할 수 있다. 
◆ 테이블의 파티션 단위로 디스크 I/O를 분산해 부하를 줄일 수 있다. 


오라클 DBMS에서 제공하는 파티셔닝 방식에는 레인지(range) 파티셔닝, 해시(hash) 파티셔닝, 리스트(list) 파티셔닝, 컴포지트(composite) 파티셔닝(레인지-해시, 레인지-리스트) 등이 있다.

특정 컬럼 값을 기준으로 분할하는 레인지 파티셔닝
레인지 파티셔닝은 어떤 특정 컬럼의 정렬 값을 기준으로 분할하는 것이다. 주로 순차적인(historical) 데이터를 관리하는 테이블에 많이 사용된다. 예를 들면 ‘가입계약’이라는 테이블이 있고 여기에 몇 년 동안의 데이터가 쌓여 있다면, 보통 5년치 데이터만 관리하고 이 가운데 자주 액세스하는 하는 것은 최근 1~2년 정도가 일반적이다. 

따라서 이를 년별, 월별로 파티셔닝하고 애플리케이션의 SQL을 조정해 전체 데이터가 아닌 최근 정보를 가지고 있는 파티션만 액세스하도록 하면 전체 데이터베이스의 성능을 향상시킬 수 있다. 일부 사례의 경우 가입계약_1999, 가입계약_2000처럼 월별 또는 년별로 테이블을 따로 만들어 사용하기도 했지만 실제로 쓰는 데 불편한 점이 많고 액세스하는 SQL이 복잡해지는 단점이 있다. 다음은 레인지 파티션을 만드는 DDL(Data Definition Language) 스크립트다. 

CREATE TABLE CONTRACT
  (I_YYYYMMDD VARCHAR2(8), I_CUSTOMER VARCHAR2(9), …… )
TABLESPACE TBS1
STORAGE (INITIAL 2M NEXT 2M PCTINCREASE 0)
  PARTITION BY RANGE (I_YYYYMMDD)
  (PARTITION PAR_200307 VALUES LESS THAN (‘20030801’),
  PARTITION PAR_200308 VALUES LESS THAN (‘20030901’), …… )

PARTITION BY RANGE (COLUMN_LIST)는 특정 컬럼을 기준으로 파티셔닝을 할 것인지를 결정하는 것이고, VALUES LESS THAN (VALUE_LIST)는 해당 파티션이 어느 범위에 포함될 것인지 상한을 정하는 것이다. PARTITION BY RANGE에 나타나는 COLUMN_LIST를 파티셔닝 컬럼이라고 하며 이 값이 파티셔닝 키를 형성한다. 

파티셔닝 컬럼은 결합 인덱스처럼 최대 16개까지 지정할 수 있다. VALUESS LESS THAN에 나타나는 VALUE_LIST는 파티셔닝 컬럼들의 상한 값으로, 여기 지정된 값보다 작은 값만을 저장하겠다는 의미이다. 이런 스크립트에서 지정한 물리적 속성들은 각 파티션들이 생성될 때 개별적으로 물리적 속성을 지정하지 않으면 각 파티션들은 이러한 속성 값을 적용 받게 된다.

오직 성능 향상, 해시 파티셔닝
해시 파티셔닝은 특정 컬럼 값에 해시 함수를 적용해 분할하는 방식으로, 데이터의 관리 목적보다는 성능 향상에 초점을 맞춘 개념이다. 레인지 파티셔닝은 각 범위에 따라 데이터 양이 일정치 않아 분포도가 일정치 않은 단점이 있는데, 해시 파티셔닝을 이런 단점을 보완해 일정한 분포를 가진 파티션으로 나누고, 균등한 분포도를 가질 수 있도록 조율해 병렬 프로세싱으로 성능을 높인다. 실제로 분포도를 정의하기 어려운 테이블을 파티셔닝을 할 때 많이 이용하고 2의 배수 개수로 파티셔닝하는 것이 일반적이다. 

해시 파티셔닝으로 구분된 파티션들은 동일한 논리, 물리적 속성을 가지다(단 테이블스페이스(tablespace)는 유일하게 파티션별로 지정할 수 있다). 또한 레인지 파티션과 달리 각 파티션에 지정된 값들을 DBMS가 결정하므로 각 파티션에 어떤 값들이 들어 있는지를 알 수 없다. 그러나 대용량의 분포도가 일정치 않은 테이블을 마이그레이션할 때는 프로그램 병렬 방식과 함께 유용하게 사용할 수 있다. 다음은 해시 파티션을 만드는 DDL 스크립트이다.

CREATE TABLE CONTRACT
  ( SERIAL NUMBER, CODE VARCHAR2(4), ……)
TABLESPACE TBS1
STORAGE (INITIAL 2M NEXT 2M PCTINCREASE 0)
PARTITION BY HASH(SERIAL)
  (PARTITION PAR_HASH_1 TABLESPACE TBS2,
  PARTITION PAR_HASH_2 TABLESPACE TBS3, ……)

함께 쓰일 때 더욱 강력한 리스트 파티셔닝
리스트 파티셔닝은 특정 컬럼의 특정 값을 기준으로 파티셔닝을 하는 방식이다. 주로 이질적인(distinct) 값이 많지 않고 분포도가 비슷하며 다양한 SQL의 액세스 패스에서 해당 컬럼의 조건이 많이 들어오는 경우 유용하게 사용된다. 예를 들어 ‘서비스 계약’이라는 테이블이 있고 서비스를 최초 가입한 대리점을 ‘가입 대리점’, 변경사항을 처리한 대리점을 ‘처리 대리점’이라고 한다면 모든 서비스의 가입, 해지, 전환 등의 처리 데이터에는 이 두 대리점이 존재한다. 테이블 구조를 보면 다음과 같다.

CREATE TABLE SERVICE_CONTRACT
  (I_YYYYMMDD VARCHAR2(8), I_CUSTOMER VARCHAR2(6),
  I_DLR_IND VARCHAR2(2), I_DEALER VARCHAR2(6), ……)

즉 I_DLR_IND(대리점 구분)라는 컬럼이 존재하고 ‘A’일 때는 ‘가입 대리점’, ‘S’일 때는 ‘처리 대리점“이라고 할 때 대부분의 조회 패턴에는 가입 대리점 또는 처리 대리점에 해당하는 값이 들어오기 마련이다. 이럴 때 I_DLR_IND로 리스트 파티셔닝을 한다면 어떨까. 즉 집합의 서브 타입을 분류할 때 리스트 파티션은 매우 유용하다. 지금 예로 든 것은 단편적인 것에 불과하지만 리스트 파티셔닝의 위력은 강력하다. 특히 컴포지트 파티션에서 레인지 파티션과 함께 사용하면 전체 데이터베이스의 성능을 크게 향상시킬수 있다. 다음은 리스트 파티션을 만드는 DDL 스크립트이다.

CREATE TABLE SERVICE_CONTRACT
  (I_YYYYMMDD VARCHAR2(8), I_CUSTOMER VARCHAR2(6),
  I_DLR_IND VARCHAR2(2), I_DEALER VARCHAR2(6), …….)
TABLESPACE TBS1
STORAGE (INITIAL 2M NEXT 2M PCTINCREASE 0)
PARTITION BY LIST (I_DLR_IND)
  (PARTITION PAR_A VALUES (‘A’), PARTITION PAR_S VALUES (‘S’))

PARTITION BY LIST에 나타나는 COLUMN_LIST는 파티셔닝 컬럼으로 파티션 키에 해당하고(단 단일 컬럼만 지정할 수 있다), VALUESS LESS THAN에 나타나는 VALUE_LIST는 파티셔닝 컬럼들의 값이다. 여기에 나타낸 값에 해당하는 행들을 저장하겠다는 의미가 된다. 

레인지의 장점을 그대로, 레인지-해시 컴포지트 파티셔닝
레인지-해시 컴포지트 파티셔닝은 레인지 방식을 사용해 데이터를 파티셔닝하고 각각의 파티션 내에서 해시 방식으로 서브 파트셔닝을 하는 방식이다. 서브 파티션이 독립된 세그먼트가 되는 것이 특징으로, 다음과 같은 장점이 있다.


◆ 관리와 성능 등 레인지 파티션의 장점을 그대로 수용한다.
◆ 해시 파티션의 이점인 데이터 균등 배치와 병렬화
◆ 서브 파티션에 특정 테이블스페이스를 지정할 수 있다.
◆ 서브 파티션별로 풀 스캔을 할 수 있어 스캔 범위를 줄여 성능을 향상시킨다.


레인지 파티션에서 해당 테이블이 단지 논리적인 구조이고 실제 데이터는 파티셔닝된 세그먼트에 저장됐던 것처럼 컴포지트 파티션에서도 해당 테이블과 파티셔닝된 테이블은 단지 파티셔닝을 위한 논리적인 구조일 뿐이다. 데이터는 가장 하위에 위치한 서브 파티션 영역에 저장된다. 다음은 레인지-해시 컴포지트 파티션을 생성하는 DDL 스크립트이다. PARTITION BY RANGE (I_YYYYMMDD)에 의해 레인지로 파티션을 한 후 SUBPARTITION BY HASH에 의해 서브 파티셔닝을 수행했음을 알 수 있다.

CREATE TABE TB_RANGE_HASH
  (I_YYYYMMDD VARCHAR2(8), I_SERIAL NUMBER, SALE_PRICE NUMBER, ……)
TABLESPACE TBS1
STORAGE (INITIAL 2M NEXT 2M PCTINCREASE 0)
PARTITION BY RANGE (I_YYYYMMDD)
SUBPARTITION BY HASH (I_SERIAL)
  (PARTITION SALES_1997 VALUES LESS THAN (‘19980101’)
  (SUBPARTITION SALES_1997_Q1 TABLESPACE TBS2,
  SUBPARTITION SALES_1997_Q2 TABLESPACE TBS3), ……)

레인지-리스트 컴포지트 파티셔닝
레인지-리스트 컴포지트 파티셔닝은 레인지 방식을 사용해 데이터를 파티셔닝하고 각 파티션 안에서 리스트 방식을 이용해 서브 파티셔닝하는 방식이다(이때 서브 파티션은 독립된 세그먼트가 된다). 레인지-리스트 컴포지트 파티션은 레인지-해시 컴포지트 파티션과 비슷하지만 서브 파티션이 리스트 파티션이라는 점이 다르다. 실제 업무에서는 레인지-해시보다 유용한 면이 많다. 다음은 레인지-리스트 컴포지트 파티션을 생성하는 DDL 스크립트이다.

CREATE TABLE TB_RANGE_LIST (
  I_YYYYMMDD VARCHAR2(8), I_AGR_IND VARCHAR2(2), I_DELAER VARCHAR2(6), …….)
TABLESPACE TBS1
STORAGE (INITIAL 2M NEXT 2M PCTINCREASE 0 MAXEXTENTS UNLIMITED)
PARTITION BY RANGE (I_YYYYMMDD)
SUBPARTITION BY LIST (I_AGR_IND) 
  (PARTITION PAR_1997 VALUES LESS THAN (‘19980101’)
  (SUBPARTITION PAR_1997_A VALUES (‘A’), SUBPARTITION PAR_1997_A VALUES (‘S’)),
    ……)

파티션된 인덱스의 참뜻
‘파티션된 인덱스(partitioned index)’라고 하면 대부분의 개발자들은 로컬 인덱스를 떠올린다. 또한 파티션된 테이블에서만 쓰이는 것으로 생각한다. 그러나 이것은 명백한 오산이다. 파티션된 인덱스는 파티션된 테이블과 별개의 것으로, 단지 많은 상호 연관을 갖고 있을 뿐이다. 파티션된 인덱스는 문자 그대로 인덱스를 파티셔닝한 것으로, 해당 테이블이 파티션된 테이블이든 파티션되지 않은(non-partitioned) 테이블이든 상관없이 만들 수 있다. 

예를 들면 ‘EMP’ 테이블의 크기가 상당히 크고 파티션되지 않은 일반 테이블일 경우 다음과 같은 과정을 통해 파티션된 인덱스를 만들 수 있다. 이를 ‘Global Prefixed Partitioned Index’라고 부르는데, 파티션 인덱스와 마찬가지로 대용량 데이터 환경에서 성능을 높이고 관리를 편리하게 하기 위해서다. 

CREATE INDEX EMP_IDX1 ON EMP (DEPTNO)
GLOBAL
PARTITION BY RANGE (DEPTNO)
  (PARTITION PAR_10 VALUES LESS THAN (‘20’) TABLESPACE TBS1,
  PARTITION PAR_20 VALUES LESS THAN (‘30’) TABLESPACE TBS2,
  PARTITION PAR_30 VALUES LESS THAN (‘40’) TABLESPACE TBS3,
  PARTITION PAR_40 VALUES LESS THAN (‘50’) TABLESPACE TBS4,
  PARTITION PAR_MAX VALUES LESS THAN (MAXVALUE) TABLESPACE TBS5)

파티션된 인덱스가 유용한 이유는, 앞서 파티션의 개념에서 설명한 것처럼 하나의 인덱스를 여러 개의 독립적인 물리 속성을 가진 세그먼트로 나누어 생성, 관리할 수 있기 때문이다. 오라클 DBMS에서 제공하는 인덱스는 글로벌/로컬 인덱스와 Prefixed/Non-Prefixed 인덱스로 분류된다.

파티션된 인덱스와 일반 인덱스 사이의 차이점은 파티션 테이블과 일반 테이블의 그것과 동일하다. 인덱스는 인덱스 컬럼과 Rowid 순으로 값이 정렬되는데, 이런 특성은 파티션 인덱스에서도 동일하다. 많은 개발자들이 파티션된 인덱스는 전체 테이블 값이 정렬되지 않는다고 생각하지 하지만 이것은 사실과 다르다. 글로벌 파티션된 인덱스의 경우 테이블에 대해 값 정렬이 보장돼 있으며, 인덱스도 파티션별로 독립적으로 관리할 수 있다. 두 가지 방식의 차이는 <그림 1>과 같다.

<그림 1> 파티션된 인덱스와 파티션되지 않은 인덱스의 차이


파티션되지 않은 인덱스는 하나의 루트(root) 노드에서 리프(leaf) 노드까지 전체적인 밸런스를 유지하는 구조이고, 파티션 인덱스는 파티션 별로 독립적인 루트 노드와 리프 노드를 갖고 있음을 알 수 있다. 따라서 파티션되지 않으면 대용량 테이블에서는 글로벌 인덱스의 깊이(depth)가 매우 깊어질 수 있는 단점이 있다.

반면 파티션된 인덱스는 각 파티션별 깊이가 일반 인덱스의 깊이보다 얕고 인덱스도 파티션 별로 할 수 있어 병렬 프로세싱을 이용한 인덱스 관리에 매우 효과적이다.

그렇다면 글로벌 인덱스와 로컬 인덱스는 어떤 차이가 있는 것일까? 많은 개발자들이 파티션됐는지 여부로 판단하지만 이것은 잘못된 생각이다. 앞서 설명한 것처럼 글로벌 인덱스도 파티셔닝할 수 있으며, 이를 파티션별로 관리할 수도 있다. 글로벌 인덱스와 로컬 인덱스의 가장 큰 차이는 ‘정렬’이다. 즉 글로벌 인덱스는 테이블 전체에 대해 인덱스된 컬럼과 Rowid 순으로 정렬되고, 로컬 인덱스는 해당 파티션 내에서만 인덱스된 컬럼과 Rowid 순으로 정렬된다.

또한 로컬 인덱스는 ‘Local’이라는 말에서 알 수 있듯이 지역적인 인덱스로, 해당 테이블(base table)의 파티션 키로 파티셔닝된 인덱스다. 일반적으로 로컬 인덱스의 구성 컬럼에 반드시 파티션 키가 포함돼야 가능한 것으로 알려져 있지만 로컬 인덱스에는 파티션 키가 포함되어 있지 않아도 사용할 수 있다. 다음 예제를 보자. PACKAGE_DLR_IDX1 인덱스의 구성 컬럼에 테이블 파티션 키인 I_DLR_IND가 포함되지 않아도 검색조건에 I_DLR_IND = ‘C’라는 검색 조건이 있기 때문에 해당 파티션의 로컬 인덱스를 이용하는 것을 알 수 있다. 

select
*from PACKAGE_DLR
where i_package = ‘AAA’ and i_dlr_ind = ‘C’
OperationObject NamePStartPStop
SELECT STATEMENT Hint=CHOOSE   
TABLE ACCESS BY LOCAL INDEXROWIDPACKAGE_DLR33
INDEX RANGE SCANPACKAGE_DLR_IDX33


글로벌 인덱스는 전역적인 인덱스로, 기본적으로는 파티션되지 않은 인덱스이다. 대부분의 개발자들은 글로벌 인덱스를 파티셔닝해 사용할 생각을 하지 못하는데, 대용량 테이블에서 인덱스 관리의 효율성을 높이고 인덱스 검색 성능을 높이기 위해서는 이를 파티셔닝하는 것이 좋다. 글로벌 인덱스는 기본 테이블의 파티션 키와 무관하게 파티셔닝하는 것으로 설사 기본 테이블의 파티션 키로 글로벌 인덱스를 파티셔닝했다고 해도 로컬 인덱스처럼 동일파티셔닝(equipartitioning)된 개념이 아니므로 테이블 DDL시 전체 인덱스를 다시 생성해야 한다.

그렇다면 글로벌 파티션 인덱스의 인덱스 컬럼 값은 어떻게 전체 테이블에 대해 정렬을 보장하는 것일까. 예를 들어 5000만 건의 파티션되지 않은 EMP 테이블을 부서번호에 따라 파티셔닝했다고 가정하면 다음과 같다.

CREATE INDEX EMP_IDX1 ON EMP (DEPTNO)
GLOBAL
PARTITION BY RAGE (DEPTNO)
(PARTITION PAR_10 VALUES LESS THAN (‘20’) TABLESPACE TBS1,
PARTITION PAR_20 VALUES LESS THAN (‘30’) TABLESPACE TBS2,
PARTITION PAR_30 VALUES LESS THAN (‘40’) TABLESPACE TBS3,
PARTITION PAR_40 VALUES LESS THAN (‘50’) TABLESPACE TBS4,
PARTITION PAR_MAX VALUES LESS THAN (‘MAXVALE’) TABLESPACE TBS2,

<그림 2> Global Prefixed Partitioned 인덱스


<그림 2>는 Global Prefixed Partitioned 인덱스의 구조다. Prefixed와 Non-Prefixed는 인덱스 파티셔닝 키가 인덱스의 선두 컬럼으로 오는가 그렇지 않은가의 차이가 있다. <그림 2>에서도 ‘Prefixed’란 인덱스의 파티션 키(DEPTNO)가 인덱스 선두 컬럼(DEPTNO)이 되는 것을 알 수 있다. 글로벌 인덱스의 경우 모든 인덱스 컬럼 값이 정렬돼 있다. 각 인덱스 파티션의 루트 블럭(root block)에 들어가는 값들이 인덱스 파티션에 따라 정렬되기 때문에 자연적으로 리프 블럭(leaf block)에 들어가는 모든 값들도 정렬되는 것이다. 반면 Global Non-Prefixed 인덱스를 파티셔닝하면 레인지 파티셔닝 방식으로만 가능하다. 이것은 정렬 때문인데, 레인지 파티션은 정렬 기능을 이용해 파티셔닝 키 자체를 생성하는데 반해 다른 파티셔닝 방식은 정렬과 상관없이 수행하기 때문이다.

로컬 인덱스는 Prefixed 인덱스와 Non-Prefixed 인덱스를 모두 지원한다. 로컬 인덱스는 기본적으로 현재 테이블의 파티션 키가 인덱스의 파티션 키가 되기 때문에 인덱스 컬럼에 현재 테이블의 파티션 키가 포함되지 않아도 인덱스를 생성할 수 있다. 또한 인덱스 컬럼 값의 정렬이 전체 테이블에 대해 보장된 것도 아니기 때문에 인덱스 파티션 키가 인덱스의 선두 컬럼이 될 필요가 없다. 또한 Non-Partitioned 인덱스이든 파티션 인덱스든 상관없이 인덱스를 이용하고자 할 때는 무조건 인덱스 파티션 키를 조회해야 하는 글로벌 인덱스와 달리 로컬 인덱스는 조회 검색조건에 파티션 키가 들어올 수도 있고 들어오지 않을 수도 있다.

대용량 DB 테이블과 인덱스 전략
파티션 인덱스 전략은 파티션 테이블과 밀접하게 연관되어 수립해야 하지만 여기서는 파티션 인덱스를 위주로 이야기를 풀어본다. 먼저 인덱스 크기에 대한 논의는 기본적으로 테이블보다는 훨씬 작게 생성, 관리하는 것이 원칙이다. 따라서 중소 용량의 데이터베이스 환경에서는 파티션 인덱스의 유용성을 따질 필요가 없다. 단 중소 용량의 데이터 환경일 경우에서도 테이블이 파티셔닝돼 있다면 파티션 인덱스를 고려해야 한다. 또한 기본적으로 파티션되지 않은 인덱스(일반 인덱스) 전략을 기본으로 해 테이블이 파티셔닝 된 경우와 인덱스를 파티셔닝했을 때의 장점을 비교해 보아야 한다.

먼저 테이블 파티션 키가 항상 ‘=’로 들어오는 경우 또는 파티션 범위가 크지 않은 경우에는 로컬 인덱스가 최상이다. 인덱스 컬럼의 순서와 구성은 액세스 패스에 따라 생성하면 되지만 최대한 가볍게 생성하는 것이 좋다. 기본 테이블의 파티션 키는 반드시 포함될 필요가 없으나, 테이블이 레인지 파티션이고 한 파티션 범위 안에서 파티션 키의 분포도가 좋을 경우 이를 포함하는 것을 고려해 볼만하다. 이렇게 하면 각 파티션당 인덱스가 파티션되지 않았을 때보다 가벼워지고 데이터 마이그레이션을 할 때도 테이블 파티션과 인덱스 파티션이 동일하므로 exchange, add, drop, split 등 파티션별 관리도 용이하다.

또한 빠른 응답 시간을 요구하는 환경에서 대용량 파티션 테이블의 조회 조건에 파티션 키가 들어오지 않을 가능성이 있다면 파티션 글로벌 인덱스를 고려해 볼만하다. 이렇게 하면 파티션되지 않은 글로벌 인덱스와 달리 레인지 파티션 별로 인덱스가 가벼워지는 장점이 있고, 레인지 파티션 별로 인덱스 split와 rebuild 명령을 독립적으로 수행할 수 있다. 컬럼 분포도에 따른 파티셔닝이나 민감한(critical)한 상수 레인지에 대해서는 파티션을 독립적으로 생성해 인덱스 크기를 줄임으로써 인덱스 검색 시간을 줄일 수 있는 이점도 있다.

exchange는 파티션된 테이블의 특정 파티션과 파티션되지 않은 일반 테이블 간의 구조를 서로 바꾸는 것으로, 대용량의 파티션된 테이블을 관리하는 데 상당한 효과가 있다. <그림 2>와 같이 데이터가 없는 새로운 데이터 테이블과 데이터가 들어 있는 파티션 2를 exchange하면 파티션 2에 해당하는 디렉토리 정보가 새로운 데이터로 바뀌고 새 테이블 데이터에는 데이터가 들어간다. 이것은 실제 데이터가 이동하는 것이 아니라 데이터를 저장하는 테이블 정보만을 업데이트하는 것이다. 한 가지 주의할 점은 exchange하고자 하는 파티션과 테이블의 구조가 같아야 하고 속성들의 특성도 같아야 한다는 사실이다.
exchange의 기본적인 문법은 다음과 같다.


Alter table Tb_Partition
Exchange partition par_200306
With table Tb_Exchange
(Without validation Including indexes)


<그림 3> 대용량 DB에서 exchange 작업


한편 파티션된 대용량 테이블에 split 함수를 실행하면 많은 시간이 걸린다. 이럴 때 exchange 기능을 이용하면 빠르고 안전하게 작업할 수 있다. <그림 4>에서 보는 것처럼 split를 해야 하는 파티션을 exchange에 의해 빈 공간으로 만든 다음 split을 하고 다시 데이터를 채우기 위해 split하는 것이다. 이렇게 하면 대용량의 데이터라도 매우 빠른 시간내에 split 작업을 수행할 수 있다.

<그림 4> 대용량 DB에서 split 작업


한편 대부분의 DBA들과 개발자들은 동일한 테이블을 생성할 때 create table ~ as select 구문을 이용한다. 대용량의 데이터일 경우 parallel 옵션을 줘 생성하기도 한다. 만약 1억 건의 테이블을 그대로 생성한다고 할 때 어떤 방법이 효과적일까. 이렇게 파티션된 대용량 테이블을 생성할 때는 exchange, program parallel 방법을 사용하는 것이 바람직하다.

<그림 5> 동일 테이블을 만들 때


<그림 5>는 이 과정을 도식화한 것이다. 먼저 생성할 TB_PART_1 테이블의 빈껍데기를 만든다. 대용량의 파티션된 테이블의 파티션 각각을 create table ~ as select 구문의 parallel 옵션을 이용해 각 테이블로 생성한다. 이후 미리 생성해 놓은 TB_PART_1 테이블의 파티션과 만들어 놓은 테이블들을 exchange하는 것이다. 이때 파티션별로 200105.sql, 200106.sql, 200107.sql…… 형식으로 만들어 놓고 이 프로그램들을 동시에 실행하면(program parallel) 극적인 효과를 볼 수 있다.

이번엔 데이터 마이그레이션에 대해 살펴 보자. 원격으로 데이터를 옮겨야 할 때 보통 database link를 이용한다. 네트워크를 통해 데이터를 옮기면 직렬(serial)로 데이터가 이동되므로 속도가 현저하게 떨어지기 때문이다. 따라서 소스 테이블을 파티셔닝하고 해당 파티션을 액세스하는 프로그램을 각각 띄워 병렬 프로세싱을 하게 되면 매우 빠른 속도로 데이터를 옮길 수 있다.

소스 테이블을 파티셔닝할 수 있는 상황이라면 테이블의 분포를 보고 레인지나 리스트 방식으로 파티셔닝할 수 있고, 일정한 분포가 존재하지 않는 테이블이라면 해시 파티셔닝으로 분포도를 고르게 나눈 다음 해당 파티션을 읽는 뷰를 액세스해 데이터를 옮기는 것이 좋다.

예를 들어 다음은 중대형 정도 크기인 약 2700만 건의 회원 테이블을 옮기는 DDL 스크립트다. 앞서 언급한 대로 이를 바로 database link를 이용해 처리하면 네트워크의 속도가 떨어져 엄청난 시간이 소요된다. 그러나 이것을 일반 테이블을 여러 개로 파티션을 나누어서 파티션과 병렬 처리하면 성능이 크게 향상된다. 작업 순서는 다음과 같다.


create table t_cust_hash
storage (initial 5M next 5M pctincrease 0)
partition by hash(mem_no)
(
partition par_hash_1 TABLESPACE TS_DATA,
partition par_hash_2 TABLESPACE TS_DATA,
partition par_hash_3 TABLESPACE TS_DATA,
partition par_hash_4 TABLESPACE TS_DATA,
partition par_hash_6 TABLESPACE TS_DATA,
partition par_hash_7 TABLESPACE TS_DATA,
partition par_hash_8 TABLESPACE TS_DATA,
partition par_hash_9 TABLESPACE TS_DATA,
partition par_hash_10 TABLESPACE TS_DATA,
)
nologging
as
select /*+ parallel(x 10) */ * from t_cust x


이제 다음과 같이 소스 테이블 뷰 생성한 후


create or replace view t_cust_1
as select * from t_cust_hash partition (par_hash_1);

create or replace view t_cust_2
as select * from t_cust_hash partition (par_hash_2);

create or replace view t_cust_3
as select * from t_cust_hash partition (par_hash_3)

……


다음과 같이 프로그램 패러럴(program parallel) 작업을 동시에 실행한다.


T_cust_1.sql
create table t_cust_1
storage (initial 5M next 5M pctincrease 0)
nologging
tablespace njh
as
select /*+ parallel(x 4) */ * from t_cust_1@remote x;

T_cust_2.sql
create table t_cust_2
storage (initial 5M next 5M pctincrease 0)
nologging
tablespace njh
as
select /*+ parallel(x 4) */ * from t_cust_2@remote x


이것은 단적인 예에 지나지 않는다. 활용할 수 있는 사례는 얼마든지 있을 것이다. 한편 인덱스는 전체 데이터에 대해 해당 컬럼의 값으로 정렬하기 때문에 대용량 테이블의 경우 create, rebuild 명령을 실행할 때 많은 시간이 필요하다. 이때 파티션된 인덱스를 만들면 인덱스의 생성과 관리를 더 활용적으로 할 수 있다. 다음은 파티션된 인덱스를 Unusable로 생성한 사례다(로컬/글로벌 파티션된 인덱스).

먼저 파티션 인덱스를 ‘unusable’ 옵션을 이용해 생성한다. 실제 데이터를 정렬해 만드는 것이 아니라 일종의 껍데기를 만드는 과정이다. 이제 앞서 살펴본 병렬 처리를 이용해 여러 파티션을 동시에 rebuild를 하면 대용량 데이터라도 빠른 시간에 인덱스를 생성할 수 있다.


CREATE INDEX EMP_IDX1 ON EMP (DEPTNO)
GLOBAL
PARTITION BY RANGE (DEPTNO)
  (PARTITION PAR_10 VALUES LESS THAN (‘20’) TABLESPACE TBS1,
  PARTITION PAR_20 VALUES LESS THAN (‘30’) TABLESPACE TBS2,
  PARTITION PAR_30 VALUES LESS THAN (‘40’) TABLESPACE TBS3,
  PARTITION PAR_40 VALUES LESS THAN (‘50’) TABLESPACE TBS4,
  PARTITION PAR_MAX VALUES LESS THAN (MAXVALUE) TABLESPACE TBS5)
UNUSABLE;


이제 파티션별로 index1.sql, index2.sql 등을 독립적으로 병렬 실행한다.


ALTER INDEX EMP_IDX1 REBUILD PARTITION PAR_10 PARALLEL 4; ---‘ index1.sql
ALTER INDEX EMP_IDX1 REBUILD PARTITION PAR_20 PARALLEL 4; ---‘ index2.sql
ALTER INDEX EMP_IDX1 REBUILD PARTITION PAR_30 PARALLEL 4; ---‘ index3.sql
ALTER INDEX EMP_IDX1 REBUILD PARTITION PAR_40 PARALLEL 4; ---‘ index4.sql
ALTER INDEX EMP_IDX1 REBUILD PARTITION PAR_MAX PARALLEL 4; ---‘ index5.sql







(1) Outer NL 조인

  • NL 조인은 그 특성상 Outer 조인 할때 방향이 한쪽으로 고정된다.
  • Outer 기호( + )가 붙지 않은 테이블이 항상 드라이빙 테이블로 선택된다.
  • Leading 이나 Ordered 힌트로 그 순서를 바꿀 수 없다.
Nested Loop Outer
SQL> SELECT *
  2    FROM dept d, emp e
  3   WHERE e.deptno(+) = d.deptno
  4  ;

13 개의 행이 선택되었습니다.


Execution Plan
----------------------------------------------------------
Plan hash value: 1350698460

-----------------------------------------------------------------------------------------------
| Id  | Operation                    | Name           | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |                |    12 |   684 |     4   (0)| 00:00:01 |
|   1 |  NESTED LOOPS OUTER          |                |    12 |   684 |     4   (0)| 00:00:01 |
|   2 |   TABLE ACCESS FULL          | DEPT           |     4 |    80 |     3   (0)| 00:00:01 |
|   3 |   TABLE ACCESS BY INDEX ROWID| EMP            |     3 |   111 |     1   (0)| 00:00:01 |
|*  4 |    INDEX RANGE SCAN          | EMP_DEPTNO_IDX |     4 |       |     0   (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------

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

   4 - access("E"."DEPTNO"(+)="D"."DEPTNO")


Statistics
----------------------------------------------------------
          0  recursive calls
          0  db block gets
         15  consistent gets
          0  physical reads
          0  redo size
       1600  bytes sent via SQL*Net to client
        384  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
         13  rows processed

(2) Outer 소트 머지 조인

  • 소트머지 조인은 소트된 중간집합을 이용한다는 점만 다를뿐 처리루틴은 NL조인과 다르지 않다.
  • 즉, 그 특성상 Outer 조인 할때 방향이 한쪽으로 고정된다.
  • Outer 기호( + )가 붙지 않은 테이블이 항상 드라이빙 테이블로 선택된다.
  • Leading 이나 Ordered 힌트로 그 순서를 바꿀 수 없다.
Merge Join Outer
SQL> SELECT /*+ use_merge(d e) */ *
  2    FROM dept d, emp e
  3   WHERE e.deptno(+) = d.deptno
  4  ;

13 개의 행이 선택되었습니다.


Execution Plan
----------------------------------------------------------
Plan hash value: 2251696546

----------------------------------------------------------------------------------------
| Id  | Operation                    | Name    | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |         |    12 |   684 |     6  (17)| 00:00:01 |
|   1 |  MERGE JOIN OUTER            |         |    12 |   684 |     6  (17)| 00:00:01 |
|   2 |   TABLE ACCESS BY INDEX ROWID| DEPT    |     4 |    80 |     2   (0)| 00:00:01 |
|   3 |    INDEX FULL SCAN           | PK_DEPT |     4 |       |     1   (0)| 00:00:01 |
|*  4 |   SORT JOIN                  |         |    12 |   444 |     4  (25)| 00:00:01 |
|   5 |    TABLE ACCESS FULL         | EMP     |    12 |   444 |     3   (0)| 00:00:01 |
----------------------------------------------------------------------------------------

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

   4 - access("E"."DEPTNO"(+)="D"."DEPTNO")
       filter("E"."DEPTNO"(+)="D"."DEPTNO")


Statistics
----------------------------------------------------------
          1  recursive calls
          0  db block gets
         11  consistent gets
          0  physical reads
          0  redo size
       1592  bytes sent via SQL*Net to client
        384  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          1  sorts (memory)
          0  sorts (disk)
         13  rows processed

(3) Outer 해시 조인

9i 까지의 Outer 해시 조인
  • Outer 기호( + )가 붙지 않은 테이블이 항상 Bulid Input 테이블로 선택된다.
  • Leading 이나 Ordered 힌트로 그 순서를 바꿀 수 없다.
Hash Join Outer
SQL> SELECT /*+ use_hash(d e) */ *
  2    FROM dept d, emp e
  3   WHERE e.deptno(+) = d.deptno
  4  ;

13 개의 행이 선택되었습니다.


Execution Plan
----------------------------------------------------------
Plan hash value: 3713469723

---------------------------------------------------------------------------
| Id  | Operation          | Name | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |      |    12 |   684 |     7  (15)| 00:00:01 |
|*  1 |  HASH JOIN OUTER   |      |    12 |   684 |     7  (15)| 00:00:01 |
|   2 |   TABLE ACCESS FULL| DEPT |     4 |    80 |     3   (0)| 00:00:01 |
|   3 |   TABLE ACCESS FULL| EMP  |    12 |   444 |     3   (0)| 00:00:01 |
---------------------------------------------------------------------------

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

   1 - access("E"."DEPTNO"(+)="D"."DEPTNO")


Statistics
----------------------------------------------------------
          1  recursive calls
          0  db block gets
         15  consistent gets
          0  physical reads
          0  redo size
       1684  bytes sent via SQL*Net to client
        384  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
         13  rows processed
  • 그림_2_19
  • 그림_2_20
10g 의 Right Outer 해시 조인
  • Outer 기호( + )가 붙은 테이블도 Bulid Input 될 수 있다.
  • Swap_join_inputs 힌트로 그 순서를 바꿀 수 있다.
Hash Join Right Outer
SQL> SELECT /*+ use_hash(d e) swap_join_inputs(d) */ *
  2    FROM dept d, emp e
  3   WHERE e.deptno = d.deptno(+)
  4  ;

12 개의 행이 선택되었습니다.


Execution Plan
----------------------------------------------------------
Plan hash value: 4261033907

------------------------------------------------------------------------------
| Id  | Operation             | Name | Rows  | Bytes | Cost (%CPU)| Time     |
------------------------------------------------------------------------------
|   0 | SELECT STATEMENT      |      |    12 |   684 |     7  (15)| 00:00:01 |
|*  1 |  HASH JOIN RIGHT OUTER|      |    12 |   684 |     7  (15)| 00:00:01 |
|   2 |   TABLE ACCESS FULL   | DEPT |     4 |    80 |     3   (0)| 00:00:01 |
|   3 |   TABLE ACCESS FULL   | EMP  |    12 |   444 |     3   (0)| 00:00:01 |
------------------------------------------------------------------------------

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

   1 - access("E"."DEPTNO"="D"."DEPTNO"(+))


Statistics
----------------------------------------------------------
          1  recursive calls
          0  db block gets
         15  consistent gets
          0  physical reads
          0  redo size
       1650  bytes sent via SQL*Net to client
        384  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
         12  rows processed
  • 차이점 비교
    9i Hash Join Outer10g Hash Join Right Outer
    1. Outer 집합인 Dept 테이블을 해시테이블로 Build한다.1. Inner 집합인 Dept 테이블을 해시테이블로 Build한다.
    2. Inner 집합인 Emp 테이블을 읽으면서 해시테이블 탐색(Probe)2. Outer 집합인 Emp 테이블을 읽으면서 해시테이블 탐색(Probe)
    3. 조인에 성공한 자료를 결과집합에 삽입하고 해쉬테이블에 성공여부 체크3. 조인 성공여부와 상관없이 결과집합에 삽입
    4. 해시테이블에서 조인에 실패한 자료를 결과집합에 삽입 
Right Outer 해시 조인 탄생 배경
  • 그림_2_21
    • 정상적인 설계 모델에 있어서 고객없는 주문, 상품 없는 주문은 없다.
    • 따라서 주문은 아우터 조인의 기준집합이 될 수 없다.
    • 적은 량의 자료인 고객, 상품이 기준이 되어 아우터 조인하는 것이 유리함
    • 따라서 Outer 집합이 해시테이블로 빌드되는 알고리즘을 선택
  • 그림_2_22
    • 비정상적 설계 모델에서는 고객없는 주문, 상품 없는 주문이 존재할 수 있다.
    • 주문을 기준으로 고객이나 상품을 아우터 조인하는 경우가 발생
    • 큰 집합인 주문이 해시테이블로 빌드되면서 성능저하 발생
    • 이에 Inner 집합도 해시테이블로 빌드될수 있도록 알고리즘 추가
9i 이전 버전에서 Outer 해시 조인 튜닝
Outer 해시 조인 튜닝
SQL> CREATE INDEX idx_emp2 ON emp(empno, deptno);

인덱스가 생성되었습니다.

SQL>
SQL> SELECT /*+ ordered index_ffs(e) full(d) full(e2) use_hash(e d) use_hash(e2)
  2             parallel_index(e) parallel(d) parallel(e2) */
  3         d.*, e2.*
  4    FROM emp e, dept d, emp e2
  5   WHERE e.deptno = d.deptno(+)
  6     AND e2.empno = e.empno
  7  ;

12 개의 행이 선택되었습니다.


Execution Plan
----------------------------------------------------------
Plan hash value: 2161030882

-----------------------------------------------------------------------------------
| Id  | Operation              | Name     | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT       |          |    12 |   768 |     9  (12)| 00:00:01 |
|*  1 |  HASH JOIN             |          |    12 |   768 |     9  (12)| 00:00:01 |
|*  2 |   HASH JOIN OUTER      |          |    12 |   324 |     6  (17)| 00:00:01 |
|   3 |    INDEX FAST FULL SCAN| IDX_EMP2 |    12 |    84 |     2   (0)| 00:00:01 |
|   4 |    TABLE ACCESS FULL   | DEPT     |     4 |    80 |     3   (0)| 00:00:01 |
|   5 |   TABLE ACCESS FULL    | EMP      |    12 |   444 |     3   (0)| 00:00:01 |
-----------------------------------------------------------------------------------

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

   1 - access("E2"."EMPNO"="E"."EMPNO")
   2 - access("E"."DEPTNO"="D"."DEPTNO"(+))


Statistics
----------------------------------------------------------
          1  recursive calls
          0  db block gets
         18  consistent gets
          0  physical reads
          0  redo size
       1650  bytes sent via SQL*Net to client
        384  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
         12  rows processed
  • Big 집합인 emp를 풀스캔 하는 대신 인덱스만 스캔하여 해시테이블 빌드
  • Build Input의 크기를 줄임으로써 Disk IO를 최소화 하려는 아이디어
  • 아우터 해시조인 완성후 emp의 자료를 조회하기 위해 다시 셀프 조인이 필요
  • Disk IO 는 줄었지만 해시버킷당 엔트리 갯수가 많아서 생기는 문제는 피할 수 없다.
  • 책의 예제에서는 주문일시 구간을 나누어 쿼리를 여러번 수행하는 해결책을 제시한다.
  • 주문테이블은 주문일시로 Range 파티셔닝 되어 있을 것이고
    일정한 주문일시 구간내의 고객수는 많지 않을 것이므로
    해시버킷당 엔트리 갯수가 많아서 생기는 문제를 최소화 할 수 있다.
    고객테이블을 반복적으로 읽는 ?율에도 불구하고 빠르게 수행될 것으로 예상

(4) Full Outer 조인

테스트 테이블 생성
SQL> EXEC DBMS_RANDOM.SEED(150);

PL/SQL 처리가 정상적으로 완료되었습니다.

SQL>
SQL> CREATE TABLE 입금
  2  AS
  3  SELECT ROWNUM 일련번호
  4       , ROUND(DBMS_RANDOM.VALUE(1, 20)) 고객ID
  5       , ROUND(DBMS_RANDOM.VALUE(1000, 1000000), -2) 입금액
  6    FROM dual CONNECT BY LEVEL <= 10
  7  ;

테이블이 생성되었습니다.

SQL>
SQL> CREATE TABLE 출금
  2  AS
  3  SELECT ROWNUM 일련번호
  4       , ROUND(DBMS_RANDOM.VALUE(1, 20)) 고객ID
  5       , ROUND(DBMS_RANDOM.VALUE(1000, 1000000), -2) 입금액
  6    FROM dual CONNECT BY LEVEL <= 10
  7  ;

테이블이 생성되었습니다.

SQL> EXEC DBMS_STATS.GATHER_TABLE_STATS(user, '입금');

PL/SQL 처리가 정상적으로 완료되었습니다.

SQL> EXEC DBMS_STATS.GATHER_TABLE_STATS(user, '출금');

PL/SQL 처리가 정상적으로 완료되었습니다.
'Left Outer 조인 + Union All + Anti 조인(Not Exists 필터)' 이용
Left Outer 조인 + Union All + Anti 조인(Not Exists 필터)
SQL> set autot on exp
SQL> set pagesize 20
SQL> 
SQL> SELECT a.고객ID, a.입금액, b.출금액
  2    FROM (SELECT 고객ID, SUM(입금액) 입금액 FROM 입금 GROUP BY 고객ID) a
  3       , (SELECT 고객ID, SUM(출금액) 출금액 FROM 출금 GROUP BY 고객ID) b
  4   WHERE b.고객ID(+) = a.고객ID
  5   UNION ALL
  6  SELECT a.고객ID, Null, a.출금액
  7    FROM (SELECT 고객ID, SUM(출금액) 출금액 FROM 출금 GROUP BY 고객ID) a
  8   WHERE NOT EXISTS (SELECT 'x' FROM 입금 WHERE 고객ID = a.고객ID)
  9  ;

    고객ID     입금액     출금액           결과를 통해 처리과정을 유추해 보자.
---------- ---------- ---------- --------------------------------------------------
        6     707000     342900     ┐ 1. 입금 테이블을 해시테이블로 빌드
         3     259400     768100     ├ 2. 출금테이블을 스캔하면서 해시테이블 탐색(Probe)
       19     398300     558800     ┘ 3. 조인에 성공한 자료를 결과집합에 삽입
        13      59400                ┐
         8     957000                │
         1     224100                ├ 4. 해시테이블에서 조인에 성공하지 못한 집합
         2     231900                │
        18     336900                │
         4    1211700                ┘
        11                599500     ┐
        20                193300     │
         5                264500     │
        15                299400     ├ 5. 출금테이블 자료중 입금에 없는 자료 추가
        12                958200     │
        10                205700     │
         9                 18100     ┘

16 개의 행이 선택되었습니다.


Execution Plan
----------------------------------------------------------
Plan hash value: 3598059211

------------------------------------------------------------------------------
| Id  | Operation             | Name | Rows  | Bytes | Cost (%CPU)| Time     |
------------------------------------------------------------------------------
|   0 | SELECT STATEMENT      |      |    10 |   478 |    16  (63)| 00:00:01 |
|   1 |  UNION-ALL            |      |       |       |            |          |
|*  2 |   HASH JOIN OUTER     |      |     9 |   468 |     9  (34)| 00:00:01 |
|   3 |    VIEW               |      |     9 |   234 |     4  (25)| 00:00:01 |
|   4 |     HASH GROUP BY     |      |     9 |    63 |     4  (25)| 00:00:01 |
|   5 |      TABLE ACCESS FULL| 입금 |    10 |    70 |     3   (0)| 00:00:01 |
|   6 |    VIEW               |      |    10 |   260 |     4  (25)| 00:00:01 |
|   7 |     HASH GROUP BY     |      |    10 |    70 |     4  (25)| 00:00:01 |
|   8 |      TABLE ACCESS FULL| 출금 |    10 |    70 |     3   (0)| 00:00:01 |
|   9 |   HASH GROUP BY       |      |     1 |    10 |     8  (25)| 00:00:01 |
|* 10 |    HASH JOIN ANTI     |      |     1 |    10 |     7  (15)| 00:00:01 |
|  11 |     TABLE ACCESS FULL | 출금 |    10 |    70 |     3   (0)| 00:00:01 |
|  12 |     TABLE ACCESS FULL | 입금 |    10 |    30 |     3   (0)| 00:00:01 |
------------------------------------------------------------------------------

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

   2 - access("B"."고객ID"(+)="A"."고객ID")
  10 - access("고객ID"="고객ID")
Ansi Full Outer 조인
Ansi Full Outer 조인
SQL> SELECT NVL(a.고객ID, b.고객ID) 고객ID
  2       , a.입금액, b.출금액
  3    FROM (SELECT 고객ID, SUM(입금액) 입금액 FROM 입금 GROUP BY 고객ID) a
  4    FULL OUTER JOIN
  5         (SELECT 고객ID, SUM(출금액) 출금액 FROM 출금 GROUP BY 고객ID) b
  6      ON a.고객ID = b.고객ID
  7  ;

    고객ID     입금액     출금액
---------- ---------- ----------
         6     707000     342900
         3     259400     768100
        19     398300     558800
        13      59400
         8     957000
         1     224100
         2     231900
        18     336900
         4    1211700
        11                599500
        20                193300
         5                264500
        10                205700
        12                958200
         9                 18100
        15                299400

16 개의 행이 선택되었습니다.


Execution Plan
----------------------------------------------------------
Plan hash value: 82146744

---------------------------------------------------------------------------------
| Id  | Operation                | Name | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------------
|   0 | SELECT STATEMENT         |      |    18 |   936 |    28  (15)| 00:00:01 |
|   1 |  VIEW                    |      |    18 |   936 |    28  (15)| 00:00:01 |
|   2 |   UNION-ALL              |      |       |       |            |          |
|*  3 |    HASH JOIN OUTER       |      |     9 |   612 |     9  (34)| 00:00:01 |
|   4 |     VIEW                 |      |     9 |   342 |     4  (25)| 00:00:01 |
|   5 |      HASH GROUP BY       |      |     9 |    63 |     4  (25)| 00:00:01 |
|   6 |       TABLE ACCESS FULL  | 입금 |    10 |    70 |     3   (0)| 00:00:01 |
|   7 |     VIEW                 |      |    10 |   300 |     4  (25)| 00:00:01 |
|   8 |      HASH GROUP BY       |      |    10 |    70 |     4  (25)| 00:00:01 |
|   9 |       TABLE ACCESS FULL  | 출금 |    10 |    70 |     3   (0)| 00:00:01 |
|  10 |    HASH GROUP BY         |      |     9 |    63 |    19   (6)| 00:00:01 |
|* 11 |     FILTER               |      |       |       |            |          |
|  12 |      TABLE ACCESS FULL   | 출금 |    10 |    70 |     3   (0)| 00:00:01 |
|  13 |      SORT GROUP BY NOSORT|      |     1 |     3 |     3   (0)| 00:00:01 |
|* 14 |       TABLE ACCESS FULL  | 입금 |     1 |     3 |     3   (0)| 00:00:01 |
---------------------------------------------------------------------------------

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

   3 - access("A"."고객ID"="B"."고객ID"(+))
  11 - filter( NOT EXISTS (SELECT /*+ UNNEST */ 0 FROM "입금" "입금" WHERE
              "고객ID"=:B1 GROUP BY "고객ID"))
  14 - filter("고객ID"=:B1)
Native Hash Full Outer 조인
  • 오라클 11g에서 선보임
  • 10.2.0.4버전에서 Hidden 파라미터 조정으로 이 기능 사용 가능
    Native Hash Full Outer 조인
    SELECT /*+ opt_param('_optimizer_native_full_outer_join', 'force') */
           NVL(a.고객ID, b.고객ID) 고객ID
         , a.입금액, b.출금액
      FROM (SELECT 고객ID, SUM(입금액) 입금액 FROM 입금 GROUP BY 고객ID) a
      FULL OUTER JOIN
           (SELECT 고객ID, SUM(출금액) 출금액 FROM 출금 GROUP BY 고객ID) b
        ON a.고객ID = b.고객ID
    ;
    
    다음 결과 및 실행계획은 조작임을 미리 밝힙니다.(버전관계로 테스트 못함)
    
        고객ID     입금액     출금액           결과를 통해 처리과정을 유추해 보자.                  
    ---------- ---------- ---------- --------------------------------------------------
            6     707000     342900     ┐
            13      59400                │
             8     957000                │
             1     224100                ├ 1. 출금 테이블을 해시테이블로 빌드
             3     259400     768100     │ 2. 입금테이블을 스캔하면서 해시테이블 탐색(Probe)
           19     398300     558800     │ 3. 조인 성공과 상관없이 모두 결과집합에 삽입
             2     231900                │
            18     336900                │
             4    1211700                ┘
            11                599500     ┐
            20                193300     │
             5                264500     │
            10                205700     ├ 4. 해시테이블에서 조인에 성공하지 못한 집합
            12                958200     │
             9                 18100     │
            15                299400     ┘
    
    16 개의 행이 선택되었습니다.
    
    ---------------------------------------------------------------------------------
    | Id  | Operation               | Name     | Rows| Bytes| Cost (%CPU)| Time     |
    ---------------------------------------------------------------------------------
    |   0 | SELECT STATEMENT        |          |     |      |            | 00:00:01 |
    |   1 |  VIEW                   | VW_FOJ_0 |     |      |            | 00:00:01 |
    |   2 |   HASH JOIN FULL OUTER  |          |     |      |            | 00:00:01 |
    |   3 |    VIEW                 |          |     |      |            | 00:00:01 |
    |   4 |     HASH GROUP BY       |          |     |      |            | 00:00:01 |
    |   5 |      TABLE ACCESS FULL  | 입금      |     |      |            | 00:00:01 |
    |   6 |    VIEW                 |          |     |      |            | 00:00:01 |
    |   7 |     HASH GROUP BY       |          |     |      |            | 00:00:01 |
    |   8 |      TABLE ACCESS FULL  | 출금      |     |      |            | 00:00:01 |
    ---------------------------------------------------------------------------------
    
Union all 을 이용한 Full Outer 조인
Union all 을 이용한 Full Outer 조인
SQL> SELECT 고객ID
  2       , SUM(입금액) 입금액
  3       , SUM(출금액) 출금액
  4    FROM (SELECT 고객ID, 입금액, TO_NUMBER(null) 출금액 FROM 입금
  5           UNION ALL
  6          SELECT 고객ID, TO_NUMBER(null) 입금액, 출금액 FROM 출금
  7          )
  8   GROUP BY 고객ID
  9  ;

    고객ID     입금액     출금액
---------- ---------- ----------
         1     224100
         6     707000     342900
        13      59400
        11                599500
         2     231900
        20                193300
         4    1211700
         5                264500
         8     957000
         3     259400     768100
        18     336900
        19     398300     558800
        10                205700
        12                958200
         9                 18100
        15                299400

16 개의 행이 선택되었습니다.


Execution Plan
----------------------------------------------------------
Plan hash value: 2888689472

-----------------------------------------------------------------------------
| Id  | Operation            | Name | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------
|   0 | SELECT STATEMENT     |      |    20 |   780 |     7  (15)| 00:00:01 |
|   1 |  HASH GROUP BY       |      |    20 |   780 |     7  (15)| 00:00:01 |
|   2 |   VIEW               |      |    20 |   780 |     6   (0)| 00:00:01 |
|   3 |    UNION-ALL         |      |       |       |            |          |
|   4 |     TABLE ACCESS FULL| 입금 |    10 |    70 |     3   (0)| 00:00:01 |
|   5 |     TABLE ACCESS FULL| 출금 |    10 |    70 |     3   (0)| 00:00:01 |
-----------------------------------------------------------------------------


===== 아래와 같이 출처를 표기합니다. =====================================================================

  • 이 문서는 구루비에서 작성하였습니다.
  • 이 문서를 다른 블로그나 홈페이지에 게재하실 경우에는 출처를 꼭 밝혀 주시면 고맙겠습니다.~^^
  • 출처 : http://wiki.gurubee.net/pages/viewpage.action?pageId=6259015&
  • 구루비 지식창고의 모든 문서는 크리에이티브 커먼즈의 저작자표시-비영리-동일조건변경허락(BY-NC-SA) 라이선스에 따라 자유롭게 사용할 수 있습니다.

  • 'ORACLE > SQL' 카테고리의 다른 글

    잡동사니 퀴리  (0) 2017.07.05
    DML 수행 시 내부 절차  (0) 2017.06.20
    관리를 위한 쿼리 모음  (0) 2017.06.16
    Oracle 스크립트 모음  (0) 2017.06.16
    병렬 DML 모니터링 방법  (0) 2017.06.15

    참조 출처  : http://www.gurubee.net/lecture/1876 

               

    1. Flashback 개요
      가. 개요 : 사용자의 논리적인 장애(DDL, DML)를 빠르게 복구해내는 방법, undo segment 사용
      나. 종류
        - Row Level Flashback : 특정 row만 과거시점으로 되돌리는 기능, commit된 데이터만 flashback 할 수 있음
        - Table Level Flashback : 특정 table만 과거시점으로 되될리는 기능
        - Database Level Falshback : 데이터베이스 전체를 특정 시점으로 되돌리는 기능, 불완전 복구와 유사한 기능


    2. Flashback을 사용하기 위한 요구조건

      자동 언두 관리 시스템을 사용해야 합니다. (UNDO_MANAGEMENT 파라미터를 AUTO로 설정)
        - UNDO_MANAGEMENT = AUTO

      이전의 어느 시점까지의 언두(UNDO)정보를 보유하여 Flashback Query를 수행할것인지 UNDO_RETENTION 파라미터를 설정해야 합니다.
        - ALTER SYSTEM SET UNDO_RETENTION=1800

      일반사용자가 Flashback 기능을 이용하기 위해서 DBMS_FLASHBACK 패키지에 대한 EXECUTE권한이 있어야 합니다.

    3. Flashback 사용하기

      Flashback의 사용 방법은 과거시점의 특정 시간으로 사용하는 방법과 SCN(System Change Number)을 사용하는 방법이 있습니다.

      과거시점의 시간 사용DBMS_FLASHBACK.ENABLE_AT_TIME(query_time IN TIMESTAMP);

      SCN 사용 : DBMS_FLASHBACK.ENABLE_AT_SYSTEM_CHANGE_NUMBER(query_scn IN NUMBER);

      과거시점의 시간을 지정하여 Flashback 기능을 사용 할 경우 오라클은 내부적으로 이를 SCN으로 전환하여 처리 합니다. 

      시간 정보를 SCN으로 Mapping하는 시간이 필요한데 통상 5분 주기로 이루어 집니다. 

      따라서, 시간으로 지정할때는 현재보다 5분이상 차이가 나는 과거시점을 지정해야 합니다.

      또한 Flashback 기능은 무한대로 이전의 데이터를 조회할 수 있는 기능이 아니고, 

      관리자가 UNDO_RETENTION 파라미터를 통해서 정해준 시간(초) 동안의 데이터를 조회할 수 있습니다. 

      디폴트 UNDO_RETENTION 시간은 10800(3시간) 입니다.

      그리고 Flashback data를 참고하는 경우엔 DML, DDL등의 작업을 직접 수행 할 수 없습니다.

    4. Flashback 사용을 위한 환경설정

     
    C:\> SQLPLUS /NOLOG
     
     
    -- SYSDBA 권한으로 접속
    SQL> CONN / AS SYSDBA
     
     
    -- UNDO MANAGEMENT MODE 확인
    SQL> SHOW PARAMETER UNDO;
    NAME                        TYPE        VALUE
    --------------------------- ----------- ---------
    undo_management             string       AUTO
    undo_retention              integer      10800
    undo_suppress_errors        boolean      FALSE
    undo_tablespace             string       UNDOTBS1
     
       
    -- undo_management가 MANUAL로 되어있을경우 아래와 같이 변경하고 
    -- UNDO 테이블스페이스를 생성하고 지정합니다..
    SQL> ALTER SYSTEM SET UNDO_MANAGEMENT = AUTO
         SCOPE=SPFILE;
     
     
    -- UNDO 테이블 스페이스 생성
    SQL> CREATE UNDO TABLESPACE UNDOTBS2
         DATAFILE 'D:\oracle\oradata\oracle\UNDOTBS2.dbf' 
         SIZE 1000M;
     
     
    -- UNDO 테이블 스페이스 지정
    SQL> ALTER SYSTEM SET UNDO_TABLESPACE=UNDOTBS2
     
     
    -- UNDO_RETENTION 시간을 변경하시면 
    -- 실제 적용을 위해 5분정도 기다려야 합니다.
    SQL> ALTER SYSTEM SET UNDO_RETENTION=1800
     
     
    -- scott유저에게 DBMS_FLASHBACK EXEUCTE 권한 부여 
    SQL> GRANT EXECUTE ON DBMS_FLASHBACK TO SCOTT;  
        


    [예제] 


    1. Row Level Flashback 예제

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    select versions_startscn startscn, verstions_endscn endscn,
    versions_xid, versions_operation operation, 컬럼명
    from 테이블명 versions between scn minvalue and maxvalue
    where 조건내용;
     
    select undo_sql
    from flashback_transaction_query
    where talbe_name='테이블이름'
    and commit_scn between 시작scn and 종료scn
    order by start_timestamp desc;

        

        update 수행 오류 복구하기


        1)  update 수행

        2)  flashback 이력 조회

             TXID = 7co4de.....  opt = U 에 해당정보에 대한 데이터는 갱신된 결과이다.

             - ENDSCN 에 해당되는 row의 데이터가 이전 데이터이다.

             - 원복은 해당 table에 대한 unique key 값을 조건으로 하여 이전 데이터 형태로 갱신한다.  



        



    2. Table Level Flashback 예제
      가. scn 번호로 flashback

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    conn / as sysdba
     
    grant dba to scott;
     
    conn scott/tiger
     
    create table test01 (no number);
     
    insert into test01 values (1);
     
    commit;
     
    insert into test01 values (2);
     
    commit;
     
    insert into test01 values (3);
     
    commit;
     
    select from test01;
     
    -- 현재 scn 메모
    select current_scn from v$database;
     
    -- 잘못된 업데이트 문장 수행
    update test01 set no=10;
     
    commit;
     
    select from test01;
     
    -- 앞서 scn 메모한 곳으로 flashback
    flashback table test01 to scn 'scn번호';
     
    alter table test01 enable row movement;
     
    flashback table test01 to scn 'scn번호';
     
    select from test01;



      나. timestamp로 flashback

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    conn scott/tiger
     
    create table test02 (no number(3));
     
    insert into test02 values (1);
     
    commit;
     
    insert into test02 values (2);
     
    commit;
     
    insert into test02 values (3);
     
    commit;
     
    select from test02;
     
    -- 잘못된 업데이트 문장 수행
    update test02 set no=10;
     
    commit;
     
    select from test02;
     
    -- 5분전으로 flashback
    flashback table test02 to timestamp (systimestamp - interval '5' minute);
     
    alter table test02 enable row movement;
     
    -- 테이블이 생성되기 이전시점이라서 오류발생
    flashback table test02 to timestamp (systimestamp - interval '5' minute);
                    *
    ERROR at line 1:
    ORA-01466: unable to read data - table definition has changed
     
    -- 1분전으로 flashback
    flashback table test02 to timestamp (systimestamp - interval '1' minute);
     
    -- 원하는 데이터가 아님
    select from test02;
     
            NO
    ----------
            10
            10
            10
     
    -- 200초 이전으로 되돌아감
    flashback table test02 to timestamp (systimestamp - interval '200' second);
     
    -- 원하는 데이터 발견
    select from test02;
     
            NO
    ----------
             1
             2
             3



      다. drop 된 테이블 복구

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    conn scott/tiger
     
    -- 테이블 생성
    create table test01 (no number);
     
    desc test01
     
    -- 테이블 삭제
    drop table test01;
     
    select from test01;
     
    -- 테이블 복구
    flashback table test01 to before drop;
     
    select from test01;
     
    -- 테이블 완전 삭제
    drop table test01 purge;
     
    -- 테이블 생성
    create table test02 (no number);
     
    -- 테이블 삭제
    drop table test02;
     
    -- 휴지통 확인
     show recyclebin;
     
    -- 테이블 확인(BIN$ 로 시작하는 테이블 존재 확인가능)
    select from tab;
     
    -- 휴지통에서 원하는 테이블만 삭제
    purge table test02;
     
    -- 휴지통에서 삭제됐는지 확인
    show recyclebin
     
    -- 휴지통에 있는 모든 테이블 삭제
    purge recyclebin;
     
    -- 휴지통 확인
    show recyclebin
     
    -- 현재 세션에서 휴지통 기능 비활성화
    alter session set recyclebin=off;


    참고 : 휴지통(recyclebin)은 사용자마다 할당되며, 다른사용자의 휴지통은 접근 할 수 없다.

      라. foreign key 제약조건(consraint)으로 묶인 테이블의 삭제 및 복구
         foreign key로 묶인 테이블을 drop 후 flashback 하게 되면, 수동으로 다시 제약조건을 생성해 줘야 한다.



    4. Database Level Flashback 예제


      가. 필요시점
        - truncate table 장애 복구 시
        - 특정 시점으로 전체 데이터베이스 되돌릴 때
      나. 전통방식의 백업/복구와의 비교
        - 전통방식에 비해 복구 속도가 빠름(datafile을 restore 하는 과정이 없음)
        - 전통방식이 백업데이터, archivelog, redolog 파일을 이용하지만, flashback는 flashback log 사용
        - 전통방식의 복구의 경우 특정시점으로 복구하였으나 원하는 결과가 나타나지 않아 다른시점으로 변경해야 하는 경우 모든 작업을 처음부터 다시해줘야 했으나, flashback 방식은 언제라도 원하는 시점으로 되돌아 갈 수 있음  
      다. 사전 환경설정
        - parameter 파일에 db_flashback_retention_target 설정
        - mount 단계에서 flashback on 설정

    $ vi $ORACLE_HOME/dbs/inittest.ora

    db_flashback_retention_target=30


    1
    2
    3
    4
    5
    6
    7
    8
    9
    startup mount
     
    alter database archivelog;
     
    alter database flashback on;
     
    select flashback_on from v$database;
     
    alter database open;



      라. truncate table 된 데이터 복구

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    conn scott/tiger
     
    create table test03 (no number);
     
    insert into test03 values (1);
     
    commit;
     
    insert into test03 values (2);
     
    commit;
     
    insert into test03 values (3);
     
    commit;
     
    select from test03;
     
    -- 잘못된 truncate 명령어 발생
    truncate table test03;
     
    select from test03;
     
    -- 복구시작
    conn /as sysdba
     
    shutdown immediate
     
    startup mount
     
    flashback database to timestamp (systimestamp - interval '5' minute);
     
    alter database open;
     
    alter database open resetlogs;
     
    select from scott.test03;
     
    -- 원하는 데이터가 없어 다른 시점으로 재시도
    shutdown immediate;
     
    startup mount
     
    flashback database to timestamp (systimestamp - interval '10' minute);
     
    alter database open resetlogs;
     
    select from scott.test03;



      마. 업데이트를 통한 복구

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    -- 업데이트 전 시간을 지정하여 변경 전 데이터를 확인
    SELECT *
      FROM TABLE_NAME
           VERSIONS BETWEEN TIMESTAMP TO_TIMESTAMP('2015-02-25 16:10:00''YYYY-MM-DD HH24:MI:SS')
                                  AND TO_TIMESTAMP('2015-02-25 16:11:00''YYYY-MM-DD HH24:MI:SS')
     WHERE ID = 'test';
     
    -- 변경 전 데이터로 업데이트
    UPDATE TABLE_NAME
          SET COL1= '변경전 데이터'
     WHERE ID = 'test';


    참고 :Flashback Data Archive
    11g의 새로운 기능으로 Undo segment의 commit 데이터를 특정 테이블스페이스에 archive한다. 
    10g이하 버전에서는 다른사용자에 의해 undo segment가 덮어 쓰여지면 flashback 할 수 없는 상황이 발생하였으나, 11g에서는 이 기능을 통해 undo segment가 덮어 쓰여지기전 해당 undo segment를 별도의 파일에 archive 함으로써, 복구를 원하는 시점으로 데이터를 flashback 할 수 있게 되었다.





    ***. 추가정보***

    The following script creates a new tablespace, then creates two flashback data archives using the CREATE FLASHBACK ARCHIVE command. The first is limited in size to 10Gig with a retention period of 1 year, while the second has an unlimited quota and a retention period of 2 years.

    CREATE TABLESPACE fda_ts
      DATAFILE '/u01/app/oracle/oradata/DB11G/fda1_01.dbf'
      SIZE 1M AUTOEXTEND ON NEXT 1M;
    
    CREATE FLASHBACK ARCHIVE DEFAULT fda_1year TABLESPACE fda_ts
      QUOTA 10G RETENTION 1 YEAR;
    
    CREATE FLASHBACK ARCHIVE fda_2year TABLESPACE fda_ts
      RETENTION 2 YEAR;

    Management of flashback archives falls into three distinct categories.

    • Tablespace management.
      -- Set as default FBA
      ALTER FLASHBACK ARCHIVE fba_name SET DEFAULT;
      
      -- Add up to 10G of the specified tablespace to the specified flashback archive.
      ALTER FLASHBACK ARCHIVE fba_name ADD TABLESPACE ts_name QUOTA 10G;
      
      -- Add an unlimited quota of the specified tablespace to the specified flashback archive.
      ALTER FLASHBACK ARCHIVE fba_name ADD TABLESPACE ts_name;
      
      -- Change the tablespace quota to 20G.
      ALTER FLASHBACK ARCHIVE fba_name MODIFY TABLESPACE ts_name QUOTA 20G;
      
      -- Change the tablespace quota to unlimited.
      ALTER FLASHBACK ARCHIVE fba_name MODIFY TABLESPACE ts_name;
      
      -- Remove the specified tablespace from the archive.
      ALTER FLASHBACK ARCHIVE fba_name REMOVE TABLESPACE ts_name;
    • Modifying the retention period.
      ALTER FLASHBACK ARCHIVE fba_name MODIFY RETENTION 2 YEAR;
    • Purging data.
      -- Remove all historical data.
      ALTER FLASHBACK ARCHIVE fba_name PURGE ALL;
      
      -- Remove all data before the specified time.
      ALTER FLASHBACK ARCHIVE fba_name PURGE BEFORE TIMESTAMP (SYSTIMESTAMP - INTERVAL '1' DAY);
      
      -- Remove all data before the specified SCN.
      ALTER FLASHBACK ARCHIVE fba_name PURGE BEFORE SCN 728969;

    Archives are removed using the DROP FLASHBACK ARCHIVE command, which drops the archive and all its historical data, but doesn't drop the associated tablespace.

    DROP FLASHBACK ARCHIVE fba_name;

    To enable flashback archiving on a specific table the user must have the FLASHBACK ARCHIVE object privilege on the specific flashback archive used. To try this out create a test user and grant the object privilege on the first flashback archive created earlier.

    CONN sys/password AS SYSDBA
    
    CREATE USER fda_test_user IDENTIFIED BY fda_test_user QUOTA UNLIMITED ON users;
    
    GRANT CONNECT, CREATE TABLE TO fda_test_user;
    GRANT FLASHBACK ARCHIVE ON fda_1year TO fda_test_user;

    If we connect to the test user we are able to create a table and associate it with the default flashback archive as follows.

    CONN fda_test_user/fda_test_user
    
    CREATE TABLE test_tab_1 (
      id          NUMBER,
      desription  VARCHAR2(50),
      CONSTRAINT test_tab_1_pk PRIMARY KEY (id)
    )
    FLASHBACK ARCHIVE;

    If we try to create a similar table, but point it at the second archive it fails, as we have no privileges on it.

    CONN fda_test_user/fda_test_user
    
    CREATE TABLE test_tab_2 (
      id          NUMBER,
      desription  VARCHAR2(50),
      CONSTRAINT test_tab_2_pk PRIMARY KEY (id)
    )
    FLASHBACK ARCHIVE fda_2year;
    CREATE TABLE test_tab_2 (
    *
    ERROR at line 1:
    ORA-55620: No privilege to use Flashback Archive
    
    
    SQL>

    The ALTER TABLE command allows existing tables to have flashback archiving switched on or off.

    -- Enable using the default FBDA.
    ALTER TABLE table_name FLASHBACK ARCHIVE;
    
    -- Enable using specific FBDA.
    ALTER TABLE table_name FLASHBACK ARCHIVE fda_name;
    
    -- Disable flashback archiving.
    ALTER TABLE table_name NO FLASHBACK ARCHIVE;


    2. 트렌잭션과 락.
    - 동시성제어 : 커밋된시점 추적10g pseudo 컬럼   
      ora_rowscn timestamp 자체관리(SCN)

     Create table T ROWDEPENDENCIES ...
     Scn_to_timestamp 함수 5일만 제공(동시성제어)

    - pragma autonomous_transaction;
      메인 트랜잭션 영향 주지않고 서브트랜잭션 커밋
      Autonomous 트랜잭션이라고 한다.

    - 선분이력 정합성 유지 : 상위엔티티 (고객)
       select 고객Id from 고객
       Where 고객id=1 for update nowait
     
    3. 오라클성능관리
    - explain plan >> autotrace >> Sql트레이스
      >> SQL변경.힌트 >> 반정규화.집계테이블

    - 인덱스구조변경. 통계정보 실행계획변경 대비
       현재 실행계획 확보를 위해 sql_plan_repository
       Plan_table에 실행계획 저장
      

    - 실행계획 및 통계 확인
      SET AUTOTRACE  traceonly 
                                       [explain|statistics]
    - 권한부여 (실행통계)
       V_$sesstat. V_$statname. V_$mystat
       dba. select_catalog_role
       SQL> grant plustrace to scott;

    - alter session set_trace = true[false] ;
      user_dump_dest 파라미터 위치 trc파일 저장

    . TKProf 유틸리티 사용해서 보기
      tkprof xxx.trc report.prf sys=no


    - 이벤트 트레이스 : 바인드변수.대기이벤트현황


    - AWR (automatic workload repository)
      .direct memory access (DMA) 방식 SGA
       엑세스
      .1시간 주기. 1주일보관
      . SQL> @? /rdbms/admin/sure port;
      . 11g colored SQL : top SQL 미포함 시 추가
        수집되도록 마크하는 기능

     .select * from table (dbms_xplan.display_awr ('ge72gsye7d7aj',null,null, 'ALL')
       >>  dbms_xplan.display_cursor

    - ASH (active session history)

     V$active_session_history  정보가 없을때..
      >> dba_hist_active_sess_history (AWR)

    - V$sql 집중 튜닝 대상 SQL선정및 튜닝전후 비교용

      P.228 참조

    4. 라이브러리 캐시 최적화
     - 커서캐싱기법 Parse Call 부하 감소시키기
       .바인드변수 사용 여부(static.dynamic 의미없음)
       .애플리케이션 커서 : 커서를 루프 완료 후 닫음
       .PL/SQL 사용 : 커서캐싱 자동으로됨.
     - static 구현 방법
       .In-list 항목 가변적이고 갯수가 많을때
         AND INSTR  (:in list, 분류코드) > 0

      <방법 3>
      FROM dual
      Connect by level <= length (:inlist)/2

    5. Call 최소화
     - one-sql 예) 행/열 .. pivot (cross Table)
    - IOT(index organizes table)
         >> Create table .... organization index;
      . 크기가 작고 NL조인반복 (코드)
      . 폭이 좁고 row수많은 m:m association 테이블
      . 넓은범위 검색용(like. between)
      . 입력/조회 패턴이 다른 테이블 : 일자별등록     
        조회는 사원별(pk사번+일자)
    - rownum 필터 시 조건인덱스 사용 : stop key
    - 페이징 : 이전 마지막값+rowid 과 union all 이용
    - in > between > like 순 효율(코드값 table)
    - 선분이력 [고객번호+시작일자+종료일자]
                 or [고객번호+종료일자+시작일자]

          >> index_desc (시작+종료)
               and rownum <= 1 추가
     - 순차적 증가 컬럼 인덱스는 항상 우측 블록으로
       값생성
        Buffer busy waits. gc buffer busy
        >> index 해시파티셔닝 > reverse Key

    2. Join 원리와 활용
       - Merge into .. using select .. on
         When matched then update
         set ...
         where ... ;
          << bypass_ujvc 힌트 대체할 수 있음.
      - 사용 사례 많음.

     - 결과 건수는 작은데 조인 시 블록I/O 많이 발생
       >> JOIN 시 인덱스만을 이용하고 결과 Table 읽기

     ☆해쉬조인. rowid. 인덱스를 위한 table 중복 사용

    3. 옵티마이져 원리
     - 통계정보
       테이블별 로우체인(로우마이그레이션) 발생 현황을
       주기적으로 진단 수치증가 테이블 찾기
       Select * from v$start at
       Where name in ('table fetch by rowid',
                                    'table fetch continued row');
     -
    5. 소트튜닝
      - sort area 사용 크기 적게 SQL 작성
      - 인덱스 sort. Group by. 이용
      - max 대신 rank() rnum=1 이용하기


    6. 파티셔닝
     - DML 경합

    + Recent posts