Skip to content

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/SQLSELECT 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]) ​

판일경과CPUrecursive callsexecute count논리 읽기
P1 rev00 커서 FOR 루프10만 건 적재84~90cs44~45cs101,350100,028110,383
P1 rev01 INSERT … SELECT〃7cs4~5cs183367,403
보정 rev00 (루프 · 행마다 함수 · COUNT(*) 분기)정산 100,020건 보정214~224cs134~142cs301,185299,768816,984
보정 rev01 (BULK · FORALL · 등급표)〃27~28cs14~15cs2,4572,29822,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만 건) ​

루프레벨구동 커서 fetchrows ÷ fetchrecursive calls논리 읽기경과
커서 FOR 루프 (가져오기만)1100,0011100,008100,00915~16cs
커서 FOR 루프 (가져오기만)21,0011001,0081,8274~5cs
커서 FOR 루프 (가져오기만)31,0011001,0081,8274cs
명시적 커서 OPEN / FETCH … INTO / CLOSE2100,0011100,009100,00916cs
P1 rev00 (가져오기 + 행마다 INSERT)1100,0011200,387208,62294~102cs
P1 rev0021,001100101,350110,38384~90cs
P1 rev0031,001100101,386110,43087~98cs

판정

  • 레벨 2부터 커서 FOR 루프는 100행씩 묶어 가져온다. 설계(개정계획)의 질문 "2에서 자동 100행 배열 페치가 되는가"의 답은 예다.
  • 명시적 커서의 FETCH … INTO는 레벨 2에서도 한 건씩이다. 묶으려면 코드에서 BULK COLLECT를 써야 한다.
  • 한 건씩 가져오면 논리 읽기도 늘어난다(1,827 → 100,009). 같은 블록을 페치할 때마다 다시 잡기 때문이다(← 09화 ARRAYSIZE 15에서 본 것).
  • 자동으로 묶이는 것은 '가져오는 쪽'뿐이다. 루프 안의 INSERT, UPDATE, SELECT INTO는 레벨과 상관없이 행마다 나간다(P1 rev00은 레벨 2와 3에서도 recursive calls 10만).

2.2 레벨 3 — 지역 서브프로그램 인라인 (실습/07 [C] · 지역 함수 300만 번 · 두 번 돌려 같은 값) ​

레벨경과
10.16초
20.16초
30.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 …
cCPU(마이크로초)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을 처리하느라 오라클이 부른 SQL1
og옵티마이저 목표: 1 ALL_ROWS · 2 FIRST_ROWS · 3 RULE · 4 CHOOSE1(딕셔너리 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]) ​

SQLExecuteFetchrowsrows ÷ Fetch읽는 법
구동 커서 SELECT STLM_SEQ, CUS_ID, STLM_AMT …11,001100,020100레벨 2의 자동 배열 페치
보정률 함수 안 SELECT CUS_GD … = :B1100,020100,02099,8701Fetch ≈ Execute ≈ rows: 행마다 호출된 한 건 조회
SELECT COUNT(*) FROM T_STLM_ADJ …99,87099,87099,8701〃(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 · 커서 페치)1P1 rev00 INSERT 100,000 · 보정 rev00 300,000
SQL 안에서 부른 PL/SQL 함수, 그 함수 안의 SQL1~2한 문장 MERGE + 함수: recursive calls 100,399(§8)
하드 파싱의 딕셔너리 조회2첫 INSERT 파싱 때 obj$ 조회(og=4)
공간 관리(익스텐트 할당)2update seg$ …
동적 표본 추출(통계 없는 테이블)1~2rev01 트레이스의 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건) ​

LIMITrecursive callsexecute count논리 읽기redo경과판이 끝난 뒤 세션 PGA 변화
1032,24922,17061,97015.6MB44~51cs—
1005,2204,16425,53611.51MB28~45cs—
1,0002,4552,29822,26011.19MB28cs약 0.6MB
10,0002,1112,04522,54411.16MB28~44cs약 3.6MB
100,0002,0752,01822,02511.15MB31~40cs약 34MB

1,000을 넘기면 왕복은 거의 줄지 않고(2,455 → 2,075) 메모리만 커진다. 10이면 왕복이 다시 늘어난다. 100~100,000 구간의 경과 시간 차이는 장비 잡음 범위 안에 있다.

5.3 FORALL은 왕복을 묶을 뿐 일을 묶지 않는다 (실습/05 [A]) ​

판(정산 100,020건 · 이력이 빈 첫 판)경과recursive calls논리 읽기redo
rev00 루프214~219cs301,187816,42555.77MB
FORALL + 원소마다 MERGE … USING (SELECT v_seq(i) FROM dual)130~147cs2,260426,66953.89MB
FORALL INSERT … VALUES / FORALL UPDATE(구동 커서에서 이력 바깥 조인으로 갈라서)27~28cs2,45722,36311.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 OUT196~202cs약 2ms
IN OUT NOCOPY0cs—

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 · 두 번 돌린 범위) ​

판경과CPUrecursive calls함수 호출논리 읽기redo
[A] MERGE + 함수 행마다 · 오류 기록 없음3~4cs에 실패—654569(실패 시점까지)1,8450.03MB
[B] [A] + LOG ERRORS155~162cs78~80cs100,399100,020616,71455.90MB
[C] 함수를 스칼라 서브쿼리로 + LOG ERRORS146~147cs63~64cs58,02557,679490,61055.94MB
[E] 규칙을 SQL로(CASE) · 반송은 따로23~32cs10cs58016,60911.15MB
[F] [E]의 MERGE + LOG ERRORS(오류 0건)105~117cs33~34cs57~850316,25155.84MB
[G] 함수 · 고객마다(WITH + /*+ MATERIALIZE */)25~34cs11cs2,0781,99722,71111.17MB
[G'] 같은 문장 · 힌트 없음67~74cs53~55cs99,90799,850315,56111.16MB
(비교) 보정 rev01 PL/SQL27~28cs14~15cs2,4572,00022,36311.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 / 0

rev00이 이미 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 판 전체12.38~2.39초
__static_sql_exec_line11(INSERT)99,8701.47~1.50초약 15µs
FN_ADJ_RATE_C15100,0200.41~0.42초약 4.1µs
└ __static_sql_exec_line4(함수 안 SELECT)100,0200.36~0.37초약 3.6µs
__static_sql_exec_line9(COUNT(*))99,8700.37초약 3.7µs
__sql_fetch_line6(구동 커서 페치)1,0010.02초
__static_sql_exec_line22(COMMIT)1000.004초
rev01 판 전체10.26~0.39초
__static_sql_exec_line68(FORALL INSERT)1010.21~0.34초
__sql_fetch_line45(BULK COLLECT)1010.02초
FN_ADJ_RATE_C152,0000.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 EXCEPTIONSrev01 = 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) ​

STEADYSPIKE (1분째부터 1분 · NL · LIT · LOCK)SPIKE (1분째부터 1분 · NL · LIT · Q2 가중치 40)
세션 실행 수873 · 878587 · 656776 · 782
에러000
끝난 이유DEADLINE · DEADLINEDEADLINE · DEADLINEDEADLINE · 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 3Q1 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 1Q2 rev00 39(ON CPU) · Q2 rev01 2 · Q3 1
원본 지문(앞 = 뒤)같다같다같다
남은 드라이버 세션000
  • 0분째는 세 런에서 같다. 씨앗이 같으면 순서도 같다. 몇 번째까지 가는지는 시간이 정한다(SPIKE 뒤의 2분째는 런마다 다르다. SPIKE 구간에서 고른 조각이 달라 난수를 쓴 횟수가 달라지기 때문이다).
  • 2세션 LOCK SPIKE에서는 세션 1이 번호 행을 쥐고 세션 2가 그 뒤에 서므로, 다른 나쁜 판은 거의 돌지 않는다. CPU 폭발을 보려면 SPIKE_KIND=NL에 Q2를 키운 SPIKE_WEIGHTS를 준다(셋째 열).
  • STOP: 3세션 LOCK SPIKE 18초째의 V$SESSION은 S1 N1 hold(PL/SQL lock timer, Idle이라 ASH에는 찍히지 않는다), S2와 S3 N1 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~219cs27~28cs169~180cs73~75cs
CPU134~138cs14~15cs127~128cs29cs
recursive calls301,176 ~ 301,1872,452 ~ 2,457301,140 ~ 301,1432,400 ~ 2,406
함수 호출100,0202,000100,0202,000
논리 읽기816,300 ~ 816,42522,245 ~ 22,363803,482 ~ 803,511213,896 ~ 214,084
redo55.74~55.77MB11.17~11.24MB26.65MB22.44~23.29MB
undo16.70~16.71MB3.66~3.69MB9.15MB6.97~7.40MB
커밋10011001

측정 환경 ​

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 OFrev01에는 필요 없었다§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 단위 Fetch363 · 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 · 패키지 상태lnplsPL/SQL Optimization and Tuning · PL/SQL Packages (Package State) · Formal Parameters (NOCOPY)
PLSQL_OPTIMIZE_LEVELrefrn · lnplsInitialization Parameters · PL/SQL Optimizer
DBMS_HPROFarpls · adfnsDBMS_HPROF · Using the PL/SQL Hierarchical Profiler
10046 · TKPROFtgsqlPerforming Application Tracing
DML 오류 기록 (LOG ERRORS) · DBMS_ERRLOGsqlrf · arplsINSERT (error_logging_clause) · DBMS_ERRLOG
DBMS_APPLICATION_INFO · DBMS_SESSION.SET_IDENTIFIERarplsDBMS_APPLICATION_INFO · DBMS_SESSION
V$MYSTAT · V$STATNAME (recursive calls · user calls · session uga memory)refrnStatistics Descriptions

다음 화가 여기서 가져가는 것 ​

화가져가는 것
15b화면이 쓸 등급표: 패키지 변수(§6)가 아니라 결과 캐시로 · DETERMINISTIC · 스칼라 서브쿼리 캐시의 칸(§8.5)
16PKG_LOAD_DRIVER(§12): STEADY 부하 위에서 AWR을 처음 읽는다 · T_LD_LOG의 조각별 횟수를 AWR 'SQL ordered by Executions'와 대조 · MODULE = 'LOAD_DRIVER'
17SPIKE 프로필: 평균에 묻히는 1분(NL의 CPU · LOCK의 블로킹 체인 enq: TX - row lock contention) · ACTION · CLIENT_IDENTIFIER
18「SQL 코딩 가이드」 후보: WHEN OTHERS THEN NULL 금지 · 행마다 부르는 함수는 횟수를 적는다 · FORALL에는 묶이는 문장을 · ERROR_INDEX는 첨자가 아니다 · T3 2호