테마
14화 · 야간 배치가 아침을 넘긴다
6월 3일 월요일, 일요일 밤에 시작한 배치가 월요일 아침 업무 시간까지 돌았다. 오늘은 두 번 틀린다. 그리고 무기 두 개를 한꺼번에 들었다가, 둘이 서로를 망치는 모습을 본다.
오전 9시 05분
월요일 아침이면 늘 하는 대로 리비전 폴더부터 열고 P1_rev00을 띄웠다. 지난주에 처음으로 리비전 파일로 남긴 조각이다. 맨 위에 내가 붙여 둔 메모가 있었다.
-- 1,000건 커밋은 지금은 안전하다 (P1 은 읽는 테이블을 고치지 않는다).
-- 전진은 측정이 정당화할 때(14화).측정이 정당화할 때. 그때가 언제일지 몰랐는데, 답은 메신저로 먼저 왔다.
조민우 09:03
정산 화면이 이상해요. 지난주 정산 건수가 반밖에 안 보여요.
새로고침할 때마다 조금씩 늘어나요. 이거 뭐예요?화면을 두 번 봤다. 새로고침할 때마다 건수가 늘어난다는 건, 데이터가 지워진 게 아니라 아직 들어오는 중이라는 뜻이었다. 그런데 정산은 밤사이에 다 들어와 있어야 했다.
답을 쓰기도 전에 다음 메시지가 왔다.
오과장 09:14
어젯밤 배치가 방금 끝났어요. 09시 12분.
04시 마감인데 다섯 시간을 넘겼네요. 월말 정리가 붙는 첫 일요일이라 길 줄은 알았는데…박팀장 09:21
야간 배치가 업무 시간까지 돈 건 처음입니다. 정산팀이 9시에 빈 화면을 봤어요.
오늘 중으로 원인과 대책 부탁합니다. 숫자로요.마지막 두 글자를 보고 선배가 웃었다. 커피를 들고 와서 내 화면에 뜬 메시지 세 개를 차례로 읽었다.
"박팀장님이 '숫자로'라는 말을 다 쓰시네. 석 달 걸렸다." 선배가 의자를 끌어왔다. "오과장님 배치가 뭘 하는지부터 보자."
오과장이 보내 준 일요일 밤 체인은 네 단계였다.
[주간 야간 체인] 일 22:00 시작 → 월 04:00 마감
① T_ORD_BIG 재적재 TRUNCATE → INSERT 100만 → 통계 수집 (10화의 그 배치)
② 월말 이력 정리 T_ORD_LOG 에서 가장 오래된 한 달을 DELETE ← 매달 첫 일요일만
③ 월마감 집계 /*+ PARALLEL(8) */ ← 지난달 추가
④ P1 정산 적재 T_ORD → T_ORD_STLM (1,000건마다 COMMIT)
로그: 시작 22:00:04 · 끝 09:12:37"단계별 시각은요?"
"로그엔 시작이랑 끝만 남아요." 오과장의 답장은 짧았다. 곧 한 줄이 더 왔다. "③의 PARALLEL은 지난달에 제가 붙였어요. 집계가 느려서요. 별로 안 빨라지긴 했는데 그냥 뒀어요."
답은 이미 알 것 같았다. 지난주까지 잘 끝나던 체인이 이번 주에 넘쳤고, 이번 주에만 달라진 건 하나뿐이었다.
"②예요. 월말 정리요. 이번 주에만 붙은 단계잖아요. 05화에서 DELETE가 얼마나 무거운지도 봤고요."
"그럴듯하네." 선배가 말했다. "근데 '이번 주에 달라진 것'이 '시간을 잡아먹은 것'과 같은지는 아직 몰라. 단계마다 재 봐. 범인 지목은 그다음이야."
오전 9시 40분 — 컵을 채운 것과 넘치게 한 것
운영 로그에 단계별 시각이 없으니, 연습 DB에서 네 단계를 하나씩 다시 돌려 보기로 했다. 원본은 건드리지 않고 모두 복사본에서 했다. 인덱스 셋까지 원본과 똑같이 붙인 T_ORD_BIG_C14, 그리고 05화와 같은 생성식으로 다시 지은 이력 테이블 T_ORD_LOG_C14이다.
내 가설부터 쟀다.
② 월말 정리 DELETE FROM T_ORD_LOG_C14 WHERE LOG_DT < DATE '2023-02-01'
17,235 행 · redo 4.84 MB · 0.06 초0.06초. 숫자를 다시 봤다. 이력 테이블 50만 행 중 한 달치를 지우는 데 1초도 안 걸렸다.
"…이게 범인이면 체인이 0.06초 늘어난 거네요."
"나머지도 재 봐."
단계 (연습 DB · 단독 실행) 경과 redo
① 재적재 — 일반 INSERT 100만 (인덱스 3개 켠 채) 329 초 916.6 MB
① 재적재 — 통계 수집 1.06 초 1.4 MB
② 월말 이력 정리 (DELETE 한 달) 0.06 초 4.84 MB
③ 월마감 집계 (PARALLEL(8) · 연습 DB 에선 고객별 합계로) 0.05 초 —
④ P1 정산 적재 (10만 건) 0.89 초 28.66 MB①이 체인의 99%였다. 5분 29초였다. 나머지 셋을 다 더해도 2초가 안 됐다.
"월말 정리가 넘치게 한 건 맞을 수도 있어." 선배가 표의 맨 윗줄을 짚었다. "컵이 이미 찰랑찰랑했으면 한 방울로도 넘치거든. 근데 컵을 채운 건 이거야. 이걸 안 보고 월말 정리를 고치면 다음 달 첫 일요일에 또 넘쳐."
그러고는 329초 옆의 숫자를 하나 더 짚었다.
② 적재+커밋 경과 32,899 cs 그중 'log file switch (checkpoint incomplete)' 대기 30,953 cs"5분 29초 중에 5분 9초가 기다린 시간이야. 연습 DB의 리두 로그는 10MB짜리 두 개거든. 이 적재가 리두를 916MB 썼어. 10MB짜리로 그걸 다 받으려면 아흔 번 넘게 갈아 끼워야 해. 갈아 끼울 때마다 '앞 로그의 블록이 아직 디스크에 안 내려갔다'면서 멈춰 기다린 거야." 선배가 말했다. "10화에서 이 배치를 운영 그대로 재 봤을 때는 9분 13초였지. 오늘은 5분 29초고. 시간은 그날 서버 사정을 타. 그런데 리두 916MB는 안 타."
이번엔 ③을 봤다. 오과장이 지난달에 붙였다는 PARALLEL(8)이었다.
"이건요? 0.05초면 병렬이 잘 돈 거 아니에요?"
"실행계획을 봐."
xplan
Plan hash value: 4189269332 ← 힌트 없이 돌린 것과 같은 번호
Hint Report (identified by operation id / Query Block Name / Object Alias):
Total hints for statement: 1 (U - Unused (1))
5 - SEL$1 / "O"@"SEL$1"
U - PARALLEL(o 8)U - Unused. 힌트 없이 돌린 계획과 해시 값이 똑같았다. 실행계획 어디에도 PX로 시작하는 줄이 없었고, V$PQ_TQSTAT은 0행이었다.
"이 연습 DB는 Oracle Database Free야. 병렬 실행을 안 해. 그런데 PARALLEL(8)을 붙여도 에러를 안 내. 힌트를 읽기만 하고 쓰지 않은 채 조용히 직렬로 돌아." 선배가 말했다. "운영 DB가 병렬로 도는지는 우리도 아직 몰라. 오과장님도 '붙였는데 안 빨라져서 그냥 뒀다'고 했지. 효과도 부작용도 아무도 재 보지 않은 힌트가 운영 배치에 한 달째 박혀 있는 거야."
"그럼 ③은요?"
"용의자 명단에서 '판정 불가'로 빼 둬. 여기선 못 재. 못 잰 걸 근거로 쓰지 않는 게 오늘의 첫 번째 규칙이야." 선배가 ①을 가리켰다. "몸통부터 보자."
Free에서 병렬이 '안 된 것'을 화면으로 확인하는 증거 다섯 가지와 PX 계획 판독은
2_파고들기.md §9에 있다. 대규모 병렬 실측은 별권에서 다룬다.
오전 10시 30분 — 청구서
"02화 마지막 날 기억나?" 선배가 물었다. "64배를 벌고 퇴근할 때 내가 물었지. '이게 공짜일 것 같아?'"
기억났다. 그때 선배는 "14화에서 그 청구서가 와"라고 했다. 04화에서는 5만 건으로 1차 증거를 쟀고, 05화에서는 인덱스 두 개가 테이블보다 크다는 걸 봤다. 09화에서 세 번째 인덱스를 만들 때는 "청구서에 한 줄 더"라고 적어 두었다.
"오늘이 그걸 정산하는 날이야. 04화 방법 그대로, 지금 상태까지 다시 재 봐."
인덱스를 하나씩 붙여 가며, 복사본에 5만 건을 넣었다가 롤백하는 일을 되풀이했다. 측정마다 앞에 04화에서 배운 대로 워밍업을 두 번 돌렸다.
| 인덱스 | 생긴 화 | redo | 블록 변경 | 논리 읽기 |
|---|---|---|---|---|
| 0개 | — | 2.44MB | 1,921 | 2,763 |
1개 (ORD_DT) | 02화 | 14.13MB | 102,481 | 153,739 |
2개 +(ORD_DT, ORD_AMT) | 02화 | 26.09MB | 201,060 | 301,956 |
3개 +(ORD_DT, ORD_SEQ) | 09화 | 38.55MB | 301,704 | 453,230 |
(4개 +(CUS_ID, ORD_AMT)) | 08화에서 기각한 안 | 50.62MB | 397,312 | 596,977 |
04화에서 잰 0개, 1개, 2개의 값(2.44, 14.13, 26.10)이 거의 그대로(26.10 → 26.09) 다시 나왔다. 그리고 그 아래로 한 줄이 늘었다.
"인덱스 하나에 리두 12MB 안팎, 블록 변경 10만씩이에요. 5만 건에 10만이면…"
"한 행 넣을 때마다 인덱스 하나당 블록을 두 번 고친다는 거지. 정렬된 자리를 찾아가서 한 번, 그 변경을 되돌릴 정보를 언두에 적느라 한 번." 선배가 0개 줄과 3개 줄을 손가락으로 이었다. "리두 15.8배, 블록 변경 157배. 테이블에 행을 넣는 일보다 인덱스 셋을 고치는 일이 훨씬 커. 그리고 08화에서 그 커버링 인덱스를 기각한 게 다행이었다는 것도 숫자로 나왔네. 만들었으면 청구서에 한 줄이 더 붙었을 거야."
나는 이 표를 오래 봤다. 인덱스 셋은 모두 만든 이유가 있었다. 02화의 91, 09화의 포스트잇. 낮에 번 것을 밤에 갚고 있었던 셈이다. 그리고 그 밤이 아침을 넘겼다.
그래서 두 번째로 틀렸다.
"인덱스를 다 지우면 되겠네요. 셋 다요. 재적재가 2.44MB짜리가 되잖아요."
"지우면 월요일 아침 Q1은?"
"…5,827이요. 91이 아니라."
"Q4는?" 선배가 손가락을 하나씩 접었다. "밤을 살리려고 낮을 죽이면 그건 튜닝이 아니라 이사야. 문제를 낮으로 옮긴 거지." 선배가 일어섰다. "인덱스가 낮에 필요한 건 맞아. 그러니까 질문을 바꿔 봐. 인덱스가 밤 동안에도 한 행 한 행 따라다녀야 해?"
오전 11시 10분 — 하역장
선배가 향한 곳은 1층 뒤편 하역장이었다. 월요일 오전마다 복사용지와 사무용품이 들어오는 곳이다. 마침 납품 트럭 한 대가 후진해 들어와 있었고, 총무팀 직원 둘이 서로 다른 방식으로 짐을 받고 있었다.
한 사람은 상자를 하나씩 뜯었다. 상자 하나를 들고 비품 창고로 들어가 선반에서 빈칸을 찾아 꽂고, 나와서는 책상 위 대장 세 권(품목별, 부서별, 입고일별)에 한 줄씩 적었다. 그러고는 다음 상자를 들었다.
다른 사람은 지게차에 올랐다. 팔레트째 들어 올려 창고 맨 안쪽 빈 구역에 통째로 내려놓았다. 대장은 손도 대지 않았다.
"저분은 대장을 언제 써요?"
"다 내리고 나서요." 총무팀 과장이 웃었다. "입고 목록을 뽑아 품목 순으로 정렬해 놓고, 대장 세 권을 처음부터 새로 써요. 한 줄씩 끼워 넣는 것보다 그게 훨씬 빨라요. 끼워 넣다 보면 대장이 금방 너덜너덜해지거든요. 페이지 사이에 쪽지 붙이고, 뒤로 넘기고."
선배가 나를 봤다. "어느 쪽이 오과장님 배치야?"
"…상자를 하나씩 뜯는 쪽이요. 일반 INSERT요. 행마다 테이블 빈칸을 찾고, 인덱스 셋에 한 줄씩 적고."
"지게차가 APPEND야. 직접 경로 적재지. 테이블의 빈칸을 찾지 않고 고수위선 위에 블록째 써. 그리고 대장을 '다 내린 뒤 정렬해서 새로 쓰는 것'이 인덱스 끄고 켜기야. 적재 전에 인덱스를 UNUSABLE로 꺼 두고, 다 넣은 뒤 REBUILD해."
자리로 돌아와 같은 100만 행을 네 가지 방법으로 다시 넣었다. 매번 TRUNCATE부터 했다.
| 넣는 법 (100만 행 · 인덱스 3개) | 경과 | redo | 블록 변경 |
|---|---|---|---|
| 일반 INSERT · 켠 채 (오과장 배치 그대로) | 329초 | 916.6MB | 5,455,266 |
APPEND · 켠 채 | 8.3초 | 268.8MB | 205,171 |
| 일반 INSERT · 끄고 → 켜기 (적재 + 재생성) | 1.1 + 5.1초 | 48.4 + 0.4MB | 45,383 + 3,005 |
APPEND · 끄고 → 켜기 (적재 + 재생성) | 0.26 + 4.74초 | 0.2 + 0.4MB | 1,872 + 3,003 |
마지막 줄을 소리 내어 읽었다. 329초가 5초로, 리두 916MB가 0.6MB로.
"이게… 같은 일이에요?"
"행수 100만, 금액 합계 255,398,623,700. 같은 일이야." 선배가 표의 세 번째 줄을 짚었다. "그리고 이 줄 봐. 지게차 없이 대장만 나중에 써도 6초야. 329초의 대부분은 테이블이 아니라 인덱스였어. 상자 하나 들 때마다 대장 세 권을 펼치느라 그랬던 거지."
다시 켠 인덱스를 확인했다. LEAF_BLOCKS는 2,653, 3,206, 3,344로 갓 만든 크기 그대로였다. 켠 채 적재한 쪽은 4,374, 3,555, 5,505였다. 같은 100만 행인데 인덱스가 최대 65% 더 컸다. 끼워 넣는 동안 블록이 반쯤 빈 채로 쪼개진 것이다. 과장 말 그대로 대장이 너덜너덜해져 있었다.
"그런데 APPEND만 쓴 둘째 줄은 왜 268MB나 돼요? 지게차인데."
"테이블 몫은 지게차가 날랐는데, 인덱스 셋은 여전히 켜져 있었으니까. 직접 경로라도 켜져 있는 인덱스는 적재가 끝날 때 한꺼번에 고쳐 넣어. 그게 268MB야." 선배가 말했다. "그리고 넷째 줄의 리두가 거의 0인 건 이 연습 DB가 NOARCHIVELOG라서야. 그 모드에서는 직접 경로 적재가 데이터 리두를 거의 안 남겨. NOLOGGING을 붙여도 숫자가 그대로였지? 운영이 ARCHIVELOG면 그 칸이 달라져. 그러니까 표에 '연습 DB 기준'이라고 꼭 적어."
⛳ 오늘은 여기까지(중단 지점 1) 여기까지가 오늘의 절반이다. 체인의 99%는 인덱스 셋을 켠 채 한 행씩 넣는 재적재였다. 같은 100만 행을
APPEND+ 인덱스 끄고 켜기로 넣으면 329초 → 5초로 줄어드는 것도 쟀다. 이어서 읽으면 그 무기의 대가를 재고, P1을 한 문장으로 다시 쓰고, 05화의 청구서를 받는다. 그리고 무기 둘을 함께 들었다가 크게 덴다.
오후 1시 10분 — 지게차가 도는 동안
점심을 먹고 오니 선배가 메신저로 하역장 사진을 한 장 보내 놓았다. 지게차가 창고 안에 들어가 있는 동안 창고 문 앞에 노란 줄이 쳐져 있었다.
"무기에는 대가가 있어. 오전엔 지게차 칭찬만 했지. 이번엔 지게차 때문에 못 하게 되는 일을 재 보자."
첫째는 그 노란 줄이었다. APPEND로 1,000건을 넣고 커밋하기 전에, 내 세션이 그 테이블에 건 락을 확인했다.
일반 INSERT (커밋 전) TM LMODE 3 ROW EXCLUSIVE → 다른 트랜잭션 : 넣을 수 있다
APPEND (커밋 전) TM LMODE 6 EXCLUSIVE → 다른 트랜잭션 : ORA-00054 (TM 락을 못 얻음)"6이면 테이블 통째야. 커밋할 때까지 다른 트랜잭션은 한 줄도 못 넣어. 밤에는 상관없지. 그런데 오늘처럼 배치가 아침까지 가면?"
"…낮 업무가 그 테이블에 못 써요."
"그래서 무기를 쓰면 '반드시 마감 전에 끝난다'는 조건이 더 중요해져. 못 끝내면 느린 정도가 아니라 아예 막혀."
둘째는 인덱스를 꺼 둔 동안의 일이었다. 대장을 새로 쓰는 동안에는 대장이 없다. 인덱스가 꺼진 상태로 낮의 Q1을 돌렸다.
인덱스 꺼진 동안 TABLE ACCESS FULL Buffers 5,822
다시 켠 뒤 INDEX RANGE SCAN Buffers 91에러는 없었다. skip_unusable_indexes가 기본으로 켜져 있어서 옵티마이저가 꺼진 인덱스를 조용히 건너뛰고 전체 스캔을 골랐다.
셋째는 05화의 분필 선이었다. 복사본에서 2022년 1년치 33만 건을 DELETE하고 같은 행을 다시 넣었다. 한쪽은 일반 INSERT로, 다른 쪽은 APPEND로 넣었다.
DELETE 직후 다시 채운 뒤 전체 스캔 Buffers
일반 INSERT 5,915 6,016 5,889
APPEND 5,915 7,990 7,862"지게차는 창고 중간의 빈 선반을 안 써. 맨 안쪽 빈 구역에만 내려놔." 선배가 말했다. "DELETE로 비운 칸을 APPEND는 안 써. 분필 선 위에만 쓰니까. 월말 정리는 DELETE로 하고 낮 적재는 APPEND로 하는 테이블은 매달 한 달치씩 커져."
넷째는 내가 찾아낸 것이었다. 오전에 APPEND로 재적재한 복사본에는 통계가 저절로 모여 있었다. 23ai는 빈 테이블에 직접 경로로 넣으면 적재하면서 통계를 모은다. 10화에서 정책서에 적어 둔 바로 그 동작이다. NOTES 칸에는 STATS_ON_LOAD가 찍혀 있었다. 그런데 한 칸이 이상했다.
COLUMN_NAME HISTOGRAM NOTES
ORD_ST NONE STATS_ON_LOAD ← 원본은 FREQUENCY (10화 정책서 1호)"선배님, 온라인 통계는 히스토그램을 안 모아요. 10화에서 ORD_ST에 건 그거요."
선배가 눈을 가늘게 떴다. "그럼 오과장님한테 'APPEND로 바꾸면 통계 수집 단계를 빼도 된다'고 말하면?"
"10화 사고가 모양만 바꿔서 다시 나요. 이번엔 행수는 맞는데 CANCEL이 다시 33만 건으로 보이고요."
"좋아. 그리고 그 단계가 얼마였지?"
"1.06초요." 오전 표를 다시 봤다. 10화에서 오과장이 "통계까지 돌리면 04시를 넘겨요"라며 뺐던 바로 그 단계였다. "통계 수집이 문제가 아니었네요. 04시 마감을 위협한 건 처음부터 인덱스 셋이었어요."
"10화가 넘긴 질문의 답이 그거야. 빼면 안 되는 1초를, 329초 옆에서 빼려고 했던 거지."
하나가 더 있었다. 책에는 APPEND로 넣은 트랜잭션은 커밋 전에는 그 테이블을 자기 자신도 못 읽는다(ORA-12838)고 적혀 있었다. 재 봤다. 읽혔다. 1,001,000건이 그대로 나왔다.
"이 버전(23ai)에서 풀린 제약이야. 운영이 이전 버전이면 책대로 막혀." 선배가 말했다. "책에 적힌 제약도 내 버전에서 재 보고 쓴다. 반대도 마찬가지고."
APPEND가 아예 무시되는 조건(FK나 트리거가 있으면 무시되고, 계획의Note에 이유가 적힌다)과 대가 전체는2_파고들기.md §4에 있다.
오후 2시 00분 — P1을 한 문장으로
이제 아침에 조민우 씨가 보낸 메시지가 남았다. 새로고침할 때마다 늘어나던 정산이었다.
"P1은 체인의 맨 끝이야. 아침까지 밀린 건 앞 단계 탓이지만, 반만 보인 건 P1 탓이야." 선배가 P1_rev00을 띄웠다. "왜 반만 보였을까?"
TRUNCATE로 비우고, 한 건씩 넣고, 1,000건마다 커밋한다. 13화에서 "P1은 fetch across commit이 아니라서 안전하다"고 적은 바로 그 구조였다. 01555에는 안전했다. 그런데…
"커밋할 때마다 남한테 보여요. 1,000건씩요. 조민우 씨는 P1이 절반쯤 왔을 때 화면을 연 거고요."
그대로 재 봤다. P1이 5만 번째 행을 넣는 순간, 다른 트랜잭션에서 정산 테이블을 셌다.
rev00 이 50000번째 행을 넣는 순간, 남의 눈에 보이는 정산 : 50000 건13화에서 본 반쯤 반영이 이번엔 장애가 아니라 정상 실행 중에 보인 것이었다.
13화에서 만든 「커밋 주기 기준」의 첫 줄을 다시 읽었다. "집합 연산 한 문장으로 쓸 수 있나? → 그렇게 쓴다(커밋 한 번). 언두가 걱정이면 14화." P1은 읽은 것을 그대로 넣을 뿐이니 한 문장으로 쓸 수 있었다.
sql
-- P1_rev01
INSERT INTO T_ORD_STLM (STLM_SEQ, ORD_SEQ, CUS_ID, STLM_DT, STLM_AMT)
SELECT ROWNUM, ORD_SEQ, CUS_ID, ORD_DT, ORD_AMT
FROM T_ORD
WHERE ROWNUM <= p_limit;
COMMIT;그리고 오전의 지게차까지 붙인 판을 더해 셋을 나란히 쟀다.
판 (10만 건) 경과 redo undo 커밋 논리 읽기
rev00 커서 루프 · 1,000건 커밋 0.89 초 28.66 MB 6.50 MB 101 110,444
rev01 INSERT … SELECT 0.08 초 4.47 MB 0.26 MB 2 7,408
rev02 + APPEND 0.11 초 0.08 MB 0.02 MB 2 2,858"undo가 6.50에서 0.02로 줄었어요."
"강도현 씨가 2020년 7월에 1,000건씩 끊은 이유가 뭐였지?"
"언두가 모자라서요. ORA-30036."
"직접 경로는 고수위선 위의 새 블록에 써. 커밋 전에는 그 블록이 아직 테이블에 없는 거나 마찬가지라, 롤백할 때는 고수위선을 안 올리면 끝이야. 되돌림 슬립을 쓸 일이 없지." 선배가 13화의 정정 전 사본함 얘기를 꺼냈다. "사본함에 슬립을 안 넣으니 함이 찰 일도 없고. 2020년의 근거가 이 판에서는 사라진 거야. 그래서 커밋 한 번으로도 안전해."
정합성도 쟀다. 08화 이후 모든 리포트에 적용해 온 규칙이다. rev00과 rev02의 결과를 나란히 놓고 건수, 합계, 양방향 MINUS, 번호 중복을 비교했다.
NEW_ROWS OLD_ROWS NEW_SUM OLD_SUM NEW_MINUS_OLD OLD_MINUS_NEW DUP_SEQ
100000 100000 25,519,833,100 25,519,833,100 0 0 0다섯 개 다 통과였다. 그런데 선배가 한 줄을 더 쳐 보라고 했다.
"정산번호(STLM_SEQ)도 같아. 왜 같지? 둘 다 ORDER BY가 없는데."
"…읽은 순서대로 번호를 붙이니까요. 테이블이 ORD_SEQ 순으로 쌓여 있으면 같고요." ORD_SEQ 순서와 비교해 보니 다른 행은 0이었다.
"그럼 그걸 리포트의 리스크 칸에 써. '이 데이터에서 같다'는 '맞다'가 아니야. 02화에서 배운 거지. 원천 테이블이 MOVE되거나 다시 쌓이면 번호가 바뀔 수 있어. rev00도 마찬가지고."
하나가 남았다. rev02는 한 문장이라 중간 상태가 없다. TRUNCATE 직후부터 커밋까지 다른 세션에는 0건으로 보이고, 커밋하는 순간 10만 건이 한꺼번에 보인다.
"반만 보이는 것보다 텅 빈 게 나아요?"
"둘 다 안 돼. 그러니까 아침 전에 끝나야 하는 거고." 선배가 말했다. "그래도 둘 중에 고르라면 빈 화면이 나아. 반쯤 찬 화면은 사람이 맞는 숫자라고 믿고 써 버리거든. 빈 화면은 아무도 안 믿어."
커서 FOR 루프가 왜 느린지, 즉 SQL과 PL/SQL 사이를 10만 번 오가는 비용이 어디서 나는지는 다음 주(15화)에 잰다. 오늘은 한 문장으로 바꿀 수 있는가까지만 본다.
오후 2시 40분 — 05화의 청구서
"이제 ②야. 아침에 네가 범인으로 지목했다가 0.06초로 풀려난 거." 선배가 말했다. "시간을 잡아먹은 범인은 아니었어. 그런데 05화에서 내가 이건 14화에 청구서가 온다고 했지. 무엇으로 온다고 했어?"
05화 마지막 장면, 3층 문서보관실의 분필 선 앞에서 나눈 대화가 떠올랐다. "매달 똑같은 일을 하네요." "그래. 오늘 네가 한 건 치료가 아니라 청소야."
"…시간이 아니라 읽는 양이요. 지워도 분필 선이 안 내려가니까."
한 달치를 DELETE한 뒤 이력 테이블의 전체 스캔을 쟀다. 3,602 → 3,602. 05화와 똑같은 숫자가 05화와 똑같이 꿈쩍하지 않았다. 매달 청소(SHRINK SPACE)를 하면 3,489로 내려가지만, 그 청소에 redo가 17.96MB 들었다. 테이블 전체를 옮겨 쓰기 때문에 지운 한 달치(4.84MB)보다 네 배 가까이 비쌌다.
"05화에서 문서보관실 구역을 칸막이로 나누면 어떻게 된다고 했지?"
"지난달 칸막이를 통째로 들어낸다고요."
같은 데이터를 LOG_DT 기준 월별 파티션 30개로 다시 지었다. 그리고 같은 한 달을 들어냈다.
월말 정리 (한 달 · 17,235 행) redo undo 블록 변경 경과 그 뒤 전체 스캔
DELETE 4.84 MB 2.32 MB 36,691 0.06 초 3,602 (그대로)
(매달 청소) SHRINK SPACE 17.96 MB 6.62 MB 126,577 2.21 초 3,489
DROP PARTITION 0.01 MB 0.01 MB 96 0.04 초 3,630 → 3,505"행을 하나씩 지우지 않아. 칸막이째 들어내." 선배가 말했다. "05화에서 강도현 씨 커밋 로그 기억나?"
2021-03-30 hd.kang
이력 테이블 비대. 월 1회 DELETE 로 6개월 이전 삭제하는 배치 추가.
TRUNCATE 는 WHERE 를 못 쓰므로 불가. 파티션은 유상 옵션이라 승인 못 받음."그때도 옳았어. 숫자 없이 '파티션 사 주세요'라고 하면 승인이 안 나. 지금은 숫자가 있지. 그걸 오늘 리포트에 붙여서 예산을 다시 올리는 거야."
그런데 들어낸 뒤 인덱스 목록에 빨간 줄이 하나 생겼다. 주문번호로 이력을 찾는 인덱스를 파티션 없이(GLOBAL) 만들어 뒀는데, 그 인덱스가 UNUSABLE이 돼 있었다. 그 상태로 낮의 이력 조회 화면을 돌리자 에러 없이 전체 스캔 3,505로 갈아탔다. 인덱스가 살아 있으면 3블록이면 끝나는 조회다(다음 달 칸을 들어낸 뒤 잰 값).
"칸막이를 들어낼 때, 칸막이를 가로지르는 대장이 있으면 그 대장이 깨져. UPDATE GLOBAL INDEXES를 붙이면 살아 있고." 다음 달 칸은 그렇게 들어냈다. redo 0.01MB, 인덱스는 VALID, 조회는 3블록이었다. "이 버전은 지워진 칸을 가리키는 대장 항목을 나중에 따로 치워. 그래서 들어내는 순간에는 여전히 싸."
오후 3시 30분 — 칸막이를 몇 개 여는가
"이력 테이블에서 됐으면 T_ORD_BIG도 월별로 나누면 되잖아요."
"나눠 봐. 그리고 Q1이 칸막이를 몇 개 여는지 세 봐."
T_ORD_BIG을 주문월 기준 파티션 36개(2022-01 ~ 2024-12)와 PMAX 하나로 다시 쌓은 복사본을 만들었다. 그리고 리비전 폴더의 두 판을 나란히 돌렸다.
Q1 rev00 TO_CHAR(ORD_DT,'YYYYMMDD') BETWEEN '20240601' AND '20240630'
| PARTITION RANGE ALL | Pstart 1 | Pstop 37 | Buffers 5,868 |
Q1 rev01 ORD_DT >= DATE '2024-06-01' AND ORD_DT < DATE '2024-07-01'
| PARTITION RANGE SINGLE| Pstart 30 | Pstop 30 | Buffers 161 |Pstart와 Pstop을 보면 어디부터 어디까지 칸막이를 열었는지 알 수 있다. rev00은 1번부터 37번까지 전부 열었고, rev01은 30번 하나(2024년 6월)만 열었다.
"02화 출입대장 기억나지? 날짜순 대장인데 '여덟 자리 숫자로 바꿔서' 물으면 대장 순서가 소용없었잖아." 선배가 말했다. "칸막이도 똑같아. 칸막이에는 '2024년 6월'이라고 붙어 있어. TO_CHAR로 바꾼 '20240601'은 어느 칸막이에도 안 붙어 있지. 그러니까 전부 열어 보는 수밖에 없어."
TRUNC(ORD_DT,'MM') = DATE '2024-06-01'도 해 봤다. 칸막이가 월 단위이니 이건 알아듣지 않을까 싶었다. 결과는 ALL · 1 · 37 · 5,868. 가공은 가공이었다. 바인드 변수로 바꾸면 Pstart가 KEY로 바뀌었다. 계획을 세울 때는 몇 번 칸인지 모르고 실행할 때 고른다는 뜻이다. 그래도 161이었다.
"파티션 키는 가공하지 않은 채로 조건에 와야 칸막이를 골라. 인덱스 규칙이랑 똑같아. 그래서 rev01은 파티션에서도 맞는 판이야."
그런데 161을 보니 걸리는 게 하나 있었다. "원본 T_ORD_BIG에서 rev02는 91이었잖아요. 파티션으로 나눴는데 오히려 늘었어요."
"한 칸을 통째로 읽은 게 161이고, 커버링 인덱스는 91이야. 파티션이 조회를 빠르게 해 주는 게 아니야. 파티션 덕분에 '한 칸만 열면 되는' 것뿐이고, 그 칸 안에서는 다시 인덱스 얘기야."
같은 커버링 인덱스를 칸마다 하나씩(LOCAL) 만들어 봤다. 옵티마이저는 그래도 6월 칸 FULL(161)을 골랐고, 억지로 태우니 89였다. 파티션 없는 원본의 91과 거의 같았다.
"봐. 칸 안에서는 똑같은 인덱스 얘기야." 선배가 말했다. "파티션이 확실하게 이기는 건 아까 본 거야. 칸막이째 들어내는 것, 그리고 칸막이째 다시 싣는 것."
나는 그 말을 바로 받았다.
"그럼 둘 다 쓰면 되겠네요. 월별 파티션에, 주간 재적재는 오전에 잰 지게차로요. TRUNCATE하고 APPEND로요."
"해 봐."
오후 4시 10분 — 무기 둘
파티션 복사본을 TRUNCATE하고 INSERT /*+ APPEND */로 100만 행을 다시 실었다. 그리고 같은 rev01을 돌렸다.
| PARTITION RANGE SINGLE | Pstart 30 | Pstop 30 | Buffers 1,169 |멍하니 화면만 봤다. 161이 1,169가 됐다. 같은 6월 칸 하나, 같은 27,390건이었다. 칸막이도 정확히 하나만 열었다.
"…일곱 배요. 왜요? 칸은 하나만 열었는데."
복사본을 처음부터 다시 지어서 같은 일을 해 봤다. 또 1,169였다. 테이블 크기를 확인했다.
CTAS 로 만든 직후 세그먼트 36개 · 288 MB
TRUNCATE → APPEND 뒤 세그먼트 36개 · 576 MB두 배가 돼 있었다. 같은 100만 행인데.
"무기 둘을 다 칭찬했는데." 선배가 의자를 돌렸다. "05화 분필 선은 어디 그어져 있을까. 6월 칸 안을 들여다봐."
T_ORD_BIG_P14 P202406 : full 160 · 빈 블록(fs4) 1006데이터가 든 블록은 160개였다. 그리고 분필 선 아래에 완전히 빈 블록이 1,006개 있었다. 전체 스캔은 분필 선까지 다 열어 보니, 160 + 1,006으로 1,169 근처가 나온 것이다.
"빈 블록이 왜 선 아래에 있어요? TRUNCATE했으면 다 비웠잖아요."
"TRUNCATE는 칸마다 첫 익스텐트 하나만 남기고 나머지를 반납해. 그런데 파티션의 첫 익스텐트는 8MB야. 1,024블록이지. 이 버전은 파티션을 큼직하게 시작하거든." 선배가 하역장 얘기로 돌아갔다. "지게차 규칙 기억나? 맨 안쪽 빈 구역에 내려놓는다. 이 지게차는 지금 구역에 남은 자리를 안 써. 구역의 남은 칸을 전부 '비었음'으로 표시해 두고 다음 구역부터 내려놔. 그래서 칸막이마다 그런 빈 구역이 8MB씩 생긴 거야. 그리고 그 빈 구역이 분필 선 아래로 들어가 버렸어."
CTAS로 처음 지었을 때는 161이었다. CTAS는 첫 구역부터 채웠고, APPEND만 첫 구역을 건너뛰었다. 비교용으로 파티션을 STORAGE (INITIAL 64K)로, 즉 첫 구역을 작게 잡아 만든 복사본에도 똑같이 TRUNCATE → APPEND를 해 봤다.
| 6월 한 칸 전체 스캔 | 세그먼트 합계 | Buffers | 빈 블록 |
|---|---|---|---|
파티션 · CTAS로 처음 지음 | 288MB | 161 | — |
파티션 · TRUNCATE → APPEND 재적재 | 576MB | 1,169 | 1,006 |
파티션 INITIAL 64K · TRUNCATE → APPEND | 72MB | 169 | 5 |
"무기 하나하나는 다 맞았어. 파티션 덕분에 칸막이째 들어낼 수 있었고, 지게차는 329초를 5초로 줄였어. 그런데 둘을 같이 쓰니까 서로를 망쳤지." 선배가 말했다. "그리고 이게 제일 무서운 종류야. 에러가 없어. 계획도 SINGLE로 멀쩡해. 칸막이를 하나만 연다는 것까지 맞아. 그 한 칸이 일곱 배로 뚱뚱해졌을 뿐이야. Pstart/Pstop만 보고 '파티션 잘 탄다'고 보고했으면, 다음 주 월요일부터 6월 조회가 7배로 무거워졌을 거야."
리포트 초안에 그 내용을 옮겨 적었다. "파티션 + TRUNCATE + APPEND 재적재는 칸마다 첫 익스텐트(8MB)를 비워 둔다. 파티션 저장 절에 INITIAL을 명시한다."
오후 4시 50분 — 이미 파티션을 쓰는 곳
"이 병을 앓는 데가 하나 더 있어." 선배가 다른 창을 열었다. 04화에서 본 버거원 POS였다. "버거원은 이미 파티션을 쓰고 있잖아. 우리가 예산을 올리기 전에, 먼저 쓰고 있는 데가 어떻게 쓰는지 보자."
버거원에는 같은 주문 100만 행이 네 가지 모양으로 있었다. 크기부터 쟀다.
ORD 파티션 없음 세그먼트 1 · 176 MB
ORD_LIST LIST 25칸 (주문월 ORD_YM) 세그먼트 24 · 384 MB
ORD_LIST_HASH LIST 25칸 × HASH 8조각 세그먼트 192 · 3,072 MB같은 100만 행이 3GB를 차지했다. 칸 안을 들여다보니 아까 본 그 숫자가 있었다. 서브파티션마다 데이터 블록은 120개 남짓인데 빈 블록이 1,006개였다. 이관 스크립트가 빈 테이블을 먼저 만들고 INSERT /*+ APPEND PARALLEL(ORL 4) */로 채운 흔적이었다. 192칸마다 8MB씩 비워 둔 것이다.
"2012년 6월 한 달을 세면?" 같은 질문을 다섯 가지로 물었다.
2012년 6월 · 매장별 주문 수 칸 Buffers
ORD_LIST ORD_YM = '201206' SINGLE 6 1,980
ORD_LIST SUBSTR(ORD_DT,1,6) = '201206' ALL 1 ~ 25 47,460
ORD_LIST ORD_DT LIKE '201206%' (키가 아닌 컬럼 · 로컬 인덱스) ALL 1 ~ 25 41,897
ORD_LIST_HASH ORD_YM = '201206' SINGLE + HASH 1 ~ 8 9,039
(기준) 파티션 없는 ORD · 정렬된 테이블 + 인덱스 (← 04화) — 1,089맨 아래 줄에서 눈이 멈췄다. 파티션이 없는 ORD가 제일 적게 읽었다. 04화에서 본, 잘 정렬된 바로 그 테이블이었다.
"키를 가공하면 칸을 다 열고, 키가 아닌 컬럼으로 물으면 칸마다 인덱스를 하나씩 뒤져. 그리고 칸 안에 빈 블록을 안고 있으면 한 칸을 열어도 비싸." 선배가 말했다. "파티션은 설계야. 키를 무엇으로 잡았는지, 화면이 그 키로 묻는지, 칸이 몇 개인지, 어떻게 채우는지. 버거원은 넷 다 근거가 없어. 해시 8조각은 누가 왜 나눴는지 아무도 몰라."
같은 설계(25칸 × 해시 8조각)를 CTAS로 다시 지어 보면 3GB의 정체를 가를 수 있었다. 다만 그러려면 먼저 자리를 만들어야 했다. 버거원 DB는 Free의 12GB 한도에 거의 차 있었다. 그래서 크기를 이미 잰 ORD_ITEM_LIST_HASH(3GB)를 지웠다. 버거원은 오늘이 마지막 무대였다.
ORD_LIST_HASH (빈 테이블 + APPEND) 3,072 MB · 6월 9,039
같은 설계 · CTAS 1,536 MB · 6월 982
같은 설계 · CTAS + INITIAL 64K 379 MB · 6월 9983GB의 절반은 칸마다 8MB씩 잡아 둔 자리였고, 나머지 절반은 APPEND가 비워 둔 빈 블록이었다. 읽기 비용을 9배로 키운 건 뒤의 절반이었다. 그리고 이관 스크립트의 PARALLEL(ORL 4)와 PARALLEL 8은 아침에 본 대로 이 DB(Free)에서는 처음부터 돌 수 없었다.
"버거원 보고서에 쓸 문장이 나왔네." 선배가 말했다. "'파티션을 쓰고 있다'는 사실은 아무것도 보증하지 않는다."
⛳ 오늘은 여기까지(중단 지점 2) 무기 둘(파티션과
APPEND)을 함께 들었다가 한 달 조회가 161 → 1,169로 늘어나는 것을 봤다. 원인(8MB 첫 익스텐트를 비워 두는 직접 경로 적재)과 처방(INITIAL을 명시하면 169)도 쟀다. 버거원의 3GB도 같은 병이었다. 오늘 몫은 여기까지만 해도 다 한 것이다. 이어지는 저녁에는 처음으로 배치 리포트(T3)를 쓰고, 04화가 남긴 인덱스 폐기 승인서를 마무리한다.
오후 6시 00분 — 처음으로 쓰는 배치 리포트
리포트를 쓰려고 T1 양식을 열자 선배가 말렸다.
"T1은 SQL 하나를 '덜 읽게' 하는 문서야. 배치는 목표 자체가 달라. 정해진 시간 창 안에 끝내는 것이 목표지. 블록 수가 아니라 마감까지 남은 시간으로 말해야 하고, 단계별로 쪼개야 하고, 실패하면 어디서 다시 시작하는지, 도는 동안 낮 업무의 무엇을 막는지까지 적어야 해. 그래서 양식이 따로 있어야 해."
그렇게 해서 T3 배치 SLA 리포트 양식을 만들었다. 1호는 일요일 밤 체인이었다. 숫자는 모두 연습 DB 복사본에서 잰 것이고, 칸마다 그 사실을 적었다.
단계 (연습 DB · 단독 실행) 지금 (운영 그대로) 바꾼 뒤
① 재적재 100만 · 인덱스 3개 일반 INSERT 329 초 · 916.6 MB APPEND + 끄고 켜기 5.0 초 · 0.6 MB
① 통계 수집 1.06 초 1.06 초 (유지 — 히스토그램)
② 월말 이력 정리 DELETE 0.06 초 · 전체 스캔 그대로 (파티션 승인 시) DROP PARTITION 0.04 초
③ 월마감 집계 PARALLEL(8) 0.05 초 · 힌트 Unused 판정 불가 — 힌트 제거 후 운영에서 재측정
④ P1 정산 적재 rev00 0.89 초 · 28.66 MB rev02 0.11 초 · 0.08 MB양식에서 반드시 채워야 하는 칸을 채우다가 두 번 멈췄다. 하나는 「동시 실행 영향」 칸이었다. 바꾼 뒤의 체인은 도는 동안 T_ORD_BIG에 배타 락을 걸고, 인덱스가 꺼진 몇 초 동안 Q1이 91이 아니라 5,822블록을 읽는다. 마감 전에 끝나면 아무도 모르지만, 넘기면 막힌다. 다른 하나는 「추세」 칸으로, 데이터가 얼마나 늘면 다시 넘치는지를 적는 자리였다. 운영 로그에 단계별 시각이 없어서 쓸 수가 없었다.
"그게 재발방지 1번이야. 단계마다 시작과 끝을 로그에 남긴다. 오늘 아침에 그게 있었으면 연습 DB에서 다시 재현할 필요도 없었어."
리포트 끝에는 13화의 「커밋 주기 기준」 경로(#1 집합 연산 → 커밋 한 번)와 정합성 회귀 체크리스트를 붙였다. 체크리스트 E 칸에는 정산번호가 ORDER BY 없이 읽은 순서로 붙는다는 것을, H 칸에는 정산 테이블을 읽는 화면이 적재하는 동안 0건을 본다는 것을 적었다.
마지막으로 04화가 승인 대기로 넘긴 서류가 남아 있었다. T2-002: IX_ORD_BIG_DT 폐기. 04화에서는 인덱스 2개를 기준으로 근거를 적었는데, 지금은 3개였다. 같은 방법으로 다시 쟀다.
5만 건 INSERT 인덱스 3개 (DT · DT_AMT · DT_SEQ) redo 38.55 MB
IX_ORD_BIG_DT 만 뺀 2개 (DT_AMT · DT_SEQ) redo 26.85 MB (−11.70 MB · 블록 변경 301,708 → 201,131)T2 양식을 펴 보니 3번 「비용」 칸에 "【14화】 야간 배치 적재 소요 Δ"라는 줄이 빈칸으로 남아 있었다. 오늘 재 보니 그 한 줄로는 모자랐다. 어떤 방식으로 적재하느냐에 따라 인덱스 하나의 값이 전혀 달랐기 때문이다. 일반 INSERT로 켠 채 넣으면 행마다 비용이 청구되고, 끄고 켜기로 넣으면 재생성 몇 초로 바뀐다. 그래서 「적재 비용」 칸을 새로 만들어 T2를 고치고, 개정 이력에 이유를 남겼다.
T2-002의 그 칸에는 두 줄이 들어갔다. 지금 방식(일반 INSERT로 켠 채)이면 5만 건마다 redo를 11.70MB 덜 낸다. 바꾼 방식(끄고 켜기)이면 재생성 하나 몫만 덜 낸다. 어느 쪽이든 Q1은 커버링 인덱스를 쓰니 91 그대로였다. 04화가 요구한 선결 조건(T_ORD_BIG을 쓰는 SQL 전수 조사)은 그대로 남겨 두고 승인 요청을 올렸다. 실제로 지우는 건 여전히 다음 주다. '명백하다'가 가장 위험한 단어라는 04화의 문장도 그대로 유효했다.
오후 7시 10분
퇴근하면서 박팀장과 오과장에게 같은 메시지를 보냈다.
나 19:05
어젯밤 체인의 99% 는 T_ORD_BIG 재적재였습니다 (월말 정리는 1% 미만).
인덱스 3개를 켠 채 한 행씩 넣는 방식이라, 연습 DB 기준 같은 100만 건이 329초 → 5초(인덱스를 끄고 적재 후 재생성)로 줄어듭니다.
통계 수집 단계는 빼면 안 됩니다(1초짜리이고, 빼면 히스토그램이 사라집니다).
정산 화면이 반만 보인 건 P1 이 1,000건마다 커밋했기 때문이고, 한 문장 적재로 바꿉니다(결과 대조 통과).
PARALLEL(8) 은 효과를 확인할 수 없어 빼고, 운영에서 단계별 시각을 남겨 다시 보겠습니다.
월말 정리를 근본적으로 없애려면 파티션(유상 옵션)이 필요합니다 — 근거 수치를 리포트에 붙였습니다.오과장 19:11
인덱스를 끄고 넣는다는 건 생각을 못 했네요. 이번 주 수요일 밤에 연습 서버에서 같이 돌려 봐요.박팀장 19:20
숫자 확인했습니다. 파티션 건은 다음 주 예산 회의에 올리겠습니다.리비전 폴더를 열었다. P1_rev00 옆에 새 판이 두 개 생겼다.
P1_rev00 커서 FOR 루프 · 1,000건 커밋 0.89 초 · redo 28.66 MB · undo 6.50 MB
P1_rev01 INSERT … SELECT · 커밋 한 번 0.08 초 · redo 4.47 MB · undo 0.26 MB
P1_rev02 + APPEND 0.11 초 · redo 0.08 MB · undo 0.02 MB13화에 붙여 둔 메모 아래에 한 줄을 더 적었다. 전진. 측정이 정당화했다. 단, rev02는 마감 전에 끝난다는 전제에서만 안전하다(배타 락).
오늘 남은 것
| 무엇 | 값 |
|---|---|
| 체인 단계 분해 (연습 DB) | 재적재 329초 = 99% · 월말 정리 0.06초 · 집계 0.05초 · P1 0.89초 |
| 청구서 정산 (5만 건) | 인덱스 0 → 3개: redo 2.44 → 38.55MB · 블록 변경 1,921 → 301,704 |
| 무기 (100만 건) | 일반·켠 채 329초 · 916.6MB → APPEND + 끄고 켜기 5.0초 · 0.6MB |
| 대가 | 배타 락(LMODE 6 · ORA-00054) · 꺼진 동안 Q1 5,822 · DELETE로 비운 칸 무시(5,889 vs 7,862) · 온라인 통계에 히스토그램 없음 · ORA-12838은 이 버전에서 안 남 |
| P1 | rev00 → rev01 → rev02 · undo 6.50 → 0.02MB · 정합성 5항목 통과 |
| 월말 정리 | DELETE 3,602 그대로 · SHRINK redo 17.96MB · DROP PARTITION redo 0.01MB |
| Pruning | rev00 ALL 1~37 5,868 · rev01 SINGLE 30 161 |
| 무기 둘이 만나면 | 파티션 + TRUNCATE→APPEND: 6월 161 → 1,169(빈 블록 1,006) · INITIAL 64K → 169 |
| 버거원 | ORD_LIST_HASH 3GB · 6월 9,039(CTAS 982) · SUBSTR 가공 47,460 |
| 병렬 | PARALLEL(8) → Hint Report U - Unused · V$PQ_TQSTAT 0행 |
| T3 배치 SLA 리포트 | 새 양식 · 1호 |
| T2 개정 | 「적재 비용」 칸 · T2-002 마무리 |
오늘 배운 한 문장
배치 시간을 잡아먹는 것은 행을 넣는 일 자체가 아니라, 행마다 따라붙는 일들이다. 인덱스 유지, 리두, 언두, 커밋을 행마다 치르지 않고 한 번에 치르는 것이 배치 튜닝이다. 그리고 무기마다 대가가 있다. 대가를 재지 않고 무기 둘을 함께 들면 둘이 서로를 망친다.
다음으로 넘기는 것
- 커서 FOR 루프는 왜 느린가: SQL ↔ PL/SQL 왕복에서 비용이 생기는 구조, 한 문장으로 못 바꾸는 행 단위 로직 → 15화
- 단계별 시각이 남는 배치: 운영 체인을 시간으로 보는 도구 → 16 · 17화
- 오늘의 T3, T2 개정, 「SQL 코딩 가이드」 후보 조문(파티션 키는 가공하지 않는다 · 적재 방식을 적는다) → 18화
다음 →
3_실습과_기록.md에서 체인을 직접 재현하고, 무기와 그 대가를 재고, T3 1호를 쓴다. 개념이 막히면2_파고들기.md를 펼친다.