테마
06화 · 파고들기
필요할 때 찾아보는 문서다. 처음 읽을 때는
1_사건.md만으로 충분하다.
| 찾는 것 | 바로 가기 |
|---|---|
| 조인 3종을 한 문장으로 | §1 세 가지 답 |
Starts로 알 수 있는 것 | §2 Starts |
| NL의 총일량은 어떻게 계산하나 | §3 NL |
| 해시 조인의 build / probe | §4 해시 |
| 손익분기점을 직접 재는 법 | §5 손익분기점 |
PGA · work area 3등급 · Used-Tmp | §6 work area |
| 힌트는 무엇을 하는가 · 왜 늙는가 | §7 힌트 |
| 「힌트 유효기간 대장」 양식 | §8 대장 |
| 측정값 전부 | §9 측정 |
| 공식문서 좌표 | §10 좌표 |
1. 조인 3종 — 같은 질문의 세 가지 답
조인 대상은 언제나 두 집합이다. 먼저 읽는 쪽이 outer(선행), 거기에 맞춰 찾는 쪽이 inner(후행)다. 세 방식은 모두 한 질문에 답하는 방법이다.
바깥 한 건에 맞는 안쪽을, 어디서 찾을 것인가?
| 방식 | 안쪽을 어디서 찾나 | 안쪽에 필요한 것 | 비용의 성질 |
|---|---|---|---|
| NL (Nested Loops) | 매번 원래 자리로 가서 | 조인 컬럼 인덱스 | outer 건수 N에 비례 |
| 해시 (Hash Join) | PGA에 복사해 둔 해시 테이블에서 | 없음. 대신 메모리 | 양쪽을 한 번씩 읽는 값 |
| 소트 머지 (Sort Merge) | 양쪽을 줄 세워 나란히 놓고 | 없음. 대신 정렬 공간 | 양쪽 정렬 비용 + 한 번씩 훑기 |
결정적 차이 — 무엇이 N에 비례하는가
NL : outer 스캔 + N × (inner 한 건 찾는 비용) ← N 이 곱해진다
해시 : outer 스캔 + inner 스캔 + 해시 테이블 적재 ← N 이 곱해지지 않는다
소트 머지 : outer 스캔 + inner 스캔 + 양쪽 정렬 ← N 이 곱해지지 않는다06화의 핵심은 이 한 줄이다. NL만 N이 곱해진다. 그래서 데이터가 늘어나면 NL 비용만 함께 커진다.
등치(=) 조건이 아닐 때
해시 조인은 등치 조건에서만 쓸 수 있다. 해시 함수는 "같은 값"끼리만 같은 자리로 보내기 때문이다. BETWEEN, >, < 같은 비등치 조인은 NL이나 소트 머지로만 처리할 수 있다. (이 교재에서는 선분이력을 다룰 때 비등치 조인을 만난다.)
2. Starts — 왕복 횟수
DISPLAY_CURSOR(FORMAT=>'ALLSTATS LAST')의 첫 열로, 그 단계가 몇 번 실행됐는지를 보여 준다.
| 자리 | 정상값 | 1이 아니면 |
|---|---|---|
| outer 쪽(NL의 위쪽 자식) | 1 | 그 위에 또 조인이 있다 |
| inner 쪽(NL의 아래쪽 자식) | outer의 A-Rows와 같다 | 그게 왕복 횟수다 |
| 스칼라 서브쿼리 | 보통 값의 종류 수 | 캐시가 안 먹으면 행 수만큼 뛴다(→ 07화) |
SORT JOIN(소트 머지의 아래쪽) | 조인 키 그룹 수 | 왕복이 아니라 되감기다(싸다) |
06화 실측
xplan
| 4 | NESTED LOOPS | | 1 | 1000K| ... |
| 6 | TABLE ACCESS FULL | T_ORD_BIG | 1 | 1000K| ... | ← outer, Starts 1
|* 7 | INDEX UNIQUE SCAN | PK_M_CUS | 1000K| 1000K| ... | ← inner, Starts 1,000,000
| 8 | TABLE ACCESS BY INDEX ROWID | M_CUS | 1000K| 1000K| ... | ← inner, Starts 1,000,000Starts와 A-Rows를 나란히 놓고 보자. outer가 100만 행을 내보냈고, inner가 정확히 100만 번 열렸다.
📌
Starts는 인덱스로 줄지 않는다 06화에서
M_CUS에 커버링 인덱스를 만들어 봤다.Buffers는 1,026,849 → 36,852로 28배 줄었다. 그런데Starts는 1,000,000 그대로였다.인덱스는 왕복 한 번에 드는 비용을 줄인다. 왕복 횟수는 조인 방식을 바꿔야만 줄어든다.
3. NL 조인의 총일량
총일량 = outer 스캔 비용 + N × (inner 한 건을 찾는 비용)06화 실측 분해 (Q2 rev00)
| 자리 | 오퍼레이션 | Starts | Buffers | 건당 |
|---|---|---|---|---|
| outer | TABLE ACCESS FULL T_ORD_BIG | 1 | 5,826 | — |
| inner ① 주소 찾기 | INDEX UNIQUE SCAN PK_M_CUS | 1,000,000 | 21,011 | 0.021 |
| inner ② 본문 읽기 | TABLE ACCESS BY INDEX ROWID M_CUS | 1,000,000 | 1,000,000 | 1.000 |
| 스칼라 서브쿼리 | M_RGN | 8 | 12 | — |
| 1,026,849 |
inner 한 건을 찾는 데 약 1.02블록이 든다. 그 자체는 싸다. 비싼 이유는 그 1.02를 100만 번 곱한다는 데 있다.
⚠️ "한 건에 4~5블록"이라는 흔한 설명
교과서는 보통 "루트 → 브랜치 → 리프 → 테이블"로 세어 건당 4~5블록이라고 설명한다. 실측값은 1.02였다. 이유는 두 가지다.
PK_M_CUS가BLEVEL 1 · LEAF_BLOCKS 6으로 아주 작다. 인덱스 전체가 7블록이다- 실행계획에
NESTED LOOPS가 두 겹으로 나타나 있다. 오라클의 NL 조인 배치(batching)다. inner 쪽 탐색을 모아서 처리하므로, 같은 블록을 연달아 읽을 때 논리적 읽기가 한 번으로 묶인다그래서 건당 비용은 외우는 값이 아니라 재는 값이다. 다만 N이 곱해진다는 성질은 언제나 같다.
4. 해시 조인의 build와 probe
[1] build : 작은 쪽을 한 번 읽어, 조인 키에 해시 함수를 걸어
PGA 안의 해시 테이블(버킷 배열)에 올린다
[2] probe : 큰 쪽을 흘려보내며, 각 행의 조인 키에 같은 해시 함수를 걸어
해당 버킷만 뒤진다해시 함수는 같은 값을 반드시 같은 버킷으로 보낸다. 그래서 한 버킷만 보면 된다. (다른 값이 같은 버킷에 들어올 수는 있어서, 버킷 안에서는 실제 값을 다시 비교한다.)
어느 쪽이 build가 되는가 — 실측으로 확인
LEADING에 적은 첫 테이블이 build가 된다. 06화에서 두 방향을 다 재 봤다.
| 힌트 | build | Buffers | OMem | Used-Mem |
|---|---|---|---|---|
LEADING(c o) USE_HASH(o) | M_CUS 2,000행 | 5,868 | 1,423K | 1,679K (0) |
LEADING(o c) USE_HASH(c) | T_ORD_BIG 100만 행 | 5,868 | 60M | 77M (0) |
📌 build를 잘못 잡으면 블록이 아니라 메모리에서 드러난다
Buffers는 5,868로 똑같다. 두 실행계획의 차이는 쓴 메모리가 45배라는 것 하나뿐이다. 그리고 메모리가 모자라는 순간 이 차이가 시간으로 터진다(§6).작은 쪽을 build로 잡는다. 통계가 정확하면 옵티마이저가 알아서 그렇게 한다.
메모리에 다 못 올라가면 — Grace 분할
build 쪽이 work area보다 크면 오라클은 양쪽을 같은 해시 함수로 파티션해서 TEMP에 나눠 쓰고, 짝이 맞는 파티션끼리 하나씩 올려 조인한다. 실행계획에 Used-Tmp가 나타나는 게 바로 이때다. 자세한 내용은 §6에, 깊은 이야기는 09화에 있다.
5. 손익분기점을 직접 재는 법
NL이 이기는 구간은 분명히 있다. 외울 숫자는 없지만 재는 방법은 있다.
두 식을 세운다
NL 비용 ≈ outer 스캔 + N × (inner 건당 비용) ← 기울기가 있다
해시 비용 ≈ outer 스캔 + inner 스캔 + 적재 ← N 과 무관한 상수두 식이 만나는 N이 손익분기점이다.
06화 실측 (T_ORD_BIG × M_CUS · 실습/01)
OUTER_ROWS NL_BUFFERS HJ_BUFFERS VERDICT NL_OVER_HJ
10 25 36 NL 승 .7
20 35 36 NL 승 1
25 40 36 HASH 승 1.1
100 115 36 HASH 승 3.2
2000 2088 45 HASH 승 46.4
20000 20798 151 HASH 승 137.7
200000 211665 1229 HASH 승 172.2
1000000 1026837 5856 HASH 승 175.3작은 N 구간에서 두 식을 읽어 내면 이렇다.
NL = 15 + N (6 블록 outer + 9 블록 인덱스 + N)
해시 = 36 (30 블록 M_CUS + 6 블록 outer)
15 + N = 36 → N = 21손익분기점은 outer 21건이다. 그 아래로 20건까지는 NL이 이긴다.
⚠️ 21은 이 조인에만 해당한다
M_CUS를 한 번 훑는 데 30블록이 들어서 나온 값이다. inner가 커지면 분기점은 뒤로 밀린다. 분기점은 "inner를 한 번 읽는 값"으로 정해진다.
inner가 해시의 상수항이 분기점이 작다(코드 테이블 수십 블록) 작다 앞으로 당겨진다(NL 구간이 좁다) 크다(수십만 블록) 크다 뒤로 밀린다(NL 구간이 넓어진다) 그래서 "NL은 소량, 해시는 대량"이라는 말은 절반만 맞다. 정확히 말하면 "outer 건수 × inner 건당 비용"과 "inner 전체를 한 번 읽는 값"을 비교하는 문제다.
2018년 회의록이 빠뜨린 것 — 실측으로 확인
| 2018년 (2만 건) | 지금 (100만 건) | |
|---|---|---|
| NL | 20,798 | 1,026,849 |
| 해시 | 151 | 5,869 |
| NL이 진 배수 | 137.7배 | 175.0배 |
| NL의 손해(절대량) | 약 2만 블록 ≈ 10밀리초(12.6ms vs 2.4ms) | 약 102만 블록 ≈ 0.5초 |
📌 이 표가 06화에서 가장 중요하다
2018년에도 NL은 지고 있었다. 약 138배 차이였다. 달라진 것은 틀린 대가의 크기다. 10밀리초는 아무도 못 느끼지만 0.5초는 민원이 된다.
그래서 정확하게 말하면 이렇다. 힌트의 유효기간이 지난 것이 아니라, 처음부터 틀린 힌트의 대가가 커진 것이다. 앞의 해석은 "그때는 옳았으니 어쩔 수 없다"로 끝나고, 뒤의 해석은 "주기적으로 다시 재야 한다"로 이어진다.
6. PGA와 work area 3등급
PGA는 내 책상이다
| SGA의 버퍼 캐시 | PGA | |
|---|---|---|
| 누가 쓰나 | 모든 세션이 공유 | 그 서버 프로세스 전용 |
| 보호 장치 | 래치가 필요하다 | 없다 |
| 무엇이 올라가나 | 데이터 블록 | 정렬·해시·비트맵 작업 공간 |
같은 데이터라도 PGA에서 다루는 쪽이 싸다. 공유하지 않으니 잠글 필요가 없기 때문이다.(래치와 버퍼 캐시의 내부는 07화에서 다룬다.)
work area는 PGA 안에서 오퍼레이션 하나(해시 조인 하나, 정렬 하나)에 배정되는 공간이다. WORKAREA_SIZE_POLICY=AUTO이면 오라클이 PGA_AGGREGATE_TARGET 범위 안에서 알아서 나눠 준다.
3등급 — 실행계획에서 읽는 법
| ... | OMem | 1Mem | Used-Mem | Used-Tmp|
| ... | 1423K| 1423K| 1679K (0)| |
^^^^^ ^
실제 패스 수| 등급 | 괄호 | 무슨 일이 있었나 | Used-Tmp |
|---|---|---|---|
| optimal | (0) | 전부 메모리에서 끝났다 | 없음 |
| onepass | (1) | TEMP에 한 번 내렸다가 한 번 올렸다 | 있음 |
| multipass | (N) | 내렸다 올리기를 N번 반복했다 | 있음. 많다 |
| 열 | 뜻 |
|---|---|
OMem | optimal로 끝내려면 필요하다고 추정한 크기 |
1Mem | onepass로라도 끝내려면 필요한 크기 |
Used-Mem | 실제로 쓴 메모리 + (패스 수) |
Used-Tmp | TEMP에 내려놓은 양 |
V$SQL_WORKAREA — 계획 밖에서 같은 사실 확인
sql
SELECT w.operation_id, w.operation_type, w.policy,
w.estimated_optimal_size, w.estimated_onepass_size,
w.last_memory_used, w.last_execution, w.last_tempseg_size
FROM v$sql_workarea w WHERE w.sql_id = '&sql_id';06화 실측값이다(세 등급이 모두 나온다).
TAG OP_ID OPERATION_TYPE POLICY EST_OPT EST_1P LAST_MEM LAST_EXEC LAST_TEMP
---- ----- --------------- ------- ---------- ---------- ---------- ----------- ----------
W1 4 HASH-JOIN AUTO 1457152 1457152 1703936 OPTIMAL
W2 4 HASH-JOIN MANUAL 63507456 8785920 260096 4 PASSES 25165824
W3 5 SORT (v2) MANUAL 73728 73728 66560 1 PASS 1048576
W3 7 SORT (v2) MANUAL 27657216 1907712 66560 100 PASSES 26214400☠️ 블록은 그대로인데 시간만 터진다
Buffers | Reads | Writes | 시간 | |
|---|---|---|---|---|
| W1 optimal | 5,869 | 0 | 0 | 0.17초 |
| W2 4패스 | 5,868 | 11,784 | 2,952 | 5~8초 |
일량(Buffers)은 한 블록도 늘지 않았다. 일하는 장소가 메모리에서 디스크로 바뀌었을 뿐이다.
(W2의 시간은 실행할 때마다 5~8초 사이를 오간다. 그래도 Buffers가 5,868에서 꿈쩍도 하지 않는다는 사실은 그대로다.)
📌 T0 측정규약의 첫 사각지대
01화에서 정한 T0은 "
Buffers로 말한다"이고, 그 규약은 옳다. 다만 이 경로 하나는Buffers에 찍히지 않는다. 해시 조인이나 정렬이 있는 실행계획에서는Used-Mem/Used-Tmp/Reads/Writes를 함께 본다. 본격적인 이야기는 09화에서 한다.
재현 방법 (Free 컨테이너 안에서)
메모리 등급이 뒤집히는 현상은 세션 파라미터만으로 재현한다. 인스턴스는 건드리지 않는다.
sql
ALTER SESSION SET workarea_size_policy = MANUAL;
ALTER SESSION SET hash_area_size = 65536; -- 64KB
ALTER SESSION SET sort_area_size = 65536;
-- ... 측정 ...
ALTER SESSION SET workarea_size_policy = AUTO; -- 반드시 원복이 교재의 실습은 Docker로 띄운 Oracle Database Free 한 대에서 모두 끝난다. 서버 메모리 자체를 바꿔 가며 대규모 EE 환경과 비교하는 일은 이 교재에서 다루지 않는다. 다루더라도 별권에서 다룬다. 여기서처럼 세션 단위로 메모리를 줄이는 것이 같은 현상을 같은 컨테이너에서 재현하는 정식 방법이다.
7. 힌트는 무엇을 하는가
힌트는 "지시"가 아니라 "선택지 제한"이다
옵티마이저는 가능한 실행계획 후보를 만들어 놓고 그중 비용이 가장 싼 것을 고른다. 힌트는 그 후보 목록에서 나머지를 지워 버린다. 남은 것 중에서 고르니 "강제"처럼 보일 뿐이다.
| 힌트 | 지우는 것 |
|---|---|
LEADING(o c) | o가 선행이 아닌 모든 조인 순서 |
USE_NL(c) | c를 NL이 아닌 방식으로 붙이는 모든 계획 |
USE_HASH(c) | c를 해시가 아닌 방식으로 붙이는 모든 계획 |
USE_MERGE(c) | c를 소트 머지가 아닌 방식으로 붙이는 모든 계획 |
그래서 힌트는 옳고 그름을 정하는 게 아니라 "그 시점의 데이터에서 가장 싼 선택"을 못 박아 두는 일이다. 데이터가 바뀌면 그 선택은 저절로 낡는다. 그런데 힌트는 스스로 바뀌지 않는다. 코드에 그대로 박혀 있다.
02화와의 차이 — 이번엔 힌트가 먹었다
| 02화 | 06화 | |
|---|---|---|
| 힌트 | INDEX(o IX_ORD_BIG_DT) | LEADING(o c) USE_NL(c) |
| 결과 | 조용히 무시됨(컬럼이 가공돼 쓸 수가 없었다) | 그대로 적용됨 |
| 교훈 | 오라클은 못 따를 힌트를 말없이 버린다 | 오라클은 따를 수 있는 힌트는 끝까지 따른다 |
힌트가 무시되는 것보다 무서운 건, 몇 해 동안 충실히 지켜지는 것이다.
힌트를 떼기 전에 확인할 것
sql
-- ① 힌트를 명시한 계획과 힌트 없는 계획의 Plan hash value 를 비교한다
-- 같으면: 옵티마이저는 이미 같은 결론이다. 힌트는 아무것도 안 하고 있다
-- 다르면: 옵티마이저가 다른 선택을 한다. 양쪽을 다 재고 판단한다
SELECT sql_id, plan_hash_value, buffer_gets, executions FROM v$sql WHERE ...;
-- ② 결과가 같은지 반드시 확인한다 (건수·합계)
-- ③ 환경 의존성을 적는다 (해시 조인이면 PGA)06화에서 재 보니 힌트 없는 실행계획과 USE_HASH를 명시한 실행계획의 plan_hash_value가 둘 다 2292855265였다. 옵티마이저는 그 몇 해 내내 같은 답을 내고 있었고, 힌트가 그 답을 막고 있었다.
8. 힌트 유효기간 대장
이 교재의 새 표준 문서다. 양식은 힌트_유효기간_대장.md에 있다.
왜 필요한가
2018-11-06 회의록에는 현상, 조치, 결과, 결론이 다 있었다. 빠진 것은 한 줄이었다.
※ 측정 조건 : 운영 DB 복제본, 주문 20,000 건이 줄은 첨부 자료 끝에 있긴 했지만 본문에는 없었고, 재검토 주기도 없었다. 그래서 데이터가 50배로 느는 동안 아무도 다시 재지 않았다.
힌트는 데이터를 보고 내린 판단이다.판단을 기록할 때 전제를 같이 적지 않으면, 전제가 바뀌어도 아무도 알아채지 못한다.
열 구성 (요약)
| 열 | 무엇을 적나 |
|---|---|
| 힌트 / 위치 | USE_NL(c) · PKG_ORD_STLM.Q2 |
| 붙인 시점 / 근거 | 2018-11-06 성능개선 회의록 |
| 그때의 데이터 규모 ★ | 주문 20,000건 · M_CUS 2,000건 |
| 그때의 효과 | 3초 → 0.4초(회의록 기재) |
| 현재 규모 | 주문 1,000,000건(50배) |
| 현재 재측정값 | NL 1,026,849 vs 해시 5,869 |
| 판정 | 폐기 / 유지 / 재검토 |
| 다음 재검토 조건 ★ | "M_CUS가 10배가 되면"처럼 숫자로 |
★ 표시한 두 칸 때문에 이 양식이 있다. 나머지는 보조 칸이다.
유지 판정도 있다
대장은 "힌트를 다 떼라"는 문서가 아니다. 유지하기로 한 힌트에도 전제와 재검토 조건을 적어 두는 문서다. 오늘 만든 rev01(힌트 없음)도 M_CUS가 2,000행이라서 해시가 이긴 것이므로, 같은 대장에 한 줄을 차지한다.
9. 이 화의 측정값 전부
모두 ora-hanorder에서 잰 값이다(2026-10-18). 환경: Oracle AI Database 26ai Free 23.26.2.0.0 · db_block_size 8192 · pga_aggregate_target 512M · workarea_size_policy AUTO · 히스토그램 없음(FOR ALL COLUMNS SIZE 1). 데이터: T_ORD_BIG 1,000,000 / M_CUS 2,000 / M_RGN 8 · 인덱스는 02화 산출물 2개와 PK.
Q2 리비전별 (실습/00)
| 단계 | SQL | 계획 | Buffers | inner Starts | plan_hash_value |
|---|---|---|---|---|---|
| [A] rev00 | 힌트 그대로 | NESTED LOOPS | 1,026,849 | 1,000,000 | 2289018667 |
| [C] | rev00 + M_CUS 커버링 인덱스 | NESTED LOOPS + INDEX RANGE SCAN | 36,852 | 1,000,000 | 2696332737 |
| [D] rev01 | 힌트 제거 | HASH JOIN | 5,869 | 1 | 2292855265 |
| [E] | LEADING(c o) USE_HASH(o) 명시 | HASH JOIN | 5,868 | 1 | 2292855265 |
| [F] | LEADING(o c) USE_HASH(c) (build 뒤집기) | HASH JOIN | 5,868 | 1 | 305033139 |
| [G] | USE_MERGE | MERGE JOIN | 5,868 | — | 766613549 |
| [H] | rev00 − 스칼라 서브쿼리 | NESTED LOOPS | 1,026,837 | 1,000,000 | 3030409043 |
[A] → [D] = 1,026,849 → 5,869 = 175.0배. 결과 3행(900,000 / 90,000 / 10,000)은 모든 단계에서 같았다.
TABLE ACCESS FULL T_ORD_BIG줄은 실행할 때마다 5,826 / 5,827로 1블록씩 흔들린다. 그래서 [A] 합계는 1,026,849 ~ 1,026,853, [D]는 5,868 ~ 5,869 사이를 오간다. 정상이다.
[A] 비용 분해
| 자리 | Starts | Buffers |
|---|---|---|
TABLE ACCESS FULL T_ORD_BIG (outer) | 1 | 5,826 |
INDEX UNIQUE SCAN PK_M_CUS (inner 주소) | 1,000,000 | 21,011 |
TABLE ACCESS BY INDEX ROWID M_CUS (inner 본문) | 1,000,000 | 1,000,000 |
M_RGN (스칼라 서브쿼리) | 8 | 12 |
| 합계 | 1,026,849 |
스칼라 서브쿼리 분리 (07화로 넘긴다)
Buffers | |
|---|---|
| 스칼라 포함 ([A]) | 1,026,849 |
| 스칼라 제거 ([H]) | 1,026,837 |
| 기여분 | 12 (전체의 0.0012%) |
M_RGN 쪽 Starts는 8이다. RGN_ID가 8종류뿐이라 오라클이 결과를 재사용했다. 그 메커니즘과, 언제 재사용이 안 되는지는 07화에서 다룬다. 여기서는 "범인이 아니다"까지만 확인한다.
크기 스윕 (실습/01)
| outer 행수 | NL | 해시 | 승자 | NL ÷ 해시 |
|---|---|---|---|---|
| 10 | 25 | 36 | NL | 0.7 |
| 20 | 35 | 36 | NL | 1.0 |
| 25 | 40 | 36 | 해시 | 1.1 |
| 100 | 115 | 36 | 해시 | 3.2 |
| 2,000 | 2,088 | 45 | 해시 | 46.4 |
| 20,000 (2018년) | 20,798 | 151 | 해시 | 137.7 |
| 200,000 | 211,665 | 1,229 | 해시 | 172.2 |
| 1,000,000 | 1,026,837 | 5,856 | 해시 | 175.3 |
손익분기점은 outer 21건이다(15 + N = 36). 해시 쪽은 NO_QUERY_TRANSFORMATION을 걸어 Group By Placement 변환을 막고 조인 방식만 비교했다.
⚠️ N ≥ 2,000 구간의 NL 값은 실행할 때마다 ±0.5% 흔들린다. 스크립트가
T_ORD_SZ를 매번 새로 쌓아서 블록 패킹이 조금씩 달라지기 때문이다(실측 폭: 2,000 → 2,081~2,095 · 20,000 → 20,798~20,833 · 200,000 → 211,102~212,411). 해시 쪽과 N ≤ 100 구간, 그리고 배수(약 138배, 172배, 175배)는 변하지 않는다.
work area (실습/02)
| 정책 | 계획 | Buffers | Used-Mem | Used-Tmp | Reads/Writes | 시간 | |
|---|---|---|---|---|---|---|---|
| W1 | AUTO | HASH JOIN | 5,869 | 약 1,700K (0) | — | 0 / 0 | 0.17초 |
| W2 | MANUAL 64KB | HASH JOIN(build 뒤집음) | 5,868 | 약 250~300K (4) | 24M | 11,784 / 2,952 | 5~8초 |
| W3 | MANUAL 64KB | MERGE JOIN | 6,573 | SORT JOIN 66,560 (1) / (100) | 1,024K / 25M | 13,930 / 13,930 | 22~32초 |
W1의 Used-Mem은 실행할 때마다 1,679K ~ 1,739K로 흔들린다. 판정은 괄호 안의 (0)으로 한다.
10. 원천 좌표
전체 지도 →
00_자료준비/06_원서와_공식문서_지도.mdPDF 페이지 = 책 페이지 − 2 ·성능고도화II_목차색인.md(공개 사이트에는 없음)
성능고도화 II권 Ch2 — 조인 (주 원천)
| 주제 | 절 | 책 페이지 |
|---|---|---|
| NL 조인 기본 메커니즘 | Ch2 01 (1) | 210 |
| 힌트로 NL 조인을 제어하는 방법 | Ch2 01 (2) | 211 |
| NL 조인 수행 과정 분석 | Ch2 01 (3) | 214 |
| NL 조인의 특징 | Ch2 01 (4) | 217 |
| 배치 I/O · 버퍼 pinning 효과 | Ch2 01 (7)(8) | 225 · 226 |
| 소트 머지 조인 기본 메커니즘 | Ch2 02 (1) | 235 |
| 소트 머지 조인의 특징 | Ch2 02 (2) | 238 |
| 조인 조건식이 등치(=)가 아닐 때 | Ch2 02 (5) | 246 |
| 해시 조인 기본 메커니즘 | Ch2 03 (1) | 249 |
| 두 가지 해시 조인 알고리즘 (Grace 분할 포함) | Ch2 03 (3) | 252 |
| 메모리를 초과할 때 처리 방식 | Ch2 03 (4) | 256 |
| 해시 조인 사용 기준 | Ch2 03 (6) | 264 |
| 조인 순서의 중요성 | Ch2 04 | 266 |
| 스칼라 서브쿼리를 이용한 조인 · 캐싱 효과 | Ch2 07 (1)(2) | 284 · 285 → 07화 |
오라클 공식문서
| 주제 | 매뉴얼 | 장 제목 |
|---|---|---|
| 조인 방식 3종 | tgsql | Joins |
| 힌트 문법과 적용 조건 | tgsql | Influencing the Optimizer |
| PGA와 work area | cncpt | Memory Architecture (Program Global Area) |
| work area 자동 관리 | tgdba | Tuning the Database Memory (PGA Memory Management) |
이 화에서 꼭 확인할 뷰
sql
V$SQL_WORKAREA -- policy · estimated_optimal_size · last_execution · last_tempseg_size
V$SQL_WORKAREA_ACTIVE -- 지금 돌고 있는 work area (장시간 쿼리 관찰용)
V$SQL_PLAN_STATISTICS_ALL -- last_starts · last_cr_buffer_gets (DISPLAY_CURSOR 가 줄여 쓴 값의 원본)
V$PGASTAT -- PGA 전체 사용량과 over-allocation 횟수다음 화가 여기서 가져가는 것
| 화 | 가져가는 것 |
|---|---|
| 07 | M_RGN 쪽 Starts가 8인 이유: 스칼라 서브쿼리 캐싱은 왜 되고 언제 깨지나. 그리고 PGA에 래치가 없는 이유(= 버퍼 캐시에는 있는 이유) |
| 08 | Q2가 100만 행을 읽어 3행을 만든다는 사실 자체. 그 요구가 맞는가 · 조인 카디널리티 · 쿼리 변환 |
| 09 | Used-Tmp와 패스 수. W2의 4 PASSES, W3의 100 PASSES를 그대로 들고 간다 |
| 10 | E-Rows 1000K vs A-Rows 1000K: 06화에서는 추정이 정확했다. 추정이 틀리면 조인 선택이 어떻게 무너지는가 |
| 18 | Q2 rev01의 재검토 조건: 대장에 적은 "M_CUS가 10배가 되면"을 실제로 다시 잰다 |