Skip to content

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_어제와_오늘.sql
powershell
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 · Q291 · 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]오늘의 Q1E-Rows 1 · IX_C10_DT + TABLE ACCESS BY INDEX ROWID · 27,274☐
[E2]오늘의 Q2E-Rows 1 · NESTED LOOPS · M_CUS 쪽 Starts 1000K · 1,026,852☐
[E3]힌트로 해시 고정 (기각할 대안)HASH JOIN · 5,868. E-Rows는 여전히 1☐
[E4]정합성: 오늘 Q2 vs 원본 Q23 = 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_ROWSA_ROWSBUFFERSMS
[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_적재방식과_통계.sql
powershell
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_손계산.sql
powershell
Get-Content 02_손계산.sql -Encoding UTF8 | docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1

원본을 읽기만 한다. 약 5초 걸린다.

▶ 채우고 나서 펼치기
#손계산실측
①1,000,000 / 1,095 = 913.2913
②1,000,000 × (30/1,094 + 1/1,095) = 28,335.5. 닫힌 끝 하나에 1/NDV28,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.5609
⑧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종.sql
powershell
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: OriginalComputed: 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
9CFirst K RowsK = 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.sql
powershell
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] 세션만 TRUENote 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] Q1E-Rows 1 · IX_C10_DT + 테이블 · 27,899~27,900
[3] Q2E-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_본사_문자날짜.sql
powershell
Get-Content 04_본사_문자날짜.sql -Encoding UTF8 | docker exec -i ora-dist sqlplus -s pup/pup@localhost/FREEPDB1

03화를 마친 상태에서 시작한다. 원본 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 HYBRID6월 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 5TOP-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_정책_적용.sql
powershell
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_ST FREQUENCY)에서 시작한다.

「통계 수집 정책서」 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_GD3 = 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_PERCENT 10 → 재적재 시각이 바뀌면 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문 (오늘 버전) ​

  1. 조건 반전: "재적재가 일반 INSERT가 아니라 APPEND였다면 월요일 아침은 어떻게 됐을까? 23ai가 아니라 11g였다면?"
  2. 반례 요구: "히스토그램을 넣었더니 E-Rows가 더 틀려지는 경우를 두 가지 든다면?" (힌트: 버킷 · 상관)
  3. 측정 지목: "'자동 수집이 적재 도중에 돌았다'를 운영에서 증명하려면 무엇을 봐야 하나?" (힌트: 통계 이력의 시각과 배치 로그의 COMMIT 시각)

태그는 _막힘로그.md로 옮긴다.


완료 체크리스트 ​

  • [ ] STEP 0: Q1 rev03과 Q2 rev01을 열어 보고, 고칠 게 없다는 것을 확인했다
  • [ ] STEP 1: 보기 전에 예측 4개를 적었다
  • [ ] STEP 2: [E2]에서 E-Rows 1과 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_STFREQUENCY 히스토그램 (버킷 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'이라고 글자로 박혀 있을 때의 이야기였다. 화면이 그 값을 바인드 변수로 넘기면, 오라클은 무엇을 보고 계획을 세울까.