1. Sequential 액세스의 선택도를 높인다
2. Random 액세스 발생량을 줄인다.
! Sequential 액세스의 선택도를 높이는 방법을 학습 !
Sequential 액세스 선택도
선두 컬럼
where col1 = 1 and col2 = 'A' and col3 = '나' and col4 = 'a'
- 인덱스 구성 컬럼이 모두 '=' 조건으로 비교될 때는 조건을 만족하는 레코드들이 모두 연속되게 모여 있다.(5~7번까지)
where col1 = 1 and col2 = 'A' and col3 = '나' and col4 >= 'a';
- 선행 컬럼은 모두 '='이고 맨 마지막 컬럼만 범위검색 조건(부등호, between, like)일 때도
조건을 만족하는 레코드가 모두 모여 있다(5~10번)
where col1 = 1 and col2 = 'A' and col3 between '가' and '다' and col4 = 'a'
- 중간 컬럼이 범위검색 조건일 때는 col1부터 col3까지 세 조건만을 만족하는 인덱스 레코드는
서로 모여 있지만(2~12번) col4 족건까지 만족하는 레코드는 흩어지게 된다(2,3,5,6,7,11번)
where col1 = 1 and col2 <='B' and col3 = '나' and col4 between 'a' and 'b'
- 두 번째 컬럼 col2가 범위검색 조건인 경우는 col1부터 col2가지 두 조건만을 만족하는 인덱스 레코드는
서로 모이지만(2~16번) col3와 col4 조건까지 만족하는 레코드는 흩어지게 된다.(5,6,7,8,9,14,15번)
- 인덱스 : 아파트시세코드 + 평형 + 평형타입 + 인터넷매물
where 아파트시세코드 = :a
where 아파트시세코드 = :a and 평형 = :b
where 아파트시세코드 = :a and 평형 = :b and 평형타입 = :c
where 아파트시세코드 = :a and 평형 = :b and 평형타입 between :c and :d
- 인덱스 : 아파트시세코드 + 평형 + 평형타입 + 인터넷매물
select 해당층, 평당가, 입력일, 해당동, 매물구분, 연사용일수, 중개업소코드
from 매물아파트매매
where 아파트시세코드='A0101450900056'
and 평형 = '59'
and 평형타입 = 'A'
and 인터넷매물 between '1' and '2'
order by 입력일 desc
- 인덱스매물이 between 조건이지만 선행 컬럼들이 모두 '=' 조건이기 때문에 전혀 비효율 없이 조건을 만족하는 2건을 빠르게 찾았다. <p.135 그림 1-38 참조>
- 인덱스 선행 컬럼이 모두 '=' 조건일 때 필요한 범위만 스캔하고 멈출 수 있는 것은, 조건을 만족하는 레코드가 모두 한데 모여 있기 때문.
- 인덱스 : 인터넷매물 + 아파트시세코드 + 평형 + 평형타
- 인덱스 선두 컬럼 인터넷매물에 between 연산자를 사용하면 나머지 조건을 만족하는 레코드들이 인터넷 매물 값범위로 뿔뿔이
흩어져 있다. 조건을 만족하지 않는 레코드까지 스캔하고서 버려야 하는 비효율 발생.
선두 컬럼이 BETWEEN일 때 스캔 시작과 종료 지점
select 해당층, 평당가, 입력일, 해당동, 매물구분, 연사용일수, 중개업소코드
from 매물아파트매매
where 인터넷매물 in ('1', '2')
and 아파트시세코드 = 'A01011350900056'
and 평형 = '59'
and 평형타입 = 'A'
order by 입력일 desc
- 월별로 10만 개 판매데이터 입력
- 'A'가 10만개, 'B'가 110만개
create table 월별고객별판매집계
as
select rownum 고객번호
, '2008' || lpad(ceil(rownum/100000), 2, '0') 판매월
, decode(mod(rownum, 12), 1, 'A', 'B') 판매구분
, round(dbms_random.value(1000,100000), -2) 판매금액
from dual
connect by level <= 1200000 ;
create index 월별고객별판매집계_IDX1 on 월별고객별판매집계(판매구분, 판매월);
select count(*)
from 월별고객별판매집계 t
where 판매구분 = 'A'
and 판매월 between '200801' and '200812';
Rows Row Source Operation
------- ---------------------------------------------------
0 STATEMENT
1 SORT AGGREGATE (cr=281 pr=269 pw=0 time=95791 us)
100000 INDEX RANGE SCAN 월별고객별판매집계_IDX1 (cr=281 pr=269 pw=0 time=...)
- IDX1 인덱스를 스캔하면서 281개 블록 I/O 발생
create index 월별고객별판매집계_IDX2 on 월별고객별판매집계(판매월, 판매구분);
select /*+ index(t 월별고객별판매집계_IDX2) */ count(*)
from 월별고객별판매집계 t
where 판매구분 = 'A'
and 판매월 between '200801' and '200812';
Rows Row Source Operation
------- ---------------------------------------------------
0 STATEMENT
1 SORT AGGREGATE (cr=3090 pr=3100 pw=0 time=981731 us)
100000 INDEX RANGE SCAN 월별고객별판매집계_IDX2 (cr=3090 pr=3100 pw=0 time=...)
- IDX2 인덱스를 스캔하면서 3,090개 블록 I/O가 발생
select /*+ index(t 월별고객별판매집계_IDX2) */ count(*)
from 월별고객별판매집계 t
where 판매구분 = 'A'
and 판매월 in ( '200801', '200802', '200803', '200804', '200805', '200806'
, '200807', '200808', '200809', '200810', '200811', '200812' ) ;
Rows Row Source Operation
------- ---------------------------------------------------
0 STATEMENT
1 SORT AGGREGATE (cr=314 pr=2 pw=0 time=115212 us)
100000 INLIST ITERATOR (cr=314 pr=2 pw=0 time=800042 us)
100000 INDEX RANGE SCAN 월별고객별판매집계_IDX2 (cr=314 pr=2 pw=0 time=...)
- between 조건을 IN-List로 변환, 블록 I/O 개수가 314개로 감소
- 인덱스 브랜치 블록을 10번 반복 탐색하는 비효율이 있긴 하지만 리프 블록을 스캔할 때의 비효율을 제거, 1/10 수준으로 성능 개선
select /*+ INDEX_SS(t 월별고객별판매집계_IDX2) */ count(*)
from 월별고객별판매집계 t
where 판매구분 = 'A'
and 판매월 between '200801' and '200812' ;
Rows Row Source Operation
------- ---------------------------------------------------
0 STATEMENT
1 SORT AGGREGATE (cr=300 pr=0 pw=0 time=120292 us)
100000 INDEX SKIP SCAN 월별고객별판매집계_IDX2 (cr=300 pr=0 pw=0 time=...)
- 큰 비효율 없이 단 300 블록만 읽었다.
ex)
* 회사, 지역, 상품명을 입력하여 '가입상품' 테이블에서 데이터를 조회하는 프로그램.
1. 회사는 반드시 입력
2. 지역은 입력하지 않을 수도 있다.
3. 상품명은 단어 중 일부만 입력하고도 조회 가능
# 쿼리 1 : 회사, 지역, 상품명 모두 입력
SELECT ....
FROM 가입상품
WHERE 회사 = :com
AND 지역 = :reg
AND 상품명 LIKE :prod || '%'
# 쿼리 2 : 회사, 상품명만 입력
SELECT ....
FROM 가입상품
WHERE 회사 = :com
AND 상품명 LIKE :prod || '%'
- 인덱스 : 회사 + 지역 + 상품명
- 각각의 스캔 범위
# 하나의 쿼리로 작성
SELECT ....
FROM 가입상품
WHERE 회사 = :com
AND 지역 LIKE :reg || '%'
AND 상품명 LIKE :prod || '%'
- 회사, 지역, 상품명이 모두 입력했을 때와 지역을 입력하지 않았을 때의 스캔 범위
- 지역을 입력한 경우 인덱스 스캔 범위가 늘어난다.
- 인덱스 컬럼에 범위검색 조건을 남용하면 첫 번째 범위검색 조건에 의해 스캔 범위가 대부분 결정되며, 그 다음을 따르는 조건부터는 스캔 범위를 줄이는 데에 크게 기여하지 못하므로 성능상 불리할 수 있음
범위검색 조건만으로 구성된 쿼리 튜닝 사례
select *
from gis데이터
where gis_위도 betwwen :승객위도 - 1 and :승객위도 + 1
and gis_경도 between :승객경도 - 1 and :승객경도 + 1
and sysdate between gis_시작일자 and gis_종료일자
gis데이터_x01 : gis_위도 + gis_경도 + gis_시작일자 + gis_종료일자 + gis_지역코드 + gis_위치명
문제점 :
- 조건절이 모두 between 범위검색 조건
- 인덱스 스캔 범위는 인덱스 선두 컬럼인 'gis_위도' 컬럼에 대한 between 조건에 의해 결정.
- gis데이터 테이블에는 당일치만 보관.
- 당일 영업 시작시에는 빠르지만 밤 시간에는 고객의 특정 위치 기준으로 위도상 좌우 1km 이내에 평균 100만 개 레코드.
- 인덱스 스캔 원리상 현재 데이터 모델로는 과도한 인덱스 스캔 범위를 줄일 방법은 없다.
튜닝 :
- 1km 이내를 한 번에 조회하지 말고 50m, 200m, 1km 순으로 나눠서 쿼리
- 가장 가까운 데 위치한 하나의 택시를 찾는게 목적.
- 세 구간으로 나누어 쿼리하면 대부분 첫 번째 쿼리에서 찾게 된다.
- 인덱스 스캔량을 1/20로 줄일 수 있다.
- 50m, 200m, 1km 이내의 가장 가까운 택시를 찾을 때 위의 쿼리의 1을 0.05, 0.2, 1 로 바꾸어 쿼리.
- 추가로 최종 건수 한 건만을 우편번호와 조인하도록 쿼리 변경.
select *
from (
select rownum rnum, 도서번호, 도서명, 가격, 저자, 출판사, isbn
from (
select 도서번호, 도서명, 가격, 저자, 출판사, isbn
from 도서
where 도서명 like :book_nm || '%'
order by 도서명
)
where rownum <= 100
)
where rnum >= 91 --> 10 페이지만 출력
- 도서명 컬럼에 인덱스가 있다면 첫 번째 rownum 조건에 해당하는 레코드만 읽고 멈춘다. count(stopkey)오퍼레이션 적용.
- 사용자들이 앞쪽 일부 레코드만 주로 볼 경우 효과적
- 뒤쪽 어느 페이지로 이동하더라도 빠르게 조회되도록 구현해야 한다면?
- 아래는 첫번째 페이지를 출력하고 나서 '다음' 버튼을 누를 경우 예
select *
from (
select /*+ index(도서 도시명_idx) */
rowid rid, 도서번호, 도서명, 가격, 저자, 출판사, isbn
from 도서
where 도서명 like :book_nm || '%'
and 도서명 = :last_book_nm -- 이전 페이지에서 출력된 마지막 도서명
and rowid > :last_rid -- 이전 페이지에서 출력된 마지막 도서의 rowid
union all
select /*+ index(도서 도시명_idx) */
rowid rid, 도서번호, 도서명, 가격, 저자, 출판사, isbn
from 도서
where 도서명 like :book_nm || '%'
and 도서명 > :last_book_nm -- 이전 페이지에서 출력된 마지막 도서명
)
where rownum <= 10
;
Rows Row Source Operation
------- ---------------------------------------------------
0 STATEMENT
10 COUNT STOPKEY (cr=382 pr=0 pw=0 time=218 us)
10 VIEW (cr=382 pr=0 pw=0 time=197 us)
10 UNION-ALL (cr=382 pr=0 pw=0 time=180 us)
1 FILTER (cr=4 pr=0 pw=0 time=141 us)
1 TABLE ACCESS BY INDEX ROWID 도서 (cr=4 pr=0 pw=0 time=108 us)
1 INDEX RANGE SCAN 도시명_IDX (cr=3 pr=0 pw=0 time=137 us)(Object ID 52955)
9 TABLE ACCESS BY INDEX ROWID 도서 (cr=378 pr=0 pw=0 time=56979 us)
9 INDEX RANGE SCAN 도시명_IDX (cr=377 pr=0 pw=0 time=56916 us)(Object ID 52955)
- union all 위쪽 브랜치에서 한건, 아래쪽에서 9건 읽었다.
- 아래쪽 인덱스 스캔단계에서 377 블록을 읽었다.
- 도서명에 대한 범위검색 조건이 두 개인데 그 중 like 조건을 인덱스 액세스 조건으로 사용했기 때문.
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter(ROWNUM<=10)
4 - filter(:LAST_BOOK_NM LIKE :BOOK_NM||'%')
6 - access("도서명"=:LAST_BOOK_NM)
filter("도서명" LIKE :BOOK_NM||'%' AND ROWID>CHARTOROWID(:LAST_RID))
8 - access("도서명" LIKE :BOOK_NM||'%')
filter("도서명">:LAST_BOOK_NM AND "도서명" LIKE :BOOK_NM||'%')
- '오라클' 도서를 처음부터 스캔하다가 :last_book_nm 보다 큰 9개 레코드를 찾고서 멈추었다.
- 뒤쪽 페이지로 많이 이동할수록 그 비효율은 커진다.
- 해결방안 : '도서명 > :last_book_nm' 조건이 인덱스 액세스 조건으로 사용하도록 한다.
select *
from (
select /*+ index(도서 도시명_idx) */
rowid rid, 도서번호, 도서명, 가격, 저자, 출판사, isbn
from 도서
where 도서명 like :book_nm || '%'
and 도서명 = :last_book_nm -- 이전 페이지에서 출력된 마지막 도서명
and rowid > :last_rid -- 이전 페이지에서 출력된 마지막 도서의 rowid
union all
select /*+ index(도서 도시명_idx) */
rowid rid, 도서번호, 도서명, 가격, 저자, 출판사, isbn
from 도서
where rtrim(도서명) like :book_nm || '%'
and 도서명 > :last_book_nm -- 이전 페이지에서 출력된 마지막 도서명
)
where rownum <= 10
;
Rows Row Source Operation
------- ---------------------------------------------------
0 STATEMENT
10 COUNT STOPKEY (cr=7 pr=0 pw=0 time=143 us)
10 VIEW (cr=7 pr=0 pw=0 time=128 us)
10 UNION-ALL (cr=7 pr=0 pw=0 time=117 us)
1 FILTER (cr=4 pr=0 pw=0 time=93 us)
1 TABLE ACCESS BY INDEX ROWID 도서 (cr=4 pr=0 pw=0 time=72 us)
1 INDEX RANGE SCAN 도시명_IDX (cr=3 pr=0 pw=0 time=76 us)(Object ID 52955)
9 TABLE ACCESS BY INDEX ROWID 도서 (cr=3 pr=0 pw=0 time=123 us)
9 INDEX RANGE SCAN 도시명_IDX (cr=2 pr=0 pw=0 time=75 us)(Object ID 52955)
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter(ROWNUM<=10)
4 - filter(:LAST_BOOK_NM LIKE :BOOK_NM||'%')
6 - access("도서명"=:LAST_BOOK_NM)
filter("도서명" LIKE :BOOK_NM||'%' AND ROWID>CHARTOROWID(:LAST_RID))
8 - access("도서명">:LAST_BOOK_NM)
filter(RTRIM("도서명") LIKE :BOOK_NM||'%')
- 부등호 조건을 만족하는 첫 번째 레코드부터 스캔을 시작했기 때문에 스캔한 블록수는 2.
select /*+ index(도서 도시명_idx) use_concat ordered_predicates */
rowid rid, 도서번호, 도서명, 가격, 저자, 출판사, isbn
from 도서
where 도서명 like :book_nm || '%'
and ((도서명 > :last_book_nm)
or
(도서명 = :last_book_nm and rowid > :last_rid) )
and rownum <= 10
;
select /*+ index(도서 도시명_idx) */
rowid rid, 도서번호, 도서명, 가격, 저자, 출판사, isbn
from 도서
where 도서명 like :book_nm || '%'
and 도서명 >= :last_book_nm
and lpad(도서명, 50) || rowid > lpad(:last_book_nm, 50) || :last_rid
and rownum <= 10
;
=>
and lpad(도서명, 50) || rowidtochar(rowid) > lpad(:last_book_nm, 50) || :last_rid
- 문자형으로 변환된 rowid는 rowid 값 그대로 비교할 때와 정렬순서가 다르다.
select greatest('AAAH+WAAJAAAHxTAA9', 'AAAH+WAAJAAAHxTAA+') from dual;
GREATEST('AAAH+WAA
------------------
AAAH+WAAJAAAHxTAA9
select greatest( chartorowid('AAAH+WAAJAAAHxTAA9')
, chartorowid('AAAH+WAAJAAAHxTAA+') ) from dual;
GREATEST(CHARTOROW
------------------
AAAH+WAAJAAAHxTAA+
select * from 월별고객별판매집계
where 판매월 like '2009%';
select * from 월별고객별판매집계
where 판매월 between '200901' and '200912';
<쿼리 1>
select count(*)
from 월별고객별판매집계
where 판매월 between '200901' and '200902'
and 판매구분 = 'A';
<쿼리 2>
select count(*)
from 월별고객별판매집계
where 판매월 like '2009%'
and 판매구분 = 'A';
<쿼리 3>
select count(*)
from 월별고객별판매집계
where 판매월 >= '200901'
and 판매월 < '200903'
and 판매구분 = 'A';
<쿼리 4>
select count(*)
from 월별고객별판매집계
where 판매월 between '200901' and '200902'
and 판매구분 = 'B';
<쿼리 5>
select count(*)
from 월별고객별판매집계
where 판매월 like '2009%'
and 판매구분 = 'B';
<쿼리 6>
select count(*)
from 월별고객별판매집계
where 판매월 between '200900' and '200902'
and 판매구분 = 'B';
<쿼리 7>
select count(*)
from 월별고객별판매집계
where 판매월 like '200901%'
and 판매구분 = 'B';
- 과거, 현재, 미래 임의 시점을 조회
:dt between 시작일 and 종료일
- 현재 시점을 조회할 경우(종료일을 '99991231'로 넣는다고 약속한 경우)
1. 선분이력 테이블에 정보를 미리 입력해 두는 경우
to_char(sysdate, 'yyyymmdd') between 시작일 and 종료일
2. 미리 입력해 두지 않는 경우
종료일 = '99991231'
select *
from 고객별연체금액
where 고객번호 = '123'
and '20050131' between 시작일 and 종료일;
=>
select *
from 고객별연체금액
where 고객번호 = '123'
and 시작일 <= '20050131'
and 종료일 >= '20050131';
select /*+ index_desc(a idx_x01) */ *
from 고객별연체금액
where 고객번호 = '123'
and '20050131' between 시작일 and 종료일
and rownum <= 1;
select *
from 고객별연체금액
where 고객번호 = '123'
and '20050131' between 시작일 and 종료일;
=>
select *
from 고객별연체금액
where 고객번호 = '123'
and 시작일 <= '20050131'
and 종료일 >= '20050131';
select *
from 고객별연체금액
where 고객번호 = '123'
and '20050131' between 시작일 and 종료일
and rownum <= 1;
select *
from 고객별연체금액
where 고객번호 = '123'
and '20050131' between 시작일 and 종료일;
=>
select *
from 고객별연체금액
where 고객번호 = '123'
and 시작일 <= '20050131'
and 종료일 >= '20050131';
select *
from 고객별연체금액
where 고객번호 = '123'
and '20050131' between 시작일 and 종료일
and rownum <= 1;
select *
from 고객별연체금액
where 고객번호 = '123'
and '20020930' between 시작일 and 종료일;
=>
select *
from 고객별연체금액
where 고객번호 = '123'
and 시작일 <= '20020930'
and 종료일 >= '20020930';
select *
from 고객별연체금액
where 고객번호 = '123'
and '20050131' between 시작일 and 종료일
and rownum <= 1;
create index emp_x01 on emp(deptno, job, sal, ename, mgr, comm);
set autotrace traceonly explain;
select /*+ ordered use_nl(e) index(e emp_x01) */ *
from dept d, emp e
where d.loc = 'CHICAGO'
and e.deptno = d.deptno
and e.job like 'SALE%'
and e.job between 'A' and 'Z'
and e.sal >= 1000
and e.ename like '%A%'
and trim(e.ename) = 'ALLEN'
and e.comm >= 300
and to_char(e.hiredate, 'yyyymmdd') like '198102%' ;
Execution Plan
----------------------------------------------------------
Plan hash value: 346692358
---------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 57 | 5 (0)| 00:00:01 |
|* 1 | TABLE ACCESS BY INDEX ROWID| EMP | 1 | 37 | 2 (0)| 00:00:01 |
| 2 | NESTED LOOPS | | 1 | 57 | 5 (0)| 00:00:01 |
|* 3 | TABLE ACCESS FULL | DEPT | 1 | 20 | 3 (0)| 00:00:01 |
|* 4 | INDEX RANGE SCAN | EMP_X01 | 1 | | 1 (0)| 00:00:01 |
---------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter(TO_CHAR(INTERNAL_FUNCTION("E"."HIREDATE"),'yyyymmdd') LIKE
'198102%')
3 - filter("D"."LOC"='CHICAGO')
4 - access("E"."DEPTNO"="D"."DEPTNO" AND "E"."JOB" LIKE 'SALE%' AND
"E"."SAL">=1000 AND "E"."COMM">=300 AND "E"."SAL" IS NOT NULL)
filter("E"."ENAME" LIKE '%A%' AND "E"."JOB" LIKE 'SALE%' AND
TRIM("E"."ENAME")='ALLEN' AND "E"."JOB">='A' AND "E"."JOB"<='Z' AND
"E"."SAL">=1000 AND "E"."COMM">=300)
create table t as select rownum no from dual connect by level <= 1000000 ;
create index t_idx on t(no) pctfree 0;
delete from t where no <= 500000 ;
commit;
set autotrace on statistics
- 맨 왼쪽부터 스캔하도록 조건 부여
select * from t where no > 0 and rownum <= 1;
Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
1001 consistent gets
0 physical reads
0 redo size
420 bytes sent via SQL*Net to client
400 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed
drop table t;
create table t as select rownum no from dual connect by level <= 1000000 ;
create index t_idx on t(no) pctfree 0 ;
select /*+ index(t) */ count(*) from t where no > 0;
Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
2001 consistent gets
0 physical reads
0 redo size
424 bytes sent via SQL*Net to client
400 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed
delete from t where mod(no, 10) < 5 ;
commit;
select /*+ index(t) */ count(*) from t where no > 0;
Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
2001 consistent gets
0 physical reads
0 redo size
424 bytes sent via SQL*Net to client
400 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed
alter index t_idx coalesce;
alter index t_idx shrink space;
alter index t_idx shrink space compact;
alter index t_idx rebuild;
alter index t_idx rebuild online;