테마
11화 · 실습과 기록
여기서 하는 일: 3만 번 넘기고, 서버 스위치 둘(FORCE와 피킹 끄기)을 내 세션에서 재고, 문장을 한 줄 고친다.
1_사건.md를 읽은 것은 "선배가 하는 걸 지켜본 것"이고, 이 문서를 끝내야 직접 해 본 것이 된다. 개념이 막히면2_파고들기.md를 펼친다.시간이 없는 주에는 STEP 0 → 1 → 2 → 4 → 6 → 7 → 10만 한다(약 60분).
STEP 0 — 직전 리비전을 연다 · 전제를 확인한다 (5분) ★ 매주 처음 하는 일
bash
docker start ora-hanorder오늘은 조각 하나가 아니라 여섯 조각이 사는 곳을 연다. 지금 살아 있는 리비전 넷을 나란히 본다.
bash
cat ../03화_날짜가_문자로_박혀_있다/리비전/Q1_rev03.sql
cat ../06화_100만_번의_왕복/리비전/Q2_rev01.sql
cat ../08화_금액이_50배로_찍힌다/리비전/Q3_rev01.sql
cat ../09화_100페이지째부터_화면이_멈춘다/리비전/Q4_rev02.sql조각마다 "화면이 고르는 값"이 어디에 있는지 적는다(Q1은 조회월 :p_ym, Q2와 Q3는 없음, Q4는 포스트잇 :p_dt :p_seq).
10화를 마쳤다면 이미 이 상태다 — 확인만 한다
sql
-- ① ORD_ST 에 FREQUENCY 히스토그램 (기대: FREQUENCY · 버킷 3)
SELECT column_name, num_buckets, histogram FROM user_tab_col_statistics
WHERE table_name = 'T_ORD_BIG' AND column_name = 'ORD_ST';
-- ② 테이블 선호도 (기대: FOR ALL COLUMNS SIZE 1 FOR COLUMNS SIZE 254 ORD_ST)
SELECT DBMS_STATS.GET_PREFS('METHOD_OPT', USER, 'T_ORD_BIG') FROM dual;
-- ③ 인덱스 3개 (기대: IX_ORD_BIG_DT · IX_ORD_BIG_DT_AMT · IX_ORD_BIG_DT_SEQ)
SELECT index_name FROM user_indexes WHERE table_name = 'T_ORD_BIG' ORDER BY 1;
-- ④ 세션 기본값 (기대: cursor_sharing EXACT · session_cached_cursors 50 · statistics_level TYPICAL)
SELECT name, value FROM v$parameter
WHERE name IN ('cursor_sharing', 'session_cached_cursors', 'statistics_level', 'shared_pool_size');①②가 다르면 10화를 건너뛰었거나 상태 복구 스크립트를 돌린 것이다. 걱정하지 않아도 된다. 이 화의 실습 스크립트(00, 01, 02, 03)는 맨 앞 [준비]에서 이 상태를 확인하고, 없으면 같은 상태를 만든다(
SET_TABLE_PREFS+GATHER_TABLE_STATS(..., no_invalidate => FALSE)). 있으면 손대지 않는다.
PowerShell에서는 먼저
$OutputEncoding = New-Object System.Text.UTF8Encoding $false한 줄을 실행한다(안내문의 한글이?로 깨지지 않게 한다).
STEP 1 — 예측을 봉인한다 (4분)
인프라팀: "같은 SQL 이 수만 개. CURSOR_SHARING=FORCE 로 한 번에."
정산 목록(Q4)을 개발팀은 포스트잇 값을 문자열로 이어 붙여 반영했다.
Q1. 정산 목록을 3만 번 넘기면, 시간의 대부분은 어디에 쓰일 것 같은가? ______________
Q2. 3만 번 넘긴 직후, 공유 풀(V$SQL)에 그 문장이 몇 개 남아 있을 것 같은가? ______
Q3. FORCE 를 켜면 3만 번의 하드 파싱은 몇 번이 될 것 같은가? ______
Q4. "상태 탭" 처럼 상태값을 상수로 박은 문장 셋이 FORCE 로 하나가 되면,
누가 손해를 볼 것 같은가? ACS 가 고쳐 줄 것 같은가? ______________
Q5. 바인드 피킹을 끄면 포스트잇 문장(rev02)은 어떻게 될 것 같은가? ______________▶ 적고 나서 펼치기
Q1: "데이터 읽기"라고 쓰기 쉽다. 실측은 CPU 36.17초 중 파싱이 34.66초다. 어떻게 읽을지 정하는 데 썼다.
Q2: "3만 개"라고 쓰기 쉽다. 실측은 300~400개다. 나머지는 이미 밀려났고, 앞서 올라와 있던 이웃(Q1)까지 같이 밀려났다.
Q3: "1"이라고 쓰기 쉽다. 실측은 329로, 서로 다른 날짜 수와 같다. DATE '…' 리터럴은 FORCE가 바인드로 바꿔도 값마다 자식을 만든다(HASH_MATCH_FAILED).
Q4: 그날 처음 열린 탭이 정한 계획을 세 탭이 같이 탄다. 완료가 먼저면 취소 탭 5,826 → 50,347, 취소가 먼저면 완료 탭 1,016 → 5,826. 1,000건 화면에서 ACS는 고치지 못했다(일량 칸이 같다).
Q5: "첫 손님 문제가 사라진다"고 쓰기 쉽다. 실측은 2페이지 1,005,991블록이다. 값을 모르면 OR를 두 갈래로 펼쳐 둘 다 끝까지 읽는다.
STEP 2 — 3만 번 넘긴다 (5분 · 실행에 약 40초) ★
bash
cd 10_본편_튜너의_1년/11화_같은_SQL이_수만_개/실습
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 00_공유풀의_3만_개.sqlpowershell
Set-Location 10_본편_튜너의_1년\11화_같은_SQL이_수만_개\실습
Get-Content 00_공유풀의_3만_개.sql -Encoding UTF8 | docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1⚠️ [A]가 30초 넘게 걸리는 것이 정상이다. 일부러 하드 파싱을 3만 번 시킨다. 멈춘 게 아니다.
| 단계 | 무엇 | 기대 | ☐ |
|---|---|---|---|
| [준비] | 10화 상태 확인 | >> 10화 종료 상태 그대로다 · ORD_ST FREQUENCY · 인덱스 3개 | ☐ |
| [A0] | 이웃 Q1 rev03을 한 번 | V$SQL에 1행 | ☐ |
| [A] | 개발팀 방식 3만 번 | 약 36초 · parse count (hard) 약 30,000 · parse time cpu 약 3,470cs | ☐ |
| [A] 뒤 | 남은 문장 | 300~400 · FMS_KINDS 1 | ☐ |
| [A] 뒤 | 이웃 Q1 | 0행. 밀려났다 | ☐ |
| [B] | 바인드로 3만 번 | 약 0.7초 · 하드 1 · 실행 30,000 · 클릭당 15.6블록 | ☐ |
| [C] | 탐지 쿼리 | 맨 위가 [A] 무리 (PARENTS 수백) | ☐ |
옮겨 적는다
| [A] 리터럴 | [B] 바인드 | |
|---|---|---|
| 걸린 시간 | ||
| parse count (hard) | ||
| parse time cpu (cs) | ||
| CPU (cs) | ||
| session logical reads ÷ 30,000 |
📌 이 표에서 볼 것
Buffers는 둘이 거의 같다(클릭당 17 vs 16). T0대로Buffers만 보면 아무 일도 없는 셈이다. 다른 것은 파싱이다. 하드 파싱 3만 번이 CPU의 96%를 먹었다. 한 번 실행한 일량이 아니라 "실행하기 전에 한 일"이 문제다.
STEP 3 — 파싱 네 가지 (10분) · (선택) TKPROF
bash
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 05_파싱_네_가지.sqlpowershell
Get-Content 05_파싱_네_가지.sql -Encoding UTF8 | docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1채점표: 돌리기 전에 "파스 호출"과 "하드 파싱" 칸을 예상으로 채워 보자.
| 같은 2,000번 클릭 | 예상 parse | 예상 hard | 실측 parse | 실측 hard | 실측 cache hit | 실측 CPU (cs) |
|---|---|---|---|---|---|---|
| [1] 하드: 리터럴 | 2,059 | 2,034 | — | 163 | ||
| [2] 소프트: 캐시 0 | 2,008 | 4 | 0 | 7 | ||
| [3] 세션 캐시: 캐시 50 | 2,001 | 4 | 2,004 | 6 | ||
| [4] 열어 둔 채 | 2 | 3 | 7 | 5 |
(실측값은 실행마다 몇 단위씩 다르다. [1]의 CPU는 163, 170이었다)
☐ [2] 와 [3] 의 차이가 'cache hit' 칸에만 있는가 — 혼자서는 시간 차이가 안 보인다 (STEP 9 에서 여럿이면?)
☐ [4] 의 parse 가 2 인가 — 파스 호출이 없다
☐ [5] 덤 — DBMS_SQL 로 rev02 를 부르면 2페이지 약 100만 블록, rev03 은 10여 블록인가
☐ [5] 의 계획에 VW_ORE_ · UNION-ALL · OPT_PARAM('_optim_peek_user_binds' 'false') 가 보이는가[5]는 STEP 6의 예고편이다.
DBMS_SQL은 값을 묶기 전에 파싱하므로 엿볼 값이 없다. 피킹을 끈 것과 같다.
(선택) 10046 + TKPROF로 Parse 줄을 읽는다
bash
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 90_10046_TKPROF.sql스크립트가 트레이스 파일 경로를 찍는다. 파일은 컨테이너 안에 있다. TKPROF도 컨테이너 안에서 돌리고 결과만 꺼낸다.
bash
# Git Bash — MSYS_NO_PATHCONV=1 을 꼭 붙인다 (없으면 /opt/... 가 C:/Program Files/Git/opt/... 로 바뀐다)
T=/opt/oracle/diag/rdbms/free/FREE/trace
MSYS_NO_PATHCONV=1 docker exec ora-hanorder sh -c "/opt/oracle/product/26ai/dbhomeFree/bin/tkprof $T/FREE_ora_NNN_CH11_P.trc /tmp/ch11_p.txt sys=no"
MSYS_NO_PATHCONV=1 docker cp ora-hanorder:/tmp/ch11_p.txt .
grep -n -A 14 "CH11 \[S\]" ch11_p.txtpowershell
# PowerShell — 경로 변환이 없으므로 그대로
docker exec ora-hanorder sh -c "/opt/oracle/product/26ai/dbhomeFree/bin/tkprof /opt/oracle/diag/rdbms/free/FREE/trace/FREE_ora_NNN_CH11_P.trc /tmp/ch11_p.txt sys=no"
docker cp ora-hanorder:/tmp/ch11_p.txt .
Select-String -Path .\ch11_p.txt -Pattern 'CH11 \[S\]' -Context 0,14NNN은 스크립트가 찍은 경로의 숫자로 바꾼다. tkprof는 which로 찾아지지 않으니 전체 경로로 부른다.
| 문장 | Parse | Execute | Fetch | query | Misses in library cache during parse | ☐ |
|---|---|---|---|---|---|---|
[L] 리터럴 (표가 20개) | 1 | 1 | 1 | 14 | 1 (표마다) | ☐ |
[S] 클릭마다 파스 호출 | 20 | 20 | 20 | 280 | 1 | ☐ |
[O] 열어 둔 채 | 1 | 20 | 20 | 280 | 1 | ☐ |
☐ 맨 끝 OVERALL TOTALS FOR ALL RECURSIVE STATEMENTS 의 Misses during parse 가 L 20 + S 1 + O 1 + 재귀 몫인가 (실측 33)
☐ [S] 의 Row Source 가 card=49957, [O] 가 card=999990 인가 — 같은 문장인데 [S] 는 값을 엿보지 못했다STEP 4 — FORCE를 내 세션에만 켠다 (10분) ★
bash
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 01_FORCE_와_여섯_조각.sqlpowershell
Get-Content 01_FORCE_와_여섯_조각.sql -Encoding UTF8 | docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1모두 ALTER SESSION으로 바꾼다. 끝에서 EXACT로 되돌리고 확인한다. 약 5초 걸린다.
| 단계 | 무엇 | 기대 | ☐ |
|---|---|---|---|
| [A][B] | Q1 · Q2 · Q3 · Q4를 EXACT / FORCE | 91 · 5,869 · 840 · 15. 계획 넷 다 그대로 | ☐ |
| [B] | FORCE가 바꾼 텍스트 | Q1 '01'·'YYYYMMDD'·1 · Q3 SELECT 1·FETCH FIRST 5 · Q4 날짜·번호·ROWNUM → :"SYS_B_n" · Q2 그대로 | ☐ |
| [B] | Q4의 자식 | 같은 날짜 2·3페이지 = 자식 0 · 다른 날짜 100페이지 = 자식 1 · HASH_MATCH_FAILED | ☐ |
| [C] | 상태 탭 EXACT | 완료 1,016 (인덱스 거꾸로) · 미결 · 취소 5,826 (FULL + Top-N 정렬) | ☐ |
| [D] | FORCE · 완료 먼저 | 취소 50,347 ×3 · 미결 13,510 | ☐ |
| [E] | FORCE · 취소 먼저 | 완료 5,826 ×3 | ☐ |
| [F] | CURSOR_SHARING_EXACT 힌트 | 1,016 · 5,826 (EXACT와 같다) | ☐ |
| [G] | 결과 칸 길이 | SUBSTR 16 → 160 · 'A' 1 → 32 | ☐ |
| [H] | FORCE로 3만 번 | 약 1.5초 · 하드 약 329 · VERSION_COUNT 329 · 부모 10.5MB · V$SQL 합계 113.1MB | ☐ |
💡 [C]~[F]의 각 탭 첫 실행에는 하드 파싱 중에 읽은 블록 수십 개가 더해질 수 있다(예: 완료 첫 실행 1,016~1,065). 그 계획의 진짜 값은 같은 자식을 두 번째로 실행할 때부터 나온다.
표를 하나 만든다 — FORCE의 이득과 함정
| 이득 | 함정 | |
|---|---|---|
| Q4 (개발팀 구현) | 하드 30,000 → ______ | 자식 ______ 개 (사유: ______________) |
| 상태 탭 | — | 완료 먼저인 날 취소 ______ · 취소 먼저인 날 완료 ______ |
| 결과 칸 | — | SUBSTR ______ 바이트 |
| Q1 · Q2 · Q3 | 텍스트만 바뀜 | 없음 (계획 그대로) |
STEP 5 — ACS는 언제 움직이나 (10분)
bash
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 02_ACS_는_언제_움직이나.sqlpowershell
Get-Content 02_ACS_는_언제_움직이나.sql -Encoding UTF8 | docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1| 단계 | 무엇 | 기대 | ☐ |
|---|---|---|---|
| [A] | 1,000건 · 완료 → 취소 ×4 | 취소 50,347 ×4 · 자식 0 하나 · BS Y · BA N · CS_HISTOGRAM 1:5 | ☐ |
| [B] | 1,000건 · 취소 → 완료 ×4 | 완료 5,826 ×4 · 1:5 | ☐ |
| [C] | 100건 · 완료 → 취소 ×3 | 104 → 8,334 (나쁜 한 번) → 자식 1 (BA Y) 5,826 · 선택도 0.018~0.022 · BIND_EQUIV_FAILURE | ☐ |
| [D] | [B]를 statistics_level = ALL로 | 완료가 두 번째부터 1,016. 고쳐진다 · CS_HISTOGRAM 1:1 2:1 | ☐ |
| [E] | [C]를 PL/SQL이 열어 둔 채 | 취소 8,333 ×5 · 파스 호출 1 · 자식 하나 | ☐ |
☠️ [B]와 [D]를 나란히 놓고 본다
문장도 순서도 같다. 다른 것은
statistics_level하나다. ALL에서는 ACS가 고쳤고, TYPICAL(운영 기본값)에서는 못 고쳤다.DISPLAY_CURSOR(… 'ALLSTATS LAST')를 쓰려고 켜는 바로 그 설정이다. 이 화의 ACS와 FORCE 측정을 TYPICAL에서,V$SQL.BUFFER_GETS가 늘어난 양으로 한 것도 그래서다. T0에 한 줄을 보탠다. 측정 조건은 운영과 같게 맞추고, 다르면 다르다고 적는다.
☐ [A] 의 1,0xx 와 50,347 이 같은 칸(1번)에 들어간 것을 CS_HISTOGRAM 으로 확인했다
☐ [C] 에서 고쳐지기 전 "나쁜 한 번" 의 값을 적었다: ______
☐ [E] 에서 파스 호출이 1 이라 자식을 바꿀 기회가 없었다는 것을 PARSE_CALLS 로 확인했다STEP 6 — 피킹을 끄면 (10분) ★ 오늘의 클라이맥스
bash
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 03_피킹을_끄면.sqlpowershell
Get-Content 03_피킹을_끄면.sql -Encoding UTF8 | docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1⚠️
_optim_peek_user_binds는 숨은 파라미터다. 스크립트는 내 세션에서만 바꾸고 끝에서 되돌린다. 운영에서는 오라클 지원 없이 바꾸지 않는다.
| 단계 | 무엇 | 기대 | ☐ |
|---|---|---|---|
| [A] | 피킹 끔 · 상태 탭 | 셋 다 인덱스 · 완료 1,017 · 미결 13,510 · 취소 50,347 · BS N | ☐ |
| [B] | 피킹 끔 · rev02 (DATE 바인드) | 2페이지 1,005,991 · 100페이지 1,005,006 · 1,000페이지 995,954 · 10,000페이지 905,413 · 마지막 18 | ☐ |
| [B] | 그 계획 | VW_ORE_ · UNION-ALL · Outline에 OR_EXPAND · OPT_PARAM('_optim_peek_user_binds' 'false') | ☐ |
| [C] | 피킹 끔 · rev03 | 14 · 14 · 17 · 16 · 16 · INDEX RANGE SCAN DESCENDING 하나 | ☐ |
| [D] | 피킹 켬 · rev02를 2페이지 / 마지막에서 태어나게 | 둘 다 14~17. 피킹이 켜져 있으면 rev02도 멀쩡하다 | ☐ |
| [E] | 08화의 NVL(:c, cust_id) | 계획 해시 하나 · :c=123 인덱스 갈래 Starts 1 → 13 · :c=NULL FULL 갈래 Starts 1 → 1,205 | ☐ |
[B]의 계획에서 짚을 것
☐ 7번 줄 access("ORD_DT"<=:P_DT) filter("ORD_DT"<:P_DT) — 2페이지면 거의 100만 행
☐ 9번 줄 LNNVL("ORD_DT"<:P_DT) — 첫 갈래와 겹치지 않게 막는 조건
☐ 3번 줄 SORT ORDER BY STOPKEY — 두 갈래를 다 읽은 뒤에야 11건을 자른다📌 [B]와 [E]를 나란히 — OR-Expansion의 두 얼굴
[E]는 두 갈래를 계획에 넣어 두고 실행 때
:C IS NULL필터가 고른다. 값이 바뀌어도 커서도 하나, 계획도 하나다. [B]는 값을 몰라서 두 갈래를 둘 다 걸었다. 08화가 넘긴 "바인드 값에 따라 갈리는 계획"의 답은 [E] 쪽이다.
STEP 7 — rev03과 정합성 (10분) ★ 오늘의 산출물
bash
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 04_포스트잇_rev03.sqlpowershell
Get-Content 04_포스트잇_rev03.sql -Encoding UTF8 | docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1포스트잇을 진짜 DATE 바인드로 넘긴다(EXECUTE IMMEDIATE … USING). 약 5초 걸린다.
| 단계 | 무엇 | 기대 | ☐ |
|---|---|---|---|
| [A] | rev03 페이지 곡선 | 1페이지 14 · 2 14 · 100 15 · 1,000 17 · 10,000 16 · 마지막 16 (10건 · 다음 없음) | ☐ |
| [B] | 100페이지 vs rev00 | 10 = 10 · 2,627,200 · 0 / 0 · 순서 같음 | ☐ |
| [B] | 92페이지 (날짜 경계) | 10 = 10 · 2,848,500 · 0 / 0 · 날짜 2종류 | ☐ |
| [B] | 마지막 페이지 | 10 = 10 · 3,095,700 · 0 / 0 | ☐ |
| [C] | 10만 페이지 전체 순회 | pages=100000 rows=1000000 sum=255398623700 mismatch=0 | ☐ |
| [C] | 다음 페이지 문장 | LOADS 1 · 실행 99,999 | ☐ |
| [D] | 정렬 키 중복 | 0 | ☐ |
리비전 파일 리비전/Q4_rev03.sql을 연다. rev02와의 diff는 한 줄이다.
diff
- WHERE ORD_DT <= :p_dt AND (ORD_DT < :p_dt OR ORD_SEQ <= :p_seq)
+ WHERE ORD_DT <= :p_dt AND NOT (ORD_DT = :p_dt AND ORD_SEQ > :p_seq)이게 리비전인가 — T0 제1조로 따진다
피킹이 켜진 평소 조건에서 한 번 돌리면 rev02도 14, rev03도 14다. 평소 조건으로는 리비전이 아니다(08화에서 Q2의 '먼저 묶고 붙이기'를 보류한 것과 같은 잣대다). 리비전의 근거는 조건을 붙인
Buffers N → M이다. 값을 엿보지 못하고 파싱될 때 2페이지 1,005,991 → 14. 그 조건은 오늘 실제로 두 번 나왔다(피킹 끄기 제안,DBMS_SQL). 리포트에는 조건까지 같이 적는다.
STEP 8 — 부모와 자식 · IN 리스트 (10분)
bash
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 06_부모와_자식.sql
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 07_IN리스트.sqlpowershell
Get-Content 06_부모와_자식.sql -Encoding UTF8 | docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1
Get-Content 07_IN리스트.sql -Encoding UTF8 | docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB106 — 바인드 문장인데 자식이 셋
| 확인 | 기대 | ☐ |
|---|---|---|
V$SQLAREA | VERSION_COUNT 3 · 실행 5 · LOADS 3 | ☐ |
자식별 그릇 (V$SQL_BIND_METADATA.MAX_LENGTH) | 자식 0 32 · 자식 1 · 2 2000 | ☐ |
| Y인 칸 | 자식 1 BIND_LENGTH_UPGRADEABLE · 자식 2 OPTIMIZER_MODE_MISMATCH | ☐ |
REASON 칸 | 자식 0 Bind mismatch · 자식 1 · 2 Optimizer mismatch. Y 칸과 한 칸 어긋나 읽힌다 | ☐ |
Y 칸은 "그 자식이 새로 생긴 이유",
REASON은 "그 자식을 다음 파스가 못 쓴 이유"에 가깝다. "왜 자식이 생겼나"는 Y 칸으로 답한다. →2_파고들기.md §5
07 — 고르는 수가 매번 다른 IN 리스트
| 방식 | 기대 문장 수 | 기대 하드 파싱 | 실측 | ☐ |
|---|---|---|---|---|
| ① 리터럴 | 300 | 약 300 | ☐ | |
| ② 길이만큼 바인드 | 10 | 40 | ☐ | |
| ③ 10칸 고정 | 1 | 4 | ☐ | |
| ④ 컬렉션 | 1 | 1 | ☐ |
STEP 9 — (선택) 네 명이 동시에 · 라이브러리 캐시 안 (15분)
08 — 네 명 동시 (07화 03 시리즈와 같은 방식)
bash
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 08a_동시_준비.sql
for i in 1 2 3 4; do docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 08b_일꾼_리터럴.sql & done; wait
for i in 1 2 3 4; do docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 08b_일꾼_바인드.sql & done; wait
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 08c_동시_결과.sqlPowerShell에서는 08a_동시_준비.sql 머리말에 적힌 Start-Job 방식을 쓴다.
| 클릭 1,000번당 | 리터럴 (실측 두 번) | 바인드 (실측 두 번) | 내 값 |
|---|---|---|---|
| CPU (cs) | 135.5 · 136.5 | 3.4 · 3.6 | |
| Concurrency 대기 (cs) | 9.58 · 11.25 | 0.11 · 0.15 | |
| 걸린 시간 (cs) | 204.6 · 171.4 | 5.1 · 5.4 |
☐ 리터럴 일꾼의 대기 맨 위가 latch: shared pool 인가 (일꾼 하나 23~25 cs)
☐ resmgr:cpu quantum 이 실행마다 크게 흔들리는가 — CPU 2개를 넷이 나눠 쓴 줄서기다. 흔들리지 않는 것은 CPU 열이다
☐ (더 해 보기) 08b_일꾼_바인드.sql 의 ALTER SESSION 줄 주석을 풀고 다시 → 결과표에 SOFT 로 찍힌다.
library cache: mutex X 가 보이는가 (실측 SOFT: Concurrency 0.28 cs / 1,000번)91 — 라이브러리 캐시 안 (SYSDBA)
bash
MSYS_NO_PATHCONV=1 docker exec -i ora-hanorder sqlplus -s / as sysdba < 91_라이브러리캐시_SYSDBA.sqlpowershell
Get-Content 91_라이브러리캐시_SYSDBA.sql -Encoding UTF8 | docker exec -i ora-hanorder sqlplus -s / as sysdba☐ X$KGLOB 에서 부모 1줄 (자식번호 65535 · heap 6 없음 · heap 0 4,064) 과 자식 2줄 (heap 0 8,080 · heap 6 36,416)
☐ 하드 파싱이 만드는 가장 큰 덩어리가 heap 6 (실행계획) 이라는 것
☐ V$LIBRARYCACHE 의 SQL AREA — RELOADS (밀려났다가 다시 올라온 횟수) 를 적는다: ______STEP 10 — T1 리포트 9호 ★ 오늘의 산출물
양식은 T1_단건SQL_튜닝리포트.md를 따른다.
┌──────────────────────────────────────────────────────────────────────────┐
│ T1 단건 SQL 튜닝 리포트 — 9호 │
├──────────────────────────────────────────────────────────────────────────┤
│ 1. 대상 : PKG_ORD_STLM Q4 (정산 목록 · 개발팀 반영본) · rev02 → rev03 │
│ + 공유 풀 전체 (CURSOR_SHARING · 바인드 피킹 제안) │
│ 2. 측정 환경 : ora-hanorder · 26ai Free 23.26.2.0.0 · cpu 2 · 공유 풀 416MB │
│ 10화 종료 상태 (ORD_ST FREQUENCY) · statistics_level TYPICAL │
│ 3. 부하 조건 : 단일 세션 3만 클릭 + 4 세션 동시 ★ 비면 리포트 무효 │
│ 4. 현상 : 하드 파싱 ______ · 파싱 CPU ______ · V$SQL 잔존 ______ │
│ 5. 원인 : ______________________________________________________ │
│ [성능고도화 I권 04장 p.272 · 277 · 283] [tgsql Cursor Sharing] │
│ 6. 개선안 : 채택 — 포스트잇 바인드(구현) + rev03 (OR 없는 조건) │
│ 상태 탭 리터럴은 그대로 둔다 │
│ 기각 — ① FORCE ② FORCE + 피킹 끄기 ③ rev02 를 바인드로만 │
│ 7. 효과 : 3만 클릭 하드 파싱 ______ → ______ │
│ 값 없이 파싱될 때 2페이지 Buffers ______ → ______ │
│ 8. 정합성 : ★ 08화 정합성 회귀 체크리스트 A~H │
│ 9. 환경 의존성: ______________________________________________________ │
│ 10. 리스크 : ______________________________________________________ │
│ 11. 롤백 절차: ______________________________________________________ │
│ 12. 재측정 : ______________________________________________________ │
└──────────────────────────────────────────────────────────────────────────┘6번 「기각한 대안」 — 셋 다 숫자로
①
CURSOR_SHARING = FORCE: Q4 3만 클릭 하드 파싱 30,431 → 329, 36.19초 → 1.45초. 효과는 있다. 그러나DATE '…'는 값마다 자식이 생기고(HASH_MATCH_FAILED·VERSION_COUNT329), 상태 탭은 첫 탭이 계획을 정해 취소 5,826 → 50,347 또는 완료 1,016 → 5,826이 된다. 1,000건 화면에서는 ACS도 고치지 못하고, 결과 칸 길이가 16 → 160바이트로 바뀐다. ② FORCE + 바인드 피킹 끄기: 상태 탭은 값과 상관없이 인덱스 계획을 탄다(취소는 늘 50,347). rev02는 2페이지에서 1,005,991블록을 읽는다. ③ rev02를 바인드로만 고치기: 피킹이 켜져 있는 동안은 rev03과 같다(14~17). 피킹이 꺼지거나DBMS_SQL로 호출되는 날에는 1,005,991이 된다.
8번 「정합성」 — 08화 체크리스트 A~H
STEP 7의 숫자로 채운다. 양식은 정합성_회귀_체크리스트.md를 따른다.
▶ 채운 예 (쓰고 나서 대조)
| # | 항목 | 대상 / 방법 | 결과 | 판정 |
|---|---|---|---|---|
| A | 기준 | rev00 (OFFSET + FULL 전체 정렬). 원장은 T_ORD_BIG 전체를 (ORD_DT DESC, ORD_SEQ DESC)로 정렬한 것 | — | 정함 |
| B | 건수 · 합계 · MINUS 양방향 · 중복 키 | rev03 (DATE 바인드) vs rev00, 100페이지 | 10 = 10 · 2,627,200 = 2,627,200 · 0 / 0 · 순서 같음 · 정렬 키 중복 0 | 통과 |
| C | 잘라 내기 전 전체 | rev03으로 100,000페이지를 끝까지 넘기며 전체 정렬 커서와 대조 | 100,000페이지 · 1,000,000행 · 255,398,623,700 · 어긋난 행 0 | 통과 |
| D | 조인마다 1 쪽 | Q4는 조인이 없다 | — | 해당 없음 (조인 없음) |
| E | 경계 데이터 | 날짜 경계 92페이지 · 마지막 페이지 | 10 = 10 · 2,848,500 · 0 / 0 · 날짜 2종류 / 10 = 10 · 3,095,700 · 0 / 0 | 통과 |
| F | 계획 해시와 무관하게 대조 | rev02와 rev03은 피킹을 켜면 계획 해시가 같다(3610158121). 그래서 결과로 대조 | (B · C) | 통과 |
| G | 변환을 근거로 쓰지 않음 | OR-Expansion은 성능 설명에만 썼다. 뜻이 같다는 것은 C로 증명 | — | 통과 |
| H | 영향 분석 | 바뀐 것은 Q4 다음 페이지 문장 하나. 인덱스 · 통계 · 파라미터 변경 없음 (FORCE · 피킹 끄기 기각) | — | 통과 |
9번 「환경 의존성」 — 09화의 교훈대로 "무엇을 재 볼지"까지
- rev03은 바인드 피킹 설정과 무관하다(켬, 끔,
DBMS_SQL모두 14~17). rev02는 피킹이 켜져 있어야 멀쩡하다.- 화면 프로그램이 바인드로 넘기는지는 이 SQL 파일로 정할 수 없다. 반영 뒤
V$SQLAREA의FORCE_MATCHING_SIGNATURE무리를 재 볼 것(탐지 쿼리)- 측정 조건:
statistics_level = TYPICAL. ALL에서는 ACS의 판단이 달라진다(실습/02[D]).
10번 「리스크」 · 11번 「롤백」 · 12번 「재측정」
- 리스크: 개발팀이 반영하기 전까지 하드 파싱은 그대로다. 누가
NOT (A AND B)를 "정리"하다가OR로 되돌릴 수 있으니 주석으로 이유를 남긴다. 화면에 검색 조건이 붙는 날에는 다시 잰다(재협상서 1호의 조건). - 롤백: rev03 → rev02. 다음 페이지 조건 한 줄을 되돌린다(뜻이 같으므로 결과는 그대로이고, 피킹이 켜져 있는 한 일량도 그대로다). 인덱스나 파라미터를 바꾸지 않았으므로 되돌릴 DDL은 없다.
- 재측정: 반영 다음 주 같은 시간대에
V$SYSSTAT의parse count (hard)증가분과 탐지 쿼리의 상위 무리를 본다. 상태 탭 세 문장이 여전히 셋인지도 확인한다.
STEP 11 — 「SQL 코딩 가이드」 v0.1 ★ 이 교재의 새 표준
문서는 40_보고서와_표준/템플릿/SQL_코딩가이드.md에 있다.
연습: 아래 PR을 가이드의 체크리스트 A, B로 리뷰한다.
java
// 개발팀 PR: 정산 상태 확인 화면에 "기간" 검색을 추가
String sql = "SELECT ORD_SEQ, CUS_ID, ORD_DT, ORD_AMT FROM (SELECT ... FROM T_ORD_BIG"
+ " WHERE ORD_ST = '" + tab + "'"
+ " AND ORD_DT >= DATE '" + fromDate + "' AND ORD_DT < DATE '" + toDate + "'"
+ " ORDER BY ORD_DT DESC, ORD_SEQ DESC) WHERE ROWNUM <= 1000";▶ 리뷰 예
| # | 판정 | 이유 |
|---|---|---|
| A1 | No | fromDate, toDate는 화면이 고르는 값이므로 바인드로 쓴다(조문 1) |
| A2 | Yes (조건부) | tab은 코드값 셋이고 값마다 길이 다르다(11화 실측). 그래서 리터럴을 유지한다. 단 tab을 그대로 이어 붙이지 말고 코드에서 세 문장 중 하나를 고른다(조문 2) |
| A4 | No | DATE '…'를 이어 붙였다. FORCE 아래에서도 값마다 자식이 생긴다 |
| B2 | 재야 한다 | 기간 조건이 붙으면 "최근 1,000건"을 찾는 길이 바뀔 수 있다. 탭 × 기간 폭으로 다시 잰다. 게다가 지금이 바로 화면에 검색 조건이 붙는 날이다. 09화 재협상서 1호의 재협상 조건이 걸렸다 |
STEP 12 — 서술 답안 (400~600자)
인프라팀(박팀장 경유)에게 보낼 회신을 쓴다. 제안은 FORCE로 한 번에와 피킹을 끄자, 두 가지였다.
4단계 프레임(현상/진단 → 원인 → 개선안 → 근거/효과)은 01화 3_실습과_기록.md와 같다.
▶ 선배의 첨삭 포인트 (쓰고 나서)
① "FORCE는 효과가 없습니다"는 틀린 보고다. 효과는 있다. 3만 클릭에 하드 파싱이 30,431 → 329로 줄었다. 정확하게 쓰면 "하드 파싱은 크게 줄지만, 일부러 리터럴로 쓴 화면이 첫 손님 값에 묶입니다"이다. 상대의 제안이 어디까지 맞는지를 먼저 쓴다(09화 PGA 회신과 같다).
② 비교의 짝을 붙인다.30,431 → 329만 쓰면 FORCE를 채택하자는 근거가 된다. 옆에 바인드면 1을 붙인다.
③ 피킹 끄기는 "지금 고칠 문장"으로 반박한다. 인프라팀이 멈칫하는 건 일반론("피킹은 좋은 기능입니다")이 아니라 "개발팀이 이번 주에 고칠 정산 목록이 2페이지부터 100만 블록을 읽습니다"라는 말을 들었을 때다.
④ 고칠 사람과 날짜를 쓴다. "개발팀이 바인드로"만 쓰면 다음 주에 또 같은 일이 생긴다. 가이드 문서를 함께 보낸다. 리포트의 리스크 칸은 개발자가 읽는 문서가 아니었다.
STEP 13 — 파인만 자가설명
절차는 01화와 같다. 1단 백지 설명 → 2단 막힘 태그(최소 1개) → 3단 원천 복귀 → 4단 재설명 순서로 진행한다.
오늘 자주 나오는 태그 세 가지
| 태그 | 전형적인 문장 |
|---|---|
#인과 | "바인드 피킹이 왜 '첫 손님'에게 계획을 맡기는지, 그게 왜 FORCE와 만나면 문제가 되는지 설명하다가 막혔다" → 2_파고들기.md §6 · §8 |
#용어 | "소프트 파싱과 세션 커서 캐시 적중이 무엇을 다르게 건너뛰는지 말로 못 하겠다" → §2 |
#수치 | "1,0xx와 50,347이 왜 ACS에게 같은 일량인지 모르겠다" → §7: 칸이 셋뿐이다(그리고 칸의 기준은 공식문서에 없다) |
4단 되묻기 3문 (오늘 버전)
- 조건 반전: "상태 탭이 '최근 1,000건'이 아니라 '최근 100건'이었다면, FORCE의 함정은 어떻게 달라지나?" (힌트:
실습/02[C]) - 반례 요구: "바인드가 리터럴보다 나쁜 문장을 이 화의 데이터로 하나 든다면?"
- 측정 지목: "'FORCE를 켜도 괜찮다'를 증명하려면 무엇을 재야 하나? 이 컨테이너로 못 재는 것은 무엇인가?" (힌트: 공유 풀 전체의 리터럴 문장 목록 · 값 순서 · 수백 세션 동시)
태그는 _막힘로그.md로 옮긴다.
완료 체크리스트
- [ ] STEP 0: 리비전 넷에서 "화면이 고르는 값"을 찾았고, 10화 상태(FREQUENCY)를 확인했다
- [ ] STEP 1: 보기 전에 예측 5개를 적었다
- [ ] STEP 2: 3만 번을 넘겨 하드 파싱 · 파싱 CPU · 남은 문장 수 · 밀려난 이웃을 옮겨 적었다
- [ ] STEP 3: 파싱 네 가지 표를 채웠다(선택: TKPROF의 Parse 줄과 Misses를 읽었다)
- [ ] STEP 4: FORCE의 이득 하나와 함정 셋(DATE 자식 · 상태 탭 · 칸 길이)을 표로 만들었다
- [ ] STEP 5: ACS가 고친 경우와 못 고친 경우, 그리고 statistics_level이 결과를 바꾼 것을 봤다
- [ ] STEP 6: 피킹을 끄면 rev02 2페이지가 약 100만 블록이 되는 것과, 08화 NVL과의 차이를 봤다
- [ ] STEP 7: rev03의 10만 페이지 mismatch 0을 봤고, 리비전의 근거를 조건을 붙여 적었다
- [ ] STEP 8:
VERSION_COUNT3의 사유를 Y 칸으로 읽었고, IN 리스트 네 방식의 문장 수를 확인했다 - [ ] STEP 10: T1 9호를 썼고, 「기각한 대안」 셋 · 정합성 체크리스트 A~H · 환경 의존성을 채웠다
- [ ] STEP 11: SQL 코딩 가이드로 PR 하나를 리뷰했다
- [ ] STEP 12: 인프라팀 회신을 산문으로 썼다
- [ ] STEP 13: 막힘 태그 최소 1개를
_막힘로그.md에 옮겼다
완결성 게이트: STEP 10의 6번 칸(기각한 대안 셋)과 8번 칸(정합성 체크리스트), STEP 11의 리뷰가 비어 있으면 이 화는 미완성이다. 이 화의 진짜 산출물은 rev03 한 줄이 아니라, "어느 값이 바인드이고 어느 값이 리터럴인지는 SQL을 쓰는 사람이 정한다"를 개발팀이 읽는 문서로 만든 것이다.
다음 화로 넘기는 상태
| 무엇 | 값 |
|---|---|
T_ORD_BIG | 변화 없음: 인덱스 3개 · ORD_ST FREQUENCY 히스토그램 (10화 상태 그대로) · 원본 테이블에 쓴 행 없음 |
Q1 · Q2 · Q3 | 리비전 없음 (Q1 rev03은 이미 바인드 · Q2 · Q3는 입력값 없음 · FORCE 아래에서도 계획 그대로) |
Q4 현재 리비전 | rev03 · 바인드 · OR 없는 조건 · 어느 페이지든 14~17 · 값 없이 파싱돼도 14~17 |
| 세션 파라미터 | cursor_sharing EXACT · optimizer_mode ALL_ROWS · _optim_peek_user_binds 기본값 · statistics_level TYPICAL (스크립트마다 원복 확인) |
| 남은 객체 | 없음. 수첩 T_C11_RUN은 08c가 지운다 |
| 새 표준 | 「SQL 코딩 가이드」 v0.1 |
| 열어 둔 질문 | 처리량이 오르면 드러나는 줄서기 → 12화 · PL/SQL의 열어 둔 커서와 ACS → 15화 · 가이드 v1.0 → 18화 |
다음 → 12화: 하드 파싱 뒤에 줄 서 있던 것이 앞으로 나온다.