상황에 따라 적절한 조인 방법을 사용하는 것도 SQL 튜닝에 중요한 요소이다.
참고 사항
- TABLE ACCESS BY INDEX ROWID 단계는 인덱스를 사용하여 ROWID를 얻고, 이것으로 테이블 레코드에 빠르게 접근한다. 이미 인덱스나 데이터가 버퍼에 로드되어있으면 디스크 I/O가 줄어들고 성능이 향상된다.
- Buffers는 쿼리 실행 단계에서 메모리(버퍼 캐시)로 읽어온 데이터 블록의 수이다. 이는 디스크 I/O로 메모리에 로드한 데이터 블록 수이다. 만약 이미 데이터가 버퍼에 올라와있다면 Buffers는 0이다.
-- xplan.sql
set linesize 200
set pagesize 1000
SELECT *
FROM table(dbms_xplan.display_cursor(null,null,'ALLSTATS LAST +hint_report')) ;
Nested Loop Join
- Driving 테이블과 Probe 테이블 선정:
- 조인할 두 테이블 중 하나를 Driving 테이블로, 다른 하나를 Probe 테이블로 선정합니다. 보통 Driving 테이블은 작은 테이블로 선택됩니다.
- Driving 테이블 스캔:
- Driving 테이블의 각 행을 하나씩 읽습니다. 이 과정은 Driving 테이블의 전체를 스캔하는 것을 포함합니다.
- 각 행에 대해 Probe 테이블 스캔:
- Driving 테이블의 현재 행을 기준으로 Probe 테이블을 스캔합니다. 이 과정에서 각 Probe 테이블의 행을 순회하여 조인 조건에 맞는 행을 찾습니다.
- 조인 결과 생성:
- Driving 테이블의 현재 행과 Probe 테이블에서 조건에 맞는 행을 조인하여 결과를 생성합니다.
- 다음 Driving 테이블 행으로 이동:
- Driving 테이블의 다음 행으로 이동하여 위의 과정을 반복합니다.
Driving 테이블 스캔 - INDEX SCAN / FULL TABLE SCAN
Probe 테이블 스캔
- 키 컬럼이 인덱스인 경우, Driving 테이블의 상수 행 키 값에 대응되는 인덱스 확인 -> ROWID로 데이터 블록 버퍼에 로드 -> 조건 필터링 후 JOIN (데이터 블록은 한번 버퍼에 올라가면 추가 I/O 없이 사용)
- 키 컬럼이 인덱스가 아닌 경우, PROBE 테이블을 INDEX/FULL TABLE SCAN -> Driving 테이블의 상수 행 키 값에 대응되는 PROBE 테이블 키 값 확인(모든 행을 SCAN) -> 조건 필터링 후 JOIN
Single Block I/O -> 내부 루프에서 랜덤 액세스, Single Block I/O가 발생할 수 있음
소량의 데이터(OLTP)에서 유리 : Driving 테이블이 작고, Probe가 Index Scan이 가능하다면 적은 I/O로 조인 가능. Sort Merge Join에서는 일단 두 테이블을 모두 I/O 해야함
대량의 데이터(batch)에서 큰 부하가 발생 (Loop 횟수가 상당히 증가 -> Probe 테이블 스캔이 많아질 수 있다.)
결국 Driving 테이블의 크기가 중요하다.
ALTER SESSION SET statistics_level = ALL ;
SELECT TABLE_NAME, INDEX_NAME, COLUMN_NAME, COLUMN_POSITION
FROM ALL_IND_COLUMNS
WHERE TABLE_NAME in ('DEPARTMENTS', 'EMPLOYEES')
ORDER BY TABLE_NAME, INDEX_NAME, COLUMN_POSITION;
-- before oracle 11g : use prefetch
SELECT /*+ leading(d) use_nl(e) index(e empl_deptno_ix) nlj_prefetch(e) */
d.department_id, d.department_name, e.last_name, e.salary
FROM departments d, employees e
WHERE d.department_id = e.department_id;
@xplan
-- oracle 11g : use batching
SELECT /*+ leading(d) use_nl(e) index(e empl_deptno_ix) */
d.department_id, d.department_name, e.last_name, e.salary
FROM departments d, employees e
WHERE d.department_id = e.department_id;
@xplan
Driving 테이블인 departments를 풀 테이블 스캔하면서 3개 블록을 버퍼에 올렸다. Nested Loop 방식으로 departments 테이블을 외부 순회하고, EMPL_DEPTNO_IX 인덱스를 내부 순회하며 상수 departments_id와 equal 조건으로 필터링한다. Starts는 해당 단계가 실제로 실행된 횟수를 나타내며 외부 순회가 일어난 횟수인 27번만큼 INDEX RANGE SCAN을 수행했다. 각 외부 순회 단계에서 JOIN 작업이 일어난다.
이후, JOIN된 테이블을 외부 순회하면서 EMPL_DEPTNO_IX의 ROWID로 employees 테이블에 랜덤 액세스를 수행한다.
11g 버전 이전에는 prefetch 방식을 적용하고 11g 버전부터는 batching 방식을 사용한다. 두 방식의 절차는 동일하다고 볼 수 있지만 Probe 테이블에 대한 I/O에서 차이가 존재한다.
prefetch 방식을 적용하기 위해서는 nlj_prefetch 힌트를 작성하면 되며 Nested Loop를 통해 Driving 테이블과 Probe 테이블의 인덱스를 조인한 후, Probe 테이블에 효율적으로 액세스할 수 있도록 ROWID에 대해 reorganize한다. 이후 Probe 테이블에 ROWID로 랜덤 액세스를 할때 대상 블록 뿐 아니라 추후에 I/O가 필요할 것이라고 예상되는 블록도 같이 액세스한다.
batching 방식은 Probe 테이블에 랜덤 액세스할 때만 차이가 있는데 vector I/O를 통한 배치 방식으로 Multiple physical I/O를 병렬로 처리하여 I/O의 latency를 줄이고자 한다.
실행 계획에서 prefetch 방식은 NESTED LOOPS 위에 Probe 테이블에 대한 액세스가 표시되며 batch 방식은 두개의 NESTED LOOPS로 표현된다.
Hash Join
>>>>>>> Hash Join
> /*+ use_hash(a b) */
> Driving 테이블이 소량일 때 성능 극대화(?)
> 다만 Hash join은 조인조건이 Equal일때만 가능, 앞의 조인은 Non-Equal 조인도 사용 가능
> Driving 테이블을 메모리에 올리고 키값을 해시 매핑으로 해시맵으로 으로 만듦(키: 해시값 쌍). 해시값을 ROWID처럼 사용한다.
> Nested Loop Join과 Sort Merge Join은 데이터 자체를 올리고 처리
> Hash Join은 Driving 테이블의 키를 해시맵으로 만들고 JOIN을 수행하므로
> 해시 테이블은 PGA / UGA / Workarea의 공간(해시 Area)을 사용하고 해시맵을 만들때 CPU를 사용하기때문에 메모리 사용량과 CPU 사용량이 많음
> Nested Loop 조인에서의 루핑, Sort Merge Join의 정렬 작업이 부담스러울 때, 초대용량 테이블 조인시 권장된다.
> Parallel 쿼리와 함께 사용시 수행속도가 극대화
> 응답시간(Response Time)이 중요하다면 Nested Loop Join이 유리 -> 온라인 프로그램에서는 권장 안함
> Hash Table: Build Input(Driving) 테이블을 담아두는 임시 공간(Workarea)
> 일반적으로 크기가 작은 테이블이 Build Input(Driving) 테이블로 정해짐
> 해시 탐색: Probe(후행) 테이블의 레코드를 순회하면서 해시 함수를 사용해 해시 테이블에서 일치하는 레코드를 찾는다.
> 매칭된 레코드를 결합하여 조인 결과를 생성
===================================================================================
08. 조인 튜닝 방법론
상황에 따라 적절한 Join 방법을 사용하는 것도 SQL 튜닝에 중요한 요소이다.
해당 내용은 (참고자료/JOIN_SUBQUERY_기본이론) 위주
>>> 참고 사항
V$SQL_PLAN 뷰에서 STARTS 컬럼은 해당 SQL 실행 계획 단계가 실행된 횟수를 나타냅니다. 즉, 이 컬럼은 특정 실행 계획 단계가 얼마나 많은 번 수행되었는지를 보여줍니다. 이 정보는 쿼리 성능 분석 및 최적화에 유용합니다.
>>>>>>> 조인 튜닝
> 조인 종류: Nested Loop Join, Sort Merge Join, Hash Join, Outer Join
>>>>>>> Nested Loop Join
> Driving 테이블이라는 개념이 존재
> Driving 테이블은 먼저 액세스하는 테이블이고, 이 테이블이 중요
> 루핑을 통해 조인 수행
>> D 테이블의 레코드를 읽고 키 값을 가지고 후행(Inner) 테이블에 일치하는 행을 찾음
>> D 테이블에서 검색되는 범위가 좁은게 성능을 위해 중요
>> Single Block I/O: 디스크에서 블락을 메모리(버퍼 캐시)에 올린 후, 조인 작업을 수행하는데 디스크에서 단일 블락만 메모리에 올림. 따라서 대규모 작업에서는 I/O
>> Multi Block I/O 불가한 것이 Nested Loop 조인의 단점이라 대규모 조인에서는 불리할 수 있음
>> D, 혹은 I 테이블에 인덱스 사용 가능하다면 옵티마이저가 보통 Nested Loop 조인을 선택
>> 소량의 데이터 처리에서 유리 (D 테이블 관점에서). 테이블 액세스가 필요하다면 Random Access.
>> 결과적으로 소량 데이터(Online)에서는 유리하나 대량 데이터(batch)에서는 큰 부하
> hint로 /*+ use_nl(a b) */ 사용, 선/후행 테이블을 결정하진 않음
> D, I 테이블의 결정은 힌트로 넣거나, 아닌 경우 옵티마이저가 선택
> 조인식 : where a.deptno = b.deptno, 서로 다른 테이블의 컬럼과 컬럼을 비교한것
> 옵티마이저가 힌트에 first_rows를 사용하면 옵티마이저가 하나의 레코드를 빨리 가져오는 실행 계획을 선택하는 경향이 있다. ALL_ROWS는 단위 시간당 처리량(쓰루풋) 관점에서 실행 계획을 선택하는 경향이 있다.
> 많은 양의 데이터 조인 시 랜덤 액세스 증가
SELECT /*+ optimizer_features_enable('8.1.7')
use_nl(d e)
index(e empl_deptno_ix)
no_nlj_prefetch(e) */
d.department_id, d.department_name, e.last_name, e.salary
FROM departments d, employees e
WHERE d.department_id = e.department_id ;
@xplan
> 루핑 (INDEX RANGE SCAN -> TABLE ACCESS BY INDEX ROWID)가 27회 발생
SELECT /*+ optimizer_features_enable('9.2.0')
leading(d) use_nl(e) index(e empl_deptno_ix) nlj_prefetch(e) */
d.department_id, d.department_name, e.last_name, e.salary
FROM departments d, employees e
WHERE d.department_id = e.department_id ;
@xplan
> leading(d) use_nl(e) 힌트를 통해 d를 driving 테이블로 설정하고 nested loop join 사용
> nlj_prefetch(e) 힌트를 통해 employees 테이블을 27번 검색하는것이 아닌 employees.department_id 인덱스를 우선 잡아놓고 배치로 employees 테이블을 scan
> 이 경우에는 employees를 한번에 메모리에 올리고 1회만 검색을 수행하는 것
SELECT /*+ leading(d) use_nl(e) nlj_batching(e) */
d.department_id, d.department_name, e.last_name, e.salary
FROM departments d, employees e
WHERE d.department_id = e.department_id ;
@xplan
> 여기서는 앞의 실행 계획과 다른 것은 NESTED LOOPS를 두번한다. 메모리에 한번에 올리는 것이 아님. 메모리에 올라온 인덱스를 기반으로 해당 위치만 찾아서 EMPLOYEES 테이블에 접근
>>>>>>> Nested Loop Join 동작
from emp a, dept b
where a.deptno = b.deptno
> emp가 D 테이블, dept.deptno가 인덱스(PK)인 경우
>> emp에서 풀 테이블 스캔(multi block I/O)
(루핑 시작)
>> emp 엔트리 deptno에 대해서 대응되는 dept.deptno 인덱스 확인
>> 해당 rowid로 dept 테이블 랜덤 액세스
(루핑 끝)
>> 순서: EMP TABLE ACCESS FULL -> INDEX UNIQUE SCAN PK_DEPT -> TABLE ACCESS BY INDEX ROWID
>> 이런 경우 INDEX UNIQUE SCAN과 TABLE ACCESS BY INDEX ROWID의 Starts 횟수가 엄청 늘어남. (반복)
>> 여기서는 루핑이 테이블-테이블간 일어남
>> STARTS 컬럼은 SQL 실행 계획의 각 단계가 실제로 실행된 횟수를 나타냅니다.
from emp a, dept b
where a.deptno = b.deptno
> dept가 D테이블, emp.deptno가 인덱스인 경우
>> dept에서 풀 테이블 스캔(multi block I/O)
(루핑 시작)
>> dept 엔트리 deptno에 대해서 대응되는 emp.deptno 인덱스 확인
>> 해당 rowid로 emp 테이블 랜덤 액세스
(루핑 끝)
>> 순서에서 루핑이 emp 풀 스캔 후, emp.depno 인덱스하고만 일어나고, 루프가 끝난 후 index rowid로 emp 테이블 스캔
> 옵티마이저는 보통 조인식 이외의 조건이 걸려있는 테이블을 Driving 테이블로 선정함
> 하기 실행 계획이 있다. (A-B 각 테이블에 대한 스캔 방식)
>> Full - Unique
>> Full - Range
>> Range - Range
>> Unique - Unique
>>>>>>> Sort Merge Join
> 조인되는 컬럼에 인덱스 존재 유무가 문제되지 않음
> 중요한 것은 Join되는 대상에 대해서는 항상 SORTING을 하고 JOIN을 진행한다.
> 처리량이 많은 전체 범위(batch)에 주로 사용
>> Nested Merge Join는 루핑을 함, 하지만 sort merge join은 각 테이블을 쭉 읽고(메모리에 올림), JOIN을 수행
>> Nested Merge Join의 단점은 Driving 테이블의 검색 범위가 많으면 많은 루핑이 일어난다는 것, 이런 경우에는 Sort Merge Join이 유리할 수 있다.
> WHERE 조건이 없으면 양 테이블의 풀 테이블 스캔을 적용
> 읽어야되는 데이터가 많으면 Sort Merge Join을 사용하는 것이 좋을 수 있다.
> 조인된 컬럼을 기준으로 SORTING해야할때 Sort Merge Join이 유리
> /*+ use_merge(a b) */
> 테이블별로 데이터 블락을 스캔하고 나중에 병합이기 때문에 루핑이 없다
> 정렬된 두 테이블의 커서가 각각 밑으로 내려오면서 조건에 의해 조인되므로 각 테이블을 한번 훑는다고 생각하면 됨.
> Range -> Merge <- Range
select /*+ use_merge(a b) */ a.dname, b.empno, b.ename
from dept a, big_emp b
where a.deptno = b.deptno
and a.deptno between 10 and 20
and b.hiredate between to date( , yyyy ) '20020101', 'yyyymmdd'
) and
to_date('20021231', 'yyyymmdd')
order by b.deptno ;
>> a에서 deptno에 대한 INDEX 스캔하고 랜덤 엑세스하고 depno로 정렬 (deptno로 랜덤 엑세스하므로 MERGE 전에 SORTING 작업이 없다. 하지만 Used-Mem이 차있는 이유는 정리 작업 등의 소규모 작업이 이루어짐)
>> b에서 hiredate에 대한 INDEX 스캔하고 랜덤 엑세스하고 depno로 정렬
>> 이후 둘을 MERGE JOIN
> Full -> Merge <- Full
select /*+ use_merge(a b) */
a.dname, b.empno, b.ename
from dept a, big_emp b
where a.deptno = b.deptno
and b l 000 d
b.sal > 1000 ;
>> a에 대해 풀 테이블 스캔후 deptno에 대한 SORTING
>> b에 대해 풀 테이블 스캔후 depno에 대한 SORTING
>> 둘을 MERGE JOIN
> Non equi join에서는 sort merge join이 유리할 수 있음. 데이터를 한번에 올려서 보는 것이기 때문에
> sort merge join에서 leading 테이블도 중요할 수 있다. 조인의 기준이 되는 테이블이기 때문에 범위가 작은 테이블이 오는게 좋다.
>>>>>>> Hash Join
> /*+ use_hash(a b) */
> Driving 테이블이 소량일 때 성능 극대화(?)
> 다만 Hash join은 조인조건이 Equal일때만 가능, 앞의 조인은 Non-Equal 조인도 사용 가능
> Driving 테이블을 메모리에 올리고 키값을 해시 매핑으로 해시맵으로 으로 만듦(키: 해시값 쌍). 해시값을 ROWID처럼 사용한다.
> Nested Loop Join과 Sort Merge Join은 데이터 자체를 올리고 처리
> Hash Join은 Driving 테이블의 키를 해시맵으로 만들고 JOIN을 수행하므로
> 해시 테이블은 PGA / UGA / Workarea의 공간(해시 Area)을 사용하고 해시맵을 만들때 CPU를 사용하기때문에 메모리 사용량과 CPU 사용량이 많음
> Nested Loop 조인에서의 루핑, Sort Merge Join의 정렬 작업이 부담스러울 때, 초대용량 테이블 조인시 권장된다.
> Parallel 쿼리와 함께 사용시 수행속도가 극대화
> 응답시간(Response Time)이 중요하다면 Nested Loop Join이 유리 -> 온라인 프로그램에서는 권장 안함
> Hash Table: Build Input(Driving) 테이블을 담아두는 임시 공간(Workarea)
> 일반적으로 크기가 작은 테이블이 Build Input(Driving) 테이블로 정해짐
> 해시 탐색: Probe(후행) 테이블의 레코드를 순회하면서 해시 함수를 사용해 해시 테이블에서 일치하는 레코드를 찾는다.
> 매칭된 레코드를 결합하여 조인 결과를 생성
>>>>>>> Cartesian Join (Cross join)
> 조인 조건이 없는 경우, 두 테이블의 카티시안 곱으로 결과 산출
> 쓰는 경우가 없다고 보자
>>>>>>> Outer Join
> Outer Join은 보통 Nested Loop 조인으로 풀리는 경우가 많다
> 실행 계획을 보면 NESTD LOOPS 뒤에 "OUTER"가 붙음
from emp a, dept b
where a deptno = b deptno(+)
> emp 테이블의 모든 행에 대응되는 행이 dept에 없더라도 다 보여줌
> nested loop join + driving 테이블에서 매칭되지 않은 행도 포함
> (+)가 붙은 테이블이 후행(Probe) 테이블
> /*+ use_merge(a b) */로 sort merge join을 사용할 수 도 있다.
> Full Outer Join
> from emp a full outer join dept b 형식으로 작성
> UNION-ALL로 매칭되지 않는 모든 행을 포함
> full outer join을 하면 쿼리 변환기에 의해 인라인 뷰(Outer Join과 서브쿼리의 UNION ALL)로 변환
> Hash join으로 outer join을 풀 수도 있다.
> Nested Loop Join, Hash Join의 driving 테이블은 옵티마이저가 선택함
> outer join에서 nested loop, hash join을 사용할때는 기본적으로 +가 붙지 않는 쪽이 driving(building) 테이블로 사용
>>>>>>> 인덱스와 조인 실행 계획 사례 연구
SELECT d.deptno, d.dname, e.empno, e.ename, e.sal
FROM cp_dept d, cp_emp e
WHERE d.deptno = e.deptno ;
@xplan
> 해시 조인이 사용됨. 조인 컬럼에 인덱스가 없으면서 equal 조건인 경우 해시 조인이 우선시됨
SELECT /*+ use_nl(d e) */
d.deptno, d.dname, e.empno, e.ename, e.sal
FROM cp_dept d, cp_emp e
WHERE d.deptno = e.deptno ;
@xplan
> Nested Loop 사용하는 경우, 인덱스가 없으므로 풀 테이블 스캔 이후 조건 루프 반복
CREATE INDEX cp_emp_deptno ON cp_emp(deptno) ;
CREATE INDEX cp_dept_deptno ON cp_dept(deptno) ;
SELECT /*+ leading(d) use_nl(e) */
d.deptno, d.dname, e.empno, e.ename, e.sal
FROM cp_dept d, cp_emp e
WHERE d.deptno = e.deptno ;
@xplan
> index를 만들어서 nested loop 쓰면 NESTED LOOPS를 두번쓴다.
> DEPT를 풀 테이블 스캔 -> deptno 컬럼 값을 순회하면서 EMP 인덱스에서 확인 -> 해당 인덱스를 갖는 ROWID를 찾아놓음 -> 두번째 NESTED LOOP에서 ROWID 순회하며 EMP 테이블 접근
> 다만 후행 테이블은 ROWID로 조회하므로 순회는 14번하나 실제로 디스크 I/O는 1회만 발생
> prefetch가 사용되었다는 것은, 첫번째 NESTED LOOP는 인덱스와 수행, 두번째는 데이터 테이블과 NESTED LOOP하는 것
> 인덱스가 있는 경우, prefetch로 처리하는 경우가 많다.
> 후행 테이블의 조인 조건 컬럼이 인덱스가 걸려있는 경우, prefetch가 수행될 수 있다.
SELECT /*+ leading(c) use_hash(s) no_index(c(cust_id)) */ SUM(amount_sold)
FROM customers c, sales s
WHERE c.cust_id = s.cust_id ;
@xplan
> 추가 조건이 없으므로 인덱스를 막아놓는게 좋을 수 있다. 어차피 풀 테이블 스캔을 해야되는데 인덱스를 거쳐서 풀 스캔하는 것보다 처음부터 풀 테이블 스캔하는게 효율적
> 풀 테이블 스캔을 어차피 해야할때는 Multi-Block I/O를 지원하는 풀 테이블 스캔을 하도록 Index 스캔을 막는게 낫다.
SELECT /*+ leading(s) use_hash(c) no_index(c(cust_id)) */ SUM(amount_sold)
FROM customers c, sales s
WHERE c.cust_id = s.cust_id ;
@xplan
> driving 테이블을 바꿈. Used-Mem의 차이가 남. 이전보다 훨씬 많은 Workarea를 잡음.
> 이는 해시 테이블 사이즈 차이가 크다는 것. 즉, 드라이빙 테이블 데이가 많으므로 해시 테이블 사이즈도 커짐을 의미
>>>>>> 문제. 다음 문장을 최적화할 수 있는 조인 순서 및 조인 방법은?
> 1. driving 테이블 선택이 우선이다. employees 조건은 10건, departments는 21건으로 employees 테이블을 드라이빙으로 하는게 좋은 조건이다.
SELECT /*+ leading(e) use_nl(d) index(e(job_id)) index(d(department_id)) */
e.employee_id, e.last_name, e.job_id, d.department_id, d.department_name
FROM employees e, departments d
WHERE e.department_id = d.department_id
AND d.location_id = 1700
AND e.job_id IN ('FI_ACCOUNT','PU_CLERK') ;
@xplan
> employees 테이블의 인덱스를 사용해서 조회, 따라서 Buffers가 3으로 작다
> 후행 테이블의 인덱스를 사용하므로 prefetch 방식이 적용될 수 있다.
>> 선행 테이블과 후행 테이블의 인덱스 간의 JOIN이 우선 일어난다.
>> prefetch가 뭔지 모르겠다. (166 페이지 관련 내용 확인)
>> 강사님은 prefetch 기능을 후행 테이블의 인덱스로 1차로 거르고 2차로 데이터에 접근하는 필터 효과가 있어서 좋다고 하심
SELECT /*+
leading(d) use_nl(e) index(e empl_deptno_ix) nlj_prefetch(e) */
d.department_id, d.department_name, e.last_name, e.salary
FROM departments d, employees e
WHERE d.department_id = e.department_id ;
@xplan
SELECT /*+ leading(d) use_nl(e) */
d.department_id, d.department_name, e.last_name, e.salary
FROM departments d, employees e
WHERE d.department_id = e.department_id ;
@xplan
위 두 구문에서 그 차이가 있음.
> Outer Join
SELECT /*+ leading(d) use_hash(e) */
d.department_id, d.department_name, e.last_name, e.salary
FROM departments d, employees e
WHERE d.department_id (+) = e.department_id
AND d.location_id (+) = 1700 ;
@xplan
> (+)가 있는 테이블이 후행 테이블이 됨. -> 즉, 문법 상으로 옵티마이저가 driving 테이블을 선택 (힌트를 무시) -> 그런데 여기서 emp가 driving이 상대적으로 많이 커서 hash 테이블이 커진다.
> swap_join_inputs을 사용하면 문법에 관계 없이 driving 테이블을 변경할 수 있다. . 단 hash join에서만 사용
> adaptive plan
> 기본적으로 비활성. 활성화하면 최적의 플랜을 잡아놓고 서브 플랜도 잡아놓음. 실제 런타임시에 그 순간의 통계 정보를 반영하여 미리 잡아놓은 플랜들의 Cost를 재비교하여 그 중 최적 플랜으로 수행 (실제 런타임 때 실제 실행 계획은 1개임)
> 켜놓으면 sql 명령이 느려질 수 있다. 통계 정보가 계속 변화하면 켜놓을 수 있다.
> adaptive plan이 활성화 되어있으면 -로 표시되는 애들도 있음. (서브플랜)
> 실제 실행하여 실제 실행 계획을 볼 때 PLAN_TABLE 결과와 다를 수 있음.