본문 바로가기

데이터베이스/SQL 튜닝

[SQL튜닝/옵티마이저 원리] 쿼리 변환1 (서브쿼리 Unnesting, push_subq, 뷰 Merging, 뷰 Merging이 불가한 경우)

🖥️ 들어가며

📌 쿼리 변환이란?
1. 휴리스틱 쿼리 변환
- 결과만 보장된다면 무조건 쿼리 변환을 수행합니다.
- 일종의 규칙기반 최적화 기법입니다.
- 경험적으로 항상 더 나은 성능을 보일 것이라는 옵티마이저 개발팀의 판단이 반영되어 있습니다.

2. 비용 기반 쿼리 변환
- 변환된 쿼리의 비용이 더 낮을 때만 활용합니다.
- 낮지 않을 경우, 원본 쿼리를 이용 최적화합니다.

3. 오라클 버전에 따른 쿼리 변환의 변화
- 8i: 복합 뷰(Complex View) Merging 을 사용자 힌트에 의해서만 수행
- 9i: 결과가 보장될 수 있는 형태라면 무조건 변환 시도
- 10g: CBO 모드로 동작하기 시작 (쿼리 변환의 결과가 더 나은 성능을 낼 것으로 판단될 때만 수행)

 


✏️ 1. 서브쿼리 Unnesting

  • nest: 상자 등을 차곡차곡 포개 넣다, 중첩. ↔ unnest: 중첩된 상태를 풀어낸다.
  • DBMS가 서브쿼리를 최적화하기 위해 내부적으로 Join 형태로 변경하는 것을 의미합니다.
  • 서브쿼리 분류
    • 1. 인라인 뷰
      : from 절에 나타나는 서브쿼리
    • 2. 중첩된 서브쿼리
      : 결과집합을 한정하기 위해 where절에 사용된 서브쿼리
    • 3. 스칼라 서브쿼리
      : 한 레코드당 정확히 하나의 컬럼값만 리턴하는 것이 특징, 주로 select-list에서 사용되지만 몇 가지 예외사항을 제외하면 컬럼이 올 수 있는 대부분 위치에서 사용 가능
  • 중첩된 서브쿼리(Nested Subquery)
    • 메인쿼리와 부모 자식이라는 종속적, 계층적 관계가 존재합니다.
    • 따라서 논리적 관점에서 그 처리과정은 IN, Exists를 불문하고 필터 방식이어야 합니다.
    • 즉, 메인쿼리에서 읽히는 레코드마다 서브쿼리 반복 수행조건에 맞지 않는 데이터를 골라내는 것입니다.
    • 하지만 서브쿼리를 처리할 때 필터 방식이 항상 최적의 수행속도를 보장하지 못합니다.
    • 옵티마이저는 둘 중 하나를 선택
      • 옵티마이저의 선택 1. 서브쿼리 Unnesting
        : 동일한 결과를 보장하는 조인문을 변환하고 나서 최적화합니다.
      • 옵티마이저의 선택 2. 서브쿼리에 필터(Filter) 오퍼레이션
        : 서브쿼리를 Unnesting 하지 않고 원래 상태에서 최적화합니다. 메인쿼리와 서브쿼리를 별도 서브플랜으로 구분해 각각 최적화 수행 → 이때 서브쿼리에 필터 오퍼레이션이 나타납니다.
  • 서브쿼리 Unnesting의 이점
    • 서브쿼리를 메인쿼리와 같은 레벨로 풀어내면 → 다양한 액세스 경로, 조인 메소드를 평가할 수 있습니다.
    • 조인 형태로 변환했을 경우, 더 나은 실행계획을 찾을 가능성이 증가합니다.
    • 서브쿼리 Unnesting 관련 힌트
      • unnest: 서브쿼리를 Unnesting → 조인방식으로 최적화하도록 유도
      • no_unnest: 서브쿼리를 그대로 둔 상태 → 필터 방식으로 최적화하도록 유도
    • 하지만 서브쿼리를 Unnesting한 결과가 항상 좋은 것은 아닙니다.
    • 최근 옵티마이저는 서브쿼리 Unnesing 여부에 대해 Cost를 산정 후 결정하는 방향으로 발전하였습니다. (10g)
  • 서브쿼리 기본 예시
    • Unnesing 하지 않은 경우
      • 옵티마이저가 서브쿼리를 별도의 서브플랜으로 최적화합니다.
      • 이처럼 Unnesting 하지 않은 서브쿼리를 수행할 때는 메인쿼리에서 읽히는 레코드마다 값을 넘기면서 서브쿼리를 반속 수행해야 합니다.
    • Unnesting 한 경우
      • 서브쿼리인데도 일반적인 Nested Loop Join 방식으로 수행되었습니다.
      • 1차 쿼리변환: 서브쿼리 Unnesting
        select *
        from   (select deptno from dept) a, emp b
        where b.deptno = a.deptno;
      • 2차 쿼리변환: View Merging -- 본 쿼리문의 경우, View Merging까지 되었기에 실행계획에 View가 없음
        select emp.*
        from   dept, emp
        where emp.deptno = dept.deptno;
-- Unnesting 하지 않은 경우
select *
from emp
where deptno in (select /*+ no_unnest */ deptno from dept)
 ------------------------------------------------------------------------------
| Id  | Operation          | Name    | Rows  | Bytes | Cost (%CPU)| Time     |
------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |         |     5 |   185 |     3   (0)| 00:00:01 |
|*  1 |  ☆FILTER☆        |         |       |       |            |          |
|   2 |   TABLE ACCESS FULL| EMP     |    14 |   518 |     3   (0)| 00:00:01 |
|*  3 |   INDEX UNIQUE SCAN| PK_DEPT |     1 |     3 |     0   (0)| 00:00:01 |
------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   1 - filter( EXISTS (SELECT /*+ NO_UNNEST */ 0 FROM "DEPT"
              "DEPT" WHERE "DEPTNO"=:B1))
   3 - access("DEPTNO"=:B1)


-- Unnesting 한 경우
select *
from emp
where deptno in (select /*+ unnest */ deptno from dept)
----------------------------------------------------------------------------------------------
| Id  | Operation                   | Name           | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |                |    10 |   350 |     2   (0)| 00:00:01 |
|   1 |  TABLE ACCESS BY INDEX ROWID| EMP            |     3 |    99 |     1   (0)| 00:00:01 |
|   2 |   ☆NESTED LOOPS☆          |                |    10 |   350 |     2   (0)| 00:00:01 |
|   3 |    INDEX FULL SCAN          | DEPT_PK        |     4 |     8 |     1   (0)| 00:00:01 |
|*  4 |    INDEX UNIQUE SCAN        | EMP_DEPTNO_IDX |     3 |       |     0   (0)| 00:00:01 |
----------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   4 - access("DEPTNO"="DEPTNO")

 

  • 서브쿼리가 M쪽 집합이거나 Non-Unique 인덱스일 때
select *
from   emp
where  deptno in (select deptno from dept);
  • 위 예제의 특성
    • 서브쿼리가 1쪽 집합
    • dept 테이블에 deptno가 PK
    • 즉, 결과집합이 emp 테이블 수준으로 출력됩니다.
  • 🤖 옵티마이저의 고민
    • 서브쿼리가 M쪽 집합
    • dept 테이블에 PK 인덱스가 없어, 1쪽 집합임을 옵티마이저가 확신하지 못할 경우
    • 이때는 서브쿼리 Unnesting 시도 시 결과집합이 서브쿼리(M쪽) 수준으로 출력됩니다.
    • emp 테이블과 dept 테이블
      • 1쪽 집합: 하나의 부서(dept)에 여러 직원(emp)이 소속될 수 있음. 즉, dept는 1쪽.
      • M쪽 집합: 여러 직원(emp)이 포함된 테이블. 즉, emp는 M쪽.
select *
from   dept
where  deptno in (select deptno from emp);
=> 서브쿼리 Unnesting
select *
from   (select deptno from emp) a, dept b
where  a.deptno = b.deptno;
dept 테이블   emp 테이블
deptno deptname   empno name deptno
01 인사부   1 홍길동 01
02 총무부   2 김길동 02
03 고객관리부   3 홍길순 01
09 승인부   4 이순신 03
      5 이태백 01
      6 삼태백 04
  • 1쪽 결과집합: emp 테이블의 데이터 중 부서번호가 일치하는 직원만 출력
empno name deptno
1 홍길동 01
2 김길동 02
3 홍길순 01
4 이순신 03
5 이태백 01
  • M쪽 결과집합: dept와 emp를 Join한 결과는, emp 테이블을 기준으로 중복 데이터도 모두 포함
deptno deptname empno name
01 인사부 1 홍길동
01 인사부 3 홍길순
01 인사부 5 이태백
02 총무부 2 김길동
03 고객관리부 4 이순신

 

  • 옵티마이저의 선택
    • 1쪽 집합임을 확신할 수 없는 서브쿼리쪽이 드라이빙 된다면
      • 먼저 Sort Unique 오퍼레이션 수행 → 1쪽 집합으로 만든 뒤 → Join
      • 즉, M쪽을 확실한 1쪽으로 만들어주기 위해 Sort Unique 오퍼레이션을 수행하는 것입니다.
    • 메인쿼리쪽 테이블이 드라이빙 된다면
      • 세미 조인 방식으로 조인 (조인이 성사되는 첫 번째 행을 찾으면, 더 이상 Inner 테이블을 탐색하지 않고, 다음 Outer 테이블의 행으로 넘어감)
  • ⭐⭐⭐ Sort Unique 오퍼레이션 수행
    • dept 테이블에서 PK 제약 제거, deptno 컬럼에 Non-Unique 인덱스 생성하였습니다.
    • 쿼리 분석
      • dept 테이블에서 deptno 값을 가져올 때 Sort Unique 중복 데이터를 제거 → 이후 emp 테이블과 조인하는 방식
      • 인덱스를 사용해 dept에서 빠르게 deptno을 스캔하고 → 중복 제거 → emp 테이블과 Nested Loops 조인으로 연결됩니다.
  • ⭐⭐ 세미 조인 방식으로 수행
    • 세미 조인: 기본적으로 NL 조인과 동일한 프로세스로 진행하지만, Outer 테이블의 한 로우가 Inner 테이블의 한 로우와 조인에 성공하는 순간 진행을 멈추고 Outer 테이블의 다음 로우를 계속 처리하는 방식
    • 세미 조인 방식으로 변환의 장점: NL 세미조인, Hash 세미조인, Sort Merge 세미조인도 가능하다는 것입니다.
    • 쿼리 분석
      • emp 테이블에서 deptno가 dept 의 deptno와 일치하는 행만 반환합니다.
      • 세미 조인: emp 테이블의 각 해에 대해 dept 탐색 → 첫 번째 매칭이 발견되면 멈추고 다음 emp 행으로 넘어갑니다.
-- Sort Unique 오퍼레이션 수행
alter table dept drop primary key;
create index dept_deptno_idx on dept(deptno);

SQL> explain plan for
  2  select /*+ leading(dept@qb1) use_nl(emp) */ * from emp
  3  where  deptno in (select /*+ unnest qb_name(qb1) */ deptno from dept)
  4  ;

--------------------------------------------------------------------------------------
| Id  | Operation          | Name            | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |                 |    14 |   560 |     6  (17)| 00:00:01 |
|   1 |  NESTED LOOPS      |                 |    14 |   560 |     6  (17)| 00:00:01 |
|   2 | ☆SORT UNIQUE☆    |                 |     4 |    12 |     1   (0)| 00:00:01 |
|   3 |    INDEX FULL SCAN | DEPT_DEPTNO_IDX |     4 |    12 |     1   (0)| 00:00:01 |
|*  4 |   TABLE ACCESS FULL| EMP             |     4 |   148 |     2   (0)| 00:00:01 |
--------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   4 - filter("DEPTNO"="DEPTNO")
   
-- 세미 조인 방식 수행
SQL> explain plan for
  2  select * from emp
  3  where  deptno in (select deptno from dept)
  4  ;

--------------------------------------------------------------------------------------
| Id  | Operation          | Name            | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |                 |    14 |   560 |     3   (0)| 00:00:01 |
|   1 |☆NESTED LOOPS SEMI☆|                |    14 |   560 |     3   (0)| 00:00:01 |
|   2 |   TABLE ACCESS FULL| EMP             |    14 |   518 |     3   (0)| 00:00:01 |
|*  3 |   INDEX RANGE SCAN | DEPT_DEPTNO_IDX |     4 |    12 |     0   (0)| 00:00:01 |
--------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   3 - access("DEPTNO"="DEPTNO")

 

 

 

✏️ 2. Pushing 서브쿼리

  • Unnesting되지 않은 서브쿼리는 항상 필터 방식으로 처리되고, 대개 마지막 단계에 처리됩니다.
  • 서브쿼리를 먼저 처리하여 다음 수행단계로 넘어가는 Rows 수를 줄일 수 있다면 성능이 향상될 것입니다.
  • Pushing 서브쿼리
    • 실행계획상 가능한 앞 단계에서 서브쿼리 필터링이 처리되도록 강제하는 것입니다.
    • ⭐⭐⭐ 힌트: push_subq
    • Unnesting 되지 않은 서브쿼리에만 작동 (no_unnest 힌트와 같이 기술하는 것이 올바름)
  • no_push_subq, push_subq 예시
    • no_push_subq
      • 서브쿼리를 푸시하지 않고 수행합니다.
      • 테이블 스캔: Table Access Full emp1, Table Access By Index RowID emp2
      • 서브쿼리: Filter, Nested Loops 방식으로 처리
      • 서브쿼리는 dept 테이블에서 deptno와 loc 조건을 확인하면서 exists 조건을 평가합니다.
      • 성능: CPU 시간 0.06초 | 디스크 I/O: 37 | 쿼리 시간 14358
    • push_subq
      • 서브쿼리를 푸시하여 외부 테이블 emp1, emp2에서 필터링 전에 DEPT 테이블을 조회합니다.
      • 서브쿼리: dept 테이블에 대해 미리 필터링 수행 → 메인 테이블 조회
      • 서브쿼리: Filter, Nested Loops 방식으로 처리
      • 성능: CPU 시간 0.01초 | 디스크 I/O: 0 | 쿼리 시간 3356
  • ⭐⭐⭐ push_subq 정리
    • 1. Join과 서브쿼리의 처리 순서
      • 기존: 조인 후 서브쿼리 처리
      • 변경: 서브쿼리를 먼저 처리한 후 조인 실행
    • 2. 인덱스 활용
      • 메인쿼리에서 제공하는 인덱스를 포함하여 처리
      • 인덱스끼리 연산을 먼저 수행한 후 필요한 경우에만 테이블 Random Access 실행 (I/O 줄임)
    • 3. 효과적인 사용 조건
      • 서브쿼리의 필터링 효과와 캐싱 효과가 클 때 유용
      • 메인쿼리(M):서브쿼리(1)의 비율일 때 효과적, 반대의 경우 효과 없음
-- emp 테이블을 1000번 복제한 emp1, emp2 테이블 생성
create table emp1 as
select * from emp, (select rownum no from dual connect by level <= 1000);

create table emp2 as select * from emp1;

alter table emp1 add constraint emp1_pk primary key(no, empno);

alter table emp2 add constraint emp2_pk primary key(no, empno);

-- no_push_subq
select /*+ leading(e1) use_nl(e2) */
       sum(e1.sal)
     , sum(e2.sal)
from   emp1 e1
     , emp2 e2
where  e1.no    = e2.no
and    e1.empno = e2.empno
and    exists (select /*+ NO_UNNEST NO_PUSH_SUBQ */ 'x'
               from   dept
               where  deptno = e1.deptno
               and    loc    = 'NEW YORK')

call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        1      0.01       0.03          0         10          0           0
Execute      1      0.00       0.00          0          0          0           0
Fetch        2      0.04       0.11         37      14348          0           1
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total        4      0.06       0.14         37      14358          0           1

Misses in library cache during parse: 1
Optimizer mode: ALL_ROWS
Parsing user id: 91  

Rows     Row Source Operation
-------  ---------------------------------------------------
      1  SORT AGGREGATE (cr=14348 pr=37 pw=37 time=0 us)
   3000   FILTER  (cr=14348 pr=37 pw=37 time=462 us)
  14000    NESTED LOOPS  (cr=14342 pr=35 pw=35 time=519 us)
  14000     NESTED LOOPS  (cr=342 pr=35 pw=35 time=286 us cost=12719 size=2002 card=22)
  14000      TABLE ACCESS FULL EMP1 (cr=95 pr=0 pw=0 time=56 us cost=29 size=758472 card=14586)
  14000      INDEX UNIQUE SCAN EMP2_PK (cr=247 pr=35 pw=35 time=0 us cost=0 size=0 card=1)(object id 91815)
  14000     TABLE ACCESS BY INDEX ROWID EMP2 (cr=14000 pr=0 pw=0 time=0 us cost=1 size=39 card=1)
      1    TABLE ACCESS BY INDEX ROWID DEPT (cr=6 pr=2 pw=2 time=0 us cost=2 size=11 card=1)
      3     INDEX RANGE SCAN DEPT_DEPTNO_IDX (cr=3 pr=1 pw=1 time=0 us cost=1 size=0 card=1)(object id 91722)


-- push_subq
select /*+ leading(e1) use_nl(e2) */
       sum(e1.sal)
     , sum(e2.sal)
from   emp1 e1
     , emp2 e2
where  e1.no    = e2.no
and    e1.empno = e2.empno
and    exists (select /*+ NO_UNNEST PUSH_SUBQ */ 'x'
               from   dept
               where  deptno = e1.deptno
               and    loc    = 'NEW YORK')

call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        1      0.00       0.00          0          8          0           0
Execute      1      0.00       0.00          0          0          0           0
Fetch        2      0.01       0.01          0       3348          0           1
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total        4      0.01       0.02          0       3356          0           1

Misses in library cache during parse: 1
Optimizer mode: ALL_ROWS
Parsing user id: 91  

Rows     Row Source Operation
-------  ---------------------------------------------------
      1  SORT AGGREGATE (cr=3348 pr=0 pw=0 time=0 us)
   3000   NESTED LOOPS  (cr=3348 pr=0 pw=0 time=157 us)
   3000    NESTED LOOPS  (cr=348 pr=0 pw=0 time=95 us cost=663 size=91 card=1)
   3000     TABLE ACCESS FULL EMP1 (cr=101 pr=0 pw=0 time=37 us cost=29 size=37908 card=729)
      1      TABLE ACCESS BY INDEX ROWID DEPT (cr=6 pr=0 pw=0 time=0 us cost=2 size=11 card=1)
      3       INDEX RANGE SCAN DEPT_DEPTNO_IDX (cr=3 pr=0 pw=0 time=0 us cost=1 size=0 card=1)(object id 91722)
   3000     INDEX UNIQUE SCAN EMP2_PK (cr=247 pr=0 pw=0 time=0 us cost=0 size=0 card=1)(object id 91815)
   3000    TABLE ACCESS BY INDEX ROWID EMP2 (cr=3000 pr=0 pw=0 time=0 us cost=1 size=39 card=1)

 

 

✏️ 3. 뷰 Merging

  • 뷰 Merging이란
    • 서브쿼리 Unnesting과 함께 가장 중요한 쿼리변환 기법 중 하나입니다.
    • 말 그대로 View를 메인쿼리 안에 통합시킨다는 의미입니다.
  • 쿼리문 1 vs. 2 비교
    • 쿼리 1
      • 사람이 읽긴 편하지만 옵티마이저에겐 좋지 않은 쿼리입니다.
      • 옵티마이저는 가급적 위와 같은 쿼리 블록을 아래처럼 풀어내려는 습성이 있습니다.
    • 쿼리 2
      • 쿼리 1의 뷰 쿼리 블록은 액세스 쿼리 블록과 머지 과정을 거쳐 → 쿼리 2와 같은 형태로 변환됩니다. (뷰 머징)
      • 이와 같이 쿼리가 풀어지면 옵티마이저는 더욱 다양한 액세스 경로를 탐색할 수 있습니다.
-- 쿼리 1: 뷰
select *
  from (select * from emp where job = 'SALESMAN') a
     , (select * from dept where loc = 'CHICAGO') b
 where a.deptno = b.deptno

-- 쿼리 2: 뷰 머징
select *
  from emp a, dept b
 where a.deptno = b.deptno
   and a.job = 'SALESMAN'
   and b.loc = 'CHICAGO'

 

  • 단순 뷰(Simple View) Merging
    • ⭐⭐⭐ 조건절과 조인문만을 포함하는 단순 뷰는 no_merge 힌트를 사용하지 않는 한, 언제든 Merge가 일어납니다.
    • 단순 뷰는 Merging 해도 성능이 나빠지지 않습니다.
create or replace view emp_salesman
as
select empno, ename, job, mgr, hiredate, sal, comm, deptno
from   emp
where  job = 'SALESMAN' ;

-- no_merge: 뷰 머징이 일어날 수 없음
select /*+ no_merge(e) */ e.empno, e.ename, e.job, e.mgr, e.sal, d.dname
from   emp_salesman e, dept d
where  d.deptno = e.deptno
and    e.sal >= 1500 ;

--------------------------------------------------------------
| Id  | Operation                    | Name         | E-Rows |
--------------------------------------------------------------
|   0 | SELECT STATEMENT             |              |        |
|   1 |  NESTED LOOPS                |              |        |
|   2 |   NESTED LOOPS               |              |      1 |
|   3 |    VIEW                      | EMP_SALESMAN |      1 | --- 뷰 그대로 사용
|*  4 |     TABLE ACCESS FULL        | EMP          |      1 |
|*  5 |    INDEX UNIQUE SCAN         | DEPT_PK      |      1 |
|   6 |   TABLE ACCESS BY INDEX ROWID| DEPT         |      1 |
--------------------------------------------------------------

-- View Merging
select /*+ merge(e) */ e.empno, e.ename, e.job, e.mgr, e.sal, d.dname
from   emp_salesman e, dept d
where  d.deptno = e.deptno
and    e.sal >= 1500 ;

---------------------------------------------------------
| Id  | Operation                    | Name    | E-Rows |
---------------------------------------------------------
|   0 | SELECT STATEMENT             |         |        |
|   1 |  NESTED LOOPS                |         |        |
|   2 |   NESTED LOOPS               |         |      1 |
|*  3 |    TABLE ACCESS FULL         | EMP     |      1 |
|*  4 |    INDEX UNIQUE SCAN         | DEPT_PK |      1 |
|   5 |   TABLE ACCESS BY INDEX ROWID| DEPT    |      1 |
---------------------------------------------------------

 

  • 복합 뷰(Complex View) Merging
    • Group by절, Select 절에 Distinct 연산자를 포함합니다.
    • 복합 뷰는 \_complex_view_merging 파라미터를 true로 설정할 때만 Merging이 일어납니다.
    • ⭐⭐⭐ 이러한 복합 뷰를 포함하는 뷰 Merging은 성능이 나빠질 수 있습니다.
      • Group by 절
      • Select 절에 Distinct 연산자 포함
    • 쿼리 수행 비용 조사 후 적용여부를 판단하는 방향으로 옵티마이저가 발전하고 있습니다.
      (9i에서는 뷰를 무조건 머징, 10g부터 비용기반 쿼리 변환 방식으로 전환)
select d.dname, avg_sal_dept
  from dept d
    , (select deptno, avg(sal) avg_sal_dept
         from emp
        group by deptno) e
 where d.deptno = e.deptno
   and d.loc = 'CHICAGO'

-----------------------------
| Id  | Operation           |
-----------------------------
|   0 | SELECT STATEMENT    |
|   1 |  HASH GROUP BY      |
|*  2 |   HASH JOIN         | --- 뷰를 해체하여 일반 조인으로 변경
|*  3 |    TABLE ACCESS FULL|
|   4 |    TABLE ACCESS FULL|
-----------------------------

-- no_merge
select d.dname, avg_sal_dept
  from dept d
    , (select /*+no_merge*/deptno, avg(sal) avg_sal_dept
         from emp
        group by deptno) e
 where d.deptno = e.deptno
   and d.loc = 'CHICAGO'

-------------------------------
| Id  | Operation             |
-------------------------------
|   0 | SELECT STATEMENT      |
|*  1 |  HASH JOIN            |
|   2 |   JOIN FILTER CREATE  |
|*  3 |    TABLE ACCESS FULL  |
|   4 |   VIEW                | --- 뷰 그대로 사용
|   5 |    HASH GROUP BY      |
|   6 |     JOIN FILTER USE   |
|*  7 |      TABLE ACCESS FULL|
-------------------------------

 

  • ⭐⭐⭐ 집CR 함수는 불가, Group By는 가능
  • 뷰 Merging이 불가능한 경우
    • 1. 합(Set) 연산자: UNION, UNION ALL, INTERSECT, MINUS
    • 2. Connect By 절
    • 3. ROWNUM Pseudo 컬럼
    • 4. Select-list에 집계 함수(AVG, COUNT, MAX, MIN, SUM) 사용
    • 5. 분석 함수

 


📒 정리하면

  1. 서브쿼리 Unnesting: 중첩 서브쿼리를 조인 형태로 변환, 다양한 액세스 경로 탐색이 가능합니다.
    (힌트: unnest / no_unnest)
  2. Unnesting 미실행 시, 메인쿼리 레코드마다 서브쿼리를 반복 수행합니다.
  3. 옵티마이저의 선택
    1. 1쪽 집합임을 확인할 수 없는 서브쿼리 드라이빙: Sort Unique 오퍼레이션 수행
      (Sort Unique: M쪽 집합을 1쪽 변환 시 사용, 중복 제거 후 조인)
    2. 메인쿼리쪽 드라이빙: 세미 조인 방식으로 조인합니다.
      (세미 조인: 첫 매칭 시 다음 컬럼으로 넘어감, NL/Hash/Sort Merge 세미조인 방식 적용 가능)
  4. Pushing 서브쿼리: push_subq 힌트. 서브쿼리 필터링을 조인 전 수행, I/O 감소 효과가 있습니다.
    1. 적용 요건: 메인쿼리(M):서브쿼리(1) 비율 시 효과적, 반대의 경우 비효율적입니다.
    2. 인덱스 활용: 메인쿼리 인덱스 기반으로 연산을 먼저 처리하고, 테이블 Random Access를 최소화합니다.
  5. 뷰 Merging: 단순 뷰는 기본 머지, 복합 뷰는 _complex_view_merging 파라미터로 제어합니다. (+ 복합 뷰는 CBO 판단 필요)
  6. 뷰 Merging이 불가한 경우: 집CR 함수는 불가, Group By는 가능
    1. 집: 집합 연산자, Union, Union All, Intersect, Minus
    2. C: Connect By 절
    3. R: Rownum Pseudo 컬럼
    4. 함수: Select-list에 집계 함수, 분석 함수

 


반응형