PAGE 처리 할때나 아니면 부분범위 처리를 할때 우리는 TOP SQL 을 많이 사용한다.
오늘은 rownum 을 이용한 TOP N건 처리시 몇가지 함정이 있으므로 정확한 사용법에 대해서 알아보고자 한다.
먼저 환경을 만들기 위하여 테이블및 인덱스를 생성 한다.
환경 : ORACLE 11.1.0.6
create table scott.big_emp as --> BIG_EMP 생성
select rownum as empno, a.ename, a.job, a.mgr, a.hiredate, a.sal + rownum as sal, a.comm, a.deptno
from scott.emp a,
(select level as no from dual connect by level <= 2000) b
;
create index scott.ix_emp_01 on scott.big_emp (deptno, sal) --> deptno, sal 로 구성된 결합 인덱스 생성
;
create table scott.new_dept as --> NEW_DEPT 생성
select a.* , a.deptno as page_count --> 추가적으로 page_count 컬럼생성
from scott.dept a
;
alter table scott.new_dept add --> PK 및 UNIQUE 인덱스 생성
( constraint pk_new_dept primary key (deptno) using index );
begin --> 통계정보 생성
dbms_stats.gather_table_stats('scott', 'big_emp', cascade => true);
dbms_stats.gather_table_stats('scott', 'new_dept', cascade => true);
end;
EMP 와 DEPT 를 이용하여 테이블및 인덱스를 생성 하였고 new_dept 테이블에는 부서별로 PAGE COUNT 라는 컬럼이 추가적으로 생성 되었다.
아래의 SQL 을 보자.
select /*+ gather_plan_statistics */ e.*
from (select /*+ index_desc(e IX_EMP_01) */
e.EMPNO, e.ENAME, e.SAL, e.DEPTNO, d.dname
from big_emp e,
new_dept d
where e.deptno = d.deptno
and d.deptno = 20
order by e.sal desc) e
where rownum < :v_page_count --> :v_page_count 에 숫자 20을 대입한다.
;
select * from table(dbms_xplan.display_cursor(null,null,'allstats last'));
20번 부서에 해당하는 사원중에서 급여가 큰순으로 20건만 보겠다는 의미이다.
------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | A-Rows | A-Time | Buffers |
------------------------------------------------------------------------------------------------
|* 1 | COUNT STOPKEY | | 1 | 20 |00:00:00.01 | 8 |
| 2 | VIEW | | 1 | 20 |00:00:00.01 | 8 |
| 3 | NESTED LOOPS | | 1 | 20 |00:00:00.01 | 8 |
| 4 | TABLE ACCESS BY INDEX ROWID | NEW_DEPT | 1 | 1 |00:00:00.01 | 2 |
|* 5 | INDEX UNIQUE SCAN | PK_NEW_DEPT | 1 | 1 |00:00:00.01 | 1 |
| 6 | TABLE ACCESS BY INDEX ROWID | BIG_EMP | 1 | 20 |00:00:00.01 | 6 |
|* 7 | INDEX RANGE SCAN DESCENDING| IX_EMP_01 | 1 | 20 |00:00:00.01 | 3 |
------------------------------------------------------------------------------------------------
위 PLAN 상의 A-Rows 에 나타나듯이 부서번호 20번에 해당하는 모든건을 엑세스및 조인한것이 아니라
정확히 BIG_EMP 쪽을 20 건만 scan 하였다.
이것이 COUNT STOPKEY 의 위력이다.
즉 BIG_EMP 테이블에 deptno = 20 조건을 만족하는 건이 아무리 많더라도 20 건만 읽고 끝낸다는 것이다.
하지만 rownum < :v_page_count 조건대신에 부서별로 PAGE COUNT 를 테이블에 저장 하고
그 값을 이용하여 TOP SQL 을 작성 해야 한다면 이야기가 달라진다.
아래의 SQL 을 보면 부서별로 PAGE COUNT 를 지정 하고 있다.
from new_dept;
<결과>
DEPTNO PAGE_COUNT
------ ----------
10 10
20 20
30 30
40 40
20번 부서의 경우 20 건만 보겠다는 의미이다.
이제 NEW_DEPT 테이블의 PAGE_COUNT 값으로 TOP SQL 을 작성해서 실행 해보자.
select /*+ gather_plan_statistics */ *
from (select /*+ index_desc(e IX_EMP_01) */
e.EMPNO, e.ENAME, e.SAL, e.DEPTNO, d.dname, d.page_count
from big_emp e,
new_dept d
where e.deptno = d.deptno
and d.deptno = 20
order by e.sal desc) a
where rownum < a.page_count ;
select * from table(dbms_xplan.display_cursor(null,null,'allstats last'));
-------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | A-Rows | A-Time | Buffers |
-------------------------------------------------------------------------------------------------
| 1 | COUNT | | 1 | 20 |00:00:00.01 | 6269 |
|* 2 | FILTER | | 1 | 20 |00:00:00.01 | 6269 |
| 3 | VIEW | | 1 | 10000 |00:00:00.09 | 6269 |
| 4 | NESTED LOOPS | | 1 | 10000 |00:00:00.07 | 6269 |
| 5 | TABLE ACCESS BY INDEX ROWID | NEW_DEPT | 1 | 1 |00:00:00.01 | 2 |
|* 6 | INDEX UNIQUE SCAN | PK_NEW_DEPT | 1 | 1 |00:00:00.01 | 1 |
| 7 | TABLE ACCESS BY INDEX ROWID | BIG_EMP | 1 | 10000 |00:00:00.04 | 6267 |
|* 8 | INDEX RANGE SCAN DESCENDING| IX_EMP_01 | 1 | 10000 |00:00:00.01 | 29 |
-------------------------------------------------------------------------------------------------
어이 없게도 20번 부서에 해당하는 전체건(10000 건)을 다 SCAN 하였다.
COUNT STOPKEY Operation 대신에 FILTER Operation 이 사용되었기 때문이다.
아래처럼 ROWNUM 조건을 인라인뷰 내로 밀어 넣어도 결과는 마찬가지 이다.
from (select /*+ index_desc(e IX_EMP_01) */
e.EMPNO, e.ENAME, e.SAL, e.DEPTNO, d.dname, d.page_count
from big_emp e,
new_dept d
where e.deptno = d.deptno
and d.deptno = 20
and rownum <= d.page_count
order by e.sal desc)
;
select * from table(dbms_xplan.display_cursor(null,null,'advanced allstats last'));
--------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | E-Rows | Cost (%CPU)| A-Rows | A-Time | Buffers |
--------------------------------------------------------------------------------------------------------------
| 1 | VIEW | | 10071 | 7898 (1)| 20 |00:00:00.01 | 6269 |
| 2 | COUNT | | | | 20 |00:00:00.01 | 6269 |
|* 3 | FILTER | | | | 20 |00:00:00.01 | 6269 |
| 4 | NESTED LOOPS | | 10071 | 7898 (1)| 10000 |00:00:00.11 | 6269 |
| 5 | TABLE ACCESS BY INDEX ROWID | NEW_DEPT | 1 | 1 (0)| 1 |00:00:00.01 | 2 |
|* 6 | INDEX UNIQUE SCAN | PK_NEW_DEPT | 1 | 0 (0)| 1 |00:00:00.01 | 1 |
| 7 | TABLE ACCESS BY INDEX ROWID | BIG_EMP | 10071 | 7897 (1)| 10000 |00:00:00.08 | 6267 |
|* 8 | INDEX RANGE SCAN DESCENDING| IX_EMP_01 | 10071 | 27 (0)| 10000 |00:00:00.05 | 29 |
--------------------------------------------------------------------------------------------------------------
그렇다면 이런 경우에 어떻게 TOP 처리를 할수 있겠는가?
이 문제는 ROWNUM 에 대한 기본을 알면 해결된다.
TOP N건 처리시에 ROWNUM 과의 비교대상은 반드시 상수(혹은 변수) 이어야 한다.
그렇지 않다면 TOP 처리는 실패한다.
아래의 SQL은 이 문제를 해결하였다.
select /*+ gather_plan_statistics */ *
from (select /*+ index_desc(e IX_EMP_01) */
e.EMPNO, e.ENAME, e.SAL, e.DEPTNO, d.dname, d.page_count
from big_emp e,
new_dept d
where e.deptno = d.deptno
and d.deptno = 20
order by e.sal desc)
where rownum <= (select page_count from new_dept where deptno = 20) ;
select * from table(dbms_xplan.display_cursor(null,null,'advanced allstats last'));
-------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | E-Rows | Cost (%CPU)| A-Rows | A-Time | Buffers |
-------------------------------------------------------------------------------------------------------------
|* 1 | COUNT STOPKEY | | | | 20 |00:00:00.01 | 10 |
| 2 | VIEW | | 10071 | 7898 (1)| 20 |00:00:00.01 | 8 |
| 3 | NESTED LOOPS | | 10071 | 7898 (1)| 20 |00:00:00.01 | 8 |
| 4 | TABLE ACCESS BY INDEX ROWID | NEW_DEPT | 1 | 1 (0)| 1 |00:00:00.01 | 2 |
|* 5 | INDEX UNIQUE SCAN | PK_NEW_DEPT | 1 | 0 (0)| 1 |00:00:00.01 | 1 |
| 6 | TABLE ACCESS BY INDEX ROWID | BIG_EMP | 10071 | 7897 (1)| 20 |00:00:00.01 | 6 |
|* 7 | INDEX RANGE SCAN DESCENDING| IX_EMP_01 | 10071 | 27 (0)| 20 |00:00:00.01 | 3 |
| 8 | TABLE ACCESS BY INDEX ROWID | NEW_DEPT | 1 | 1 (0)| 1 |00:00:00.01 | 2 |
|* 9 | INDEX UNIQUE SCAN | PK_NEW_DEPT | 1 | 0 (0)| 1 |00:00:00.01 | 1 |
-------------------------------------------------------------------------------------------------------------
PLAN 에서 보는것과 같이 서브쿼리를 사용하여 PAGE COUNT 를 상수화 하였다.
즉 rownum <= (서브쿼리) 형태가 되면 COUNT STOPKEY Operation 을 정상적으로 사용할수 있다.
"같은 테이블을 반복하여 읽지마라." 라는 튜닝의 기본적인 원칙이 깨지는 순간이다.
하지만 모든 규칙은 예외가 있음을 기억해야 한다.
결론 :
위에서 보는 바와 같이 COUNT STOPKEY 와 FILTER Operation 은 천당과 지옥의 차이이다.
몇천만건 혹은 몇억건 이상인 환경에서 FILTER Operation 이 나온다면?
더이상 그 프로그램은 사용할수 없을것이다.
TOP SQL 처리시 주의사항은 ROWNUM 과 비교시 항상 상수(혹은변수)와 비교하거나 서브쿼리등을 이용하여 상수화 한후에 비교 하여야 한다는 것이다.
'Oracle > SQL Tuning' 카테고리의 다른 글
USE_CONCAT 힌트 제대로 알기 (5) | 2009.07.17 |
---|---|
분석함수를 이용한 TOP SQL은 튜닝이 불가한가? (5) | 2009.05.22 |
인덱스없는 컬럼의 Order by 시 페이징 처리는 효율적인가? (2) | 2008.08.26 |
다양한 검색조건의 튜닝방법 (3) | 2008.08.05 |
DBMS_ADVANCED_REWRITE (1) | 2008.04.15 |