FILTER PUSH DOWN 을 위해 ROWNUM, RANK 제거
인라인 뷰 안의 RANK/ROWNUM 같은 결과집합 의존 함수가 있으면 옵티마이저는 외부 술어를 뷰 안으로 푸시 다운 하지 않습니다. 불필요한 RANK 를 제거하니 EMP_JOB_IX 인덱스 RANGE SCAN 으로 처리되어 풀스캔과 WINDOW SORT 가 사라집니다.
인라인 뷰 안에
RANK/ROWNUM같은 결과집합 의존 함수가 있으면, 옵티마이저는 외부 술어를 뷰 안으로 푸시 다운 하지 않습니다.
핵심 정리
SELECT ... FROM (SELECT ... FROM employee) a, department b WHERE a.department_id = b.department_id AND a.job_id = 'MK_REP'같은 쿼리에서 옵티마이저는a.job_id술어를 뷰 안으로 밀어 넣을 수 있습니다 (FILTER PUSH DOWN).- View Merging 을 방지해야 하는 케이스가 있습니다 (
NO_MERGE힌트). 조인이 풀려 머지되면 뷰 안으로 술어를 푸시할 기회 자체가 사라지기도 하기 때문에, 의도적으로 뷰 경계를 유지해야 할 때 사용합니다. - 뷰 내부에 불필요한
ROWNUM/RANK가 있으면 반드시 제거합니다. ROWNUM,RANK는 결과집합에 의존적이므로 옵티마이저가 손대지 않습니다 — 행을 미리 걸러 버리면 결과집합이 달라질 수 있기 때문입니다. FILTER PUSH DOWN 도 시도하지 않습니다.- FILTER PUSH DOWN 효과를 보기 위해 RANK 를 제거한 쿼리는 원본 쿼리와 결과가 다를 수 있습니다. RANK 가 응용에서 실제로 필요했다면 함부로 제거하면 안 됩니다.
5번이 가장 중요합니다. RANK 를 제거할 수 있는지는 “결과집합에 RANK 결과가 정말 필요한가” 를 먼저 점검해야 합니다. 본 글의 시나리오는 RANK 결과가 사용되지 않는 경우입니다.
시나리오
같은 두 테이블, 같은 조건, 같은 조인. 단 하나만 다릅니다 — 인라인 뷰 안에 RANK 함수가 있느냐.
- 테이블/인덱스:
employee(EMP_JOB_IX (job_id)),department(DEPT_ID_PK (department_id)) - 외부 술어:
a.job_id = 'MK_REP' - 인라인 뷰는
NO_MERGE힌트로 머지를 막아 둠 (시연 목적 — 푸시 다운 동작을 명확히 보기 위함)
1) 원본 — RANK 함수가 뷰 안에 존재
뷰 안에 RANK () OVER (ORDER BY salary) 가 들어가 있어 옵티마이저는 외부 술어 a.job_id = 'MK_REP' 를 뷰 안으로 밀어 넣지 못합니다. RANK 의 결과는 입력 행 집합 전체에 의존하므로, 행을 미리 걸러내면 RANK 값 자체가 (걸러진 부분집합 안에서의 순위로) 달라지기 때문입니다. 결과적으로 EMPLOYEE 를 풀스캔 하고 모든 행에 대해 RANK 를 계산한 뒤, 뷰 외부에서 JOB_ID 로 필터링합니다.
1
2
3
4
5
6
7
8
9
SELECT a.employee_id, a.first_name, a.last_name, a.email,
b.department_name, a.salary_rank
FROM (SELECT /*+ NO_MERGE */
employee_id, first_name, last_name, job_id, email, department_id,
RANK () OVER (ORDER BY salary) salary_rank
FROM employee) a,
department b
WHERE a.department_id = b.department_id
AND a.job_id = 'MK_REP';
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
---------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost |
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 106 | 11236 | 7 |
| 1 | MERGE JOIN | | 106 | 11236 | 7 |
| 2 | TABLE ACCESS BY INDEX ROWID | DEPARTMENT | 27 | 540 | 2 |
| 3 | INDEX FULL SCAN | DEPT_ID_PK | 27 | | 1 |
|* 4 | SORT JOIN | | 107 | 9202 | 5 |
|* 5 | VIEW | | 107 | 9202 | 4 |
| 6 | WINDOW SORT | | 107 | 5565 | 4 |
| 7 | TABLE ACCESS FULL | EMPLOYEE | 107 | 5564 | 3 |
---------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
4 - access("A"."DEPARTMENT_ID"="B"."DEPARTMENT_ID")
filter("A"."DEPARTMENT_ID"="B"."DEPARTMENT_ID")
5 - filter("A"."JOB_ID"='MK_REP')
Id 7 의 TABLE ACCESS FULL EMPLOYEE 와 Id 6 의 WINDOW SORT 가 모든 행에 대해 RANK 를 계산하기 위한 비용입니다. JOB_ID = ‘MK_REP’ 인 한 줌의 행만 필요한데, 옵티마이저는 그 술어를 뷰 안으로 못 밀어 넣어 Id 5 의 VIEW 단에서 뒤늦게 필터링합니다.
2) 수정 — RANK 제거
뷰에서 RANK 컬럼이 실제로 사용되지 않거나 다른 방법으로 대체 가능하다면 RANK 를 제거합니다. 결과집합 의존 함수가 사라졌으므로 옵티마이저는 외부 술어 a.job_id = 'MK_REP' 를 뷰 안으로 자유롭게 푸시할 수 있고, EMP_JOB_IX 인덱스 RANGE SCAN 으로 한 줌의 행만 즉시 추려냅니다.
1
2
3
4
5
6
7
SELECT a.employee_id, a.first_name, a.last_name, a.email, b.department_name
FROM (SELECT /*+ NO_MERGE */
employee_id, first_name, last_name, job_id, email, department_id
FROM employee) a,
department b
WHERE a.department_id = b.department_id
AND a.job_id = 'MK_REP';
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
----------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost |
----------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 88 | 2 |
| 1 | NESTED LOOPS | | | | |
| 2 | NESTED LOOPS | | 1 | 88 | 2 |
| 3 | VIEW | | 1 | 68 | 2 |
|* 4 | INDEX RANGE SCAN | EMP_JOB_IX | 1 | | 1 |
|* 5 | INDEX UNIQUE SCAN | DEPT_ID_PK | 1 | | 0 |
| 6 | TABLE ACCESS BY INDEX ROWID | DEPARTMENT | 1 | 20 | 1 |
----------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
4 - access("JOB_ID"='MK_REP')
5 - access("A"."DEPARTMENT_ID"="B"."DEPARTMENT_ID")
Id 4 의 INDEX RANGE SCAN EMP_JOB_IX 만으로 EMPLOYEE 의 필요 행이 추려졌습니다 — 술어가 뷰 안으로 밀려 들어와 인덱스가 활용된 결과입니다. 풀스캔과 WINDOW SORT 가 통째로 사라졌습니다.
분석
왜 RANK/ROWNUM 이 FILTER PUSH DOWN 을 막는가
옵티마이저의 술어 푸시 다운(predicate pushdown) 은 결과 정합성이 보장될 때만 안전합니다. RANK () OVER (ORDER BY salary) 의 결과는 입력 행 집합 전체 를 본 뒤 결정됩니다 — 옵티마이저가 외부의 JOB_ID = 'MK_REP' 를 뷰 안으로 밀어 넣어 입력 행을 미리 걸러 버리면, RANK 값이 (걸러진 부분집합 안에서의 순위로) 달라집니다.
ROWNUM 도 같은 종류 — 결과 행이 만들어지는 순서·개수에 의존하므로 술어를 위로 빼거나 아래로 밀어 넣으면 의미가 변합니다.
옵티마이저는 “결과를 바꿀 가능성이 있는 변환” 은 시도하지 않습니다. 그래서 RANK / ROWNUM 이 뷰 안에 있는 한 술어 푸시 다운은 봉인 됩니다.
- 원본 (RANK 존재):
EMPLOYEE FULL SCAN→WINDOW SORT(전 행 RANK 계산) →VIEW외부에서JOB_ID필터 - 수정 (RANK 제거): 술어가 뷰 내부로 푸시 →
EMP_JOB_IX RANGE SCAN→ 추려진 결과만DEPARTMENT와 NESTED LOOPS
View Merging 과 NO_MERGE 의 관계
본 시나리오는 의도적으로 NO_MERGE 힌트를 넣어 인라인 뷰가 머지되지 않게 한 상태입니다. 머지가 일어나면 인라인 뷰가 사라지고 단일 SELECT 로 평탄화되므로 “푸시 다운” 자체가 의미를 잃습니다. 그래서 두 변환은 순서가 있습니다 — 옵티마이저는 보통 머지를 먼저 시도하고, 머지가 불가능하거나 불리할 때 푸시 다운을 검토합니다.
뷰 안에 RANK/ROWNUM 이 있으면 머지도 보통 막힙니다 (윈도우 함수는 머지하면서 의미가 바뀌기 때문). 그러면 푸시 다운까지 막혀 이중으로 변환 봉인 상태가 됩니다.
RANK/ROWNUM 은 단순한 분석 함수가 아니라, 옵티마이저에게 “이 뷰는 입력 의존이니 건드리지 마” 라는 신호입니다. 비즈니스에 꼭 필요한 게 아니면 빼는 것이 성능 최적화의 첫 걸음입니다.
결과 동등성 — 빼도 되는지 먼저 확인
수정 쿼리는 SELECT 절에서 a.salary_rank 컬럼이 함께 빠진 점에 주목해야 합니다. 원본과 수정은 결과집합이 다릅니다 — 컬럼 수가 다르고, RANK 가 응용에서 실제로 쓰였다면 수정 쿼리는 잘못된 답입니다.
- 응용에서 RANK 결과를 사용하지 않는데 잘못 들어가 있던 경우: 안전하게 제거 OK
- RANK 결과를 꼭 써야 하는 경우: 인덱스 활용을 위해 RANK 를 빼는 건 옵션이 아닙니다. 다른 접근(예: JOB_ID = ‘MK_REP’ 인 부분집합 안에서만 RANK 를 계산하도록 쿼리 구조 변경) 을 검토해야 합니다.
비교 요약
| 관점 | 원본 (RANK 포함) | 수정 (RANK 제거) |
|---|---|---|
| EMPLOYEE 접근 | TABLE ACCESS FULL | INDEX RANGE SCAN EMP_JOB_IX |
| WINDOW SORT | 발생 (107 행 전부) | 없음 |
| 술어 적용 위치 | 뷰 외부 (Id 5 filter) | 뷰 내부 (Id 4 access) |
| 조인 방식 | MERGE JOIN | NESTED LOOPS |
| 추정 비용 | 7 | 2 |
| 결과집합 | salary_rank 컬럼 포함 | salary_rank 컬럼 없음 |
정리
세 줄로 압축하면:
- 인라인 뷰 안의
RANK/ROWNUM은 결과집합 의존 함수입니다. 옵티마이저는 결과를 바꿀 가능성이 있는 변환(머지·푸시 다운) 을 시도하지 않습니다. - 응용에서 쓰이지 않는 RANK/ROWNUM 은 즉시 제거하세요. 한 줄 제거로 풀스캔과 WINDOW SORT 가 통째로 사라지는 케이스가 흔합니다.
- 단, 결과 동등성을 먼저 확인하세요. RANK 결과가 비즈니스에 필요하면 빼는 것은 답이 아닙니다 — 쿼리 구조 자체를 다시 짜야 합니다.