Download presentation
Presentation is loading. Please wait.
1
쿼리 변환 (Query Transformation)
A조
2
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을 물리적으로 최적화하는 것이다.
3
Optimizer 구조도 Optimizer Components Parser Query Transformer Cost
Estimator Plan Generator Row Source Optimizer Components Heuristic QT Cost Based 위 그림을 설명하면 Query Parser가 SQL을 검사하여 넘겨주면 Query Transformer (Logical Optimizer)가 SQL을 변환시켜서 Cost Estimator(Physical Optimizer)에 넘겨준다. 이 때 Cost Estimator는 통계정보 등을 참조하여 변환되지 않은 SQL의 Cost와 변환된 SQL의 Cost를 비교하여 가장 낮은 Cost(비용)를 갖는 SQL을 찾아내어 Plan Generator에 넘겨주고 실행계획을 완성후 SQL의 결과를 Fetch한다.
4
QT 정보추출 QT에서 실행계획의 중요성은 매우 1.PLAN을 볼 수 있는 Tool 필요
2.일반적으로 Event Trace + tkprof를 많이 사용함 ▶제약조건이 많음 (FTP 권한, Dump Trace 구성, Predicate정보, QB정보…) DBMS_XPLAN.DISPLAY 또는 DBMS_XPLAN.DISPLAY_CURSOR를 사용하는 편이 유리하며 이벤트가 갖지 못한 장점이 있음 항목 DBMS_XPLAN DISPLAY 10046 Trace DISPLAY_CURSOR 수행결과 예측 실측 SQL수행 1개 1개 이상 FUNCTION 수행정보 Ⅹ ○ 단계별 통계치 PLAN정보 추가정보
5
QT 정보추출 Event 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 | | 0 | SELECT STATEMENT | | | | |* 1 | TABLE ACCESS FULL| EMPLOYEES | | | Predicate Information (identified by operation id): 1 - filter("DEPARTMENT_ID"=60 OR "SALARY"=4200 AND "DEPARTMENT_ID"=60) Trace File 내용 (생략…) ****************************************** ----- 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)
6
QT의 두 가지 방식 쿼리변환은 크게 2가지 형태로 작동한다. 1.휴리스틱(Heuristic) 쿼리변환
SELECT COUNT(*) FROM TAB1 ORDER BY COL1; COUNT(*)를 하는데 굳이 Order By를 해야 할까? 이런 상황에서 Order By 부하를 없애는 가장 좋은 방법은 Order By를 수행하지 않는 것임. Heuristic Rule임 1.휴리스틱(Heuristic) 쿼리변환 옵티마이져가 성능을 향상시키기 위해서 비용계산 (Costing)과정이 필요한 경우도 있지만 특정한 경우에는 Costing과정이 필요 없기 때문에 이러한 과정을 생략하고 바로 특정 Rule을 적용하여 최적화를 진행하겠다는 의미. (30개 정도가 있음) 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; 인덱스의 구성 및 드라이빙 테이블 선택 등…
7
서브쿼리 Unnesting - 별도 설명 Subquery Unnesting의 이점 서브쿼리의 분류
1.인라인 뷰(Inline View) -FROM절에 나타나는 서브쿼리 2.중첩된 서브쿼리(Nested Subquery) -결과집합을 한정하기 위해 WHERE절에 사용된 서브쿼리 3.스칼라 서브쿼리(Scalar Subquery) -SELECT-LIST에서 사용되는 서브쿼리 옵티마이져의 서브쿼리 처리방식 1.Subquery Unnesting -동일한 결과를 보장하는 조인형태로 변환하여 최적화 (서브쿼리를 인라인 뷰로 바꾸어 정상적인 조인처리) 2.Filter Operation -원래 상태에서 메인쿼리와 서브쿼리를 각각 최적화 (이때 서브쿼리에 Filter 오퍼레이션이 발생함) Subquery Unnesting의 이점 1.서브쿼리를 메인쿼리와 같은 레벨로 풀어낸다면 다양한 액세스 경로와 조인 메소드를 활용할 수 있다. 2.서브쿼리 Unnesting과 관련한 HINT - UNNEST : 서브쿼리를 조인방식으로 최적화하도록 유도한다. - NO_UNNEST : 서브쿼리를 필터 방식으로 최적화하도록 유도한다.
8
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 | | | | (0)| 00:00:01 | | 1 | TABLE ACCESS BY INDEX ROWID| EMPLOYEES | | | (0)| 00:00:01 | |* 2 | INDEX RANGE SCAN | EMP_DEPARTMENT_IX | | | (0)| 00:00:01 | PREDICATE INFORMATION (IDENTIFIED BY OPERATION ID): 2 - ACCESS("DEPARTMENT_ID"=60) [CSE IN 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)
9
HQT - CSE(Common Subexpression Elimination) 계속
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 | | | | (0)| 00:00:01 | |* 1 | TABLE ACCESS FULL| EMPLOYEES | | | (0)| 00:00:01 | Predicate Information (identified by operation id): 1 - filter("DEPARTMENT_ID"=60 OR "SALARY"=4200 AND "DEPARTMENT_ID"=60) [CSE IN 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
10
HQT - DE(Distinct Elimination)
1.내용 - SELECT절에 UNIQUE한 컬럼이 포함되어 있는 경우 불필요한 Distinct를 제거하라 2.효과 - 불필요한 Sort Unique / Hash Unique룰 제거함으로 성능효과를 가져온다. 3.기타 - 11g의 New Feature | 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") 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) ); 위의 SQL을 보면 DISTINCT 로 인해 PLAN에 HASH UNIQUE가 발생함 SQL을 분석해보면 SELECT LIST의 컬럼들이 UNIQUE COLUMN임을 알 수 있음 D.LOCATION_ID를 L.LOCATION_ID로 대체하면 다음과 같음
11
HQT - DE(Distinct Elimination)
1.내용 - SELECT절에 UNIQUE한 컬럼이 포함되어 있는 경우 불필요한 Distinct를 제거하라 2.효과 - 불필요한 Sort Unique / Hash Unique룰 제거함으로 성능효과를 가져온다. 3.기타 - 11g의 New Feature | 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") 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) ); 컬럼 하나만 바뀌었을 뿐인데 실행계획에서 HASH UNIQUE가 사라짐. FROM절에 나열된 모든 테이블의 PK(UNIQUE)컬럼이 SELECT절에 나올 경우 DISTINCT가 제거됨 반복적으로 사용되거나 대용량 집합에서 성능향상효과를 가져올 수 있다.
12
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 | 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 | 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 ); | 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 | Subquery가 Unnesting되어 드라이빙 집합이 되면 메인쿼리의 집합을 보존하기 위해 Default로 Distinct가 추가된다. 이 때 DE, DEUI 기능이 수행된다면 Distinct가 제거된다.
13
DE의 또 다른 유형 - DEUI(Distinct Elimination using Unique Index)
UNIQUE INDEX를 사용하여 INDEX UNIQUE SCAN이 발생하는 경우에는 DISTINCT를 제거하는 기능 | 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 | 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; 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는 절대로 수행되지 않는다.
14
HQT - SJ(Semi Join) : FILTER 처리
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권 참조) | 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) SELECT /*+ QB_NAME(MAIN) */ '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;
15
HQT - SJ(Semi Join) : UNNEST 처리
| 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) */ '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 | 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; SJ(SEMI JOIN)의 강점은 필요에 따라 조인 방식을 취사선택 가능하다는 점이다
16
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) ; DEPARTMENT_ID, SALARY WHERE DEPARTMENT_ID= 60 ; SELECT a.고객번호, a.고객명 FROM 고객 a, 공통코드 b WHERE a.고객구분코드= b.고객구분코드(+); FROM 고객 a; 1.HQT의 원리를 인식하고 응용하여 튜닝에 적용시킨다. 2.HQT가 마땅히 수행되어야 함에도 그렇지 못할때 Trace등을 이용하여 원인을 밝힌다. ▶약 34개의 HQT가 있음(SVM, JE, OJTAJ, …) ▶Logical Optimizer가 자동으로 HQT를 적용해 주는데 이러한 Rule이 모두 필요가 있는가? 1.Heuristic Rule을 모르는 상태에서 Transformer가 원본 SQL을 변환시키는 경우 바뀐 SQL과 실행계획을 결코 이해할 수 없다. 2.HQT의 원리를 이해하고 각각의 상황에 맞는 Rule을 적용한다면 우리가 원하는 SQL 최적화에 이를 수 있다.
17
CBQT - CBPPD(Cost Based Predicate Push Down)
1.내용 - Complex View에 외부의 Filter 조건을 밀어 넣어라 2.효과 - CV(Complex View)에 Filter가 뷰 내부로 파고들 수 있기에 성능향상을 기대함 3.기타 - CV(Complex View)란 Group By, Distinct를 사용한 View를 의미한다. | 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) 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 기준으로 2번과 5번에서 Filter가 발생함. 특히 EMP_SAL_IX 인덱스의 사용을 선택함 이것은 QT가 SQL을 MAX(SALARY) > 조건을 보고 CBQT는 인덱스를 사용할 수 있게 E.SALARY > 32900 조건을 추가하고 있음. 또 주의 깊게 봐야 할 사항은 NO_MERGE HINT를 사용했음에도 불구하고 CV(Complex View)내에 조건을 밀어 넣어 성능향상을 가져온다.(반드시 NO_MERGE HINT가 필요한 건 아님)
18
CBQT - CBPPD(Cost Based Predicate Push Down) 또 다른 예제
앞 Page에서 설명했듯이 CBPPD 실행 시 NO_MERGE HINT가 반드시 필요한 것은 아님. Optimizer가 유리 하다고 판단하면 CBPPD는 언제든지 자동으로 실행됨 | 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") 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; 위에서 보는 바와 같이 NO_MERGE 힌트 없이도 CBPPD가 발생한다. 인라인 뷰 내부에 SALARY > 조건이 성공적으로 Push Down 되어 처리됨
19
CBQT - SJC(Set to Join Conversion) 계속
1.내용 - 집합연산 중에서 조인으로 바꿀 수 있는 연산 MINUS, INTERSECT를 조인으로 바꿀 수 있음 2.효과 - 집합연산 수행에 발생되는 SORT UNIQUE를 HASH UNIQUE로 대체하여 SORT 부하를 없앨 수 있음 3.기타 - 교집합은 일반 조인기법으로, 차집합은 ANTI JOIN으로 처리가능 | Id | Operation | Name | Rows | Bytes | | 0 | SELECT STATEMENT | | | | | 1 | INTERSECTION | | | | | 2 | SORT UNIQUE | | | | | 3 | NESTED LOOPS | | | | | 4 | NESTED LOOPS | | | | | 5 | TABLE ACCESS BY INDEX ROWID| EMPLOYEES | | | |* 6 | INDEX RANGE SCAN | EMP_JOB_IX | | | |* 7 | INDEX UNIQUE SCAN | DEPT_ID_PK | | | | 8 | TABLE ACCESS BY INDEX ROWID | DEPARTMENTS | | | | 9 | SORT UNIQUE | | | | | 10 | NESTED LOOPS | | | | | 11 | NESTED LOOPS | | | | | 12 | TABLE ACCESS FULL | DEPARTMENTS | | | |* 13 | INDEX RANGE SCAN | EMP_DEPARTMENT_IX | | | |* 14 | TABLE ACCESS BY INDEX ROWID | EMPLOYEES | | | 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) --전통적인 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) */ AND E.SALARY > 5000;
20
CBQT - SJC(Set to Join Conversion) 계속
1.내용 - 집합연산 중에서 조인으로 바꿀 수 있는 연산 MINUS, INTERSECT를 조인으로 바꿀 수 있음 2.효과 - 집합연산 수행에 발생되는 SORT UNIQUE를 HASH UNIQUE로 대체하여 SORT 부하를 없앨 수 있음 3.기타 - 교집합은 일반 조인기법으로, 차집합은 ANTI JOIN으로 처리가능 | Id | Operation | Name | Rows | Bytes | | 0 | SELECT STATEMENT | | | | | 1 | HASH UNIQUE | | | | | 2 | NESTED LOOPS | | | | | 3 | NESTED LOOPS | | | | | 4 | NESTED LOOPS | | | | |* 5 | TABLE ACCESS BY INDEX ROWID| EMPLOYEES | | | |* 6 | INDEX RANGE SCAN | EMP_JOB_IX | | | | 7 | TABLE ACCESS BY INDEX ROWID| DEPARTMENTS | | | |* 8 | INDEX UNIQUE SCAN | DEPT_ID_PK | | | |* 9 | INDEX UNIQUE SCAN | DEPT_ID_PK | | | |* 10 | TABLE ACCESS BY INDEX ROWID | DEPARTMENTS | | | 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") --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) */ AND E.SALARY > 5000; ID 기준으로 5번에 보면 QT가 발생함(다음 Page에서 별도 설명)
21
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) */ 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) */ 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인 경우도 조인에 성공함(매우 유용한 함수임 - ★★★)
22
CBQT - SJC(Set to Join Conversion) - MINUS 사용예제(앞 Page 동일예제)
| Id | Operation | Name | Bytes | | 0 | SELECT STATEMENT | | | | 1 | MINUS | | | | 2 | SORT UNIQUE | | | | 3 | NESTED LOOPS | | | | 4 | NESTED LOOPS | | | | 5 | TABLE ACCESS BY INDEX ROWID| EMPLOYEES | | |* 6 | INDEX RANGE SCAN | EMP_JOB_IX | | |* 7 | INDEX UNIQUE SCAN | DEPT_ID_PK | | | 8 | TABLE ACCESS BY INDEX ROWID | DEPARTMENTS | | | 9 | SORT UNIQUE | | | | 10 | NESTED LOOPS | | | | 11 | NESTED LOOPS | | | | 12 | TABLE ACCESS FULL | DEPARTMENTS | | |* 13 | INDEX RANGE SCAN | EMP_DEPARTMENT_IX | | |* 14 | TABLE ACCESS BY INDEX ROWID | EMPLOYEES | | 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) | Id | Operation | Name | Bytes | | 0 | SELECT STATEMENT | | | | 1 | HASH UNIQUE | | | |* 2 | HASH JOIN ANTI | | | | 3 | VIEW | VW_STJ_ | | | 4 | NESTED LOOPS | (STJ:SET TO JOIN) | | | 5 | NESTED LOOPS | | | | 6 | TABLE ACCESS BY INDEX ROWID| EMPLOYEES | | |* 7 | INDEX RANGE SCAN | EMP_JOB_IX | | |* 8 | INDEX UNIQUE SCAN | DEPT_ID_PK | | | 9 | TABLE ACCESS BY INDEX ROWID | DEPARTMENTS | | | 10 | VIEW | VW_STJ_ | | | 11 | NESTED LOOPS | (STJ:SET TO JOIN) | | | 12 | NESTED LOOPS | | | | 13 | TABLE ACCESS FULL | DEPARTMENTS | | |* 14 | INDEX RANGE SCAN | EMP_DEPARTMENT_IX | | |* 15 | TABLE ACCESS BY INDEX ROWID | EMPLOYEES | | 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임(검증작업이 필요함)
23
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; | Id | Operation | Name | | 0 | SELECT STATEMENT | | | 1 | SORT AGGREGATE | | |* 2 | HASH JOIN | | | 3 | VIEW | VW_SQ_ | |* 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) Id 기준으로 3번을 보면 VW_SQ_1이 UNNESTING 되어 인라인 뷰로 바뀌었고 뷰 내부에 GROUP BY가 추가된 것을 볼 수 있다. 서브쿼리(VW_SQ_1)에는 GROUP BY가 없지만 QT가 GROUP BY를 추가한 것이다. SELECT /*+ QB_NAME(MAIN) */ COUNT(*) FROM EMPLOYEES S , (SELECT EMPLOYEE_ID AS ITEM_1, MAX(SALARY) AS “MAX(SALARY)” 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 | | |* 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("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) 2개의 실행계획은 완전히 같다. 메인 쿼리의 집합을 보존하기 위하여 서브쿼리로부터 만들어진 인라인 뷰에 C.EMPLOYEE_ID로 GROUP BY가 추가된 것을 볼 수 있다.
24
CBQT - CSU(Complex Subquery Unnesting) 계속
CSU Parameter 및 Hint 1.Parameter - "_unnesting_subquery"이며 default로 "true" 2.Hint - UNNEST, NO_UNNEST (메인에서 사용시 서브에 QB_NAME힌트를 사용 쿼리블럭을 지정해야 함) ▶MAIN 쿼리에서 UNNEST 사용예제 SELECT /*+ */ 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; | Id | Operation | Name | | 0 | SELECT STATEMENT | | | 1 | SORT AGGREGATE | | |* 2 | HASH JOIN | | | 3 | VIEW | VW_SQ_ | |* 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) QB_NAME을 이용하여 서브쿼리에 쿼리블럭명을 지정하여 UNNESTING 시키고 있다. ▶MAIN 쿼리에서 NO_UNNEST 사용예제 SELECT /*+ */ 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; | 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 - 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를 어떻게 사용할 것인지 잘 판단하고 사용해야 함
25
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 | FILTER | | | | | 2 | HASH GROUP BY | | | | | 3 | NESTED LOOPS | | | | | 4 | NESTED LOOPS | | | | | 5 | TABLE ACCESS BY INDEX ROWID| EMPLOYEES | | | |* 6 | INDEX RANGE SCAN | EMP_JOB_DEPT_IX | | | |* 7 | INDEX RANGE SCAN | EMP_DEPARTMENT_IX | | | | 8 | TABLE ACCESS BY INDEX ROWID | EMPLOYEES | | | 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") Plan분석 1.CVM을 하기 위해 NO_PUSH_PRED 사용 2.GROUP BY된 인라인 뷰가 사라짐 3.ID 1번 기준으로 마지막에 GROUP BY절과 FILTER(HAVING)가 만들어 짐
26
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 | NESTED LOOPS | | | | | 2 | TABLE ACCESS BY INDEX ROWID | EMPLOYEES | | | |* 3 | INDEX RANGE SCAN | EMP_JOB_DEPT_IX | | | |* 4 | VIEW PUSHED PREDICATE | | | | | 5 | SORT AGGREGATE | | | | | 6 | TABLE ACCESS BY INDEX ROWID| EMPLOYEES | | | |* 7 | INDEX RANGE SCAN | EMP_DEPARTMENT_IX | | | 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") Plan 분석 1.앞 Page의 CVM과는 다름 2.View Merging이 발생하지 않함 3.JPPD형태로 GROUP BY된 VIEW가 유지 4.인라인 뷰 바깥에서 인라인 뷰를 파고 들음 (미약하지만 CVM보다는 좋은 형태로 풀림) NO_MERGE로 제어가 가능하다고 하나, 상황에 따라 NO_PUSH_PRED HINT로 제어도 효과적임
27
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 : JOB_ID + DEPARTMENT_ID(앞에서 미리 생성함) EMP_JOB_IX : JOB_ID 2.EMP_DEPARTMENT_IX : DEPARTMENT_ID
28
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. _PHONE_NUM FROM DEPARTMENTS D , ( SELECT /*+ QB_NAME(INNER1) */ EMPLOYEE_ID, DEPARTMENT_ID, JOB_ID, AS _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 _PHONE_NUM 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 | | | | (0)| | 1 | NESTED LOOPS | | | | (0)| | 2 | TABLE ACCESS BY INDEX ROWID | DEPARTMENTS | | | (0)| |* 3 | INDEX RANGE SCAN | DEPT_LOCATION_IX | | | (0)| | 4 | VIEW | | | | (0)| | 5 | UNION ALL PUSHED PREDICATE | | | | | | 6 | TABLE ACCESS BY INDEX ROWID| EMPLOYEES | | | (0)| |* 7 | INDEX RANGE SCAN | EMP_JOB_DEPT_IX | | | (0)| | 8 | TABLE ACCESS BY INDEX ROWID| EMPLOYEES | | | (0)| |* 9 | INDEX RANGE SCAN | EMP_JOB_DEPT_IX | | | (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를 사용함)
29
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 | | | | (0)| | 1 | NESTED LOOPS OUTER | | | | (0)| | 2 | TABLE ACCESS BY INDEX ROWID | EMPLOYEES | | | (0)| |* 3 | INDEX RANGE SCAN | EMP_JOB_DEPT_IX | | | (0)| | 4 | VIEW PUSHED PREDICATE | | | | (0)| | 5 | NESTED LOOPS | | | | (0)| | 6 | TABLE ACCESS BY INDEX ROWID| DEPARTMENTS | | | (0)| |* 7 | INDEX UNIQUE SCAN | DEPT_ID_PK | | | (0)| | 8 | TABLE ACCESS BY INDEX ROWID| LOCATIONS | | | (0)| |* 9 | INDEX UNIQUE SCAN | LOC_ID_PK | | | (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와 연결되어 처리됨
30
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 | | | | (20)| | 1 | NESTED LOOPS OUTER | | | | (20)| | 2 | TABLE ACCESS FULL | DEPARTMENTS | | | (0)| | 3 | VIEW PUSHED PREDICATE | | | | (20)| | 4 | SORT GROUP BY | | | | (20)| | 5 | MERGE JOIN | | | | (20)| | 6 | TABLE ACCESS BY INDEX ROWID | JOBS | | | (0)| | 7 | INDEX FULL SCAN | JOB_ID_PK | | | (0)| |* 8 | SORT JOIN | | | | (34)| | 9 | TABLE ACCESS BY INDEX ROWID| EMPLOYEES | | | (0)| |* 10 | INDEX RANGE SCAN | EMP_DEPARTMENT_IX | | | (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)
31
CBQT - JPPD(Join Predicate Push Down) CASE3(계속)
CASE 3 - JPPD Extension 대신 CVM 발생 SELECT /*+ LEADING(D) USE_NL(E) */ (중간생략...) , ( SELECT /*+ USE_NL(E, J) */ ~~ ) E WHERE D.DEPARTMENT_ID = E.DEPARTMENT_ID; | Id | Operation | Name | Rows | Bytes | | 0 | SELECT STATEMENT | | | | | 1 | HASH GROUP BY | | | | |* 2 | HASH JOIN | | | | | 3 | MERGE JOIN | | | | | 4 | TABLE ACCESS BY INDEX ROWID| DEPARTMENTS | | | | 5 | INDEX FULL SCAN | DEPT_ID_PK | | | |* 6 | SORT JOIN | | | | | 7 | TABLE ACCESS FULL | EMPLOYEES | | | | 8 | TABLE ACCESS FULL | JOBS | | | 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") CASE 3 - JPPD Extension 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 | | | | | 1 | NESTED LOOPS | | | | | 2 | TABLE ACCESS FULL | DEPARTMENTS | | | | 3 | VIEW PUSHED PREDICATE | | | | | 4 | SORT GROUP BY | | | | | 5 | NESTED LOOPS | | | | | 6 | NESTED LOOPS | | | | | 7 | TABLE ACCESS BY INDEX ROWID| EMPLOYEES | | | |* 8 | INDEX FULL SCAN | EMP_JOB_DEPT_IX | | | |* 9 | INDEX UNIQUE SCAN | JOB_ID_PK | | | | 10 | TABLE ACCESS BY INDEX ROWID | JOBS | | | 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로 해결가능)
32
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관계가 없다고 가정 SELECT /*+ QB_NAME(MAIN) */ E.EMPLOYEE_ID, E.LAST_NAME, E. 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 ); | Id | Operation | Name | Rows | Bytes | | 0 | SELECT STATEMENT | | | | | 1 | NESTED LOOPS ANTI | | | | | 2 | TABLE ACCESS BY INDEX ROWID| EMPLOYEES | | | |* 3 | INDEX RANGE SCAN | EMP_JOB_DEPT_IX | | | |* 4 | INDEX UNIQUE SCAN | DEPT_ID_PK | | | Predicate Information (identified by operation id): 3 - access("E"."JOB_ID"='AD_ASST') 4 - access("D"."DEPARTMENT_ID"="E"."DEPARTMENT_ID") JPPD 발생 안함 CASE 3 - JPPD Extension(Anti Join JPPD 편법) -- EMPLOYEES와 DEPARTMENTS의 R/I관계가 없다고 가정 SELECT /*+ QB_NAME(MAIN) */ E.EMPLOYEE_ID, E.LAST_NAME, E. 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 | NESTED LOOPS ANTI | | | | | 2 | TABLE ACCESS BY INDEX ROWID| EMPLOYEES | | | |* 3 | INDEX RANGE SCAN | EMP_JOB_DEPT_IX | | | | 4 | VIEW PUSHED PREDICATE | VW_SQ_ | | | | 5 | NESTED LOOPS | | | | | 6 | FAST DUAL | | | | |* 7 | INDEX UNIQUE SCAN | DEPT_ID_PK | | | Predicate Information (identified by operation id): 3 - access("E"."JOB_ID"='AD_ASST') 7 - access("D"."DEPARTMENT_ID"="E"."DEPARTMENT_ID") JPPD 발생함
33
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가 고려됨을 알아두자
Similar presentations
© 2025 SlidePlayer.com Inc.
All rights reserved.