Nested Loops Join 고급 실행 구조: ROWID Batching·Hint 검증

SQL 고급 활용 및 튜닝 · NL 조인

이 페이지의 목차

NL Join의 Prefetch·Batched ROWID Access·Buffer Pinning과 조인 순서 Hint가 반복 I/O와 결과 순서에 미치는 영향을 이해합니다.

핵심 요약

Oracle의 Nested Loops Join은 논리적으로 Outer 한 행마다 Inner를 탐색하지만, 실제 실행계획은 다음처럼 더 세분화될 수 있습니다.

도식
Outer Row Source
→ Inner Index Probe
→ Outer Column + Inner ROWID 중간 Row Source
→ ROWID로 Inner Table Access
→ 최종 Join Row

Oracle의 현재 NL 구현에서는 한 업무 Join에 두 개의 NESTED LOOPS Row Source가 나타날 수 있습니다.

예시·텍스트
NESTED LOOPS
  NESTED LOOPS
    Outer Row Source
    INDEX RANGE SCAN INNER_INDEX
  TABLE ACCESS BY INDEX ROWID BATCHED INNER_TABLE

핵심 해석입니다.

도식
첫 번째 NL
  → Outer 값과 Inner Index에서 얻은 ROWID를 결합

두 번째 NL
  → 중간 ROWID로 Inner Table Row를 읽음

TABLE ACCESS BY INDEX ROWID BATCHED는 Index에서 얻은 여러 ROWID의 Table Block 방문 방식을 개선하지만 다음 작업량은 그대로 남을 수 있습니다.

  • 큰 Outer Row Source
  • Inner Starts 증가
  • 넓은 Index Range Scan
  • 과도한 후보 ROWID
  • Table Filter에서 대량 탈락
  • 대량 결과 Full Fetch

Hint는 다음 결정을 각각 다르게 유도합니다.

Hint주된 역할
LEADING(a b)지정 Row Source를 선두로 하는 Join Order 유도
ORDEREDFROM 절에 작성된 순서대로 Join Order 유도
USE_NL(b)b를 Inner로 하는 Nested Loops Join 유도
USE_NL_WITH_INDEX(b idx)b를 NL Inner로 하고 Join Predicate를 Key로 사용할 수 있는 Index 경로 유도
NO_USE_NL(b)b를 Inner로 하는 NL 후보 제외
INDEX(b idx)지정 Index Access Path 유도

이 이론의 범위

이 이론은 SQLP의 SQL 고급활용 및 튜닝 → 조인 순서와 조인 방식 → NL 조인 고급 실행 구조·Hint 검증 범위에서 두 단계 NL, ROWID Batching, Cache 재사용, Join Hint와 실제 Cursor 검증을 다룹니다.


학습 목표

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

  • 최신 NL Plan에 두 개의 NESTED LOOPS가 나타나는 이유를 설명한다.
  • Inner Index Probe와 Inner Table by ROWID를 하나의 논리적 Inner Access로 읽는다.
  • TABLE ACCESS BY INDEX ROWID BATCHED의 목적과 한계를 설명한다.
  • Batching과 Outer 반복량 감소를 구분한다.
  • Physical Read 감소와 Logical I/O·CPU 반복 비용을 구분한다.
  • LEADING과 ORDERED의 우선순위·제약을 설명한다.
  • USE_NL이 지정 Row Source를 Inner로 사용할 때 의미가 있음을 설명한다.
  • USE_NL_WITH_INDEX의 Join Predicate Key 조건을 설명한다.
  • Alias·Query Block을 정확히 지정해야 하는 이유를 설명한다.
  • DISPLAY_CURSOR의 ALLSTATS LAST·PREDICATE·ALIAS·OUTLINE·NOTE로 Hint 적용을 검증한다.
  • Index·NL·BATCHED Plan이 결과 순서를 보장하지 않음을 설명한다.
  • Hint Plan과 기본 Plan을 동일 Bind·Fetch 조건에서 비교한다.

본문

현재 NL 실행 구조

기존에 익숙한 형태

예시·텍스트
NESTED LOOPS
  Outer Row Source
  TABLE ACCESS BY INDEX ROWID INNER_TABLE
    INDEX RANGE SCAN INNER_INDEX

논리적 흐름입니다.

도식
Outer Row
→ Inner Index Probe
→ ROWID 획득
→ Inner Table Row 읽기

두 개의 NESTED LOOPS가 보이는 형태

Oracle의 현재 NL 구현에서는 다음 Plan이 나타날 수 있습니다.

예시·텍스트
NESTED LOOPS
  NESTED LOOPS
    TABLE ACCESS BY INDEX ROWID OUTER_TABLE
      INDEX RANGE SCAN OUTER_INDEX
    INDEX RANGE SCAN INNER_INDEX
  TABLE ACCESS BY INDEX ROWID BATCHED INNER_TABLE

각 Row Source의 역할입니다.

도식
안쪽 NESTED LOOPS
  Outer Row Source
  + Inner Index Entry·ROWID
  → 중간 Row Source 생성

바깥 NESTED LOOPS
  중간 ROWID
  + Inner Table Row
  → 필요한 Inner Column을 포함한 결과 생성

두 NL을 각각 별개의 업무 Join으로 단정하지 않습니다. Object Alias·Predicate·Operation Parent-Child 관계를 통해 하나의 Join에서 Index Probe와 Table Access가 분리된 구조인지 확인합니다.

Covering Index

필요한 Inner Predicate와 반환 Column을 모두 Index에서 해결하면 Table Access가 생략될 수 있습니다.

예시·텍스트
NESTED LOOPS
  Outer Row Source
  INDEX RANGE SCAN INNER_COVERING_INDEX

이 경우에도 Index Entry 수와 Leaf Scan 비용은 남습니다. Covering을 위해 Column을 과도하게 추가하면 Index 폭·LEAF_BLOCKS·DML·Redo·Buffer Cache 비용이 증가할 수 있습니다.


TABLE ACCESS BY INDEX ROWID BATCHED

기본 의미

Index Scan은 후보 Row의 ROWID를 반환합니다.

도식
Index Entry
→ ROWID
→ Table Block·Row

BATCHED Table Access는 여러 ROWID를 모아 Table Block 방문 순서를 효율화하는 방식입니다.

예시·텍스트
ROWID를 하나씩 즉시 방문
vs
ROWID를 일정 단위로 모아 관련 Block 접근을 조정

대표 목적입니다.

  • 같은 Table Block의 반복 방문 감소 가능
  • Clustering이 좋지 않은 Range Scan의 Block 접근 완화
  • ROWID 요청 처리 오버헤드 완화
  • Buffer Cache 재사용 가능성 향상

해결하지 못하는 것

예시·텍스트
BATCHED
  ≠ Outer Starts 감소
  ≠ Index Leaf 후보 감소
  ≠ 후보 ROWID 제거
  ≠ Table Filter 제거
  ≠ Table Access 제거

다음 Plan은 Table Row를 계속 읽는다는 뜻입니다.

예시·텍스트
TABLE ACCESS BY INDEX ROWID BATCHED ORDERS
  INDEX RANGE SCAN ORDERS_CUSTOMER_IX

Index 후보 100,000건 중 Table Filter 후 1,000건만 반환한다면 Batching으로 순서는 개선될 수 있어도 대량 Table 방문 자체는 남습니다.

Operation 이름만으로 단정하지 않는 항목

BATCHED 표기만으로 다음을 확정하지 않습니다.

  • 고정 Batch 크기
  • 특정 Multiblock Read 방식
  • 실제 Physical Read Call 수
  • 결과 Row 순서
  • 성능 개선 크기

확인은 Starts·A-Rows·Buffers·Reads·A-Time과 Wait·Trace가 필요합니다.


Buffer Cache 재사용과 반복 비용

Outer에서 같은 Inner Key 또는 인접 Key가 반복되면 같은 Index Branch·Leaf·Table Block을 다시 방문할 수 있습니다.

예시·텍스트
Outer Key
10, 10, 10, 20, 20

Block이 Cache에 남아 있으면 Physical Read가 줄 수 있습니다. 그러나 다음 비용은 남습니다.

  • Consistent Buffer Get
  • 반복 Index Probe
  • CPU·Predicate 평가
  • Latch·Mutex 관련 처리
  • 결과 Row 생성·전달
도식
Physical Read 0
  ≠ NL 반복 비용 0

Buffers·CPU·Starts가 큼
  → Cache Hit 상태에서도 비효율 가능

Outer Key 정렬

Outer를 Join Key 순으로 정렬하면 인접 Probe의 Cache 재사용성이 좋아질 수 있습니다.

하지만 다음 비용이 추가됩니다.

  • Sort CPU
  • Workarea Memory
  • TEMP Spill 가능성
  • 첫 행 응답 지연

정렬은 일반 규칙이 아니라 측정할 대안입니다. 결과 순서가 목적이면 최종 ORDER BY를 명시합니다.


Join Order Hint

LEADING

SQL
SELECT /*+ LEADING(c o) */
       ...
FROM   customers c
JOIN   orders o
  ON   o.customer_id=c.customer_id;

LEADING(c o)는 c에서 시작해 o를 연결하는 Join Order를 유도합니다.

주의합니다.

  • Join Graph 의존성 때문에 지정 순서로 먼저 Join할 수 없으면 무시될 수 있음
  • 서로 충돌하는 여러 LEADING Hint는 무시될 수 있음
  • ORDERED가 함께 있으면 ORDERED가 LEADING보다 우선함
  • Query Transformation 후 Query Block·Alias가 달라질 수 있음

ORDERED

SQL
SELECT /*+ ORDERED */
       ...
FROM   customers c
JOIN   orders o
  ON   o.customer_id=c.customer_id
JOIN   order_items i
  ON   i.order_id=o.order_id;

ORDERED는 FROM 절의 작성 순서를 Join Order로 사용하도록 유도합니다.

도식
ORDERED
  → SQL Text 순서와 강하게 결합

LEADING
  → 원하는 선두·부분 순서를 명시

SQL Refactoring으로 FROM 절이 바뀌면 ORDERED의 의미도 달라질 수 있습니다.


Join Method Hint

USE_NL

SQL
SELECT /*+ LEADING(c o) USE_NL(o) */
       ...
FROM   customers c
JOIN   orders o
  ON   o.customer_id=c.customer_id;

USE_NL(o)는 o를 다른 Row Source와 연결할 때 o를 Inner로 하는 NL Join을 유도합니다.

중요합니다.

도식
USE_NL(o)
  → Join Order를 직접 지정하지 않음
  → o가 Outer이면 Hint가 의도대로 적용되지 않을 수 있음

따라서 LEADING·ORDERED와 함께 대안 Plan을 시험합니다.

NO_USE_NL

SQL
SELECT /*+ NO_USE_NL(o) */
       ...

o를 Inner로 하는 NL Join 후보를 제외하도록 유도합니다. Hash·Merge가 반드시 선택된다는 보장은 없으며 다른 유효한 후보 중 Cost를 비교합니다.

USE_NL_WITH_INDEX

SQL
SELECT /*+
          LEADING(c o)
          USE_NL_WITH_INDEX(o orders_customer_ix)
       */
       c.customer_id,
       o.order_id
FROM   customers c
JOIN   orders o
  ON   o.customer_id=c.customer_id;

의미입니다.

예시·텍스트
o를 NL Inner로 사용
+
ORDERS_CUSTOMER_IX의 Key 중 적어도 하나에
Join Predicate를 탐색 Key로 사용

공식 조건입니다.

  • Index를 지정하지 않으면, Join Predicate를 Index Key로 사용할 수 있는 어떤 Index가 필요
  • Index를 지정하면, 그 Index가 Join Predicate 중 적어도 하나를 Key로 사용할 수 있어야 함
  • Index 존재만으로 충분하지 않음
  • Join Predicate·Column 순서·Data Type·Transformation을 확인

Access Path Hint와의 조합

SQL
SELECT /*+
          LEADING(c o)
          USE_NL(o)
          INDEX(o orders_customer_date_ix)
       */
       ...

각 Hint가 제어하는 가설입니다.

도식
LEADING(c o)
  → c를 먼저 읽고 o를 다음에 연결

USE_NL(o)
  → o를 NL Inner로 연결

INDEX(o idx)
  → o의 Access Path로 지정 Index Scan 유도

하나의 Hint가 Join Order·Method·Access Path를 모두 결정한다고 생각하지 않습니다.

강제 Plan이 더 느릴 수 있는 대표 사례입니다.

  • Outer A-Rows가 예상보다 큼
  • 지정 Index Range가 넓음
  • 많은 후보 ROWID
  • Table Filter에서 대량 탈락
  • 인기 Bind 값
  • Full Fetch 작업
  • Index Clustering이 불리함

Alias와 Query Block

Alias 사용

SQL
FROM orders o

Alias가 있으면 Hint에서도 o를 사용합니다.

SQL
/*+ USE_NL(o) INDEX(o orders_customer_ix) */

다음은 대상이 일치하지 않을 수 있습니다.

SQL
/*+ USE_NL(orders) */
FROM orders o

Query Block

Inline View·CTE·Subquery·Transformation이 있으면 Object Alias가 여러 Query Block에 존재하거나 사라질 수 있습니다.

SQL
SELECT /*+ QB_NAME(main) */
       ...
FROM (
    SELECT /*+ QB_NAME(order_qb) */
           ...
    FROM orders o
) v;

확인 도구입니다.

  • QB_NAME
  • +ALIAS
  • +OUTLINE
  • Query Block Name
  • Object Alias
  • Hint Report의 Used·Unused·Invalid

Hint Text만 보고 적용됐다고 판단하지 않습니다.


DBMS_XPLAN으로 검증

실행 통계를 수집합니다.

SQL
SELECT /*+
          LEADING(c o)
          USE_NL_WITH_INDEX(o orders_customer_ix)
          GATHER_PLAN_STATISTICS
       */
       ...
FROM   customers c
JOIN   orders o
  ON   o.customer_id=c.customer_id
WHERE  c.grade=:grade;

실제 Cursor를 확인합니다.

SQL
SELECT *
FROM TABLE(
  DBMS_XPLAN.DISPLAY_CURSOR(
    :sql_id,
    :child_no,
    'ALLSTATS LAST +PREDICATE +ALIAS +OUTLINE +NOTE'
  )
);

Format별 확인 내용

Format확인 내용
ALLSTATS LAST마지막 실행의 Starts·A-Rows·Buffers·Memory
+PREDICATEAccess·Filter Predicate와 Operation 위치
+ALIASQuery Block·Object Alias
+OUTLINEPlan을 재현하는 Outline Hint
+NOTEAdaptive Plan·Dynamic Statistics·Profile 등
Hint ReportHint의 Used·Unused·Invalid와 이유

실제 적용 판단

확인합니다.

  1. 의도한 Join Order인가?
  2. Hint 대상 Object가 Inner인가?
  3. NL Operation이 실제로 사용됐는가?
  4. 지정 Index가 사용됐는가?
  5. USE_NL_WITH_INDEX의 Join Predicate Key 조건을 충족했는가?
  6. Starts·A-Rows·Buffers·Reads가 개선됐는가?
  7. 결과 Row와 업무 의미가 같은가?
  8. 다른 Bind·Full Fetch에서 회귀하지 않는가?

Plan 비교

후보 A: NL·Index·BATCHED

예시·텍스트
NESTED LOOPS
  TABLE ACCESS BY INDEX ROWID CUSTOMERS
    INDEX RANGE SCAN CUSTOMERS_GRADE_IX
  TABLE ACCESS BY INDEX ROWID BATCHED ORDERS
    INDEX RANGE SCAN ORDERS_CUSTOMER_IX

후보 B: Hash Join

예시·텍스트
HASH JOIN
  TABLE ACCESS FULL CUSTOMERS
  TABLE ACCESS FULL ORDERS

공정한 비교 조건입니다.

  • 동일한 SQL 결과
  • 동일한 Bind 값·Data Type
  • 동일한 Statistics·Optimizer Parameter
  • 동일한 Parallel Degree
  • 동일한 Fetch 범위
  • 동일한 Projection
  • 유사한 Cache 상태

측정합니다.

항목NL PlanHash Plan
StartsInner 반복 횟수Build·Probe 실행 횟수
A-RowsOuter·Index 후보·Table 결과Build·Probe 입력·결과
Buffers반복 Index·ROWID AccessScan·Hash 처리
ReadsCache Miss·Table AccessScan·Spill I/O
A-TimeFirst Row·반복 누적Build 후 Probe·전체 완료
Used-TmpSort가 있으면 발생Hash Spill 시 발생

결과 순서

SQL 결과 순서는 ORDER BY가 있을 때만 보장됩니다.

다음은 순서를 보장하지 않습니다.

  • Index Range Scan
  • Index Descending Scan만 존재
  • NL Join
  • BATCHED ROWID Access
  • 현재 Version에서 우연히 같은 순서
  • Hint로 Access Path 고정

결정적 정렬 예입니다.

SQL
ORDER BY order_date DESC,
         order_id DESC

order_date가 같은 Row의 순서를 안정화하려면 order_id 같은 Unique Tie-Breaker를 추가합니다.

Hint Plan을 변경하면 행 생산 순서도 달라질 수 있으므로 결과 순서에 Plan 특성을 이용하지 않습니다.


안전한 적용 절차

예시·텍스트
1. 실제 SQL_ID·Child·Bind·Fetch 목표를 고정한다.
2. 두 개의 NL이 하나의 Index·Table Inner Access를 분리한 구조인지 확인한다.
3. Inner Starts·Index A-Rows·Table A-Rows를 비교한다.
4. BATCHED 아래 Table Buffers·Filter 탈락을 확인한다.
5. 결과 순서는 ORDER BY로 정의한다.
6. LEADING 또는 ORDERED로 Join Order 가설을 시험한다.
7. USE_NL 대상이 실제 Inner Alias인지 확인한다.
8. USE_NL_WITH_INDEX의 Join Predicate Key 조건을 확인한다.
9. DISPLAY_CURSOR와 Hint Report로 실제 적용 여부를 확인한다.
10. 기본 Plan과 동일 Bind·Fetch Runtime 및 다른 Bind 회귀를 비교한다.

자주 혼동하는 판단

혼동정확한 기준
두 개 NL이면 업무 Join이 두 번하나의 Join에서 Index ROWID 생성과 Table Access가 분리될 수 있음
BATCHED면 Table Access가 없음ROWID 기반 Table Access를 Batch로 처리
BATCHED면 Outer 반복 감소Starts는 Outer·Join Cardinality가 결정
Physical Read가 0이면 비용이 없음Logical I/O·CPU·Predicate 반복은 남음
USE_NL이 Join Order도 결정USE_NL은 Method, LEADING·ORDERED는 Order
USE_NL 대상이 Outer여도 적용지정 Object가 Inner일 때 의미가 있음
USE_NL_WITH_INDEX는 Index 존재만 확인Join Predicate를 Index Key로 사용할 수 있어야 함
ORDERED와 LEADING이 함께면 LEADING 우선ORDERED가 LEADING을 덮어씀
Hint 문법이 맞으면 적용됨Alias·Query Block·Transformation·유효성 확인
Index·NL Plan이면 정렬 보장결과 순서는 ORDER BY만 보장

복습 문제

답을 생각한 뒤 정답과 근거를 펼쳐 보세요.

01

최신 NL Plan에서 두 개의 NESTED LOOPS가 나타날 수 있는 이유를 설명하시오.

정답과 근거

두 개의 NL

  • 첫 NL은 Outer Row와 Inner Index Entry·ROWID를 결합합니다.
  • 두 번째 NL은 그 ROWID로 Inner Table Row를 읽습니다.
  • 하나의 업무 Join에서 Index Probe와 Table Access가 분리돼 표시될 수 있습니다.
02

TABLE ACCESS BY INDEX ROWID BATCHED가 개선하는 부분과 제거하지 못하는 작업량을 설명하시오.

정답과 근거

BATCHED

  • 여러 ROWID의 Table Block 방문 순서를 조정해 같은 Block 반복 방문과 접근 오버헤드를 완화할 수 있습니다.
  • Outer Starts, 넓은 Leaf Scan, 후보 ROWID 수, Table Filter 탈락, 대량 결과는 제거하지 못합니다.
03

Physical Read가 적어도 NL 반복 비용이 클 수 있는 이유를 설명하시오.

정답과 근거

Cache Hit와 반복 비용

  • Cache에 Block이 있으면 Physical Read는 줄 수 있습니다.
  • 그러나 Consistent Get, 반복 Index Probe, CPU, Predicate 평가, Row 생성 비용은 계속 발생합니다.
  • Starts·Buffers·CPU·Elapsed를 함께 봅니다.
04

Outer Join Key 정렬이 Cache 재사용에 도움을 줄 수 있지만 항상 좋은 것은 아닌 이유를 설명하시오.

정답과 근거

Outer Key 정렬

  • 같은·인접 Key Probe가 연속되면 Cache 재사용성이 좋아질 수 있습니다.
  • 하지만 Sort CPU·Memory·TEMP·첫 행 지연이 추가됩니다.
  • 대안 Plan으로 실측해야 하며 결과 순서는 최종 ORDER BY로 정의합니다.
05

LEADING과 ORDERED의 차이와 함께 사용했을 때의 우선순위를 설명하시오.

정답과 근거

LEADING·ORDERED

  • LEADING은 지정 Row Source를 선두로 하는 Join Order를 유도합니다.
  • ORDERED는 FROM 절의 작성 순서를 사용하도록 유도합니다.
  • 둘을 함께 지정하면 ORDERED가 LEADING보다 우선합니다.
06

USE_NL(o)가 Join Order를 직접 결정하지 않는 이유를 설명하시오.

정답과 근거

USE_NL

  • USE_NL은 지정 Row Source를 다른 Row Source와 연결할 때 Inner로 사용하는 Join Method Hint입니다.
  • Join Order를 정하지 않으므로 대상이 Outer가 되면 의도대로 적용되지 않을 수 있습니다.
  • LEADING·ORDERED와 함께 검증합니다.
07

USE_NL_WITH_INDEX(o idx)가 적용되기 위한 Index 조건을 설명하시오.

정답과 근거

USE_NL_WITH_INDEX

  • 지정 Object가 NL Inner여야 합니다.
  • 지정 Index 또는 후보 Index가 Join Predicate 중 적어도 하나를 Index Key로 사용할 수 있어야 합니다.
  • Index 존재만으로는 충분하지 않습니다.
08

Alias·Query Block을 정확히 지정해야 하는 이유를 설명하시오.

정답과 근거

Alias·Query Block

  • Alias가 있으면 Hint는 실제 Alias를 기준으로 Object를 식별합니다.
  • CTE·Inline View·Transformation이 있으면 같은 Object가 다른 Query Block에 있거나 Merge될 수 있습니다.
  • QB_NAME·ALIAS·OUTLINE·Hint Report로 대상을 확인합니다.
09

ALLSTATS LAST +PREDICATE +ALIAS +OUTLINE +NOTE에서 각각 확인할 내용을 설명하시오.

정답과 근거

DISPLAY_CURSOR Format

  • ALLSTATS LAST: 마지막 실행 Starts·A-Rows·Buffers·Memory
  • PREDICATE: Access·Filter 조건과 위치
  • ALIAS: Query Block·Object Alias
  • OUTLINE: Plan 재현에 관련된 Hint
  • NOTE: Adaptive Plan·Dynamic Statistics·Profile 등의 정보입니다.
10

Hint Plan을 기본 Plan과 공정하게 검증하고 결과 순서를 보장하는 절차를 설명하시오.

정답과 근거

공정한 검증 - 동일 결과·Bind·Data Type·Statistics·Parallel·Fetch 범위를 사용합니다. - Hint Report와 실제 Join Order·Method·Index를 확인합니다. - Starts·A-Rows·Buffers·Reads·A-Time·TEMP를 비교합니다. - 다른 Bind와 Full Fetch 회귀를 확인합니다. - 결과 순서는 결정적인 ORDER BY로 보장합니다.