Oracle Hint
=> "옵티마이저가 잘못된 판단을 한다고 생각될 때에는 Hint를 주어 올바른 방향으로 수행하도록 유도할 수 있다"
1) 개념 한 줄 요약
NLJ = 중첩 for문
바깥(Driving)에서 한 행씩 뽑고 → 안쪽(Driven)을 인덱스로 찔러 매칭 행을 찾는다.
2) Driving / Driven 의미와 선택 기준
Driving: 먼저 읽는 테이블(바깥 루프). “조인당하는 메인”이 아니라 루프의 기준.
Driven: 나중에 읽는 테이블(안쪽 루프). 조인 키 인덱스가 있어야 빠르다.
실무 선택 규칙
Driving = 필터로 작아지는 쪽(선택도 높음, Top-N, 기간 좁음)
Driven = 조인키(+ 추가 필터) 인덱스 보유(선두 = + 다음 컬럼 범위)
3) 조인 순서가 왜 중요한가
NLJ 비용 ≈ (Driving 결과 행수) × (Driven 탐색 비용)
→ 바깥을 최대한 작게, 안쪽은 인덱스 Range Scan으로.
4) 힌트: ORDERED, USE_NL
반드시 USE_NL가 있어야 NLJ가 되는 건 아님. 옵티마이저가 상황 따라 NL/해시를 선택.
계획이 자꾸 틀리면:
/+ ORDERED USE_NL(별칭) INDEX(별칭 인덱스명) / 로 순서·방식·인덱스를 부드럽게 유도.
5) 인덱스는 NLJ의 생명선
Driven 쪽에 조인키(+ 필터 컬럼) 복합 인덱스가 있어야 한 번 탐색 비용이 작다.
SARGable: 컬럼에 함수/형변환 금지, 반개구간(>= AND <) 사용.
6) WHERE 필터가 있을 때의 차이
Driving 필터: 바깥 행수↓ → 전체 루프↓(효과 큼)
Driven 필터: 인덱스와 결합 시 한 번 탐색 범위↓
둘 다 컬럼 가공 금지가 기본
7) 은행 도메인 예시 (간결·핵심만)
7-1. 이상적 NLJ (고객⇢거래, 기간 좁음)
/*+ ORDERED USE_NL(t) INDEX(t IDX_TXN_ACCT_DT) */
SELECT t.txn_id, t.acct_id, t.txn_amt
FROM customers c, transactions t
WHERE c.region = :r -- Driving을 작게 만드는 강한 필터
AND t.acct_id = c.acct_id -- 조인 (Driven)
AND t.txn_dt >= :d1
AND t.txn_dt < :d2; -- 반개구간 (Range Scan 유도)
Driving = customers(지역으로 소수만)
Driven = transactions((acct_id, txn_dt) 인덱스 가정) → INDEX RANGE SCAN
**7-2. 잘못된 패턴 → 리라이트
**-- Bad: Driven 컬럼 가공으로 인덱스 무력화
WHERE t.acct_id = c.acct_id
AND TO_CHAR(t.txn_dt,'YYYYMM') = :yyyymm; -- X
-- Good: SARGable 반개구간
WHERE t.acct_id = c.acct_id
AND t.txn_dt >= :m
AND t.txn_dt < :m_next; -- O
8) 실무 체크 포인트(짧게)
Driving: 강한 필터/Top-N으로 작게
Driven: 조인키(선두 =) + 범위 컬럼 인덱스, SARGable
힌트: 틀릴 때만 ORDERED + USE_NL(Driven)(+ INDEX)
피해야 할 것: Driven 컬럼에 TO_CHAR/TO_NUMBER, TRUNC, %값% 등
플랜에서 볼 것:
NESTED LOOPS 아래 Driven의 INDEX RANGE SCAN 유무
Starts(내부 호출 수) 급증, BUFFERS 급증 지점
한 줄 결론
작게 뽑는 테이블을 Driving으로, Driven은 인덱스로 ‘정확히’ 찌른다.
안쪽 컬럼 가공 금지, 반개구간·UNION ALL로 Range Scan을 확보하고, 필요 시 ORDERED/USE_NL로 의도를 고정하자.
1) Hash Join 한 줄 정의 & 언제 쓰나
정의: 작은 쪽을 메모리에 해시 테이블(build)로 만들고, 큰 쪽을 연속 스캔(probe)하며 해시로 매칭.
→ 인덱스 없어도 빠름, 랜덤 I/O 대신 순차 스캔.
바로 쓰는 기준
조인키 인덱스가 없음 / 컬럼 가공으로 인덱스 못 탐
인덱스가 있어도 Driving→Driven 랜덤 접근이 많아 NLJ가 비쌀 때
둘 다 큼(필터 후 수십만~수백만 로우), 리포트/일회성 집계(OLAP 스타일)
반대로, 바깥(Driving)이 매우 작고 Driven에 인덱스가 잘 잡히면 NLJ가 유리.
2) 인덱스가 뭔지 & 있는지 빠르게 확인
인덱스: 특정 컬럼(들)을 정렬된 색인으로 만들어 ROWID를 빠르게 찾는 구조.
존재 확인
-- 내 스키마: 테이블/컬럼 인덱스 확인
SELECT index_name
FROM user_ind_columns
WHERE table_name = 'TRANSACTIONS'
AND column_name = 'ACCT_ID';
복합 인덱스 순서
SELECT index_name, column_position, column_name
FROM user_ind_columns
WHERE table_name = 'TRANSACTIONS'
ORDER BY index_name, column_position;
3) 실무 힌트 세트 (짧고 강하게)
기본 유도: /+ ORDERED USE_HASH(별칭) / (+ 필요 시 FULL(별칭))
Build 쪽(해시 테이블)은 작은 집합이 유리. 필요하면 /+ SWAP_JOIN_INPUTS(별칭) /로 바꾸기.
SARGable 유지(컬럼 가공 금지), 반개구간(>= AND <)로 범위 좁히기.
조인키 타입 일치(암시적 변환 방지).
큰 해시가 TEMP로 스필하면 느려짐 → 지나치게 큰 집합이면 필터 더 좁히거나 시간대 조정/병렬은 DBA 협의.
4) 은행 예시 (오라클 전통 문법, 최소 예시만)
A. 조인키 인덱스가 없거나 NLJ 랜덤 I/O가 큰 경우
/*+ ORDERED USE_HASH(t) FULL(t) */
SELECT t.txn_id, t.acct_id, t.txn_amt
FROM accounts a, transactions t
WHERE t.acct_id = a.acct_id
AND a.region = :r -- Driving을 작게(강한 필터)
AND t.txn_dt >= :d1
AND t.txn_dt < :d2; -- 반개구간 (SARGable)
해석: accounts를 먼저 작게 만든 뒤, transactions는 해시 조인 + 연속 스캔. 인덱스 없거나 NLJ가 랜덤 I/O 폭탄이면 이쪽.
B. 대용량 리포트/일회성 집계(OLAP 성격)
/*+ ORDERED USE_HASH(t) FULL(a) FULL(t) */
SELECT a.branch_id, SUM(t.txn_amt) AS sum_amt
FROM accounts a, transactions t
WHERE t.acct_id = a.acct_id
AND t.txn_dt >= :m
AND t.txn_dt < :m_next
GROUP BY a.branch_id;
해석: 월간 대량 집계. 둘 다 큼 → 해시 조인 + 풀스캔이 안정적.
C. OUTER JOIN(+) + Hash Join 유도
/*+ ORDERED USE_HASH(k) */
SELECT a.acct_id, k.kyc_level
FROM accounts a, kyc_info k
WHERE k.acct_id(+) = a.acct_id -- LEFT OUTER JOIN
AND a.status = 'ACTIVE';
해석: 활성 계좌 기준, KYC는 있을 수도 없을 수도. USE_HASH(k)로 k를 해시로 붙임.
5) “크다”의 감(룰 오브 썸)
절대 기준은 없음. 아래는 체감용:
NLJ 적합: Driving 결과 수천~수만 행 이하 + Driven에 좋은 인덱스 → 랜덤 접근 총량이 작음.
Hash 적합: Driving 결과가 수십만 이상이거나, Driven 인덱스가 무의미/없음, 양쪽 모두 큼.
애매하면 /+ gather_plan_statistics / + DBMS_XPLAN.DISPLAY_CURSOR('ALLSTATS LAST')로 A-Rows/BUFFERS 비교.
6) OLTP vs OLAP, 일회성 추출에서는?
OLTP(거래계): 짧고 잦은 소량 조회. NLJ + 인덱스 선호, 풀스캔 자제.
OLAP(분석/리포트): 큰 집합, 집계. Hash + 풀스캔이 안정적.
일회성 대량 추출(업무시간 외 권장): Hash Join OK. 다만
필터 확실히(기간/지점 등),
컬럼 가공 금지,
가능하면 오프피크. (병렬/작업창은 DBA와 협의)
7) 언제 Hash, 언제 NL — 초간단 결정표
상황 추천
Driving 작고 Driven 인덱스 좋음 NLJ
조인키 인덱스 없음/무의미 Hash
둘 다 큼(필터 후 수십만+) Hash
Top-N/즉시반환 필요 NLJ 쪽이 유리
함수/형변환으로 인덱스 무력화 리라이트(SARGable) 또는 Hash
8) 마지막 체크리스트
ORDERED + USE_HASH(별칭) 로 유도, 필요 시 FULL(별칭)
Build = 작은 쪽, 필요 시 SWAP_JOIN_INPUTS
SARGable / 반개구간 / 타입 일치
A-Rows/BUFFERS로 실제 비교 (ALLSTATS LAST)
OLTP는 NLJ 위주, OLAP/일회성 대량은 Hash 위주
한 줄: 인덱스가 힘 못 쓰거나 랜덤 I/O가 커질 땐 Hash로 “크게 훑고 맞춘다”.
필터로 집합을 줄이고, 힌트로 순서와 방식만 가볍게 고정해주면 됩니다.
AS-IS
SELECT EMP_NO, FIRST_NAME, LAST_NAME
FROM EMPLOYEES
WHERE EMP_NO < 20000
AND (SELECT MAX(SALARY)
FROM SALARIES
WHERE EMP_NO = EMPLOYEES.EMP_NO) > 140000;

TO-BE
SELECT e.emp_no, e.first_name, e.last_name
FROM employees e
WHERE e.emp_no < 20000
AND EXISTS (
SELECT 1 -- 단순 존재여부만 보는거라 더미값. 1말고 어떤 값을 써도 상관없음
FROM salaries s
WHERE s.emp_no = e.emp_no
AND s.salary > 140000
)

