CBQT - JPPD(Join Predicate Push Down) CASE3(계속)

Download Report

Transcript CBQT - JPPD(Join Predicate Push Down) CASE3(계속)

쿼리 변환
(Query Transformation)
www.bysql.net
A조
Logical Optimization vs Physical Optimization
Physical Optimization의 개념은 최적의 Access Path를 결정하고 이에 따른 최적의
조인방법, 조인순서를 결정하는 것이다.
Logical Optimization은 성능향상을 위해서 옵티마이져가 SQL을 재작성(튜닝) 하는
것으로
정의할 수 있다. 과거에는 개발자가 수동으로 SQL을 수정했다면 이제는
옵티마이져가 많은 부분을 자동으로 재 작성하게 되었다. 그래서 Logical
Optimization을 Query Transformation 이라고도 하고 Logical Optimizer는 Query
Transformer(이하 QT)라고도 불린다.
QT는 옵티마이져의 3대 Components이다
(다음 Page)
Physical Optimization은 Logical Optimization에 종속적인 개념이지 대등한 개념이
아니다. 왜냐면 Logical Optimizer가 SQL을 성능향상의 목적으로 재작성하면
Physical Optimizer는 Logical Optimizer가 작성한 틀(SQL) 내에서 최적화 할 수
밖에 없기 때문이다. Logical Optimization은 SQL 자체를 튜닝(변경)하는 것이고
Physical Optimization은 튜닝된 SQL을 물리적으로 최적화하는 것이다.
Optimizer 구조도
Optimizer Components
Parser
Query
Transformer
Heuristic
QT
Cost
Estimator
Plan
Generator
Row Source
Generator
Cost Based
QT
위 그림을 설명하면 Query Parser가 SQL을 검사하여 넘겨주면 Query Transformer
(Logical Optimizer)가 SQL을 변환시켜서 Cost Estimator(Physical Optimizer)에
넘겨준다. 이 때 Cost Estimator는 통계정보 등을 참조하여 변환되지 않은 SQL의
Cost와 변환된 SQL의 Cost를 비교하여 가장 낮은 Cost(비용)를 갖는 SQL을 찾아내어
Plan Generator에 넘겨주고 실행계획을 완성후 SQL의 결과를 Fetch한다.
QT 정보추출
QT에서 실행계획의 중요성은 매우
1.PLAN을 볼 수 있는 Tool 필요
2.일반적으로 10046 Event Trace
+ tkprof를 많이 사용함
▶제약조건이 많음
(FTP 권한, Dump Trace 구성,
Predicate정보, QB정보…)
DBMS_XPLAN.DISPLAY 또는
DBMS_XPLAN.DISPLAY_CURSOR를
사용하는 편이 유리하며 10046 이
벤트가 갖지 못한 장점이 있음
항목
DBMS_XPLAN
DISPLAY
10046
Trace
DISPLAY_CURSOR
수행결과
예측
실측
실측
SQL수행
1개
1개 이상
1개
FUNCTION
수행정보
Ⅹ
○
Ⅹ
단계별
통계치
Ⅹ
○
Ⅹ
PLAN정보
○
○
○
추가정보
○
Ⅹ
○
DBMS_XPLAN
QT 정보추출 - 10053 Event
ALTER SESSION SET EVENTS '10053 trace name context forever, level 1';
Trace File 내용
SELECT /*+ QB_NAME(MAIN) */
DEPARTMENT_ID, SALARY
FROM EMPLOYEES
WHERE (DEPARTMENT_ID= 60 AND SALARY = 4200)
OR (DEPARTMENT_ID= 60) ;
(생략…)
******************************************
----- Current SQL Statement for this session (sql_id=5yw47a9yn39v1) ----SELECT /*+ QB_NAME(MAIN) */
DEPARTMENT_ID, SALARY
FROM EMPLOYEES
WHERE (DEPARTMENT_ID= 60 AND SALARY = 4200)
OR (DEPARTMENT_ID= 60)
*******************************************
Legend
The following abbreviations are used by optimizer trace.
CBQT - cost-based query transformation
JPPD - join predicate push-down
OJPPD - old-style (non-cost-based) JPPD
CVM - complex view merging
SPJ - select-project-join
SJC - set join conversion
SU - subquery unnesting
OBYE - order by elimination
SLP - select list pruning
(중간 생략…)
*************************
Common Subexpression elimination (CSE)
*************************
CSE:
CSE bypassed on query block MAIN (#0): Disabled by parameter.
OBYE:
Considering Order-by Elimination from view MAIN (#0)
***************************
Order-by elimination (OBYE)
***************************
OBYE:
OBYE bypassed: no order by to eliminate.
CVM: Considering view merge in query block MAIN (#0)
query block MAIN (#0) unchanged
Considering Query Transformations on query block MAIN (#0)
(생략…)
ALTER SESSION SET EVENTS '10053 trace name context off';
------------------------------------------------------| Id | Operation
| Name
| Rows | Bytes |
------------------------------------------------------|
0 | SELECT STATEMENT |
|
5 |
35 |
|* 1 | TABLE ACCESS FULL| EMPLOYEES |
5 |
35 |
------------------------------------------------------Predicate Information (identified by operation id):
--------------------------------------------------1 - filter("DEPARTMENT_ID"=60 OR "SALARY"=4200 AND
"DEPARTMENT_ID"=60)
QT의 두 가지 방식
쿼리변환은 크게 2가지 형태로 작동한다.
1.휴리스틱(Heuristic) 쿼리변환
옵티마이져가 성능을 향상시키기 위해서 비용계산
SELECT COUNT(*)
FROM TAB1
ORDER BY COL1;
(Costing)과정이 필요한 경우도 있지만 특정한
경우에는 Costing과정이 필요 없기 때문에 이러한
COUNT(*)를 하는데 굳이 Order By를 해야 할까?
과정을 생략하고 바로 특정 Rule을 적용하여
이런 상황에서 Order By 부하를 없애는 가장 좋은
최적화를 진행하겠다는 의미. (30개 정도가 있음)
방법은 Order By를 수행하지 않는 것임.
Heuristic Rule임
2.비용기반(Cost Based) 쿼리변환
비용기반 쿼리변환은 QT가 쿼리블럭을 최적화 하는
방법을 정할 때 스스로 결정하지 못하고 Cost Estimator
에게 어떠한 방법이 제일 좋은 것인지 물어보고 결정하는
방법. Cost Estimator란 쿼리블럭의 Cost를 Return 해주는
모듈이며 우리가 SQL의 Plan에서 쉽게 볼 수 있는 Cost가
Cost Estimator가 계산한 값이다. (20개 정도 있음)
SELECT COUNT(*) FROM TAB1
WHERE TAB1.COL2 IN (SELECT TAB2.COL2
FROM TAB2, TABL3
WHERE TAB2.COL1=TAB3.COL1
AND TAB2.COL3=1
AND TAB3.COL4=2;
인덱스의 구성 및 드라이빙 테이블 선택 등…
서브쿼리 Unnesting - 별도 설명
서브쿼리의 분류
옵티마이져의 서브쿼리 처리방식
1.인라인 뷰(Inline View)
1.Subquery Unnesting
-FROM절에 나타나는 서브쿼리
2.중첩된 서브쿼리(Nested Subquery)
-결과집합을 한정하기 위해 WHERE절에 사용된
서브쿼리
-동일한 결과를 보장하는 조인형태로 변환하여 최적화
(서브쿼리를 인라인 뷰로 바꾸어 정상적인 조인처리)
2.Filter Operation
-원래 상태에서 메인쿼리와 서브쿼리를 각각 최적화
(이때 서브쿼리에 Filter 오퍼레이션이 발생함)
3.스칼라 서브쿼리(Scalar Subquery)
-SELECT-LIST에서 사용되는 서브쿼리
Subquery Unnesting의 이점
1.서브쿼리를 메인쿼리와 같은 레벨로 풀어낸다면 다양한 액세스 경로와 조인 메소드를
활용할 수 있다.
2.서브쿼리 Unnesting과 관련한 HINT
- UNNEST : 서브쿼리를 조인방식으로 최적화하도록 유도한다.
- NO_UNNEST : 서브쿼리를 필터 방식으로 최적화하도록 유도한다.
HQT - CSE(Common Subexpression Elimination)
1.내용 - Where 절에서 OR 사용시 중첩된 조건절은 제거하라
2.효과 - 불필요한 SQL 조건절을 찾아내어 성능항샹을 기대함
3.기타 - 해당 Parameter(“_eliminate_common_subexpr”)
ALTER SESSION SET EVENTS '10053 trace name context forever, level 1';
SELECT /*+ QB_NAME(MAIN) */
DEPARTMENT_ID, SALARY
FROM EMPLOYEES
WHERE (DEPARTMENT_ID= 60 AND SALARY = 4200)
OR (DEPARTMENT_ID= 60) ;
ALTER SESSION SET EVENTS '10053 trace name context off';
------------------------------------------------------------------------------------------------| ID | OPERATION
| NAME
| ROWS | BYTES | COST (%CPU)| TIME
|
------------------------------------------------------------------------------------------------| 0 | SELECT STATEMENT
|
|
5 |
35 |
2 (0)| 00:00:01 |
| 1 | TABLE ACCESS BY INDEX ROWID| EMPLOYEES
|
5 |
35 |
2 (0)| 00:00:01 |
|* 2 | INDEX RANGE SCAN
| EMP_DEPARTMENT_IX |
5 |
|
1 (0)| 00:00:01 |
------------------------------------------------------------------------------------------------PREDICATE INFORMATION (IDENTIFIED BY OPERATION ID):
--------------------------------------------------2 - ACCESS("DEPARTMENT_ID"=60)
[CSE IN 10053 Event]
*************************
Common Subexpression elimination (CSE)
*************************
CSE:
CSE performed on query block MAIN (#0).
(중간생략...)
finally: "EMPLOYEES"."DEPARTMENT_ID"=60 AND ("EMPLOYEES"."SALARY"=4200 OR 0=0)
HQT - CSE(Common Subexpression Elimination) 계속
▶만약, CSE 기능이 없다면…
ALTER SESSION SET "_eliminate_common_subexpr" = FALSE;
ALTER SESSION SET EVENTS '10053 trace name context forever, level 1';
SELECT /*+ QB_NAME(MAIN) */
DEPARTMENT_ID, SALARY
FROM EMPLOYEES
WHERE (DEPARTMENT_ID= 60 AND SALARY = 4200)
OR (DEPARTMENT_ID= 60) ;
ALTER SESSION SET EVENTS '10053 trace name context off';
------------------------------------------------------------------------------| Id | Operation
| Name
| Rows | Bytes | Cost (%CPU)| Time
|
------------------------------------------------------------------------------| 0 | SELECT STATEMENT |
|
5 |
35 |
3 (0)| 00:00:01 |
|* 1 | TABLE ACCESS FULL| EMPLOYEES |
5 |
35 |
3 (0)| 00:00:01 |
------------------------------------------------------------------------------Predicate Information (identified by operation id):
--------------------------------------------------1 - filter("DEPARTMENT_ID"=60 OR "SALARY"=4200 AND
"DEPARTMENT_ID"=60)
[CSE IN 10053 Event]
*************************
Common Subexpression elimination (CSE)
*************************
CSE:
CSE bypassed on query block MAIN (#0): Disabled by parameter.
(중간생략...)
finally: "EMPLOYEES"."DEPARTMENT_ID"=60 AND "EMPLOYEES"."SALARY"=4200 OR EMPLOYEES"."DEPARTMENT_ID"=60
HQT - DE(Distinct Elimination)
1.내용 - SELECT절에 UNIQUE한 컬럼이 포함되어 있는 경우 불필요한 Distinct를 제거하라
2.효과 - 불필요한 Sort Unique / Hash Unique룰 제거함으로 성능효과를 가져온다.
3.기타 - 11g의 New Feature
ALTER TABLE DEPARTMENTS DROP CONSTRAINTS
DEPT_LOC_FK;
SELECT /*+ FULL(D) */
DISTINCT D.DEPARTMENT_ID, D.LOCATION_ID
FROM DEPARTMENTS D, LOCATIONS L
WHERE D.LOCATION_ID = L.LOCATION_ID;
ALTER TABLE DEPARTMENTS ADD
(
CONSTRAINT DEPT_LOC_FK FOREIGN KEY
(LOCATION_ID)
REFERENCES LOCATIONS (LOCATION_ID)
);
------------------------------------------| Id | Operation
| Name
|
------------------------------------------| 0 | SELECT STATEMENT
|
|
| 1 | HASH UNIQUE
|
|
| 2 | NESTED LOOPS
|
|
| 3 |
TABLE ACCESS FULL| DEPARTMENTS |
|* 4 |
INDEX UNIQUE SCAN| LOC_ID_PK |
------------------------------------------Predicate Information (identified by operation id):
--------------------------------------------------4 - access("D"."LOCATION_ID"="L"."LOCATION_ID")
위의 SQL을 보면 DISTINCT 로 인해 PLAN에 HASH UNIQUE가 발생함
SQL을 분석해보면 SELECT LIST의 컬럼들이 UNIQUE COLUMN임을 알 수 있음
D.LOCATION_ID를 L.LOCATION_ID로 대체하면 다음과 같음
HQT - DE(Distinct Elimination)
1.내용 - SELECT절에 UNIQUE한 컬럼이 포함되어 있는 경우 불필요한 Distinct를 제거하라
2.효과 - 불필요한 Sort Unique / Hash Unique룰 제거함으로 성능효과를 가져온다.
3.기타 - 11g의 New Feature
ALTER TABLE DEPARTMENTS DROP CONSTRAINTS
DEPT_LOC_FK;
SELECT /*+ FULL(D) */
DISTINCT D.DEPARTMENT_ID, L.LOCATION_ID
FROM DEPARTMENTS D, LOCATIONS L
WHERE D.LOCATION_ID = L.LOCATION_ID;
ALTER TABLE DEPARTMENTS ADD
(
CONSTRAINT DEPT_LOC_FK FOREIGN KEY
(LOCATION_ID)
REFERENCES LOCATIONS (LOCATION_ID)
);
-----------------------------------------| Id | Operation
| Name
|
-----------------------------------------| 0 | SELECT STATEMENT |
|
| 1 | NESTED LOOPS
|
|
| 2 | TABLE ACCESS FULL| DEPARTMENTS |
|* 3 | INDEX UNIQUE SCAN| LOC_ID_PK |
-----------------------------------------Predicate Information (identified by operation id):
--------------------------------------------------3 - access("D"."LOCATION_ID"="L"."LOCATION_ID")
컬럼 하나만 바뀌었을 뿐인데 실행계획에서 HASH UNIQUE가 사라짐.
FROM절에 나열된 모든 테이블의 PK(UNIQUE)컬럼이 SELECT절에 나올 경우 DISTINCT가 제거됨
반복적으로 사용되거나 대용량 집합에서 성능향상효과를 가져올 수 있다.
DE의 활용편
SELECT /*+ */
J.*
FROM JOB_HISTORY J
WHERE (J.EMPLOYEE_ID, J.DEPARTMENT_ID)
IN (
SELECT E.EMPLOYEE_ID, E.DEPARTMENT_ID
FROM EMPLOYEES E
, DEPARTMENTS D
WHERE E.DEPARTMENT_ID = D.DEPARTMENT_ID
AND D.LOCATION_ID = 1800
);
-------------------------------------------------------------| Id | Operation
| Name
|
-------------------------------------------------------------| 0 | SELECT STATEMENT
|
|
| 1 | VIEW
| VM_NWVW_2
|
| 2 | HASH UNIQUE
|
|
| 3 |
NESTED LOOPS
|
|
| 4 |
NESTED LOOPS
|
|
| 5 |
NESTED LOOPS
|
|
| 6 |
TABLE ACCESS BY INDEX ROWID| DEPARTMENTS
|
|* 7 |
INDEX RANGE SCAN
| DEPT_LOCATION_IX |
| 8 |
TABLE ACCESS BY INDEX ROWID| EMPLOYEES
|
|* 9 |
INDEX RANGE SCAN
| EMP_DEPARTMENT_IX |
|* 10 |
INDEX RANGE SCAN
| JHIST_EMPLOYEE_IX |
|* 11 |
TABLE ACCESS BY INDEX ROWID | JOB_HISTORY
|
--------------------------------------------------------------------------------------------------------------------------| Id | Operation
| Name
|
-------------------------------------------------------------| 0 | SELECT STATEMENT
|
|
| 1 | NESTED LOOPS
|
|
| 2 | NESTED LOOPS
|
|
| 3 |
NESTED LOOPS
|
|
| 4 |
TABLE ACCESS BY INDEX ROWID| DEPARTMENTS
|
|* 5 |
INDEX RANGE SCAN
| DEPT_LOCATION_IX
|
| 6 |
TABLE ACCESS BY INDEX ROWID| JOB_HISTORY
|
|* 7 |
INDEX RANGE SCAN
| JHIST_DEPARTMENT_IX |
|* 8 |
INDEX RANGE SCAN
| EMP_DEPARTMENT_IX |
|* 9 | TABLE ACCESS BY INDEX ROWID | EMPLOYEES
|
--------------------------------------------------------------
SELECT /*+ */
J.*
FROM JOB_HISTORY J
WHERE (J.EMPLOYEE_ID, J.DEPARTMENT_ID)
IN (
SELECT E.EMPLOYEE_ID, D.DEPARTMENT_ID
FROM EMPLOYEES E
, DEPARTMENTS D
WHERE E.DEPARTMENT_ID = D.DEPARTMENT_ID
AND D.LOCATION_ID = 1800
);
Subquery가 Unnesting되어 드라이빙 집합이 되면 메인쿼리의 집합을 보존하기 위해 Default로
Distinct가 추가된다. 이 때 DE, DEUI 기능이 수행된다면 Distinct가 제거된다.
DE의 또 다른 유형 - DEUI(Distinct Elimination using Unique Index)
UNIQUE INDEX를 사용하여 INDEX UNIQUE SCAN이 발생하는 경우에는 DISTINCT를 제거하는 기능
SELECT DISTINCT
D.DEPARTMENT_ID, L.CITY, L.COUNTRY_ID
FROM DEPARTMENTS D, LOCATIONS
L
WHERE D.LOCATION_ID = L.LOCATION_ID
AND D.DEPARTMENT_ID = 10;
---------------------------------------------------| Id | Operation
| Name
|
---------------------------------------------------| 0 | SELECT STATEMENT
|
|
| 1 | NESTED LOOPS
|
|(SORT 발생X)
| 2 | TABLE ACCESS BY INDEX ROWID| DEPARTMENTS |
|* 3 |
INDEX UNIQUE SCAN
| DEPT_ID_PK |
| 4 | TABLE ACCESS BY INDEX ROWID| LOCATIONS |
|* 5 |
INDEX UNIQUE SCAN
| LOC_ID_PK |
----------------------------------------------------
LOCATION 쪽의 UNIQUE 컬럼이 SELECT절에 없음에도 불구하고 SORT(HASH) UNIQUE 가 사라졌다.
그 이유는 두 테이블 모두 INDEX UNIQUE SCAN을 사용하였기 때문에 결과가 한건 혹은 0건임을 보장한다.
따라서 DISTINCT는 제거 되어도 무방하다.
이 기능은 DE가 아니다(파라미터로 CONTROL 할 수 없고 수행 로직이 다르기 때문이다)
SELECT /*+ FULL(D) */ DISTINCT
D.DEPARTMENT_ID, L.CITY, L.COUNTRY_ID
FROM DEPARTMENTS D, LOCATIONS
L
WHERE D.LOCATION_ID = L.LOCATION_ID
AND D.DEPARTMENT_ID = 10;
----------------------------------------------------| Id | Operation
| Name
|
----------------------------------------------------| 0 | SELECT STATEMENT
|
|
| 1 | HASH UNIQUE
|
|
| 2 | NESTED LOOPS
|
|
| 3 |
NESTED LOOPS
|
|
|* 4 |
TABLE ACCESS FULL
| DEPARTMENTS |
|* 5 |
INDEX UNIQUE SCAN
| LOC_ID_PK |
| 6 |
TABLE ACCESS BY INDEX ROWID| LOCATIONS |
-----------------------------------------------------
DE는 INDEX UNIQUE SCAN과 상관없이 SELECT 절에 각 테이블의 UNIQUE 컬럼만 있으면 수행되지만
DEUI는 PLAN의 INDEX UNIQUE SCAN이라면 언제나 수행 가능하다.
반대로 UNIQUE 인덱스를 사용하지 않으면 DEUI는 절대로 수행되지 않는다.
HQT - SJ(Semi Join) : FILTER 처리
1.내용 – 서브쿼리를 Semi 조인으로 변환하라
2.효과 – 다양한 조인 형태로 활용이 가능함(NL-Join, MJ-Join, HJ-Join)
3.기타 – 서브쿼리가 조인으로 변경되는 경우, UNNEST와 NO_UNNEST로 제어가 가능
세미 조인의 중요한 포인트 3가지
1.서브쿼리를 SEMI JOIN으로 변환함
2.SEMI JOIN으로 바뀐 서브쿼리 쪽의 집합은 항상 후행처리가 됨
3.SEMI JOIN으로 바뀐 서브쿼리의 부분을 SCAN할 시에 만족되는 첫 번째 ROW만 발견하면 빠져 나옴
-메인 쿼리의 데이터를 서브쿼리로 체크하는 개념이므로 세미 조인이 적용된 SQL에서 서브쿼리의 블록은 항상 후행처리됨
-예외적으로 Hash Join Right (Semi/Anti)인 경우와 DSJ(Driving Semi Join)은 서브쿼리가 Driving 처리됨(고도화 2권 참조)
SELECT /*+ QB_NAME(MAIN) NO_UNNEST(@SUB) */
'NO_UNNEST사용한 경우' AS SQL_CASE
, D.DEPARTMENT_ID
, D.DEPARTMENT_NAME
, D.LOCATION_ID
FROM DEPARTMENTS D
WHERE EXISTS (
SELECT /*+ QB_NAME(SUB) */
1
FROM EMPLOYEES E
WHERE E.DEPARTMENT_ID =
D.DEPARTMENT_ID
)
AND D.LOCATION_ID = 1700;
---------------------------------------------------------| Id | Operation
| Name
|
---------------------------------------------------------| 0 | SELECT STATEMENT
|
|
|* 1 | FILTER
|
|
| 2 | TABLE ACCESS BY INDEX ROWID| DEPARTMENTS
|
|* 3 |
INDEX RANGE SCAN
| DEPT_LOCATION_IX |
|* 4 | INDEX RANGE SCAN
| EMP_DEPARTMENT_IX |
---------------------------------------------------------Predicate Information (identified by operation id):
--------------------------------------------------1 - filter( EXISTS (SELECT /*+ NO_UNNEST QB_NAME ("SUB") */ 0 FROM
"EMPLOYEES" "E“ WHERE "E"."DEPARTMENT_ID"=:B1))
3 - access("D"."LOCATION_ID"=1700)
4 - access("E"."DEPARTMENT_ID"=:B1)
HQT - SJ(Semi Join) : UNNEST 처리
SELECT /*+ QB_NAME(MAIN) */
'SJ가 발생한 경우' AS SQL_CASE
, D.DEPARTMENT_ID
, D.DEPARTMENT_NAME
, D.LOCATION_ID
FROM DEPARTMENTS D
WHERE EXISTS (SELECT /*+ QB_NAME(SUB) */ 1
FROM EMPLOYEES E
WHERE E.DEPARTMENT_ID =
D.DEPARTMENT_ID)
AND D.LOCATION_ID = 1700;
---------------------------------------------------------| Id | Operation
| Name
|
---------------------------------------------------------| 0 | SELECT STATEMENT
|
|
| 1 | NESTED LOOPS SEMI
|
|
| 2 | TABLE ACCESS BY INDEX ROWID| DEPARTMENTS
|
|* 3 |
INDEX RANGE SCAN
| DEPT_LOCATION_IX |
|* 4 | INDEX RANGE SCAN
| EMP_DEPARTMENT_IX |
----------------------------------------------------------
SELECT /*+ QB_NAME(MAIN) USE_HASH(E@SUB) */
'SJ가 발생한 경우' AS SQL_CASE
, D.DEPARTMENT_ID
, D.DEPARTMENT_NAME
, D.LOCATION_ID
FROM DEPARTMENTS D
WHERE EXISTS (SELECT /*+ QB_NAME(SUB) */ 1
FROM EMPLOYEES E
WHERE E.DEPARTMENT_ID =
D.DEPARTMENT_ID)
AND D.LOCATION_ID = 1700;
----------------------------------------------------------| Id | Operation
| Name
|
----------------------------------------------------------| 0 | SELECT STATEMENT
|
|
|* 1 | HASH JOIN SEMI
|
|
| 2 | TABLE ACCESS BY INDEX ROWID| DEPARTMENTS
|
|* 3 |
INDEX RANGE SCAN
| DEPT_LOCATION_IX |
| 4 | INDEX FULL SCAN
| EMP_DEPARTMENT_IX |
-----------------------------------------------------------
SJ(SEMI JOIN)의 강점은 필요에 따라 조인 방식을 취사선택 가능하다는 점이다
Heuristic Query Transformation Note
▶올바른 SQL 작성방법은?
SELECT /*+ QB_NAME(MAIN) */
DEPARTMENT_ID
, SALARY
FROM EMPLOYEES
WHERE (DEPARTMENT_ID= 60 AND
SALARY = 4200)
OR (DEPARTMENT_ID= 60) ;
SELECT /*+ QB_NAME(MAIN) */
DEPARTMENT_ID, SALARY
FROM EMPLOYEES
WHERE DEPARTMENT_ID= 60 ;
SELECT a.고객번호, a.고객명
FROM 고객 a, 공통코드 b
WHERE a.고객구분코드= b.고객구분코드(+);
SELECT a.고객번호, a.고객명
FROM 고객 a;
1.HQT의 원리를 인식하고 응용하여 튜닝에 적용시킨다.
2.HQT가 마땅히 수행되어야 함에도 그렇지 못할때 10053 Trace등을 이용하여 원인을 밝힌다.
▶약 34개의 HQT가 있음(SVM, JE, OJTAJ, …)
▶Logical Optimizer가 자동으로 HQT를 적용해 주는데 이러한 Rule이 모두 필요가 있는가?
1.Heuristic Rule을 모르는 상태에서 Transformer가 원본 SQL을 변환시키는 경우 바뀐 SQL과 실행계획을 결코
이해할 수 없다.
2.HQT의 원리를 이해하고 각각의 상황에 맞는 Rule을 적용한다면 우리가 원하는 SQL 최적화에 이를 수 있다.
CBQT - CBPPD(Cost Based Predicate Push Down)
1.내용 - Complex View에 외부의 Filter 조건을 밀어 넣어라
2.효과 - CV(Complex View)에 Filter가 뷰 내부로 파고들 수 있기에 성능향상을 기대함
3.기타 - CV(Complex View)란 Group By, Distinct를 사용한 View를 의미한다.
CREATE INDEX EMP_SAL_IX ON EMPLOYEES(SALARY);
EXEC DBMS_STATS.GATHER_TABLE_STATS(USER,
'EMPLOYEES', CASCADE => TRUE);
SELECT /*+ QB_NAME(V_MAIN) */
DEPARTMENT_ID, MAX_SAL
FROM (
SELECT /*+ QB_NAME(VI) NO_MERGE */
DEPARTMENT_ID, MAX(SALARY) MAX_SAL
FROM EMPLOYEES E
GROUP BY DEPARTMENT_ID
)
WHERE MAX_SAL > 32900;
----------------------------------------------------| ID | OPERATION
| NAME
|
----------------------------------------------------|
0 | SELECT STATEMENT
|
|
|
1 | VIEW
|
|
|* 2 |
FILTER
|
|
|
3 |
HASH GROUP BY
|
|
|
4 |
TABLE ACCESS BY INDEX ROWID| EMPLOYEES |
|* 5 |
INDEX RANGE SCAN
| EMP_SAL_IX |
-----------------------------------------------------
PREDICATE INFORMATION (IDENTIFIED BY OPERATION ID):
--------------------------------------------------2 - FILTER(MAX("SALARY")>32900)
5 - ACCESS("SALARY">32900)
Id 기준으로 2번과 5번에서 Filter가 발생함. 특히 EMP_SAL_IX 인덱스의 사용을 선택함
이것은 QT가 SQL을 MAX(SALARY) > 32900 조건을 보고 CBQT는 인덱스를 사용할 수 있게 E.SALARY > 32900
조건을 추가하고 있음.
또 주의 깊게 봐야 할 사항은 NO_MERGE HINT를 사용했음에도 불구하고 CV(Complex View)내에 조건을
밀어 넣어 성능향상을 가져온다.(반드시 NO_MERGE HINT가 필요한 건 아님)
CBQT - CBPPD(Cost Based Predicate Push Down) 또 다른 예제
앞 Page에서 설명했듯이 CBPPD 실행 시 NO_MERGE HINT가 반드시 필요한 것은 아님. Optimizer가 유리
하다고 판단하면 CBPPD는 언제든지 자동으로 실행됨
-------------------------------------------------------SELECT /*+ QB_NAME(V_MAIN) */
D.DEPARTMENT_ID, D.DEPARTMENT_NAME,
E.MAX_SAL
FROM DEPARTMENTS D
, (
SELECT /*+ QB_NAME(VI) */
DEPARTMENT_ID, MAX(SALARY) MAX_SAL
FROM EMPLOYEES E
GROUP BY DEPARTMENT_ID
) E
WHERE D.DEPARTMENT_ID = E.DEPARTMENT_ID
AND E.MAX_SAL > 32900;
| Id | Operation
| Name
|
-------------------------------------------------------|
0 | SELECT STATEMENT
|
|
|
1 | NESTED LOOPS
|
|
|
2 |
NESTED LOOPS
|
|
|
3 |
VIEW
|
|
|* 4 |
FILTER
|
|
|
5 |
HASH GROUP BY
|
|
|
6 |
TABLE ACCESS BY INDEX ROWID| EMPLOYEES
|
|* 7 |
INDEX RANGE SCAN
| EMP_SAL_IX |
|* 8 |
INDEX UNIQUE SCAN
| DEPT_ID_PK |
| 9 | TABLE ACCESS BY INDEX ROWID
| DEPARTMENTS |
-------------------------------------------------------Predicate Information (identified by operation id):
--------------------------------------------------4 - filter(MAX("SALARY")>32900)
7 - access("SALARY">32900)
8 - access("D"."DEPARTMENT_ID"="E"."DEPARTMENT_ID")
위에서 보는 바와 같이 NO_MERGE 힌트 없이도 CBPPD가 발생한다.
인라인 뷰 내부에 SALARY > 32900 조건이 성공적으로 Push Down 되어 처리됨
CBQT - SJC(Set to Join Conversion) 계속
1.내용 - 집합연산 중에서 조인으로 바꿀 수 있는 연산 MINUS, INTERSECT를 조인으로 바꿀 수 있음
2.효과 - 집합연산 수행에 발생되는 SORT UNIQUE를 HASH UNIQUE로 대체하여 SORT 부하를 없앨 수 있음
3.기타 - 교집합은 일반 조인기법으로, 차집합은 ANTI JOIN으로 처리가능
--전통적인 Intersect를 사용한 경우
SELECT /*+ QB_NAME(ABOVE) USE_NL(E, D) */
E.EMPLOYEE_ID, E.PHONE_NUMBER,
D.DEPARTMENT_NAME
FROM EMPLOYEES E, DEPARTMENTS D
WHERE E.DEPARTMENT_ID = D.DEPARTMENT_ID
AND E.JOB_ID
= 'IT_PROG'
INTERSECT
SELECT /*+ QB_NAME(BELOW) USE_NL(E, D) */
E.EMPLOYEE_ID, E.PHONE_NUMBER,
D.DEPARTMENT_NAME
FROM EMPLOYEES E, DEPARTMENTS D
WHERE E.DEPARTMENT_ID = D.DEPARTMENT_ID
AND E.SALARY
> 5000;
----------------------------------------------------------------------------| Id | Operation
| Name
| Rows | Bytes |
----------------------------------------------------------------------------| 0 | SELECT STATEMENT
|
|
5 | 2629 |
| 1 | INTERSECTION
|
|
|
|
| 2 | SORT UNIQUE
|
|
5 | 235 |
| 3 |
NESTED LOOPS
|
|
|
|
| 4 |
NESTED LOOPS
|
|
5 | 235 |
| 5 |
TABLE ACCESS BY INDEX ROWID| EMPLOYEES
|
5 | 155 |
|* 6 |
INDEX RANGE SCAN
| EMP_JOB_IX
|
5 |
|
|* 7 |
INDEX UNIQUE SCAN
| DEPT_ID_PK
|
1 |
|
| 8 |
TABLE ACCESS BY INDEX ROWID | DEPARTMENTS
|
1 |
16 |
| 9 | SORT UNIQUE
|
|
57 | 2394 |
| 10 |
NESTED LOOPS
|
|
|
|
| 11 |
NESTED LOOPS
|
|
57 | 2394 |
| 12 |
TABLE ACCESS FULL
| DEPARTMENTS
|
27 | 432 |
|* 13 |
INDEX RANGE SCAN
| EMP_DEPARTMENT_IX |
10 |
|
|* 14 |
TABLE ACCESS BY INDEX ROWID | EMPLOYEES
|
2 |
52 |
----------------------------------------------------------------------------Predicate Information (identified by operation id):
--------------------------------------------------6 - access("E"."JOB_ID"='IT_PROG')
7 - access("E"."DEPARTMENT_ID"="D"."DEPARTMENT_ID")
13 - access("E"."DEPARTMENT_ID"="D"."DEPARTMENT_ID")
14 - filter("E"."SALARY">5000)
CBQT - SJC(Set to Join Conversion) 계속
1.내용 - 집합연산 중에서 조인으로 바꿀 수 있는 연산 MINUS, INTERSECT를 조인으로 바꿀 수 있음
2.효과 - 집합연산 수행에 발생되는 SORT UNIQUE를 HASH UNIQUE로 대체하여 SORT 부하를 없앨 수 있음
3.기타 - 교집합은 일반 조인기법으로, 차집합은 ANTI JOIN으로 처리가능
--SESSION LEVEL에서 변경하는 경우
ALTER
SESSION SET "_convert_set_to_join" = TRUE;
SELECT /*+ QB_NAME(ABOVE) USE_NL(E, D) */
E.EMPLOYEE_ID, E.PHONE_NUMBER,
D.DEPARTMENT_NAME
FROM EMPLOYEES E, DEPARTMENTS D
WHERE E.DEPARTMENT_ID = D.DEPARTMENT_ID
AND E.JOB_ID
= 'IT_PROG'
INTERSECT
SELECT /*+ QB_NAME(BELOW) USE_NL(E, D) */
E.EMPLOYEE_ID, E.PHONE_NUMBER,
D.DEPARTMENT_NAME
FROM EMPLOYEES E, DEPARTMENTS D
WHERE E.DEPARTMENT_ID = D.DEPARTMENT_ID
AND E.SALARY
> 5000;
----------------------------------------------------------------------| Id | Operation
| Name
| Rows | Bytes |
----------------------------------------------------------------------| 0 | SELECT STATEMENT
|
|
1 |
67 |
| 1 | HASH UNIQUE
|
|
1 |
67 |
| 2 | NESTED LOOPS
|
|
|
|
| 3 |
NESTED LOOPS
|
|
1 |
67 |
| 4 |
NESTED LOOPS
|
|
3 | 153 |
|* 5 |
TABLE ACCESS BY INDEX ROWID| EMPLOYEES |
3 | 105 |
|* 6 |
INDEX RANGE SCAN
| EMP_JOB_IX |
5 |
|
| 7 |
TABLE ACCESS BY INDEX ROWID| DEPARTMENTS |
1 |
16 |
|* 8 |
INDEX UNIQUE SCAN
| DEPT_ID_PK |
1 |
|
|* 9 |
INDEX UNIQUE SCAN
| DEPT_ID_PK |
1 |
|
|* 10 |
TABLE ACCESS BY INDEX ROWID | DEPARTMENTS |
1 |
16 |
----------------------------------------------------------------------Predicate Information (identified by operation id):
--------------------------------------------------5 - filter("E"."SALARY">5000 AND
SYS_OP_MAP_NONNULL("PHONE_NUMBER") IS NOT NULL)
6 - access("JOB_ID"='IT_PROG')
8 - access("E"."DEPARTMENT_ID"="D"."DEPARTMENT_ID")
9 - access("DEPARTMENT_ID"="D"."DEPARTMENT_ID")
10 - filter("D"."DEPARTMENT_NAME"="D"."DEPARTMENT_NAME")
ID 기준으로 5번에 보면 QT가 발생함(다음 Page에서 별도 설명)
CBQT - SJC(Set to Join Conversion) 계속
결과적으로 STJ의 INTERSECT의 경우는 CBQT에 의해 SQL이 다음과 같이 변환됨
SELECT /*+ QB_NAME(ABOVE) USE_NL(E, D) */
E.EMPLOYEE_ID, E.PHONE_NUMBER,
D.DEPARTMENT_NAME
FROM EMPLOYEES E, DEPARTMENTS D
WHERE E.DEPARTMENT_ID = D.DEPARTMENT_ID
AND E.JOB_ID
= 'IT_PROG'
INTERSECT
SELECT /*+ QB_NAME(BELOW) USE_NL(E, D) */
E.EMPLOYEE_ID, E.PHONE_NUMBER,
D.DEPARTMENT_NAME
FROM EMPLOYEES E, DEPARTMENTS D
WHERE E.DEPARTMENT_ID = D.DEPARTMENT_ID
AND E.SALARY
> 5000;
SELECT DISTINCT A.EMPLOYEE_ID, A.PHONE_NUMBER,
A.DEPARTMENT_NAME
FROM (SELECT /*+ QB_NAME(ABOVE) */
E.EMPLOYEE_ID, E.PHONE_NUMBER,
D.DEPARTMENT_NAME
FROM EMPLOYEES E, DEPARTMENTS D
WHERE E.DEPARTMENT_ID = D.DEPARTMENT_ID
AND E.JOB_ID
= 'IT_PROG') A,
(SELECT /*+ QB_NAME(BELOW) */
E.EMPLOYEE_ID, E.PHONE_NUMBER,
D.DEPARTMENT_NAME
FROM EMPLOYEES E, DEPARTMENTS D
WHERE E.DEPARTMENT_ID = D.DEPARTMENT_ID
AND E.SALARY
> 5000 ) B
WHERE A.EMPLOYEE_ID = B.EMPLOYEE_ID
AND SYS_OP_MAP_NONNULL(A.PHONE_NUMBER) =
SYS_OP_MAP_NONNULL(B.PHONE_NUMBER)
AND A.DEPARTMENT_NAME = B.DEPARTMENT_NAME;
참고 (SYS_OP_MAP_NONNULL 함수)
- SELECT 절의 컬럼이 NULL을 허용하는 경우 발생. 예를 들어 A.PHONE_NUMBER와 B.PHONE_NUMBER
두 개의 값이 모두 NULL이라면 조인에 실패한다.(NULL = NULL은 항상 거짓이기에)
- 이럴 경우 SYS_OP_MAP_NONNULL를 사용하면 조인이 되는 양쪽의 PHONE_NUMBER 모두 NULL인 경우도
조인에 성공함(매우 유용한 함수임 - ★★★)
CBQT - SJC(Set to Join Conversion)
--------------------------------------------------------------------| Id | Operation
| Name
| Bytes |
--------------------------------------------------------------------| 0 | SELECT STATEMENT
|
| 2629 |
| 1 | MINUS
|
|
|
| 2 | SORT UNIQUE
|
| 235 |
| 3 |
NESTED LOOPS
|
|
|
| 4 |
NESTED LOOPS
|
| 235 |
| 5 |
TABLE ACCESS BY INDEX ROWID| EMPLOYEES
| 155 |
|* 6 |
INDEX RANGE SCAN
| EMP_JOB_IX
|
|
|* 7 |
INDEX UNIQUE SCAN
| DEPT_ID_PK
|
|
| 8 |
TABLE ACCESS BY INDEX ROWID | DEPARTMENTS
|
16 |
| 9 | SORT UNIQUE
|
| 2394 |
| 10 |
NESTED LOOPS
|
|
|
| 11 |
NESTED LOOPS
|
| 2394 |
| 12 |
TABLE ACCESS FULL
| DEPARTMENTS
| 432 |
|* 13 |
INDEX RANGE SCAN
| EMP_DEPARTMENT_IX |
|
|* 14 |
TABLE ACCESS BY INDEX ROWID | EMPLOYEES
|
52 |
--------------------------------------------------------------------Predicate Information (identified by operation id):
--------------------------------------------------6 - access("E"."JOB_ID"='IT_PROG')
7 - access("E"."DEPARTMENT_ID"="D"."DEPARTMENT_ID")
13 - access("E"."DEPARTMENT_ID"="D"."DEPARTMENT_ID")
14 - filter("E"."SALARY">5000)
- MINUS 사용예제(앞 Page 동일예제)
---------------------------------------------------------------------| Id | Operation
| Name
| Bytes |
---------------------------------------------------------------------| 0 | SELECT STATEMENT
|
|
59 |
| 1 | HASH UNIQUE
|
|
59 |
|* 2 | HASH JOIN ANTI
|
|
59 |
| 3 |
VIEW
| VW_STJ_1
| 155 |
| 4 |
NESTED LOOPS
| (STJ:SET TO JOIN) |
|
| 5 |
NESTED LOOPS
|
| 235 |
| 6 |
TABLE ACCESS BY INDEX ROWID| EMPLOYEES
| 155 |
|* 7 |
INDEX RANGE SCAN
| EMP_JOB_IX
|
|
|* 8 |
INDEX UNIQUE SCAN
| DEPT_ID_PK
|
|
| 9 |
TABLE ACCESS BY INDEX ROWID | DEPARTMENTS
|
16 |
| 10 |
VIEW
| VW_STJ_2
| 1596 |
| 11 |
NESTED LOOPS
| (STJ:SET TO JOIN) |
|
| 12 |
NESTED LOOPS
|
| 2394 |
| 13 |
TABLE ACCESS FULL
| DEPARTMENTS
| 432 |
|* 14 |
INDEX RANGE SCAN
| EMP_DEPARTMENT_IX |
|
|* 15 |
TABLE ACCESS BY INDEX ROWID | EMPLOYEES
|
52 |
---------------------------------------------------------------------Predicate Information (identified by operation id):
--------------------------------------------------2 - access("EMPLOYEE_ID"="EMPLOYEE_ID" AND
SYS_OP_MAP_NONNULL("PHONE_NUMBER")=
SYS_OP_MAP_NONNULL("PHONE_NUMBER") AND
"DEPARTMENT_NAME"="DEPARTMENT_NAME")
7 - access("E"."JOB_ID"='IT_PROG')
8 - access("E"."DEPARTMENT_ID"="D"."DEPARTMENT_ID")
14 - access("E"."DEPARTMENT_ID"="D"."DEPARTMENT_ID")
15 - filter("E"."SALARY">5000)
-SJC는 SORT UNIQUE의 부하는 줄일 수 있지만 HASH JOIN + HASH UNIQUE의 부하가 있음
-대용량 튜닝시 반드시 고려해야 하는 TRADE OFF임(검증작업이 필요함)
CBQT - CSU(Complex Subquery Unnesting)
1.내용 - 복잡한 서브쿼리(Group By + 집계함수, 서브쿼리 From절에 2개 이상 Table)를 조인으로 대체
2.효과 - 조인 처리시 선.후행을 조정함으로 성능효과를 가져올 수 있다
3.기타 - UNNEST, NO_UNNEST HINT로 처리가능
SELECT /*+ QB_NAME(MAIN) */ COUNT(*)
FROM EMPLOYEES S
WHERE SALARY IN (
SELECT /*+ QB_NAME(SUB) */ MAX(SALARY)
FROM EMPLOYEES C
WHERE C.EMPLOYEE_ID = S.EMPLOYEE_ID
)
AND EMPLOYEE_ID BETWEEN 100 AND 150;
SELECT /*+ QB_NAME(MAIN) */ COUNT(*)
FROM EMPLOYEES S
, (SELECT EMPLOYEE_ID AS ITEM_1, MAX(SALARY) AS
FROM EMPLOYEES C
WHERE EMPLOYEE_ID BETWEEN 100 AND 150
GROUP BY EMPLOYEE_ID) VW_SQ_1
WHERE S.EMPLOYEE_ID = VW_SQ_1.ITEM_1
AND S.SALARY
= VW_SQ_1."MAX(SALARY)"
AND S.EMPLOYEE_ID BETWEEN 100 AND 150;
---------------------------------------------------------| Id | Operation
| Name
|
---------------------------------------------------------|
0 | SELECT STATEMENT
|
|
|
1 | SORT AGGREGATE
|
|
|* 2 |
HASH JOIN
|
|
|
3 |
VIEW
| VW_SQ_1
|
|* 4 |
FILTER
|
|
|
5 |
HASH GROUP BY
|
|
|
6 |
TABLE ACCESS BY INDEX ROWID| EMPLOYEES
|
|* 7 |
INDEX RANGE SCAN
| EMP_EMP_ID_PK |
|
8 |
TABLE ACCESS BY INDEX ROWID
| EMPLOYEES
|
|* 9 |
INDEX RANGE SCAN
| EMP_EMP_ID_PK |
----------------------------------------------------------
---------------------------------------------------------| Id | Operation
| Name
|
---------------------------------------------------------|
0 | SELECT STATEMENT
|
|
|
1 | SORT AGGREGATE
|
|
|* 2 |
HASH JOIN
|
|
|
3 |
VIEW
|
|
|* 4 |
FILTER
|
|
|
5 |
HASH GROUP BY
|
|
|* 6 |
TABLE ACCESS BY INDEX ROWID| EMPLOYEES
|
|* 7 |
INDEX RANGE SCAN
| EMP_EMP_ID_PK |
|
8 |
TABLE ACCESS BY INDEX ROWID
| EMPLOYEES
|
|* 9 |
INDEX RANGE SCAN
| EMP_EMP_ID_PK |
----------------------------------------------------------
Predicate Information (identified by operation id):
--------------------------------------------------2 - access("SALARY"="MAX(SALARY)" AND
"ITEM_1"="S"."EMPLOYEE_ID")
4 - filter(MAX("SALARY")>0)
7 - access("C"."EMPLOYEE_ID">=100 AND "C"."EMPLOYEE_ID"<=150)
9 - access("EMPLOYEE_ID">=100 AND "EMPLOYEE_ID"<=150)
Predicate Information (identified by operation id):
--------------------------------------------------2 - access("S"."EMPLOYEE_ID"="VW_SQ_1"."ITEM_1" AND
"S"."SALARY"="VW_SQ_1"."MAX(SALARY)")
4 - filter(MAX("SALARY")>0)
6 - filter("SALARY">0)
7 - access("EMPLOYEE_ID">=100 AND "EMPLOYEE_ID"<=150)
9 - access("S"."EMPLOYEE_ID">=100 AND "S"."EMPLOYEE_ID"<=150)
Id 기준으로 3번을 보면 VW_SQ_1이 UNNESTING 되어
인라인 뷰로 바뀌었고 뷰 내부에 GROUP BY가 추가된
것을 볼 수 있다. 서브쿼리(VW_SQ_1)에는 GROUP BY가 없지만
QT가 GROUP BY를 추가한 것이다.
2개의 실행계획은 완전히 같다. 메인 쿼리의 집합을 보존하기 위하여
서브쿼리로부터 만들어진 인라인 뷰에 C.EMPLOYEE_ID로 GROUP BY가
추가된 것을 볼 수 있다.
“MAX(SALARY)”
CBQT - CSU(Complex Subquery Unnesting) 계속
CSU Parameter 및 Hint
1.Parameter - "_unnesting_subquery"이며 default로 "true"
2.Hint - UNNEST, NO_UNNEST (메인에서 사용시 서브에 QB_NAME힌트를 사용 쿼리블럭을 지정해야 함)
▶MAIN
SELECT
FROM
WHERE
쿼리에서 UNNEST 사용예제
/*+ UNNEST(@SUB) */ COUNT(*)
EMPLOYEES S
SALARY IN (
SELECT /*+ QB_NAME(SUB) */ MAX(SALARY)
FROM EMPLOYEES C
WHERE C.EMPLOYEE_ID = S.EMPLOYEE_ID
)
AND EMPLOYEE_ID BETWEEN 100 AND 150;
▶MAIN
SELECT
FROM
WHERE
---------------------------------------------------------| Id | Operation
| Name
|
---------------------------------------------------------|
0 | SELECT STATEMENT
|
|
|
1 | SORT AGGREGATE
|
|
|* 2 |
HASH JOIN
|
|
|
3 |
VIEW
| VW_SQ_1
|
|* 4 |
FILTER
|
|
|
5 |
HASH GROUP BY
|
|
|
6 |
TABLE ACCESS BY INDEX ROWID| EMPLOYEES
|
|* 7 |
INDEX RANGE SCAN
| EMP_EMP_ID_PK |
|
8 |
TABLE ACCESS BY INDEX ROWID
| EMPLOYEES
|
|* 9 |
INDEX RANGE SCAN
| EMP_EMP_ID_PK |
----------------------------------------------------------
-------------------------------------------------------| Id | Operation
| Name
|
-------------------------------------------------------|
0 | SELECT STATEMENT
|
|
|
1 | SORT AGGREGATE
|
|
|* 2 |
FILTER
|
|
|
3 |
TABLE ACCESS BY INDEX ROWID | EMPLOYEES
|
|* 4 |
INDEX RANGE SCAN
| EMP_EMP_ID_PK |
|
5 |
SORT AGGREGATE
|
|
|
6 |
TABLE ACCESS BY INDEX ROWID| EMPLOYEES
|
|* 7 |
INDEX UNIQUE SCAN
| EMP_EMP_ID_PK |
--------------------------------------------------------
Predicate Information (identified by operation id):
--------------------------------------------------2 - access("SALARY"="MAX(SALARY)" AND
"ITEM_1"="S"."EMPLOYEE_ID")
4 - filter(MAX("SALARY")>0)
7 - access("C"."EMPLOYEE_ID">=100 AND "C"."EMPLOYEE_ID"<=150)
9 - access("EMPLOYEE_ID">=100 AND "EMPLOYEE_ID"<=150)
QB_NAME을 이용하여 서브쿼리에 쿼리블럭명을 지정하여
UNNESTING 시키고 있다.
쿼리에서 NO_UNNEST 사용예제
/*+ NO_UNNEST(@SUB) */ COUNT(*)
EMPLOYEES S
SALARY IN (
SELECT /*+ QB_NAME(SUB) */ MAX(SALARY)
FROM EMPLOYEES C
WHERE C.EMPLOYEE_ID = S.EMPLOYEE_ID
)
AND EMPLOYEE_ID BETWEEN 100 AND 150;
Predicate Information (identified by operation id):
--------------------------------------------------2 - filter("SALARY"= (SELECT /*+ NO_UNNEST QB_NAME ("SUB") */
MAX("SALARY") FROM "EMPLOYEES" "C"
WHERE "C"."EMPLOYEE_ID"=:B1))
4 - access("EMPLOYEE_ID">=100 AND "EMPLOYEE_ID"<=150)
7 - access("C"."EMPLOYEE_ID"=:B1)
NO_UNNEST를 사용하여 CSU가 발생하지 않고 FILTER 서브쿼리로
처리된 예제이다. 상황에 따라 CSU를 어떻게 사용할 것인지 잘
판단하고 사용해야 함
CBQT - CVM(Complex View Merging)
1.내용 - CV(Group By + 집계함수, Distinct 포함 View)을 Main 쿼리 안으로 Merging하는 기능
2.효과 - 일반적으로 성능개선효과를 가져오나, 상황에 따라 역효과 발생
3.기타 - MERGE, NO_MERGE HINT로 처리가능
인덱스현황 1.EMP_JOB_DEPT_IX
: JOB_ID + DEPARTMENT_ID (다음 장 설명을 위해)
2.EMP_DEPARTMENT_IX : DEPARTMENT_ID
SELECT /*+ QB_NAME(OUTER) */ E.*
FROM EMPLOYEES E
, (SELECT /*+ QB_NAME(INNER) NO_PUSH_PRED */ -- JPPD를 방지하기 위해 힌트 사용(뒤에 별도 설명)
DEPARTMENT_ID, AVG(SALARY) SALARY
FROM EMPLOYEES
GROUP BY DEPARTMENT_ID) AVG
WHERE E.DEPARTMENT_ID = AVG.DEPARTMENT_ID
AND E.SALARY
>= AVG.SALARY
AND E.JOB_ID
= 'HR_REP';
----------------------------------------------------------------------------| Id | Operation
| Name
| Rows | Bytes |
----------------------------------------------------------------------------|
0 | SELECT STATEMENT
|
|
1 |
76 |
|* 1 | FILTER
|
|
|
|
|
2 |
HASH GROUP BY
|
|
1 |
76 |
|
3 |
NESTED LOOPS
|
|
|
| 1.CVM을 하기 위해 NO_PUSH_PRED 사용
|
4 |
NESTED LOOPS
|
|
31 | 2356 |
|
5 |
TABLE ACCESS BY INDEX ROWID| EMPLOYEES
|
1 |
69 | 2.GROUP BY된 인라인 뷰가 사라짐
|* 6 |
INDEX RANGE SCAN
| EMP_JOB_DEPT_IX
|
1 |
| 3.ID 1번 기준으로 마지막에 GROUP BY절과
|* 7 |
INDEX RANGE SCAN
| EMP_DEPARTMENT_IX |
10 |
|
FILTER(HAVING)가 만들어 짐
|
8 |
TABLE ACCESS BY INDEX ROWID | EMPLOYEES
|
31 |
217 |
-----------------------------------------------------------------------------
Plan분석
Predicate Information (identified by operation id):
--------------------------------------------------1 - filter("E"."SALARY">=AVG("SALARY"))
6 - access("E"."JOB_ID"='HR_REP')
7 - access("E"."DEPARTMENT_ID"="DEPARTMENT_ID")
CBQT - CVM(Complex View Merging) 계속
앞 Page에 계속해서 CV가 병합(Merging)되지 않고 JPPD 형태로 풀렸을 경우(NO_MERGE)
인덱스현황 1.EMP_JOB_DEPT_IX
: JOB_ID + DEPARTMENT_ID (다음 장 설명을 위해)
2.EMP_DEPARTMENT_IX : DEPARTMENT_ID
SELECT /*+ QB_NAME(OUTER) */ E.* -- 예제가 조금은 이상함(양해 바람 ^^;;)
FROM EMPLOYEES E
, (SELECT /*+ QB_NAME(INNER) */
DEPARTMENT_ID, AVG(SALARY) SALARY
FROM EMPLOYEES
GROUP BY DEPARTMENT_ID) AVG
WHERE E.DEPARTMENT_ID = AVG.DEPARTMENT_ID
AND E.SALARY
>= AVG.SALARY
AND E.JOB_ID
= 'HR_REP';
---------------------------------------------------------------------------| Id | Operation
| Name
| Rows | Bytes |
---------------------------------------------------------------------------|
0 | SELECT STATEMENT
|
|
1 |
82 |
|
1 | NESTED LOOPS
|
|
1 |
82 |
|
2 |
TABLE ACCESS BY INDEX ROWID | EMPLOYEES
|
1 |
69 |
|* 3 |
INDEX RANGE SCAN
| EMP_JOB_DEPT_IX
|
1 |
|
|* 4 | VIEW PUSHED PREDICATE
|
|
1 |
13 |
|
5 |
SORT AGGREGATE
|
|
1 |
7 |
|
6 |
TABLE ACCESS BY INDEX ROWID| EMPLOYEES
|
10 |
70 |
|* 7 |
INDEX RANGE SCAN
| EMP_DEPARTMENT_IX |
10 |
|
----------------------------------------------------------------------------
Plan 분석
1.앞 Page의 CVM과는 다름
2.View Merging이 발생하지 않함
3.JPPD형태로 GROUP BY된 VIEW가 유지
4.인라인 뷰 바깥에서 인라인 뷰를 파고 들음
(미약하지만 CVM보다는 좋은 형태로 풀림)
Predicate Information (identified by operation id):
--------------------------------------------------3 - access("E"."JOB_ID"='HR_REP')
4 - filter("E"."SALARY">="AVG"."SALARY")
7 - access("DEPARTMENT_ID"="E"."DEPARTMENT_ID")
NO_MERGE로 제어가 가능하다고 하나, 상황에 따라 NO_PUSH_PRED HINT로 제어도 효과적임
CBQT - JPPD(Join Predicate Push Down)
1.내용 - 뷰 또는 인라인 뷰와 조인시에 조인 조건을 뷰 내부로 침투시키는 기능
2.효과 - 일반적인 업무형태에 많이 나타나며 NL 조인만 사용가능함
3.기타 - Version에 따라 기능의 차이가 있음
JPPD가 활용가능한 5가지 형태의 SQL
①
②
③
④
⑤
JPPD Union View : Union을 사용한 JPPD
JPPD Union All View : Union All을 사용한 JPPD
JPPD Outer Join View : 뷰에 Outer 조인을 사용한 JPPD
Multi Level JPPD : 뷰 내부에 또 다른 뷰가 있을 경우의 JPPD
JPPD Extension : Distinct나 Group by, Semi / Anti-Join을 사용한 뷰의 JPPD(11g NF)
인덱스현황 1.EMP_JOB_DEPT_IX
EMP_JOB_IX
: JOB_ID + DEPARTMENT_ID(앞에서 미리 생성함)
: JOB_ID
2.EMP_DEPARTMENT_IX : DEPARTMENT_ID
CBQT - JPPD(Join Predicate Push Down) CASE1
CASE 1 - JPPD Union all
인덱스현황 1.EMP_JOB_DEPT_IX
: JOB_ID + DEPARTMENT_ID, EMP_JOB_IX : JOB_ID
2.EMP_DEPARTMENT_IX : DEPARTMENT_ID
SELECT /*+ QB_NAME(OUTER) LEADING(D) USE_NL(E) PUSH_PRED(E) */
D.DEPARTMENT_ID, D.DEPARTMENT_NAME, E.EMPLOYEE_ID, E.JOB_ID, E.EMAIL_PHONE_NUM
FROM DEPARTMENTS D
, (
SELECT /*+ QB_NAME(INNER1) */ EMPLOYEE_ID, DEPARTMENT_ID, JOB_ID, EMAIL AS EMAIL_PHONE_NUM
FROM EMPLOYEES
WHERE JOB_ID = 'AD_ASST‘ -- JOB_ID 조건 밖에 없음을 유의하면서 보자(★★★)
UNION ALL
SELECT /*+ QB_NAME(INNER1) */ EMPLOYEE_ID, DEPARTMENT_ID, JOB_ID, PHONE_NUMBER AS EMAIL_PHONE_NUM
FROM EMPLOYEES
WHERE JOB_ID = 'AD_PRES‘ -- JOB_ID 조건 밖에 없음을 유의하면서 보자(★★★)
) E
WHERE D.DEPARTMENT_ID = E.DEPARTMENT_ID
AND D.LOCATION_ID
= 1700;
---------------------------------------------------------------------------------------| Id | Operation
| Name
| Rows | Bytes | Cost (%CPU)|
---------------------------------------------------------------------------------------|
0 | SELECT STATEMENT
|
|
1 |
56 |
86
(0)|
|
1 | NESTED LOOPS
|
|
1 |
56 |
86
(0)|
|
2 |
TABLE ACCESS BY INDEX ROWID | DEPARTMENTS
|
21 |
399 |
2
(0)|
|* 3 |
INDEX RANGE SCAN
| DEPT_LOCATION_IX |
21 |
|
1
(0)|
|
4 |
VIEW
|
|
1 |
37 |
4
(0)|
|
5 |
UNION ALL PUSHED PREDICATE |
|
|
|
|
|
6 |
TABLE ACCESS BY INDEX ROWID| EMPLOYEES
|
1 |
24 |
2
(0)|
|* 7 |
INDEX RANGE SCAN
| EMP_JOB_DEPT_IX |
1 |
|
1
(0)|
|
8 |
TABLE ACCESS BY INDEX ROWID| EMPLOYEES
|
1 |
31 |
2
(0)|
|* 9 |
INDEX RANGE SCAN
| EMP_JOB_DEPT_IX |
1 |
|
1
(0)|
---------------------------------------------------------------------------------------Predicate Information (identified by operation id):
--------------------------------------------------3 - access("D"."LOCATION_ID"=1700)
7 - access("JOB_ID"='AD_ASST' AND "DEPARTMENT_ID"="D"."DEPARTMENT_ID")
9 - access("JOB_ID"='AD_PRES' AND "DEPARTMENT_ID"="D"."DEPARTMENT_ID”
Plan 분석
1.Predicate(Id 7,9) 정보를 참조
2.access("JOB_ID"='AD_ASST' AND
"DEPARTMENT_ID"="D"."DEPARTMENT_ID")
3.사용함으로 원하는 Access 패턴으로 유도함
(단일 Index가 아닌 결합 Index를 사용함)
CBQT - JPPD(Join Predicate Push Down) CASE2
CASE 2 - JPPD Outer Join
SELECT E.EMPLOYEE_ID, E.FIRST_NAME, E.LAST_NAME, E.PHONE_NUMBER, D.CITY, D.COUNTRY_ID
FROM EMPLOYEES E
, (
SELECT D.DEPARTMENT_ID, L.CITY, L.COUNTRY_ID
FROM DEPARTMENTS D, LOCATIONS L
WHERE D.LOCATION_ID = L.LOCATION_ID
) D
WHERE D.DEPARTMENT_ID(+) = E.DEPARTMENT_ID
AND E.JOB_ID
= 'AD_PRES';
--------------------------------------------------------------------------------------| Id | Operation
| Name
| Rows | Bytes | Cost (%CPU)|
--------------------------------------------------------------------------------------|
0 | SELECT STATEMENT
|
|
1 |
69 |
4 (0)|
|
1 | NESTED LOOPS OUTER
|
|
1 |
69 |
4 (0)|
|
2 |
TABLE ACCESS BY INDEX ROWID | EMPLOYEES
|
1 |
46 |
2 (0)|
|* 3 |
INDEX RANGE SCAN
| EMP_JOB_DEPT_IX |
1 |
|
1 (0)|
|
4 |
VIEW PUSHED PREDICATE
|
|
1 |
23 |
2 (0)|
|
5 |
NESTED LOOPS
|
|
1 |
22 |
2 (0)|
|
6 |
TABLE ACCESS BY INDEX ROWID| DEPARTMENTS
|
1 |
7 |
1 (0)|
|* 7 |
INDEX UNIQUE SCAN
| DEPT_ID_PK
|
1 |
|
0 (0)|
|
8 |
TABLE ACCESS BY INDEX ROWID| LOCATIONS
|
23 |
345 |
1 (0)|
|* 9 |
INDEX UNIQUE SCAN
| LOC_ID_PK
|
1 |
|
0 (0)|
--------------------------------------------------------------------------------------Predicate Information (identified by operation id):
--------------------------------------------------3 - access("E"."JOB_ID"='AD_PRES')
7 - access("D"."DEPARTMENT_ID"="E"."DEPARTMENT_ID")
9 - access("D"."LOCATION_ID"="L"."LOCATION_ID")
PLAN 분석
1.Id 4번에 JPPD가 발생함
2.결과적으로 인라인뷰 D 내의 DEPARTMENT
테이블에 조인 조건인 E.DEPARTMENT_ID =
D.DEPARTMENT_ID 조건이 추가됨
3.DEPARTMENT와 LOCATION의 조인조건인
LOCATION_ID와 연결되어 처리됨
CBQT - JPPD(Join Predicate Push Down) CASE3
CASE 3 - JPPD Extension
-오라클 11g에서 추가된 JPPD 기능(★★★)
-10g까지는 Distinct, Group by, Semi / Anti Join을 사용한 뷰에 대해서는 조인 조건이 뷰 내부로 파고들지 못함
-11g에서 이러한 제약조건들이 사라지는 데 이를 JPPD Extension이라 한다.
SELECT /*+ LEADING(D) USE_NL(E) */
D.DEPARTMENT_ID, D.DEPARTMENT_NAME, E.JOB_TITLE, E.SUM_SAL, E.MAX_SAL
FROM DEPARTMENTS D
, (
SELECT E.DEPARTMENT_ID, E.JOB_ID,
MIN(J.JOB_TITLE) JOB_TITLE, SUM(E.SALARY) SUM_SAL, MAX(E.SALARY) MAX_SAL
FROM EMPLOYEES E, JOBS J
WHERE E.JOB_ID = J.JOB_ID
GROUP BY E.DEPARTMENT_ID, E.JOB_ID
) E
WHERE D.DEPARTMENT_ID = E.DEPARTMENT_ID(+);
------------------------------------------------------------------------------------------| Id | Operation
| Name
| Rows | Bytes | Cost (%CPU)|
------------------------------------------------------------------------------------------|
0 | SELECT STATEMENT
|
|
27 | 1701 |
138 (20)|
|
1 | NESTED LOOPS OUTER
|
|
27 | 1701 |
138 (20)|
|
2 |
TABLE ACCESS FULL
| DEPARTMENTS
|
27 |
432 |
3
(0)|
|
3 |
VIEW PUSHED PREDICATE
|
|
1 |
47 |
5 (20)|
|
4 |
SORT GROUP BY
|
|
1 |
43 |
5 (20)|
|
5 |
MERGE JOIN
|
|
10 |
430 |
5 (20)|
|
6 |
TABLE ACCESS BY INDEX ROWID | JOBS
|
19 |
513 |
2
(0)|
|
7 |
INDEX FULL SCAN
| JOB_ID_PK
|
19 |
|
1
(0)|
|* 8 |
SORT JOIN
|
|
10 |
160 |
3 (34)|
|
9 |
TABLE ACCESS BY INDEX ROWID| EMPLOYEES
|
10 |
160 |
2
(0)|
|* 10 |
INDEX RANGE SCAN
| EMP_DEPARTMENT_IX |
10 |
|
1
(0)|
------------------------------------------------------------------------------------------Predicate Information (identified by operation id):
--------------------------------------------------8 - access("E"."JOB_ID"="J"."JOB_ID")
filter("E"."JOB_ID"="J"."JOB_ID")
10 - access("E"."DEPARTMENT_ID"="D"."DEPARTMENT_ID")
PLAN분석
1.Group By(Distinct)에도 JPPD가 발생함
2.만약, 위 SQL에서 (+)조건을 생략한다면…?
CVM이 발생할 가능성이 높음
(Complex View Merging)
CBQT - JPPD(Join Predicate Push Down) CASE3(계속)
CASE 3 - JPPD Extension 대신 CVM 발생
CASE 3 - JPPD Extension
SELECT /*+ LEADING(D) USE_NL(E) */
(중간생략...)
, (
SELECT /*+ USE_NL(E, J) */ ~~
(중간생략...)
) E
WHERE D.DEPARTMENT_ID = E.DEPARTMENT_ID;
SELECT /*+ LEADING(D) USE_NL(E) */
(중간생략...)
, (
SELECT /*+ NO_MERGE USE_NL(E, J) */ ~~
(중간생략...)
) E
WHERE D.DEPARTMENT_ID = E.DEPARTMENT_ID;
---------------------------------------------------------------------| Id | Operation
| Name
| Rows | Bytes |
---------------------------------------------------------------------|
0 | SELECT STATEMENT
|
|
106 | 6784 |
|
1 | HASH GROUP BY
|
|
106 | 6784 |
|* 2 |
HASH JOIN
|
|
106 | 6784 |
|
3 |
MERGE JOIN
|
|
106 | 3922 |
|
4 |
TABLE ACCESS BY INDEX ROWID| DEPARTMENTS |
27 | 567 |
|
5 |
INDEX FULL SCAN
| DEPT_ID_PK |
27 |
|
|* 6 |
SORT JOIN
|
|
107 | 1712 |
|
7 |
TABLE ACCESS FULL
| EMPLOYEES
|
107 | 1712 |
|
8 |
TABLE ACCESS FULL
| JOBS
|
19 | 513 |
----------------------------------------------------------------------
---------------------------------------------------------------------------| Id | Operation
| Name
| Rows | Bytes |
---------------------------------------------------------------------------|
0 | SELECT STATEMENT
|
|
20 | 1260 |
|
1 | NESTED LOOPS
|
|
20 | 1260 |
|
2 |
TABLE ACCESS FULL
| DEPARTMENTS
|
27 | 432 |
|
3 |
VIEW PUSHED PREDICATE
|
|
1 |
47 |
|
4 |
SORT GROUP BY
|
|
1 |
43 |
|
5 |
NESTED LOOPS
|
|
|
|
|
6 |
NESTED LOOPS
|
|
10 | 430 |
|
7 |
TABLE ACCESS BY INDEX ROWID| EMPLOYEES
|
10 | 160 |
|* 8 |
INDEX FULL SCAN
| EMP_JOB_DEPT_IX |
10 |
|
|* 9 |
INDEX UNIQUE SCAN
| JOB_ID_PK
|
1 |
|
| 10 |
TABLE ACCESS BY INDEX ROWID | JOBS
|
1 |
27 |
----------------------------------------------------------------------------
Predicate Information (identified by operation id):
--------------------------------------------------2 - access("E"."JOB_ID"="J"."JOB_ID")
6 - access("D"."DEPARTMENT_ID"="E"."DEPARTMENT_ID")
filter("D"."DEPARTMENT_ID"="E"."DEPARTMENT_ID")
Predicate Information (identified by operation id):
--------------------------------------------------8 - access("E"."DEPARTMENT_ID"="D"."DEPARTMENT_ID")
filter("E"."DEPARTMENT_ID"="D"."DEPARTMENT_ID")
9 - access("E"."JOB_ID"="J"."JOB_ID")
해결책
1.NO_MERGE HINT로 제어
2.WHY? JPPD의 개념은 인라인 뷰 내부로 파고드는 것이기에 VIEW MERGE가 발생하면 안됨
JPPD Extension은 Semi/Anti 조인에도 적용시킬 수 있으나 약간의 제약사항이 존재함
제약사항 - 서브쿼리의 FROM절에 테이블이 2개 이상 존재하는 경우 발생함(DUAL로 해결가능)
CBQT - JPPD(Join Predicate Push Down) CASE3(계속)
JPPD Extension은 Semi/Anti 조인에도 적용시킬 수 있으나 약간의 제약사항이 존재함
제약사항 - 서브쿼리의 FROM절에 테이블이 2개 이상 존재하는 경우 발생함(DUAL로 해결가능)
CASE 3 - JPPD Extension(Anti Join JPPD)
-- EMPLOYEES와 DEPARTMENTS의 R/I관계가 없다고 가정
CASE 3 - JPPD Extension(Anti Join JPPD 편법)
-- EMPLOYEES와 DEPARTMENTS의 R/I관계가 없다고 가정
SELECT /*+ QB_NAME(MAIN) */ E.EMPLOYEE_ID, E.LAST_NAME,
E.EMAIL
FROM EMPLOYEES E
WHERE E.JOB_ID = 'AD_ASST'
AND NOT EXISTS (
SELECT /*+ QB_NAME(SUB) UNNEST */ 1
FROM DEPARTMENTS D
WHERE D.DEPARTMENT_ID = E.DEPARTMENT_ID
);
SELECT /*+ QB_NAME(MAIN) */ E.EMPLOYEE_ID, E.LAST_NAME,
E.EMAIL
FROM EMPLOYEES E
WHERE E.JOB_ID = 'AD_ASST'
AND NOT EXISTS (
SELECT /*+ QB_NAME(SUB) UNNEST */ 1
FROM DEPARTMENTS D, DUAL L
WHERE D.DEPARTMENT_ID = E.DEPARTMENT_ID
);
-----------------------------------------------------------------------| Id | Operation
| Name
| Rows | Bytes |
-----------------------------------------------------------------------|
0 | SELECT STATEMENT
|
|
1 |
36 |
|
1 | NESTED LOOPS ANTI
|
|
1 |
36 |
|
2 |
TABLE ACCESS BY INDEX ROWID| EMPLOYEES
|
1 |
32 |
|* 3 |
INDEX RANGE SCAN
| EMP_JOB_DEPT_IX |
1 |
|
|* 4 |
INDEX UNIQUE SCAN
| DEPT_ID_PK
|
27 |
108 |
------------------------------------------------------------------------
-----------------------------------------------------------------------| Id | Operation
| Name
| Rows | Bytes |
-----------------------------------------------------------------------|
0 | SELECT STATEMENT
|
|
1 |
34 |
|
1 | NESTED LOOPS ANTI
|
|
1 |
34 |
|
2 |
TABLE ACCESS BY INDEX ROWID| EMPLOYEES
|
1 |
32 |
|* 3 |
INDEX RANGE SCAN
| EMP_JOB_DEPT_IX |
1 |
|
|
4 |
VIEW PUSHED PREDICATE
| VW_SQ_1
|
1 |
2 |
|
5 |
NESTED LOOPS
|
|
1 |
4 |
|
6 |
FAST DUAL
|
|
1 |
|
|* 7 |
INDEX UNIQUE SCAN
| DEPT_ID_PK
|
1 |
4 |
------------------------------------------------------------------------
Predicate Information (identified by operation id):
--------------------------------------------------3 - access("E"."JOB_ID"='AD_ASST')
4 - access("D"."DEPARTMENT_ID"="E"."DEPARTMENT_ID")
JPPD 발생 안함
Predicate Information (identified by operation id):
--------------------------------------------------3 - access("E"."JOB_ID"='AD_ASST')
7 - access("D"."DEPARTMENT_ID"="E"."DEPARTMENT_ID")
JPPD 발생함
Cost Based Query Transformation Note
1.Logical Optimizer는 Cost Estimator에게 CBQT 적용 전/후의 Cost를 구하여 비용이 저렴한
쪽을 선택한다.
2.Cost Estimator의 불완전성 때문에 Logical Optimizer는 종종 잘못된 판단을 할 수 있다.
이 때 사람이 개입하여 올바른 길로 안내해야 한다.
여러 가지 힌트를 사용하거나 원리에 맞게 수동으로 SQL을 재작성 한다면 어렵지 않게 해결할
수 있지만 그렇게 하려면 각각의 개념을 알아야 한다.
3.CBQT 내용 중 실무에서 가장 많이 사용되는 기능은 OR-Expansion의 종류(고도화 2권 참조)와
CSU(Complex Subquery Unnesting), CVM(Complex View Merging), JPPD(Join Predicate Push Down) 등이
있다. 특히, CSU(Complex Subquery Unnesting)는 CBQT의 출발점이 된다.
CSU가 적용되면 연이어 CVM 혹은 JPPD가 고려됨을 알아두자