Skip to content

15화 · 실습과 기록 ​

여기서 하는 일: 커서 FOR 루프 안을 재 보고, 트레이스 한 줄을 읽고, 주간 보정 배치를 정합성까지 고친다. 그리고 다음 주에 쓸 부하 발생기를 설치해 시운전하고 T3 배치 SLA 리포트 2호를 쓴다.1_사건.md를 읽은 것은 "선배가 하는 걸 지켜본 것"이고, 이 문서를 끝내야 직접 해 본 것이 된다. 개념이 막히면 2_파고들기.md를 펼친다.

시간이 없는 주에는 STEP 0 → 1 → 2 → 4 → 6 → 9 → 10만 한다(약 60분). 트레이스, HPROF, NOCOPY는 다음 주에 돌아와서 해도 된다. 단, STEP 9는 빼면 안 된다. 16화가 이 위에서 시작한다.


이 화의 실습 지도 ​

스크립트무엇계정소요
실습/00_준비와_점검.sql14화 종료 상태 확인 · 복사본(C15) · 판(版)들 · 수첩sqlp10초
실습/01_P1_루프의_내부.sql★ P1 rev00 vs rev01 · 자동 배열 페치 · 레벨 1·2·3 · 왕복 한 번의 비용sqlp15초
실습/02_10046_TKPROF.sql★ 트레이스 다섯 판(P1 레벨 2·1 · 보정 rev00·rev01 · SQL*Plus ARRAYSIZE)sqlp20초 + TKPROF
실습/03_보정배치_rev00.sql★ 보정 rev00: 한 문장의 비용 · 배치의 비용 · 정합성(170건)sqlp10초
실습/04_한문장의_함정.sqlMERGE + 함수 · 스칼라 서브쿼리 캐시 · NO_DATA_FOUND → NULL · LOG ERRORS · MATERIALIZEsqlp15초
실습/05_보정배치_rev01.sql★ rev01: FORALL MERGE의 함정 · LIMIT · ERROR_INDEX · 정합성sqlp15초
실습/91_HPROF_권한_SYSDBA.sqlDBMS_HPROF 실행 권한(한 번)SYS1초
실습/06_HPROF_계층프로파일.sql계층 프로파일 rev00 vs rev01sqlp10초
실습/07_NOCOPY와_패키지상태.sqlNOCOPY · 패키지 변수와 UGA · 레벨 3 인라인sqlp10초
실습/07a_상태_세션A.sql · 07b_재컴파일_세션B.sqlORA-04068: 두 세션 동시에sqlp × 225초
../00_자료준비/고객사/L1_ora-hanorder/05_PKG_LOAD_DRIVER.sql · 05_부하_돌리기.sh · .ps1★★ 부하 발생기 설치 · 시운전(환경 폴더)sqlp3분 × 2
실습/99_정리.sqlC15 · HPROF 테이블 삭제 · 14화 종료 상태 확인(드라이버는 남긴다)sqlp수 초

PowerShell에서는 먼저 $OutputEncoding = New-Object System.Text.UTF8Encoding $false 한 줄을 실행한다. 실행 명령은 각 스크립트 머리말에 적힌 두 줄(Git Bash / PowerShell) 그대로다. 예:

bash
docker 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 · pkg1000000 · 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 ​

판경과CPUrecursive callsexecute countcur_fetch논리 읽기☐
A rev00 L284~9044~45101,350100,0281,001110,383☐
A rev0174~51833607,403☐

[B][C] 가져오는 쪽 · 레벨 ​

판cur_fetchrecursive calls논리 읽기경과☐
B forloop L21,0011,0081,8274~5☐
B explicit L2100,001100,009100,00916☐
C forloop L1100,001100,008100,00915~16☐
C rev00 L1100,001200,386~200,387208,622~208,62594~102☐
C rev00 L31,001101,386110,43087~98☐
☐ 레벨 2 의 커서 FOR 루프는 100행씩 가져온다 (rows ÷ fetch = 100)
☐ 명시적 커서의 FETCH … INTO 는 레벨 2 에서도 한 건씩이다
☐ rev00 의 시간 중 '가져오기' 는 4 cs — 나머지는 루프 안의 INSERT 10만 번
☐ 끝의 확인에서 두 프로시저의 레벨이 다시 2 다

[D] 왕복 한 번의 비용 — 마지막 표 ​

pl_to_sql_ussql_to_pl_ussql_to_udf_us
약 2.5약 0.530.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.txtSELECT ORD_SEQ … Fetch · rows1,001 · 100,000☐
〃INSERT INTO T_STLM_P1_C15 Execute100,000☐
c15_C15_P1L1.txtSELECT ORD_SEQ … Fetch · query100,001 · 100,009☐
c15_C15_ADJ00.txtOVERALL RECURSIVE Execute · Fetch299,865 · 200,892☐
〃SELECT CUS_GD … Execute · Fetch · rows100,020 · 100,020 · 99,870: Fetch ≈ Execute☐
〃INSERT INTO T_STLM_ADJ_C15 Execute · rows99,870 · 99,850(20은 ORA-01400)☐
c15_C15_ADJ01.txtOVERALL RECURSIVE Execute · Fetch290 · 103☐
〃SELECT CUS_GD …없다 ← 아래 함정☐
c15_C15_CLI.txtarray15 · array100의 Fetch6,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
cepcrcumisrdepogplh
3이어야11DISPLAY_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] 배치경과CPUrecursive calls함수 호출논리 읽기redo커밋☐
B rev00 first223~224141~142301,185100,020816,98455.75MB100☐
B rev00 again159~171126~128301,139100,020803,47226.67MB100☐
한 건당3.018.17☐

[C] 정합성 — 08화 체크리스트로 채운다 ​

#항목대상 / 방법결과판정
A기준원장 = T_ORD_STLM_C15 전체100,020정함
B건수보정 이력 · 반송 · 오류 기록99,850 · 0 · 0
B원장에만 있는 정산MINUS170
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 set223~32cs · 논리 읽기 16,609 · 이력 99,850 · 반송 170☐
F set2+LE105~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 merge130~1472,2602,000426,66953.89MB☐
B rev01 first27~282,4572,00022,36311.24MB☐
B rev01 again73~752,4062,000214,08423.29MB☐
B rev00 first214~219301,187100,020816,42555.77MB☐
C limit 10 / 100 / 1000 / 10000 / 10000032,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_rowsadj_rowserr_rowsoverlapunaccounteddup_seq☐
100,02099,850170000☐
반송 사유건수☐
1403 NO_CUS: customer not in master150☐
1400 NO_AMT: amount not fixed20☐
☐ 보정액 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_계층프로파일.sql
powershell
Get-Content 실습\91_HPROF_권한_SYSDBA.sql -Encoding UTF8 | docker exec -i ora-hanorder sqlplus -s / as sysdba

91 없이 06을 돌리면 첫 줄에서 PLS-00201: identifier 'DBMS_HPROF' must be declared로 멈춘다(교재 실측, 스크립트가 WHENEVER SQLERROR EXIT로 바로 끝난다). 패키지도 동의어도 있다. 실행 권한이 없을 뿐이다.

RUNID부름횟수그 아래 전부☐
1판 전체(rev00)12.38~2.39초☐
1__static_sql_exec_line11 INSERT99,8701.47~1.50초☐
1FN_ADJ_RATE_C15100,0200.41~0.42초☐
1__static_sql_exec_line9 COUNT99,8700.37초☐
1__sql_fetch_line6 페치1,0010.02초☐
2판 전체(rev01)10.26~0.39초☐
2FORALL INSERT101☐
2FN_ADJ_RATE_C152,0000.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 x1000196~202cs☐
[A] IN OUT NOCOPY x10000cs☐
[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 · 30.16 · 0.16 · 0.10초☐
07a A2 statelessPKG_LOAD_DRIVER는 그냥 된다(설치 전이면 PLS-00201)☐
07a A2 statefulORA-04068 · ORA-04061 PKG_RATE_C15. 그리고 그 뒤 출력이 사라진다☐
07a A3cached=1(빈 상태로 새로)☐
07a A5ORA-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) ​

STEADYSPIKE (NL · LIT · LOCK)☐
세션 실행 수 · 에러 · 끝난 이유873 · 878 · 0 · DEADLINE587 · 656 · 0 · DEADLINE☐
0분째 Q1 · Q2 · Q3 · Q4 · N1 · P1168 · 19 · 69 · 243 · 79 · 8같다(같은 씨앗)☐
1분째179 · 30 · 64 · 222 · 74 · 9Q1 28 · Q2 rev00 7 · Q3 3 · Q4 lit 21 · N1 hotfix 7 · N1 hold 6 · P1 3☐
2분째174 · 33 · 55 · 245 · 74 · 6181 · 26 · 61 · 233 · 74 · 7☐
조각 합계Q1 521 · Q2 82 · Q3 188 · Q4 710 · N1 227 · P1 23Q1 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 1N1 hotfix 43(enq: TX - row lock contention) · P1 rev02 6 · Q2 rev01 2 · Q3 1 · Q2 rev00 1☐
원본 지문 앞 = 뒤같다같다☐
남은 드라이버 세션00☐

실행 수와 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] · 이력이 빈 첫 판)

판처리경과redoundorecursive calls논리 읽기주된 대기출처
rev0099,850 보정 · 170 사라짐214~219cs55.77MB16.71MB301,187816,425log file switch (checkpoint incomplete)(TKPROF INSERT)05 [B] · 02 [T3]
rev0199,850 보정 · 170 반송27~28cs11.24MB3.69MB2,45722,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. 실패 · 재기동 · 멱등

항목rev00rev01
중간에 죽으면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)

rev00rev01
User Call (NON-RECURSIVE) Execute1 (+ 트레이스 끄기 1)1 (+ 1)
Recursive Call Execute · Fetch299,865 · 200,892290 · 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_rev006/3214~224cs55.77MB+ 1,000건 커밋(12화) · 보정 이력 · COUNT(*)(13화 뒤)
ADJ_rev016/1027~28cs11.24MBBULK 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문 (오늘 버전) ​

  1. 조건 반전: "보정률 규칙을 정산팀이 아니라 우리가 갖고 있고 화면이 그 함수를 쓰지 않는다면, 그래도 rev01이 맞나?"
  2. 반례 요구: "WHEN OTHERS를 써도 되는 경우를 하나 든다면? 그때 반드시 해야 할 일은?"
  3. 측정 지목: "운영의 보정 배치에 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를 찾았다 · raw FETCH 한 줄의 칸을 채웠다 · 사라진 커서를 raw에서 셌다
  • [ ] STEP 4: 정합성 체크리스트로 170건을 찾았다
  • [ ] STEP 5: 함수 호출 100,020 · SQL 안의 NO_DATA_FOUND = NULL · LOG ERRORS의 비용을 봤다
  • [ ] STEP 6: rev01의 원장 = 보정 + 반송 · 빠진 것 0을 확인했다 · ERROR_INDEX 6 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 실행 권한
P1rev02에서 완결(rev03 없음)
주간 보정 배치ADJ_rev01: 리비전 파일(연습 DB의 운영 패키지는 바꾸지 않았다)
새 산출물T3 2호(9번 칸 처음으로) · PKG_LOAD_DRIVER(환경 폴더 L1)
열어 둔 것시스템 전체: AWR → 16화 · 평균에 묻힌 1분: ASH → 17화 · 등급표를 화면에: 결과 캐시 → 15b화

다음 → 15b화 「같은 답을 두 번 계산하지 마라」 · 16화 「처음으로 시스템 전체를 본다」: 오늘 만든 부하 발생기가 처음으로 연습 DB를 시끄럽게 한다.