@markdown
# 쿼리변환(2)
<br/>
## 조건절 이행
____
- A 테이블에서 사용된 필터 조건이 조인 조건절을 타고 반대편 B 테이블에 대한 필터 조건으로 이행되는 경우
<pre><code class="sql" style="font-size:15px">SELECT *
FROM DEPT D, EMP E
WHERE E.JOB = 'MANAGER'
AND E.DEPTNO = 10
AND D.DEPTNO = E.DEPTNO;
-----------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-----------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 7 |
| 1 | NESTED LOOPS | | 1 | | 1 |00:00:00.01 | 7 |
| 2 | NESTED LOOPS | | 1 | 1 | 3 |00:00:00.01 | 5 |
| 3 | TABLE ACCESS BY INDEX ROWID| DEPT | 1 | 1 | 1 |00:00:00.01 | 3 |
|* 4 | INDEX RANGE SCAN | DEPT_PK | 1 | 1 | 1 |00:00:00.01 | 2 |
|* 5 | INDEX RANGE SCAN | FK_EMP | 1 | 3 | 3 |00:00:00.01 | 2 |
|* 6 | TABLE ACCESS BY INDEX ROWID | EMP | 3 | 1 | 1 |00:00:00.01 | 2 |
-----------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
4 - access("D"."DEPTNO"=10)
5 - access("E"."DEPTNO"=10)
6 - filter("E"."JOB"='MANAGER')
</code></pre>
- 위 쿼리에서 deptno = 10은 emp 테이블에 대한 필터조건이다. 하지만 실행계획에 나타난 정보를 보면 dept 테이블에도 같은 조건이 추가된 것을 확인할 수 있다.
- `e.deptno = 10` → `e.deptno = d.deptno` → `d.deptno = 10`으로 추론이 되어 조건절 이행이 일어나 쿼리 변환이 일어났다.
- 이렇게 조건절 이행으로 인해 Hash Join, Merge Join 등을 수행하기 전에 emp, dept 테이블에 필터링을 적용함으로써 조인되는 데이터 양을 줄일 수 있는 효과가 있다.
<br/>
## 불필요한 조인 제거
____
- 결과 집합에 영향을 미치지 않는 조인 조건을 제거함
- 1:M 관계인 두 테이블을 조인하는 쿼리문에서 조인문을 제외한 어디에서도 1쪽 테이블을 참조하지 않는다면, 쿼리 수행시 불필요해 결과집합에 영향을 미치지 않는다.
- 이런 경우 옵티마이저는 M쪽 테이블만 읽도록 불필요한 조인을 제거하여 쿼리를 변환한다.
- 이 조인 제거 기능을 Oracle에서 사용하기 위해선 PK, FK 제약 설정해주어야 하고, Null 허용 칼럼을 사용하는 조인 조건을 삭제 시 문제가 될 수 있기 때문에 옵티마이저가 내부적으로 `e.deptno is not null` 조건을 추가해준다.
## OR 조건을 Union으로 변환(OR-Expansion)
____
<pre><code class="sql" style="font-size:15px">SELECT * FROM EMP
WHERE JOB = 'CLERK' OR DEPTNO = 20
</code></pre>
- 위의 쿼리가 그대로 실행되면 OR 조건이므로 Full Table Scan으로 처리된다.
- 이때 JOB과 DEPTNO에 각각 생성된 인덱스를 사용하고 싶다면 아래와 같이 UNION ALL 형태로 바꿔주면 됨
<pre><code class="sql" style="font-size:15px">SELECT * FROM EMP
WHERE JOB = 'CLERK'
UNION ALL
SELECT * FROM EMP
WHERE DEPTNO = 20
AND LNNVL(JOB='CLERK')
</code></pre>
- JOB과 DEPTNO 칼럼을 선두로 갖는 두 인덱스가 각각 사용되었고, UNION ALL 위쪽 브랜치는 JOB = 'CLERK'인 집합을 읽고 아래쪽 브랜치는 DEPTNO = 20인 집합만을 읽는다.
- 이때 EMP 테이블 액세스가 두번 일어났고, 중복 액세스가 일어나더라도 결과집합에는 중복이 없게 하려고 UNION ALL 아래쪽에 Oracle이 내부적으로 LNNVL 함수를 사용한 것을 확인할 수 있다.
- Oracle에서 OR-Expansion을 제어하기 위한 힌트로 `USE_CONCAT`, `NO_EXPAND`가 있다.
<br/>
## 기타 쿼리 변환
____
### 집합 연산을 조인으로 변환
- `Intersect`나 `Minus` 같은 집합 연산을 조인 형태로 변환하는 것을 말함
- 아래의 deptno=10에 속한 사원들의 JOB, MGR을 제외시키고 나머지 JOB, MGR 집합을 찾는 쿼리인데, Sort Unique 연산을 수행한 후에 MINUS 연산을 수행한 것을 확인할 수 있다.
<pre><code class="sql" style="font-size:15px">SELECT JOB, MGR FROM EMP
MINUS
SELECT JOB, MGR FROM EMP
WHERE DEPTNO = 10;
----------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
----------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 12 | 204 | 8 (63)| 00:00:01 |
| 1 | MINUS | | | | | |
| 2 | SORT UNIQUE | | 12 | 144 | 4 (25)| 00:00:01 |
| 3 | TABLE ACCESS FULL| EMP | 12 | 144 | 3 (0)| 00:00:01 |
| 4 | SORT UNIQUE | | 4 | 60 | 4 (25)| 00:00:01 |
|* 5 | TABLE ACCESS FULL| EMP | 4 | 60 | 3 (0)| 00:00:01 |
PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
5 - filter("DEPTNO"=10)
</code></pre>
<br/>
- 아래의 쿼리는 옵티마이저가 MINUS 연산을 조인 형태로 변환했을 떄의 실행계획이다.
<pre><code class="sql" style="font-size:15px">SELECT /*+ opt_param('_convert_set_to_join','true' ) */ JOB, MGR FROM EMP
MINUS
SELECT JOB, MGR FROM EMP
WHERE DEPTNO = 10;
----------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
----------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 2 | 54 | 8 (25)| 00:00:01 |
| 1 | HASH UNIQUE | | 2 | 54 | 8 (25)| 00:00:01 |
|* 2 | HASH JOIN ANTI | | 2 | 54 | 7 (15)| 00:00:01 |
| 3 | TABLE ACCESS FULL| EMP | 12 | 144 | 3 (0)| 00:00:01 |
|* 4 | TABLE ACCESS FULL| EMP | 4 | 60 | 3 (0)| 00:00:01 |
----------------------------------------------------------------------------
PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access(SYS_OP_MAP_NONNULL("JOB")=SYS_OP_MAP_NONNULL("JOB") AND
SYS_OP_MAP_NONNULL("MGR")=SYS_OP_MAP_NONNULL("MGR"))
4 - filter("DEPTNO"=10)
</code></pre>
- Anti 조인을 수행하고 나서 중복 값을 제거하기 위해 Hash Unique 연산을 수행하는 것을 확인할 수 있다.
- 아래와 같은 형태로 쿼리 변환이 일어난 것
<pre><code class="sql" style="font-size:15px">SELECT DISTINCT JOB, MGR
FROM EMP E
WHERE NOT EXISTS(
SELECT 'X' FROM EMP
WHERE DEPTNO = 10
AND SYS_OP_MAP_NONNULL(JOB)=SYS_OP_MAP_NONNULL(E.JOB)
AND SYS_OP_MAP_NONNULL(MGR)=SYS_OP_MAP_NONNULL(E.MGR)
);
</code></pre>
<br/>
### 조인 컬럼에 IS NOT NULL 조건 추가
<pre><code class="sql" style="font-size:15px">SELECT COUNT(E.EMPNO), COUNT(D.DNAME)
FROM EMP E, DEPT D
WHERE D.DEPTNO = E.DEPTNO
AND SAL <= 2900
</code></pre>
- 위의 쿼리에서 조인문을 처리할 때 조인 컬럼 deptno가 null인 데이터는 조인에 실패하기 때문에 조인 액세스가 불필요함
- 그래서 아래와 같은 필터 조건을 추가해 주면 불필요한 테이블 액세스 및 조인 시도를 줄일 수 있어 쿼리 성능 향상에 도움이 된다.
<pre><code class="sql" style="font-size:15px">SELECT COUNT(E.EMPNO), COUNT(D.DNAME)
FROM EMP E, DEPT D
WHERE D.DEPTNO = E.DEPTNO
AND SAL <= 2900
AND E.DEPTNO IS NOT NULL
AND D.DEPTNO IS NOT NULL
</code></pre>
- `IS NOT NULL` 조건을 사용자가 직접 기술하지 않아도 옵티마이저가 필요하다고 판단되면 내부적으로 추가해준다.
<br/>
### 필터 조건 추가
<pre><code class="sql" style="font-size:15px">
SELECT * FROM EMP
WHERE SAL BETWEEN :MIN AND :MAX
</code></pre>
- 위의 쿼리 처럼 바인드 변수로 BETWEEN 검색하는 쿼리가 있을 때 사용자의 실수로 MIN, MAX를 바꿔 넣는다면 공집합 결과가 출력될 것이다.
- Oracle 9i 부터 이를 방지하기 위해 옵티마이저가 임의로 필터 조건식을 추가해준다.
<pre><code class="sql" style="font-size:15px">Predicate Information (identified by operation id):
---------------------------------------------------
1 - fillter(TO_NUMBER(:MIN) <= TO_NUMBER(:MAX))
</code></pre>
- 위 필터 조건이 추가되면 블록 I/O가 전혀 발생하지 않는다.
<br/>
### 조건절 비교 순서
<pre><code class="sql" style="font-size:15px">-- A Col : ... 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, ...
-- B Col : ... 991, 992, 993, 994, 995, 996, 997, 998, 999, 1000, ...
SELECT * FROM T
WHERE A = 1 -- 비교연산 일량 비효율 발생
AND B = 1000;
</code></pre>
- 위와 같이 테이블에 데이터가 있을 때 조건절 순서대로 조회 한다면 A, B를 수행한다면 A 컬럼이 대부분 1이여서 B = 1000까지 비교 연산을 수행하게 되므로 비효율적이다.
- 그래서 B 컬럼 조건식을 먼저 비교 연산을 수행하게 하는 것이 유리하다.
- 최신 옵티마이저는 비교 연산해야 할 일량을 고려해 선택도가 낮은 컬럼의 조건식부터 처리하도록 내부적으로 순서를 조정한다.
> SQL 전문가 가이드 : 과목3. SQL 고급 활용 및 튜닝, 3장 옵티마이저 원리 - 제2절 쿼리변환(p.584 ~ p.592)
'SQLP 자격증' 카테고리의 다른 글
SQLP - 인덱스 기본 원리 (0) | 2018.06.03 |
---|---|
SQLP - 쿼리변환(1) (0) | 2018.05.22 |
SQLP - 옵티마이저 (0) | 2018.05.08 |
SQLP - 동시성 제어 (0) | 2018.05.07 |
SQLP - 트랜잭션(Transaction) (0) | 2018.05.02 |