본문 바로가기
OraclE

옵티마이저와 신나게 노는 방법(엔코아)

by 타마마임팩트_쫀 2008. 11. 6.
 

출처 블로그>Timeless | 유진

원문 http://blog.naver.com/joa2341/40014856688

옵티마이저가 실행 계획을 수립하는 원리와 비용 산정 방식을 정확히 안다면 옵티마이저가 왜 그런 판단을 하는지 이해할 수 있다. 그렇다면 옵티마이저가 항상 최선의 판단을 내릴 수 있도록 사용자는 훌륭한 조언자가 될 수 있어야 한다. 우리 팀의 목표는 DB 실무자가 옵티마이저와 신나게 놀 수 있게 만드는 것이다.

누구나 여행을 다녀오면 기쁜 혹은 아름다운 추억을 만들어 온다. 눈요기할 것이 많은 관광지로 떠난 여행이 아닌 사람과의 커뮤니케이션 여행은 이번이 처음이라 필자는 이번 여행이 조금은 부담스러웠고 두려웠다. 하지만 전문가가 되기 위해 필자와 함께 했던 최부열씨의 의지가 하늘을 찌를 듯해 기존에 느껴보지 못했던 멋진 여행이었다. 개인적으로는 이번 여행이 ‘DB 전문가’라는 표현보다 ‘데이터 전문가’가 되기 위한 고행의 일부라고 생각한다. 데이터는 DBMS를 통해 구체화되어 존재할 수 있듯이 ‘데이터 전문가’란 ‘DB 전문가’로서의 능력 또한 기본적으로 겸비하고 있어야 하는 덕목이지 않을까.


그렇다면 훌륭한 DB 전문가가 되려면 무엇이 필요할까? 무엇보다 옵티마이저에 대한 깊은 이해가 바탕이 될 것이다. 옵티마이저의 동작 방식을 이해한다는 것은 단순히 사용자 질의에 대한 빠른 응답만을 목적으로 하기보다는 컴퓨팅 시스템 전반에 대한 물리적, 논리적, 수학적, 도덕적(?) 지식을 바탕으로 하여 궁극적으로 데이터에 대한 깊은 애정을 갖는 ‘데이터 전문가’로 나아갈 수 있는 관문이라고 생각한다.

우리의 여행은 ‘옵티마이저의 이해’

바로 필자와 최부열씨가 함께 떠난 여행의 주제는 ‘옵티마이저에 대한 이해’이다. 최부열씨는 현재 관공서 업무 전산화를 주로 하는 회사에서 팀장을 맡고 있고, 군대 시절부터 사용한 오라클 DBMS에 대해 많은 관심을 갖고 있는 사람이다. 최부열씨가 처음에 던진 질문들만 보더라도 얼마나 속속들이 오라클의 옵티마이저에 대해 알고 싶은 지 그 열정을 느낄 수 있을 것이다. 지면상의 이유로 여기서는 옵티마이저의 동작에 대한 핵심 몇 부분만 소개할 수밖에 없음이 안타까울 뿐이다. 최부열씨가 던진 옵티마이저에 대한 화두를 크게 7개로 묶으면 다음과 같다.

 

Q1. SQL문에 대한 실행계획을 수립해 비용산정 후 가장 비용이 적은 실행계획을 선택함에 있어서 포괄적이기는 하나 보편적인 실행계획 수립방법(원리) 비용산정 기준이 되는 통계정보 참조(인용) 기준에 대해 알고 싶다. 그리고 비용산정 기준에 대한 통계정보는 어떤 방식과 근거로 수집되어 관리되는지 궁금하다.

Q2. 옵티마이저가 더 정확한 판단을 하도록 유도하기 위해 사용자가 취할 수 있는 방법이 있다면 알고 싶다.

Q3. SQL문에 힌트를 추가한 경우 옵티마이저의 실행계획에 어떤 영향을 미치는지 궁금하다.

Q4. 스스로 학습하는 옵티마이저(Self-Learning Optimizer)에 대해 알고 싶다

Q5. 오라클에서 더 이상 RBO에 대해 지원하지 않는다고 하는데, 이는 CBO에 대한 자신감(?)이라고 이야기를 들었다. 이에 대한 의견을 듣고 싶다.

Q6. 오라클 버전이 8.1.7로 올라가면서 Analytic Function 기능이 추가됨으로써 기존 SQL문에서 어렵게 구현했던 정보들을 손쉽게 사용할 수 있게 되었다. 이 Analytic Function을 사용할 때 옵티마이저는 어떤 실행계획을 세우는 것인지 궁금하다.

Q7. 옵티마이저의 최적화 과정에 대한 세부 정보를 살펴볼 수 있는 방법에 대해 알고 싶다.

지면 관계상 이 글에서는 <질문 1>과 <질문 2>, 그리고 <질문 5>에 대한 답변만 다룬다. 나머지 답변은 아이마소(www.imaso.co.kr)를 통해 전체 내용을 공개할 것이다. 그럼 필자와 최부열씨가 30일 동안 치열하게 여행한 시간으로 들어가 보자.

질문 1, 옵티마이저가 실행계획을 수립하는 원리
첫 번째 질문은 오라클 옵티마이저가 실행계획을 수립하는 원리와 비용산정 방식에 대해 알고 싶다는 것이었다. 먼저 오라클 옵티마이저의 기본 아키텍처를 설명하면, SQL Trace 결과에 나타나는 수행 단계를 보면 Parse → Execute → Fetch 3단계로 이루어져 있는데, Parse 단계를 좀 더 자세히 들여다보면 다음과 같은 일들이 수행되고 있다.

1. Statement matching, syntactic and semantic checks : 해당 SQL이 라이브러리 캐시(Library Cache)에 이미 캐싱되어 있는지 확인하고, 구문(syntax) 검사와 의미(semantic) 검사를 수행하는 파싱 단계이다.
※ select * form dual → syntax error
※ select x from dual → semantic error

2. Query Transformation : 앞 단계의 수행결과인 Parse Tree를 분석해서 의미적으로 동일(같은 결과 집합을 반환)하면서도 더 나은 성능을 제공하는 형태로 재작성하는 단계이다. 옵티마이저 내부적으로 매우 여러 형태의 질의 변환을 수행하는데, 이해를 돕기 위해 View Merging에 대한 예를 하나만 보도록 하겠다.

select d.name, avg_sal_dept
from dept d
,(select deptno
, avg(sal) avg_sal_dept
from emp group by deptno) e
where d.deptno = e.deptno
and d.loc = ‘OAKLAND’;

select d.name, avg(sal)
from dept d, emp e
where d.deptno = e.deptno
and d.loc = ‘OAKLAND’
group by d.rowid, d.name;

3. Determine object costs and cardinalities : 각 오브젝트에 대한 액세스 방법(Access Method)과 비용을 계산하고 각 단계에서 반환되는 행의 수를 예측한다.

4. Cost different join orders : 조인 순서(Join Ordering)와 조인 방법(Join Method)을 평가하고, 총 비용(overall cost)이 가장 낮은 실행계획을 선택한다.

5. Build structures for runtime : 런타임시 SQL 실행 엔진이 사용할 수 있는 데이터 구조체로 포맷팅하고 라이브러리 캐시에 캐싱한다.

1번 파싱 단계는 쿼리 파서(Query Parser), 2~4까지의 옵티마이징 단계는 쿼리 옵티마이저, 마지막 5번 단계는 로우-소스 제너레이터(Row-Source Generator)에 의해 수행된다. 이렇게 생성된 로우 소스는 SQL 실행 엔진에 의해 수행되고 어레이 패치(Array Fetch) 명령을 통해 그 결과를 최종 사용자에게 전달하게 된다. 여기서 가장 핵심적인 단계는 2~4까지의 옵티마이징 단계인데, 이를 수행하는 옵티마이저는 <그림 1>에서 보는 것처럼 3개의 구성요소(components)로 이뤄져 있다.

◆ 옵티마이저의 구성요소
1. Query Transformer : 앞에서 설명한 Query Transformation을 담당한다.
2. Estimator : 데이터 딕셔너리에 저장된 다양한 통계 정보를 이용해서 선택도(Selectivity), 카디널러티(Cardinality), 비용(Cost) 등의 예상치 값(measure)들을 산정하는 역할을 수행하는데, 뒤에서 언급되는 비용 산정 방식에서 좀 더 자세히 설명하겠다.
3. 플랜 제너레이터(Plan Generator) : 주어진 쿼리를 위해 가능한 실행계획들을 생성해낸 후에 Estimator의 도움을 받아서 가장 낮은 비용을 갖는 하나를 선택하는 과정을 총괄한다.

지금까지 살펴본 것처럼 SQL 최적화 단계에서는 순식간이긴 하지만 굉장히 많은 일들이 내부적으로 수행되고 있으며, 따라서 가급적 이 단계를 반복적으로 수행하지 않도록 하기 위해 모든 DBMS는 나름대로의 캐싱 기술을 사용하고 있다. DBMS 스스로가 LRU 알고리즘을 사용해서 캐시 영역을 최적으로 운영하려고 노력하지만 캐시 영역은 어차피 유한한 자원이므로 개발자들의 노력이 보태져야 시스템 효율을 극대화할 수 있다. 그러기 위해서는 같은 형태의 SQL이 반복적으로 파싱되지 않도록 바인딩 변수를 적극적으로 사용해야 하겠고, 경우에 따라서는 캐싱 여부를 확인하는 작업(=소프트 파싱)까지도 생략하기 위한 커서 공유 기법들을 사용해야 한다.
<그림 2>에서 알 수 있듯이 옵티마이징 단계에서 비용 기준 옵티마이저(CBO)는 데이터 딕셔너리에 저장된 테이블과 인덱스에 대한 통계 정보를 광범위하게 사용하는데, 이 통계 정보들은 옵티마이저가 각각의 실행계획에 대한 ‘예상 비용(estimated cost)’을 산출하고 Decision Tree를 생성하기 위해 사용된다.



지금까지의 설명을 요약하면, 옵티마이저는 먼저 질의 수행을 위해 가능하다고 생각되는 실행계획들을 찾아내고 각 실행계획의 비용을 계산한다. 이렇게 계산된 비용들을 평가한 후에 가장 최소 비용이라고 판단되는 실행계획을 최종적으로 선택하는 과정이다. 그런데 여기서 옵티마이저가 실행계획들을 비교할 때 사용하는 기준으로서의 비용(cost)의 개념이 어디까지나 예상 비용임을 이해하는 것이 중요하다.

이제 이러한 기본적인 메커니즘을 바탕으로 플랜 제너레이터가 실행계획을 생성하는 원리와 Estimator가 비용을 산정하는 방식에 대해 좀 더 자세히 살펴보도록 하자.

 

풀랜 제너레이터가 실행계획을 생성하는 원리

먼저 가능한 실행계획들을 탐색하는 과정에 대해서 설명하면 여러 개의 테이블을 가지고 조인을 수행하고자 할 때 가능한 실행계획의 갯수는 몇 가지나 될까? 예를 들어, 조인 순서만을 고려하더라도 5개의 테이블을 조인하는 쿼리는 5!=120개의 가능한 실행계획을 갖는다. 120개 수행 순서에 포함되어 있는 각각의 테이블 조인에 대해 Nested Loop, Sort Merge, Hash Join 등의 다양한 조인 방법(Join Method)들을 고려해야 한다.

그리고 테이블을 액세스하는 방식에는 크게 두 가지가 있는데, 인덱스를 경유하는 방식과 Full Table Scan 방식이 있다. 그리고 인덱스를 액세스하는 방식에도 Index Range Scan, Index Unique Scan, Index Full Scan, Index Fast Full Scan, Index Skip Scan 등이 있고 계속해서 새로운 방식들이 도입되고 있다. 이런 모든 액세스 방법들까지 고려한다면 모두 얼마나 될까? 그리고 그러한 모든 실행계획의 예상 비용을 구하고자 한다면 시간은 얼마나 걸릴까? 5개의 테이블을 조인하기 위해서는 아마 수천 개의 실행계획을 비교 평가해야 하고 현재 이 정도까지는 옵티마이저가 아주 빠른 속도로 수행해내고 있다. 옵티마이저의 발전과 함께 하드웨어의 발전도 크게 한 몫을 했기 때문일 것이다.

그런데 흔한 경우는 아니겠지만 조인에 참여하는 테이블의 갯수가 10개 이상이 된다면 그 갯수는 기하급수적으로 증가해 수십, 수백만 개의 실행계획들을 비교해야만 하는 상황에 이를 것이며, 그렇다면 배보다 배꼽이 더 커져서 실제 SQL 수행시간보다 파싱에 소요되는 시간이 더 커질 수 있다. 따라서 옵티마이저가 가능한 실행계획을 모두 고려할 수는 없는 노릇이므로 오라클은 SQL 최적화에 걸리는 시간을 단축시키기 위해 비용을 계산해야 할 실행계획들의 수를 줄일 수 있는 지능적인 테크닉들을 사용하고 있다.

가장 중요한 테크닉은 적응적 탐색 전략이다. 1초 만에 수행될 수 있는 쿼리를 최적화하기 위해 10초를 소비하는 것은 낭비이다. 하지만 수십 분 내지는 한 시간 동안 수행되어야 하는 쿼리를 위해서라면 수초 이상 수분이 걸리더라도 더 나은 실행계획을 찾기 위해 계속적으로 최적화 단계를 수행하는 것이 결코 낭비가 아닐 수 있다. 그래서 쿼리 수행시 예상되는 총 수행시간에 비해 쿼리 최적화에 걸리는 시간이 일정 비율을 넘지 않도록 적응적인 탐색 전략을 사용한다.
또 다른 중요한 테크닉으로는 Multiple Initial orderings heuristic이 있다. 앞에서 설명한 방식에 의해 옵티마이저는 탐색 도중이더라도 최적이라고 판단되는 실행계획을 발견하면 더 이상 진행하지 않고 멈추게 되는데, 그럴 경우 미처 고려하지 않은 실행계획들 중에서 실제로 더 나은 실행계획이 있었을지도 모를 일이다. 그래서 처음 탐색을 시작할 때 임의의 순서로 시작하는 것보다는 heuristic한 방법을 동원해서 거의 최적이거나 최소한 아주 좋은 실행계획일 것이라고 판단되는 실행계획들 순으로 정렬한 후에 그 순서에 따라 일을 진행하는 방식을 사용한다.

 

비용산정 방식

이제, 비용산정 방식에 대해 간단히 살펴보겠다. 앞에서 잠시 언급했듯이 비용산정 모듈은 옵티마이저 내부에서 Estimator에 의해 수행되는데, 데이터 딕셔너리에서 관리하는 다양한 통계 정보를 기반으로 3개의 다른 ‘예상치’ 값(measure)을 계산해낸다.

1. 선택도(Selectivity)
2. 카디널러티(Cardinality, Rows)
3. 비용(Cost)

 

선택도는 특정 행 집합에서 해당 조건을 만족하는 레코드가 차지하는 비율을 가리키는 개념이다. 행 집합은 테이블 또는 뷰의 전체집합이 될 수도 있고, 조인 또는 Group By 연산에 의한 중간 집합일 수도 있다.
히스토그램 통계가 없다면 컬럼 값이 골고루 분포되어 있다는 가정을 세우고 테이블과 인덱스의 통계정보만을 이용해서 선택도를 계산하게 된다. 예를 들어, ‘=’ 조건으로 검색된 특정 컬럼의 distinct value가 20으로 분석되어 있다면 선택도는 0.2(=1/20)가 되고, 이는 데이터를 읽는 동안 20개 중 하나씩 조건을 만족하는 레코드를 찾게 될 것이라고 가정하는 것이다.
만약 히스토그램 정보가 생성되어 있다면 distinct value 값을 이용하지 않고 조건에 사용된 컬럼의 미리 계산된 분포 값을 직접 활용하게 되므로 훨씬 정확한 근거를 가지고 비용을 평가할 수 있게 된다.

 

Execution Plan
---------------------------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE (Cost=2 Card=1 Bytes=32)
1 0 TABLE ACCESS (BY INDEX ROWID) OF ‘EMP’ (Cost=2 Card=1 Bytes=32)
2 1 INDEX (FULL SCAN) OF ‘PK_EMP’ (UNIQUE) (Cost=1 Card=15)

 

카디널러티는 앞의 실행계획에서 Card로써 표시되는 부분인데, 최종결과 건수 혹은 다음 단계로 흘러 들어가는 중간결과 건수를 의미한다. 앞에서 계산된 선택도와 전체 레코드 수(num_rows)를 곱해서 계산하게 되는데, 정확한 카디널러티는 좋은 실행계획을 만들기 위한 가장 중요한 정보이다.
비용은 앞의 실행계획에서 Cost로 표시되는 부분으로서, 실행계획 상의 각 연산들을 수행할 때 소요되는 시간비용을 상대적으로 계산한 것이다. 내부적으로 사용되는 공식에 의해 계산되고, 주로 CPU 비용과 디스크 I/O 비용을 고려하게 된다.
사실 비용산정 과정에서 옵티마이저는 수많은 가정들을 세우고 계산식을 적용하게 되는데, 그러한 가정들이 항상 성립할 수 없는 불완전한 가정이므로 때로는 잘못된 비용을 산정할 수밖에 없는 한계를 지니게 된다. 그리고 이러한 말 못 할 무수히 많은 가정들을 어떻게 풀어나갈 것인가가 DBMS 업체들의 고민이다.
컬럼의 히스토그램 정보를 얼마나 정확히 유지할 것인가? 조건식에 사용된 컬럼 간의 결합 형태에 따른 정확한 분포도를 얻을 수 있는가? 서로 밀접한 상관관계를 갖는 컬럼간의 결합분포도는 어떤 계산식을 사용해야 정확히 산정할 수 있을까? 그렇다고 컬럼 간의 가능한 결합 형태에 따라 별도의 히스토그램 정보들을 모두 수집해서 관리할 수도 없는 노릇이다.

또한 SQL 수행 당시의 시스템 환경, 예를 들면 CPU 상황, 메모리 상황, 디스크 I/O 속도 등에 대한 고려가 중요한 변수인데, 사실 옵티마이저는 이런 실행환경에 대해서 많은 가정들을 세워 최적화를 수행하고 있다. 옵티마이저 자체가 특정 상황에 맞춰서 최적화되어 있다는 이야기인데, 각 시스템의 가변적인 환경요인에 의해 생기는 오차를 보정해 주기 위해 옵티마이저에게 도움을 줄 수 있는 몇 개의 초기화 파라미터들이 있다. 이에 대해서는 다음 질문에 대한 답변을 통해 설명하도록 하겠다.

 

질문 2, 옵티마이저의 정확한 판단을 돕는 방법

옵티마이저가 보다 정확한 판단을 하도록 유도하기 위해 사용자가 취할 수 있는 방법이 있다면 알고 싶다는 것이 두 번째 질문이었다.

 

최적의 SQL 사용과 전략적 인덱스 구성

우선 최적의 SQL 사용과 전략적인 인덱스 구성을 해주어야 한다. 인덱스뿐만 아니라 각종 무결성 제약조건(Integrity Constraints)도 적절히 사용함으로써 옵티마이저가 정확한 판단을 할 수 있도록 단서를 제공해 주어야 한다. 이는 결국 사람만이 할 수 있는 부분이며, 옵티마이저는 사람이 내리는 명령과 사람이 제공해 주는 옵티마이징 팩터들을 가지고 주어진 환경에서 최선을 다할 뿐이다. 인덱스 구성과 SQL문 자체가 옵티마이저에게 미치는 영향은 너무나 지대하고 많이 알려진 사실이므로 따로 설명하지 않겠다. 반면 제약 설정이 옵티마이저에게 미치는 영향에 대해서는 잘 모르거나 잘못 알고 있는 사람들이 많으므로 설명이 필요할 것 같다.
PK, FK, NOT NULL 등과 같은 제약조건들은 데이터의 무결성을 보장해 줄 뿐만 아니라 옵티마이저가 실행계획을 생성하고, 질의 재작성(query rewrite)를 수행하는 과정에서 매우 중요한 정보 역할을 한다. 가장 간단하면서도 흔한 예가 NOT NULL 제약 사용과 관련된 것인데, 컬럼이 NULL 값을 허용하느냐 안 하느냐는 SQL 옵티마이저 입장에서는 굉장히 중요한 정보이다. 그런데 대개의 프로그래머들이 NOT NULL 제약조건 사용하기를 아주 꺼려한다. 프로그래밍하는 도중에 자주 에러를 만나게 된다는 이유인데, 다음의 예를 보면 생각이 달라질 것이다.

 

SELECT 부서번호, COUNT(*) FROM 사원 GROUP BY 부서번호;

 

'부서번호’에 인덱스가 있고, NOT NULL이 정의되어 있다면 FULL INDEX SCAN 또는 이보다 더 빠른 FAST FULL INDEX SCAN을 통해 인덱스만 가지고도 SQL을 처리할 수 있다. 그런데 업무적으로 NULL을 허용할 수 없는 컬럼임에도 불구하고 NOT NULL 제약조건을 정의하지 않았다면 옵티마이저 입장에서는 FULL TABLE SCAN을 할 수 밖에 없다. 오라클은 INDEX에 NULL 값을 저장하지 않기 때문인데, 이를 해결하기 위해 ‘WHERE 부서번호 IS NOT NULL’이라는 조건을 추가해 주면 또다시 FULL INDEX SCAN으로 잘 풀린다. 하지만 업무적으로 볼 때 부서번호가 NULL인 경우는 없고 NULL 값이 들어오지 않도록 애플리케이션 로직으로 처리했음을 개발자들은 알고 있기 때문에 ‘WHERE 부서번호 IS NOT NULL’ 조건을 불필요한 것이라고 여기고 조건을 생략한다. 하지만 이를 받아들이는 옵티마이저 입장에서는 정보의 부재로 인해 FULL TABLE SCAN을 할 수밖에 없는 입장에 놓이게 된다는 이야기이다. 시스템 운영 초기에는 별 문제 없이 잘 돌아가겠지만 점점 데이터가 쌓여갈수록 이 작은 실수 하나가 엄청난 성능 저하를 초래하게 되는 것이다. 참고로 SQL 서버와 같은 DBMS에서는 NULL 값도 INDEX에 저장되기 때문에 이와 같은 상황에서 인덱스를 제대로 활용한다.
한 가지 사례를 더 든다면, 서브 쿼리에서 반환되는 컬럼에 UNIQUE 인덱스를 갖거나 UNIQUE 또는 PK 제약이 설정되어 있다면 메인 쿼리에 결과 집합을 제공하기에 앞서 수행되어야 하는 SORT 연산을 생략할 수 있다.
앞의 사례를 통해 알 수 있는 것처럼 DEPT 테이블 DEPTNO 컬럼에 PK 또는 UNIQUE 제약을 설정한 것이 옵티마이저가 SORT 연산을 생략할 수 있도록 실마리를 제공한 것이다.


좀 더 현실적으로 일어날 수 있는 상황을 예로 들기 위해, 어떤 쇼핑몰의 고객 중 ‘서울’에 거주하는 고객이 1000만 명이고, 하루 주문량이 10만 건인데, 고객 ID 컬럼에 중복 값이 전혀 없다는 정보를 옵티마이저에게 제공하지 않았다고 가정하겠다. 이런 상황에서 다음의 SQL이 어떻게 수행될 지 그리고 시간이 얼마나 걸릴 지를 앞의 실행계획을 참고해서 가늠해 보기 바란다.

 

SELECT 주문번호, 주문일, 배송지, 고객ID, ...
FROM 주문
WHERE 고객ID IN (SELECT 고객ID FROM 고객 WHERE 거주지역 = ‘서울’)
AND 판매일 = :YESTERDAY

 

이외에도 무수히 많은 사례들이 있고 굉장히 중요한 내용이지만, 미처 다 소개해 드릴 수 없는 것이 안타까울 뿐이다. 그리고 앞서 CBO의 최적화 수행과정을 설명하면서 질의 재작성(Query Rewrite)에 대해서 설명했는데, 옵티마이저가 효과적으로 질의를 재작성하기 위해서는 변경 전 SQL과 변경 후 SQL의 결과가 동일하다는 단단한 보증이 필요하다. 그런데 필요한 제약들이 제대로 설정되어 있지 않다면 결과에 대한 자신감을 가질 수 없어 옵티마이저는 질의 재작성을 포기하게 된다. 제약설정은 옵티마이저에게 매우 중요한 메타데이터 역할을 한다는 것을 기억하기 바란다.

 

<리스트 1> Unique 제약에 의한 SORT 연산 생략

SQL> Create index emp_deptno_idx on emp(deptno);

SQL> set autotrace traceonly explain

SQL> select empno, ename, sal, deptno
2 from emp
3 where deptno in (
4 select /*+ use_nl(dept) */ deptno
5 from dept
6 where loc = 'CHICAGO';

Execution Plan
----------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE
1 0 TABLE ACCESS (BY INDEX ROWID) OF 'EMP'
2 1 NESTED LOOPS
3 2 SORT (UNIQUE)
4 3 TABLE ACCESS (FULL) OF 'DEPT'
5 2 INDEX (RANGE SCAN) OF 'EMP_DEPTNO_IDX' (NON-UNIQUE)

SQL> alter table dept add
2 constraint uk_dept_deptno unique(deptno);

SQL> select empno, ename, sal, deptno
2 from emp
3 where deptno in (
4 select /*+ use_nl(dept) */ deptno
5 from dept
6 where loc = 'CHICAGO';

Execution Plan
--------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE
1 0 TABLE ACCESS (BY INDEX ROWID) OF 'EMP'
2 1 NESTED LOOPS
3 2 TABLE ACCESS (FULL) OF 'DEPT'
4 2 INDEX (RANGE SCAN) OF 'EMP_DEPTNO_IDX' (NON-UNIQUE)

 

적절한 비용 모델을 선택하는 것

그리고 옵티마이저의 정확한 판단을 유도하기 위해 적절한 비용 모델(Cost Model)을 선택해 주어야 한다. 옵티마이저가 지니는 가장 큰 한계 중 하나가 지금 질의요청을 받은 SQL이 어떤 환경에서 수행되고 있는지, 즉 클라이언트 특징과 애플리케이션 커서의 수행 방식을 이해할 수 없다는 점이다. 한번 예를 들어 보자.
어느 회사나 ‘고객’ 테이블이 있는데, ‘고객명’ 컬럼에 인덱스가 있다면 다음의 SQL을 수행하기 위해서는 테이블 전체를 스캔한 후 ‘고객명’ 순으로 정렬하는 방식과 정렬 연산을 생략하기 위해 인덱스를 경유하는 방법이 있다. 그러면 옵티마이저는 둘 중 어느 것을 선택하는 것이 가장 현명할까?

 

SELECT * FROM 고객 ORDER BY 고객명

 

잘 알겠지만 이 SQL이 OLTP성 환경에서 수행된다면 인덱스를 사용하는 것이 나은 방법일 수 있지만 전체범위를 처리하는 배치 프로그램 또는 DSS 환경이라면 테이블 전체를 빠르게 읽어들인 후 정렬하는 것이 더 나은 방법일 것이다. 어떤 환경에서 수행되느냐에 따라 최선이 바뀔 수 있다는 것이다.


그런데 옵티마이저가 이런 판단을 스스로 할 수는 없으므로 오라클에서는 두 가지 비용 모델(Cost Model)을 제공하고 사용자가 둘 중 하나를 선택할 수 있도록 하고 있다. All_ROWS와 FIRST_ROWS가 그것인데, 옵티마이저의 최적화 목표를 가장 빠른 응답속도에 맞출 것인지 아니면 전체범위 처리 효율에 맞출 것인지를 설정하는 옵션이다. 그런데, FIRST_ROWS는 부분범위 처리(Partial Range Scan)를 하고 ALL_ROWS는 전체범위 처리를 하도록 지시하는 힌트라고 잘못 알고 있는 사람이 많다. 옵티마이저 모드는 실행계획을 작성하는 단계에서 최적화 목표를 달리 가져갈 뿐이며 실행계획이 수립되고 나서 수행 단계에 들어가서는 부분범위 처리를 불가능하게 만드는 오퍼레이션(예를 들어, Distinct, Group by, Merge Join 등)이 중간에 포함되어 있지 않는 한 ALL_ROWS 모드에서도 부분범위 처리를 한다.


이 두 옵티마이저 모드의 차이점을 짧게 요약해서 말하면, FIRST_ROWS는 전체 결과 집합 중 ‘첫 번째 행을 가장 빨리 반환할 수 있는 방식’으로 실행계획을 수립한다는 것이고, ALL_ROWS는 첫 번째 행을 반환하는 시간은 조금 더 걸리더라도 ‘전체 결과 집합을 가장 빨리 처리할 수 있는 방식’으로 실행계획을 수립한다는 것이다. 따라서 FIRST_ROWS는 대개 INDEX SCAN을 선호할 것이고 반대로 ALL_ROWS는 INDEX SCAN보다는 TABLE FULL SCAN을 선호할 것이다.


앞에서 제시한 SQL 옵티마이저 모드를 변경하면서 각각 실행시켜 보면 그 차이를 명확히 알 수 있을 것이다. 아마 FIRST_ROWS 모드에서는 정렬 연산을 생략하고 빠르게 첫 번째 행을 반환하기 위해 ‘고객명’ 컬럼에 생성한 인덱스를 사용하는 실행계획을 수립할 것이고, ALL_ROWS 모드에서는 아마 TABLE FULL SCAN 방식으로 처리되었을 것이다.


이렇듯 비용 모델에 대한 정확한 이해를 바탕으로 적당한 옵티마이저 모드를 선택하는 것이 중요한데, 옵티마이저 모드를 잘 설정하더라도 한계는 여전히 존재한다. 우리가 SQL을 튜닝하고자 할 때 자주 사용하는 것이 SQL Trace 결과인데, Parse, Execute, Fetch 각 단계에서 나타나는 숫자들(수행횟수, CPU 및 Elapsed Time, 물리적 I/O, 논리적 I/O, 처리된 결과 레코드 수)을 보고 문제점들을 찾아내서 인덱스를 조정해 주고 SQL의 비효율적인 부분을 수정한다. 하지만 옵티마이저는 Parse 단계에만 관여할 뿐 이후 단계에서 어떤 일들이 일어나는지에 대해서는 전혀 모니터링하고 있지 않기 때문에(앞으로 발전해야 할 CBO의 모습이다) 한계점을 갖게 된다는 것이다. 다시 말해, OLTP 환경에서 FIRST_ROWS 모드로 설정하더라도 개발자가 SQL 수행과 동시에 모든 결과 행을 그리드에 뿌려주는 방식으로 애플리케이션을 작성했거나 커서를 연 후에 패치를 많이 수행한 다음 뒤쪽 일부 데이터만 출력하는 방식으로 코딩했다면 인덱스를 경유하는 방식이 오히려 훨씬 많은 블럭 I/O를 일으켜 낭패를 보게 된다. 따라서 SQL 수행 후 애플리케이션에서 커서를 어떻게 다룰 것인지는 개발자의 판단과 능력, 습관 내지는 선호에 해당하는 부분이어서 옵티마이저가 아무리 발전한다고 해도 개발자의 역할이 가장 중요하다고 하겠다.

 

초기화 파라미터의 적절한 조정

이와 함께 초기화 파라미터를 적절히 조정해 주어야 한다. 비용 기반 옵티마이저의 행동에 영향을 미칠 수 있는 초기화 파라미터들은 매우 많다. 앞서 소개한 OPTIMIZER_MODE를 포함해서 20~30여 개쯤 되는데, 지면관계상 모든 파라미터를 설명할 수는 없으므로 몇 가지만 설명하겠다.


옵티마이저는 Nested Loop Join시 Inner(=Drived) 테이블의 데이터를 매번 디스크에서 읽어온다는 가정을 하고 비용을 계산한다. 하지만 실제로는 DB 버퍼 캐시에 존재하는 블럭을 읽을 확률이 더 높기 때문에 예상보다 훨씬 적은 수의 디스크 I/O가 발생하게 되고, 특히 Inner 테이블을 액세스하기 위해 경유하는 인덱스의 클러스터링 팩터가 매우 좋다면 인덱스뿐만 아니라 테이블 데이터 블럭 I/O도 훨씬 줄게 된다. 따라서 Nested Loop 조인에 대한 옵티마이저의 비용 산정은 가장 최악의 상황을 가정한 것이 되고, 따라서 소트 머지 조인(Sort Merge Join)이나 해시 조인(Hash Join)이 선택될 가능성이 높아지게 된다.


OPTIMIZER_INDEX_CACHING와 OPTIMIZER_INDEX_ COST_ADJ 파라미터의 설정 값을 조정함으로써 이러한 예측치와 실측치 간의 차이를 보정해 주면 효과적인데, 좀 더 자세히 보도록 하겠다.


OPTIMIZER_INDEX_CACHING 파라미터는 디폴트 0으로 설정되어 있으며 가능한 값의 범위는 0~100이다. 이 값은 인덱스 블럭이 DB 버퍼 캐시에서 찾아질 가능성을 의미하는데, 예를 들어 100으로 설정할 경우 SQL 조건에 따라 읽어야 하는 인덱스 블럭이 100% DB 버퍼 캐시에서 찾아질 것을 가정하는 것이고, 0으로 설정할 경우 버퍼 캐시에서 찾아질 가능성이 0이라고 가정하는 것이다. 따라서 이 파라미터를 각자의 시스템 실정에 맞게 조정한다면 옵티마이저가 이전보다 더 좋은 실행계획을 수립할 가능성이 매우 높아진다.


OPTIMIZER_INDEX_COST_ADJ 파라미터는 디폴트 100으로 설정되어 있으며, 가능한 값의 범위는 1부터 10000까지이다. 이 파라미터는 인덱스 탐색 비용에 대한 평가를 조정하기 위해 사용하는 것으로서, 디폴트 설정 값 100은 인덱스에 대한 액세스 비용이 보통의 비용 모델에 근거해서 평가될 것이고, 10으로 설정한다면 보통의 인덱스 액세스 비용의 1/10로 평가될 것임을 의미한다. 따라서 이 값이 작을수록 인덱스를 경유한 테이블 액세스(싱글 블럭 I/O) 비용이 더 적게 평가될 것이고, 클수록 인덱스를 경유하지 않는 Full Table Scan(멀티 블럭 I/O)의 액세스 비용이 더 적게 평가될 것이다. 바꿔 말하면, 이 파라미터는 싱글 블럭 I/O와 멀티 블럭 I/O에 대한 상대적인 평가로 해석할 수 있고, 따라서 테이블 데이터 블럭이 평균적으로 얼마만큼 캐싱되어 있는지에 대한 의미로 해석할 수도 있다. 앞에서 설명한 OPTIMIZER_INDEX_CACHING이 인덱스가 캐싱되어 있을 확률을 가리키는 것처럼 말이다.
사실 이 두 파라미터의 기본 설정은 DW 환경에나 적합한 것이다. 따라서 더 일반적이라고 할 수 있는 OLTP 성향의 시스템에서는 OPTIMIZER_INDEX_CACHING을 훨씬 높게 설정하고 OPTIMIZER_INDEX_COST_ADJ를 줄여 줄 필요가 있다.
오해하지 말아야 할 것은 이들 파라미터를 조정하는 것이 SQL을 더 빠르게 수행되도록 한다거나 인덱스가 얼마만큼 캐싱될 지를 조절하는 것이 아니라는 사실이다. 다만 선정된 실행계획들을 평가하고 선택하는 과정에 영향을 주기 위해 사용하는 것으로서, 옵티마이저가 아무리 똑똑하더라도 사람만이 판단할 수 있는 그런 정보들을 옵티마이저에게 전달해 줌으로써 옵티마이저가 스스로 최선의 선택을 해나갈 수 있도록 도와주기 위한 기능이다.


앞에서 이미 살펴봤듯이 많은 조인을 수반하는 SQL 하나를 파싱하고 최적화하기 위해서는 무수히 많은 실행계획들을 생성해서 평가해 봐야만 하는데, 이러한 CBO의 일의 양을 조절하기 위해 사용할 수 있는 파라미터가 OPTIMIZER_SEARCH_LIMIT과 OPTIMIZER_ MAX_PERMUTATIONS이다. 이외에도 SORT_AREA_SIZE, DB_FILE_MULTIBLOCK_READ_COUNT 등이 옵티마이저가 해시 조인과 머지 조인 그리고 테이블 스캔의 비용을 평가하는 데 큰 영향을 미친다.

 

주기적인 통계정보의 갱신

또한 주기적으로 모든 테이블과 인덱스에 대한 통계정보를 갱신해 주어야 한다. 앞서 답변에서도 설명했듯이 CBO가 제대로 작동하기 위해서는 통계정보가 반드시 필요하며, Analyze 명령 또는 DBMS_ STATS 패키지의 다양한 프로시저 호출을 통해 수집된다. 또한 인덱스 컬럼 값의 데이터 분포가 균일하지 않은 상황에서는 컬럼 값에 대한 히스토그램 정보를 생성해 주는 것이 매우 유용한데, 만약 컬럼의 히스토그램 정보가 없으면 옵티마이저는 컬럼의 데이터 분포가 균일하다고 가정하기 때문이다.


만일 컬럼 데이터의 분포가 균일하지 않은 상황에서 히스토그램 정보가 없다거나, 있는데도 옵티마이저가 엉뚱한 실행계획을 생성하는 경우에는 특별한 조치가 필요하다. 특히 바인드 변수를 사용할 경우에는 앞으로 바인딩될 값을 옵티마이저가 미리 알 수 없기 때문에 균등 분포를 가정한 실행계획을 수립하게 되어 아무리 히스토그램 정보를 생성해 두었어도 무용지물이 된다. 따라서 이런 경우에는 반드시 바인딩되는 값의 종류에 따라 실행계획이 분리될 수 있도록 SQL을 구사해 주어야 하며, 만약 이런 기법을 사용할 수 없는 상황이라면 다이내믹 SQL을 사용하는 편이 오히려 더 나을 수도 있다.


오라클9i R2부터는 시스템 통계(System Statistics)라는 기능을 제공하고 있는데, 해당 시스템만의 독특한 성능 특성을 실제적으로 측정함으로써 옵티마이저에게 더 정확한 정보를 제공하기 위한 것으로서 반드시 이 기능을 활용할 것을 권장한다.
실행계획 각 단계의 액세스 비용을 평가하기 위해 주로 CPU 비용과 디스크 I/O 비용을 고려한다고 앞에서 언급했었다. 따라서 해당 시스템의 특징을 이해하고 I/O 비용과 CPU 비용 사이에 적절한 비중을 적용해서 실행계획을 평가하는 것이 가장 효율적인 실행계획을 선택하는 데 있어 매우 중요한 요소이다. 그런데 앞서 소개한 파라미터 OPTIMIZER_INDEX_CACHING과 OPTIMIZER_INDEX_ COST_ADJ의 경우만 보더라도 오라클에서 설정한 기본 값들이 특정 상황에 맞춰져 있어 우리 시스템 환경에 맞지 않는 경우가 흔하고, CPU와 디스크 성능에 대한 평가도 옵티마이저를 개발하기 위해 사용된 특정 환경에 맞춰져 있기 때문에 실제 오라클이 인스톨되는 플랫폼에는 상대적으로 맞지 않을 수 있다. 그리고 시스템의 I/O 특징이라는 것이 여러 다른 요인들에 의해 영향을 받기 때문에 항상 고정적이지도 않다.
그래서 최근에 오라클에서 제공하기 시작한 것이 시스템 통계(System Statistics)인데, 시스템 통계를 생성할 때는 지정한 기간 동안 해당 시스템에서 실제로 일어나고 있는 활동들을 관찰하고 분석한다. CPU 성능, 싱글 블럭 I/O 성능, 멀티 블럭 I/O 성능이 주요 관찰 대상인데, 추측이 아닌 실측치를 갖고 실행계획에 나타나는 각 오퍼레이션에 대한 CPU와 디스크 성능을 평가함으로써 더 정확한 실행계획을 수립할 수 있게 된 것이다.


심지어 우리의 데이터베이스 시스템이 낮에는 OLTP성 트랜잭션을 처리하고 밤에는 주로 OLAP 리포트를 생성한다면 양쪽 모두에 대한 통계 정보를 수집하고 나서 필요할 때마다 OLTP 또는 OLAP 통계 정보로 활성화시킬 수가 있다. 이것은 옵티마이저가 현재 이용 가능한 시스템 리소스의 상황을 고려해서 비용을 산출하는 것과 비슷한 효과를 얻을 수 있게 한다.


시스템 통계를 사용하는 실질적인 사례를 간단히 보여주겠다. 우선 시스템 통계를 수집해야 하는데, 시스템이 가장 일반적인 부하를 갖는 시간대를 선택해야 한다. 다음의 명령어를 통해 낮 시간에 720분 동안 통계를 수집하고 mystats 테이블에 저장하게 된다.

 

DBMS_STATS.GATHER_SYSTEM_STATS(
gathering_mode => ‘interval’,
interval => 720,
stattab => ‘mystats’,
statid => ‘OLTP’);

그리고 다음의 명령어를 통해 밤 시간에도 시스템 통계를 수집하고, mystats 테이블에 저장한다.

DBMS_STATS.GATHER_SYSTEM_STATS(
gathering_mode => ‘interval’,
interval => 720,
stattab => ‘mystats’,
statid => ‘OLAP’);

이제 남은 일은 수집된 통계 정보를 갖고 데이터 딕셔너리를 갱신하는 것인데, 낮에는 낮 시간에 얻은 통계 정보를, 밤에는 밤 시간에 수집한 통계 정보를 사용하게 될 것이다. 다음의 명령어는 OLTP 통계 정보를 import한다.

DBMS_STATS.IMPORT_SYSTEM_STATS(‘mystats’, ‘OLTP’);

밤이 되면 다시 다음의 명령어를 통해 OLAP 통계정보를 import한다.

DBMS_STATS.IMPORT_SYSTEM_STATS(‘mystats’, ‘OLAP’);

이런 import 작업들은 DBMS_JOB 패키지를 이용해서 JOB으로 등록하고 적당한 시간에 자동으로 수행되도록 하는 것이 나을 것이다.

세션 레벨에서의 파라미터 변경

세션 레벨에서 파라미터를 변경해 주어야 한다. 오라클이 라이브러리 캐시에서 SQL의 캐싱 여부를 확인할 때, 실행계획 생성에 영향을 미칠 수 있는 세션 레벨 파라미터들의 설정 값이 같은지에 대해서도 확인을 한다(Environment Match Check). 한 가지 예(<리스트 2>)를 보겠다.

<리스트 2> Environment Match Check
SQL> alter system flush shared_pool;

SQL> alter session set workarea_size_policy = manual;

SQL> select count(*) from emp;

SQL> alter session set workarea_sise_policy = auto;

SQL> select count(*) from emp;

SQL> select sql_text, child_number, hash_value, address from v$sql
2 where sql_text like 'select count(*) from emp%';

SQL_TEXT CHILD_NUMBER HASH_VALUE ADDRESS
---------------------------- -------- ------- ----------------
select count(*) from emp 0 2744835752 C0000000169DE890
select count(*) from emp 1 2744835752 C0000000169DE890

현재 <리스트 2>의 SHARED_POOL에는 같은 SQL에 대해 두 개의 공유 커서가 캐싱되어 있는 것을 확인할 수 있는데, 옵티마이저에 영향을 줄 수 있는 파라미터를 변경했기 때문이다. 이러한 특징을 이용해서 각 애플리케이션마다 다르게 세션 파라미터를 설정해 준다면 같은 SQL이더라도 수행되는 애플리케이션 특성에 맞춰서 다른 실행계획이 사용되도록 조정할 수 있다.
최적화를 위한 마지막 히든 카드, 힌트를 사용할 수 있다. 어떤 힌트를 어떻게 사용해야 하는지에 대한 내용은 너무 방대하므로 생략하기로 하겠다. 다만, 힌트를 사용해서 무작정 실행계획을 자신의 생각대로 제어하려고 하기 보다는 앞에서 설명한 방법들을 이용해 좀 더 정확한 통계정보를 제공하고 초기화 파라미터를 적절히 설정해 줌으로써 옵티마이저가 스스로 일을 잘 할 수 있도록 도와주려는 노력이 선행되어야 함을 기억하기 바란다.

 

질문 3, 오라클에서 RBO를 지원하지 않는 이유

최부열씨는 오라클에서 더 이상 RBO에 대해 지원을 하지 않는다고 하는데, 이는 CBO에 대한 자신감이라는 이야기를 들었단다. 정말일까?
RBO는 오라클10g부터 더 이상 지원되지 않는다. Rule-Based Optimizer도 하나의 소프트웨어인데 이 소프트웨어에 문제가 발견되더라도 이를 바로 잡기 위해 코드에 손대는 일이 없을 것이고, 새로운 객체 타입이 생겨 파싱에 문제가 생기더라도 그대로 놔두겠다는 이야기이다.


따라서 오라클9i R2가 RBO를 지원하는 마지막 버전이 되었다. 없어질 것이라는 이야기가 이미 오래 전부터 있었지만 그동안 계속 지원했던 것은 CBO에 대한 자신감이 없어서가 아니라 하위 버전 호환성(Backward Compatible) 때문이다. RBO가 나은지 CBO가 나은지는 더 이상 논란거리가 될 수 없는데, 데이터가 점점 더 대용량화되어 가는 현대의 IT 환경에서 CBO의 선택이 당연하다고 볼 수 있다.
대용량을 처리해야 하는 배치 쿼리나 DSS 환경에서 RBO가 인덱스를 사용함으로써 오히려 낭패를 본 경험들을 많이 했을 텐데, RBO는 잘 알다시피 “인덱스는 항상 좋고, 따라서 항상 인덱스를 사용해야 한다”라는 우선순위에 따라 행동하기 때문에 대용량 데이터를 처리할 때 종종 그런 심각한 문제를 일으키게 되는 것이다.
만약 CBO가 똑같은 상황에 처한다면 아마 인덱스가 아닌 다른 방법을 사용해서 SQL을 수행했을 텐데, 이는 CBO가 인덱스를 무시한다거나 덜 선호해서 그런 게 아니라 다만 데이터의 볼륨을 고려해서 인덱스의 사용이 최선이 아니라는 판단을 한 것뿐이다. 그리고 CBO는 현재 RBO가 이용할 수 없는 더 많은 무기들을 갖고 최적화를 수행한다.

 

옵티마이저 힌트의 사용과 관련된 문제

CBO를 사용하기로 결정했다면 또 한 가지 매우 중요한 결정사항이 남아 있는데, 바로 옵티마이저 힌트(Optimizer Hints)의 사용과 관련된 문제이다. 오라클이 CBO에 대한 자신감을 갖게 되었냐고 질문했는데, 아마도 RBO와의 비교보다는 CBO가 힌트 없이도 충분히 효율적인지에 대한 질문으로 파악된다. 이미 오라클10g에서 지원하지 않기로 한 이상 RBO냐 CBO냐에 대한 논쟁은 의미 없어진 반면 힌트 사용에 관한 논쟁은 앞으로도 상당기간 동안 지속될 것으로 보인다.
어떤 개발팀에서는 하나의 SQL에 대한 최적의 실행계획은 단 하나만 존재한다고 믿기 때문에 모든 SQL에 힌트를 달아서 실행계획이 변하지 않도록 고정시켜 버린다. 이러한 팀에서 통계정보는 임의질의(Ad-hoc Query)를 위해서만 필요하다고 생각한다. 반대로 어떤 개발팀에서는 데이터가 시시각각 변하기 때문에 통계정보의 변화에 따라 실행계획도 적절히 바뀔 수 있도록 옵티마이저에게 재량권을 줘야 한다고 주장한다. 따라서 가급적 힌트 사용을 자제해야 한다는 입장에 서 있는 것이며, CBO가 가진 기본 사상에 따르면 이 주장이 옳을 수 있다.
모든 SQL의 실행계획을 고정시켜야 한다고 생각하고 또 그렇게 개발하고 있는 팀에서는 그런 불안은 다소 덜하지만 데이터 볼륨의 급작스러운 증가가 발생하거나 새로운 인덱스가 추가 또는 삭제될 때마다 관련 있는 SQL들을 찾아 일일이 확인하고 바뀐 환경에 맞게 변경해 주어야 한다. 게다가 모든 SQL이 프로그램 소스에 내장되어 있다면 보통 까다로운 작업이 아닐 수 없다. 따라서 이 방식을 선호하는 팀이라고 하더라도 SQL에 직접 힌트를 적용하기보다는 오라클 8i부터 지원되기 시작한 Optimizer Plan Stability(Stored Outlines) 기능을 적극적으로 활용하는 것이 좋다.


옵티마이저가 완벽하지 않은 한 최적화에 실패하는 10%에 대해서는 어쩔 수 없이 힌트를 사용해야 한다는 데에는 CBO를 신봉하는 입장에서도 이견이 있을 수 없고, 따라서 앞에서 소개한 논쟁은 주로 후자를 염두에 두고 있다. 즉 힌트를 사용하기 때문에 생기는 비용을 지불해서라도 90점 짜리 실행계획을 100점으로 만들기 위해 노력할 것인가의 문제인데, 과연 어느 쪽 주장이 더 타당한 것일까?
시스템이 처한 환경에 따라 다른 전략을 가져가야 한다는 건 뻔한 대답이겠지만, 테이블 볼륨이 빠르게 자주 변하는 환경에서는 옵티마이저가 SQL 수행시에 자유롭게 더 좋은 실행계획을 찾을 수 있도록 믿고 맡겨야 한다는 주장이 설득력을 얻을 수 있다. 특히 Ad-hoc Query가 주를 이루는 DSS 환경에서는 다른 선택의 여지가 없다. 반면, 시스템이 아주 정적이어서 테이블 통계정보가 거의 바뀌지 않거나 동시접속자가 매우 많아서 미션 크리티컬 업무가 주를 이루는 환경이라면 이야기가 달라진다. 힌트를 사용해서라도 모든 SQL의 실행계획을 100점 짜리를 만들어 줌으로써 시스템이 안정적으로 운영될 수 있도록 해야 한다.

“SQL 개발자는 옵티마이저가 되어야 한다”

 

마지막으로 강조하고 싶은 것은, SQL 개발자 스스로가 옵티마이저가 되어야 한다는 것이다. 사용자에 의해 이미 작성된 SQL에 대한 실행계획을 현재의 90점 수준에서 그 이상으로 끌어올리는 문제는 하드웨어의 발전을 통해 더 많은 통계정보를 활용할 수 있게 되고 새로운 알고리즘이 개발되면서 지속적으로 달성되어갈 것이다. 그런데 옵티마이저가 생성해내는 실행계획의 효율성 문제와는 별개로 SQL 자체의 효율성 문제를 생각해 보자는 것인데, 사실 이마저도 옵티마이저의 질의 재작성 기능을 통해 어느 정도까지는 보완되어가고 있다. 하지만 기본적인 형태의 SQL에 대해서만 가능한 일이며, 현실의 복잡한 업무를 반영하고 있는 모든 SQL을 분석해서 의미적으로 동일(같은 결과 집합을 반환)하면서도 더 나은 성능을 제공하는 형태로 재작성할 수 있기를 옵티마이저에게 바라기는 무리이다. 옵티마이저에게 의지하기보다는 내가 효율적인 SQL을 구사해 주면 될 일이다. 참고로 『An Introduction to Database Systems』의 저자로 유명한 C.J.Da te는 간단한 SQL문 하나를 갖고도 52개의 다른 SQL문으로 표현이 가능함을 보여준 적이 있다. SQL 자체를 어떻게 해서든 최적으로 만들어내려는 노력은 현재나 미래에나 반드시 필요한 개발자의 필수 덕목임을 다시 한번 강조하는 것으로 답변을 마치겠다.


'OraclE' 카테고리의 다른 글

How to Relink Oracle Database Software on UNIX  (0) 2008.11.20
relink  (0) 2008.11.20
오라클 analyze 자동화  (0) 2008.11.06
Listener.log 파일 남기지 않는 방법  (0) 2008.10.22
DataPump *  (0) 2008.10.17