Oracle 기반으로 처음 시작하는 사람을 위한 단계별 가이드
난이도 · 입문 학습기간 · 12주 기준 DBMS · Oracle 19c 선수 학습 · SQL 기초, 데이터 모델링 입문
1. SQL 튜닝이란
SQL 튜닝은 같은 결과를 내는 SQL이 더 적은 자원(I/O, CPU, 메모리, 대기)으로 처리되도록 SQL·인덱스·통계·구조를 개선하는 작업이다.
튜닝의 본질 — "읽지 않아도 되는 블록을 읽지 않게 하는 것"
대부분의 느린 SQL은 CPU가 부족해서가 아니라 필요 이상으로 많은 블록을 읽거나, 같은 블록을 반복해서 읽기 때문에 느리다.
1.1 튜닝의 대상과 순서
| 우선순위 | 대상 | 효과 | 예 |
|---|---|---|---|
| 1 | 설계 (모델) | 매우 큼 | 잘못된 식별자, 과도한 반정규화, 이력 구조 |
| 2 | SQL·애플리케이션 | 큼 | 불필요한 반복 호출, 비효율 조인, 바인드 변수 미사용 |
| 3 | 인덱스·통계 | 큼 | 조건에 맞는 인덱스, 정확한 옵티마이저 통계 |
| 4 | 저장 구조 | 중간 | 파티셔닝, 클러스터링 팩터, 압축 |
| 5 | 인스턴스·OS | 제한적 | SGA/PGA 크기, 파라미터, 스토리지 |
1.2 응답 시간 공식
응답 시간 (Response Time) = 서비스 시간 (CPU Time) + 대기 시간 (Wait Time)
- 튜닝은 둘 중 어느 쪽이 큰가를 먼저 확인하는 데서 시작한다.
- 대기 시간은 대기 이벤트(Wait Event)로 구분된다 —
db file sequential read,db file scattered read,direct path read,enq: TX - row lock contention,log file sync등
2. Oracle 아키텍처 핵심
2.1 튜닝에 필요한 최소 구조
┌──────────────────────── SGA (공유 메모리) ────────────────────────┐
│ ┌───────────────┐ ┌────────────────────┐ ┌──────────────┐ │
│ │ Shared Pool │ │ Database Buffer │ │ Redo Log │ │
│ │ - Library │ │ Cache │ │ Buffer │ │
│ │ Cache (SQL) │ │ (데이터 블록 캐시) │ │ │ │
│ │ - Dictionary │ │ │ │ │ │
│ └───────────────┘ └────────────────────┘ └──────────────┘ │
└──────────────────────────────────────────────────────────────────┘
▲ 파싱/실행계획 공유 ▲ Logical Read │ LGWR
│ │ ▼
┌─────┴─────┐ PGA (정렬·해시) ┌─┴───────────┐ ┌───────────┐
│ Server │─────────────────►│ Data Files │ │ Redo Logs │
│ Process │ Physical Read └─────────────┘ └───────────┘
└───────────┘
| 구성 요소 | 튜닝 관점의 의미 |
|---|---|
| Library Cache | 파싱된 SQL과 실행계획 보관. 재사용되면 Soft Parse, 없으면 Hard Parse |
| Buffer Cache | 블록을 메모리에서 읽으면 Logical Read, 디스크에서 읽으면 Physical Read |
| PGA | 정렬(Sort), 해시 조인(Hash Area) 작업 공간. 부족하면 Temp 사용 (direct path write temp)
|
| Redo | 변경 기록. 커밋 빈도가 과도하면 log file sync 대기 증가
|
2.2 블록 I/O 단위
- Oracle은 행 단위가 아니라 블록 단위(보통 8KB)로 읽는다. 한 행만 필요해도 블록 전체를 읽는다.
- Single Block I/O — 인덱스 탐색, 테이블 랜덤 액세스 (
db file sequential read) - Multi Block I/O — Full Table Scan, Index Fast Full Scan (
db file scattered read,direct path read) - 튜닝의 핵심 지표는 Logical Read (= buffer gets, consistent gets + db block gets)이다. Physical Read는 캐시 상태에 따라 달라지지만 Logical Read는 SQL과 실행계획이 같으면 거의 일정하다.
3. SQL 처리 과정
파싱
문법·권한 검사
Library Cache 탐색
최적화
통계 기반 비용 계산
실행계획 선택
실행 엔진 생성
실행 가능한
코드로 변환
실행·인출
블록 읽기
결과 전송
3.1 Hard Parse vs Soft Parse
| 구분 | 동작 | 비용 |
|---|---|---|
| Hard Parse | Library Cache에 없음 → 최적화·실행계획 생성 | 높음 (CPU, Shared Pool 래치/뮤텍스 경합) |
| Soft Parse | Library Cache에 있음 → 기존 실행계획 재사용 | 낮음 |
3.2 바인드 변수
-- 나쁜 예: 리터럴 → 값마다 다른 SQL로 인식되어 매번 Hard Parse SELECT * FROM TB_ORDER WHERE ORDER_NO = 'O20261104001'; SELECT * FROM TB_ORDER WHERE ORDER_NO = 'O20261104002'; -- 좋은 예: 바인드 변수 → 하나의 SQL로 공유 SELECT * FROM TB_ORDER WHERE ORDER_NO = :order_no;
# Python (oracledb / cx_Oracle) — 바인드 변수 사용
cur.execute("SELECT * FROM TB_ORDER WHERE ORDER_NO = :order_no", order_no=v_order_no)
# 대량 처리 — executemany로 Array 처리 (네트워크 왕복·파싱 최소화)
cur.executemany("INSERT INTO TB_LOG (ID, MSG) VALUES (:1, :2)", rows)
💡 바인드 변수의 그림자 — 값 분포가 심하게 치우친 컬럼(예: 상태코드 '완료' 99%, '오류' 1%)은 하나의 실행계획이 모든 값에 최적이 아니다. Oracle은 Bind Peeking과 Adaptive Cursor Sharing(11g+)으로 이를 보완한다. 이런 컬럼에는 히스토그램 통계가 중요하다.
4. 옵티마이저와 통계
4.1 비용 기반 옵티마이저 (CBO)
CBO는 통계 정보를 이용해 가능한 실행계획들의 예상 비용(Cost)을 계산하고 가장 낮은 것을 선택한다.
| 개념 | 의미 | 실행계획 컬럼 |
|---|---|---|
| 선택도 (Selectivity) | 조건을 만족하는 행의 비율 (0~1) | — |
| 카디널리티 (Cardinality) | 예상 결과 행 수 = 총 행 수 × 선택도 | Rows (E-Rows)
|
| 비용 (Cost) | 예상 I/O + CPU를 단일 블록 I/O 단위로 환산한 값 | Cost
|
⚠ 튜닝의 출발점 — 실행계획이 나쁜 원인의 대부분은 카디널리티 추정 오류다. 예상 행 수(E-Rows)와 실제 행 수(A-Rows)가 수십 배 이상 차이 나는 단계를 찾으면 원인이 보인다.
4.2 옵티마이저 통계
| 대상 | 주요 항목 | 딕셔너리 뷰 |
|---|---|---|
| 테이블 | 행 수(NUM_ROWS), 블록 수, 평균 행 길이 | DBA_TAB_STATISTICS
|
| 컬럼 | Distinct 값 수(NDV), NULL 수, 최소/최대값, 히스토그램 | DBA_TAB_COL_STATISTICS
|
| 인덱스 | 높이(BLEVEL), 리프 블록 수, 클러스터링 팩터 | DBA_IND_STATISTICS
|
| 시스템 | CPU 속도, I/O 성능 | SYS.AUX_STATS$
|
-- 테이블 통계 수집 (19c 기본 권장값 사용) EXEC DBMS_STATS.GATHER_TABLE_STATS(ownname => 'APP', tabname => 'TB_ORDER', cascade => TRUE); -- 통계 확인 SELECT table_name, num_rows, blocks, last_analyzed, stale_stats FROM dba_tab_statistics WHERE owner = 'APP' AND table_name = 'TB_ORDER'; -- 컬럼 통계와 히스토그램 SELECT column_name, num_distinct, num_nulls, histogram FROM dba_tab_col_statistics WHERE owner = 'APP' AND table_name = 'TB_ORDER';
5. 실행계획 읽기
5.1 실행계획 확인 방법
| 방법 | 특징 | 실제 실행 여부 |
|---|---|---|
EXPLAIN PLAN + DBMS_XPLAN.DISPLAY |
예상 계획. 바인드 타입·값 미반영으로 실제와 다를 수 있음 | ✕ |
SET AUTOTRACE TRACEONLY (SQL*Plus) |
예상 계획 + 실행 통계(consistent gets 등) | ○ |
DBMS_XPLAN.DISPLAY_CURSOR |
실제 사용된 계획 + 단계별 실측(A-Rows, Buffers) | ○ (권장) |
| SQL Trace + TKPROF | 이벤트 10046, 대기 이벤트까지 상세 추적 | ○ |
| Real-Time SQL Monitor | 장시간·병렬 SQL의 진행 상황 실시간 확인 (Tuning Pack 라이선스) | ○ |
-- 실측 실행계획 확인 (가장 많이 쓰는 방법)
SELECT /*+ GATHER_PLAN_STATISTICS */ o.order_no, c.cust_nm
FROM tb_order o
JOIN tb_customer c ON c.cust_no = o.cust_no
WHERE o.order_dt >= DATE '2026-11-01';
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(NULL, NULL, 'ALLSTATS LAST'));
-- 특정 SQL_ID 조회
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('&sql_id', NULL, 'ALLSTATS LAST +PEEKED_BINDS'));
5.2 실행계획 예시와 읽는 순서
---------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | Buffers |
---------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 120 | 375 |
| 1 | NESTED LOOPS | | 1 | 118 | 120 | 375 |
| 2 | NESTED LOOPS | | 1 | 118 | 120 | 255 |
| 3 | TABLE ACCESS BY INDEX ROWID| TB_ORDER | 1 | 118 | 120 | 15 |
|* 4 | INDEX RANGE SCAN | IX_ORDER_02 | 1 | 118 | 120 | 3 |
|* 5 | INDEX UNIQUE SCAN | PK_CUSTOMER | 120 | 1 | 120 | 240 |
| 6 | TABLE ACCESS BY INDEX ROWID | TB_CUSTOMER | 120 | 1 | 120 | 120 |
---------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
4 - access("O"."ORDER_DT">=TO_DATE(' 2026-11-01 00:00:00', 'syyyy-mm-dd hh24:mi:ss'))
5 - access("C"."CUST_NO"="O"."CUST_NO")
| 읽는 원칙 | 설명 |
|---|---|
| 안쪽(들여쓰기 깊은 곳)부터 | 가장 깊이 들여쓴 단계가 먼저 실행된다 (4 → 3) |
| 같은 깊이면 위에서 아래로 | 형제 단계 중 위쪽이 먼저 (NL 조인의 Outer → Inner) |
| 부모는 자식 결과를 받는다 | 3·5 결과를 2(NL)가, 2·6 결과를 1(NL)이 결합 |
| 컬럼 | 의미 | 체크 포인트 |
|---|---|---|
| Starts | 해당 단계가 실행된 횟수 | NL 조인 Inner 쪽은 Outer 행 수만큼 반복 |
| E-Rows | 옵티마이저 예상 행 수 (1회 실행당) | E-Rows × Starts 와 A-Rows 비교 |
| A-Rows | 실제 행 수 (전체 누적) | 예상과 큰 차이 → 통계·조건 확인 |
| Buffers | 읽은 블록 수 (누적, Logical Read) | 가장 크게 증가하는 단계가 튜닝 대상 |
| Predicate | access: 인덱스 탐색 범위 결정 / filter: 읽은 후 걸러냄 |
filter로 많이 버려지면 비효율 |
6. 인덱스 튜닝
6.1 B*Tree 인덱스 구조
[ Root ]
/ \
[ Branch ] [ Branch ]
/ | \ ...
[Leaf] [Leaf] [Leaf] ← 키 값 + ROWID, 정렬되어 양방향 연결
│
▼ ROWID로 테이블 블록 접근 (Random Single Block I/O)
[ Table Block ]
- 인덱스 탐색 비용 = 수직 탐색(Root→Leaf) + 수평 스캔(Leaf 범위) + 테이블 랜덤 액세스
- 일반적으로 테이블 랜덤 액세스가 가장 비싸다 → 인덱스에서 최대한 걸러내야 한다
6.2 인덱스 스캔 방식
| 방식 | 동작 | 발생 조건 |
|---|---|---|
| Index Unique Scan | 정확히 1건 탐색 | UNIQUE/PK 전체 컬럼 = 조건 |
| Index Range Scan | 범위 탐색 (가장 일반적) | 선두 컬럼 조건 존재 |
| Index Full Scan | 리프 전체를 순서대로 Single Block 읽기 | 정렬 대체, 선두 컬럼 조건 없음 |
| Index Fast Full Scan | 인덱스 전체를 Multi Block으로 읽기 (순서 무관) | 필요한 컬럼이 모두 인덱스에 있음 |
| Index Skip Scan | 선두 컬럼을 건너뛰며 탐색 | 선두 컬럼 NDV가 매우 적을 때 |
| Table Access By Index ROWID | 인덱스 ROWID로 테이블 접근 | 인덱스에 없는 컬럼 필요 |
6.3 인덱스를 사용하지 못하는 경우
| 원인 | 나쁜 예 | 개선 |
|---|---|---|
| 인덱스 컬럼 가공 | WHERE TO_CHAR(ORDER_DT,'YYYYMMDD') = '20261104' |
WHERE ORDER_DT >= DATE '2026-11-04' AND ORDER_DT < DATE '2026-11-05'
|
| 묵시적 형변환 | WHERE CUST_NO = 1001 (CUST_NO는 VARCHAR2) |
WHERE CUST_NO = '1001'
|
| 부정형 조건 | WHERE STAT_CD <> '90' |
긍정형 IN 목록으로 변환 검토 |
| 앞쪽 와일드카드 | WHERE CUST_NM LIKE '%철수' |
업무 요건 재검토, Oracle Text 검토 |
| NULL 검색 | WHERE RETURN_DT IS NULL (단일 컬럼 인덱스) |
결합 인덱스에 상수 추가 (RETURN_DT, 0)
|
| 선두 컬럼 누락 | 인덱스 (CUST_NO, ORDER_DT)에 WHERE ORDER_DT = ...만 |
인덱스 컬럼 순서 재설계 |
| OR 조건 | WHERE A = :1 OR B = :2 |
UNION ALL 분리 또는 OR-Expansion 확인 |
6.4 클러스터링 팩터 (Clustering Factor)
- 인덱스 순서와 테이블 데이터 저장 순서가 얼마나 일치하는가를 나타내는 지표
- 테이블 블록 수에 가까울수록 좋고, 행 수에 가까울수록 나쁘다
- 같은 건수를 읽어도 클러스터링 팩터가 나쁘면 테이블 랜덤 I/O가 폭증한다
SELECT i.index_name, i.clustering_factor, t.blocks, t.num_rows
FROM dba_indexes i JOIN dba_tables t
ON t.owner = i.table_owner AND t.table_name = i.table_name
WHERE i.table_owner = 'APP' AND i.table_name = 'TB_ORDER';
7. 조인 튜닝
7.1 세 가지 조인 방식
| 구분 | Nested Loops | Hash Join | Sort Merge Join |
|---|---|---|---|
| 동작 | Outer 행마다 Inner를 인덱스로 탐색 | 작은 집합으로 해시 테이블(Build) → 큰 집합(Probe) 비교 | 양쪽을 조인 키로 정렬 후 병합 |
| 적합한 경우 | 소량 결과, OLTP, 부분 범위 처리 | 대량 데이터, 배치·DW, 조인 키 인덱스 없음 | 이미 정렬된 경우, 비동등(<, >) 조인
|
| 핵심 조건 | Inner 쪽 조인 컬럼에 인덱스 | Build 집합이 작아서 PGA에 들어갈 것 | 정렬 비용 감당 가능 |
| 조인 조건 | 모든 조건 | = 조인만 | 비동등 조인 가능 |
| 힌트 | USE_NL |
USE_HASH |
USE_MERGE
|
7.2 조인 순서가 성능을 결정한다
-- 조건: 특정 고객(1명)의 최근 주문
-- 좋은 순서: 고객(1건) → 주문(인덱스 CUST_NO+ORDER_DT) → NL
SELECT /*+ LEADING(c o) USE_NL(o) INDEX(o IX_ORDER_01) */
c.cust_nm, o.order_no, o.order_dt
FROM tb_customer c
JOIN tb_order o ON o.cust_no = c.cust_no
WHERE c.cust_no = :cust_no
AND o.order_dt >= ADD_MONTHS(SYSDATE, -1);
- NL 조인에서 Outer(Driving) 집합은 결과 건수가 가장 적은 쪽을 선택한다.
- Hash 조인에서 Build 집합은 작은 쪽을 선택한다 (
SWAP_JOIN_INPUTS로 조정).
7.3 서브쿼리
- 옵티마이저는 서브쿼리를 Unnesting(조인으로 변환)하거나 Filter(메인 행마다 실행, 결과 캐싱)로 처리한다.
- 스칼라 서브쿼리는 메인 쿼리 행 수만큼 수행될 수 있다 → 입력값 종류가 많으면 조인으로 변환 검토
EXISTS/IN은 Semi Join,NOT EXISTS/NOT IN은 Anti Join으로 변환된다 (NOT IN은 NULL 처리에 주의)
8. 힌트 (Hint) 기초
⚠ 힌트는 옵티마이저의 판단을 강제한다. 데이터가 변하면 오히려 독이 될 수 있다. 먼저 통계·인덱스·SQL 구조로 해결하고, 힌트는 원인을 이해한 뒤 최소한으로 사용한다. 운영 환경에서 계획 고정이 필요하면 SQL Plan Baseline(SPM)을 검토한다.
| 분류 | 힌트 | 의미 |
|---|---|---|
| 최적화 목표 | ALL_ROWS |
전체 결과 처리량 최적화 (기본) |
FIRST_ROWS(n) |
첫 n건 응답 속도 최적화 (화면 페이징) | |
| 액세스 | FULL(t) |
Full Table Scan |
INDEX(t idx) |
지정 인덱스 사용 | |
INDEX_FFS(t idx) |
Index Fast Full Scan | |
| 조인 순서 | LEADING(t1 t2 …) |
조인 순서 지정 |
ORDERED |
FROM 절 순서대로 조인 | |
| 조인 방식 | USE_NL(t) |
NL 조인 |
USE_HASH(t) |
Hash 조인 | |
SWAP_JOIN_INPUTS(t) |
Hash 조인 Build 집합 지정 | |
| 쿼리 변환 | UNNEST / NO_UNNEST |
서브쿼리 Unnesting 제어 |
MERGE / NO_MERGE |
뷰 병합 제어 | |
| 진단 | GATHER_PLAN_STATISTICS |
실측 통계 수집 (DISPLAY_CURSOR용) |
- 테이블에 별칭(Alias)을 쓰면 힌트에도 반드시 별칭을 써야 한다. 잘못된 힌트는 오류 없이 무시된다.
9. 진단 도구
9.1 문제 SQL 찾기
-- 메모리(Shared Pool)에 있는 SQL 중 Logical Read 상위
SELECT *
FROM (SELECT sql_id, executions,
buffer_gets,
ROUND(buffer_gets / NULLIF(executions,0)) AS gets_per_exec,
ROUND(elapsed_time / NULLIF(executions,0) / 1000, 1) AS ms_per_exec,
SUBSTR(sql_text, 1, 80) AS sql_text
FROM v$sql
WHERE parsing_schema_name NOT IN ('SYS','SYSTEM')
ORDER BY buffer_gets DESC)
WHERE ROWNUM <= 20;
9.2 도구별 용도
| 도구 | 범위 | 언제 쓰나 | 라이선스 |
|---|---|---|---|
V$SQL, V$SESSION |
현재 | 지금 느린 SQL·세션 확인 | 기본 |
| DBMS_XPLAN | SQL | 실행계획·실측 통계 | 기본 |
| SQL Trace + TKPROF | 세션·SQL | Parse/Execute/Fetch 단계별 시간, 대기 이벤트 상세 | 기본 |
| AWR 리포트 | 인스턴스 (스냅샷 구간) | 특정 시간대 시스템 부하·Top SQL·대기 이벤트 | Diagnostics Pack |
ASH (V$ACTIVE_SESSION_HISTORY) |
세션 (1초 샘플링) | 짧은 순간의 장애·경합 원인 | Diagnostics Pack |
| SQL Monitor | SQL | 5초 이상·병렬 SQL 진행 상황 | Tuning Pack |
| SQL Tuning Advisor | SQL | 인덱스·프로파일 권고 | Tuning Pack |
| Statspack | 인스턴스 | AWR 라이선스가 없을 때 대안 | 기본 (무료) |
-- SQL Trace (10046 이벤트, 대기·바인드 포함) ALTER SESSION SET tracefile_identifier = 'TUNE01'; EXEC DBMS_MONITOR.SESSION_TRACE_ENABLE(waits => TRUE, binds => TRUE); -- ... 대상 SQL 실행 ... EXEC DBMS_MONITOR.SESSION_TRACE_DISABLE; -- OS에서 -- $ tkprof ORCL_ora_12345_TUNE01.trc tune01.txt sys=no sort=exeela,fchela
10. 튜닝 절차
AWR·V$SQL로
대상 SQL 선정
실측 실행계획
Buffers·시간 기록
E-Rows vs A-Rows
병목 단계 찾기
SQL·인덱스·통계
하나씩 변경
결과 동일성
전후 수치 비교
배포 후
회귀 모니터링
💡 전후 비교는 숫자로 — "빨라졌다"가 아니라 Buffers 125,400 → 380, Elapsed 4.2s → 0.03s, Executions/일 30만 건 처럼 Logical Read와 실행 횟수로 기록한다. 응답 시간은 캐시 상태에 따라 흔들리지만 Logical Read는 재현된다.
11. 튜닝 사례 맛보기
사례 1. 컬럼 가공으로 인한 Full Scan
-- Before : TABLE ACCESS FULL TB_ORDER, Buffers 182,330 SELECT order_no, cust_no FROM tb_order WHERE TO_CHAR(order_dt, 'YYYYMMDD') = :ymd; -- After : INDEX RANGE SCAN IX_ORDER_02 (+ Partition Pruning), Buffers 42 SELECT order_no, cust_no FROM tb_order WHERE order_dt >= TO_DATE(:ymd, 'YYYYMMDD') AND order_dt < TO_DATE(:ymd, 'YYYYMMDD') + 1;
사례 2. 인덱스 컬럼 순서 — filter로 버려지는 행
- 조건:
WHERE ORDER_STAT_CD = '10' AND ORDER_DT BETWEEN :d1 AND :d2 - 기존 인덱스
(ORDER_DT, ORDER_STAT_CD)→ 범위 조건이 선두라 기간 내 모든 상태를 스캔 후 filter - 개선 인덱스
(ORDER_STAT_CD, ORDER_DT)→ = 조건 선두, 범위 조건 후행으로 스캔 범위 최소화 - 결과: 인덱스 Buffers 12,840 → 95
사례 3. 애플리케이션 반복 호출 (Row-by-Row)
# Before: 고객 1만 명 → SQL 1만 번 호출 (네트워크 왕복 + Parse/Execute 반복)
for cust_no in cust_list:
cur.execute("SELECT SUM(order_amt) FROM tb_order WHERE cust_no = :c", c=cust_no)
# After: 집합 처리 1번
cur.execute("""
SELECT cust_no, SUM(order_amt)
FROM tb_order
WHERE order_dt >= :d
GROUP BY cust_no""", d=v_from_dt)
- 튜닝 효과가 가장 큰 영역은 종종 SQL 내부가 아니라 호출 방식이다.
12. 학습 커리큘럼 (12주)
학습 대상 SQL을 작성할 줄 아는 개발자·신입 DBA | 주당 학습 이론 3시간 + 실습 3시간 | 목표 실행계획을 읽고 병목을 찾아 인덱스·SQL 수준에서 개선할 수 있다
| 주차 | 단계 | 학습 주제 | 세부 내용 | 실습 / 산출물 |
|---|---|---|---|---|
| 1 | 기초 처리 원리 |
튜닝 개요와 아키텍처 | 응답시간 = CPU + Wait, SGA/PGA, 백그라운드 프로세스, 블록·익스텐트·세그먼트 | 실습 환경 구성, 샘플 스키마 대량 데이터 생성 |
| 2 | SQL 처리 과정 | Parse·Execute·Fetch, Hard/Soft Parse, Library Cache, 바인드 변수, Array 처리 | 리터럴 vs 바인드 Parse 횟수 비교 | |
| 3 | I/O 메커니즘 | Logical/Physical Read, Single/Multi Block I/O, Buffer Cache, 주요 대기 이벤트 | AUTOTRACE로 consistent gets 측정 | |
| 4 | 분석 옵티마이저 |
옵티마이저와 통계 | CBO, 선택도·카디널리티·비용, DBMS_STATS, 히스토그램, 동적 샘플링 | 통계 유무에 따른 계획 변화 관찰 |
| 5 | 실행계획 읽기 | DBMS_XPLAN 포맷, 읽는 순서, Predicate(access/filter), E-Rows vs A-Rows | 실행계획 10개 해석 연습 | |
| 6 | 핵심 인덱스·조인 |
인덱스 튜닝 ① | B*Tree 구조, 스캔 방식, 인덱스 사용 불가 패턴, 클러스터링 팩터 | 컬럼 가공·형변환 SQL 개선 |
| 7 | 인덱스 튜닝 ② | 결합 인덱스 컬럼 순서, 커버링 인덱스, 테이블 랜덤 액세스 최소화, 인덱스 설계 절차 | 핵심 SQL 기반 인덱스 설계서 | |
| 8 | 조인 튜닝 | NL·Hash·Sort Merge 원리, 조인 순서, Semi/Anti 조인, 힌트 기초 | 조인 방식별 Buffers 비교 | |
| 9 | 응용 SQL 구조 |
쿼리 변환과 서브쿼리 | Unnesting, View Merging, 조건 Pushdown, 스칼라 서브쿼리 캐싱 | 서브쿼리 → 조인 재작성 |
| 10 | 정렬·페이징·대량 처리 | Sort 연산 제거, Top-N·페이징 쿼리, 부분 범위 처리, 파티션 프루닝, 병렬 처리 기초 | 화면 페이징 SQL 튜닝 | |
| 11 | 실전 진단·사례 |
진단 도구 | SQL Trace/TKPROF, AWR 리포트 읽기, ASH, SQL Monitor, SPM 개요 | AWR 리포트 Top SQL 분석 보고서 |
| 12 | 종합 튜닝 실습 | 문제 SQL 선정 → 측정 → 분석 → 개선 → 검증 → 문서화 | 튜닝 보고서 (전후 Buffers·시간 비교) 발표 |
12.1 단계별 학습 목표
🟥 1~3주 · 기초
- SQL이 처리되는 과정을 설명한다
- 바인드 변수의 필요성을 이해한다
- Logical Read로 SQL 비용을 측정한다
🟧 4~5주 · 분석
- 통계가 실행계획에 미치는 영향을 안다
- 실행계획을 순서대로 읽는다
- 카디널리티 오류 단계를 찾는다
🟦 6~8주 · 핵심
- 인덱스를 못 타는 SQL을 고친다
- 조건에 맞는 결합 인덱스를 설계한다
- 상황에 맞는 조인 방식을 선택한다
🟪 9~12주 · 응용·실전
- 쿼리 변환을 이해하고 SQL을 재작성한다
- AWR·Trace로 문제 SQL을 찾는다
- 튜닝 결과를 수치로 보고한다
12.2 실습 환경
| 구분 | 권장 | 비고 |
|---|---|---|
| DB | Oracle Database 23ai Free, 사내 개발 DB (19c) | 운영 DB에서 실습 금지 |
| 클라이언트 | SQL*Plus, SQL Developer, Toad, Orange | DBMS_XPLAN 출력은 SQL*Plus가 가장 깔끔 |
| 샘플 데이터 | SH / HR 샘플 스키마, CONNECT BY LEVEL로 수백만 건 생성 |
데이터가 적으면 튜닝 효과가 보이지 않는다 |
| 권한 | SELECT_CATALOG_ROLE, ALTER SESSION, V$ 뷰 조회 권한 |
DISPLAY_CURSOR 사용에 필요 |
-- 실습용 대량 데이터 생성 예 (약 500만 건)
CREATE TABLE tb_order_lab NOLOGGING AS
SELECT 'O' || LPAD(LEVEL, 10, '0') AS order_no,
DATE '2024-01-01' + MOD(LEVEL, 1000) AS order_dt,
'C' || LPAD(MOD(LEVEL, 50000), 6, '0') AS cust_no,
DECODE(MOD(LEVEL, 100), 0, '90', 1, '30', '10') AS order_stat_cd,
ROUND(DBMS_RANDOM.VALUE(1000, 500000)) AS order_amt
FROM dual CONNECT BY LEVEL <= 5000000;
EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, 'TB_ORDER_LAB');
12.3 연계 자격증 · 추천 학습
- SQLP (SQL 전문가) — 3과목 'SQL 고급 활용 및 튜닝'이 본 과정과 직결
- Oracle Certified Professional — Database Administration / Performance Management and Tuning 계열
- Oracle 공식 문서 SQL Tuning Guide, Database Performance Tuning Guide (19c)
13. 튜닝 체크리스트
✅ SQL 리뷰 시 확인할 항목 (펼치기)
| 영역 | 점검 항목 |
|---|---|
| 작성 | □ 바인드 변수를 사용하는가 (리터럴 SQL 없음) |
| □ 인덱스 컬럼을 가공하거나 형변환하지 않는가 | |
| □ 반복 호출(Row-by-Row) 대신 집합 처리·Array 처리를 하는가 | |
| 실행계획 | □ 실측 계획(ALLSTATS LAST)으로 확인했는가 |
| □ E-Rows와 A-Rows가 크게 다른 단계는 없는가 | |
| □ Buffers가 급증하는 단계, filter로 대량 버려지는 단계는 없는가 | |
| 인덱스 | □ 조건에 맞는 결합 인덱스 순서인가 (= 조건 선두) |
| □ 테이블 랜덤 액세스 후 버려지는 행이 많지 않은가 | |
| 조인 | □ Driving 집합이 가장 작은 쪽인가 |
| □ NL Inner 쪽에 조인 인덱스가 있는가 / Hash Build 쪽이 작은가 | |
| 통계·운영 | □ 통계가 최신이며 Stale 상태가 아닌가 |
| □ 결과 동일성 검증과 전후 수치 기록을 했는가 |
14. 용어집
| 용어 | 영문 | 설명 |
|---|---|---|
| 옵티마이저 | Optimizer (CBO) | 통계 기반으로 최저 비용 실행계획을 선택하는 엔진 |
| 실행계획 | Execution Plan | SQL을 처리하는 단계별 절차 |
| 카디널리티 | Cardinality | 각 단계의 예상 결과 행 수 |
| 선택도 | Selectivity | 조건을 만족하는 행의 비율 |
| 논리적 읽기 | Logical Read (Buffer Gets) | 버퍼 캐시에서 읽은 블록 수 |
| 물리적 읽기 | Physical Read | 디스크에서 읽은 블록 수 |
| 하드 파싱 | Hard Parse | 실행계획을 새로 생성하는 파싱 |
| 바인드 변수 | Bind Variable | 값 대신 변수를 사용해 SQL을 공유하는 방식 |
| 히스토그램 | Histogram | 컬럼 값 분포 통계 |
| 클러스터링 팩터 | Clustering Factor | 인덱스 순서와 테이블 저장 순서의 일치 정도 |
| 드라이빙 테이블 | Driving (Outer) Table | 조인에서 먼저 읽히는 집합 |
| 부분 범위 처리 | Partial Range Processing | 전체 결과가 아닌 첫 일부만 빠르게 반환 |
| 파티션 프루닝 | Partition Pruning | 조건에 해당하는 파티션만 읽는 것 |
| 대기 이벤트 | Wait Event | 세션이 자원을 기다리는 사유 |
| AWR | Automatic Workload Repository | 성능 통계를 주기적으로 저장하는 저장소 |
| ASH | Active Session History | 활성 세션 상태를 1초 단위로 샘플링한 이력 |
선수 학습 ← 데이터 모델링 입문 · Oracle DB 물리 설계 | 다음 학습 → 실행계획 읽기 실습 · 인덱스 튜닝 심화 · 조인 튜닝 심화 · AWR 리포트 분석