미리 규정해 놓은 규칙대로 수행하는 Rule-based optimizer를 사용할것인지, 아니면 좀더 똑똑한 Cost-based optimizer를 사용할것인지 결정하는 것은 매우 중요한 일이다. 아직도 많은 사용자들이 rule-based optimizer를 사용하고 있는 것으로 보이는데 그 이유는 무었일까?
Cost-based optimizer로 변환을 시도한 많은 사용자들이
기대하였던 바와는 달리 성능의 저하를 경험하였을 것이다. 그리고 이것을 tuning할 시간을 갖지 못하거나 새로운
optimizer를 골치아프게 생각하게된 사용자들은 과거의 익숙한 경험인 rule-based optimizer를 고수하게 되는
경향이 있다.
Rule-Based Optimizer란 무었인가?
INIT.ora 에 OPTIMZER_MODE=RULE이라고
정의하던가, 아니면 OPTIMIZER_MODE를 CHOOSE, FIRST_ROWS 또는 ALL_ROWS라고 정의하였어도
ANALYZE를 수행하지 않아서 테이블및 인덱스의 통계치가 없으면 오라클은 rule-based optimizing을 하게 된다.
아니면 SELECT문의 hint로써 RULE을 정의하여도 rule-based optimizer를 사용할 수 있게 된다.
Rule-based optimizer는 다음과 같이 이미 정의되어 있는
규칙에 따른다. 규칙은 테이블내의 로우 갯수라든가 컬럼값의 분포같은 통계치와는 무관하다. Optimizer는 아래 규칙에서 가장
낮은 순위를 우선적으로 선택한다. 예를 들면 쿼리를 하는데 single column index를 이용하는 방법과 single
row by unique or primary key를 이용하는 방법이 가능하다면 optimizer는 순위 4인 single
row by unique or primary key를 선택하게 된다.
Single row by ROWID
Single row by cluster join
Single row by hash cluster with unique or primary key
Single row by unique or primary key
Cluster join
Hash cluster key
Indexed cluster key
Composite key
Single column indexes
Bounded range on index columns
Unbounded range on indexed columns
Sort merge join
MAX or MIN on indexed column
ORDER BY on indexed columns
FULL TABLE SCAN
하지만 이 규칙에서 벗어나는 경우도 있기는 하다. 어떤
경우에는 순위 15위인 FULL TABLE SCAN이 query를 drive하기도 한다. 예를 들면 테이블의 하나가
WHERE절을 가지지 않는 경우에서 이런 현상을 볼수 있다. 한테이블은 유니크 키를 가지고 다른 테이블은 single
column non-unique index를 가지는 두개의 테이블을 조인하는쿼리에서 보통 낮은 순위의 규칙이 drive로
사용된다. 다음 예에서 각 테이블은 acct_no 컬럼을 index로 가지고 있다.
SELECT /*+ RULE */ COUNT(*)
FROM acct , trans, acct_details
WHERE acct.acct_no = trans.acct_no
AND acct.acct_no = acct_details.acct_no
/
HASH JOIN
TABLE ACCESS FULL ACCT_DETAILS
NESTED LOOPS
TABLE ACCESS FULL TRANS
INDEX UNIQUE SCAN ACCT_NDX1
우리는 이러한 규칙 목록을 공부하여야 하고, 이것을 SQL Coding 할때 고려하여야 된다. 규칙 목록이 rule-based optimizer의 전부는 아니다. 이것을 이야기 해보도록 하자.
알아야 할 규칙 (하나)
테이블이 two-column index와
one-column index를 가지고 있고, WHERE절이 이들 3개의 column을 모두 참조한다고 했을때,
one-column index가 unique 또는 primary key가 아닌 한 single index는 사용되지 않는다.
많은 사용자들이 오라클이 두개의 index를 모두 사용하여 index
merge를 할것이라고 믿고 있다. 하나의 테이블에서 index merge는 index가 single column일 경우에만
수행된다. 이것은 cost-based나 rule-based에 모두 적용된다.
Rule-based optimizer가 driving table을
선정할때 다른 join table이 single column nonunique inde를 가지고 있다면 two-column
index를 가진 table이 항상 driving table이 된다.
EMP_NDX1 (DEPT_NO)
EMP_NDX2(EMP_NO, EMP_NAME)
WHERE EMP_NAME = 'HAN'
AND EMP_NO = 1
AND DEPT_NO = 1
인덱스 EMP_NDX2만 사용된다.
알아야 할 규칙 (둘)
WHERE절이 two-column index의 한 컬럼과 single-column index의 컬럼을 사용할 경우에는 single-column index만이 사용된다.
EMP_NDX1 (EMP_NAME)
EMP_NDX2(EMP_NO, DEPT_NO)
WHERE EMP_NAME = 멒URRY?
AND EMP_NO = 1
인덱스 EMP_NDX1만 사용된다.
알아야 할 규칙 (셋)
조건이 같은 경우에는 가장 최근에 생성된 인덱스만을 사용한다.
아래 예에서 같은 날 오후 4시와 5시에 각각 two-column index가 생성되었다. WHERE절이 이들 4개의 컬럼을 모두 참조할때 5시에 생성된 인덱스를 사용하게 된다.
EMP_NDX1 (EMP_NAME, EMP_CATEGORY) created 4pm
EMP_NDX2(EMP_NO, DEPT_NO) created 5pm
WHERE EMP_NAME = 'HAN'
AND EMP_NO = 1
AND DEPT_NO = 1
AND EMP_CATEGORY = 'CLERK'
인덱스 EMP_NDX2만 사용된다.
알아야 할 규칙 (넷)
Three-column(A,B,C) index와
two-column(D,E) index가 있고 아래 예와 같이 WHERE절에서 5개의 컬럼을 모두 사용하는 경우일때, LIKE로
인하여 two-column index를 사용하게 된다. LIKE가 =보다 순위가 낮기 때문이다.
WHERE A=1
AND B=1
AND C LIKE '한%'
AND D=1
AND E=1
인데스(D,E)가 사용된다.
알아야 할 규칙 (다섯)
WHERE절이 사용하는 컬럼이 많이 포함된 인덱스가 사용된다.
WHERE A=1
AND B=1
AND C=1
AND F=1
AND G=1
75% 컬럼이 사용된 인덱스(A,B,C,D)가 선택된다.
65% 컬럼이 사용된 인덱스(F,G,H)는 선택되지 않는다.
알아야 할 규칙 (여섯)
드라이빙 테이블을 결정할때 첫번째 목록이 두번째 목록보다 우선한다.
Full Unique or Primary Key
All columns in index =
Higher % of columns
Not Unique or Primary Key
Most columns in index =
Lower % of columns
순위가 동등한 인덱스 경우에는 rule-based optimizer는 FROM 절에서 마지막에 기술한 인덱스를 선택한다.
일반적인 오해
Oracle8은 rule-based optimizer를 지원하지 못한다......NOT TRUE
Rule-based optimizer에서는 hint를 사용하지 못한다......NOT TRUE
Rule-based optimizer로 잘 tuning된 application은 cost-based optimizer에서도 잘 돌아간다......불행하게도 그렇지 못하다. 추가적인 tuning작업이 필요하다.
Rule-based optimizer의 일반적인 문제
1.
잘못된 드라이빙 테이블
55%
2.
잘못된 드라이빙 인덱스
25%
3.
부적당한 인덱스 사용
15%
4.
기타
5%
1. 잘못된 드라이빙 테이블
Cost-based optimizer에서 rule-base
optimizer로 변경하였을때 잘못된 드라이빙 테이블이 선정되는것을 자주 경험하게 된다. 드라이빙 테이블이 잘못 선정되었을때
그 영향을 알아보기 위하여 다음을 고려하여 보자
TRANS 테이블을 검색하는 문장이 있는데 TRANS의
COST_CENTRE는 ACCT테이블의 ACCT_ID와 같은 의미로 두개의 테이블을 JOIN할수 있게 한다. TRANS
테이블에는 '대구'라는 값을 가진 20,000 이상의 row가 있다. ACCT 테이블에는 ACCT_ID=9의 값을 가진 row가
단 9row뿐이다. TRANS 테이블에는 ACCT_ID=9이고 COST_CENTRE='대구'인 row는 하나도 없다.
ACCT 테이블은 인덱스 ACCT_NDX1(ACCT_ID)를 가지고, TRANS 테이블은 인덱스 TRANS_NDX1(ACCT_ID)와 TRANS_NDX2(COST_CENTRE)가 있다.
다음 문장은 cost-based optimizer로 수행되었고 두개의 table은 모두 ANALYZE되어 있었다.
EXPLAIN PLAN을 보면 ACCT가 드라이빙 테이블이 되었고,TRANS 테이블은 NAD-EQUAL에서 보듯이 두개의 인덱스가 merge되었다.
1 select count(*) from trans a, acct b
2 where a.cost_centre = '대구'
3 and b.acct_id = 9
4 * and a.acct_id = b.acct_id
real: 1977
Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE
1 0 SORT (AGGREGATE)
2 1 NESTED LOOPS
3 2 INDEX (RANGE SCAN) OF 'ACCT_NDX1' (NON-UNIQUE)
4 2 AND-EQUAL
5 4 INDEX (RANGE SCAN) OF 'TRANS_NDX1' (NON-UNIQUE)
6 4 INDEX (RANGE SCAN) OF 'TRANS_NDX2' (NON-UNIQUE)
다음은 테이블의 통계치를 삭제하여 rule-based
optimizer를 적용시킨 것이다. 나는 ANALYZE TABLE ACCT DELETE STATISTICS 와 ANALYZE
TABLE TRANS DELETE STATISTICS를 수행시켰다.
EXPLAIN PLAN을 보면 TRANS를 드라이빙 테이블로
사용하였다. Rule-based에서 인덱스들의 순위가 동등할 경우 FROM 절에 기술된 마지막 테이블부터 선택된다는것을 기억할
것이다. 결과는 cost-based에 비하여 12배의 시간을 필요로 하게 되었다.
1 select count(*) from acct a, trans b
2 where b.cost_centre = '대구'
3 and a.acct_id = 9
4 * and a.acct_id = b.acct_id
real: 19772
Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE
1 0 SORT (AGGREGATE)
2 1 NESTED LOOPS
3 2 TABLE ACCESS (BY ROWID) OF 'TRANS'
4 3 INDEX (RANGE SCAN) OF 'TRANS_NDX1' (NON-UNIQUE)
5 2 INDEX (RANGE SCAN) OF ACCT_NDX1' (NON-UNIQUE)
TRANS 테이블에는 ACCT_ID와 COST_CENTRE
두개의 컬럼이 모두 있다. 문장에서 3번째 ACCT_ID가 ACCT.ACCT_ID가 아니라 TRANS.ACCT_ID로 되었더라면
rule-based optimizer는 TRANS 테이블을 드라이빙 테이블로 선택하지 않았을 것이다.
TRANS.ACCT_ID=9인 경우에 EXPLAIN은 다음과 같았다.
1 select count(*) from trans a, acct b
2 where a.cost_centre = '대구'
3 and a.acct_id = 9
4 * and a.acct_id = b.acct_id
real: 1904
Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE
1 0 SORT (AGGREGATE)
2 1 NESTED LOOPS
3 2 AND-EQUAL
4 3 INDEX (RANGE SCAN) OF 'TRANS_NDX1' (NON-UNIQUE)
5 3 INDEX (RANGE SCAN) OF 'TRANS_NDX2' (NON-UNIQUE)
6 2 INDEX (RANGE SCAN) OF 'ACCT_NDX1' (NON-UNIQUE)
이렇게 하기 위하여 문제는 SQL을 변경하여야 한다는 것이다. Package로 된 SQL을 변경한다는것은 불가능하다. 드라이빙 테이블을 어떻게 변경할 것인가?
우
리가 할수 있는것은 TRANS 테이블에서 COST_CENTRE 인덱스를 삭제하고 ACCT_ID, COST_CENTRE 인덱스를
새로 만드는 것이다. 이 방법은 COST_CENTRE값이 적어도 10,000 rows이상일 경우에 효과가 있을 수 있다.
SQL> 1 select count(*) from acct a, trans b
SQL> 2 where b.cost_centre = '대구'
SQL> 3 and a.acct_id = 9
SQL> 4* and a.acct_id = b.acct_id;
real: 1653
Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE
1 0 SORT (AGGREGATE)
2 1 NESTED LOOPS
3 2 INDEX (RANGE SCAN) OF 'ACCT_NDX1' (NON-UNIQUE)
4 2 INDEX (RANGE SCAN) OF 'TRANS_NDX3' (NON-UNIQUE)
2. 잘못된 드라이빙 인덱스
TRANS 테이블이 두개의 single-column
index를 가지고 있다고 가정하자. TRANS 는 TRANS_NDX2(ACCT_ID)와
TRANS_NDX3(COST_CENTRE) 인덱스를 가지고 있다. Rule-based optimizer입장에서 이 두개의
index는 acess하는데 차이가 없다. 모두 규칙 순위 9 아래이다.
Rule-based optimizer에서 WHERE절의 순서는
중요하다. TRANS 테이블은 600,000 rows를 가지고 있는데 COST_CENTRE='대구'값을 갖는것은 170,000
rows이고, ACCT_ID=9인 것은 단 3개뿐이다. COST_CENTRE가 WHERE절 선두에 기술된 다음 문장은 그
인덱스가 드라이빙 인덱스로 사용되었음을 주목하라. Response time은 4초를 상회하였다.
select count(*) from trans
where cost_centre='대구'
and acct_id = 9;
real: 4225
Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE
1 0 SORT (AGGREGATE)
2 1 AND-EQUAL
3 2 INDEX (RANGE SCAN) OF 'TRANS_NDX3' (NON-UNIQUE)
4 2 INDEX (RANGE SCAN) OF 'TRANS_NDX2' (NON-UNIQUE)
WHERE절의 순서를 보다 적은 row를 가지는 ACCT_ID로 변경하니까 런타임이 25%이상 줄었다. 드라이빙 인덱스 역시 변경되었다.
select count(*) from trans
where acct_id = 9
and cost_centre='대구';
real: 1044
Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE
1 0 SORT (AGGREGATE)
2 1 AND-EQUAL
3 2 INDEX (RANGE SCAN) OF 'TRANS_NDX2' (NON-UNIQUE)
4 2 INDEX (RANGE SCAN) OF 'TRANS_NDX3' (NON-UNIQUE)
3. 부적당한 인덱스 사용
1,000,000 rows를 가진 TRANS 테이블을
생각해보자. 이 테이블에는 BUSINESS_CENTRE, ACCT_ID, TRANS_DATE의 3개 컬럼으로 이루어진 인덱스가
있다. 각각 대략 330,000 rows씩을 가지는 3개의 business center가 있다. 각 ACCT_ID와
TRANS_DATE의 조합은 최대 20 rows정도가 된다. 이 인덱스는 아래 쿼리를 위한 것이었다.
SELECT BUSINESS_UNIT, ACCT_ID, SUM(AMOUNT)
FROM TRANS
WHERE ACCT_ID BETWEEN 1000 AND 1100
AND BUSINESS_UNIT='TIN'
AND TRANS_DATE > SYSDATE - 7
GROUP BY BUSINESS_UNIT, ACCT_ID;
인덱스는 이 문장을 위해서 아주 효과적으로 사용되었다.
어떤 배치 작업이 다음 쿼리를 수행한다면 여기서도 rule-based optimizer는 인덱스를 사용하였을 것이고, 테이블을 full scan하는것보다 훨씬 오랬동안 배치 작업은 끝나지 못했을 것이다.
SELECT SUM(AMOUNT)
FROM TRANS
WHERE BUSINESS_UNIT='TIN'
GROUP BY BUSINESS_UNIT;
그 이유는 UNIT을 위한 330,000 번에다가 인덱스를
또 읽어야 했기 때문이다. 이것은 330,000번의 물리적인 read를 한것과 같다. FULL TABLE SCAN을 하였을 경우
한 블록에 100개의 row가 있다고 했을때 330,000 row를 읽는데는 약 10,000번만 읽으면 족했을것이다.