테마
10화 · 실습과 기록
여기서 하는 일: 일요일 밤을 복사본에서 재현하고, E-Rows를 손으로 계산하고, 히스토그램을 만들어 보고, 정책서 1호를 원본에 건다.
1_사건.md를 읽은 것은 "선배가 하는 걸 지켜본 것"이고, 이 문서를 끝내야 직접 해 본 것이 된다. 개념이 막히면2_파고들기.md를 펼친다.시간이 없는 주에는 STEP 0 → 1 → 2 → 4 → 5 → 8 → 9만 한다(약 60분). STEP 8(정책 적용)은 건너뛰면 안 된다. 11화가 그 상태에서 시작한다.
STEP 0 — 직전 리비전을 연다 · 전제를 확인한다 (3분) ★ 매주 처음 하는 일
bash
docker start ora-hanorder
docker start ora-dist # STEP 7 만bash
cat ../06화_100만_번의_왕복/리비전/Q2_rev01.sql
cat ../03화_날짜가_문자로_박혀_있다/리비전/Q1_rev03.sql오늘은 이 두 파일을 한 글자도 고치지 않는다. 그런데도 계획이 바뀌는 것을 보게 된다.
sql
-- ① T_ORD_BIG 의 인덱스 (기대: 3개 — IX_ORD_BIG_DT · IX_ORD_BIG_DT_AMT · IX_ORD_BIG_DT_SEQ)
SELECT index_name FROM user_indexes WHERE table_name = 'T_ORD_BIG' ORDER BY 1;
-- ② 통계 선호도와 히스토그램 (기대: 'FOR ALL COLUMNS SIZE 1' · 히스토그램 전부 NONE — 09화 종료 상태)
SELECT DBMS_STATS.GET_PREFS('METHOD_OPT', USER, 'T_ORD_BIG') FROM dual;
SELECT column_name, histogram FROM user_tab_col_statistics WHERE table_name = 'T_ORD_BIG';
-- ③ 자동 통계 수집과 그 창 (기대: ENABLED · 평일 22:00 4시간 · 토/일 06:00 20시간)
SELECT client_name, status FROM dba_autotask_client WHERE client_name LIKE 'auto optimizer%';
SELECT window_name, repeat_interval, duration FROM dba_scheduler_windows WHERE window_name LIKE '%DAY_WINDOW';②에서
ORD_ST가 이미FREQUENCY라면 이 화를 한 번 돌린 것이다(STEP 8의 결과). 그대로 진행해도 된다. 모든 실험은 복사본에서 하고, STEP 8이 다시 같은 상태로 되돌린다. ②에서ORD_ST외의 열에 히스토그램이 있으면 밤사이 자동 수집이 만든 것이다.00_자료준비/고객사/L1_ora-hanorder/04_상태복구.sql을 돌린 뒤 시작한다.
STEP 1 — 예측을 봉인한다 (4분)
일요일 밤: T_ORD_BIG 을 TRUNCATE → (자동 통계 수집이 빈 테이블을 수집) → 100만 행 재적재 → 통계 수집 없음
월요일 아침: 같은 Q1 · Q2 를 돌린다
Q1. 월요일 아침 Q2 의 Buffers 는? (어제 5,869) ______
Q2. 통계를 '0 건' 이 아니라 '아예 없음' 으로 두면 Q2 는? ______
Q3. ORD_ST = 'CANCEL' 의 E-Rows 는 333,333 이다. 손으로 계산하는 식은?
______________________
Q4. ORD_ST 에 히스토그램을 넣으면 'CUS_000001 고객의 취소 건' (실제 500) 의 E-Rows 는
더 정확해질까, 더 틀릴까? ______▶ 적고 나서 펼치기
Q1: 대부분 "조금 늘어난다"고 쓴다. 실측(복사본)은 1,026,852다. 06화 첫날 강도현의 USE_NL 힌트가 만들던 1,026,849와 세 블록 차이다. 옵티마이저가 테이블을 1행이라고 믿고 NL을 골랐다.
Q2: "더 나빠진다"고 쓰기 쉽다. 실측은 5,869(해시 조인)다. 통계가 없으면 동적 샘플링으로 표본을 뜬다. 틀린 통계는 없는 통계보다 나쁘다.
Q3: NUM_ROWS × 1/NDV = 1,000,000 × 1/3.
Q4: "정확해진다"고 쓰기 쉽다. 실측은 167 → 10으로 더 틀린다. 이 데이터는 고객마다 상태가 하나뿐이다. 히스토그램에는 한 열의 분포만 담길 뿐, 열과 열 사이의 관계는 담기지 않는다.
STEP 2 — 일요일 밤을 복사본에서 재현한다 (5분) ★ 오늘의 본체
bash
cd 10_본편_튜너의_1년/10화_어제는_0.1초_오늘은_30초/실습
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 00_어제와_오늘.sqlpowershell
Set-Location 10_본편_튜너의_1년\10화_어제는_0.1초_오늘은_30초\실습
Get-Content 00_어제와_오늘.sql -Encoding UTF8 | docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1복사본 T_ORD_BIG_C10을 만들고 끝에서 지운다. 원본 T_ORD_BIG에는 한 줄도 쓰지 않는다. 30초~1분 걸린다.
⚠️ 재현 조건 — 운영은 '일반 INSERT', 이 스크립트는
APPEND + NO_GATHER_OPTIMIZER_STATISTICS연습 DB에서 인덱스 3개를 단 채 일반 INSERT로 100만 행을 넣으면 9분 넘게 걸린다(실측 9분 13초, 다른 작업과 겹친 날은 22분 46초. 10MB짜리 리두 로그 두 개 때문에
log file switch (checkpoint incomplete)대기가 생긴다. 14화에서 다룬다). 그래서 같은 통계 상태(테이블 · 인덱스 통계 0)를 1분 안에(실측 9~36초) 만든다. 그대로 재현하고 싶으면 STEP 6의90_적재_일반INSERT.sql을 돌린다.
| 단계 | 무엇 | 기대 | ☐ |
|---|---|---|---|
| [A1] [A2] | 연습 DB 원본의 Q1 · Q2 | 91 · 5,869 (해시). 멀쩡하다 | ☐ |
| [B1] [B2] | 어제: 복사본 (통계 있음) | 91 · 5,863 (해시) | ☐ |
| [C] | TRUNCATE 직후 | STALE_STATS YES · TRUNCATED YES · LIST STALE -> T_ORD_BIG_C10 | ☐ |
| [C] | 자동 수집 대신 모은 통계 | NUM_ROWS 0 · BLOCKS 0 | ☐ |
| [D] | 100만 행 적재 (수집 없음) | 9~36초 (그날 컨테이너 사정에 따라 다르다) | ☐ |
| [E1] | 오늘의 Q1 | E-Rows 1 · IX_C10_DT + TABLE ACCESS BY INDEX ROWID · 27,274 | ☐ |
| [E2] | 오늘의 Q2 | E-Rows 1 · NESTED LOOPS · M_CUS 쪽 Starts 1000K · 1,026,852 | ☐ |
| [E3] | 힌트로 해시 고정 (기각할 대안) | HASH JOIN · 5,868. E-Rows는 여전히 1 | ☐ |
| [E4] | 정합성: 오늘 Q2 vs 원본 Q2 | 3 = 3 · 합계 같음 · MINUS 0 / 0 | ☐ |
| [F] | 통계 vs 실제 · 이력 | STATS 0 / ACTUAL 1000000 · 인덱스 통계 0 · 이력 두 줄 | ☐ |
| [G1] [G2] | RESTORE_TABLE_STATS (TRUNCATE 전으로) | NUM_ROWS 1,000,000 · Q1 82 · Q2 5,868 | ☐ |
| [H2] | 다시 모으기 | 1.2~1.4초 · Q2 5,868 | ☐ |
[I] 원장을 옮겨 적는다
| TAG | 계획 (FIRST_READ) | E_ROWS | A_ROWS | BUFFERS | MS |
|---|---|---|---|---|---|
| [B2] 어제 Q2 | |||||
| [E2] 오늘 Q2 | |||||
| [G2] 되돌린 뒤 |
📌 [E2]에서 볼 것은
Buffers가 아니라E-Rows다 모든 줄의
E-Rows가 1이다. 한 줄짜리 테이블이라면 고객 카드를 한 번 찾아오는 NL이 맞다. 판단은 옳았고, 믿음이 틀렸다. 06화에서는 힌트를 지워 판단을 옵티마이저에게 돌려줬다. 오늘은 그 판단의 재료(통계)가 0이었다.
STEP 3 — 적재 방식과 통계 (10분)
bash
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 01_적재방식과_통계.sqlpowershell
Get-Content 01_적재방식과_통계.sql -Encoding UTF8 | docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1약 1분 걸리고, 전부 복사본에서 한다. [D]는 SET_TABLE_STATS로 통계를 손으로 바꾸고, 끝에서 다시 모아 USER_STATS = NO로 원복됐는지 확인한다.
| 단계 | 무엇 | 기대 | ☐ |
|---|---|---|---|
| [A] | 빈 테이블에 APPEND (힌트 없음) | NUM_ROWS 1,000,000 즉시 · NOTES = STATS_ON_LOAD · Q1 82 · Q2 5,864 | ☐ |
| [B] | 통계 삭제 후 적재 | Note dynamic sampling (level=2) · Q2 E-Rows 889,390 · 해시 5,869 | ☐ |
| [C] | 어제가 1만 행 | 통계 NUM_ROWS 10,000 · Q1 82 (E-Rows 283) · Q2 해시 5,872. 안 바뀐다 | ☐ |
| [D] | 문턱 | NUM_ROWS 2,000까지 NL · 2,500부터 HASH · 원복 USER_STATS = NO | ☐ |
| [E] | 최댓값 밖 | STALE_PERCENT 10 · 한 달 더 실어도 STALE_STATS NO · 1월 E-Rows 913 / A-Rows 27,390 | ☐ |
📌 [A]와 [B]에서 알 수 있는 것
[A]: 같은 배치가
APPEND였다면 23ai가 적재하면서 통계를 모아 사고가 나지 않았다. 일반 INSERT에는 이 기능이 없다. 적재 방식에 따라 통계가 달라지므로 정책서에 적재 방식을 적는다. [B]: 통계가 없으면 오라클은 표본을 뜬다. 0이라고 적혀 있으면 믿는다. 틀린 통계는 없는 통계보다 나쁘다.
STEP 4 — E-Rows를 손으로 계산한다 (15분) ★
스크립트를 돌리기 전에 아래 표의 '손계산' 칸부터 채운다. 재료: T_ORD_BIG 1,000,000행 · ORD_DT NDV 1,095 · 2022-01-01 ~ 2024-12-30 (폭 1,094일) · CUS_ID NDV 2,000 · M_CUS 2,000행 · T_ORD_DET 200,000행 · T_ITM_EVL 5,000행 · ITM_ID NDV 100.
| # | 조건 | 손계산 (먼저 쓴다) | 실측 E-Rows |
|---|---|---|---|
| ① | ORD_DT = DATE '2024-06-01' | ||
| ② | ORD_DT >= DATE '2024-06-01' AND < DATE '2024-07-01' | ||
| ③ | TO_CHAR(ORD_DT,'YYYYMMDD') BETWEEN … | ||
| ④ | TO_CHAR(ORD_DT,'YYYYMM') = '202406' | ||
| ⑤ | T_ORD_BIG ⋈ M_CUS (CUS_ID) | ||
| ⑥ | T_ORD_DET ⋈ T_ITM_EVL (ITM_ID) | ||
| ⑦ | ORD_DT = DATE '2025-12-30' (최댓값 + 365일) | ||
| ⑧ | 포스트잇 조건 (ROWNUM 없이) |
bash
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 02_손계산.sqlpowershell
Get-Content 02_손계산.sql -Encoding UTF8 | docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1원본을 읽기만 한다. 약 5초 걸린다.
▶ 채우고 나서 펼치기
| # | 손계산 | 실측 |
|---|---|---|
| ① | 1,000,000 / 1,095 = 913.2 | 913 |
| ② | 1,000,000 × (30/1,094 + 1/1,095) = 28,335.5. 닫힌 끝 하나에 1/NDV | 28,336 |
| ③ | 1,000,000 × 5% × 5% (가공 · 양쪽 범위) | 2,500 |
| ④ | 1,000,000 × 1% (가공 · 등치) | 10,000 |
| ⑤ | 1,000,000 × 2,000 / MAX(2,000, 2,000) | 1,000,000 |
| ⑥ | 200,000 × 5,000 / MAX(100, 100) | 10,000,000 |
| ⑦ | 913.2 × (1 − 365/1,094) = 608.5 | 609 |
| ⑧ | 1,000,000 × (1 − (1/1,095)(1 − 988,783/999,999)) | 999,990 |
②가 02화의 "거의 정확한" 28,336이었고, ③과 ④가 02화와 03화의 2,500과 10,000이었다. ⑥은 08화의 50배였다. ③과 ④는 손계산이라기보다 규칙이다. 통계가 없는 식에 오라클이 쓰는 고정값이다.
[H] [H2] — 09화가 넘긴 것
| 기대 | |
|---|---|
| 포스트잇 계획 | INDEX RANGE SCAN DESCENDING E-Rows 12 · TABLE ACCESS BY INDEX ROWID E-Rows 999K · A-Rows 11 · Cost 15 |
ROWNUM을 뗀 카디널리티 | 999,990. 999K는 멈추기 전의 숫자다 |
| 934 / 935페이지 | 인덱스 9,376 / 정렬 9,378 (IO 9,346 = FULL 1,606 + 정렬 7,740) |
09화 기록 정정. 09화에 "Cost에 정렬 CPU가 들어 있다"고 적었다. 935페이지 정렬 쪽의 비용은 대부분 IO다. 10053으로 직접 보려면 STEP 6을 한다.
[I] — 추적 SOP를 몸에 익힌다
출력의 RATIO 열에서 10 이상인 줄을 찾는다. 오늘은 [H]의 TABLE ACCESS BY INDEX ROWID T_ORD_BIG 하나(90,908배)뿐인데, 그 줄은 COUNT STOPKEY 아래라 대조 대상이 아니다. SOP의 예외를 직접 확인하는 장면이다. → 2_파고들기.md §1
STEP 5 — 01화의 16배를 고친다 · 히스토그램 4종 (15분) ★
bash
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 03_히스토그램_4종.sqlpowershell
Get-Content 03_히스토그램_4종.sql -Encoding UTF8 | docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1복사본 T_ORD_ST_C10에서 한다. 약 10초 걸린다. 원본 통계는 건드리지 않는다(맨 끝에서 원본 ORD_ST가 그대로인지 찍어 본다).
| 단계 | 무엇 | 기대 | ☐ |
|---|---|---|---|
| [H0] | 히스토그램 없음 | 네 값 모두 333,333 · CANCEL A-Rows 20,000 | ☐ |
| [H1] | FREQUENCY (SIZE 254) | 끝점 20,000 / 920,000 / 1,000,000 · 20,000 · 80,000 · 900,000 · 없는 값 10,000 | ☐ |
| [H2] | HYBRID (SIZE 2) | CANCEL 1 · PENDING 81,633 · COMPLETE 333,333 | ☐ |
| [H3] | HEIGHT BALANCED (SIZE 2 + 100%) | 전부 333,333 | ☐ |
| [J] | 6월 AND CANCEL (실제 549) | 히스토그램 없이 9,445 · 있으면 567 | ☐ |
| [K] | CUS_000001 + CANCEL (실제 500) | 없이 167 · FREQUENCY 10 · 2회차 500 (statistics feedback) · 새 문장은 다시 10 | ☐ |
| [L] | 컬럼 그룹 | L1 500 · L2 10 (무시) · L3 500 / 없는 조합 250 | ☐ |
| [M] | 표현식 통계 TO_CHAR(ORD_DT,'YYYYMM') | 10,000 → 27,778 (실제 27,390) | ☐ |
📌 [H2]와 [K1] — 히스토그램이 '오히려' 틀리는 두 자리
버킷이 모자랄 때(
SIZE 2→ HYBRID → CANCEL 1)와 상관된 열과 함께 쓸 때(167 → 10)다. 그래서 정책서는SIZE AUTO에 맡기지 않고, 열을 지정하고, 리스크 칸에 상관 조합을 적는다.
📌 [L2]를 놓치면 안 된다
컬럼 그룹을 만들었는데
E-Rows가 그대로 10이다. 그룹을 이루는 열에 히스토그램이 있으면 그룹에도 히스토그램이 있어야 그룹이 쓰인다(실측).
STEP 6 — (선택) 검산 · 지시문 · 일반 INSERT 그대로
① 10053 — 손계산을 트레이스로 검산 (5분)
bash
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 91_10053_비용검산.sql
# 찍힌 경로로:
MSYS_NO_PATHCONV=1 docker exec ora-hanorder sh -c 'grep -n -A3 "Card: Original" <경로>'
MSYS_NO_PATHCONV=1 docker exec ora-hanorder sh -c 'grep -n -A6 "SORT ressource" <경로>'
MSYS_NO_PATHCONV=1 docker exec ora-hanorder sh -c 'grep -n "First K Rows" <경로>'| 트레이스 | 찾을 줄 | 기대 |
|---|---|---|
| 9A (Q1 6월) | Card: Original | Computed: 28335.5… |
| 9B (CANCEL) | Card: Original · Histogram | 히스토그램 없을 때: NDV: 3 … Density: 0.333333 · Rounded: 333333 (Histogram 줄 없음) / STEP 8 뒤: Histogram: Freq #Bkts: 3 · Estimated selectivity: 0.020000 , endpoint value predicate · Rounded: 20000 |
| 9C (935페이지) | SORT ressource 아래 | Blocks to Sort: 5020 · Total IO sort cost: 7740 · Total Temp space used: 40166000 |
| 9C | First K Rows | K = 9350.00, N = 1000000.00 |
② SQL Plan Directive — SYS로 (5분)
bash
MSYS_NO_PATHCONV=1 docker exec -i ora-hanorder sqlplus -s / as sysdba < 92_SPD_SYSDBA.sqlpowershell
Get-Content 92_SPD_SYSDBA.sql -Encoding UTF8 | docker exec -i ora-hanorder sqlplus -s "/ as sysdba"| 단계 | 기대 |
|---|---|
| [2] 같은 문장 두 번 | 2회차 E-Rows 500 · statistics feedback used |
| [3] FLUSH 후 목록 | 복사본의 테이블 · CUS_ID · ORD_ST에 DYNAMIC_SAMPLING · USABLE · SINGLE TABLE CARDINALITY MISESTIMATE |
| [4] 기본 설정 (FALSE) | E-Rows 10. 지시문이 있어도 안 쓴다 |
| [5] 세션만 TRUE | Note 1 Sql Plan Directive used · dynamic sampling (level=AUTO) · E-Rows 500 (표본이라 흔들릴 수 있다. 다른 날은 537 · 555) |
| [6] 복사본 삭제 | 그 지시문 0 |
sqlp로
DBMS_SPD.FLUSH_SQL_PLAN_DIRECTIVE를 호출하면 ORA-38171(권한 없음, 실측)이 난다. 그래서 이 단계만 SYS로 한다. 시스템 파라미터는 바꾸지 않고ALTER SESSION만 쓴다.
③ 일반 INSERT 그대로 (약 9분 · 다른 작업과 겹치면 20분 넘게)
bash
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 90_적재_일반INSERT.sql| 단계 | 기대 (실측) |
|---|---|
| [2] 일반 INSERT 100만 행 | 9분 13초 (다른 실습과 겹친 날 22분 46초) · 적재 뒤 NUM_ROWS 0 · STALE_STATS YES · 인덱스 통계 0 |
| [3] Q1 | E-Rows 1 · IX_C10_DT + 테이블 · 27,899~27,900 |
| [3] Q2 | E-Rows 1 · NESTED LOOPS · 1,060K~1,062K (FULL 줄 7,497~7,525. 일반 INSERT로 쌓은 테이블이 더 크다) |
계획은 STEP 2의 지름길과 같다. 지름길로도 운영과 같은 통계 상태가 만들어진다는 것을 확인하는 단계다.
STALE_STATS = YES인데 통계는 0 그대로다. '오래됐다'는 표시는 누군가 통계를 모으기 전까지 아무것도 바꾸지 않는다.
STEP 7 — 본사의 문자 날짜 · 확장 통계 · TOP-FREQUENCY (10분)
★ 컨테이너가 다르다.
bash
docker exec -i ora-dist sqlplus -s pup/pup@localhost/FREEPDB1 < 04_본사_문자날짜.sqlpowershell
Get-Content 04_본사_문자날짜.sql -Encoding UTF8 | docker exec -i ora-dist sqlplus -s pup/pup@localhost/FREEPDB103화를 마친 상태에서 시작한다. 원본 CF10과 CF11의 통계는 바꾸지 않는다. 복사본 CF10_C10과 CF11_C10에서 실험하고 지운다. 맨 앞과 맨 끝에 원본 통계를 찍어 같은지 본다. 약 15초 걸린다.
| 단계 | 무엇 | 기대 | ☐ |
|---|---|---|---|
| [A] | 6월 (실제 20,953) | 손계산 SEL_GE 0.057974 · SEL_LT 0.952381 → 5,177 · BETWEEN 2,673 | ☐ |
| [B] | SLE_DT HYBRID | 6월 2만 안팎 (표본이라 실행마다 다르다. 5회 실측 19,630 ~ 21,780) · 하루 538 ~ 812 (실제 706) | ☐ |
| [C] | 표현식 통계 TO_DATE(SLE_DT,…) | 1,250 → 21,261 | ☐ |
| [D] | 매장 · 직원 / 날짜 · 요일 | 20,000 → 100,000 (실제 100,167) · 98 → 685 (실제 648) · 없는 조합 685 (실제 0) | ☐ |
| [T] | SLE_UPRC SIZE 5 | TOP-FREQUENCY · 2,000과 12,000이 E-Rows 1 · 나머지 22,378 · SIZE 3 → HYBRID · SIZE 254 → FREQUENCY | ☐ |
📌 [A] — 03화의 "글자를 숫자로 인코딩"을 손으로 계산한다
'20230601'은 수직선 위의 20230601이라는 점처럼 계산됐다. 그 수직선에서 한 달은 100칸, 해가 바뀌는 하루는 8,870칸이다. 04화 버거원에서 하루를 넘기자E-Rows가 뛴 것도 이 때문이다.
STEP 8 — 정책서 1호를 원본에 건다 (5분) ★ 이 화의 마지막 스크립트
bash
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 05_정책_적용.sqlpowershell
Get-Content 05_정책_적용.sql -Encoding UTF8 | docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1맨 앞에서 09화 종료 상태(SIZE 1)로 돌린 뒤 정책을 건다. 몇 번을 돌려도 같은 상태로 끝난다. 약 10초 걸린다.
| 단계 | 기대 | ☐ |
|---|---|---|
| [A0] 정책 전 01화 쿼리 | E-Rows 333K · A-Rows 20000 · 5,826 | ☐ |
| [B] 선호도 | FOR ALL COLUMNS SIZE 1 FOR COLUMNS SIZE 254 ORD_ST · method_opt 없이 모아도 그대로 | ☐ |
| [C0] 정책 후 | ORD_ST FREQUENCY (버킷 3) · E-Rows 20000 = A-Rows · 계획 · Buffers 그대로 | ☐ |
| 전후 대조표 | 01 CANCEL · Q1 · Q2 · Q3 · Q4의 PHV가 A와 C에서 같다 · Buffers 같다 (Q3 840/839는 FULL의 1블록 흔들림) | ☐ |
| [D] 종료 상태 | 선호도 ① · ORD_ST만 FREQUENCY ② · 인덱스 3개 · 확장 통계 0 · 복사본 0 · 다른 15개 테이블 히스토그램 0 ③ | ☐ |
⚠️ 이 스크립트를 마지막으로 돌린 상태로 10화를 끝낸다
01~09화의 실습 스크립트와
00_자료준비/.../04_상태복구.sql은T_ORD_BIG을method_opt=>'FOR ALL COLUMNS SIZE 1'로 명시해서 다시 모은다. 명시한 값이 선호도보다 우선하므로 히스토그램이 사라진다. 그것들을 돌렸다면 이 스크립트를 다시 돌린다. 11화는 이 상태(ORD_STFREQUENCY)에서 시작한다.
「통계 수집 정책서」 1호 · 2호 ★ 새 표준 문서
양식은 통계수집_정책서.md를 따른다. 작성 예가 양식 안에 있으니 보기 전에 먼저 써 본다.
1호 (T_ORD_BIG) : 1 설정 · 2 근거 · 3 영향 분석 · 4 리스크 · 5 적용하지 않은 것 · 6 되돌리는 법 · 7 재검토 조건
2호 (재적재 배치) : 수집 시점 · 대안(APPEND) · 응급(RESTORE) · 기각(LOCK) · 재검토 조건★ 1호의 4번 「리스크」를 비우면 이 화는 미완성이다
오늘 오후의 10을 적는다.
CUS_ID와 함께 쓰면 167 → 10(실제 500)이다. 그리고 "ORD_ST를 바인드 변수로 받는 SQL이 생기면 → 11화"라고 적는다. "이 히스토그램은 리터럴일 때의 이야기다"라는 문장을 지금 적어 두지 않으면, 다음 주에 그 이유를 처음부터 다시 찾게 된다.
STEP 9 — T1 리포트 8호 ★ 오늘의 산출물
양식은 T1_단건SQL_튜닝리포트.md를 따른다. 이 교재에서 처음으로 리비전이 없는 리포트다.
┌──────────────────────────────────────────────────────────────────┐
│ T1 단건 SQL 튜닝 리포트 — 8호 │
├──────────────────────────────────────────────────────────────────┤
│ 1. 대상 : Q1 (rev03) · Q2 (rev01) — 리비전 변화 없음 │
│ 진짜 대상: T_ORD_BIG 의 통계 · 재적재 배치 │
│ 2. 측정 환경 : ora-hanorder · 26ai Free 23.26.2.0.0 │
│ 통계 선호도 · 히스토그램 · 마지막 수집 시각 ★ 적는다│
│ 3. 부하 조건 : 단일 세션 · 1회 · 복사본 T_ORD_BIG_C10 재현 │
│ (재적재는 APPEND+NO_GATHER 로 같은 통계 상태 재현) │
│ 4. 현상 : Q2 계획 전문 · E-Rows 1 vs A-Rows 1,000,000 ______ │
│ 5. 원인 : ______________________________________________ │
│ [성능고도화 II권 Ch3 03(2)·08] [tgsql Optimizer Statistics Concepts] │
│ 6. 개선안 : 응급 ____________ 근본 ____________ 정책서 1·2호 │
│ 기각 ______________________________________ │
│ 7. 효과 : Q2 Buffers ________ → ________ (복사본) │
│ 01화 CANCEL E-Rows 333,333 → 20,000 │
│ 8. 정합성 : ★ 정합성 회귀 체크리스트 A~H │
│ 9. 환경 의존성: ______________________________________________ │
│ 10. 리스크 : ______________________________________________ │
│ 11. 롤백 절차: ______________________________________________ │
│ 12. 재측정 : ______________________________________________ │
└──────────────────────────────────────────────────────────────────┘6번 「기각한 대안」 — 오늘은 넷이다
① Q2에
USE_HASH힌트를 박는다. 통계가 0이어도 해시 조인이 된다(STEP 2[E3]). 그러나 06화에서 지운 힌트를 다시 박는 일이다. 통계가 틀린 것을 SQL로 덮으면, 그 테이블을 읽는 다른 SQL은 여전히 틀린다(오늘 Q1도 같이 무너졌다). ② 통계LOCK. 자동 수집이 빈 테이블 통계로 덮어쓰지 못하게 막는다. 그러나 배치의 수집에도force가 필요해지고, 재적재 크기가 바뀌는 날 낡은 통계가 굳는다. ③SIZE AUTO. 히스토그램을 오라클에 맡긴다. 쿼리 이력에 따라 열이 정해지고, 버킷이 모자라면 오히려 틀린다(SIZE 2에서 CANCEL 1). ④(CUS_ID, ORD_ST)컬럼 그룹을 지금 건다. 이 그룹을 쓰는 SQL이 없다. 걸려면 그룹에도 히스토그램이 필요하다(STEP 5[L]). 쓰는 SQL이 생길 때 잰다.
8번 「정합성」 — 체크리스트 A~H
양식은 정합성_회귀_체크리스트.md를 따른다.
▶ 채운 예 (쓰고 나서 대조)
| # | 항목 | 대상 / 방법 | 결과 | 판정 |
|---|---|---|---|---|
| A | 기준 | 원장 = 연습 DB 원본 T_ORD_BIG의 Q2 (통계 정상) · Q1 = 27,390건 / 6,982,065,200 | — | 정함 |
| B | 건수 · 합계 · MINUS 양방향 · 중복 키 | 오늘(통계 0)의 Q2 vs 원장 (00 [E4]) · 키 CUS_GD | 3 = 3 · 255,398,623,700 = 255,398,623,700 · 0 / 0 · 키 중복 0 (3행 = 등급 3종) | 통과 |
| C | 잘라 내기 전 전체 | Q2는 잘라 내지 않는다(3행 전체) · Q4 rev02는 계획 해시 변화 없음 (09화 전체 순회 유효) | — | 해당 없음 (잘라 내는 SQL의 계획이 안 바뀜) |
| D | 조인마다 1 쪽 | M_CUS PK CUS_ID: NL과 해시 모두 A-Rows 1000K (불어나지 않음) | 1,000,000 = 1,000,000 | 통과 |
| E | 경계 데이터 | 통계의 경계: 빈 테이블 · 통계 없음 · 1만 행 · 최댓값 밖 (복사본) | 계획은 바뀌어도 답(3행 · 27,390건)은 같음 | 통과 |
| F | 계획 해시와 무관하게 대조 | NL (3595456839)과 해시 (124978868): 계획이 달라도 답으로 대조 (B) | (B) | 통과 |
| G | 변환을 근거로 쓰지 않음 | — | — | 해당 없음 (변환 없음) |
| H | 영향 분석 | 정책서 1호(ORD_ST 히스토그램) → T_ORD_BIG을 읽는 조각 전부 (05 전후 대조표) | 01 · Q1 · Q2 · Q3 · Q4 계획 해시 전부 변화 없음 · Buffers 같음 | 통과 |
9번 「환경 의존성」 — "무엇을 재 볼지"까지 (09화에서 배운 것)
- 23ai의 온라인 통계 수집은 빈 세그먼트 + direct path에서만 돈다 → 재적재 방식이 바뀌면
NOTES = STATS_ON_LOAD가 찍히는지 재 본다 optimizer_adaptive_statistics = FALSE(기본): SQL Plan Directive는 쌓이기만 한다 → 이 값을 바꾸는 패치나 업그레이드 때 6조각의 계획 해시를 다시 잰다- 자동 수집 창(토·일 06:00 20시간 · 평일 22:00 4시간)과
STALE_PERCENT10 → 재적재 시각이 바뀌면LAST_ANALYZED가 적재 완료 뒤인지 재 본다
11번 「롤백 절차」
sql
-- 정책서 1호 되돌리기 (히스토그램 제거)
EXEC DBMS_STATS.SET_TABLE_PREFS(USER,'T_ORD_BIG','METHOD_OPT','FOR ALL COLUMNS SIZE 1');
EXEC DBMS_STATS.GATHER_TABLE_STATS(USER,'T_ORD_BIG',cascade=>TRUE,no_invalidate=>FALSE);
-- 확인: HISTOGRAM 전부 NONE
SELECT column_name, histogram FROM user_tab_col_statistics WHERE table_name = 'T_ORD_BIG';
-- 응급 (재적재 뒤 통계가 틀렸을 때): TRUNCATE 직전 시각으로
EXEC DBMS_STATS.RESTORE_TABLE_STATS(USER,'T_ORD_BIG',as_of_timestamp=>TIMESTAMP '<TRUNCATE 직전>',no_invalidate=>FALSE);STEP 10 — 서술 답안 (400~600자)
박팀장에게 보낼 회신을 쓴다. 아침 메시지는 "어제는 0.1초, 오늘은 30초. 배포는 없었습니다."였다.
4단계 프레임(현상/진단 → 원인 → 개선안 → 근거/효과)은 01화 3_실습과_기록.md와 같다.
▶ 선배의 첨삭 포인트 (쓰고 나서)
① "통계가 깨졌습니다"는 틀린 보고다. 통계는 깨지지 않았다. 빈 테이블을 정확하게 모은 통계였다. 틀린 것은 모은 시각이다. 원인은 사람이 고칠 수 있는 단위(배치의 단계, 적재 시각)로 쓴다.
② 오과장을 범인으로 쓰지 않는다. "자동 수집이 매일 돈다"는 말은 사실이었다. 빠진 것은 강도현의 주석에 있던 "언제"였다. 회신에는 합의한 재발 방지책(정책서 2호)을 쓴다.
③ 박팀장이 알아듣는 말로 쓴다. "Buffers 5,868 → 1,026,852" 대신 "한 번 조회에 읽는 양이 175배가 됐습니다"라고 쓴다.
④ '고쳤다'와 '재발하지 않는다'를 구분한다. 오전의 되돌리기는 응급 처치다. 오늘 밤 재적재가 또 돈다. 재발 방지는 배치 단계 복원이고, 그것이 실제로 도는지 다음 주 월요일 아침에 LAST_ANALYZED로 재 보겠다는 것까지 쓴다.
STEP 11 — 파인만 자가설명
절차는 01화와 같다. 1단 백지 설명 → 2단 막힘 태그(최소 1개) → 3단 원천 복귀 → 4단 재설명 순서로 진행한다.
오늘 자주 나오는 태그 세 가지
| 태그 | 전형적인 문장 |
|---|---|
#수치 | "28,336을 손으로 계산하다가 1/NDV를 왜 더하는지에서 막혔다" → 2_파고들기.md §2 |
#인과 | "히스토그램을 넣었는데 왜 더 틀리는지 설명하다가 막혔다" → §8 |
#용어 | "HYBRID와 HEIGHT BALANCED의 차이를 '끝점 반복 횟수'로 말하지 못하겠다" → §4 |
4단 되묻기 3문 (오늘 버전)
- 조건 반전: "재적재가 일반 INSERT가 아니라
APPEND였다면 월요일 아침은 어떻게 됐을까? 23ai가 아니라 11g였다면?" - 반례 요구: "히스토그램을 넣었더니
E-Rows가 더 틀려지는 경우를 두 가지 든다면?" (힌트: 버킷 · 상관) - 측정 지목: "'자동 수집이 적재 도중에 돌았다'를 운영에서 증명하려면 무엇을 봐야 하나?" (힌트: 통계 이력의 시각과 배치 로그의 COMMIT 시각)
태그는 _막힘로그.md로 옮긴다.
완료 체크리스트
- [ ] STEP 0: Q1 rev03과 Q2 rev01을 열어 보고, 고칠 게 없다는 것을 확인했다
- [ ] STEP 1: 보기 전에 예측 4개를 적었다
- [ ] STEP 2:
[E2]에서E-Rows1과 1,026,852를,[G]에서 되돌린 뒤 해시로 돌아온 것을 봤다 - [ ] STEP 3:
STATS_ON_LOAD와dynamic sampling을 봤고, 문턱(2,000 ~ 2,500)을 확인했다 - [ ] STEP 4: 손계산을 먼저 쓰고 대조했다. 09화의 999K가 오추정이 아니라는 것을 확인했다
- [ ] STEP 5:
[H1]20,000,[H2]CANCEL 1,[K1]10 vs 500,[L2]그룹 무시를 봤다 - [ ] STEP 7: 본사 6월 5,177을 수직선으로 손계산했다
- [ ] STEP 8:
05_정책_적용.sql을 마지막으로 돌렸고, 정책서 1호와 2호를 썼다(1호 4번 리스크 칸 포함) - [ ] STEP 9: T1 8호를 썼고, 기각한 대안 넷과 정합성 체크리스트 A~H를 채웠다
- [ ] STEP 10: 박팀장 회신을 문장으로 풀어 썼다
- [ ] STEP 11: 막힘 태그를 최소 1개
_막힘로그.md에 옮겼다
완결성 게이트: 정책서 1호의 4번(리스크)과 7번(재검토 조건), T1 8호의 6번(기각한 대안)이 비어 있으면 이 화는 미완성이다. 이 화의 진짜 산출물은 20,000이라는 숫자가 아니라, 옵티마이저가 보는 숫자를 누가, 언제, 어떻게 만드는지 문서로 남긴 것이다.
다음 화로 넘기는 상태
| 무엇 | 값 |
|---|---|
T_ORD_BIG 통계 선호도 | FOR ALL COLUMNS SIZE 1 FOR COLUMNS SIZE 254 ORD_ST |
T_ORD_BIG.ORD_ST | FREQUENCY 히스토그램 (버킷 3 · 끝점 20,000 / 920,000 / 1,000,000). 나머지 열은 NONE |
| ★ 확인 | 05_정책_적용.sql을 마지막으로 돌린 상태. 01~09화 스크립트나 04_상태복구.sql을 돌렸으면 다시 돌린다 |
T_ORD_BIG 인덱스 | 3개 그대로 (IX_ORD_BIG_DT, IX_ORD_BIG_DT_AMT, IX_ORD_BIG_DT_SEQ) |
| 리비전 | 변화 없음: Q1 rev03 · Q2 rev01 · Q3 rev01 · Q4 rev02 |
| 복사본 | T_ORD_BIG_C10 · T_ORD_ST_C10 · CF10_C10 · CF11_C10. 전부 지웠다 |
ora-dist | 변화 없음: CF10 · CF11 통계 그대로 (04의 맨 앞 · 맨 끝 대조) |
| 열어 둔 질문 | ORD_ST를 바인드 변수로 받으면 → 11화 · 재적재 시간과 인덱스 3개 → 14화 |
다음 → 11화 「같은 SQL이 수만 개」: 오늘 넣은 히스토그램은 'CANCEL'이라고 글자로 박혀 있을 때의 이야기였다. 화면이 그 값을 바인드 변수로 넘기면, 오라클은 무엇을 보고 계획을 세울까.