테마
15화 · 파고들기
조회용 문서다. 처음 읽을 때는
1_사건.md만으로 충분하다. 이 문서의 숫자는 모두 실측값이다.ora-hanorder(14화 종료 상태)의 복사본(_C15)에서 쟀고, 스크립트 번호를 함께 적었다. 모든 스크립트는 두 번 돌려 판정이 같게 나오는지 확인했다. 경과 시간(cs = 1/100초)은 그날 장비 사정에 따라 흔들린다. 판정은 recursive calls, execute count, fetch 수, 논리 읽기, redo로 하고, 초는 참고로만 읽는다.
| 찾는 것 | 바로 가기 |
|---|---|
| SQL ↔ PL/SQL 왕복 한 번의 비용 · 루프 판과 한 문장 판의 호출 수 | §1 컨텍스트 스위칭 |
PLSQL_OPTIMIZE_LEVEL 1 · 2 · 3: 커서 FOR 루프의 자동 배열 페치 · 인라인 | §2 레벨 |
raw 10046 한 줄: c e p cr cu mis r dep og plh tim | §3 raw 해독 |
TKPROF: NON-RECURSIVE / RECURSIVE · Fetch ≈ Execute · 배열 크기 · 사라지는 커서 · 꺼내는 절차 | §4 TKPROF |
BULK COLLECT LIMIT · FORALL · INDICES OF · SAVE EXCEPTIONS · ERROR_INDEX의 함정 | §5 묶어 보내기 |
| 패키지 상태 · UGA · ORA-04068 · 상태 없는 패키지의 조건 | §6 패키지 상태 |
NOCOPY: 효과와 대가 | §7 NOCOPY |
한 문장의 함정: SQL 안의 함수 · 스칼라 서브쿼리 캐시 · NO_DATA_FOUND → NULL · LOG ERRORS의 비용 · 뷰 병합 | §8 한 문장 |
WHEN OTHERS THEN NULL 구문과 08화 체크리스트 · 반송함 | §9 정합성 |
DBMS_HPROF: Free에서 쓰는 법 · 읽는 법 | §10 HPROF |
| 한 문장인가 PL/SQL인가: 판단 순서 · 「커밋 주기 기준」 경로 | §11 결정 |
PKG_LOAD_DRIVER 설계 요지 · 검증 | §12 부하 발생기 |
| 측정값 전부 | §13 측정 |
| Free에서 못 잰 것 · 재지 않은 것 | §14 한계 |
| 원천 좌표 | §15 좌표 |
1. SQL ↔ PL/SQL 왕복 — 컨텍스트 스위칭의 비용
PL/SQL과 SQL은 서로 다른 엔진이다. PL/SQL이 SQL 한 문장을 실행할 때마다, 그리고 SQL이 행 하나를 처리하다 PL/SQL 함수를 부를 때마다 엔진을 건너간다. 이렇게 한 번 건너가는 것이 '컨텍스트 스위칭'이고, PL/SQL이 부른 SQL은 V$MYSTAT의 recursive calls 항목에 쌓인다.
1.1 한 번의 비용 (실습/01 [D] · 10만 번 × 3회 평균 · 두 번 돌려 같은 값)
| 방향 | 판 | 대조판 | 한 번의 비용 |
|---|---|---|---|
| PL/SQL → SQL | 루프에서 SELECT v+1 INTO v FROM dual 10만 번(26~28cs · recursive calls 100,004) | 같은 일을 PL/SQL 대입으로(1~2cs) | 약 2.5µs |
| SQL → PL/SQL | SELECT SUM(FN(STLM_AMT)) FROM T_ORD_STLM: 순수 PL/SQL 함수를 10만 행마다(7~8cs) | 같은 식을 SQL에 직접(2~3cs) | 약 0.53µs |
SQL → PL/SQL (PRAGMA UDF) | 같은 함수에 PRAGMA UDF(4~5cs) | 〃 | 약 0.13~0.2µs |
- SQL → PL/SQL 방향은 recursive calls에 잡히지 않는다(위 두 판 모두 recursive calls 5~8). 함수 '안에' SQL이 있으면 그 SQL이 잡힌다(§8).
- 한 번의 비용은 작다. 문제는 여기에 곱해지는 횟수다. 보정 배치 rev00은 정산 한 건마다 SQL로 세 번 건너갔다. 10만 건이면 30만 번이다.
1.2 같은 일을 루프로 · 집합으로 (실습/01 [A] · 03 [B] · 05 [B])
| 판 | 일 | 경과 | CPU | recursive calls | execute count | 논리 읽기 |
|---|---|---|---|---|---|---|
| P1 rev00 커서 FOR 루프 | 10만 건 적재 | 84~90cs | 44~45cs | 101,350 | 100,028 | 110,383 |
P1 rev01 INSERT … SELECT | 〃 | 7cs | 4~5cs | 183 | 36 | 7,403 |
보정 rev00 (루프 · 행마다 함수 · COUNT(*) 분기) | 정산 100,020건 보정 | 214~224cs | 134~142cs | 301,185 | 299,768 | 816,984 |
| 보정 rev01 (BULK · FORALL · 등급표) | 〃 | 27~28cs | 14~15cs | 2,457 | 2,298 | 22,363 |
- 보정 rev00은 정산 한 건당 recursive calls 3.01 · 논리 읽기 8.17 · CPU 14µs다(
실습/03[B]). - 'A empty'(아무것도 안 하는 판)는 recursive calls가 6이다. 수첩(스냅샷) 자체가 보태는 몫이다.
2. PLSQL_OPTIMIZE_LEVEL 1 · 2 · 3
이 DB의 기본값은 2다(v$parameter). 레벨은 컴파일할 때 정해진다. ALTER PROCEDURE … COMPILE PLSQL_OPTIMIZE_LEVEL = n으로 바꾸고, USER_PLSQL_OBJECT_SETTINGS로 확인한다.
2.1 커서 FOR 루프의 자동 배열 페치 (실습/01 [B][C] · 10만 건)
| 루프 | 레벨 | 구동 커서 fetch | rows ÷ fetch | recursive calls | 논리 읽기 | 경과 |
|---|---|---|---|---|---|---|
| 커서 FOR 루프 (가져오기만) | 1 | 100,001 | 1 | 100,008 | 100,009 | 15~16cs |
| 커서 FOR 루프 (가져오기만) | 2 | 1,001 | 100 | 1,008 | 1,827 | 4~5cs |
| 커서 FOR 루프 (가져오기만) | 3 | 1,001 | 100 | 1,008 | 1,827 | 4cs |
명시적 커서 OPEN / FETCH … INTO / CLOSE | 2 | 100,001 | 1 | 100,009 | 100,009 | 16cs |
| P1 rev00 (가져오기 + 행마다 INSERT) | 1 | 100,001 | 1 | 200,387 | 208,622 | 94~102cs |
| P1 rev00 | 2 | 1,001 | 100 | 101,350 | 110,383 | 84~90cs |
| P1 rev00 | 3 | 1,001 | 100 | 101,386 | 110,430 | 87~98cs |
판정
- 레벨 2부터 커서 FOR 루프는 100행씩 묶어 가져온다. 설계(개정계획)의 질문 "2에서 자동 100행 배열 페치가 되는가"의 답은 예다.
- 명시적 커서의
FETCH … INTO는 레벨 2에서도 한 건씩이다. 묶으려면 코드에서BULK COLLECT를 써야 한다. - 한 건씩 가져오면 논리 읽기도 늘어난다(1,827 → 100,009). 같은 블록을 페치할 때마다 다시 잡기 때문이다(← 09화
ARRAYSIZE15에서 본 것). - 자동으로 묶이는 것은 '가져오는 쪽'뿐이다. 루프 안의 INSERT, UPDATE,
SELECT INTO는 레벨과 상관없이 행마다 나간다(P1 rev00은 레벨 2와 3에서도 recursive calls 10만).
2.2 레벨 3 — 지역 서브프로그램 인라인 (실습/07 [C] · 지역 함수 300만 번 · 두 번 돌려 같은 값)
| 레벨 | 경과 |
|---|---|
| 1 | 0.16초 |
| 2 | 0.16초 |
| 3 | 0.10~0.11초 |
레벨 3은 같은 프로그램 단위 안의 서브프로그램 호출을 그 자리에 풀어 넣는다(인라인). 레벨 2에서도 PRAGMA INLINE으로 하나씩 지시할 수 있다. 다른 단위(독립 함수나 다른 패키지)의 함수는 풀지 않는다.
⚠️ 측정의 함정(실측): 루프의 결과를 아무 데도 쓰지 않으면 레벨 2의 컴파일러가 루프를 통째로 지운다. 처음 잰 판은 300만 번에 0.01초로 나왔다. 결과를 패키지 변수에 남기도록 고친 뒤에야 0.16초가 나왔다.
3. raw 10046 한 줄 해독
10046 트레이스는 호출 하나를 한 줄로 적는다. 아래는 실습/02 [T3](보정 배치 rev00)에서 그대로 옮긴 줄이다. 파일은 64,595,665바이트, 800,876줄이었다(EXEC 299,867줄 · FETCH 200,892줄).
PARSING IN CURSOR #135811338131992 len=48 dep=1 uid=136 oct=3 lid=136 tim=2866994898 hv=1194102112 ad='7246c7e0' sqlid='8d8p8y93kt3b0'
SELECT CUS_GD FROM M_CUS_C15 WHERE CUS_ID = :B1
END OF STMT
PARSE #135811338131992:c=0,e=74,p=0,cr=0,cu=0,mis=1,r=0,dep=1,og=1,plh=0,tim=2866994897
EXEC #135811338131992:c=0,e=474,p=0,cr=0,cu=0,mis=1,r=0,dep=1,og=1,plh=1639701707,tim=2866995393
FETCH #135811338131992:c=0,e=9,p=0,cr=3,cu=0,mis=0,r=1,dep=1,og=1,plh=1639701707,tim=2866995418
CLOSE #135811338131992:c=0,e=1,dep=1,type=3,tim=2866995473| 필드 | 뜻 | 위 FETCH 줄에서 |
|---|---|---|
#135811338131992 | 커서 번호. 이 세션 안에서 같은 문장을 가리킨다. PARSING IN CURSOR 줄이 번호와 SQL 텍스트를 잇는다 | 보정률 함수 안의 SELECT CUS_GD … |
c | CPU(마이크로초) | 0. 짧은 호출은 CPU 측정 단위보다 작아서 0으로 찍힌다. 합계로 읽는다 |
e | 경과 시간(마이크로초) | 9µs |
p | 디스크에서 읽은 블록 | 0 |
cr | 일관 모드 읽기(consistent read) 블록 | 3: 인덱스 2 + 테이블 1(DISPLAY_CURSOR의 Buffers 3과 같다) |
cu | 현재 모드 읽기(current) 블록. DML이 고칠 블록을 잡을 때 생긴다 | 0(INSERT 줄에서는 cu=3) |
mis | 라이브러리 캐시에서 못 찾은 횟수. 1이면 하드 파싱 | 0(위 PARSE와 첫 EXEC는 1) |
r | 처리한 행 수 | 1(구동 커서의 FETCH는 r=100) |
dep | 재귀 깊이. 0 = 클라이언트가 보낸 호출 · 1 = PL/SQL(또는 트리거)이 부른 SQL · 2 = 그 SQL을 처리하느라 오라클이 부른 SQL | 1 |
og | 옵티마이저 목표: 1 ALL_ROWS · 2 FIRST_ROWS · 3 RULE · 4 CHOOSE | 1(딕셔너리 SQL은 og=4) |
plh | 실행계획 해시(Plan hash value) | 1639701707. DISPLAY_CURSOR와 같다 |
tim | 시각(마이크로초 단위 시계). 두 줄의 tim 차이 = 그 사이에 흐른 시간 | |
type (CLOSE) | 0 닫음 · 1 캐시에 넣음 · 2 캐시에 있던 것을 닫음 · 3 PL/SQL 커서 캐시에 남김 | 3 |
같은 파일의 다른 줄들도 보자.
FETCH #135811336855304:c=0,e=22,p=0,cr=1,cu=0,mis=0,r=100,dep=1,og=1,plh=3498552127,tim=2867001367 ← 구동 커서 : 한 번에 100행
EXEC #135811336771840:c=0,e=8,p=0,cr=0,cu=0,mis=0,r=0,dep=2,og=4,plh=813480514,tim=2866995622 ← dep=2 : 첫 INSERT 를 하드 파싱하며 obj$ 를 읽는다
PARSING IN CURSOR … dep=2 … update seg$ set type#=:4,blocks=:5,extents=:6, … ← dep=2 : 테이블이 커져 공간을 잡는다WAIT #…: nam='…' ela=… 줄은 대기 하나를 적은 것이다(level 8 이상). 이 파일에는 log file switch (checkpoint incomplete)가 있었다. 리두 로그가 10MB × 2라서 생긴 대기다(← 14화).
4. TKPROF 판독 — User Call과 Recursive Call
4.1 두 합계 (실습/02 [T3] · 보정 rev00 · 트레이스를 켠 채)
OVERALL TOTALS FOR ALL NON-RECURSIVE STATEMENTS ← User Call : 클라이언트(SQL*Plus)가 보낸 것
Parse 2 0.00 0.00 0 0 0 0
Execute 2 3.60 3.67 0 0 0 1
OVERALL TOTALS FOR ALL RECURSIVE STATEMENTS ← Recursive Call : 그 안에서 나간 것 전부 (dep ≥ 1)
Parse 6 0.00 0.00 0 0 0 0
Execute 299865 0.91 1.78 4 2390 312812 99852
Fetch 200892 0.19 0.18 1 501138 0 299761
total 500763 1.11 1.97 5 503528 312812 399613- NON-RECURSIVE의 경과 시간(3.67초)은 그 안의 RECURSIVE를 포함한다. 그러니 둘을 더하면 안 된다.
- 트레이스를 켜면 느려진다(rev00이 224cs → 367cs). 호출마다 줄을 하나씩 쓰므로 호출이 많을수록 더 느려진다.
- rev01(
[T4])은 RECURSIVE Execute 290 · Fetch 103 · total 400이고, NON-RECURSIVE 경과 시간은 0.05초다.
4.2 문장별 — Fetch ≈ Execute와 배열 크기 ([T3])
| SQL | Execute | Fetch | rows | rows ÷ Fetch | 읽는 법 |
|---|---|---|---|---|---|
구동 커서 SELECT STLM_SEQ, CUS_ID, STLM_AMT … | 1 | 1,001 | 100,020 | 100 | 레벨 2의 자동 배열 페치 |
보정률 함수 안 SELECT CUS_GD … = :B1 | 100,020 | 100,020 | 99,870 | 1 | Fetch ≈ Execute ≈ rows: 행마다 호출된 한 건 조회 |
SELECT COUNT(*) FROM T_STLM_ADJ … | 99,870 | 99,870 | 99,870 | 1 | 〃(IF EXISTS 분기) |
INSERT INTO T_STLM_ADJ … | 99,870 | — | 99,850 | — | current 312,710 ÷ 99,870 = 약 3블록/건 |
- 배열 크기 = rows ÷ Fetch. 클라이언트 쪽도 같다(
[T5]). SQL*Plus에서ARRAYSIZE 15로 10만 행을 받으면 Fetch 6,668 · query 7,202이고,ARRAYSIZE 100이면 Fetch 1,001 · query 1,562다(← 09화). - 등급 조회의 rows 99,870 = 100,020 − 150이다. 탈퇴 고객 150건은 0행을 돌려받았다(그리고 함수가
NO_DATA_FOUND를 던졌다). INSERT는 Execute 99,870 · rows 99,850이다. 20번은 실행됐지만 ORA-01400으로 행을 넣지 못했다(금액 미확정).
4.3 ☠️ 사라지는 커서 — 트레이스 전에 열린 커서는 TKPROF에 없다 ([T4] · 두 번 돌려 같다)
rev01의 TKPROF에는 보정률 함수 안의 SELECT CUS_GD …가 한 줄도 없다. 그런데 raw 파일에는 그 커서 번호의 EXEC 줄이 2,000개 있다(파일 전체 EXEC 2,297줄).
까닭은 이렇다. 스크립트가 트레이스를 켜기 직전에 rev01을 한 번 데웠다. 그때 열린 함수의 커서가 PL/SQL 커서 캐시에 남아 있었고, 트레이스 구간에서는 다시 파싱되지 않았다. PARSING IN CURSOR 줄이 없으면 TKPROF는 그 커서의 호출을 어느 SQL에도 붙이지 못하고 버린다. RECURSIVE 합계(Execute 290)에도 들어가지 않는다.
- 그래서 rev01의 '함수 2,000번'은 TKPROF가 아니라 raw 파일과
V$SQL.EXECUTIONS, HPROF(§10)로 확인했다. - 운영 세션에 트레이스를 걸 때는 늘 이런 일이 생긴다. 이미 돌던 세션에서 자주 쓰던 문장일수록 표에서 빠진다. TKPROF 합계가 이상하게 작으면 raw를 연다.
4.4 Recursive Call은 어디서 생기나 (이 화에서 본 것)
| 발생원 | dep | 이 화의 예 |
|---|---|---|
PL/SQL 안의 SQL(루프 안의 INSERT · SELECT INTO · 커서 페치) | 1 | P1 rev00 INSERT 100,000 · 보정 rev00 300,000 |
| SQL 안에서 부른 PL/SQL 함수, 그 함수 안의 SQL | 1~2 | 한 문장 MERGE + 함수: recursive calls 100,399(§8) |
| 하드 파싱의 딕셔너리 조회 | 2 | 첫 INSERT 파싱 때 obj$ 조회(og=4) |
| 공간 관리(익스텐트 할당) | 2 | update seg$ … |
| 동적 표본 추출(통계 없는 테이블) | 1~2 | rev01 트레이스의 SELECT /* OPT_DYN_SAMP */ 2문장(TRUNCATE한 보정 이력에 통계가 없어서) |
| 트리거 · 제약(FK) 확인 | 1 | 이 화에서는 재지 않았다(← 14화 FK · 행 트리거) |
User Call은 클라이언트가 보낸 Parse, Execute, Fetch 하나하나다. SQL*Plus가 10만 행을 ARRAYSIZE 15로 받으면 Fetch 6,668번이 전부 User Call이다. 같은 10만 행을 PL/SQL이 받으면 Recursive Call 1,001번이다.
4.5 트레이스를 꺼내고 TKPROF를 돌리는 절차 (← 11화 실습/90)
bash
# 경로는 V$DIAG_INFO 'Default Trace File' 이 알려 준다 (컨테이너 안)
T=/opt/oracle/diag/rdbms/free/FREE/trace
# Git Bash — /opt/... 를 Windows 경로로 바꾸지 않게 MSYS_NO_PATHCONV=1
MSYS_NO_PATHCONV=1 docker exec ora-hanorder sh -c "/opt/oracle/product/26ai/dbhomeFree/bin/tkprof $T/FREE_ora_NNN_C15_ADJ00.trc /tmp/c15_adj00.txt sys=no"
MSYS_NO_PATHCONV=1 docker cp ora-hanorder:/tmp/c15_adj00.txt .
MSYS_NO_PATHCONV=1 docker cp ora-hanorder:$T/FREE_ora_NNN_C15_ADJ00.trc . # raw 그 자체
MSYS_NO_PATHCONV=1 docker exec ora-hanorder sh -c "rm -f $T/*_C15_* /tmp/c15_*.txt" # 다 읽었으면tkprof는which로 찾아지지 않으므로ORACLE_HOME/bin전체 경로로 부른다. 파일은oracle:oinstall소유에 권한 640이지만,docker exec는oracle사용자로 들어가므로 그대로 읽을 수 있다.sys=no는 SYS가 파싱한 재귀 SQL(딕셔너리 조회 등)을 표에서 뺀다. 합계에는 그대로 들어간다.max_dump_file_size기본값은 32M이다. 보정 rev00의 트레이스(64MB)는 세션에서ALTER SESSION SET max_dump_file_size = UNLIMITED를 해 두어야 잘리지 않는다.
5. BULK COLLECT LIMIT · FORALL
5.1 모양
sql
OPEN c;
LOOP
FETCH c BULK COLLECT INTO v_seq, v_cus, v_amt, v_act LIMIT 1000; -- 천 건씩 받는다 (Fetch 1번)
EXIT WHEN v_seq.COUNT = 0; -- ★ c%NOTFOUND 가 아니라 COUNT 로
… PL/SQL 로 행마다 판단 (등급표 · 반송 분류) — 여기는 SQL 로 안 건너간다 …
FORALL i IN INDICES OF ins_rate SAVE EXCEPTIONS -- 천 건 중 INSERT 할 것만, 한 번에
INSERT INTO T_STLM_ADJ (…) VALUES (v_seq(i), ins_rate(i), …);
END LOOP;
CLOSE c;EXIT WHEN c%NOTFOUND를FETCH바로 뒤에 두면 마지막 덩어리(1,000건이 안 되는 나머지)를 버린다. 그래서COUNT = 0으로 판단해 나간다. (이 화의 정산 100,020건은 마지막 덩어리가 20건인데, 금액 미확정 20건이 정확히 거기에 있다.)LIMIT없는BULK COLLECT는 결과 전체를 세션 메모리(PGA)에 올린다.
5.2 LIMIT 고르기 (실습/05 [C] · 보정 rev01 · 정산 100,020건)
| LIMIT | recursive calls | execute count | 논리 읽기 | redo | 경과 | 판이 끝난 뒤 세션 PGA 변화 |
|---|---|---|---|---|---|---|
| 10 | 32,249 | 22,170 | 61,970 | 15.6MB | 44~51cs | — |
| 100 | 5,220 | 4,164 | 25,536 | 11.51MB | 28~45cs | — |
| 1,000 | 2,455 | 2,298 | 22,260 | 11.19MB | 28cs | 약 0.6MB |
| 10,000 | 2,111 | 2,045 | 22,544 | 11.16MB | 28~44cs | 약 3.6MB |
| 100,000 | 2,075 | 2,018 | 22,025 | 11.15MB | 31~40cs | 약 34MB |
1,000을 넘기면 왕복은 거의 줄지 않고(2,455 → 2,075) 메모리만 커진다. 10이면 왕복이 다시 늘어난다. 100~100,000 구간의 경과 시간 차이는 장비 잡음 범위 안에 있다.
5.3 FORALL은 왕복을 묶을 뿐 일을 묶지 않는다 (실습/05 [A])
| 판(정산 100,020건 · 이력이 빈 첫 판) | 경과 | recursive calls | 논리 읽기 | redo |
|---|---|---|---|---|
| rev00 루프 | 214~219cs | 301,187 | 816,425 | 55.77MB |
FORALL + 원소마다 MERGE … USING (SELECT v_seq(i) FROM dual) | 130~147cs | 2,260 | 426,669 | 53.89MB |
FORALL INSERT … VALUES / FORALL UPDATE(구동 커서에서 이력 바깥 조인으로 갈라서) | 27~28cs | 2,457 | 22,363 | 11.24MB |
FORALL은 원소 천 개를 SQL 엔진에 한 번에 넘긴다(왕복 1번). 그러나 SQL 엔진은 받은 문장을 원소마다 실행한다. MERGE … USING dual은 원소마다 대상 행을 찾아(인덱스 탐색) 있으면 UPDATE, 없으면 INSERT하는 행 단위 MERGE다. 그래서 논리 읽기와 redo가 루프 판과 거의 같다. 반면 INSERT … VALUES의 FORALL은 여러 행을 블록에 한꺼번에 넣는 배열 INSERT가 된다. 따라서 '이미 있나'는 행마다 묻지 말고 구동 커서에서 한 번에 갈라 둔다(바깥 조인 · NVL2(a.키, 'U', 'I')).
5.4 INDICES OF — 구멍 난 배열을 구멍째로
FORALL i IN 1 .. n은 1부터 n까지 모든 첨자가 있어야 한다(없으면 ORA-22160). 반송할 것을 빼고 INSERT와 UPDATE로 나누면 배열이 듬성듬성해진다. 이 배열을 그대로 INDICES OF 배열로 넘기면 있는 첨자만 돈다. (VALUES OF 첨자배열은 보낼 첨자를 따로 모은 배열로 고른다. 이 화에서는 쓰지 않았다.)
5.5 SAVE EXCEPTIONS와 ☠️ ERROR_INDEX의 함정 (실습/05 [D] · 두 번 돌려 같다)
SAVE EXCEPTIONS를 붙이면 실패한 원소만 모아 두고 나머지는 처리한 뒤, 끝에 ORA-24381을 한 번 던진다. 실패 목록은 SQL%BULK_EXCEPTIONS(j).ERROR_INDEX와 .ERROR_CODE로 읽는다.
첨자 1 ~ 10 중 3 · 5 를 지우고 8 에 NULL 을 심어 INDICES OF 로 INSERT
ERROR_INDEX = 6 → seq(6) = 900006 ← 그대로 첨자로 쓰면 엉뚱한 행
실제로 실패한 첨자 = 8 → seq(8) = 900008
ERROR_CODE = 1400 · SQLERRM(-1400) = "ORA-01400: cannot insert NULL into ()"ERROR_INDEX는 '몇 번째 반복'이다.INDICES OF나VALUES OF에서는 첨자가 아니므로, 지나간 순서대로 세어 첨자로 바꿔야 한다(rev01의save_bulk_errors).- 오류 번호만 남는다.
SQLERRM(-코드)로 만든 메시지에는 컬럼 이름이 없다("()").LOG ERRORS는 컬럼 이름까지 남기지만(§8.4) 그 대신 비싸다. - 그래서 rev01은 아는 실패를 이름으로 먼저 거르고(고객 없음 · 금액 미확정 → 반송함에 사유),
SAVE EXCEPTIONS는 모르는 실패를 잡는 그물로 둔다. 이 화의 데이터에서 그물에 걸린 것은 0건이다.
6. 패키지 상태와 UGA
6.1 패키지 변수는 세션 내내 남는다 (실습/07 [B])
패키지 본문(또는 스펙)에 선언한 변수는 그 패키지를 처음 부를 때 만들어져 세션이 끝날 때까지 UGA(User Global Area: 세션 몫의 메모리로, 전용 서버면 PGA 안에 있다)에 남는다.
| 측정 | |
|---|---|
등급표(고객 1,997명 · TABLE OF NUMBER INDEX BY VARCHAR2(40))를 패키지 변수에 채운다 | 함수 호출 1,997 · session uga memory +261,920~+327,400바이트. UGA는 64KB(65,480B) 덩어리로 늘어나므로, 앞서 잡아 둔 여유에 따라 덩어리 4~5개 사이에서 흔들린다 |
| 같은 세션에서 다시 돈다 | 함수 호출 0 · UGA 변화 0 |
다음 최상위 호출(새 EXEC) | 등급표 1,997명 그대로 |
세션 수만큼 복사본이 생긴다. 정산 등록 화면 세션이 200개면 등급표도 200벌이다.
6.2 ☠️ ORA-04068 — 누가 패키지를 다시 컴파일하면 (실습/07a · 07b · 두 세션 · 두 번 돌려 같다)
A2 stateless PKG_LOAD_DRIVER last_run=3 ← 상태 없는 패키지 : 그냥 된다
A2 stateful PKG_RATE_C15 ORA-04068: existing state of packages has been discarded
ORA-04061: existing state of package body "SQLP.PKG_RATE_C15" has been invalidated
(그다음 줄들이 화면에 안 나온다 — SERVEROUTPUT 을 다시 켜야 보인다)
A3 stateful PKG_RATE_C15 cached=1 ← 다시 부르면 빈 상태로 새로 시작
A5 constant PKG_CONST_C15 ORA-04068 … ← 스펙에 '상수' 하나뿐인 패키지- 다시 컴파일된 패키지의 상태를 쥔 세션이 그 패키지를 부르면 ORA-04068이 한 번 난다. 그리고 그 세션의 패키지 상태가 전부 버려진다. 다른 패키지의 변수와
DBMS_OUTPUT설정까지 버려진다(그래서 출력이 사라졌다). - 상태 없는 패키지란 스펙에도 본문에도 패키지 수준의 변수, 상수, 커서가 없는 패키지다. 이런 패키지는 다시 컴파일돼도 ORA-04068을 던지지 않는다.
- 상수 하나로도 상태가 생긴다(실측).
PKG_LOAD_DRIVER의 첫 판은 스펙에 기본 가중치 문자열CONSTANT하나를 두었다가 ORA-04068을 맞았다. 그 상수를 함수(DEFAULT_WEIGHTS)로 바꾸자 상태가 없어졌다.PKG_CONST_C15(상수 하나 + 함수 하나)도 결과가 같다. - 대책: 자주 바뀌는 패키지에는 상태를 두지 않는다. 꼭 둬야 한다면 부르는 쪽에서 ORA-04068을 잡아 한 번 다시 부르도록 처리한다.
PRAGMA SERIALLY_REUSABLE은 호출이 끝날 때마다 상태를 버린다(이 화에서는 재지 않았다).
6.3 ☠️ 자기 자신을 기다리는 컴파일 (library cache pin · 실측)
sql
BEGIN
EXECUTE IMMEDIATE 'ALTER PROCEDURE INL_C15 COMPILE PLSQL_OPTIMIZE_LEVEL = 2';
INL_C15; -- 이 블록이 INL_C15 를 부른다 → 블록이 INL_C15 의 핀을 쥔다
END;익명 블록이 정적으로 부르는 프로시저를 같은 블록 안에서 ALTER … COMPILE하면, 컴파일이 그 블록이 쥔 핀을 기다린다. 세션이 스스로를 기다린 것이다. library cache pin 대기가 800초에 이르러 SYS로 세션을 끊어야 했다. ALTER … COMPILE은 최상위 문장으로 따로 돌린다(실습/07은 그렇게 고쳤다). 같은 이유로 부하 발생기가 도는 동안 05_PKG_LOAD_DRIVER.sql을 다시 돌리면 설치 세션이 멈춘다. 돌고 있는 세션들이 패키지의 핀을 쥐고 있기 때문이다.
7. NOCOPY
IN OUT 매개변수는 기본이 값 복사다. 들어갈 때 복사본을 만들고, 정상으로 끝나면 결과를 원본에 다시 복사한다. NOCOPY는 참조로 넘기라는 힌트다.
7.1 효과 (실습/07 [A] · 원소 100,000개 TABLE OF VARCHAR2(40) INDEX BY PLS_INTEGER · 1,000번 호출 · 세 번)
| 매개변수 | 1,000번 | 한 번 |
|---|---|---|
IN OUT | 196~202cs | 약 2ms |
IN OUT NOCOPY | 0cs | — |
7.2 대가 — 예외가 나면
IN OUT 예외 뒤 x(1) = ORIG ← 복사본에서 고쳤다가 예외로 끝났으니 원본에 안 돌아온다
IN OUT NOCOPY 예외 뒤 x(1) = CHANGED ← 원본을 직접 고쳤다 — '반쯤 고친' 배열이 부른 쪽에 남는다NOCOPY는 원자성을 내준다(← 13화 '반쯤 반영'의 PL/SQL 판). 부른 쪽이 예외를 잡고 계속 진행한다면, 그 배열은 더 이상 믿을 수 없다.
복사 비용은 자료형과 상황에 따라 다르게 나온다. 처음 시험에서 같은 값을 다시 넣는 판(
x(1) := x(1))은 컴파일러가 지워 버려서IN OUT도 0cs였다. NOCOPY의 효과는 내 코드와 내 버전에서 재 보고 쓴다.
8. 한 문장의 함정
8.1 같은 일(정산 100,020건 · 보정 99,850 + 반송 170)을 한 문장으로 (실습/04 · 두 번 돌린 범위)
| 판 | 경과 | CPU | recursive calls | 함수 호출 | 논리 읽기 | redo |
|---|---|---|---|---|---|---|
[A] MERGE + 함수 행마다 · 오류 기록 없음 | 3~4cs에 실패 | — | 654 | 569(실패 시점까지) | 1,845 | 0.03MB |
[B] [A] + LOG ERRORS | 155~162cs | 78~80cs | 100,399 | 100,020 | 616,714 | 55.90MB |
[C] 함수를 스칼라 서브쿼리로 + LOG ERRORS | 146~147cs | 63~64cs | 58,025 | 57,679 | 490,610 | 55.94MB |
| [E] 규칙을 SQL로(CASE) · 반송은 따로 | 23~32cs | 10cs | 58 | 0 | 16,609 | 11.15MB |
[F] [E]의 MERGE + LOG ERRORS(오류 0건) | 105~117cs | 33~34cs | 57~85 | 0 | 316,251 | 55.84MB |
[G] 함수 · 고객마다(WITH + /*+ MATERIALIZE */) | 25~34cs | 11cs | 2,078 | 1,997 | 22,711 | 11.17MB |
| [G'] 같은 문장 · 힌트 없음 | 67~74cs | 53~55cs | 99,907 | 99,850 | 315,561 | 11.16MB |
| (비교) 보정 rev01 PL/SQL | 27~28cs | 14~15cs | 2,457 | 2,000 | 22,363 | 11.24MB |
8.2 SQL 안의 함수 — 루프가 문장 안으로 숨는다
[B]는 '한 문장'이지만 함수가 행마다 호출된다. SQL → PL/SQL로 한 번 건너가고, 그 함수 안의 SELECT가 PL/SQL → SQL로 또 한 번 건너간다(recursive calls 100,399). 성능고도화 I권에서 'Recursive Call을 포함하는 함수'라고 부르는 것이다.
8.3 ☠️ 함수 안의 NO_DATA_FOUND는 SQL 안에서 NULL이 된다 (실습/04 [D])
PL/SQL 에서 FN_ADJ_RATE('CUS_000777') → raised ORA-01403: no data found
SQL 에서 FN_ADJ_RATE('CUS_000777') → (NULL - 에러 없음)SQL이 부른 함수가 NO_DATA_FOUND를 밖으로 던지면 SQL 엔진은 이를 '그 행에는 값이 없다'로 읽는다. [A]는 보정률 칸이 NOT NULL이라 ORA-01400으로 터졌다(10만 건 전부 되돌림). 그 칸이 NULL을 받는 칸이었다면 보정률과 보정액이 NULL인 이력이 에러 없이 쌓였을 것이다. 따라서 SQL에서 부를 함수는 NO_DATA_FOUND를 안에서 처리하거나, 부르기 전에 '없는 키'를 따로 걸러 낸다.
8.4 LOG ERRORS의 비용 — 집합 처리가 행 단위가 된다 ([E] vs [F])
오류가 한 건도 나지 않는 같은 MERGE에 LOG ERRORS … REJECT LIMIT UNLIMITED만 붙였다. 23~32 → 105~117cs, 논리 읽기 16,609 → 316,251, redo 11.15 → 55.84MB. redo가 행 단위 루프 판(55.77MB)과 같아졌다. 오류를 행마다 기록할 수 있게 하려고, 문장이 묶어서 하던 변경을 한 행씩 한 것이다. 대신 LOG ERRORS는 오류 메시지에 컬럼 이름까지 남긴다([B]의 오류 기록: ADJ_RATE 150 · ADJ_AMT 20).
8.5 스칼라 서브쿼리 캐시는 고객 2,000명을 다 담지 못한다 ([C] · ← 07화)
06화 Q2의 지역 스칼라 서브쿼리는 Starts 8로 끝났다(값 8개). 오늘은 고객이 2,000명이고, 정산은 고객 번호 순이 아니라 돌아가며 들어온다. 캐시는 칸 수가 정해진 해시 표라서, 칸에 들지 못한 값은 매번 다시 부른다. 그래서 100,020 → 57,679로 42%만 줄었다. 캐시 효과는 값의 종류 수와 들어오는 순서에 달려 있다.
8.6 WITH 절과 뷰 병합 — 함수가 자리를 옮긴다 ([G] vs [G'])
힌트 있음 TEMP TABLE TRANSFORMATION → LOAD AS SELECT (고객 1,997명 × 함수) → HASH JOIN 함수 1,997번
힌트 없음 HASH JOIN (PK_M_CUS_C15 FAST FULL SCAN · T_ORD_STLM_C15 FULL) 함수 99,850번옵티마이저는 WITH 절을 본문에 합칠 수 있다(뷰 병합). 합치면 함수는 '고객 목록의 컬럼'이 아니라 '조인 결과의 컬럼'이 되어, 조인된 행마다 호출된다. 에러도 경고도 없다. MATERIALIZE(또는 NO_MERGE)로 막을 수 있지만, 그러면 성능이 힌트 하나에 걸린다(← 06화 힌트 유효기간 대장). 성능고도화 I권의 '뷰 머지 방지를 통한 함수 호출 최소화'가 바로 이것이다.
9. WHEN OTHERS THEN NULL과 정합성
9.1 무엇을 삼켰나 (실습/03 [C] · 08화 「정합성 회귀 체크리스트」)
| # | 항목 | rev00 | 판정 |
|---|---|---|---|
| A | 기준 | 원장 = 이번 주 정산 100,020건(직전 판이나 지난주 보정이 아니다) | — |
| B | 건수 · 합계 | 보정 이력 99,850 · 반송 기록 0 · 오류 기록 0 → 원장에만 있는 정산 170 | 불일치 |
| E | 경계 | 탈퇴 고객 3명의 정산 150(함수 NO_DATA_FOUND) · 금액 미확정 20(INSERT ORA-01400). 둘 다 WHEN OTHERS THEN NULL | 불일치 |
| B | 합계 | 탈퇴 고객 150건에 규정대로라면 적혔어야 할 보정액 114,864원(탈퇴 전 등급 = 원본 M_CUS) | 불일치 |
| H | 영향 | 정산 확정 화면(정산 ⟕ 보정 이력)은 그 170건을 '보정 0원'으로 보여 준다 | 불일치 |
WHEN OTHERS THEN NULL은 에러를 없애지 않는다. 에러가 났다는 사실을 없앤다. 배치는 '정상 종료'하고 오류 기록은 0이며, 건수 차이만 남는다. 그 차이도 원장과 대조할 때만 보인다.
9.2 ☠️ 직전 판과 대조하면 통과한다 (실습/05 [E])
rev01 99,850 = rev00 99,850 · MINUS 0 / 0rev00이 이미 170건을 빼먹었으니, rev01을 rev00과 대조하면 '같다'가 나온다. A 칸이 '직전 리비전 그리고 원장'인 이유가 여기에 있다(← 08화 Q3 rev00이 처음부터 50배였던 것).
9.3 반송함 — 넘긴 것을 적는다 (rev01)
원장 100,020 = 보정 99,850 + 반송 170 겹침 0 · 빠진 것 0 · 번호 중복 0
반송 사유 NO_CUS 150 · NO_AMT 20 · SAVE EXCEPTIONS 그물 0
보정액 73,638,572 = 규정대로 직접 계산 73,638,572 · MINUS 0| 규칙 | 까닭 |
|---|---|
아는 실패는 이름으로 잡는다(WHEN NO_DATA_FOUND, 금액 IS NULL 확인) | 사유가 업무 언어로 남는다 · 다른 에러까지 삼키지 않는다 |
모르는 실패는 SAVE EXCEPTIONS 그물로 잡아 ORA 번호와 함께 반송함에 넣는다 | 한 건 때문에 전체를 세우지 않는다(강도현 2019.06의 근거는 지킨다) |
WHEN OTHERS를 쓰면 기록하고 다시 올리거나(RAISE) 기록하고 센다. 절대 NULL로 끝내지 않는다 | 「SQL 코딩 가이드」 후보(→ 18화) |
| 반송함은 회차마다 지우고 다시 쓴다 | 다시 돌려도 결과가 같다(멱등) |
| 끝에 원장 = 처리 + 반송인지 센다 | 빠진 것이 0이어야 진짜 통과다 |
10. DBMS_HPROF — 계층 프로파일
10.1 Free에서 쓰는 법 (실측)
| 패키지 | SYS.DBMS_HPROF 있음(VALID) · PUBLIC 동의어 있음 |
| 권한 | PUBLIC에 EXECUTE가 없다. sqlp의 ALL_OBJECTS에 동의어만 보이고 본체는 안 보인다 → SYS가 GRANT EXECUTE ON DBMS_HPROF TO sqlp(실습/91) |
| 결과 테이블 | DBMS_HPROF.CREATE_TABLES(force_it => TRUE)가 내 스키마에 DBMSHP_RUNS · DBMSHP_FUNCTION_INFO · DBMSHP_PARENT_CHILD_INFO · DBMSHP_TRACE_DATA를 만든다(rdbms/admin/dbmshptab.sql은 옛 방식) |
| 디렉터리 | 필요 없다. START_PROFILING을 위치 없이 부르면 원자료를 DBMSHP_TRACE_DATA에 쓴다 |
| 부르는 법 | tid := DBMS_HPROF.START_PROFILING(sqlmonitor => FALSE, run_comment => '…') → 대상 실행 → DBMS_HPROF.STOP_PROFILING → rid := DBMS_HPROF.ANALYZE(trace_id => tid) |
| 판정 | Free에서 된다. 설계의 "안 되면 봉인"은 해당 없음(권한 한 줄이 필요했다) |
10.2 읽는 법 (실습/06 · 정산 100,020건 · 프로파일러를 켠 채 · 두 번 돌린 범위)
부름(DBMSHP_FUNCTION_INFO) | 횟수 | 그 아래 전부(subtree) | 한 번 |
|---|---|---|---|
| rev00 판 전체 | 1 | 2.38~2.39초 | |
__static_sql_exec_line11(INSERT) | 99,870 | 1.47~1.50초 | 약 15µs |
FN_ADJ_RATE_C15 | 100,020 | 0.41~0.42초 | 약 4.1µs |
└ __static_sql_exec_line4(함수 안 SELECT) | 100,020 | 0.36~0.37초 | 약 3.6µs |
__static_sql_exec_line9(COUNT(*)) | 99,870 | 0.37초 | 약 3.7µs |
__sql_fetch_line6(구동 커서 페치) | 1,001 | 0.02초 | |
__static_sql_exec_line22(COMMIT) | 100 | 0.004초 | |
| rev01 판 전체 | 1 | 0.26~0.39초 | |
__static_sql_exec_line68(FORALL INSERT) | 101 | 0.21~0.34초 | |
__sql_fetch_line45(BULK COLLECT) | 101 | 0.02초 | |
FN_ADJ_RATE_C15 | 2,000 | 0.007초 |
- 이름이
__static_sql_exec_lineN이면 그 단위 N번째 줄의 SQL 실행,__sql_fetch_lineN이면 커서 페치,__pkg_init은 패키지 초기화다. - FORALL 한 번이 한 부름이다. 천 건씩 묶어 보내니 101부름이 된다.
- TKPROF와 달리 트레이스 전에 열린 커서도 센다(§4.3의 함수 2,000번이 여기 있다).
- 프로파일러를 켜면 부름마다 시계를 찍느라 느려진다(rev00 224cs → 2.39초). 그러니 비율로 읽는다.
11. 한 문장인가 PL/SQL인가
13화 「커밋 주기 기준」 결정표의 #1 "집합 연산 한 문장으로 쓸 수 있나 → 그렇게 쓴다"는 그대로다. 이 화는 거기에 "쓰면 무엇을 잃나"라는 질문을 붙였다.
| 묻는 것 | 예 → | 보정 배치에서 |
|---|---|---|
| ① 행마다 판단할 규칙이 있나? | 없으면 한 문장(P1 rev01 · rev02) | 있다: 보정률(정산팀 규정) |
| ② 그 규칙을 SQL로 옮기면 규칙이 두 곳이 되나? (화면이나 다른 배치도 같은 함수를 쓴다) | 두 곳이면 옮기지 않는다 | 된다(정산 등록 화면이 같은 함수를 쓴다) → CASE 판 기각 |
| ③ 함수를 둔 채 한 문장으로 쓰면 함수가 몇 번 호출되나? | 행마다라면 루프가 숨은 것 | 100,020(그대로) · 캐시 57,679 · MATERIALIZE 1,997(힌트 하나에 기댄다) |
| ④ 한 행의 실패를 어떻게 다루나? | 전부 아니면 전무로 충분하면 한 문장 | 행마다 반송 + 사유가 필요 → 한 문장이면 LOG ERRORS(5배) 아니면 전체 롤백 |
| ⑤ 위에서 PL/SQL이 남으면 | 제대로 쓴다: BULK LIMIT · FORALL(묶이는 문장) · 고객마다 한 번 · 아는 실패는 이름으로 · SAVE EXCEPTIONS | rev01 = 27~28cs(한 문장 최선 23~34cs와 같은 수준) |
| ⑥ 그리고 같은 값이 나오는지 잰다 | 한 문장보다 크게 느리면 ②~④를 다시 따진다 | 같았다 |
커밋 주기(rev01): #1 한 문장으로 쓸 수는 있지만 ②③④ 때문에 쓰지 않는다 · #2 fetch across commit 아님(읽는 정산을 고치지 않는다) · #3 언두 3.66~3.69MB · → #4 끝에 한 번. 중간에 죽으면 전부 롤백되고, 반송함은 회차마다 다시 쓰므로 다시 돌리면 된다.
12. PKG_LOAD_DRIVER — 설계와 검증
설치, 인자, 프로필, 운용은 환경 폴더
00_자료준비/고객사/L1_ora-hanorder/README.md에 있다. 여기서는 이 화의 교훈이 어디에 들어갔나만 다룬다.
| 이 화의 교훈 | 드라이버에서 |
|---|---|
| 바인드 변수(← 11화) | 6조각 현재판은 전부 정적 SQL + 바인드 변수. 리터럴은 SPIKE의 'LIT' 판에만 일부러 넣었다 |
| 묶어서 받는다(§5) | 결과는 BULK COLLECT로 받는다 · 분 단위 기록은 메모리에 모았다가 FORALL INSERT로 한 번에 넣는다 |
| 에러를 삼키지 않는다(§9) | 조각마다 잡되 센다(T_LD_LOG.ERRS · LAST_ERR). 패키지 재설치나 세션 강제 종료 계열(ORA-04068 · 00028 · 01013 …)이면 세션을 끝낸다 · 연속 20번 실패해도 끝낸다 |
| 상태 없는 패키지(§6) | 패키지 변수 · 상수 0개. 모든 상태는 RUN_SESSION의 지역 변수 |
| 원본에 쓰지 않는다(← 03 · 13화) | P1 → 세션별 T_STLM_S<n>_LD · N1 → T_STLM_REG_LD · 핫픽스 채번 → T_STLM_NUM_LD · 원본 지문 뷰 V_LD_FINGERPRINT |
| 결정적(← 사양 §12) | Park–Miller 난수 · 씨앗 = p_seed × 1000 + 세션 번호 · DBMS_RANDOM은 쓰지 않는다 |
| 세션 구조 | job_queue_processes = 1(이 DB · 실측) → 셸이 sqlplus 세션 N개를 띄운다 |
검증 (2세션 × 3분 · 생각 시간 200 ms · 씨앗 15)
| STEADY | SPIKE (1분째부터 1분 · NL · LIT · LOCK) | SPIKE (1분째부터 1분 · NL · LIT · Q2 가중치 40) | |
|---|---|---|---|
| 세션 실행 수 | 873 · 878 | 587 · 656 | 776 · 782 |
| 에러 | 0 | 0 | 0 |
| 끝난 이유 | DEADLINE · DEADLINE | DEADLINE · DEADLINE | DEADLINE · DEADLINE |
| 0분째 조각별(Q1 · Q2 · Q3 · Q4 · N1 · P1) | 168 · 19 · 69 · 243 · 79 · 8 | 같다 | 같다 |
| 1분째(SPIKE) | — | Q1 28 · Q2 rev00 7 · Q3 3 · Q4 lit 21 · N1 hotfix 7 · N1 hold 6 · P1 3 | Q1 38 · Q2 rev00 152 · Q3 21 · Q4 lit 159 · N1 17 |
| ASH(MODULE = LOAD_DRIVER) | 9샘플: Q2 rev01 4 · P1 rev02 3(enq: RO - fast object reuse) · … | N1 hotfix 43(enq: TX - row lock contention) · P1 6 · Q2 rev01 2 · Q2 rev00 1 | Q2 rev00 39(ON CPU) · Q2 rev01 2 · Q3 1 |
| 원본 지문(앞 = 뒤) | 같다 | 같다 | 같다 |
| 남은 드라이버 세션 | 0 | 0 | 0 |
- 0분째는 세 런에서 같다. 씨앗이 같으면 순서도 같다. 몇 번째까지 가는지는 시간이 정한다(SPIKE 뒤의 2분째는 런마다 다르다. SPIKE 구간에서 고른 조각이 달라 난수를 쓴 횟수가 달라지기 때문이다).
- 2세션 LOCK SPIKE에서는 세션 1이 번호 행을 쥐고 세션 2가 그 뒤에 서므로, 다른 나쁜 판은 거의 돌지 않는다. CPU 폭발을 보려면
SPIKE_KIND=NL에 Q2를 키운SPIKE_WEIGHTS를 준다(셋째 열). - STOP: 3세션 LOCK SPIKE 18초째의
V$SESSION은 S1N1 hold(PL/SQL lock timer, Idle이라 ASH에는 찍히지 않는다), S2와 S3N1 hotfix(enq: TX - row lock contention·BLOCKING_SESSION = S1)였다.STOP_RUN뒤 1초 안에 세 세션 모두EXIT_REASON = STOP이 됐고, 남은 세션은 0이었다. - PowerShell 발사기(
.ps1)도 결과가 같다. Windows PowerShell 5.1 + UTF-8 콘솔(65001)에서는 파이프로 넘긴 글 앞에 BOM이 붙어 sqlplus가 첫 줄을 버렸다(SP2-0734).$OutputEncoding만으로는 안 되고[Console]::InputEncoding도 BOM 없는 UTF-8로 맞춰야 했다(발사기가 알아서 한다).
13. 이 화의 측정값 전부
보정 배치 rev00 → rev01 (실습/05 [B] · 두 번 돌린 범위)
| rev00 첫 판 | rev01 첫 판 | rev00 다시(UPDATE 쪽) | rev01 다시 | |
|---|---|---|---|---|
| 경과 | 214~219cs | 27~28cs | 169~180cs | 73~75cs |
| CPU | 134~138cs | 14~15cs | 127~128cs | 29cs |
| recursive calls | 301,176 ~ 301,187 | 2,452 ~ 2,457 | 301,140 ~ 301,143 | 2,400 ~ 2,406 |
| 함수 호출 | 100,020 | 2,000 | 100,020 | 2,000 |
| 논리 읽기 | 816,300 ~ 816,425 | 22,245 ~ 22,363 | 803,482 ~ 803,511 | 213,896 ~ 214,084 |
| redo | 55.74~55.77MB | 11.17~11.24MB | 26.65MB | 22.44~23.29MB |
| undo | 16.70~16.71MB | 3.66~3.69MB | 9.15MB | 6.97~7.40MB |
| 커밋 | 100 | 1 | 100 | 1 |
측정 환경
Oracle AI Database 26ai Free Release 23.26.2.0.0 · cpu_count 2 · plsql_optimize_level 2 · plsql_code_type INTERPRETED · job_queue_processes 1 · session_cached_cursors 50 · NOARCHIVELOG · 온라인 리두 로그 10MB × 2(← 14화) · control_management_pack_access DIAGNOSTIC+TUNING(ASH를 읽을 수 있다). 데이터: T_ORD_STLM 복사본 10만 + 금액 미확정 20 = 100,020건 · M_CUS 복사본 2,000 − 탈퇴 3 = 1,997명(고객마다 정산 50건).
14. Free에서 못 잰 것 · 재지 않은 것
| 못 잰 것 / 재지 않은 것 | 왜 | 대신 한 것 |
|---|---|---|
| 운영 보정 배치의 건당 시간 · 추세선 | 운영 로그에는 시작과 끝만 있다(단계별 기록은 체인에만 붙었다) | T3 2호 8번 칸에 '다음 달 두 배면 여유 음수'까지만 적음 · rev01 운영 첫 실행 뒤 재측정 |
PLSQL_CODE_TYPE = NATIVE | 이 화의 범위 밖 | — |
PRAGMA SERIALLY_REUSABLE · DETERMINISTIC · 결과 캐시 | 15b화의 몫 | §6.2 · §8에서 이름만 |
VALUES OF | rev01에는 필요 없었다 | §5.4에서 정의만 |
| 수백 세션의 UGA 압박 | CPU 2 · 세션 몇 개 | 세션 하나의 UGA 증가분(262~327KB) |
| 트리거 · FK가 만드는 재귀 호출 | 14화에서 잰 것 | §4.4 표에 '재지 않았다' |
| 대규모 동시 부하(수십 세션) | CPU 2 · 이 교재는 2~4세션 | PKG_LOAD_DRIVER는 8세션까지 받는다 · 대규모 EE 환경은 별권 |
15. 원천 좌표
전체 지도는
00_자료준비/06_원서와_공식문서_지도.md에 있다. PDF 페이지 = 책 페이지 − 2 ·성능고도화I_목차색인.md(공개 사이트에는 없음)
성능고도화 I권 (주 원천 — 05장 데이터베이스 Call 최소화 · 06장 I/O 효율화)
| 주제 | 책 페이지 |
|---|---|
| Call 통계 · User Call vs. Recursive Call · Call이 성능에 미치는 영향 | 346 · 349 · 352 |
| Array Processing 활용 · Fetch Call 최소화 · 프로그램 언어에서 Array 단위 Fetch | 363 · 369 · 385 |
| PL/SQL 함수의 특징과 성능 부하 · Recursive Call을 포함하지 않는 함수 · 포함하는 함수 | 392 · 393 · 394 |
| 함수를 필터 조건으로 쓸 때 · 함수와 읽기 일관성 · 함수의 올바른 사용 기준 | 396 · 400 · 403 |
| PL/SQL 함수 호출 부하 해소 방안: Decode/Case로 변환 · 뷰 머지 방지 · 스칼라 서브쿼리 캐싱 · Deterministic · 함수 로직을 풀어 SQL로 | 405 · 408 · 415 · 417 · 420 · 423 |
| (06장) 블록 단위 I/O · Sequential vs Random 액세스 | 432 · 434 |
오라클 공식문서
| 주제 | 매뉴얼 | 장 제목 |
|---|---|---|
BULK COLLECT · FORALL · SAVE EXCEPTIONS · INDICES OF · NOCOPY · 패키지 상태 | lnpls | PL/SQL Optimization and Tuning · PL/SQL Packages (Package State) · Formal Parameters (NOCOPY) |
PLSQL_OPTIMIZE_LEVEL | refrn · lnpls | Initialization Parameters · PL/SQL Optimizer |
DBMS_HPROF | arpls · adfns | DBMS_HPROF · Using the PL/SQL Hierarchical Profiler |
| 10046 · TKPROF | tgsql | Performing Application Tracing |
DML 오류 기록 (LOG ERRORS) · DBMS_ERRLOG | sqlrf · arpls | INSERT (error_logging_clause) · DBMS_ERRLOG |
DBMS_APPLICATION_INFO · DBMS_SESSION.SET_IDENTIFIER | arpls | DBMS_APPLICATION_INFO · DBMS_SESSION |
V$MYSTAT · V$STATNAME (recursive calls · user calls · session uga memory) | refrn | Statistics Descriptions |
다음 화가 여기서 가져가는 것
| 화 | 가져가는 것 |
|---|---|
| 15b | 화면이 쓸 등급표: 패키지 변수(§6)가 아니라 결과 캐시로 · DETERMINISTIC · 스칼라 서브쿼리 캐시의 칸(§8.5) |
| 16 | PKG_LOAD_DRIVER(§12): STEADY 부하 위에서 AWR을 처음 읽는다 · T_LD_LOG의 조각별 횟수를 AWR 'SQL ordered by Executions'와 대조 · MODULE = 'LOAD_DRIVER' |
| 17 | SPIKE 프로필: 평균에 묻히는 1분(NL의 CPU · LOCK의 블로킹 체인 enq: TX - row lock contention) · ACTION · CLIENT_IDENTIFIER |
| 18 | 「SQL 코딩 가이드」 후보: WHEN OTHERS THEN NULL 금지 · 행마다 부르는 함수는 횟수를 적는다 · FORALL에는 묶이는 문장을 · ERROR_INDEX는 첨자가 아니다 · T3 2호 |