Skip to content

16화 · 처음으로 시스템 전체를 본다 ​

6월 24일 월요일. 지난주는 숨을 고른 주였다. 이번 주 민원에는 SQL 이름이 없다. "운영 전체가 느리다." 오늘은 처음으로 SQL 한 문장이 아니라 DB 전체를 연다. 그리고 세 번 틀린다. 세 번 다 칸 하나만 보고 틀린다.


오전 9시 05분 ​

매주 처음 하는 일대로 리비전 폴더를 열었다. 이번 주에 고칠 조각은 정해져 있지 않았다. 대신 지난 리포트들의 맨 아래 칸인 「재측정 · 재발방지」에 기한이 걸린 줄이 있었다. 15화 T3 2호 12번 ①이었다. rev01 운영 적용 · 운영 소요를 다시 잰다 · 오과장 · 6/17.

메신저에 답이 먼저 와 있었다.

오과장  08:52
보정 rev01, 6/17 첫 운영 10시 정각 시작 10시 7분 끝. 오늘도 10시 6분에 끝났어요.
반송함에 사유도 남고요. 정산 확정 11시까지 50분 넘게 남아요. 처음 있는 일이에요.

T3 2호의 ①에 줄을 그었다. 그런데 그 아래 메시지가 두 개 더 있었다.

박팀장  09:10
운영팀 보고를 그대로 옮깁니다.
"오전 내내 시스템 전체가 느리다는 민원. 대시보드상 버퍼캐시 히트율은 늘 99점대 초록불이라 I/O 는 문제없음.
 DB 서버 CPU 가 오전 내내 100% 빨간불 → CPU 증설 검토 요망."
개발팀은 "Top SQL 1위만 고치면 된다"고 하고요.
보정 배치도 고쳤다고 들었는데, 왜 전체가 느린 겁니까? 금요일 증설 회의 전에 의견 부탁합니다.
조민우  09:14
저희 쪽도 하나요. 지난 월요일(6/17) 10시 반쯤 정산 등록 화면이 몇 분 동안 안 넘어갔어요.
오늘 또 그럴까 봐 걱정이에요.

박팀장의 메시지를 다시 읽었다. 이번에는 SQL 이름이 없었다. 01화부터 15화까지 민원은 늘 화면 하나, 배치 하나였다. 정산이 30초, 100페이지째부터 멈춘다, 배치가 아침을 넘긴다. 이번에는 전체였다.


오전 9시 30분 — 첫 번째 답 ​

"어느 SQL인지부터 찾으면 되죠." 커피를 들고 온 선배에게 내가 먼저 말했다. "V$SQL을 ELAPSED_TIME 순으로 세우면 제일 오래 걸린 게 나오잖아요. 그게 개발팀이 말하는 1위고요."

"그 ELAPSED_TIME은 언제부터 센 거야?"

"…그 커서가 공유 풀에 올라온 뒤부터요."

"01화에서 V$MYSTAT은 누적이라고 했지. V$SQL도, V$SYSSTAT도 다 누적이야. 운영 DB를 지난주에 재시작했으면 일주일치고, 커서가 오늘 아침에 밀려났다 다시 올라왔으면 몇 분치야." 선배가 의자를 끌어왔다. "'오전 내내 느리다'는 민원에 일주일치 누적을 들이밀면 뭐가 나와?"

"…오전이 안 보이네요."

"그래서 두 번 찍어서 빼. 01화에서 SQL 하나에 했던 걸 DB 전체에 하는 거야. 오라클은 그걸 매시 정각에 알아서 해. 그게 AWR이야."

선배가 말을 이었다. "오늘은 규칙이 하나 있어. 리포트의 칸 하나만 보고 결론을 내리지 마. 운영팀은 히트율 칸 하나, CPU 칸 하나를 봤고 개발팀은 1위 칸 하나를 봤어. 칸 하나는 결론이 아니야. 다른 칸이 같은 말을 할 때까지 맞춰 봐."


오전 9시 50분 — 어디서 보나 ​

연습 DB에서 운영과 비슷한 오전을 만들기로 했다. 그러려면 먼저 이 DB의 AWR이 어떤 상태인지 알아야 했다. 선배가 순서를 불러 주었고, 나는 그대로 받아 적었다. 나중에 이 화의 SOP가 된 메모다.

1  어디서 볼지 정한다          CDB$ROOT? FREEPDB1?  · 팩이 켜져 있나 · 스냅샷이 쌓이고 있나
2  간격 · 보존을 확인한다      바꾸면 끝에서 되돌린다
3  부하를 정한다 (T7)          세션 · 생각 시간 · 가중치 · 씨앗 · 자원 여유

SYS로 루트에 붙었다. sqlp로는 안 됐다. DBMS_WORKLOAD_REPOSITORY를 부르자 PLS-00201: identifier ... must be declared가 떴다. 15화의 DBMS_HPROF와 같은 모양이었다. 보이기는 하는데 부를 권한이 없는 것이다.

CON_NAME                          CDB$ROOT
control_management_pack_access    DIAGNOSTIC+TUNING
awr_pdb_autoflush_enabled         TRUE   (기본값)
스냅샷 간격 · 보존                1시간 · 8일   — 루트와 FREEPDB1 이 따로 갖고 있다

"Free는 진단 팩이 켜진 채로 나와. EE는 이걸 따로 사야 하고." 선배가 말했다. "계약 없이 EE 운영 서버에서 이 리포트를 뽑으면 라이선스 위반이야. 그래서 리포트 양식에 팩 확인란을 둘 거야."

그리고 한 줄이 더 눈에 들어왔다.

Host CPUs (NUM_CPUS)     32
cpu_count                 2

"이 컨테이너가 도는 기계는 CPU가 32개야. 이 DB가 쓸 수 있는 건 2개고. Free의 한계지." 선배가 그 두 줄에 동그라미를 쳤다. "운영팀이 말한 CPU 100%가 어느 쪽 숫자인지는 이따 리포트를 보면 알 수 있어."

부하는 15화에서 만든 발생기로 걸었다. 기본값(생각 시간 200ms · 2세션)으로는 이 DB가 거의 쉰다. 선배가 인자를 불러 주었다.

PROFILE=STEADY MINUTES=12 SESSIONS=3 THINK_MS=0 NOTE=C16-A ./05_부하_돌리기.sh

"세 세션이 쉬지 않고 6조각을 돌려. CPU 2개짜리에 세 명이 동시에 달려드는 거야. 붐비는 오전이지." 선배가 말했다. "그리고 +4분에 지난주까지 쓰던 보정 배치(rev00)를 한 판 얹어. 월요일 10시처럼."

스냅샷은 발사기 옆 창에서 스크립트가 찍었다. 런 시작 +2분에 하나, +11분에 하나.

4  부하를 걸고 데운다 (2분)
5  S1 — 루트에 하나, PDB 에 하나
6  측정 구간 — 사건(보정 배치)은 구간 안에
7  S2 — 부하가 아직 도는 동안

"왜 시작하자마자 안 찍어요?"

"처음 1~2분은 세션이 뜨고, 커서가 공유 풀에 올라오고, 캐시가 데워지는 시간이야. 그걸 섞으면 리포트가 '평소'가 아니라 '출발'을 재게 돼." 선배가 일어섰다. "12분 걸리지? 따라와. 기다리는 동안 보여 줄 게 있어."


오전 10시 10분 — 지하 2층 방재실 ​

엘리베이터는 B1을 지나 B2에서 섰다. 15화의 수발실보다 한 층 아래였다. 문에는 방재실 · 관리실이라고 붙어 있었다. 벽 한 면이 계기판이었고, 책상 위에 두꺼운 장부가 펼쳐져 있었다. 당직 주임이 막 정각 검침을 마치고 볼펜을 내려놓는 참이었다.

"검침 대장이에요." 주임이 장부를 돌려 보여 주었다. "매시 정각에 층별 계량기 숫자를 적어요. 계량기는 건물이 생긴 날부터 숫자가 계속 올라가기만 하고요."

시각     전체 (kWh)   3층       7층       …
09:00    1,284,113   212,407   98,331
10:00    1,284,962   212,705   98,402

"9시부터 10시까지 7층이 얼마 썼는지 알려면?"

"98,402에서 98,331을 빼요. 71이요."

"그게 AWR 리포트야." 선배가 말했다. "정각마다 계량기를 읽어 두는 게 스냅샷, 두 줄을 빼는 게 리포트야. 리포트에 나오는 숫자는 전부 뺄셈이야. V$SQL을 그냥 열면 계량기 숫자를 그대로 읽는 거고. 건물이 생긴 날부터 쌓인 누적이지."

장부 옆 계기판을 둘러봤다. 계약 전력 · 수요 전력 · 역률 99.6%. 역률 칸에 초록불이 들어와 있었다.

"역률이 99.6이면 좋은 거죠?"

"좋은 거예요. 들어온 전기 중에 일에 쓰인 비율이니까요." 주임이 말했다. "근데 건물이 전기를 얼마나 쓰는지는 그 숫자에 없어요. 한여름에 에어컨 백 대를 돌릴 때도 99.6이고, 열 대만 돌릴 때도 99.6이에요."

"그럼 저 계약 전력은요?"

"건물이 한 번에 받을 수 있는 전기의 상한이에요. 그걸 넘을 것 같으면 저 제어기가 에어컨을 번갈아 끄죠." 주임이 회색 상자 하나를 가리켰다. "에어컨 입장에선 고장 난 게 아니라 차례를 기다리는 거고요."

선배가 나를 봤다. 나는 고개를 끄덕였다. 역률은 히트율이었다. 계약 전력은 cpu_count 2였다. 차례를 기다리는 에어컨이 무엇인지는 아직 몰랐다.

엘리베이터를 기다리며 선배가 하나를 더 말했다. "그 대장에서 10시부터 11시까지는 한 줄이야. 그 한 시간 안에 7층이 5분 동안 정전됐다가 돌아와도, 대장에는 71 대신 65가 적힐 뿐이야."

AWR의 저장 구조와 스냅샷, 델타, 간격과 보존은 2_파고들기.md §1에 정리했다.


오전 10시 35분 — 머리 두 줄 ​

구간 A가 끝나 있었다. 리포트는 awrrpt의 대화형 질문에 답하는 대신 함수 하나로 뽑았다. 5,000줄이 넘었다.

sql
SELECT output FROM TABLE(DBMS_WORKLOAD_REPOSITORY.AWR_REPORT_TEXT(:dbid, :inst, :s1, :s2));

"처음부터 읽지 마. 순서를 정해 줄게. 9칸." 선배가 화이트보드에 적었다.

8  리포트를 뽑아 9칸을 정해진 순서로 읽는다
   ① 머리 (Elapsed · DB Time)   ② Load Profile   ③ Instance Efficiency
   ④ Top Foreground Events      ⑤ Host CPU · Instance CPU   ⑥ Time Model
   ⑦ SQL ordered by (4관점)     ⑧ Segments by Logical Reads   ⑨ 아는 것과 맞춰 본다
9  적고 되돌린다 (T5 · 원복)

①은 두 줄이었다.

   Elapsed:      9.00 (mins)
   DB Time:     28.08 (mins)

"9분짜리 구간에 DB Time이 28분이요? 시간이 더 길어요."

"DB Time은 세션들이 DB 안에서 보낸 시간의 합이야. CPU를 쓰든 뭘 기다리든. 세 사람이 9분 동안 쉬지 않고 일하면 27분이지." 선배가 나눗셈을 적었다. "28.08 ÷ 9.00 = 3.12. 평균 3.12명이 매 순간 DB 안에 있었어. 의자는…"

"CPU 2개요."

"3.12명이 의자 2개에 앉으려고 한 거야."

②는 초당 일의 양이었다.

DB CPU(s):                     2.0 / 초
Logical read (blocks):   272,105.0 / 초
Physical read (blocks):       38.2 / 초
Physical write (blocks):   3,475.3 / 초
Executes (SQL):            2,112.6 / 초
Hard parses (SQL):            21.8 / 초
Transactions:                 98.2 / 초

초당 27만 블록을 만지고, 디스크에서는 38개를 읽었다. 그런데 쓰기는 3,475개였다. 읽기의 90배였다.


오전 10시 55분 — 초록불 ​

③을 보자 마음이 놓였다.

Instance Efficiency Percentages (Target 100%)
    Buffer Nowait %:   99.99     Redo NoWait %:      98.63
    Buffer  Hit   %:   99.99     In-memory Sort %:  100.00
    Library Hit   %:   98.05     Soft Parse %:       96.17
 Execute to Parse %:   73.05     Latch Hit %:        99.81
 % Non-Parse CPU:      99.10

"전부 100에 가까워요. 특히 Buffer Hit 99.99. 운영팀 말이 맞네요. I/O는 문제없어요."

선배가 아무 말 없이 내 노트의 한 페이지를 펼쳤다. 07화 날짜가 적힌 페이지였다. 히트율은 건강 지표가 아니다.

"…07화에서 그랬죠. 낭비할수록 올라간다고."

"그건 분모 얘기였지. 오늘은 다른 걸 봐. 히트율은 무엇을 세지?"

"읽은 블록 중에 버퍼 캐시에서 찾은 비율이요."

"그럼 ④로 가. 시간이 어디로 갔는지 봐."

Top 10 Foreground Events by Total Wait Time
Event                            Waits   Time (sec)   Avg Wait   % DB time
DB CPU                                       1086.4                  64.5
resmgr:cpu quantum               1,633        404.1   247.43ms      24.0
enq: RO - fast object reuse      6,255        110.6    17.68ms       6.6
local write wait                10,053         54.5     5.42ms       3.2
direct path sync                 3,351         21.7     6.48ms       1.3

셋째 줄부터 다섯째 줄까지 손가락으로 짚었다. 처음 보는 이름들이었다. 선배가 하나씩 풀어 주었다. enq: RO - fast object reuse는 TRUNCATE가 그 테이블의 블록을 캐시에서 내보내기를 기다리는 것이고, local write wait는 그 블록들을 디스크에 쓰기를 기다리는 것이다. direct path sync는 APPEND로 캐시를 건너뛰고 쓴 블록이 디스크에 닿기를 기다리는 것이다.

"P1이에요. 14화 rev02. TRUNCATE하고 APPEND로 넣는 거요."

"부하 발생기의 P1은 세션마다 자기 작업 테이블을 비우고 10만 건을 넣어. 그게 이 9분 동안 쉬지 않고 돌았어." 선배가 세 줄의 퍼센트를 더했다. "6.6 + 3.2 + 1.3. DB Time의 11.1%가 I/O를 기다렸어. 쓰기 쪽 I/O를 말이야."

"그런데 Buffer Hit는 99.99…"

"히트율은 버퍼 캐시를 거쳐서 읽은 블록만 세. 쓰기는 안 세고, 캐시를 건너뛴 direct path도 안 세." 선배가 ②의 한 줄을 가리켰다. Physical write (blocks): 3,475.3. "히트율 옆에 이 숫자가 없으면 운영팀은 계속 'I/O는 문제없다'고 할 거야. 07화에서 운영팀한테 히트율 옆에 물리 읽기를 두자고 했지? 오늘은 쓰기도 같이 둬야 해."

T5 양식 초안의 ③ 칸에 주석을 달았다. 비율은 결론에 쓰지 않는다. 분자와 분모의 초당 값을 옆에 둔다. 히트율은 쓰기와 direct path를 보지 않는다.

Instance Efficiency의 식과 각 비율이 세는 것, 세지 않는 것은 2_파고들기.md §5에 정리했다.


오전 11시 30분 — CPU 100% ​

④의 둘째 줄로 돌아왔다. resmgr:cpu quantum · 24.0%.

"이건 뭐예요?"

"아까 본 방재실 제어기야." 선배가 말했다. "Free는 CPU를 2개로 묶어 두는데, 그걸 리소스 매니저로 해. 세 번째 세션이 CPU를 원하면 차례가 올 때까지 기다리게 하고, 그 기다림을 이 이름으로 적어."

그렇다면 운영팀 말이 맞았다. ⑤를 펼쳤다.

Host CPU      CPUs 32   %User 6.5   %Idle 92.5
Instance CPU  % of total CPU for Instance:            6.4
              % of busy  CPU for Instance:           84.5
              %DB time waiting for CPU - Resource Mgr: 25.1

②의 DB CPU(s): 2.0 / 초도 다시 봤다.

"CPU 100% 맞아요. 이 DB는 1초에 CPU 2초를 썼어요. cpu_count가 2니까 꽉 찬 거예요. 늘려야죠."

"기계 전체는?"

"…%Idle 92.5. 32개 중에 거의 다 놀아요."

"운영 서버도 그럴 수 있어. 대시보드의 'CPU 100%'가 서버 전체인지, DB가 쓸 수 있는 몫인지부터 물어야 해." 선배가 말했다. "그건 첫 번째 질문이고, 진짜 질문은 이거야. CPU를 늘리면 무엇이 줄어?"

④를 다시 봤다. DB CPU 64.5%, CPU 기다림 24.0%. ⑤에서는 같은 것이 25.1%로 나왔다.

"…기다림이요. 25%쯤. CPU를 두 배로 해도 줄어드는 건 그 줄 서기고, 64.5%는…"

"일 자체야. CPU를 늘리면 줄은 짧아지는데, 해야 할 일은 그대로야. 일을 줄이면 64.5%도 줄고 줄도 같이 짧아져." 선배가 말했다. "그 일이 누구 것인지는 ⑦을 보면 알아. 증설 회의에 가져갈 문장은 '100%니 늘리자'가 아니라 '늘리면 최대 25%가 준다, 일의 절반은 SQL 하나다'야."

"절반이요?"

"아직 안 봤지. 순서대로 가."

⑥ Time Model은 DB Time을 일의 종류별로 나눈 표였다.

sql execute elapsed time       1,683.3 s   99.9 %
sequence load elapsed time     1,561.0 s   92.6 %
DB CPU                         1,086.4 s   64.5 %
PL/SQL execution elapsed time     65.9 s    3.9 %
parse time elapsed                21.2 s    1.3 %
hard parse elapsed time           18.4 s    1.1 %

"퍼센트를 더하면 100이 넘는데요."

"줄끼리 겹쳐. SQL을 실행하는 동안 CPU도 쓰니까. 더하지 말고 한 줄씩 읽어."

그런데 둘째 줄에서 손이 멈췄다. sequence load elapsed time, 즉 시퀀스 번호를 받아 오는 데 쓴 시간이 92.6%였다. 리포트 맨 위의 ADDM 요약에도 같은 숫자가 1위로 떠 있었다. Sequence Usage 92.64.

"시퀀스가 범인이에요? N1 rev01이 시퀀스 쓰잖아요. 12화에서 바꾼 거요."

"그 시퀀스를 쓰는 SQL이 몇 초 걸렸는지 봐."

⑦에서 N1의 INSERT를 찾았다. INSERT INTO T_STLM_REG_LD (STLM_SEQ, …) VALUES (SQ_STLM_LD.NEXTVAL, …)의 Elapsed는 6.6초였다. 46,076번 실행됐다.

"…6.6초요. 1,561초가 아니라."

"DB Time이 1,685초야. 그중 1,561초가 시퀀스였다면 나머지 SQL이 쓸 시간은 124초밖에 없어. 그런데 Q2 하나가…" 선배가 ⑦을 가리켰다. 663.6초. "둘 중 하나는 틀렸어. 어느 쪽인지는 이 리포트만으로 정할 수 없어. 이럴 때 규칙은 하나야. 다른 칸과 맞지 않는 칸은 적어 두고, 그 칸으로는 결론을 내지 않는다."

"ADDM은 이걸 1위로 짚었는데요."

"ADDM은 다음 주에 봐. 그 권고가 무엇을 근거로 했는지, 지금 네가 본 걸로 다음 주에 따져 볼 거야."

Host CPU와 Instance CPU를 읽는 법, Free의 CPU 제한, Time Model의 겹침과 맞지 않는 줄은 2_파고들기.md §7 · §8에 정리했다.

⛳ 오늘은 여기까지(중단 지점 1) 여기까지가 오늘의 절반이다. AWR 리포트가 두 스냅샷의 뺄셈이라는 것과, 운영팀의 두 문장이 칸 하나만 본 결론이었다는 것을 봤다. 히트율이 99.99%인데도 DB Time의 11.1%가 쓰기 I/O를 기다렸고, CPU 100%는 사실이지만 늘려서 줄어드는 건 기다림 25%뿐이다. 그리고 리포트 안에도 다른 칸과 맞지 않는 칸이 있었다. 이어서 읽으면 'Top SQL 1위'를 네 방향에서 세워 보고, 리포트를 이미 아는 사실과 맞춰 본 뒤, 지난주에 고친 보정 배치가 시스템 전체에 무엇을 남겼는지 잰다.


오후 1시 00분 — 1위는 넷이다 ​

"이제 개발팀 말을 볼 차례야. 1위."

⑦의 첫 목록은 SQL ordered by Elapsed Time이었다.

 Elapsed (s)   Executions   per Exec (s)   %Total   SQL
     663.6         16,525          0.04      39.4   SELECT C.CUS_GD, COUNT(*), SUM(O.ORD_AMT) …   (Q2 rev01)
     540.5              0           N/A      32.1   BEGIN PKG_LOAD_DRIVER.RUN_SESSION(2); END;
     540.5              0           N/A      32.1   BEGIN PKG_LOAD_DRIVER.RUN_SESSION(1); END;
     540.5              0           N/A      32.1   BEGIN PKG_LOAD_DRIVER.RUN_SESSION(3); END;
     381.6         33,080          0.01      22.6   SELECT D.ITM_ID, SUM(…) … EXISTS …           (Q3 rev01)

"1위는 Q2네요. 그런데 2위부터 4위가… 실행 0번인데 32%씩이요?"

"부하 발생기 세션 셋이 부른 PL/SQL이야. 12분짜리 호출 하나가 아직 안 끝났으니 실행 수는 0이고, 그동안 그 안에서 부른 SQL의 시간은 전부 자기 몫으로 셌어. 맨 위 머리말에 써 있지." 선배가 읽었다. Resources reported for PL/SQL code includes the resources used by all SQL statements called by the code. "운영에서는 이 자리에 배치 패키지 호출이 와. 그걸 1위라고 들고 가면 '배치가 범인'이 되는데, 그 안의 무엇이 범인인지는 하나도 알려 주지 않은 셈이야."

나머지 세 목록을 나란히 놓았다.

관점1위그 값실행실행당
ElapsedQ2 rev01663.6초(39.4%)16,52540ms
CPUQ2 rev01516.1초16,52531ms
GetsQ2 rev0196,414,870블록(65.6%)16,5255,834블록
ExecutionsINSERT INTO T_STLM_ADJ_C16 …(보정 배치 rev00)99,870번—Rows per Exec 1.0

"Executions 1위는 보정 배치예요. 행마다 한 번, 한 행씩."

"15화 TKPROF에서 본 지문 그대로지. 한 건짜리 문장을 행마다 부르는 거." 선배가 말했다. "그럼 이상한 거 하나. rev00은 정산 한 건에 세 문장을 불렀어. 등급 조회, COUNT(*), INSERT. 나머지 둘은?"

목록을 끝까지 내려 봤다. 없었다. 10만 번씩 호출된 두 문장이 Executions 목록 어디에도 없었다. 목록 머리말에 한 줄이 적혀 있었다.

-> Total Executions:        1,140,499
-> Captured SQL account for   67.2% of Total

"실행의 3분의 1이 목록 밖에 있는 거야. AWR은 스냅샷 때 공유 풀의 SQL을 전부 저장하지 않아. 경과 시간, CPU, 파스 수, 메모리, 버전 수 같은 몇 가지 기준으로 상위 몇십 개만 골라. 실행 횟수는 줄 세우는 기준이지 뽑는 기준이 아니야. 한 번에 몇 마이크로초짜리를 10만 번 부른 문장은 경과 시간으로는 안 걸리니까 저장도 안 돼. 15화 계층 프로파일러에서 등급 조회가 10만 번에 0.36초였지?" 선배가 말했다. "그래서 'Executions 목록에 없다'는 말이 '안 돌았다'는 뜻은 아니야."

SQL 목록이 뽑히는 기준, Captured %, 실행 0인 PL/SQL, SQL_ID가 쪼개진 일은 2_파고들기.md §9에 정리했다.


오후 1시 40분 — 1위의 정체 ​

"그럼 Q2가 범인이죠. Elapsed, CPU, Gets 세 개가 다 1위잖아요."

"Q2의 실행당 Gets를 봐."

5,834블록. 07화에서 잰 rev01의 5,869와 거의 같았다. T_ORD_BIG을 한 번 읽는 바닥값이었다.

"한 번에 읽는 양은 이미 바닥이야. 06화에서 175배를 줄여 놓은 거고. 1위인 이유는 9분에 16,525번 호출돼서야. 범인을 찾을 게 아니라, 이 SQL을 누가 이렇게 자주 부르는지 물어야 해." 선배가 말했다. "그래도 08화에서 걸어 둔 조건 기억나?"

기억났다. 08화의 「먼저 묶고 붙이기」 보류안이다. 재검토 조건: 16화에서 시스템 전체를 잴 때 이 SQL이 CPU 상위에 오르면 rev02로 올린다. CPU 1위, 47.5%였다. 조건에 걸렸다.

SQL 하나짜리 AWR 리포트를 뽑았다(awrsqrpt 대신 AWR_SQL_REPORT_TEXT). 처음에는 No data exists for this section만 나왔다. 이 SQL은 PDB에 속하므로, 루트 구간에서 찾을 때는 마지막 인자에 PDB의 DBID를 줘야 했다. 다시 뽑자 실행계획이 나왔다.

xplan
|   1 |  HASH GROUP BY        |           |
|*  2 |   HASH JOIN           |           |
|   3 |    TABLE ACCESS FULL  | M_CUS     |
|   4 |    VIEW               | VW_GBC_5  |
|   5 |     HASH GROUP BY     |           |
|   6 |      TABLE ACCESS FULL| T_ORD_BIG |

VW_GBC_5. 08화에서 본 이름이었다. 옵티마이저가 만든 뷰, Group-by Placement다. 주문을 고객별로 먼저 묶고 붙이는 방식이다.

"…옵티마이저가 보류안을 이미 하고 있네요."

"08화에서 Q2 rev01에는 그게 안 됐잖아. 왜였지?"

"스칼라 서브쿼리요. 지역명이요. GBP: Bypassed였어요." 거기까지 말하고 리포트의 SQL 문장을 다시 봤다. SELECT C.CUS_GD, COUNT(*), SUM(O.ORD_AMT) FROM T_ORD_BIG O, M_CUS C …. 지역명이 없었다.

부하 발생기의 Q2는 운영의 Q2 rev01이 아니었다. 15화에서 만들 때 스칼라 서브쿼리 한 줄이 빠진 판이었다. 부하가 끝난 뒤 세 문장을 부하 없이 나란히 쟀다.

문장 (부하 없이 · 예열 뒤 10번)          Buffers / 회   CPU (10번 합)   계획
1  부하 발생기의 Q2                         5,856          29 cs      VW_GBC_5 (먼저 묶기)    plan 77778225
2  운영 Q2 rev01 (지역명 스칼라 있음)         5,868         113 cs      HASH JOIN 뒤 GROUP BY   plan 2292855265
3  08화 보류안 (먼저 묶고 붙이기)             5,868          32 cs

블록은 셋이 같았다. CPU는 네 배 가까이 달랐다. 운영 rev01의 실행계획 해시 2292855265는 06화에서 힌트를 지우고 얻은 바로 그 해시였다. 그리고 부하 발생기의 Q2는 08화 보류안을 이미 적용한 Q2와 같은 값이었다. 스칼라 한 줄이 빠지자 옵티마이저가 그 보류안을 알아서 적용해 버린 것이다. 등급, 건수, 금액은 1과 2가 양방향 MINUS 0 / 0으로 같았다. 다른 것은 지역명 칸 하나뿐이었다.

"그러니까 이 리포트에서 Q2가 1위인 건, 운영보다 가벼운 Q2로도 1위라는 거네요."

"조건은 걸렸어. 오히려 더 단단하게." 선배가 말했다. "그런데 오늘 고치지는 않아. 이건 진단서야. 그리고 지금 하나 더 알게 됐잖아. 이 리포트는 운영의 사진이 아니라 우리가 만든 부하의 사진이야. 부하의 생김새를 적어 두지 않으면, 다음 사람은 이 리포트로 아무것도 증명 못 해."

그게 T7, 워크로드 프로파일이 됐다. 맨 아래에 운영과 다른 점 칸을 만들고, 첫 줄에 Q2의 스칼라 한 줄을 적었다.


오후 2시 20분 — 아는 것과 맞춰 본다 ​

⑧은 짧았다.

Segments by Logical Reads       Logical Reads   %Total
T_ORD_BIG          TABLE          97,747,248     66.5
T_ORD_DET          TABLE          26,699,632     18.2
IX_ORD_BIG_DT_AMT  INDEX           9,111,776      6.2
T_ORD              TABLE           2,775,232      1.9

만진 블록의 3분의 2가 T_ORD_BIG이었다. Q2의 바닥 5,834블록 × 16,525번이 거의 그대로 여기에 와 있었다.

⑨는 리포트에 없는 칸이었다. "리포트가 부하를 제대로 봤는지 아는 것과 맞춰 봐. 운영이라면 배치 로그나 앱 로그, 오과장이 적는 시각이겠지. 연습 DB에는 부하 발생기가 조각마다 센 횟수가 있잖아."

T_LD_LOG의 2분~10분 기록과 AWR의 실행 수를 ACTION으로 맞췄다.

조각부하 발생기가 센 것AWR이 센 것
Q1 rev0399,10398,955맞다(경계 몇 판)
Q2 rev0116,54416,525맞다
Q3 rev0133,11533,080맞다
Q4 rev03(문장 3개)131,801131,644맞다
N1 rev0146,12146,076맞다
P1 rev02(문장 3개)3,3542,1311,223이 빈다

"P1만 3분의 1이 사라졌어요."

공유 풀에서 P1의 INSERT 커서를 찾았다. 구간 A 직후, 세션 1의 작업 테이블에 넣는 커서 하나는 이랬다.

INSERT /*+ APPEND */ INTO T_STLM_S1_LD …   executions 67   loads 2,058   invalidations 2,057

공유 풀에 있는 그 커서의 실행 수는 67이었다. 같은 커서가 2,058번 다시 올라왔다.

"14화의 TRUNCATE야. 테이블을 비울 때마다 그 테이블을 쓰는 커서가 무효가 돼. 무효가 된 커서는 다시 올라오고, 그 사이의 실행 수는 스냅샷 때 다 모이지 않아." 선배가 말했다. "이게 ⑨를 하는 이유야. 맞지 않는 줄에는 이유가 있고, 그 이유는 리포트 밖에 있어."

보정 배치도 맞춰 봤다. 02가 배치를 돌리며 직접 센 값이 있었다.

batch rev00  ela 232 cs  cpu 141 cs  recursive calls 301,589  logical reads 817,095  adjusted 99,850  returned 0

AWR의 SQL 목록에는 그 30만 번 중 INSERT 9만 9,870번만 있었다. 배치 시간은 2.32초. 15화에서 혼자 돌렸을 때는 2.14~2.24초였으니, 부하 위에서도 거의 같았다.


오후 2시 50분 — 지난주에 고친 것 ​

"이제 박팀장 질문에 답할 차례야. 보정 배치도 고쳤는데 왜 전체가 느린가." 선배가 말했다. "같은 부하를 한 번 더 돌려. 이번엔 +4분에 rev01."

구간 B다. 인자도 씨앗도 같았다. 다른 것은 보정 배치의 판 하나였다.

batch rev01  ela 38 cs  cpu 15 cs  recursive calls 2,913  logical reads 22,962  adjusted 99,850  returned 170

0.38초. 혼자 돌릴 때는 0.27~0.28초였다. 보정 배치만 보면 2.32초가 0.38초로, 6분의 1이 됐다. 그리고 반송함에 170건이 사유와 함께 남았다.

두 구간을 비교 리포트로 겹쳤다. awrddrpt의 대화형 질문(첫 구간의 스냅샷 둘, 둘째 구간의 스냅샷 둘)에 답하는 대신 함수 하나로 뽑았다.

sql
SELECT output FROM TABLE(DBMS_WORKLOAD_REPOSITORY.AWR_DIFF_REPORT_TEXT(:dbid, :inst, :a1, :a2, :dbid, :inst, :b1, :b2));
                         1st (rev00)     2nd (rev01)     %Diff
DB time (min)                 28.1            28.2         0.4
Executes (SQL) / 초        2,112.6         1,550.9       -26.6
Redo size (bytes) / 초     773,747.1       681,451.4      -11.9
Block changes / 초         6,483.4         5,710.4       -11.9
Logical read / 초        272,105.0       270,705.7        -0.5

첫 줄을 한참 봤다. DB time 28.1 → 28.2. 줄지 않았다.

"보정 배치가 6분의 1이 됐는데, 전체는 그대로예요."

"배치가 줄인 건 2초야. 9분 구간의 DB Time은 1,685초고." 선배가 말했다. "대신 그 2초가 남긴 자국은 선명하지. 초당 실행이 26.6% 줄었어. 9분 동안 30만 번이야. 15화에서 센 그 30만 번이 시스템 전체에서 빠진 거야. Redo도 11.9% 줄었고."

그리고 내가 묻기 전에 선배가 다른 줄 하나를 짚었다. 04로 같은 뺄셈을 직접 한 표의 ③이었다.

                       A (rev00)   B (rev01)
Execute to Parse %       73.05       63.28
Library Hit %            98.05       97.65

"고쳤더니 효율이 떨어졌어요."

"07화의 히트율이랑 똑같은 일이야. rev00은 파스 없이 30만 번을 실행했어. 쓸데없이 많이 실행하면 'Execute to Parse'는 올라가. 그 낭비를 없애면 내려가고." 선배가 말했다. "운영팀이 다음 주에 '효율이 10포인트 떨어졌다'고 경보를 보내면 오늘 이 표를 보여 줘."

박팀장에게 보낼 문장을 정리했다. 보정 배치를 고친 건 맞았다. 그 배치만 놓고 보면 6배였다. 그런데 운영 전체가 느린 원인은 거기에 있지 않았다. 9분 동안 CPU 두 개를 꽉 채우고 4분의 1을 줄 서게 한 것은 매일 매분 도는 조각들, 곧 Q2와 Q3, 그리고 쉬지 않는 P1의 쓰기였다.

두 구간 비교 리포트를 읽는 법과, %Diff로 '바뀐 것'과 '바뀌지 않아야 할 것'을 가르는 법은 2_파고들기.md §11에 정리했다.

⛳ 오늘은 여기까지(중단 지점 2) 운영팀과 개발팀의 세 문장을 9칸으로 다시 읽었다. 히트율 99.99% 옆의 쓰기 I/O 11.1%, 늘려서 줄어드는 CPU 25%, 넷인 1위를 봤다. 그리고 지난주에 고친 보정 배치는 그 배치만 보면 6배였지만 시스템 전체의 DB Time은 0.4% 차이였다. 운영 전체가 느린 원인은 다른 데 있었다. 여기까지만 해도 오늘 할 일은 다 했다. 이어지는 늦은 오후에는 PDB에서 본 AWR이 무엇을 빠뜨리는지 확인하고, 선배가 만든 '한 시간'을 처음으로 리포트 한 장으로 읽는다.


오후 3시 40분 — 루트에서 보는 이유 ​

"아까 왜 루트에서 뽑았어요? 우리 데이터는 FREEPDB1에 있잖아요."

"재 봐. PDB에서 뽑으면 무엇이 빠지나."

02는 스냅샷을 찍을 때 루트에 하나, PDB에 하나씩 찍도록 짜여 있었다. 루트에서 찍은 수동 스냅샷은 PDB에는 생기지 않기 때문이다. 자동 스냅샷은 양쪽에 다 생기는데, 수동 스냅샷은 따로였다. 번호도 따로 매겨졌다. 같은 구간 A를 세 가지 방법으로 뽑아 9칸이 다 있는지 셌다.

칸                           루트 리포트   PDB 리포트(PDB 스냅샷)   PDB 리포트(루트 스냅샷)
DB Time                       28.08 분         27.30 분                 27.30 분
Load Profile                     ○                ○                        ○
Instance Efficiency              ○                ✗                        ✗
Top 10 Foreground Events         ○                ○                        ○
Host CPU · Instance CPU          ○                ✗                        ✗
Time Model                       ○                ○                        ○
SQL ordered by (4관점)           ○                ○                        ○
Segments by Logical Reads        ○                ○                        ○
리포트 줄 수                   5,228            2,609                    2,424

PDB 리포트에는 오늘 오전에 쓴 칸 둘, ③ Instance Efficiency와 ⑤ Host CPU · Instance CPU가 아예 없었다. DB Time도 28.08이 아니라 27.30분이었다. 루트의 SYS 세션(스냅샷을 찍은 세션 같은 것)이 쓴 시간은 PDB의 몫이 아니기 때문이다. 그리고 sqlp로 PDB 안에서 보니 DBA_HIST_SQLSTAT은 보였지만 DBA_HIST_SYSSTAT과 DBA_HIST_SYS_TIME_MODEL은 0행이었다. SELECT ANY DICTIONARY 권한이 있어도 그랬다. 인스턴스 전체의 계량기는 루트에 있었다.

"그래서 루트야. 9칸이 다 있는 건 루트 리포트뿐이야." 선배가 말했다. "PDB 리포트는 PDB가 여럿일 때 '누구 몫이냐'를 가를 때 쓰는 거고."

PDB 레벨 AWR이 이 버전에서 되는 것과 안 되는 것, awr_pdb_autoflush_enabled, 함수의 l_con_dbid는 2_파고들기.md §2에 정리했다.


오후 4시 00분 — 한 시간 ​

"마지막으로 하나." 선배가 자기 노트북에서 발사기를 띄웠다. 명령은 보여 주지 않았다.

"운영의 한 시간을 흉내 낸 프로필이야. 인자는 내가 정했어. T7에는 내가 적어 둘게. 무엇이 들었는지는 묻지 마." 선배가 말했다. "운영팀이 보는 AWR은 정각마다 찍힌 한 시간짜리야. 그 한 시간을 리포트 한 장으로, 오늘 배운 순서대로 읽고 결론을 내 봐. 스냅샷은 +1분, +61분에 찍어."

62분이 지났다. 선배 화면의 발사기 창에 분마다 조각 수를 찍는 표가 올라오려는 참이었다. 선배가 그 창을 닫았다.

"그건 부하 발생기가 남긴 기록이야. 운영팀은 그런 걸 못 봐. 운영팀이 가진 건 정각마다 찍힌 AWR 한 장이야. 오늘은 그것만으로 말해. 리포트 뒤쪽에 붙어 나오는 ASH와 ADDM 절도 다음 주에 봐."

리포트를 뽑아 오전과 같은 순서로 읽었다.

① Elapsed 60.00 분 · DB Time 181.09 분 → 평균 활성 세션 3.02     DB CPU 1.84 초/초
② Logical read 251,864.8 / 초 · Physical write 3,165.3 / 초 · Executes 1,415.7 / 초
③ Buffer Hit 99.99 · Execute to Parse 63.38 · Soft Parse 96.55 · Latch Hit 99.80
④ DB CPU                              61.0 %
   resmgr:cpu quantum                 23.0 %
   enq: RO - fast object reuse         6.0 %
   enq: TX - row lock contention       5.5 %    853 waits · 평균 702.20 ms   ← 오전에 없던 줄
   local write wait                    2.9 %
   direct path sync                    1.2 %
⑤ Host %Busy 7.0 · %DB time waiting for CPU 23.8
⑥ sequence load elapsed time  9,676.7 초 (89 %)   ← 오전처럼 맞지 않는 칸

오전의 9분과 거의 같은 그림이었다. 다른 것은 ④의 넷째 줄 하나였다. 행 락, 12화에서 본 그 이름이었다.

"락이 한 시간의 5.5%예요. 853번, 한 번에 평균 0.7초."

"누가 기다렸어?"

⑦을 처음부터 끝까지 훑었다. Elapsed 1위는 Q2 4,000.5초, 그 아래로 실행 0인 PL/SQL 셋과 Q3. CPU, Gets, Executions도 오전과 같은 얼굴들이었다. FOR UPDATE가 붙은 문장, 락을 기다렸을 만한 문장은 네 목록 어디에도 없었다. 머리말에는 이렇게 적혀 있었다. Captured SQL account for 66.5% of Total DB Time.

"목록에 없어요. 오전에 배운 대로라면, 목록에 없다고 안 돈 건 아니고요."

"그럼 어디서 찾아?"

⑧의 옆 표인 Segments by Row Lock Waits에 두 줄이 있었다.

SYS.OPTSTAT_HIST_CONTROL$   TABLE    930 waits   87.82 %
SQLP.T_STLM_NUM_LD          TABLE    129 waits   12.18 %

"행 락 대기의 88%는 SYS의 통계 테이블이에요. P1이 빈 테이블에 APPEND로 넣을 때마다 오라클이 통계를 모으는데, 그때 세 세션이 같은 줄을 고치는 거고요. 나머지 12%는 부하 발생기의 채번 테이블이에요. 12화 핫픽스판 같은 게 섞였나 봐요."

결론을 적었다. 오늘 배운 순서대로, 칸마다 근거를 붙여서.

1. 이 한 시간은 오전의 9분과 같은 모양이다. CPU 2개에 평균 3.02명 — 기다림 23.8 %.
   일의 대부분은 Q2 · Q3 (Q2 rev01 이 DB Time 의 37 %). P1 의 쓰기 대기가 10 % 남짓.
2. 오전에 없던 것은 enq: TX - row lock contention 5.5 % — 853번 · 평균 0.7초.
   행 락의 88 % 는 SYS 통계 테이블 · 12 % 는 채번 작업 테이블. 그 SQL 은 목록에 없다.
3. 판정 — 이 시스템의 문제는 CPU 를 먹는 일의 양이다. 락은 한 시간의 5.5 %,
   한 번에 1초가 안 되는 부차적인 기다림이다. DB CPU 가 오전(2.01)보다 조금 낮은 1.84 인 것도 그만큼이다.

선배는 끝까지 읽고 고개를 끄덕였다. "칸마다 근거가 있네. 오늘 배운 대로야. 그대로 T5에 남겨."

맞다는 말인지, 아니라는 말인지 알 수 없었다.


오후 6시 00분 — 세 장의 양식 ​

T5 시스템 성능 리포트 1호를 썼다. 구간과 워크로드 프로파일 ID를 적고, 9칸마다 무엇을 의심했나 · 무엇으로 확인했나 · 결론을 채웠다. ADDM 칸과 '구간 안의 시간 분포' 칸은 비워 두고 (17화)라고 적었다. 맨 아래 「다음 행동」 1번은 Q2였다. 08화 보류안 재검토 조건 충족. 단, 부하 모델의 Q2는 운영 문장과 다르다.

T7 워크로드 프로파일 1호에는 ID 세 개(WP-16A · WP-16B · WP-16H)를 적었다. 발사기 명령 한 줄을 씨앗까지 그대로 옮겼다. WP-16H의 명령 칸만은 선배가 채웠고, 나는 보지 않았다. 그리고 9번 「운영과 다른 점」에는 Q2의 스칼라 한 줄, 9분에 P1이 3,354번 도는 것(운영에서는 밤에 한 번), 생각 시간 0을 적었다.

T8 정기 점검은 처음 만드는 양식이었다. 지금까지는 늘 누군가의 민원으로 시작했다. 이것은 민원이 오기 전에 매일, 매주 같은 칸을 보는 양식이다. 1호는 구간 A를 지난주, B를 이번 주로 놓고 채웠다. 주간 칸을 채우다가 04화의 서류 하나가 걸렸다. T2-002, IX_ORD_BIG_DT 폐기 건이다. 선결 조건은 DBA_INDEX_USAGE 관찰이었다. 오늘 부하가 도는 동안 AWR에 남은 실행계획 중 IX_ORD_BIG_DT를 쓴 것은 0이었다. DBA_INDEX_USAGE의 마지막 사용 시각도 오늘 부하보다 앞이었다. 같은 시간에 IX_ORD_BIG_DT_AMT는 88만 번, IX_ORD_BIG_DT_SEQ는 270만 번 쓰였다. 선결 조건 하나가 숫자로 채워진 것이다. 단, 부하 발생기가 부르는 SQL 안에서만이다. 운영 SQL 전수 조사는 그대로 남았다. 세그먼트 칸에서도 하나가 더 걸렸다. SQLP의 가장 큰 세그먼트가 T_ORD_BIG(47MB)이 아니라 휴지통에 든 LOB 하나(130.3MB)였다. 15화에서 DBMS_HPROF 결과 테이블을 다시 만들 때 PURGE 없이 지운 것이었다. 아무도 묻지 않은 것을 정기 점검이 찾아낸 셈이다. 티켓을 하나 열었다.

정합성 회귀 체크리스트는 붙이지 않았다. 이 화에서는 SQL을 한 줄도 고치지 않았다. 「해당 없음」 칸에 이유를 적었다. 진단만 했다. 보정 배치는 15화의 두 판을 복사본에서 그대로 돌렸다(보정 99,850 · 반송 170, 15화와 같다).

퇴근 전에 박팀장에게 보냈다.

나  18:40
연습 DB 에 붐비는 오전을 재현해 시스템 전체를 처음으로 재 봤습니다.
① 히트율 99점대는 사실이지만, 그 숫자는 '버퍼 캐시를 거친 읽기' 만 셉니다.
   같은 구간에 DB 시간의 11% 가 디스크 쓰기를 기다렸습니다.
② CPU 100% 도 사실입니다. 다만 CPU 를 늘려 줄일 수 있는 건 'CPU 차례를 기다린 시간' 으로, 전체의 25% 입니다.
   나머지는 일 자체이고, 그 일의 절반 가까이를 등급별 매출 집계 SQL 하나가 씁니다 (한 번에 읽는 양은 이미 최소입니다. 부르는 횟수가 많습니다).
③ 보정 배치 개선은 배치에게는 6배였지만 시스템 전체 시간으로는 거의 안 보입니다. 전체가 느린 원인은 다른 데 있습니다.
증설 회의 전에 '등급별 매출 집계 화면이 몇 초마다 새로 고쳐지는지' 를 현업에 확인하고 싶습니다.
박팀장  18:52   CPU 를 늘려도 4분의 1이라는 숫자는 회의에 그대로 가져가겠습니다.
조민우  18:58   10시 반 건은요?

조민우 씨의 메시지 앞에서 손이 멈췄다. 10시 반. 내 결론 어디에도 10시 반은 없었다. 한 시간이 한 줄이었으니까.

선배가 가방을 들다 말고 내 화면을 봤다. "그 0.7초짜리 853번 말이야."

"네."

"한 시간 동안 고르게 흩어져 있었을까?"

리포트를 다시 봤다. 853번, 599초, 평균 702.20ms. 그 숫자들만으로는 언제 일어났는지 알 수 없었다. 대장의 한 줄처럼.

"…모르겠어요. 리포트엔 그게 안 나와요."

"그럼 다음 주에 물어보자. 오늘 적은 결론은 지우지 말고." 선배가 말했다. "그게 맞는지 틀리는지, 같은 한 시간을 다시 만들어서 볼 거야."

조민우 씨에게는 이렇게 답했다. "10시 반 건은 오늘 본 한 시간짜리 기록으로는 확인이 안 됩니다. 다음 주에 시간대를 쪼개서 보겠습니다."


오늘 남은 것 ​

무엇값
AWR SOP 9단계 · 읽는 순서 9칸어디서 → 간격 · 보존 → 부하(T7) → 데우기 → S1 → 구간 → S2 → 리포트 9칸 → 적고 되돌린다
구간 A(9분 · 3세션 · 생각 시간 0)DB Time 28.08분 · 평균 활성 세션 3.12 · DB CPU 2.01초/초(cpu_count 2)
오판 1: 히트율Buffer Hit 99.99% · 같은 구간 DB Time의 11.1%가 쓰기 I/O 대기(enq: RO · local write wait · direct path sync)
오판 2: CPU호스트 CPU 32 · %Idle 92.5 · CPU를 기다린 몫 25.1%. 늘려서 줄어드는 것은 이 줄
오판 3: 1위Elapsed · CPU · Gets 1위 Q2(실행당 5,834블록 = 바닥) · Executions 1위 보정 배치 INSERT(99,870) · 실행 0인 PL/SQL 셋이 32%씩 · Captured 67.2%
맞지 않는 칸Time Model의 sequence load 1,561초(92.6%) vs 그 시퀀스를 쓰는 SQL 6.6초. 결론에 쓰지 않는다
⑨ 대조Q1 · Q2 · Q3 · Q4 · N1은 부하 기록과 맞다 · P1은 3,354 vs 2,131(TRUNCATE의 커서 무효화)
구간 A → B(보정 rev00 → rev01)배치 2.32 → 0.38초 · DB time 28.1 → 28.2분 · Executes −26.6% · Redo −11.9% · Execute to Parse % 73.05 → 63.28
부하 모델의 Q2부하 발생기의 Q2는 운영 rev01에서 지역명 스칼라가 빠진 판: 실행계획 VW_GBC_5 · CPU 10번 29cs vs 운영 rev01 113cs vs 08화 보류안 32cs(Buffers 5,856 · 5,868 · 5,868) · 08화 조건은 더 단단하게 충족
PDB 레벨 AWRPDB 리포트에는 Instance Efficiency · Host CPU · Instance CPU 칸이 없다 · DB Time 27.30분(루트 28.08) · awr_pdb_autoflush_enabled TRUE(기본값) · 루트 수동 스냅샷은 PDB에 안 생긴다 · AWR_SQL_REPORT_TEXT는 l_con_dbid가 있어야 PDB의 SQL을 찾는다
한 시간(H)60분 · DB Time 181.09분 · 평균 활성 세션 3.02 · DB CPU 1.84 · enq: TX - row lock contention 5.5%(853번 · 평균 702ms) · 그 SQL은 목록에 없음 · 행 락의 88%는 SYS 통계 테이블 → 결론 '문제는 CPU를 먹는 일의 양, 락은 부차'(→ 17화가 다시 연다)
새 양식T5 시스템 성능 리포트 · T7 워크로드 프로파일 · T8 정기 점검(제정 · 각 1호)

오늘 배운 한 문장 ​

AWR 리포트는 두 번 검침한 값의 차이이고, 칸 하나는 결론이 아니다. 히트율이든 CPU든 1위든, 한 칸에서 읽은 것을 다른 칸이 같은 말로 뒷받침할 때까지 맞춰 본다. 그리고 리포트의 모든 숫자는 그 구간의 평균이다.

다음으로 넘기는 것 ​

  • 평균이 가린 것: 한 시간 리포트로 내린 결론을 시간 축으로 펼쳐 본다 · 조민우 씨의 10시 반 → 17화
  • ADDM의 1위 'Sequence Usage 92.64': 그 근거가 된 Time Model 줄은 SQL의 경과 시간과 맞지 않았다. 권고(시퀀스 캐시를 키워라)를 무엇으로 검증할까 → 17화
  • Q2 보류안: 08화 조건 충족(모델의 가벼운 Q2로도 CPU 1위) · 적용은 T1 절차로 → 18화
  • T2-002(IX_ORD_BIG_DT 폐기): 선결 조건 하나를 오늘 쟀다 · 운영 SQL 전수 조사는 남았다 → 18화

다음 → 3_실습과_기록.md에서 SOP 9단계를 직접 밟고, 9칸을 읽고, 두 구간을 겹쳐 본 뒤 T5, T7, T8 1호를 쓴다. 개념이 막히면 2_파고들기.md를 펼친다.