테마
15화 · 실습과 기록
여기서 하는 일: 커서 FOR 루프 안을 재 보고, 트레이스 한 줄을 읽고, 주간 보정 배치를 정합성까지 고친다. 그리고 다음 주에 쓸 부하 발생기를 설치해 시운전하고 T3 배치 SLA 리포트 2호를 쓴다.
1_사건.md를 읽은 것은 "선배가 하는 걸 지켜본 것"이고, 이 문서를 끝내야 직접 해 본 것이 된다. 개념이 막히면2_파고들기.md를 펼친다.시간이 없는 주에는 STEP 0 → 1 → 2 → 4 → 6 → 9 → 10만 한다(약 60분). 트레이스, HPROF, NOCOPY는 다음 주에 돌아와서 해도 된다. 단, STEP 9는 빼면 안 된다. 16화가 이 위에서 시작한다.
이 화의 실습 지도
| 스크립트 | 무엇 | 계정 | 소요 |
|---|---|---|---|
실습/00_준비와_점검.sql | 14화 종료 상태 확인 · 복사본(C15) · 판(版)들 · 수첩 | sqlp | 10초 |
실습/01_P1_루프의_내부.sql | ★ P1 rev00 vs rev01 · 자동 배열 페치 · 레벨 1·2·3 · 왕복 한 번의 비용 | sqlp | 15초 |
실습/02_10046_TKPROF.sql | ★ 트레이스 다섯 판(P1 레벨 2·1 · 보정 rev00·rev01 · SQL*Plus ARRAYSIZE) | sqlp | 20초 + TKPROF |
실습/03_보정배치_rev00.sql | ★ 보정 rev00: 한 문장의 비용 · 배치의 비용 · 정합성(170건) | sqlp | 10초 |
실습/04_한문장의_함정.sql | MERGE + 함수 · 스칼라 서브쿼리 캐시 · NO_DATA_FOUND → NULL · LOG ERRORS · MATERIALIZE | sqlp | 15초 |
실습/05_보정배치_rev01.sql | ★ rev01: FORALL MERGE의 함정 · LIMIT · ERROR_INDEX · 정합성 | sqlp | 15초 |
실습/91_HPROF_권한_SYSDBA.sql | DBMS_HPROF 실행 권한(한 번) | SYS | 1초 |
실습/06_HPROF_계층프로파일.sql | 계층 프로파일 rev00 vs rev01 | sqlp | 10초 |
실습/07_NOCOPY와_패키지상태.sql | NOCOPY · 패키지 변수와 UGA · 레벨 3 인라인 | sqlp | 10초 |
실습/07a_상태_세션A.sql · 07b_재컴파일_세션B.sql | ORA-04068: 두 세션 동시에 | sqlp × 2 | 25초 |
../00_자료준비/고객사/L1_ora-hanorder/05_PKG_LOAD_DRIVER.sql · 05_부하_돌리기.sh · .ps1 | ★★ 부하 발생기 설치 · 시운전(환경 폴더) | sqlp | 3분 × 2 |
실습/99_정리.sql | C15 · HPROF 테이블 삭제 · 14화 종료 상태 확인(드라이버는 남긴다) | sqlp | 수 초 |
PowerShell에서는 먼저
$OutputEncoding = New-Object System.Text.UTF8Encoding $false한 줄을 실행한다. 실행 명령은 각 스크립트 머리말에 적힌 두 줄(Git Bash / PowerShell) 그대로다. 예:bashdocker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 실습/01_P1_루프의_내부.sql모두 두 번씩 돌려도 판정은 같다(호출 수와 fetch 수, 행 수는 같고 경과 시간만 장비 잡음으로 흔들린다). 원본
T_ORD·T_ORD_STLM·M_CUS·PKG_ORD_STLM에는 한 줄도 쓰지 않는다. 모두_C15복사본에서 한다.
STEP 0 — 직전 리비전을 연다 · 전제를 확인한다 (5분) ★ 매주 처음 하는 일
bash
docker start ora-hanorder
cat ../14화_야간_배치가_아침을_넘긴다/리비전/P1_rev02.sql # 14화 메모 : "rev02 는 마감 전에 끝난다는 전제 위에서만 안전하다"
cat 리비전/ADJ_rev00.sql # 오늘 고칠 물건 — 주간 정산 보정 배치 (오과장 운영본)
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 실습/00_준비와_점검.sql| 확인 | 기대 |
|---|---|
| [전제] big_rows · big_idx · hist · stlm_rows · last_no · pkg | 1000000 · 3 · FREQUENCY · 100000 · 0 · VALID |
| 데이터 지문 | 255,398,623,700 · 25,519,833,100 |
| 설정 | plsql_optimize_level 2 · job_queue_processes 1 · cpu_count 2 |
| 만든 것 | 정산 복사본 100,020 · 고객 1,997 · 이력 0 · 무효 0 |
| 경계 | 탈퇴 고객의 정산 150 · 금액 미확정 20 |
ADJ_rev00.sql에서 네 가지 모양에 밑줄을 긋는다. ① 커서 FOR 루프 ② 행마다 부르는 함수 ③ SELECT COUNT(*) INTO 후 분기 ④ WHEN OTHERS THEN NULL. 그리고 각 줄 옆의 주석에서 누가 언제 왜 넣었는지 읽는다.
STEP 1 — 예측을 봉인한다 (5분)
Q1. P1 rev00 (커서 FOR 루프 · 10만 건) 의 구동 커서는 몇 번 페치할까? ______
Q2. 보정 배치 rev00 이 정산 10만 건을 처리하며 SQL 로 건너가는 횟수는? ______
Q3. 보정을 MERGE 한 문장으로 바꾸고 함수를 그대로 넣으면, 함수는 몇 번 불릴까? ______
Q4. 보정 배치는 '정상 종료' 했고 오류 로그는 0 건이다. 보정이 빠진 정산은? ______
Q5. 오류가 한 건도 안 나는 MERGE 에 LOG ERRORS 만 붙이면 시간은? 같다 / 조금 는다 / 몇 배
Q6. FORALL 로 천 건씩 묶어 보내면 논리 읽기는 루프 판의 몇 분의 1? ______▶ 적고 나서 펼치기
- Q1: 대부분 10만이라고 쓴다. 실측은 1,001이다(레벨 2의 자동 배열 페치, 100행씩). 레벨 1이면 100,001.
- Q2: 정산 한 건에 세 번(함수 안 SELECT · COUNT · INSERT) → recursive calls 301,185.
- Q3: 100,020 그대로다. 스칼라 서브쿼리로 감싸도 57,679.
- Q4: 170건(탈퇴 고객 150 · 금액 미확정 20). 원장과 대조할 때만 보인다.
- Q5: 몇 배(23~32 → 105~117cs · redo 11 → 56MB).
- Q6: FORALL MERGE면 거의 그대로다(816,425 → 426,669). FORALL INSERT면 36분의 1이다(→ 22,363). 묶이는 문장이어야 일이 묶인다.
STEP 2 — 루프의 내부 (10분) ★
bash
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 실습/01_P1_루프의_내부.sql[A] rev00 vs rev01
| 판 | 경과 | CPU | recursive calls | execute count | cur_fetch | 논리 읽기 | ☐ |
|---|---|---|---|---|---|---|---|
| A rev00 L2 | 84~90 | 44~45 | 101,350 | 100,028 | 1,001 | 110,383 | ☐ |
| A rev01 | 7 | 4~5 | 183 | 36 | 0 | 7,403 | ☐ |
[B][C] 가져오는 쪽 · 레벨
| 판 | cur_fetch | recursive calls | 논리 읽기 | 경과 | ☐ |
|---|---|---|---|---|---|
| B forloop L2 | 1,001 | 1,008 | 1,827 | 4~5 | ☐ |
| B explicit L2 | 100,001 | 100,009 | 100,009 | 16 | ☐ |
| C forloop L1 | 100,001 | 100,008 | 100,009 | 15~16 | ☐ |
| C rev00 L1 | 100,001 | 200,386~200,387 | 208,622~208,625 | 94~102 | ☐ |
| C rev00 L3 | 1,001 | 101,386 | 110,430 | 87~98 | ☐ |
☐ 레벨 2 의 커서 FOR 루프는 100행씩 가져온다 (rows ÷ fetch = 100)
☐ 명시적 커서의 FETCH … INTO 는 레벨 2 에서도 한 건씩이다
☐ rev00 의 시간 중 '가져오기' 는 4 cs — 나머지는 루프 안의 INSERT 10만 번
☐ 끝의 확인에서 두 프로시저의 레벨이 다시 2 다[D] 왕복 한 번의 비용 — 마지막 표
| pl_to_sql_us | sql_to_pl_us | sql_to_udf_us |
|---|---|---|
| 약 2.5 | 약 0.53 | 0.13 ~ 0.2 |
끝나면
리비전/P1_rev02.sql을 연다. 15화의 판정은 P1은 rev02에서 완결이다. rev03은 없다. 왜 없는지 한 줄로 적어 둔다.
STEP 3 — 10046 · TKPROF · raw 한 줄 (20분) ★
bash
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 실습/02_10046_TKPROF.sql끝에 파일 이름 다섯 개가 찍힌다(FREE_ora_NNN_C15_P1L2.trc …). TKPROF 명령은 스크립트 머리말에 있는 그대로 쓴다. Git Bash에서는 MSYS_NO_PATHCONV=1을 꼭 붙인다.
bash
T=/opt/oracle/diag/rdbms/free/FREE/trace
for id in C15_P1L2 C15_P1L1 C15_ADJ00 C15_ADJ01 C15_CLI; do
MSYS_NO_PATHCONV=1 docker exec ora-hanorder sh -c "/opt/oracle/product/26ai/dbhomeFree/bin/tkprof $T/FREE_ora_NNN_$id.trc /tmp/c15_$id.txt sys=no"
MSYS_NO_PATHCONV=1 docker cp ora-hanorder:/tmp/c15_$id.txt .
done채점표
| 파일 | 볼 줄 | 기대 | ☐ |
|---|---|---|---|
c15_C15_P1L2.txt | SELECT ORD_SEQ … Fetch · rows | 1,001 · 100,000 | ☐ |
| 〃 | INSERT INTO T_STLM_P1_C15 Execute | 100,000 | ☐ |
c15_C15_P1L1.txt | SELECT ORD_SEQ … Fetch · query | 100,001 · 100,009 | ☐ |
c15_C15_ADJ00.txt | OVERALL RECURSIVE Execute · Fetch | 299,865 · 200,892 | ☐ |
| 〃 | SELECT CUS_GD … Execute · Fetch · rows | 100,020 · 100,020 · 99,870: Fetch ≈ Execute | ☐ |
| 〃 | INSERT INTO T_STLM_ADJ_C15 Execute · rows | 99,870 · 99,850(20은 ORA-01400) | ☐ |
c15_C15_ADJ01.txt | OVERALL RECURSIVE Execute · Fetch | 290 · 103 | ☐ |
| 〃 | SELECT CUS_GD … | 없다 ← 아래 함정 | ☐ |
c15_C15_CLI.txt | array15 · array100의 Fetch | 6,668 · 1,001(User Call) | ☐ |
☠️ 사라진 커서 — C15_ADJ01의 TKPROF에 보정률 함수의 SELECT가 없다
rev01도 함수를 2,000번 불렀다(STEP 6에서 센다). 그런데 TKPROF 표에는 없다. raw 파일에서 직접 센다.
bash
MSYS_NO_PATHCONV=1 docker exec ora-hanorder sh -c "grep -o '^EXEC #[0-9]*' $T/FREE_ora_NNN_C15_ADJ01.trc | sort | uniq -c | sort -rn | head -3"
# 2000 EXEC #… ← PARSING IN CURSOR 줄이 없는 커서 (트레이스를 켜기 전에 열렸다)스크립트가 트레이스 직전에 rev01을 한 번 데웠다. 그때 열린 함수의 커서가 PL/SQL 커서 캐시에 남아, 트레이스 구간에서 다시 파싱되지 않았다. TKPROF는
PARSING IN CURSOR가 없는 호출을 버린다. 자세한 내용은2_파고들기 §4.3에 있다.
raw 한 줄 해독 — 손으로
C15_ADJ00의 raw 파일에서 SELECT CUS_GD의 커서 번호를 찾고(PARSING IN CURSOR 줄), 그 번호의 FETCH 줄 하나를 골라 칸을 채운다.
bash
MSYS_NO_PATHCONV=1 docker cp ora-hanorder:$T/FREE_ora_NNN_C15_ADJ00.trc .
grep -n -A3 "SELECT CUS_GD" FREE_ora_NNN_C15_ADJ00.trc | head| c | e | p | cr | cu | mis | r | dep | og | plh |
|---|---|---|---|---|---|---|---|---|---|
| 3이어야 | 1 | 1 | DISPLAY_CURSOR의 실행계획 해시와 같아야 |
그리고 dep=2 줄 두 개를 찾는다. 하나는 obj$(하드 파싱), 하나는 update seg$(공간 관리)다. 내가 부르지 않았는데 오라클이 부른 SQL이다.
다 읽었으면 컨테이너 안의 트레이스를 지운다(
[T3]하나가 약 64MB).MSYS_NO_PATHCONV=1 docker exec ora-hanorder sh -c "rm -f $T/*_C15_* /tmp/c15_*.txt"
STEP 4 — 보정 배치 rev00 · 정합성 (10분) ★
bash
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 실습/03_보정배치_rev00.sql| [A] 한 문장 | 기대 | ☐ |
|---|---|---|
[A1] 고객 등급 조회 | INDEX UNIQUE SCAN · Buffers 3 | ☐ |
[A2] COUNT(*) | INDEX UNIQUE SCAN · Buffers 1 | ☐ |
| [B] 배치 | 경과 | CPU | recursive calls | 함수 호출 | 논리 읽기 | redo | 커밋 | ☐ |
|---|---|---|---|---|---|---|---|---|
| B rev00 first | 223~224 | 141~142 | 301,185 | 100,020 | 816,984 | 55.75MB | 100 | ☐ |
| B rev00 again | 159~171 | 126~128 | 301,139 | 100,020 | 803,472 | 26.67MB | 100 | ☐ |
| 한 건당 | 3.01 | 8.17 | ☐ |
[C] 정합성 — 08화 체크리스트로 채운다
| # | 항목 | 대상 / 방법 | 결과 | 판정 |
|---|---|---|---|---|
| A | 기준 | 원장 = T_ORD_STLM_C15 전체 | 100,020 | 정함 |
| B | 건수 | 보정 이력 · 반송 · 오류 기록 | 99,850 · 0 · 0 | |
| B | 원장에만 있는 정산 | MINUS | 170 | |
| E | 경계 | 마스터에 없는 고객 · 금액 미확정 | 150(3명) · 20 | |
| B | 합계 | 탈퇴 고객 150건의 빠진 보정액 | 114,864원 | |
| H | 영향 | 정산 확정 화면이 '보정 0원'으로 보는 정산 | 170 |
☠️ 배치는 정상 종료했고 오류 기록은 0이다. 170건은 원장과 대조할 때만 보인다. 지난주 보정 결과와 대조했다면 아무것도 보이지 않았을 것이다(STEP 6 [E] 끝).
STEP 5 — 한 문장의 함정 (10분)
bash
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 실습/04_한문장의_함정.sql| 판 | 기대 | ☐ |
|---|---|---|
| A one | 실패 ORA-01400 … "ADJ_RATE" · 이력 0건(10만 건 전부 되돌림) | ☐ |
| B one+LE | 함수 호출 100,020 · 155~162cs · redo 약 56MB | ☐ |
| C ssq+LE | 함수 호출 57,679 | ☐ |
| [C] 오류 기록 | 1400 ADJ_RATE 150 · 1400 ADJ_AMT 20(컬럼 이름이 있다) | ☐ |
| [D] | PL/SQL에서 ORA-01403 · SQL에서 (NULL - no error) | ☐ |
| E set2 | 23~32cs · 논리 읽기 16,609 · 이력 99,850 · 반송 170 | ☐ |
| F set2+LE | 105~117cs · redo 약 56MB(오류 0건인데) | ☐ |
| G set3 hint | 함수 1,997 · 25~34cs · 실행계획에 TEMP TABLE TRANSFORMATION | ☐ |
| G set3 nohint | 함수 99,850 · 67~74cs · 실행계획은 HASH JOIN 하나 | ☐ |
☐ 한 문장이라고 집합 처리가 아니다 — 함수가 행마다면 루프가 문장 안으로 숨은 것
☐ SQL 안에서 부른 함수의 NO_DATA_FOUND 는 에러 없는 NULL 이 된다
☐ LOG ERRORS 는 붙이는 것만으로 집합 처리를 행 단위로 만든다 (redo 를 보라)
☐ MATERIALIZE 하나를 빼면 함수가 1,997 → 99,850 번 — 에러도 경고도 없다STEP 6 — 보정 배치 rev01 (15분) ★ 오늘의 조각
bash
cat 리비전/ADJ_rev01.sql
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 실습/05_보정배치_rev01.sql| 판 | 경과 | recursive calls | 함수 호출 | 논리 읽기 | redo | ☐ |
|---|---|---|---|---|---|---|
| A forall merge | 130~147 | 2,260 | 2,000 | 426,669 | 53.89MB | ☐ |
| B rev01 first | 27~28 | 2,457 | 2,000 | 22,363 | 11.24MB | ☐ |
| B rev01 again | 73~75 | 2,406 | 2,000 | 214,084 | 23.29MB | ☐ |
| B rev00 first | 214~219 | 301,187 | 100,020 | 816,425 | 55.77MB | ☐ |
| C limit 10 / 100 / 1000 / 10000 / 100000 | 32,249 / 5,220 / 2,455 / 2,111 / 2,075 | ☐ |
[D] ERROR_INDEX = 6 -> naive seq(6) = 900006 (wrong row)
mapped subscript = 8 -> seq(8) = 900008 (the row that failed)
SQLERRM(-code) = ORA-01400: cannot insert NULL into () ← 컬럼 이름이 없다
☐ ERROR_INDEX 는 '몇 번째 반복' — INDICES OF 에서는 첨자가 아니다
☐ SAVE EXCEPTIONS 는 오류 번호만 남긴다 → 아는 실패는 이름으로 먼저 거른다[E] 정합성 — rev01
| ledger_rows | adj_rows | err_rows | overlap | unaccounted | dup_seq | ☐ |
|---|---|---|---|---|---|---|
| 100,020 | 99,850 | 170 | 0 | 0 | 0 | ☐ |
| 반송 사유 | 건수 | ☐ |
|---|---|---|
1403 NO_CUS: customer not in master | 150 | ☐ |
1400 NO_AMT: amount not fixed | 20 | ☐ |
☐ 보정액 73,638,572 = 규정대로 직접 계산한 값 73,638,572 · MINUS 0
☐ rev01 vs rev00 : 99,850 = 99,850 · MINUS 0 / 0 — '통과' 해 버린다. 기준은 원장이다 (체크리스트 A)STEP 7 — 계층 프로파일 (10분)
bash
MSYS_NO_PATHCONV=1 docker exec -i ora-hanorder sqlplus -s / as sysdba < 실습/91_HPROF_권한_SYSDBA.sql # 한 번
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 실습/06_HPROF_계층프로파일.sqlpowershell
Get-Content 실습\91_HPROF_권한_SYSDBA.sql -Encoding UTF8 | docker exec -i ora-hanorder sqlplus -s / as sysdba91 없이 06을 돌리면 첫 줄에서
PLS-00201: identifier 'DBMS_HPROF' must be declared로 멈춘다(교재 실측, 스크립트가WHENEVER SQLERROR EXIT로 바로 끝난다). 패키지도 동의어도 있다. 실행 권한이 없을 뿐이다.
| RUNID | 부름 | 횟수 | 그 아래 전부 | ☐ |
|---|---|---|---|---|
| 1 | 판 전체(rev00) | 1 | 2.38~2.39초 | ☐ |
| 1 | __static_sql_exec_line11 INSERT | 99,870 | 1.47~1.50초 | ☐ |
| 1 | FN_ADJ_RATE_C15 | 100,020 | 0.41~0.42초 | ☐ |
| 1 | __static_sql_exec_line9 COUNT | 99,870 | 0.37초 | ☐ |
| 1 | __sql_fetch_line6 페치 | 1,001 | 0.02초 | ☐ |
| 2 | 판 전체(rev01) | 1 | 0.26~0.39초 | ☐ |
| 2 | FORALL INSERT | 101 | ☐ | |
| 2 | FN_ADJ_RATE_C15 | 2,000 | 0.007초 | ☐ |
☐ TKPROF 에서 사라졌던 rev01 의 함수 2,000 번이 HPROF 에는 있다STEP 8 — NOCOPY · 패키지 상태 (15분)
bash
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 실습/07_NOCOPY와_패키지상태.sql
H="docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1"
$H < 실습/07a_상태_세션A.sql > 07a.out 2>&1 & $H < 실습/07b_재컴파일_세션B.sql; wait; cat 07a.out| 확인 | 기대 | ☐ |
|---|---|---|
| [A] IN OUT x1000 | 196~202cs | ☐ |
| [A] IN OUT NOCOPY x1000 | 0cs | ☐ |
| [A] 예외 뒤 | IN OUT ORIG · NOCOPY CHANGED | ☐ |
| [B] 첫 판 | 함수 1,997 · session uga memory +261,920~+327,400 bytes(64KB 덩어리 4~5개) | ☐ |
| [B] 둘째 판 · 다음 호출 | 함수 0(UGA는 0 또는 덩어리 하나) · cached 1997 | ☐ |
| [C] 레벨 1 · 2 · 3 | 0.16 · 0.16 · 0.10초 | ☐ |
| 07a A2 stateless | PKG_LOAD_DRIVER는 그냥 된다(설치 전이면 PLS-00201) | ☐ |
| 07a A2 stateful | ORA-04068 · ORA-04061 PKG_RATE_C15. 그리고 그 뒤 출력이 사라진다 | ☐ |
| 07a A3 | cached=1(빈 상태로 새로) | ☐ |
| 07a A5 | ORA-04068 PKG_CONST_C15: 상수 하나뿐인 패키지도 | ☐ |
07a는 STEP 9의 드라이버를 설치한 뒤에 돌려야 A1과 A2의 상태 없는 패키지 줄까지 다 볼 수 있다. 드라이버가 도는 동안에는 07b를 돌리면 안 된다(컴파일이 멈춘다.
2_파고들기 §6.3참고).
STEP 9 — PKG_LOAD_DRIVER 설치와 시운전 (10분 + 기다림 6분) ★★ 16화의 준비
부하 발생기는 이 화의 폴더가 아니라 환경 폴더에 있다. 16화와 17화가 거기서 가져다 쓴다. 설명과 인자, 프로필 표는 L1_ora-hanorder/README.md §부하 발생기에 있다.
bash
cd ../00_자료준비/고객사/L1_ora-hanorder
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 05_PKG_LOAD_DRIVER.sql # 설치 (멱등)
PROFILE=STEADY MINUTES=3 SESSIONS=2 ./05_부하_돌리기.sh # 3분
PROFILE=SPIKE MINUTES=3 SESSIONS=2 SPIKE_AT=1 SPIKE_LEN=1 ./05_부하_돌리기.sh # 3분powershell
Set-Location ..\00_자료준비\고객사\L1_ora-hanorder
Get-Content 05_PKG_LOAD_DRIVER.sql -Encoding UTF8 | docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1
.\05_부하_돌리기.ps1 -LoadProfile STEADY -Minutes 3 -Sessions 2
.\05_부하_돌리기.ps1 -LoadProfile SPIKE -Minutes 3 -Sessions 2 -SpikeAt 1 -SpikeLen 1설치 확인
| 기대 | ☐ | |
|---|---|---|
| [0] 도는 드라이버 세션 | 0 | ☐ |
| [2] 기록 테이블 | 처음이면 created 셋 · 다시 설치하면 kept 셋(지난 런 기록을 지킨다) | ☐ |
| [3] 패키지 | No errors. | ☐ |
| [5] 무효 객체 · 원본 지문 | 0 · T_ORD_BIG 1000000 / 255,398,623,700 · T_ORD_STLM 100000 · T_STLM_NUM 0 | ☐ |
시운전 결과 (교재 실측 · 2세션 × 3분 · 생각 시간 200 ms · 씨앗 15)
| STEADY | SPIKE (NL · LIT · LOCK) | ☐ | |
|---|---|---|---|
| 세션 실행 수 · 에러 · 끝난 이유 | 873 · 878 · 0 · DEADLINE | 587 · 656 · 0 · DEADLINE | ☐ |
| 0분째 Q1 · Q2 · Q3 · Q4 · N1 · P1 | 168 · 19 · 69 · 243 · 79 · 8 | 같다(같은 씨앗) | ☐ |
| 1분째 | 179 · 30 · 64 · 222 · 74 · 9 | Q1 28 · Q2 rev00 7 · Q3 3 · Q4 lit 21 · N1 hotfix 7 · N1 hold 6 · P1 3 | ☐ |
| 2분째 | 174 · 33 · 55 · 245 · 74 · 6 | 181 · 26 · 61 · 233 · 74 · 7 | ☐ |
| 조각 합계 | Q1 521 · Q2 82 · Q3 188 · Q4 710 · N1 227 · P1 23 | Q1 377 · Q2 45+7 · Q3 133 · Q4 476+21 · N1 153+7+6 · P1 18 | ☐ |
| ASH ACTION(샘플) | Q2 rev01 4 · P1 rev02 3 · think 1 · Q3 rev01 1 | N1 hotfix 43(enq: TX - row lock contention) · P1 rev02 6 · Q2 rev01 2 · Q3 1 · Q2 rev00 1 | ☐ |
| 원본 지문 앞 = 뒤 | 같다 | 같다 | ☐ |
| 남은 드라이버 세션 | 0 | 0 | ☐ |
실행 수와 0분째 숫자는 장비 속도에 따라 조금 다를 수 있다. 같은 장비에서 같은 인자로 돌렸을 때 0분째가 STEADY와 SPIKE에서 같은지, 그리고 에러 0, 지문 불변, 세션 0인지 확인한다. SPIKE 1분째에 조각 수가 확 준 것은 LOCK 때문이다. 세션 1이 번호 행을 10초씩 쥐고, 세션 2는 N1을 고를 때마다 그 줄에 선다(평균 6.2초 대기). CPU 폭발을 보고 싶다면(17화) 이렇게 돌린다.
PROFILE=SPIKE MINUTES=3 SESSIONS=2 SPIKE_KIND=NL,LIT SPIKE_WEIGHTS='Q1=10,Q2=40,Q3=5,Q4=40,N1=5,P1=0' ./05_부하_돌리기.sh→ 교재 실측: 1분째 Q2 rev00 152 · Q4 lit 159, ASH Q2 rev00 39샘플 · 전부 ON CPU.
☐ 발사기가 끝난 뒤 v$session 에 MODULE = 'LOAD_DRIVER' 인 세션이 0 이다
☐ T_LD_RUN 에 런마다 STARTED_AT · ENDED_AT 이 있다 (16화가 AWR 스냅샷 구간과 맞춘다)
☐ (선택) 도는 동안 다른 창에서 EXEC PKG_LOAD_DRIVER.STOP_RUN — 1초 안에 세션이 끝난다 · EXIT_REASON = STOP (교재 실측)
발사기 창에서 Ctrl+C 를 누르면 발사기가 같은 STOP_RUN 을 보내게 짜여 있다STEP 10 — T3 배치 SLA 리포트 2호 ★ 오늘의 산출물
양식: T3_배치_SLA_리포트.md · 붙이는 것: 정합성_회귀_체크리스트.md (08화) · 커밋주기_기준.md (13화)
┌────────────────────────────────────────────────────────────────────────────────┐
│ T3 배치 SLA 리포트 — 2호 주간 정산 보정 배치 T3-002 │
├────────────────────────────────────────────────────────────────────────────────┤
│ 측정 환경 : 연습 DB 복사본 · 26ai Free · cpu 2 · NOARCHIVELOG · 단독 실행 │
│ 정산 100,020건 (원본 10만 + 금액 미확정 20) · 고객 1,997 (탈퇴 3 제외) │
└────────────────────────────────────────────────────────────────────────────────┘1. 배치와 창(운영: 오과장 로그)
| 항목 | 내용 |
|---|---|
| 배치 · 단계 | 주간 정산 보정 배치 · 1단계(정산 → 보정 이력) |
| 트리거 · 마감 | 월 10:00 · 11:00(정산팀 정산 확정) |
| 현재 소요 · 여유 | 6/3 끝 10:52(여유 8분) · 6/10 끝 10:49(여유 11분) |
| 같은 창의 다른 일 | 정산 등록 화면(N1 rev01: 12화 이후로는 번호 때문에 막히지 않는다) · 일요일 체인은 01:47에 끝나 겹치지 않는다 |
2. 측정 환경: 위 머리말 · 리두 로그 10MB × 2 · plsql_optimize_level 2
3. 단계 분해(연습 DB · 실습/05 [B] · 이력이 빈 첫 판)
| 판 | 처리 | 경과 | redo | undo | recursive calls | 논리 읽기 | 주된 대기 | 출처 |
|---|---|---|---|---|---|---|---|---|
| rev00 | 99,850 보정 · 170 사라짐 | 214~219cs | 55.77MB | 16.71MB | 301,187 | 816,425 | log file switch (checkpoint incomplete)(TKPROF INSERT) | 05 [B] · 02 [T3] |
| rev01 | 99,850 보정 · 170 반송 | 27~28cs | 11.24MB | 3.69MB | 2,457 | 22,363 | — | 05 [B] · 02 [T4] |
4. 처리량: 정산 한 건당 recursive calls 3.01 → 0.025 · 논리 읽기 8.17 → 0.22 · redo 약 585B → 약 118B
5. 자원: redo 55.77 → 11.24MB · undo 16.71 → 3.69MB · 세션 PGA: 배열 1,000건 × 4 + 등급표(수백 KB)
6. 실패 · 재기동 · 멱등
| 항목 | rev00 | rev01 |
|---|---|---|
| 중간에 죽으면 | 1,000건 단위로 반쯤 반영(← 13화) | 전부 롤백(커밋 한 번) |
| 재기동 | 다시 돌리면 이력이 있는 건 UPDATE(행마다 COUNT(*)) | 다시 돌리면 구동 커서가 INSERT / UPDATE를 가른다 · 반송함은 회차(p_tag)마다 지우고 다시 쓴다 |
| 멱등 | 예(같은 보정액) | 예(실습/05 [B] again 같은 결과) |
7. 동시 실행 영향: 정산(T_ORD_STLM)은 읽기만 한다. 보정 이력에는 일반 INSERT의 TM LMODE 3만 건다(← 14화 실측). APPEND는 쓰지 않는다. LMODE 6이면 커밋 전까지 다른 트랜잭션이 이력에 한 줄도 쓰지 못한다. 번호(N1)는 받지 않는다. 그리고 커밋이 한 번뿐이라, 도는 동안 정산 확정 화면에는 이번 회차 보정이 하나도 안 보이다가 끝에 한꺼번에 보인다(rev00은 1,000건씩 '반쯤' 보였다, ← 14화 P1의 H). 확정 버튼은 보정 배치의 끝 로그를 보고 누른다. 이 절차는 12번 ②와 함께 정산팀과 정한다.
8. 추세: 다음 달 정산은 두 배(조민우). rev00 그대로면 운영 52분 → 약 104분 > 창 60분 → 마감 위반. rev01의 운영 소요는 측정 불가(연습 DB의 1/8을 운영에 그대로 옮기지 않는다) → 12번 ①
9. PL/SQL 호출 통계 ★ (이 칸을 처음 채운다 · 연습 DB · 10046 + TKPROF)
| rev00 | rev01 | |
|---|---|---|
| User Call (NON-RECURSIVE) Execute | 1 (+ 트레이스 끄기 1) | 1 (+ 1) |
| Recursive Call Execute · Fetch | 299,865 · 200,892 | 290 · 103(+ 함수 안 2,000: TKPROF 표 밖 · HPROF로 셈) |
행마다 호출된 한 건 조회(Fetch ≈ Execute) | 등급 조회 100,020 · COUNT(*) 99,870 | 없음(고객마다 2,000) |
| 구동 커서 배열 크기(rows ÷ Fetch) | 100(레벨 2 자동) | 990(LIMIT 1000) |
| 왕복 절감 | recursive calls 301,187 → 2,457(122분의 1) |
10. 정합성 회귀(08화 체크리스트: 실습/03 [C] · 05 [E])
| # | 항목 | 대상 / 방법 | 결과 | 판정 |
|---|---|---|---|---|
| A | 기준 | 원장 = 이번 주 정산 전체(직전 판 rev00과 대조하면 99,850 = 99,850 · MINUS 0/0으로 통과해 버린다) | 100,020 | 정함 |
| B | 건수 · 합계 · MINUS · 중복 | rev01 보정 + 반송 vs 원장 | 99,850 + 170 = 100,020 · 빠진 것 0 · 겹침 0 · 중복 0 · 보정액 73,638,572 = 73,638,572 · MINUS 0 | 통과 |
| C | 잘라 내기 전 전체 | 해당 없음: 배치 결과는 잘라 보여 주지 않는다 | — | 해당 없음 |
| D | 조인마다 1 쪽 | 구동 커서 정산 ⟕ 보정 이력(STLM_SEQ PK): 행이 불어나지 않는가 | 100,020 → 100,020 | 통과 |
| E | 경계 | 탈퇴 고객(마스터 파기) · 금액 미확정. rev00은 170건을 삼켰다 | rev01 반송 NO_CUS 150 · NO_AMT 20 | 통과 |
| F | 계획 해시 | 사용하지 않음 | — | 해당 없음 |
| G | 변환 | MATERIALIZE 판을 쓰지 않은 이유 = 뷰 병합에 기대지 않기 위해 | — | 통과 |
| H | 영향 | 정산 확정 화면(정산 ⟕ 보정 이력): rev00은 170건을 '보정 0원'으로 보여 줬다. rev01 뒤에는 반송함을 함께 보여 줘야 한다 | 정산팀 협의(조민우) | 조치 필요 |
11. 개선 이력 · 커밋 주기
| 리비전 | 날짜 | 경과(연습 DB) | redo | 비고 |
|---|---|---|---|---|
| (강도현 2019) | 2019.06 | — | — | 루프 · 함수 · WHEN OTHERS THEN NULL · 정산을 직접 고친다 |
| ADJ_rev00 | 6/3 | 214~224cs | 55.77MB | + 1,000건 커밋(12화) · 보정 이력 · COUNT(*)(13화 뒤) |
| ADJ_rev01 | 6/10 | 27~28cs | 11.24MB | BULK LIMIT · FORALL INSERT/UPDATE · 등급표 · 반송함 |
「커밋 주기 기준」 경로: #1 한 문장으로 쓸 수는 있지만, 규칙이 두 곳이 되거나(CASE) 힌트에 기대야 하고(MATERIALIZE) 실패를 행마다 다룰 수 없다(LOG ERRORS는 5배) → 쓰지 않는다 · #2 fetch across commit 아님 · #3 언두 3.7MB · → #4 끝에 한 번.
12. 재측정 · 재발방지
| # | 조치 | 담당 | 기한 | 검증 | 리스크 |
|---|---|---|---|---|---|
| ① | rev01 운영 적용 · 운영 소요를 다시 잰다 | 오과장 | 6/17(월) | 보정 시작 · 끝 시각 · 반송 건수 | 연습 DB의 1/8이 운영에서도 그대로라는 보장 없음 |
| ② | 정산 확정 화면에 반송함 표시 | 조민우 · 개발팀 | 6월 말 | 반송이 0이 아닌 주에 화면 확인 | 화면이 바뀌기 전에는 반송 건이 '보정 0원'으로 보인다 → 정산팀이 반송함을 따로 본다 |
| ③ | 탈퇴 고객 정산의 보정 규정 결정(등급 없음) | 정산팀 | 6월 말 | 규정 문서 | 결정 전까지 매주 150건 남짓 수기 처리 |
| ④ | 「SQL 코딩 가이드」 후보: WHEN OTHERS THEN NULL 금지 · 행마다 부르는 함수는 호출 수를 적는다 | 나 | 18화 | 코드 검토 | 기존 배치 전수 조사가 필요 |
STEP 11 — 서술 답안 (400~600자)
박팀장에게 보낼 회신을 쓴다. "SQL은 다 고쳤다면서 왜 아직 느린가."
▶ 선배의 첨삭 포인트 (쓰고 나서)
① "SQL이 느리다"고 쓰지 않는다. SQL은 한 문장에 1~3블록으로 정말 빠르다. 느린 건 부르는 횟수다. 정산 10만 건에 30만 번이다. 박팀장의 질문("다 고쳤다면서")에 정면으로 답하는 문장이 여기서 나온다.
② 숫자를 하나만 고른다면 '호출 수'다. "30만 번 → 2,500번"이 "2.2초 → 0.3초"보다 운영으로 옮겨 생각하기 쉽다. 연습 DB의 초는 운영의 초가 아니다.
③ 성능보다 정합성을 먼저 쓴다. 170건이 에러 없이 빠지고 화면에 '보정 0원'으로 보였다. 이것이 이번 주의 진짜 사고다. 속도는 그다음이다.
④ "PL/SQL로 바꿨다"를 변명처럼 쓰지 않는다. 한 문장이 더 빠른 판(23cs)이 있었다는 것과, 그걸 왜 쓰지 않았는지(규칙의 소유, 힌트, 실패 처리)를 한 줄씩 적는다. 그게 없으면 "왜 한 문장으로 안 했냐"가 다음 질문이 된다.
STEP 12 — 파인만 자가설명
1단 백지 설명 → 2단 막힘 태그(최소 1개) → 3단 원천 복귀 → 4단 재설명 순서로 진행한다.
| 태그 | 전형적인 문장 |
|---|---|
#인과 | "커서 FOR 루프가 100행씩 가져오는데도 왜 느린지 설명하다 막혔다" → §2.1 |
#용어 | "dep와 mis를 말로 못 하겠다" → §3 |
#수치 | "FORALL로 묶었는데 논리 읽기가 왜 그대로인지 숫자로 못 나눠 보겠다" → §5.3 |
4단 되묻기 3문 (오늘 버전)
- 조건 반전: "보정률 규칙을 정산팀이 아니라 우리가 갖고 있고 화면이 그 함수를 쓰지 않는다면, 그래도 rev01이 맞나?"
- 반례 요구: "
WHEN OTHERS를 써도 되는 경우를 하나 든다면? 그때 반드시 해야 할 일은?" - 측정 지목: "운영의 보정 배치에 TKPROF를 걸었는데 함수 안의 SELECT가 안 보인다. 그 호출 수는 무엇으로 셀까?"
태그는 _막힘로그.md로 옮긴다.
정리 — 한빛유통을 14화 종료 상태 + 부하 발생기로
bash
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 실습/99_정리.sql
MSYS_NO_PATHCONV=1 docker exec ora-hanorder sh -c "rm -f /opt/oracle/diag/rdbms/free/FREE/trace/*_C15_* /tmp/c15_*.txt"기대: c15_left 0 · hprof_left 0 · 1000000 · 3 · FREQUENCY · 100000 · 0 · VALID · 지문 255,398,623,700 · 25,519,833,100 · load_driver VALID · ld_log_tables 3
완료 체크리스트
- [ ] STEP 0:
ADJ_rev00의 네 가지 모양과 각각의 「그때의 근거」를 읽었다 - [ ] STEP 1: 보기 전에 예측 6개를 적었다
- [ ] STEP 2: 구동 커서 fetch 1,001과 레벨 1의 100,001을 봤다 · P1 완결 판정을 적었다
- [ ] STEP 3: TKPROF의
Fetch ≈ Execute를 찾았다 · rawFETCH한 줄의 칸을 채웠다 · 사라진 커서를 raw에서 셌다 - [ ] STEP 4: 정합성 체크리스트로 170건을 찾았다
- [ ] STEP 5: 함수 호출 100,020 · SQL 안의
NO_DATA_FOUND= NULL ·LOG ERRORS의 비용을 봤다 - [ ] STEP 6: rev01의 원장 = 보정 + 반송 · 빠진 것 0을 확인했다 ·
ERROR_INDEX6 vs 8을 봤다 - [ ] STEP 7: HPROF로 rev01의 함수 2,000번을 셌다(선택)
- [ ] STEP 8: ORA-04068이 그 세션의 출력까지 지우는 것을 봤다(선택)
- [ ] STEP 9: 부하 발생기를 설치하고 STEADY와 SPIKE를 3분씩 돌렸다 · 에러 0 · 지문 불변 · 남은 세션 0
- [ ] STEP 10: T3 2호를 쓰고 9번(호출 통계), 10번(정합성), 11번(커밋 주기) 칸을 채웠다
- [ ] STEP 12: 막힘 태그 최소 1개를
_막힘로그.md에 옮겼다 - [ ] 정리:
99로 14화 종료 상태와 드라이버를 확인했다
완결성 게이트: STEP 4의 정합성 표, STEP 9의 시운전, STEP 10의 9번 칸이 비면 이 화는 미완성이다.
다음 화로 넘기는 상태
| 무엇 | 값 |
|---|---|
ora-hanorder 원본 | 변화 없음: 14화 종료 상태(T_ORD_BIG 인덱스 3 · ORD_ST 히스토그램 · T_ORD_STLM 10만 · T_STLM_NUM 0 · PKG_ORD_STLM 원본 · C15 0) |
| 환경에 더한 것 | PKG_LOAD_DRIVER(상태 없음 · VALID) · 작업 테이블 T_STLM_REG_LD · T_STLM_NUM_LD · T_STLM_S1_LD~S8_LD(비어 있음) · 시퀀스 SQ_STLM_LD · 기록 T_LD_RUN · T_LD_SESS · T_LD_LOG(시운전 기록) · 뷰 V_LD_PIECE · V_LD_MINUTE · V_LD_FINGERPRINT · sqlp의 DBMS_HPROF 실행 권한 |
P1 | rev02에서 완결(rev03 없음) |
| 주간 보정 배치 | ADJ_rev01: 리비전 파일(연습 DB의 운영 패키지는 바꾸지 않았다) |
| 새 산출물 | T3 2호(9번 칸 처음으로) · PKG_LOAD_DRIVER(환경 폴더 L1) |
| 열어 둔 것 | 시스템 전체: AWR → 16화 · 평균에 묻힌 1분: ASH → 17화 · 등급표를 화면에: 결과 캐시 → 15b화 |
다음 → 15b화 「같은 답을 두 번 계산하지 마라」 · 16화 「처음으로 시스템 전체를 본다」: 오늘 만든 부하 발생기가 처음으로 연습 DB를 시끄럽게 한다.