현재 선택한 SQL 과정

SQLP 이론 학습

이론 목록으로 돌아가기

Nested Loops Join 기본 원리: Outer·Inner·반복 탐색

Driving Row마다 Inner Row Source가 반복 시작되는 NL Join 구조를 Starts와 A-Rows로 계산합니다.

예상 읽기 17

핵심 요약

Nested Loops Join은 Outer Row Source가 한 행을 생산할 때마다 그 행의 Join Key로 Inner Row Source를 다시 시작하는 조인 방식입니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
Outer Row 1
  → Inner Probe
  → Match 반환

Outer Row 2
  → Inner Probe
  → Match 반환

...

개념적인 총비용입니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
NL 총비용
≈ Outer Row Source 생성 비용
 + Outer 실제 행 수
   × Inner 1회 탐색 비용

실제 결과 Row 수는 다음 값에도 영향을 받습니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
NL 결과 Row
≈ Outer Row 수
 × Outer 한 행당 Inner 평균 Match 수

따라서 NL Join은 다음 조건에서 경쟁력이 높습니다.

  • Predicate 적용 후 Outer Row Source가 작음
  • Inner Join Key를 Index·Partition Key 등으로 빠르게 탐색 가능
  • Inner 한 번의 Probe가 적은 Row·Block만 처리
  • 첫 행·첫 페이지 응답이 중요
  • Client가 전체 결과를 끝까지 Fetch하지 않을 수 있음

반대로 작은 1회 비용도 수십만·수백만 번 반복되면 전체 Buffers와 Elapsed가 커집니다.

이 이론의 범위

이 이론은 SQLP의 SQL 고급활용 및 튜닝 → 조인 순서와 조인 방식 → NL 조인 범위에서 Outer·Inner 구조, Starts·A-Rows 해석, BATCHED ROWID Access와 반복 비용 진단을 다룹니다.


학습 목표

이 이론을 학습한 뒤에는 다음을 설명할 수 있어야 합니다.

  • Outer·Inner Row Source의 역할을 구분한다.
  • NL Join을 중첩 반복문 구조로 설명한다.
  • 원본 Table 크기보다 Filter 후 Outer Cardinality가 중요한 이유를 설명한다.
  • Inner Starts와 Outer A-Rows의 관계를 설명한다.
  • Inner A-Rows/Starts로 1회 평균 Match 수를 계산한다.
  • Oracle 11g 이후 NL Plan에 두 개의 NESTED LOOPS가 나타날 수 있는 이유를 설명한다.
  • TABLE ACCESS BY INDEX ROWID BATCHED의 목적을 설명한다.
  • Covering Index가 Inner Table Access를 제거할 수 있는 이유를 설명한다.
  • First Row와 Full Fetch 성능 목표를 구분한다.
  • Multi-Table NL에서 앞 단계의 행 증가가 뒤 단계 반복에 전파되는 과정을 설명한다.
  • Adaptive Plan에서 실제 왼쪽 Row 수에 따라 NL·Hash 후보가 선택될 수 있음을 설명한다.
  • ALLSTATS LAST에서 Starts·E-Rows·A-Rows·Buffers를 맞춰 검증한다.

1. NL Join의 기본 구조

두 Row Source의 역할입니다.

구분역할
Outer Row Source먼저 행을 생산해 조인을 이끄는 입력
Inner Row SourceOuter의 Join Key를 받아 반복 탐색되는 입력

중첩 반복문으로 표현하면 다음과 같습니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
FOR outer_row IN outer_row_source LOOP
    FOR inner_row IN inner_row_source(outer_row.join_key) LOOP
        두 Row를 결합해 반환
    END LOOP
END LOOP

Outer는 반드시 단일 Table이 아닙니다.

  • Index Scan 후 Table Access 결과
  • Full Table Scan의 Filter 결과
  • View·Inline View
  • Aggregate 결과
  • 이전 Join의 중간 결과
  • Partition Iterator 결과
CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
원본 Table
  ≠ NL의 실제 Outer 크기

Predicate 적용 후 Row Source
  = NL 반복 횟수의 출발점

2. 업무 예제

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
ORDERS
  한 행 = 주문 한 건

CUSTOMERS
  한 행 = 고객 한 명
  CUSTOMER_ID Primary Key

최근 PAID 주문과 고객명을 조회합니다.

SQL코드 영역 안에서 좌우로 이동할 수 있습니다.
SELECT /*+ GATHER_PLAN_STATISTICS */
       o.order_id,
       o.order_date,
       c.customer_name
FROM   orders o
JOIN   customers c
  ON   c.customer_id = o.customer_id
WHERE  o.order_date >= :from_date
AND    o.status = 'PAID';

ORDERS가 Outer이고 CUSTOMERS가 Inner인 논리 흐름입니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
1. 조건을 만족하는 ORDERS Row 한 건을 읽음
2. CUSTOMER_ID를 얻음
3. CUSTOMERS_PK를 Probe
4. 고객 Row를 주문 Row와 결합
5. 다음 주문에 대해 반복

Outer가 3행이고 고객 Key가 유일하면 다음과 같은 방향입니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
Outer A-Rows            3
CUSTOMERS_PK Starts     3
Inner 평균 Match        1
최종 Join A-Rows        3

3. 실행계획의 Outer와 Inner

3.1 고전적인 기본 형태

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
NESTED LOOPS
  TABLE ACCESS FULL DEPARTMENTS        ← Outer
  TABLE ACCESS BY INDEX ROWID EMPLOYEES
    INDEX RANGE SCAN EMP_DEPARTMENT_IX ← Inner 탐색

논리적으로는 DEPARTMENTS Row마다 EMP_DEPARTMENT_IX를 Probe하고, 얻은 ROWID로 EMPLOYEES Table Row를 읽습니다.

3.2 최신 Plan에서 두 개의 NESTED LOOPS

Oracle 11g 이후에는 Index에서 ROWID를 얻는 단계와 Table Row를 읽는 단계를 분리해 다음처럼 표시될 수 있습니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
NESTED LOOPS
  NESTED LOOPS
    Outer Row Source
    INDEX RANGE SCAN INNER_INDEX
  TABLE ACCESS BY INDEX ROWID INNER_TABLE

해석합니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
Inner NESTED LOOPS
  → Outer Row와 Inner Index Entry·ROWID를 결합

Outer NESTED LOOPS
  → 생성된 ROWID로 Inner Table Row를 읽음

따라서 단순히 NESTED LOOPS의 두 번째 자식만 보고 Inner Table 전체를 판단하지 않습니다. Index ROWID 생성과 Table Access를 한 묶음의 논리적 Inner Access로 읽습니다.


4. TABLE ACCESS BY INDEX ROWID BATCHED

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
TABLE ACCESS BY INDEX ROWID BATCHED
  INDEX RANGE SCAN

BATCHED Access는 Index에서 ROWID를 몇 개씩 모은 뒤 Table Block 순서에 가깝게 Row를 방문하려는 방식입니다.

목적입니다.

  • 같은 Table Block을 반복 방문하는 횟수 감소
  • Clustering이 좋지 않은 Range Scan의 Block 접근 개선
  • Index 순서 그대로 한 건씩 Table을 방문하는 비용 완화

주의합니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
BATCHED
  → Random Access 제거 X
  → ROWID 기반 Table Access를 Batch로 개선 O

후보 ROWID가 매우 많거나 Table Filter 탈락이 크면 BATCHED여도 전체 비용은 클 수 있습니다.


5. Covering Index와 Table Access

Index가 Query에 필요한 모든 Column을 포함하면 Inner Table Access가 생략될 수 있습니다.

SQL코드 영역 안에서 좌우로 이동할 수 있습니다.
SELECT o.order_id,
       c.customer_id
FROM   orders o
JOIN   customers c
  ON   c.customer_id = o.customer_id;

CUSTOMERS_PK에 필요한 Column이 모두 있다면 다음처럼 Index만으로 끝날 수 있습니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
NESTED LOOPS
  ORDERS Row Source
  INDEX UNIQUE SCAN CUSTOMERS_PK

반대로 customer_name처럼 Index에 없는 Column이 필요하면 ROWID Table Access가 추가됩니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
Index Probe
  + Table Access 여부
  = Inner 1회 비용

Covering을 위해 Column을 무조건 추가하면 Index 폭·DML·공간 비용이 증가하므로 전체 Workload로 판단합니다.


6. NL Join이 유리한 조건

6.1 Filter 후 Outer가 작음

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
ORDERS 원본 1,000,000,000행
→ 날짜·상태 조건 후 5행
→ Outer 5행

대형 Table도 선택적인 Access Path가 있으면 작은 Outer가 될 수 있습니다.

6.2 Inner Access가 효율적임

대표적으로 다음 조건입니다.

  • PK·UK Index Unique Scan
  • 선택적인 Index Range Scan
  • Partition Key를 통한 작은 Partition Access
  • Covering Index
  • Key당 Match Row가 적음
  • Inner Row·Index Block이 Cache에 잘 유지됨

6.3 부분범위 처리

NL Join은 첫 Outer Row와 Inner Match를 얻으면 즉시 결과를 반환할 수 있습니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
첫 20행
  → 전체 Build·Sort 없이 빠를 수 있음

전체 100만행
  → 반복 Probe 총비용이 더 중요

FIRST_ROWS_n 목표는 첫 n행 Cost를 선호하도록 할 수 있지만 실제 결과 순서와 Fetch 범위는 SQL·Client 계약으로 결정됩니다.


7. NL Join이 불리해지는 원인

원인반복 비용
Outer A-Rows 과다Inner Starts 증가
Inner Index 없음Full Scan·넓은 Scan 반복
낮은 선택도의 Inner Range ScanLeaf Entry·ROWID 증가
Key당 Match Row 다수Inner A-Rows·최종 결과 증가
Table Filter 대량 탈락RowID Table Access 후 폐기 반복
높은 Clustering FactorTable Block 방문 분산
전체 결과 Full FetchFirst Row 장점보다 총 반복 비용 우세
Outer Cardinality 과소 추정NL Cost를 실제보다 작게 계산
Bind별 Outer 규모 차이한 Plan이 모든 Bind에 부적합 가능

핵심 진단식입니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
Inner Total Buffers
≈ Inner Starts × Inner 평균 Buffers per Start

8. Starts·E-Rows·A-Rows

V$SQL_PLAN_STATISTICS에서 다음을 구분합니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
LAST_STARTS
  → 마지막 실행에서 Row Source가 시작된 횟수

LAST_OUTPUT_ROWS
  → 마지막 실행에서 Row Source가 생산한 누적 Row 수

LAST_CR_BUFFER_GETS
  → 마지막 실행의 Consistent Buffer Get

DBMS_XPLAN.DISPLAY_CURSOR(...,'ALLSTATS LAST')에서는 일반적으로 다음 Column으로 보입니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
Starts
E-Rows
A-Rows
Buffers

8.1 단위 맞추기

반복 Row Source에서는 E-Rows와 A-Rows를 그대로 비교하면 단위가 다를 수 있습니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
Expected Total
≈ E-Rows × Starts

Actual Total
= A-Rows

또는

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
Expected per Start
= E-Rows

Actual per Start
≈ A-Rows / Starts

예시입니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
Starts  = 100
E-Rows  = 5
A-Rows  = 300
CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
예상 총 Row = 5×100 = 500
실제 총 Row = 300

예상 Start당 5행
실제 Start당 3행

9. 실행통계 예제

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
| Id | Operation                              | Starts | E-Rows | A-Rows | Buffers |
|---:|----------------------------------------|-------:|-------:|-------:|--------:|
|  1 | NESTED LOOPS                           |      1 |     10 |    300 |   1,240 |
|  2 |  TABLE ACCESS FULL DEPARTMENTS         |      1 |      2 |    100 |      40 |
|  3 |  TABLE ACCESS BY INDEX ROWID BATCHED E |    100 |      5 |    300 |   1,200 |
|  4 |   INDEX RANGE SCAN EMP_DEPT_IX         |    100 |      5 |    300 |     300 |

해석합니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
Outer Actual Row          = 100
Inner Starts              = 100
Inner Total Output        = 300
Inner Actual per Start    = 3

Optimizer는 Outer를 2행으로 예상했으므로 Inner 반복 횟수도 심하게 과소 평가했을 가능성이 있습니다.

비용 집중 위치입니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
Index Buffers   300
Table Buffers 1,200

→ Index에서 ROWID를 찾는 비용보다
  Table Row 방문 비용이 더 큼

주의합니다.

  • 상위 Operation의 Buffers·A-Time은 하위 작업을 포함할 수 있음
  • 모든 Line Buffers를 단순 합산하지 않음
  • Statement 총량과 비용이 집중된 Branch를 구분함

10. Inner Filter와 Access Predicate

다음 Plan을 가정합니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
TABLE ACCESS BY INDEX ROWID ORDERS
  filter(status='PAID')
  INDEX RANGE SCAN ORDERS_CUSTOMER_IX
    access(customer_id=:outer_customer_id)
CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
Access Predicate
  → Index 탐색 범위를 정함

Table Filter
  → ROWID로 Table Row를 읽은 뒤 조건 평가

고객별 주문이 1,000건인데 PAID가 1건이면 다음 비용이 반복될 수 있습니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
Index Entry 1,000건
→ Table Row 1,000건 접근
→ PAID 1건 반환

가능한 대안입니다.

  • (customer_id,status) 복합 Index
  • 업무 Predicate 재작성
  • Outer·Join Order 변경
  • Hash Join 대안
  • Statistics·Histogram 개선

11. Multi-Table Nested Loops

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
NESTED LOOPS
  NESTED LOOPS
    Row Source A
    Row Source B
  Row Source C

처리 흐름입니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
A Row 1
→ B Probe
→ A+B 중간 Row 1
→ C Probe

A Row 1
→ B에서 두 번째 Match
→ A+B 중간 Row 2
→ C Probe

앞 단계에서 중간 결과가 커지면 다음 단계 Inner Starts도 증가합니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
A 100행
× B 평균 Match 20행
= A+B 2,000행
→ C Probe 최대 약 2,000회

따라서 Plan 아래쪽 최초 행 증가·Cardinality 오차를 먼저 찾습니다.


12. Adaptive NL·Hash 후보

Oracle은 Adaptive Plan에서 NL과 Hash 같은 대안 Subplan을 준비할 수 있습니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
STATISTICS COLLECTOR
→ 왼쪽 Row Source 실제 Row 수 관찰

Threshold 이하
→ NL 후보 사용

Threshold 초과
→ Hash 후보 사용

DBMS_XPLANADAPTIVE Format과 Plan Note에서 선택되지 않은 Operation이 -로 표시될 수 있습니다.

Adaptive Join은 반복되는 통계 오류를 영구 수정하지 않습니다. 같은 SQL에서 Cardinality 오차가 지속되면 Statistics·Predicate·Bind Skew를 점검합니다.


13. First Row와 Full Fetch 비교

공정한 비교 조건입니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
Plan A
  첫 20행 0.05초
  전체 100만행 30초

Plan B
  첫 20행 0.5초
  전체 100만행 8초

업무 목표에 따라 우수 Plan이 달라집니다.

측정 시 통일합니다.

  • 같은 Bind 값과 Data Type
  • 같은 Client Fetch Size
  • 같은 최종 Fetch Row 수
  • 같은 Cache·Statistics·Optimizer 환경
  • 첫 행 또는 End-of-Fetch 목표
  • 같은 결과 행·정렬 의미

ORDER BY가 없으면 NL Plan의 생산 순서가 결과 순서를 보장하지 않습니다.


14. 실제 검증 절차

SQL코드 영역 안에서 좌우로 이동할 수 있습니다.
SELECT /*+ GATHER_PLAN_STATISTICS */
       ...
FROM   ...;
SQL코드 영역 안에서 좌우로 이동할 수 있습니다.
SELECT *
FROM TABLE(
  DBMS_XPLAN.DISPLAY_CURSOR(
    :sql_id,
    :child_no,
    'ALLSTATS LAST +PREDICATE +ALIAS +NOTE'
  )
);

확인 순서입니다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
1. SQL_ID·Child Number·Bind를 고정한다.
2. NL 아래 논리적 Outer와 Inner Access 묶음을 식별한다.
3. Outer E-Rows·A-Rows를 비교한다.
4. Inner Starts와 Outer A-Rows의 관계를 본다.
5. Inner A-Rows/Starts로 평균 Match 수를 계산한다.
6. Index Buffers와 Table Buffers를 구분한다.
7. Access Predicate와 Table Filter를 구분한다.
8. BATCHED·Covering 여부를 확인한다.
9. First Row·Full Fetch를 동일 계약으로 측정한다.
10. Statistics 개선·Index 개선·Hash Join 대안을 검증한다.

자주 혼동하는 판단

혼동정확한 기준
작은 원본 Table이 항상 OuterFilter 후 Row Source와 Inner 비용이 기준
두 번째 자식 하나가 항상 Inner 전체최신 Plan에서는 Index ROWID 생성과 Table Access가 분리될 수 있음
BATCHED면 Random Access가 사라짐ROWID를 Batch로 묶어 Block 방문을 개선하는 방식
Inner Index가 있으면 NL은 항상 빠름Starts·Leaf Range·ROWID·Match 수를 함께 확인
Inner Starts는 항상 Outer A-Rows와 정확히 같음기본 관계지만 Filter·Batching·Caching·Plan 구조를 확인
E-Rows와 A-Rows는 그대로 비교반복 Row Source는 Total 또는 Per-Start 단위를 맞춤
첫 행이 빠르면 전체도 빠름First Row와 Full Fetch 계약을 분리
NL Plan이면 결과 순서가 보장됨결과 순서는 ORDER BY만 보장
BATCHED Table Buffers가 크면 Index만 RebuildOuter 규모·Filter·CF·Index Column·Join Method를 종합
Adaptive Plan이면 Statistics 문제 해결Runtime 선택일 뿐 반복 오류의 근본 원인은 별도 개선

스스로 확인하기

개념 확인 문제

문제를 누르면 바로 아래에서 정답과 해설을 확인할 수 있습니다.

01NL Join의 Outer와 Inner Row Source 역할을 설명하시오.
정답 및 해설

Outer·Inner

  • Outer는 먼저 행을 생산해 조인을 이끄는 Row Source입니다.
  • Inner는 Outer Row의 Join Key로 반복 탐색되는 Row Source입니다.
02NL Join 총비용을 Outer Row 수와 Inner 탐색 비용으로 표현하시오.
정답 및 해설

개념적 총비용

  • Outer 생성 비용 + Outer 실제 Row 수×Inner 1회 탐색 비용으로 이해합니다.
  • Key당 Match 수가 크면 최종 Join Row와 Inner Table Access도 함께 증가합니다.
03최신 Oracle NL Plan에서 두 개의 NESTED LOOPS가 나타날 수 있는 이유를 설명하시오.
정답 및 해설

두 개의 NESTED LOOPS

  • 최신 Plan에서는 첫 NL이 Outer Row와 Inner Index Entry·ROWID를 결합할 수 있습니다.
  • 두 번째 NL은 생성된 ROWID로 Inner Table Row를 읽습니다.
  • Index Scan과 Table Access를 논리적 Inner Access 묶음으로 해석합니다.
04TABLE ACCESS BY INDEX ROWID BATCHED의 목적과 한계를 설명하시오.
정답 및 해설

BATCHED

  • Index에서 여러 ROWID를 모은 뒤 Table Block 순서에 가깝게 방문해 같은 Block 반복 접근을 줄입니다.
  • ROWID Random Access 자체를 제거하는 것은 아니며 후보 ROWID가 많거나 Table Filter 탈락이 크면 여전히 비쌉니다.
05Covering Index가 Inner 1회 비용을 줄이는 이유를 설명하시오.
정답 및 해설

Covering Index

  • Query에 필요한 Inner Column이 모두 Index에 있으면 ROWID Table Access를 생략할 수 있습니다.
  • Inner 1회 비용이 Index Probe만으로 줄어듭니다.
  • Index 폭·DML 비용은 함께 평가합니다.
06Starts=2,000, E-Rows=1, A-Rows=80,000인 Inner의 예상 총 Row와 실제 Start당 Row를 계산하시오.
정답 및 해설

Starts 계산

  • 예상 총 Row는 2,000×1=2,000행입니다.
  • 실제 Start당 Row는 80,000/2,000=40행입니다.
  • 실제 반복 결과는 예상보다 Start당 40배 큽니다.
07Access Predicate와 Table Filter가 Inner 반복 비용에 미치는 차이를 설명하시오.
정답 및 해설

Access·Filter

  • Access Predicate는 Index Range를 줄여 처음부터 읽을 Entry를 제한합니다.
  • Table Filter는 ROWID로 Table Row를 읽은 뒤 탈락시키므로 반복 Table Access 비용이 이미 발생합니다.
  • 반복 Filter 탈락이 크면 복합 Index나 다른 Join 전략을 검토합니다.
08Multi-Table NL에서 앞 단계 중간 결과 증가가 뒤 단계에 미치는 영향을 설명하시오.
정답 및 해설

다단계 영향

  • 앞 NL 결과가 다음 NL의 Outer가 됩니다.
  • 앞 단계 A-Rows가 증가하면 다음 Inner Starts도 증가합니다.
  • 초기 Cardinality 오차와 1:N Match 증가가 뒤 단계로 곱셈 전파될 수 있습니다.
09Adaptive Plan에서 Statistics Collector가 NL·Hash 선택에 미치는 역할을 설명하시오.
정답 및 해설

Adaptive Join

  • Statistics Collector가 실행 초기의 왼쪽 Row 수를 관찰합니다.
  • Optimizer Threshold 이하이면 NL, 초과하면 Hash 후보를 선택할 수 있습니다.
  • 이는 Runtime 선택이며 Object Statistics 오류를 영구 수정하지 않습니다.
10NL Plan을 First Row와 Full Fetch 관점에서 공정하게 검증하는 절차를 설명하시오.
정답 및 해설

공정 검증 - 같은 SQL 결과·Bind·Data Type·Fetch Size를 사용합니다. - 첫 n행 또는 End-of-Fetch 중 목표를 고정합니다. - ALLSTATS LAST에서 Outer E/A, Inner Starts·A-Rows·Buffers를 확인합니다. - Index·Table Access와 BATCHED·Covering 여부를 구분합니다. - 대안 Plan을 동일 Fetch 계약에서 반복 측정합니다.