개발자의 질문
개발자 한명이 DBA 에게 질문을 던졌다. ‘스칼라 서브쿼리를 사용한 SQL 을 인라인뷰로 싸고 인라인뷰 외부에서 스칼라 서브쿼리를 Filter 조건으로 사용하면 스칼라 서브쿼리가 없어지는 현상이 발생합니다. 이 현상이 정상인가요?’ 그런데 DBA 는 그런일은 발생할 수가 없다고 하였다. 과연 사실일까? 정답은 스칼라 서브쿼리가 없어진다는 것이다. 예리한 눈을 가진 개발자 임에 틀림없다.
백견이 불여일행
SSTS( Scalar Subquery To Subquery ) 는 스칼라 서브쿼리를 서브쿼리로 변경시키는 Transformation 과정이다. 하지만 항상 변환되지 않는다. 스칼라 서브쿼리를 인라인뷰 외부에서 Filter 조건으로 사용할때만 가능하다. 아래의 SQL 을 보자
인덱스 상황 :
EMP_JOB_IX : employee (job_id)
DEPT_ID_PK1 : department(department_id)
SELECT a.employee_id, a.first_name, a.last_name, a.email
FROM (SELECT e.employee_id, e.first_name, e.last_name, email,
(SELECT location_id
FROM department d
WHERE d.department_id = e.department_id) AS location_id
FROM employee e
WHERE e.job_id = 'IT_PROG') a
WHERE a.location_id > 0;
위의 SQL 은 Location 을 스칼라 서브쿼리로 구현하였다. 하지만 스칼라 서브쿼리를 Select 하는데는 사용하지 않고 Where 조건으로 사용하는 것을 주목하라.
----------------------------------------------------------------------------------
| Id | Operation | Name | E-Rows |E-Bytes| Cost (%CPU)|
----------------------------------------------------------------------------------
|* 1 | FILTER | | | | |
| 2 | TABLE ACCESS BY INDEX ROWID| EMPLOYEE | 5 | 195 | 2 (0)|
|* 3 | INDEX RANGE SCAN | EMP_JOB_IX | 5 | | 1 (0)|
| 4 | TABLE ACCESS BY INDEX ROWID| DEPARTMENT | 1 | 7 | 1 (0)|
|* 5 | INDEX UNIQUE SCAN | DEPT_ID_PK1 | 1 | | 0 (0)|
----------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter(>0)
3 - access("E"."JOB_ID"='IT_PROG')
5 - access("D"."DEPARTMENT_ID"=:B1)
위의 Plan 을 보면 본능적으로 스칼라 서브쿼리가 서브쿼리로 바뀐 것을 알수 있어야 한다. 다시말하면 옵티마이져가 SQL 을 아래처럼 바꾼 것 이다.
SELECT e.employee_id, e.first_name, e.last_name, email
FROM employee e
WHERE e.job_id = 'IT_PROG'
AND (SELECT location_id
FROM department d
WHERE d.department_id = e.department_id) > 0 ;
----------------------------------------------------------------------------------
| Id | Operation | Name | E-Rows |E-Bytes| Cost (%CPU)|
----------------------------------------------------------------------------------
|* 1 | FILTER | | | | |
| 2 | TABLE ACCESS BY INDEX ROWID| EMPLOYEE | 5 | 195 | 2 (0)|
|* 3 | INDEX RANGE SCAN | EMP_JOB_IX | 5 | | 1 (0)|
| 4 | TABLE ACCESS BY INDEX ROWID| DEPARTMENT | 1 | 7 | 1 (0)|
|* 5 | INDEX UNIQUE SCAN | DEPT_ID_PK1 | 1 | | 0 (0)|
----------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter(>0)
3 - access("E"."JOB_ID"='IT_PROG')
5 - access("D"."DEPARTMENT_ID"=:B1)
실행계획과 Predicate Information 이 완전히 같음을 알수 있다. 이제 10053 Trace 를 분석해보자.
***************************
Order-by elimination (OBYE)
***************************
…중간생략
CVM: Merging SPJ view SEL$2 (#0) into SEL$1 (#0)
Registered qb: SEL$F5BB74E1 0xc4643d0 (VIEW MERGE SEL$1; SEL$2)
---------------------
QUERY BLOCK SIGNATURE
---------------------
signature (): qb_name=SEL$F5BB74E1 nbfros=1 flg=0
fro(0): flg=0 objn=70296 hint_alias="E"@"SEL$2"
View Merging 이 발생하여 SEL$2(인라인뷰 a) 가 SEL$1(메인쿼리)에 통합 되어 버렸다. View Merging 이 발생하여 새로운 쿼리블럭인 SEL$F5BB74E1 가 생성 되었다. 하지만 쿼리블럭 SEL$F5BB74E1 의 From 절을 보면 employee(Alias 로는 E) 만 존재하고 department(D)는 존재 하지 않는다. 그렇다면 스칼라 서브쿼리는 어디로 갔을까? 해답은 FPD(Filter Push Down) 기능에 있다.
**************************
Predicate Move-Around (PM)
**************************
…중간생략
query block SEL$F5BB74E1 (#0) unchanged
FPD: Considering simple filter push in query block SEL$F5BB74E1 (#0)
(SELECT "D"."LOCATION_ID" FROM "DEPARTMENT" "D")>0 AND "SYS_ALIAS_1"."JOB_ID"='IT_PROG'
FPD: Considering simple filter push in query block SEL$3 (#0)
"D"."DEPARTMENT_ID"=:B1
try to generate transitive predicate from check constraints for query block SEL$3 (#0)
finally: "D"."DEPARTMENT_ID"=:B1
FPD 기능에 의해서 위에서 새로 생성된 쿼리블럭 SEL$F5BB74E1 의 조건절에 서브쿼리를 생성하고 있다. 또한 새로 생성된 서브쿼리에 "D"."DEPARTMENT_ID"=:B1 조건을 밀어넣고 있다.
검증
스칼라 서브쿼리가 서브쿼리로 바뀌었으므로 서브쿼리에 사용할수 있는 힌트 Push_subq 를 사용해보자. 이것이 가능해야지만 진정한 서브쿼리이다.
SELECT /*+ PUSH_SUBQ(@SUB) */
a.employee_id, a.first_name, a.last_name, a.email
FROM (SELECT e.employee_id, e.first_name, e.last_name, email,
(SELECT /*+ QB_NAME(SUB) */ location_id
FROM department d
WHERE d.department_id = e.department_id) AS location_id
FROM employee e
WHERE e.job_id = 'IT_PROG') a
WHERE a.location_id > 0;
----------------------------------------------------------------------------------
| Id | Operation | Name | E-Rows |E-Bytes| Cost (%CPU)|
----------------------------------------------------------------------------------
|* 1 | TABLE ACCESS BY INDEX ROWID | EMPLOYEE | 1 | 39 | 2 (0)|
|* 2 | INDEX RANGE SCAN | EMP_JOB_IX | 5 | | 1 (0)|
| 3 | TABLE ACCESS BY INDEX ROWID| DEPARTMENT | 1 | 7 | 1 (0)|
|* 4 | INDEX UNIQUE SCAN | DEPT_ID_PK1 | 1 | | 0 (0)|
----------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter(>0)
2 - access("E"."JOB_ID"='IT_PROG')
4 - access("D"."DEPARTMENT_ID"=:B1)
실행계획을 보면 알겠지만 Subquery Pushing 이 발생하여 Id 기준으로 2번과 3번이 동일 Level 상에 존재한다. Subquery Pushing 이 성공적으로 수행된것을 알수 있다.
한단계 더 나아가 보자
EMPLOYEE 테이블에 JOB_ID, DEPARTMENT_ID 를 생성 하였다. 바로 위에서 실행한 SQL 을 다시 실행 해보자.
--> 여기서 PUSH_SUBQ 를 적용한 SQL 실행한다.
---------------------------------------------------------------------------------------
| Id | Operation | Name | E-Rows |E-Bytes| Cost (%CPU)|
---------------------------------------------------------------------------------------
| 1 | TABLE ACCESS BY INDEX ROWID | EMPLOYEE | 1 | 39 | 2 (0)|
|* 2 | INDEX RANGE SCAN | EMP_JOB_DEPT_IX | 1 | | 1 (0)|
| 3 | TABLE ACCESS BY INDEX ROWID| DEPARTMENT | 1 | 7 | 1 (0)|
|* 4 | INDEX UNIQUE SCAN | DEPT_ID_PK1 | 1 | | 0 (0)|
---------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("E"."JOB_ID"='IT_PROG')
filter(>0)
4 - access("D"."DEPARTMENT_ID"=:B1)
이것은 제공자 서브쿼리가 아니다
실행계획은 마치 제공자 서브쿼리처럼 바뀌었지만 이것은 제공자 서브쿼리가 아니고 Subquery Pushing 에 의한 효과이다. Predicate Information 을 주목하라. ID 기준으로 2번에서 Filter 가 수행된다. . (JOB_ID, DEPARTMENT_ID) 인덱스를 사용하지 않은 SQL의 Predicate Information을 보면 EMPLOYEE 테이블 엑세스 시에 filter 가 발생함을 알수 있다. 이것이 바로 Subquery Pushing 과 인덱스에 의한 Early Filter 의 효과이다. 물론 Early Filter 의 개념은 조인순서상에 서브쿼리를 최대한 먼저 조인 하는것으로 바꾸는 것이다. 하지만 위와 같은 Early Filter의 부가적인 기능도 있음을 알아야 한다.
Subquery Pushing 에 의한 Early Filter 기능은 Using Sub query Method( Filter / Access sub Query ) 글을 참조하라
결론:
이처럼 Oracle Query Transformer 는 스칼라 서브쿼리를 Filter 조건으로 사용할 때 SSTS를 발생시킨다.. 여러분이 Query Transformation 을 의도 하던 의도하지 않던 말이다.
PS :
제목이 이상하다는 의견이 있다. 제목은 옵티마이져가 변환을 수행한다는 의미이다. 수동으로 스칼라 서브쿼리를 서브쿼리로 고치라는 의미가 아니므로 착오가 없길 바란다.
'Oracle > Optimizer' 카테고리의 다른 글
Transformer - SQL 튜닝의 새로운 패러다임 (11) | 2009.10.15 |
---|---|
Temp Table Transformation (5) | 2009.09.28 |
Semi Join 의 재조명 (5) | 2009.08.31 |
Group By 를 먼저 수행하고 Join 하라( Group By Placement ) (4) | 2009.08.24 |
Parallel Query 수행시 Group by 를 먼저 수행하라(Group By Push Down) (9) | 2009.08.12 |