Nested Loops Join 튜닝: 선행 집합·Predicate·ROWID 비용
Outer·Inner Access Predicate와 Table Filter를 분리해 불필요한 ROWID Random Access가 시작되는 지점을 찾습니다.
핵심 요약
Nested Loops Join 튜닝은 단순히 Inner Join Key에 Index를 만드는 작업이 아닙니다.
NL 총비용
≈ Outer Row Source 생성 비용
+ Outer 실제 Row 수
× Inner 1회 탐색 비용
Inner 1회 탐색 비용을 더 나누면 다음과 같습니다.
Inner 1회 비용
≈ Index Root·Branch 탐색
+ 조건 범위의 Leaf Entry Scan
+ 후보 ROWID 수
+ ROWID Table Block 방문
+ Index·Table Filter 평가
따라서 NL 튜닝의 핵심 질문은 두 가지입니다.
1. Outer에서 불필요한 반복 횟수가 얼마나 만들어지는가?
2. Inner 한 번의 Probe에서 불필요한 Leaf·ROWID·Table 작업이 얼마나 발생하는가?
실행계획에서는 다음 흐름으로 확인합니다.
Outer Index A-Rows
→ Outer Table A-Rows
→ Inner Starts
→ Inner Index A-Rows
→ Inner Table A-Rows
→ Join 최종 A-Rows
Oracle의 BATCHED ROWID Access는 ROWID를 묶어 Table Block 방문을 개선하지만, 후보 ROWID 자체와 Table Filter 탈락을 제거하지는 않습니다.
이 이론의 범위
이 이론은 SQLP의
SQL 고급활용 및 튜닝 → 조인 순서와 조인 방식 → NL 조인 튜닝범위에서 Outer 후보 축소, Inner Predicate 위치, Index Column 구성과 ROWID 비용을 다룹니다.
학습 목표
이 이론을 학습한 뒤에는 다음을 설명할 수 있어야 합니다.
- NL 비용을 Outer 반복 수와 Inner 1회 비용으로 분해한다.
- Outer Index 후보와 Outer Table Filter 후 결과를 구분한다.
- Access Predicate·Index Filter·Table Filter를 Operation 위치와 함께 설명한다.
- 복합 Index에서 선행 등치 조건과 첫 Range 조건 이후 Column의 역할을 설명한다.
- Inner Index A-Rows와 Table A-Rows 차이로 ROWID 낭비를 계산한다.
- 두 개의 NESTED LOOPS와 BATCHED ROWID Access를 해석한다.
- Covering Index가 Table Access를 제거할 수 있는 조건을 설명한다.
- Starts·E-Rows·A-Rows를 Total·Per-Start 단위로 맞춘다.
- Cardinality·Histogram·Column Correlation·Bind Skew를 NL 반복 비용과 연결한다.
- NL 유지·Join Order 변경·Hash Join 전환을 동일 결과·Bind·Fetch 조건에서 비교한다.
1. 업무 예제와 기본 실행 흐름
CUSTOMERS
한 행 = 고객 한 명
ORDERS
한 행 = 주문 한 건
고객 한 명은 여러 주문 보유 가능
특정 지역의 VIP 고객이 최근 결제한 주문을 조회합니다.
SELECT /*+ GATHER_PLAN_STATISTICS */
c.customer_id,
c.customer_name,
o.order_id,
o.order_date,
o.amount
FROM customers c
JOIN orders o
ON o.customer_id = c.customer_id
WHERE c.region_code = :region_code
AND c.grade = 'VIP'
AND o.order_date >= :from_date
AND o.status = 'PAID';
CUSTOMERS가 Outer, ORDERS가 Inner라면 논리적 흐름은 다음과 같습니다.
CUSTOMERS 조건을 만족한 Row 1건
→ CUSTOMER_ID 전달
→ ORDERS Index Probe
→ 주문 ROWID Table Access
→ 날짜·상태 조건 확인
→ Match 반환
→ 다음 고객 반복
개념적 Plan입니다.
NESTED LOOPS
TABLE ACCESS BY INDEX ROWID CUSTOMERS
INDEX RANGE SCAN CUSTOMERS_REGION_IX
TABLE ACCESS BY INDEX ROWID ORDERS
INDEX RANGE SCAN ORDERS_CUSTOMER_IX
성능 조건입니다.
작은 Outer 결과
×
작은 Inner 1회 후보
×
적은 Table Filter 탈락
2. Outer 후보 행 줄이기
2.1 후보와 최종 Row를 구분한다
다음 Index만 있다고 가정합니다.
CUSTOMERS_REGION_IX(region_code)
region_code 조건 후보 50,000행
→ Table Access
→ grade='VIP' Filter
→ 최종 Outer 500행
Outer Index A-Rows = 50,000
Outer Table A-Rows = 500
Outer Table에서 49,500행이 탈락했습니다.
이 낭비는 두 비용을 만듭니다.
- 불필요한 CUSTOMER Table ROWID Access
- 실제 500행만 Inner Probe를 수행하지만 Optimizer가 후보·최종 분포를 틀리게 추정하면 Join Order·Method가 왜곡될 가능성
2.2 복합 Index 후보
CUSTOMERS_REGION_GRADE_IX(region_code, grade)
두 Equality Predicate를 Index Access에 함께 사용하면 Table 방문 전에 후보를 줄일 수 있습니다.
하지만 다음을 함께 확인합니다.
- 다른 Query의 선두 Column 사용
region_code와grade분포·상관관계- Index 중복
- 정렬 요구
- 반환 Column Covering 가능성
- DML·Redo·공간 비용
한 SQL의 후보 감소
≠ 전체 Workload 최적 Index
2.3 Outer E-Rows 오차
Outer를 20행으로 예상했으나 실제 2,000행이면 Inner 반복도 약 100배 과소 평가될 수 있습니다.
Outer E-Rows 20
Outer A-Rows 2,000
→ Inner Starts 예상 방향 20
→ 실제 Starts 방향 2,000
확인합니다.
NUM_DISTINCT·Density·Histogram(region_code,grade)Column Group Statistics- Bind Peeking·Child Cursor
- Statistics 수집 시점
- Predicate Data Type·Implicit Conversion
3. 최신 NL 실행계획 구조
Oracle 11g 이후 Plan에서는 Index ROWID 생성과 Table Access가 분리돼 두 개의 NESTED LOOPS로 보일 수 있습니다.
NESTED LOOPS
NESTED LOOPS
Outer Row Source
INDEX RANGE SCAN ORDERS_INDEX
TABLE ACCESS BY INDEX ROWID ORDERS
해석합니다.
Inner NESTED LOOPS
→ Outer Row와 Inner Index Entry·ROWID 결합
Outer NESTED LOOPS
→ 생성한 ROWID로 Inner Table Row 읽기
따라서 Inner Access를 다음 묶음으로 읽습니다.
Inner Index Scan
+
Inner Table by ROWID
Index A-Rows와 Table A-Rows 차이, 각각의 Buffers를 함께 비교합니다.
4. Access Predicate·Index Filter·Table Filter
Predicate Information은 filter라는 단어만 보지 않고 어느 Operation에 표시됐는지 확인합니다.
| 구분 | 처리 위치 | 주요 효과 |
|---|---|---|
| Access Predicate | Index Scan·Table Access의 탐색 경계 | 읽을 Index Range·Partition 범위를 직접 제한 |
| Index Filter | Index Entry를 읽은 뒤 Index Operation에서 평가 | Table로 전달할 ROWID를 줄일 수 있음 |
| Table Filter | ROWID로 Table Row를 읽은 뒤 Table Operation에서 평가 | 읽고 버리는 Table Access 발생 |
예시입니다.
5 - access(
"O"."CUSTOMER_ID"="C"."CUSTOMER_ID"
AND "O"."STATUS"='PAID'
AND "O"."ORDER_DATE">=:FROM_DATE
)
4 - filter("O"."CHANNEL_CODE"='APP')
CUSTOMER_ID·STATUS·ORDER_DATE가 Index Access에 사용되면 Leaf Scan 후보를 줄일 수 있습니다.
CHANNEL_CODE가 Table Filter라면 다음 비용이 발생합니다.
Index 후보 ROWID 생성
→ ORDERS Table Block 방문
→ CHANNEL_CODE 평가
→ 탈락 Row 폐기
5. 복합 Index Column 순서
현재 조건입니다.
o.customer_id = c.customer_id
AND o.order_date >= :from_date
AND o.status = 'PAID'
5.1 후보 A
(customer_id, order_date, status)
customer_id Equality
→ 특정 고객 범위 시작
order_date Range
→ 해당 고객의 날짜 구간 제한
status Equality
→ 첫 Range 뒤에 있어
일부 Plan에서는 Index Filter 역할이 커질 수 있음
5.2 후보 B
(customer_id, status, order_date)
customer_id Equality
+ status Equality
→ 고객·상태 조합 범위
order_date Range
→ 그 조합 안에서 날짜 범위
후보 B가 이 SQL에는 더 좁은 Start·Stop Key를 만들 수 있지만 항상 정답은 아닙니다.
비교 기준입니다.
- 고객별 Status별 주문 건수
- 날짜 범위 크기
- Status Data Skew
ORDER BY order_date활용- 다른 Query의 Predicate 조합
- Index Skip·Range 가능성
- Index 폭·DML 비용
등치 Column은 항상 Range 앞
→ 단순 규칙으로 결정하지 않음
실제 Predicate 조합·분포·정렬·Workload
→ Column 순서 결정
6. Inner ROWID Table Access 비용
다음 통계를 가정합니다.
Inner Starts 2,000
Inner Index A-Rows 30,000
Inner Table A-Rows 1,500
계산합니다.
Index 후보 per Start
= 30,000 / 2,000
= 15행
최종 Table 반환 per Start
= 1,500 / 2,000
= 0.75행
후보와 최종 Row 차이입니다.
Index 후보 30,000
→ Table 접근
→ 최종 1,500
→ 최대 28,500 후보가 Table 단계에서 탈락 가능
우선 확인합니다.
- Table Filter Column
- Index에 없는 반환 Column
- Clustering Factor
TABLE ACCESS BY INDEX ROWID BATCHED- Table Buffers
- Row Migration·Chain
- Partition Pruning 여부
7. BATCHED ROWID Access
TABLE ACCESS BY INDEX ROWID BATCHED
INDEX RANGE SCAN
Oracle은 Index에서 ROWID를 몇 개씩 모으고 Table Block 순서에 가깝게 접근해 동일 Block 방문 횟수를 줄이려 합니다.
BATCHED의 이점
→ Block 방문 순서 개선
BATCHED가 해결하지 못하는 것
→ 과도한 Index 후보 Entry
→ 많은 후보 ROWID
→ Table Filter 탈락
→ Outer Starts 폭증
따라서 BATCHED Operation이 보인다는 이유만으로 ROWID 비용이 적다고 판단하지 않습니다.
8. Covering Index
Query에 필요한 Inner Column이 모두 Index에 있으면 Table by ROWID를 생략할 수 있습니다.
SELECT o.order_id,
o.order_date
FROM customers c
JOIN orders o
ON o.customer_id=c.customer_id
WHERE ...
후보 Index가 다음 Column을 포함하고 있다고 가정합니다.
(customer_id,status,order_date,order_id)
Optimizer가 Index만으로 Predicate와 Projection을 해결하면 다음 형태가 가능합니다.
NESTED LOOPS
Outer Row Source
INDEX RANGE SCAN ORDERS_COVER_IX
Trade-off입니다.
- Index Entry 폭 증가
- LEAF_BLOCKS 증가
- Buffer Cache 점유
- DML·Redo·Undo 증가
- 다른 Index와 중복
- Range Scan당 읽는 Byte 증가
Table Access 제거 이득
vs
Index 유지 비용
전체 Workload에서 비교합니다.
9. 실행통계로 반복 비용 계산
다음 Plan을 가정합니다.
| Id | Operation | Starts | E-Rows | A-Rows | Buffers |
|---:|-------------------------------------|-------:|-------:|-------:|--------:|
| 1 | NESTED LOOPS | 1 | 40 | 1,500 | 42,000 |
| 2 | TABLE ACCESS BY INDEX ROWID CUST | 1 | 20 | 2,000 | 4,000 |
| 3 | INDEX RANGE SCAN CUST_REGION_IX | 1 | 20 | 25,000 | 500 |
| 4 | TABLE ACCESS BY INDEX ROWID ORDERS | 2,000 | 2 | 1,500 | 38,000 |
| 5 | INDEX RANGE SCAN ORDERS_CUST_IX | 2,000 | 2 | 30,000 | 8,000 |
9.1 Outer
Outer Index 후보 25,000
Outer 최종 Row 2,000
Outer 후보 탈락 23,000
Outer E-Rows 20
Outer A-Rows 2,000
→ 100배 과소 추정
9.2 Inner
Inner Starts 2,000
Index 실제 총 후보 30,000
Table 실제 최종 1,500
Index 후보 per Start = 15
최종 Row per Start = 0.75
9.3 예상과 실제 단위
Expected Total
≈ Starts × E-Rows
= 2,000×2
= 4,000
Actual Total
= 30,000
Inner 한 번의 후보도 예상보다 7.5배 큽니다.
9.4 Buffer
Index Buffers 8,000
Table Buffers 38,000
Table by ROWID Branch의 비용이 더 큽니다.
주의합니다.
- Parent Operation Buffer에는 Child 작업이 포함될 수 있음
- 모든 Plan Line Buffer를 단순 합산하지 않음
- Statement 총량과 비용 집중 Branch를 분리함
10. Cardinality·Skew·Bind
다음 분포를 가정합니다.
일반 고객
최근 PAID 주문 0~2건
인기 고객
최근 PAID 주문 50,000건
평균 통계만으로 모든 고객을 같은 Cardinality로 추정하면 한 NL Plan이 모든 Bind에 적합하지 않을 수 있습니다.
확인합니다.
- 고객별 주문 수 분포
- Popular Customer
statusHistogram(customer_id,status)상관관계- Bind Peeking 값
- Child Cursor
IS_BIND_SENSITIVEIS_BIND_AWARE- 값별 Starts·A-Rows·Buffers
Adaptive Cursor Sharing은 Bind Cardinality 범위에 따라 다른 Child Plan을 사용할 수 있습니다.
일반 고객
→ NL·Index Probe 후보
인기 고객
→ Hash·Scan 후보
11. NL 유지·Join Order 변경·Hash 전환
11.1 NL 유지
다음이면 NL 개선이 우선일 수 있습니다.
- Outer 실제 Row가 작음
- Inner 후보 per Start가 작음
- Table Filter 탈락이 적음
- First Row·부분 Fetch 중요
- Bind별 규모가 안정적
11.2 Join Order 변경
현재:
CUSTOMERS Filter
→ 고객별 ORDERS Probe
대안:
최근 PAID ORDERS Filter
→ CUSTOMER PK Probe
비교합니다.
CUSTOMERS 선행 비용
≈ 고객 Outer 생성
+ 고객 수×주문 Probe
ORDERS 선행 비용
≈ 최근 주문 Outer 생성
+ 주문 수×고객 PK Probe
원본 Table 크기가 아니라 Filter 후 A-Rows와 다음 1회 비용으로 판단합니다.
11.3 Hash Join
다음이면 Hash Join을 비교합니다.
- Outer·Inner 실제 Row가 큼
- 대부분 결과를 End-of-Fetch까지 읽음
- NL Table Block Random Access 누적
- Partition Pruning 후 Scan이 효율적
- Build Data가 Workarea에 적합
NL
반복 Index·ROWID Access
Hash
Build Scan + Hash Workarea + Probe Scan
동일 결과·Bind·Fetch 범위에서 비교합니다.
12. 진단 절차
1. SQL_ID·Child Number·Bind·Fetch 목표를 고정한다.
2. Outer Index A-Rows와 Outer Table A-Rows를 비교한다.
3. Outer의 최초 E/A 오차 Predicate를 찾는다.
4. Inner Starts를 Outer A-Rows와 연결한다.
5. Inner Access·Index Filter·Table Filter 위치를 확인한다.
6. Inner Index A-Rows/Starts로 후보 per Start를 계산한다.
7. Inner Table A-Rows/Starts로 최종 Match per Start를 계산한다.
8. Index·Table Buffers와 BATCHED·Clustering Factor를 확인한다.
9. 복합 Index·Covering·Statistics·Join Order·Hash 대안을 실험한다.
10. 동일 결과·Bind·Fetch에서 Runtime과 다른 Bind 회귀를 검증한다.
자주 혼동하는 판단
| 혼동 | 정확한 기준 |
|---|---|
| Inner Join Key Index가 있으면 완료 | 후보 per Start·Table Filter·Buffers까지 확인 |
| 조건 Column을 Index에 넣으면 모두 Access Predicate | Column 순서와 첫 Range 이후 조건 역할 확인 |
| Index Filter와 Table Filter는 같은 비용 | Table Filter는 ROWID Table Access 후 탈락 가능 |
| BATCHED면 Random Access 문제 해결 | Block 순서 개선일 뿐 후보 수·Filter 탈락은 유지 |
| Outer Table이 작으면 좋은 Driving | Filter 후 A-Rows와 다음 Probe 비용이 기준 |
| Covering Index는 무조건 좋음 | Index 폭·DML·공간·다른 SQL 비용 동반 |
| Physical Read가 적으면 빠름 | Cache Hit에서도 Logical I/O·CPU 반복 가능 |
| E-Rows와 A-Rows를 직접 비교 | 반복 Operation은 Total·Per-Start 단위 조정 |
| NL Cost가 낮으면 실제 반복도 작음 | Starts·A-Rows·Buffers로 실측 |
| 한 Bind에서 좋은 NL은 모든 Bind에 좋음 | Data Skew·ACS·다른 Child Plan 확인 |
개념 확인 문제
문제를 누르면 바로 아래에서 정답과 해설을 확인할 수 있습니다.
01NL Join 튜닝에서 줄여야 하는 두 가지 핵심 비용은 무엇인가?
핵심 비용
- Outer에서 생성되는 Inner 반복 횟수입니다.
- 각 반복의 Inner Index·ROWID Table Access·Filter 비용입니다.
02Outer Index 후보와 Outer Table 최종 Row를 비교하는 이유를 설명하시오.
Outer 후보·최종 비교
- Index 후보가 많고 Table Filter 후 적은 Row만 남으면 불필요한 Outer Table Access가 발생합니다.
- 최종 Outer Row는 다음 Inner Starts의 출발점입니다.
- E-Rows 오차도 Join Order·Method 선택에 영향을 줍니다.
03최신 Oracle Plan에서 두 개의 NESTED LOOPS가 나타날 수 있는 이유를 설명하시오.
두 개의 NESTED LOOPS
- 첫 NL이 Outer Row와 Inner Index Entry·ROWID를 결합할 수 있습니다.
- 바깥 NL이 생성된 ROWID로 Inner Table Row를 읽습니다.
- Index Scan과 Table Access를 하나의 논리적 Inner Access로 해석합니다.
04Access Predicate·Index Filter·Table Filter의 처리 위치와 비용 차이를 설명하시오.
Predicate 위치
- Access Predicate는 Index Start·Stop Key나 Partition 범위를 만들어 처음부터 읽을 범위를 줄입니다.
- Index Filter는 읽은 Index Entry를 Table 방문 전에 제거할 수 있습니다.
- Table Filter는 ROWID로 Table Row를 읽은 뒤 평가하므로 탈락 Row에도 Table Access 비용이 발생합니다.
05(customerid,orderdate,status)에서 status가 Index Filter가 될 수 있는 이유를 설명하시오.
첫 Range 이후 Column
- 복합 Index는 사전식으로 정렬됩니다.
- customer_id Equality 뒤 order_date Range가 열리면 후행 status Equality가 전체 범위를 하나의 좁은 Start·Stop 구간으로 만들기 어려울 수 있습니다.
- status는 Index Entry를 읽은 뒤 Filter 역할을 할 수 있습니다.
06Starts=2,000, Index A-Rows=30,000, Table A-Rows=1,500일 때 각각의 per-Start 값을 계산하시오.
Per-Start 계산
- Index 후보 per Start는
30,000/2,000=15행입니다. - Table 최종 Row per Start는
1,500/2,000=0.75행입니다. - 많은 후보가 Table 단계에서 탈락하는 패턴입니다.
07BATCHED ROWID Access가 해결하는 것과 해결하지 못하는 것을 설명하시오.
BATCHED
- 여러 ROWID를 묶어 Table Block 순서에 가깝게 방문해 동일 Block 반복 접근을 줄입니다.
- 과도한 Index Entry·후보 ROWID·Table Filter 탈락·Outer Starts는 제거하지 못합니다.
08Covering Index의 이점과 운영 비용을 설명하시오.
Covering
- 필요한 Predicate·반환 Column을 Index에서 모두 해결하면 Table by ROWID를 생략할 수 있습니다.
- Index 폭·LEAF_BLOCKS·Cache 점유·DML·Redo·공간 비용이 증가할 수 있습니다.
- 전체 Workload로 판단합니다.
09인기 고객과 일반 고객의 주문 수 차이가 NL Plan에 미치는 영향을 설명하시오.
Bind Skew
- 일반 고객은 Inner Probe가 작아 NL이 적합할 수 있습니다.
- 인기 고객은 수만 건을 반환해 반복 Index·Table Access가 커질 수 있습니다.
- Bind Peeking·Histogram·Adaptive Cursor Sharing과 Child Cursor를 확인합니다.
10NL 유지·Join Order 변경·Hash Join을 공정하게 비교하는 절차를 설명하시오.
공정한 비교 - 동일 SQL 결과·Bind·Data Type·Statistics를 사용합니다. - First Row 또는 Full Fetch 목표를 고정합니다. - Outer A-Rows, Inner Starts, 후보·최종 per-Start, Buffers·Reads·Elapsed를 비교합니다. - 다른 Bind와 동시 Workload의 Plan Regression도 검증합니다.