Skip to content

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만_개.sql
powershell
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] 뒤이웃 Q10행. 밀려났다☐
[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_파싱_네_가지.sql
powershell
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,0592,034—163
[2] 소프트: 캐시 02,008407
[3] 세션 캐시: 캐시 502,00142,0046
[4] 열어 둔 채2375

(실측값은 실행마다 몇 단위씩 다르다. [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.txt
powershell
# 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,14

NNN은 스크립트가 찍은 경로의 숫자로 바꾼다. tkprof는 which로 찾아지지 않으니 전체 경로로 부른다.

문장ParseExecuteFetchqueryMisses in library cache during parse☐
[L] 리터럴 (표가 20개)111141 (표마다)☐
[S] 클릭마다 파스 호출2020202801☐
[O] 열어 둔 채120202801☐
☐ 맨 끝 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_와_여섯_조각.sql
powershell
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 / FORCE91 · 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_는_언제_움직이나.sql
powershell
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건 · 완료 → 취소 ×3104 → 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_피킹을_끄면.sql
powershell
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]피킹 끔 · rev0314 · 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.sql
powershell
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 rev0010 = 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리스트.sql
powershell
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/FREEPDB1

06 — 바인드 문장인데 자식이 셋 ​

확인기대☐
V$SQLAREAVERSION_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☐
② 길이만큼 바인드1040☐
③ 10칸 고정14☐
④ 컬렉션11☐

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_동시_결과.sql

PowerShell에서는 08a_동시_준비.sql 머리말에 적힌 Start-Job 방식을 쓴다.

클릭 1,000번당리터럴 (실측 두 번)바인드 (실측 두 번)내 값
CPU (cs)135.5 · 136.53.4 · 3.6
Concurrency 대기 (cs)9.58 · 11.250.11 · 0.15
걸린 시간 (cs)204.6 · 171.45.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.sql
powershell
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_COUNT 329), 상태 탭은 첫 탭이 계획을 정해 취소 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";
▶ 리뷰 예
#판정이유
A1NofromDate, toDate는 화면이 고르는 값이므로 바인드로 쓴다(조문 1)
A2Yes (조건부)tab은 코드값 셋이고 값마다 길이 다르다(11화 실측). 그래서 리터럴을 유지한다. 단 tab을 그대로 이어 붙이지 말고 코드에서 세 문장 중 하나를 고른다(조문 2)
A4NoDATE '…'를 이어 붙였다. 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. 조건 반전: "상태 탭이 '최근 1,000건'이 아니라 '최근 100건'이었다면, FORCE의 함정은 어떻게 달라지나?" (힌트: 실습/02 [C])
  2. 반례 요구: "바인드가 리터럴보다 나쁜 문장을 이 화의 데이터로 하나 든다면?"
  3. 측정 지목: "'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_COUNT 3의 사유를 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화: 하드 파싱 뒤에 줄 서 있던 것이 앞으로 나온다.