옵티마이저 입문: SQL의 실행 방법을 결정하는 원리
같은 결과를 더 적은 I/O·Call·Sort·반복으로 만드는 SQL 최적화의 기본 흐름을 옵티마이저 추정과 실제 실행 통계로 연결합니다.
핵심 요약
SQL은 원하는 결과를 선언하지만, 그 결과를 만드는 구체적인 처리 순서와 접근 방법까지 모두 지정하지는 않습니다. Oracle의 옵티마이저(Optimizer) 는 SQL과 통계정보, 인덱스·제약조건, 데이터 분포, 최적화 환경을 바탕으로 여러 후보 실행 방법을 비교하고 실행계획을 선택합니다.
파싱된 SQL
→ Query Transformer: 의미가 같은 다른 SQL 형태 검토
→ Estimator: Selectivity·Cardinality·Cost 추정
→ Plan Generator: Access Path·Join Order·Join Method 후보 비교
→ 실행계획 선택
→ Row Source Tree가 계획을 실제 수행
옵티마이저를 이해할 때 가장 먼저 기억할 기준은 다음과 같습니다.
- 같은 결과를 만드는 실행 방법은 여러 가지일 수 있습니다.
- 옵티마이저는 검토한 후보 가운데 예상 Cost가 가장 낮은 계획을 선택합니다.
- Cost는 실제 수행시간이 아니라 후보 계획을 비교하기 위한 내부 추정값입니다.
- 계획 품질은 Selectivity와 Cardinality 추정의 정확성에 크게 영향을 받습니다.
- 인덱스 사용 여부가 아니라 최종 결과를 만드는 전체 비용으로 판단해야 합니다.
- 선택된 계획의 적절성은 실제 실행 결과로 검증해야 합니다.
이 이론의 범위
이 이론은 옵티마이저의 공통 작동 원리를 설명합니다. 인덱스 스캔별 세부 원리, 각 조인 방식의 내부 동작, 쿼리 변환별 상세 조건, 통계 수집 방법은 각각의 후속 이론에서 다룹니다.
학습 목표
이 이론을 학습한 뒤에는 다음 내용을 설명할 수 있어야 합니다.
- 옵티마이저가 필요한 이유를 설명한다.
- Query Transformer, Estimator, Plan Generator의 역할을 구분한다.
- Selectivity, Cardinality, Cost의 의미와 관계를 설명한다.
- Access Path, Join Order, Join Method의 의미를 설명한다.
- 옵티마이저와 실행계획, Row Source의 역할을 구분한다.
- 인덱스가 있어도 Full Table Scan이 선택될 수 있는 이유를 설명한다.
- 같은 SQL의 실행계획이 달라지는 조건을 설명한다.
- 예상과 실제 실행 결과를 비교해야 하는 이유를 설명한다.
1. 옵티마이저가 필요한 이유
다음 SQL은 급여가 3,000 이상인 사원을 조회합니다.
SELECT empno, ename, sal
FROM emp
WHERE sal >= 3000;
사용자가 요구한 결과는 하나지만, Oracle이 결과를 찾는 방법은 여러 가지일 수 있습니다.
방법 1: EMP 테이블 전체를 읽고 SAL 조건을 확인
방법 2: SAL 인덱스에서 조건에 맞는 ROWID를 찾은 뒤 EMP 테이블 접근
방법 3: 필요한 컬럼과 조건을 포함한 다른 인덱스 경로 검토
방법 4: 실행 환경과 설정이 허용하면 병렬 테이블 스캔 검토
어떤 방법이 유리한지는 인덱스의 존재만으로 결정되지 않습니다. 다음 요소를 함께 고려해야 합니다.
- EMP 테이블의 전체 행 수와 블록 수
SAL >= 3000을 만족할 것으로 예상되는 행의 비율- 인덱스의 컬럼 구성과 통계
- 인덱스에서 찾은 ROWID로 테이블을 방문해야 하는 횟수
- 인덱스 순서와 테이블 블록 배치의 관계
- 필요한 결과의 범위와 최적화 목표
- CPU·I/O 비용, 병렬 설정 등 최적화 환경
이처럼 여러 후보의 예상 작업량을 비교하여 실행 방법을 정하는 구성요소가 옵티마이저입니다.
2. 옵티마이저의 세 구성요소
Oracle 옵티마이저의 기본 구성은 다음 세 단계로 이해할 수 있습니다.
| 구성요소 | 역할 | 핵심 질문 |
|---|---|---|
| Query Transformer | 결과 의미를 유지하면서 더 유리한 계획을 만들 수 있도록 SQL의 내부 형태를 변환할지 검토 | 같은 결과를 만드는 다른 표현이 있는가? |
| Estimator | 통계정보를 이용해 Selectivity, Cardinality, Cost를 추정 | 각 단계에서 몇 건을 처리하며 작업량은 얼마나 될까? |
| Plan Generator | Access Path, Join Order, Join Method 조합을 검토하고 Cost를 비교 | 검토한 후보 중 어느 계획의 Cost가 가장 낮은가? |
2.1 Query Transformer
Query Transformer는 원래 SQL과 결과가 같은 범위에서 다른 내부 표현을 검토합니다.
대표적인 변환 예시는 다음과 같습니다.
- Subquery Unnesting
- View Merging
- OR Expansion
- Predicate Pushing
이 단계에서는 각 변환의 세부 조건보다, 옵티마이저가 사용자가 작성한 SQL 문장 모양만 그대로 실행하는 것은 아니라는 점을 이해하면 됩니다.
2.2 Estimator
Estimator는 통계정보를 바탕으로 조건의 선택도, 단계별 예상 행 수, 예상 작업량을 계산합니다. 이 추정이 부정확하면 Access Path와 조인 계획도 부적절하게 선택될 수 있습니다.
2.3 Plan Generator
Plan Generator는 여러 Access Path, Join Order, Join Method를 조합해 후보 계획을 검토합니다. 가능한 모든 조합을 실제로 실행해 보는 것이 아니라, 제한된 최적화 시간 안에서 후보를 탐색하고 현재까지의 최저 Cost 등을 기준으로 불필요한 탐색을 줄입니다.
3. Selectivity, Cardinality, Cost
3.1 Selectivity
Selectivity(선택도) 는 전체 행 집합에서 조건을 만족할 것으로 예상되는 비율입니다.
전체 100,000행 중 예상 100행 선택
Selectivity = 100 / 100,000 = 0.001 = 0.1%
선택도가 낮다는 것은 전체 중 적은 행만 선택된다는 의미입니다. 일반적으로 적은 행을 찾는 조건은 인덱스 접근이 유리할 가능성이 커지지만, 최종 판단은 테이블 접근을 포함한 전체 비용으로 해야 합니다.
3.2 Cardinality
Cardinality(카디널리티) 는 실행계획의 각 Operation이 반환할 것으로 예상되는 행 수입니다.
테이블 전체 행 수 × 조건 Selectivity
→ 조건 적용 후 예상 Cardinality
Cardinality는 Access Path뿐 아니라 Join Order, Join Method, Sort 작업량의 Cost 계산에도 영향을 줍니다. 앞 단계의 Cardinality를 크게 잘못 추정하면 뒤 단계의 예상 작업량도 연쇄적으로 달라질 수 있습니다.
3.3 Cost
Cost 는 옵티마이저가 후보 계획의 예상 자원 사용량을 같은 내부 기준으로 환산한 비교값입니다. I/O, CPU, 메모리 사용 예상과 Cardinality, 데이터 크기, 분포, 접근 구조 등이 Cost 계산에 영향을 줍니다.
후보 계획 A Cost = 120
후보 계획 B Cost = 350
같은 SQL과 같은 Optimizer 환경에서라면, A가 B보다 적은 작업량을 사용할 것으로 추정되었다는 의미입니다.
Cost를 해석할 때는 다음 기준을 지켜야 합니다.
- Cost 120은 120초를 뜻하지 않습니다.
- Cost의 비율과 실제 수행시간의 비율은 같지 않을 수 있습니다.
- 서로 다른 SQL의 Cost 숫자만 직접 비교해 빠른 SQL을 결정할 수 없습니다.
- Optimizer Mode가 다르면 의미 있는 단순 비교가 어렵습니다.
- 낮은 Cost의 계획도 Cardinality 추정이 틀리면 실제로 느릴 수 있습니다.
4. Plan Generator가 결정하는 핵심 항목
4.1 Access Path
Access Path는 Row Source에서 필요한 행을 가져오는 방법입니다.
대표적인 예시는 다음과 같습니다.
- Table Full Scan
- Table Access by ROWID
- Index Unique Scan
- Index Range Scan
- Index Full Scan
- Index Fast Full Scan
각 인덱스 스캔의 세부 차이는 후속 인덱스 이론에서 다룹니다.
4.2 Join Order
세 개 이상의 테이블을 조인할 때는 어느 Row Source부터 시작하고 다음에 무엇을 결합할지 결정해야 합니다.
후보 1: A → B → C
후보 2: B → A → C
후보 3: C → B → A
선행 단계의 Cardinality가 커지면 후속 조인의 반복 또는 입력 데이터량도 커질 수 있으므로 조인 순서는 전체 Cost에 큰 영향을 줄 수 있습니다.
4.3 Join Method
두 Row Source를 결합하는 대표적인 방법은 다음과 같습니다.
- Nested Loops Join
- Hash Join
- Sort Merge Join
각 방식은 입력 데이터량, 인덱스, 조인 조건, 메모리, 초기 응답과 전체 처리량 목표 등에 따라 유리한 상황이 다릅니다. 세부 내부 동작은 후속 조인 이론에서 다룹니다.
5. 옵티마이저가 참고하는 정보
| 입력 정보 | 판단에 미치는 영향 |
|---|---|
| SQL과 조건식 | 사용할 수 있는 Access Path와 Query Transformation 후보 결정 |
| 테이블 통계 | 전체 행 수, 블록 수, 평균 행 길이 등을 이용한 규모 추정 |
| 컬럼 통계 | 고유값 수, NULL 수, 히스토그램 등을 이용한 Selectivity와 Cardinality 추정 |
| 인덱스 통계 | 인덱스 높이, Leaf Block 수, Clustering Factor 등을 이용한 접근 비용 추정 |
| 제약조건 | PK·FK·NOT NULL 등 데이터 관계와 변환 가능성 판단 |
| Literal과 Bind 처리 정보 | 조건값에 따른 선택도 추정과 Cursor 계획 선택에 영향 |
| Optimizer Mode | 전체 처리량과 초기 응답 중 어떤 목표를 우선할지 결정 |
| 시스템 통계와 세션 설정 | CPU·I/O 비용, 병렬 사용, 기능 사용 범위 등에 영향 |
| Oracle 버전 | 사용할 수 있는 변환, 적응형 기능, 비용 모델 차이에 영향 |
Clustering Factor 는 인덱스 키 순서와 테이블 블록의 행 배치가 얼마나 비슷한지를 나타내는 인덱스 통계입니다. 값이 좋지 않으면 Index Range Scan 뒤의 Table Access by ROWID가 더 많은 테이블 블록 방문을 유발할 것으로 추정될 수 있습니다.
옵티마이저 통계는 실행 중 수집되는 성능 통계와 목적이 다릅니다. 옵티마이저 통계는 계획을 만들기 위한 추정 입력이며, 실제 성능은 실행 후 측정값으로 확인합니다.
6. 인덱스가 있어도 Full Table Scan을 선택할 수 있다
ORDERS.STATUS에 인덱스가 있다고 가정합니다.
SELECT order_id, customer_id, order_date
FROM orders
WHERE status = 'COMPLETED';
전체 주문의 90%가 COMPLETED라면 인덱스를 사용해도 매우 많은 ROWID를 얻고 테이블 블록을 반복 방문할 수 있습니다.
INDEX RANGE SCAN
→ 대량의 ROWID 획득
→ TABLE ACCESS BY INDEX ROWID 반복
→ 대부분의 행 반환
개념적으로는 다음과 같은 계획이 더 낮은 Cost로 평가될 수 있습니다.
SELECT STATEMENT
TABLE ACCESS FULL ORDERS
Full Table Scan은 테이블의 넓은 범위를 멀티블록 I/O로 읽을 수 있으므로, 대부분의 행을 반환해야 하는 상황에서 인덱스와 테이블을 반복 방문하는 방식보다 유리할 수 있습니다.
반대로 전체 주문의 0.1%만 CANCELLED라면 다음 SQL에서는 인덱스 접근이 유리할 가능성이 커집니다.
SELECT order_id, customer_id, order_date
FROM orders
WHERE status = 'CANCELLED';
SELECT STATEMENT
TABLE ACCESS BY INDEX ROWID ORDERS
INDEX RANGE SCAN IDX_ORDERS_STATUS
핵심은 인덱스가 존재하는가가 아니라 다음 전체 비용입니다.
인덱스 탐색 비용
+ ROWID 획득 비용
+ 테이블 블록 방문 비용
+ 추가 필터·조인·정렬 비용
7. 같은 SQL의 실행계획이 달라지는 조건
같은 SQL Text라도 다음과 같은 최적화 조건이 달라지고 새로운 Hard Parse 또는 재최적화가 발생하면 다른 실행계획이 선택될 수 있습니다.
| 변화 원인 | 실행계획에 반영되는 방식 |
|---|---|
| 통계정보 수집·변경 | 관련 Cursor가 무효화되거나 다음 최적화 시 새로운 Selectivity·Cardinality 사용 |
| 인덱스 생성·삭제 또는 객체 DDL | 기존 Shared SQL Area가 무효화되고 새로운 Access Path 검토 가능 |
| Optimizer 관련 파라미터 변경 | 다른 Optimizer 환경의 Child Cursor와 계획이 생성될 수 있음 |
| Oracle 버전 또는 기능 설정 변경 | 사용할 수 있는 변환과 비용 모델이 달라질 수 있음 |
| 시스템 통계·병렬 설정 변경 | I/O·CPU 비용과 병렬 계획의 Cost 추정이 달라질 수 있음 |
| Bind 값의 분포 차이 | Bind Peeking이나 Adaptive Cursor Sharing에 따라 여러 Child Cursor와 계획이 사용될 수 있음 |
| Cursor가 Shared Pool에서 밀려남 | 다음 실행에서 Hard Parse가 발생하여 계획을 다시 선택할 수 있음 |
중요한 점은 다음과 같습니다.
테이블의 데이터가 변했다는 사실만으로 현재 재사용 중인 실행계획이 즉시 자동 교체되는 것은 아닙니다.
데이터 변화가 계획 선택에 반영되려면 일반적으로 통계정보가 갱신되고, Cursor 무효화·Aging Out·Optimizer 환경 변화 등으로 Hard Parse 또는 재최적화가 발생해야 합니다.
따라서 과거와 실행계획이 달라졌다면 단순히 데이터량만 보는 것이 아니라 다음 항목을 함께 확인해야 합니다.
- 통계 수집 시점과 통계 변경
- 객체 DDL 또는 인덱스 변경
- Child Cursor와 Optimizer 환경 차이
- Bind 값과 데이터 편중
- Oracle 버전과 관련 파라미터
- 기존 Cursor의 무효화 또는 Aging Out
8. 옵티마이저, 실행계획, Row Source의 관계
| 구분 | 역할 |
|---|---|
| 옵티마이저 | 후보 실행 방법을 만들고 비교하여 실행계획을 선택 |
| 실행계획 | 선택된 Access Path, Join Order, Join Method 등을 Operation Tree로 표현 |
| Row Source Generator | 실행계획을 실제 수행 가능한 Row Source Tree로 구성 |
| Row Source | 각 Operation을 실행하면서 행을 읽고 가공해 상위 단계에 전달 |
옵티마이저가 실행계획 선택
→ Row Source Generator가 실행 구조 준비
→ Row Source Tree 실행
→ 결과 행 반환
옵티마이저는 계획을 선택하는 구성요소이며, SQL 수행 중 모든 행을 직접 읽는 실행 주체와는 구분해야 합니다.
9. 선택된 계획을 검증하는 기본 순서
옵티마이저가 선택한 계획을 분석할 때는 다음 순서로 확인합니다.
1. SQL이 요구하는 결과와 실제 필요한 행 수를 확인한다.
2. 각 조건의 Selectivity와 단계별 Cardinality를 예상한다.
3. 사용할 수 있는 인덱스·테이블·조인 경로를 확인한다.
4. 선택된 Access Path·Join Order·Join Method를 확인한다.
5. 옵티마이저의 예상 행 수와 Cost를 확인한다.
6. 실제 행 수, I/O, CPU, 대기, 응답시간과 비교한다.
7. 예상과 실제 차이가 크면 통계·데이터 분포·SQL·인덱스를 점검한다.
실행계획의 Cost가 낮다는 이유만으로 계획을 확정적으로 평가해서는 안 됩니다. 특히 예상 Cardinality와 실제 처리 행 수가 크게 다르면 옵티마이저가 잘못된 입력 정보나 부정확한 분포 가정을 사용했을 가능성을 점검해야 합니다.
10. 자주 혼동하는 판단과 정확한 기준
| 혼동하기 쉬운 판단 | 정확한 기준 |
|---|---|
| 인덱스가 있으면 반드시 인덱스를 사용해야 한다 | 인덱스 탐색과 Table Access by ROWID를 포함한 전체 Cost를 비교한다 |
| 선택도가 낮으면 언제나 인덱스가 유리하다 | 테이블 방문 비용, Clustering Factor, 반환 컬럼, 추가 작업까지 함께 본다 |
| Cost가 낮으면 실제 수행시간도 반드시 짧다 | Cost는 추정값이므로 실제 행 수와 수행 통계로 검증한다 |
| 서로 다른 SQL은 Cost가 작은 SQL이 더 빠르다 | Cost는 같은 SQL과 같은 Optimizer 환경의 후보 계획 비교에 사용한다 |
| 데이터량이 바뀌면 실행 중인 계획도 즉시 바뀐다 | 새로운 통계와 Hard Parse 또는 재최적화가 있어야 변경이 반영될 수 있다 |
| 실행계획이 바뀌면 성능이 반드시 나빠졌다 | 변경 전후의 실제 처리량, I/O, 대기, 응답시간을 비교한다 |
| 옵티마이저가 가능한 모든 계획을 완전히 탐색한다 | 제한된 최적화 시간과 탐색 기준 안에서 후보를 검토한다 |
| 좋은 계획은 모든 Bind 값에서 항상 같다 | 데이터 편중이 크면 Bind 값별로 유리한 계획이 달라질 수 있다 |
11. 핵심 정리
Query Transformer
→ 결과가 같은 다른 SQL 형태를 검토한다.
Estimator
→ Selectivity·Cardinality·Cost를 추정한다.
Plan Generator
→ Access Path·Join Order·Join Method 후보를 비교한다.
Selectivity
→ 조건을 만족할 것으로 예상되는 행의 비율이다.
Cardinality
→ 각 실행 단계가 반환할 것으로 예상되는 행 수다.
Cost
→ 같은 SQL의 후보 계획을 비교하기 위한 예상 작업량이다.
Execution Plan
→ 옵티마이저가 선택한 처리 방법을 Operation Tree로 표현한 결과다.
Row Source
→ 실행계획의 Operation을 실제로 수행하며 행을 생산한다.
옵티마이저 학습의 핵심은 특정 실행계획을 무조건 좋은 계획으로 외우는 것이 아닙니다. 데이터량·분포·통계·접근 구조·처리 목표에 따라 왜 그 계획이 선택되었으며, 예상과 실제가 일치하는지를 설명하는 것이 중요합니다.
개념 확인 문제
문제를 누르면 바로 아래에서 정답과 해설을 확인할 수 있습니다.
01SQL에 옵티마이저가 필요한 이유를 설명하시오.
옵티마이저가 필요한 이유
- SQL은 원하는 결과를 선언하지만 구체적인 접근 경로와 조인 순서를 모두 지정하지 않습니다.
- 같은 결과를 만드는 Table Full Scan, 인덱스 접근, 여러 조인 순서와 조인 방식이 존재하므로 예상 작업량을 비교해 실행 방법을 선택할 구성요소가 필요합니다.
02Query Transformer, Estimator, Plan Generator의 역할을 각각 설명하시오.
옵티마이저의 세 구성요소
- Query Transformer는 결과 의미를 유지하면서 더 유리한 계획을 만들 수 있는 내부 SQL 형태를 검토합니다.
- Estimator는 통계정보를 이용해 Selectivity, Cardinality, Cost를 추정합니다.
- Plan Generator는 Access Path, Join Order, Join Method 후보의 Cost를 비교해 실행계획을 선택합니다.
03Selectivity와 Cardinality의 차이와 관계를 설명하시오.
Selectivity와 Cardinality
- Selectivity는 전체 행 집합에서 조건을 만족할 것으로 예상되는 비율입니다.
- Cardinality는 각 실행계획 Operation이 반환할 것으로 예상되는 행 수입니다.
- 일반적으로 전체 행 수와 Selectivity를 이용해 조건 적용 후 Cardinality를 추정하며, 이 값은 후속 조인과 정렬의 Cost 계산에 영향을 줍니다.
04Cost가 실제 수행시간과 같은 값이 아닌 이유를 설명하시오.
Cost와 실제 수행시간
- Cost는 I/O, CPU, 메모리 사용 예상과 Cardinality 등을 내부 기준으로 환산한 후보 계획 비교값입니다.
- Cache 상태, 실제 데이터 분포, 동시성, 대기, 병렬 처리 등 실제 실행 환경에 따라 수행시간은 달라질 수 있으므로 Cost는 초 또는 밀리초가 아닙니다.
05Access Path, Join Order, Join Method의 의미를 각각 설명하시오.
세 가지 핵심 결정 항목
- Access Path는 테이블이나 인덱스 등 Row Source에서 필요한 행을 가져오는 방법입니다.
- Join Order는 여러 Row Source를 결합하는 순서입니다.
- Join Method는 Nested Loops, Hash, Sort Merge 등 두 Row Source를 결합하는 방식입니다.
06STATUS = 'COMPLETED'가 전체 행의 90%를 차지할 때 Full Table Scan이 유리할 수 있는 이유를 설명하시오.
대부분의 행을 조회할 때 Full Table Scan이 유리할 수 있는 이유
- 인덱스를 사용하면 대량의 ROWID를 얻은 뒤 많은 테이블 블록을 반복 방문할 수 있습니다.
- 결과가 테이블 대부분이라면 테이블의 넓은 범위를 멀티블록 I/O로 읽는 Full Table Scan의 전체 Cost가 더 작을 수 있습니다.
07Cardinality 추정 오류가 Join Order와 Join Method 선택에 영향을 줄 수 있는 이유를 설명하시오.
Cardinality 추정 오류의 영향
- Join Order와 Join Method의 Cost는 선행 단계에서 몇 행이 전달되는지에 크게 의존합니다.
- 실제로 많은 행이 나오는데 적은 행으로 추정하면 반복 횟수나 해시 입력 크기, 정렬량 등을 잘못 계산해 부적절한 조인 계획을 선택할 수 있습니다.
08테이블 데이터가 변경되었다는 사실만으로 현재 재사용 중인 실행계획이 즉시 바뀌지 않는 이유를 설명하시오.
데이터 변화와 실행계획 재사용
- 현재 Shared Pool에 있는 유효한 Cursor는 기존 실행계획을 재사용할 수 있습니다.
- 데이터 변화가 계획에 반영되려면 통계정보 갱신과 Cursor 무효화, Aging Out, Optimizer 환경 변화 등으로 Hard Parse 또는 재최적화가 발생해야 합니다.
09같은 SQL Text에 여러 실행계획이 사용될 수 있는 원인을 네 가지 이상 작성하시오.
같은 SQL의 실행계획이 달라질 수 있는 원인
- 통계정보 수집 또는 변경
- 인덱스 생성·삭제나 객체 DDL
- Optimizer 관련 파라미터 차이
- Oracle 버전 또는 기능 설정 차이
- 시스템 통계나 병렬 설정 차이
- Bind Peeking 또는 Adaptive Cursor Sharing
- Cursor 무효화 또는 Shared Pool Aging Out
10옵티마이저, 실행계획, Row Source의 역할을 구분하고 선택된 계획을 검증하는 기본 순서를 설명하시오.
역할 구분과 검증 순서 - 옵티마이저는 후보를 비교해 계획을 선택하고, 실행계획은 선택된 방법을 Operation Tree로 표현하며, Row Source는 각 Operation을 실제로 수행합니다. - 검증할 때는 필요한 결과와 행 수 확인 → Selectivity·Cardinality 예상 → 접근 구조 확인 → 선택된 Access Path·Join Order·Join Method 확인 → 예상 행 수와 Cost 확인 → 실제 행 수·I/O·CPU·대기·응답시간 비교 순서로 진행합니다.