테마
10화 · 파고들기
조회용 문서다. 처음 읽을 때는
1_사건.md만으로 충분하다.
| 찾는 것 | 바로 가기 |
|---|---|
| 계획이 이상할 때 어느 통계가 틀렸는지 짚는 순서 | §1 추적 SOP |
| E-Rows 손계산 공식 (등치 · 범위 · 닫힌 끝 · 고정값 · AND/OR · 조인) | §2 손계산 |
| 통계는 어디에 있나 · 어느 뷰를 보나 | §3 뷰 |
| 히스토그램 4종: 언제 생기고, 식을 어떻게 바꾸나 | §4 히스토그램 |
| 통계가 만들어지는 길 (수동 · 선호도 · 자동 수집 · 온라인 · 동적 샘플링 · 이력) | §5 수집 경로 |
| 재적재 사고 재현 · 적재 방식별 결과 · NL로 넘어가는 문턱 | §6 재현 |
| 최댓값 밖 (out-of-range) · STALE 10%의 함정 | §7 out-of-range |
| 상관 컬럼 · 확장 통계 (컬럼 그룹 · 표현식) · 히스토그램과의 짝 규칙 | §8 확장 통계 |
| 10053으로 검산하기 | §9 10053 |
| Statistics Feedback · SQL Plan Directive | §10 피드백 |
| 09화가 넘긴 것: 포스트잇 999K · 935페이지 | §11 09화 |
| 본사 문자 날짜의 수직선 (03·04화가 넘긴 것) | §12 문자 날짜 |
| 「통계 수집 정책서」 | §13 정책서 |
| 측정값 전부 | §14 측정 |
| 원천 좌표 | §15 좌표 |
1. E-Rows vs A-Rows 추적 SOP
계획이 이상하면 "어느 줄의 믿음이 틀렸나"부터 짚는다. 처방은 그다음이다.
| 단계 | 할 일 | 도구 · 판정 |
|---|---|---|
| ① | 실제 계획을 띄운다 | /*+ GATHER_PLAN_STATISTICS */ + DISPLAY_CURSOR(FORMAT=>'ALLSTATS LAST') |
| ② | 줄마다 E-Rows × Starts 값과 A-Rows 값을 나란히 놓는다 | E-Rows는 한 번 시작할 때의 예상이다. NL 안쪽처럼 Starts가 크면 곱해서 비교한다 |
| ③ | 10배 이상 어긋난 가장 안쪽(아래) 줄을 출발점으로 잡는다 | 바깥 줄의 어긋남은 대개 안쪽 줄에서 물려받은 것이다 |
| ④ | 그 줄 테이블의 통계를 본다 | NUM_ROWS · LAST_ANALYZED · STALE_STATS (§3) · 통계 이력 |
| ⑤ | 그 줄의 조건을 손으로 계산한다 (§2) | 손계산 = E-Rows이면 옵티마이저는 통계를 적힌 그대로 쓴 것이다. 틀린 것은 통계 또는 가정 |
| ⑥ | 원인을 하나로 지목한다 (아래 표) | |
| ⑦ | 고친 뒤 같은 표를 다시 뽑고, 그 테이블을 읽는 다른 SQL의 계획 해시도 확인한다 | 정합성 회귀 체크리스트 H |
②~③을 한 번에 뽑는 쿼리(실습/02_손계산.sql [I]):
sql
SELECT p.id, LPAD(' ', p.depth) || p.operation || ' ' || p.options || ' ' || p.object_name AS line,
p.last_starts AS starts, p.cardinality * p.last_starts AS e_total, p.last_output_rows AS a_rows,
ROUND(GREATEST(p.cardinality * p.last_starts, p.last_output_rows, 1)
/ GREATEST(LEAST(p.cardinality * p.last_starts, p.last_output_rows), 1), 1) AS ratio
FROM v$sql_plan_statistics_all p
WHERE p.sql_id = :sql_id AND p.child_number = :child
ORDER BY p.id;⑥ 지목표
| 지목 | 신호 | 10화 실측 예 | 처방 |
|---|---|---|---|
| 통계가 낡았다 · 빈 테이블 통계 | NUM_ROWS ≠ 실제 · E-Rows 1 | 재적재 뒤 Q2 E-Rows 1 / A-Rows 1,000,000 | 재수집 · RESTORE_TABLE_STATS (§5) |
| 분포를 모른다 (치우침) | 값마다 실제가 다른데 E-Rows가 같다 | ORD_ST 세 값 모두 333,333 | 히스토그램 (§4) |
| 최댓값 밖 | 최근 날짜 조건 · E-Rows가 하루치 | 최댓값 다음 달(2025-01) E-Rows 913 / 27,390 | 적재 직후 수집 · STALE_PERCENT (§7) |
| 상관 (독립 가정) | 조건 둘이 곱해져 작아졌다 | CUS_ID + ORD_ST 10 / 500 | 컬럼 그룹 (§8) |
| 가공 · 표현식 | 1% · 5% · 0.25% 고정값 | TO_CHAR(...)='202406' 10,000 / 27,390 | SQL을 고친다(02·03화) → 못 고치면 표현식 통계 (§8) |
COUNT STOPKEY 아래 | 그 줄만 E-Rows가 크다 | 포스트잇 999K / 11 | 대조하지 않는다 (§11) |
| 바인드 변수 | 같은 SQL인데 값마다 맞는 계획이 다르다 | — | 11화 |
2. 선택도와 카디널리티 — 손계산 공식
카디널리티(E-Rows) = NUM_ROWS × 선택도. 선택도는 "조건을 통과하는 비율"이다.
단일 조건 (히스토그램 없음)
| 조건 | 선택도 | T_ORD_BIG 손계산 | 실측 E-Rows | A-Rows |
|---|---|---|---|---|
col = 값 | 1 / NDV | 1,000,000 / 1,095 = 913.2 (ORD_DT) | 913 | 913 |
col = 값 (종류 3) | 1 / NDV | 1,000,000 / 3 (ORD_ST = 'CANCEL') | 333,333 | 20,000 |
col >= a AND col < b | (b − a) / (최댓값 − 최솟값) + 1/NDV (닫힌 끝 하나) | 1,000,000 × (30/1,094 + 1/1,095) = 28,335.5 | 28,336 | 27,390 |
col BETWEEN a AND b | (b − a) / (최댓값 − 최솟값) + 2/NDV (닫힌 끝 둘) | — | — | — |
col = 없는 값 (히스토그램 없음) | 1 / NDV 그대로 | ORD_ST = 'CLOSED' | 333,333 | 0 |
닫힌 끝 하나에 1/NDV. 02화 rev01의
E-Rows28,336은 "거의 정확한" 숫자가 아니라 이 식으로 계산한 값이다.T_ORD_BIG.ORD_DT통계: NDV 1,095 · 최솟값 2022-01-01 · 최댓값 2024-12-30 (폭 1,094일).
컬럼을 가공하면 — 고정값 (02 · 03화의 숫자)
통계가 있는 것은 컬럼이지 TO_CHAR(컬럼)이 아니다. 그래서 정해진 비율을 쓴다.
| 조건 모양 | 고정 선택도 | 실측 E-Rows (100만 행) | 어디서 봤나 |
|---|---|---|---|
f(col) = 값 | 1% | 10,000 | 03화 TO_CHAR(ORD_DT,'YYYYMM') = :p_ym |
f(col) >= 값 | 5% | 50,000 | — |
f(col) BETWEEN a AND b (양쪽 범위) | 5% × 5% = 0.25% | 2,500 | 02화 rev00 |
조건이 둘 이상 — AND는 곱, OR는 합에서 겹침을 뺀다
| 결합 | 식 | 실측 |
|---|---|---|
A AND B | s(A) × s(B), 둘이 독립이라고 가정 | 6월 AND CANCEL: 28,336 × 1/3 = 9,445 (실제 549) |
A OR B | s(A) + s(B) − s(A) × s(B) | 포스트잇 조건: 1,000,000 × (1 − (1/1,095)(1 − 988,783/999,999)) = 999,990 |
| 같은 열의 범위 둘 | s(>=a) + s(<b) − 1 | 본사 6월: 0.057974 + 0.952381 − 1 = 0.010355 (§12) |
AND의 '독립 가정'은 상관 컬럼에서 깨진다 → §8.
조인
조인 카디널리티 = |A| × |B| × 1 / MAX(NDV(A.키), NDV(B.키))| 조인 | 손계산 | 실측 E-Rows | A-Rows |
|---|---|---|---|
Q2 T_ORD_BIG ⋈ M_CUS (CUS_ID) | 1,000,000 × 2,000 / MAX(2,000, 2,000) | 1,000,000 | 1,000,000 |
Q3 T_ORD_DET ⋈ T_ITM_EVL (ITM_ID) | 200,000 × 5,000 / MAX(100, 100) | 10,000,000 | 10,000,000 |
08화의 50배는 계획의
E-Rows칸에 처음부터 적혀 있었다. 옵티마이저는 정확히 셌다. '평가가 상품당 50건'이라는 데이터 때문에 답이 50배가 된 것이다. 01화가 넘긴 질문 "FK가 0개라 조인 결과 건수 추정이 나빠지나?"의 답은, 이 두 조인에서는 아니다. 조인 카디널리티는 FK가 아니라 양쪽 키의 NDV로 계산하고, 두 조인 모두E-Rows=A-Rows였다. (FK가 쓰이는 곳은 조인 제거 같은 변환이다 ← 08화)
비용 — 커버링 인덱스 (Q1 Cost 93)
Cost ≈ BLEVEL + LEAF_BLOCKS × 선택도 (+ CLUSTERING_FACTOR × 선택도 : 테이블에 갈 때만)
= 2 + 3,206 × 0.028335 = 92.8 → 실측 Cost 9302화 §5의 식에서 세 번째 항이 빠진 형태다. 커버링이라 테이블을 읽지 않기 때문이다.
3. 통계는 어디에 있나
통계는 하나가 아니다. 테이블, 열, 인덱스 통계가 따로 저장되고 따로 바뀐다.
| 통계 | 무엇 | 뷰 | 10화에서 본 것 |
|---|---|---|---|
| 테이블 | NUM_ROWS · BLOCKS · AVG_ROW_LEN · STALE_STATS · USER_STATS | USER_TAB_STATISTICS | 재적재 뒤 NUM_ROWS 0 · BLOCKS 0 (실제 100만) |
| 열 | NUM_DISTINCT · DENSITY · NUM_NULLS · LOW/HIGH_VALUE · HISTOGRAM · NOTES | USER_TAB_COL_STATISTICS | NOTES = STATS_ON_LOAD (온라인 수집) |
| 히스토그램 | 끝점 · 끝점 반복 횟수 | USER_TAB_HISTOGRAMS · DBA_TAB_HISTOGRAMS | ORD_ST 끝점 20,000 / 920,000 / 1,000,000 |
| 인덱스 | BLEVEL · LEAF_BLOCKS · CLUSTERING_FACTOR · NUM_ROWS | USER_IND_STATISTICS | 빈 테이블 통계일 때 인덱스도 전부 0 |
| 확장 통계 | 컬럼 그룹 · 표현식 (숨은 가상 열) | USER_STAT_EXTENSIONS | SYS_STU… 이름의 열 |
| 변경 추적 | 마지막 수집 이후 INSERT · DELETE · TRUNCATE | USER_TAB_MODIFICATIONS | TRUNCATE 뒤 TRUNCATED = YES |
| 이력 | 통계가 바뀐 시각 (보존 31일) | USER_TAB_STATS_HISTORY | 재적재 전 수집 · 빈 테이블 수집 두 줄 |
| 선호도 | 테이블별 METHOD_OPT · STALE_PERCENT 등 | DBMS_STATS.GET_PREFS · USER_TAB_STAT_PREFS | 원본 T_ORD_BIG = FOR ALL COLUMNS SIZE 1 FOR COLUMNS SIZE 254 ORD_ST (10화 이후) |
LOW_VALUE와HIGH_VALUE는RAW다. 날짜는DBMS_STATS.CONVERT_RAW_VALUE, 문자는UTL_RAW.CAST_TO_VARCHAR2로 읽는다(실습/02[A]).
4. 히스토그램 4종이 식을 바꾸는 지점
히스토그램은 "있다, 없다"의 문제가 아니라 "무엇을 기억하느냐"의 문제다. 어느 종류가 만들어지는지는 값의 종류(NDV), 버킷 수(n), 표본 방식으로 정해진다.
| 종류 | 만들어지는 조건 | 기억하는 것 | 식 (등치 조건) |
|---|---|---|---|
| FREQUENCY | NDV ≤ n | 값마다 정확한 건수 (끝점 = 누적 건수) | 그 값의 건수. 없는 값은 가장 적은 값의 절반 |
| TOP-FREQUENCY | NDV > n · 표본 AUTO_SAMPLE_SIZE · 상위 n개(최솟값 · 최댓값을 억지로 포함)가 전체의 (1 − 1/n) 이상 | 상위 값의 정확한 건수 | 버킷에 든 값: 그 건수. 나머지: (전체 − 버킷 합) / (NDV − n) |
| HYBRID | NDV > n · AUTO_SAMPLE_SIZE · TOP-FREQUENCY 조건 미달 | 높이 균형 + 끝점의 반복 횟수 | 끝점 값: 반복 횟수 비율. 나머지: 밀도 |
| HEIGHT BALANCED (구형) | NDV > n · 표본을 비율로 지정 (예: estimate_percent => 100) | 같은 높이로 자른 끝점 | 두 칸 이상에 걸친 '인기 값'만 그 칸 수 비율. 나머지: 밀도 |
실측 ① — ORD_ST (값 3종, 100만 행 · 복사본 실습/03)
| 설정 | 종류 | CANCEL (20,000) | PENDING (80,000) | COMPLETE (900,000) | CLOSED (없는 값) |
|---|---|---|---|---|---|
SIZE 1 | NONE | 333,333 | 333,333 | 333,333 | 333,333 |
SIZE 254 | FREQUENCY (버킷 3) | 20,000 | 80,000 | 900,000 | 10,000 |
SIZE 2 (AUTO 표본) | HYBRID (버킷 2) | 1 | 81,633 | 333,333 | 333,333 |
SIZE 2 + estimate_percent 100 | HEIGHT BALANCED (버킷 2) | 333,333 | 333,333 | 333,333 | 333,333 |
FREQUENCY ENDPOINT_NUMBER 20,000 CANCE ← 누적. 0~20,000 이 CANCEL
920,000 COMPL ← 20,000~920,000 이 COMPLETE
1,000,000 PENDI
HYBRID ENDPOINT_NUMBER 1 CANCE 반복 1 ← 첫 칸은 최솟값 '한 건' 만 담는다
980,001 PENDI 반복 80,000 ← PENDING = 80,000 × 1,000,000 / 980,001 = 81,633
HEIGHT BALANCED ENDPOINT_NUMBER 0 CANCE · 1 COMPL · 2 PENDI ← 두 칸에 걸친 값 없음 → 전부 밀도ORD_ST에서는 TOP-FREQUENCY가 만들어지지 않는다.
SIZE 2면 최솟값(CANCEL)과 최댓값(PENDING)을 억지로 넣어야 하는데, 둘을 합쳐도 10%라서 문턱(1 − 1/2 = 50%)에 못 미친다. 그래서 HYBRID로 떨어진다.
실측 ② — 본사 CF11.SLE_UPRC (판매단가 11종, 1,096,708행 · 복사본 실습/04 [T])
| 단가 | 실제 | SIZE 5 → TOP-FREQUENCY | SIZE 254 → FREQUENCY |
|---|---|---|---|
| 2,000 (최솟값) | 12,556 | 1 | 12,556 |
| 2,500 | 533,248 | 533,248 | 533,248 |
| 3,000 | 241,587 | 241,587 | 241,587 |
| 3,500 | 187,601 | 187,601 | 187,601 |
| 4,000 · 4,500 · 5,000 · 7,000 · 8,000 · 10,000 | 12,426 ~ 24,478 | 전부 22,378 | 각각 정확 |
| 12,000 (최댓값) | 18,789 | 1 | 18,789 |
TOP-FREQUENCY 끝점: 1 → 2000 · 533,249 → 2500 · 774,836 → 3000 · 962,437 → 3500 · 962,438 → 12000
2000 과 12000 은 누적 차이가 '1' — 버킷 자리만 차지하고 건수는 1 로 기억된다
나머지 6종 = (1,096,708 − 962,438) / (11 − 5) = 22,378SIZE 3이면 최솟값과 최댓값, 상위 1종을 합쳐 51.5%라 문턱(66.7%)에 못 미치므로 HYBRID가 된다(표본 약 5,500행).
히스토그램을 어느 열에 만들지 누가 정하나
| 설정 | 어느 열에 | 버킷 |
|---|---|---|
FOR ALL COLUMNS SIZE 1 | 없음 | — |
FOR COLUMNS SIZE 254 ORD_ST | 지정한 열 | 254 (값이 적으면 그만큼) |
FOR ALL COLUMNS SIZE AUTO (기본값) | WHERE에 쓰인 적이 있고(COL_USAGE$) 치우친 열 | 오라클이 정한다 |
SIZE AUTO는 쿼리 이력을 보고 히스토그램을 만든다. 01화 무대에서 밤사이 자동 수집이ORD_ST히스토그램을 만들어 333K가 사라진 적이 있다(00_자료준비/04_일상_운용과_문제해결.md). 누가 언제 어떤 쿼리를 돌렸느냐에 따라 계획이 바뀌는 길이 생기는 셈이다. 정책서는SIZE 1을 기본으로 하고 열을 지정한다(§13).
5. 통계가 만들어지는 길
| 길 | 언제 | 무엇을 | 10화 실측 |
|---|---|---|---|
수동 GATHER_TABLE_STATS | 호출할 때 | method_opt를 주면 그대로, 안 주면 선호도대로 | 100만 행 + 인덱스 3개 1.2~1.4초 (복사본) · 원본 1.4~3.3초 |
선호도 SET_TABLE_PREFS | 이후의 모든 수집 (자동 수집 포함) | METHOD_OPT · STALE_PERCENT · ESTIMATE_PERCENT … | 원본 T_ORD_BIG에 정책서 1호 |
자동 통계 수집 (auto optimizer stats collection) | 유지보수 창: 평일 22:00부터 4시간 · 토 · 일 06:00부터 20시간 | STALE인 객체만, 선호도대로 | TRUNCATE 직후 STALE_STATS = YES · LIST STALE에 올라온다 |
| 온라인 통계 수집 (12c+) | 빈 세그먼트에 direct path(APPEND · CTAS)로 적재할 때 | 테이블 · 열 (NOTES = STATS_ON_LOAD) · 이 버전에서는 인덱스 통계도 찍혔다 | APPEND 100만 행 → NUM_ROWS 1,000,000 즉시 · 일반 INSERT는 0 그대로 |
CREATE INDEX | 인덱스를 만들 때 | 그 인덱스 통계 | 일반 INSERT 뒤 인덱스를 새로 만들면 인덱스만 1,000,000 · 테이블은 0 |
| 동적 샘플링 | 통계가 아예 없을 때 파싱 중에 | 표본을 떠서 그 SQL에만 쓴다 | DELETE_TABLE_STATS 뒤 Q2 E-Rows 889,390 · 해시 조인 5,869 (Note: dynamic sampling (level=2)) |
SET_*_STATS | 손으로 | 원하는 값 (USER_STATS = YES) | 문턱 찾기 (§6). 끝에서 다시 모아 USER_STATS = NO 확인 |
RESTORE_TABLE_STATS | 손으로 | 이력 속 과거 시각의 통계 (테이블 · 열 · 인덱스) | TRUNCATE 전으로 되돌려 Q2 5,868 · 이력 보존 31일 |
막는 힌트 · 끄는 설정
| 효과 | |
|---|---|
/*+ NO_GATHER_OPTIMIZER_STATISTICS */ | 그 적재의 온라인 통계 수집을 끈다 (실측: APPEND인데도 NUM_ROWS 0 · 인덱스 통계 0) |
LOCK_TABLE_STATS | 자동 수집과 수동 수집을 모두 막는다(force => TRUE만 통과. 공식문서 기준이며 10화에서는 재지 않았다). 정책서 2호 「적용하지 않은 것」 |
optimizer_adaptive_statistics = FALSE (기본값) | SQL Plan Directive를 계획에 쓰지 않는다 (§10) |
틀린 통계는 없는 통계보다 나쁘다. 통계가 없으면 오라클은 표본을 뜬다(동적 샘플링). 있으면 그 값이 0이든 무엇이든 믿는다.
6. 재적재 사고 — 적재 방식별 실측
복사본 T_ORD_BIG_C10(100만 행 · 인덱스 3개 IX_C10_DT · IX_C10_DT_AMT · IX_C10_DT_SEQ)으로 쟀다. 순서는 늘 같다. 통계 → TRUNCATE → (어떤 통계) → 100만 행 적재 → Q1 · Q2.
| 시나리오 | 적재 직후 통계 NUM_ROWS | Q1 계획 · Buffers | Q2 계획 · Buffers | 실습 |
|---|---|---|---|---|
| 어제 (적재 전) | 1,000,000 | 커버링 · 91 | HASH · 5,863 | 00 [B] |
빈 테이블 통계 + APPEND NO_GATHER… | 0 | IX_C10_DT + 테이블 · 27,274 | NL · 1,026,852 | 00 [E] |
빈 테이블 통계 + 일반 INSERT (운영과 같은 방식) | 0 (STALE_STATS YES) | IX_C10_DT + 테이블 · 27,899~27,900 | NL · 1,060K~1,062K | 90 |
빈 테이블 통계 + APPEND (힌트 없음) | 1,000,000 (STATS_ON_LOAD) | 커버링 · 82 | HASH · 5,864 | 01 [A] |
통계 없음 (DELETE_TABLE_STATS) | (없음) | 커버링 · 82 (E-Rows 22,564) | HASH · 5,869 (E-Rows 889,390) | 01 [B] |
1만 행 통계 + APPEND NO_GATHER… | 10,000 | 커버링 · 82 (E-Rows 283) | HASH · 5,872 (E-Rows 10,000) | 01 [C] |
빈 테이블 통계 → RESTORE_TABLE_STATS | 1,000,000 | 커버링 · 82 | HASH · 5,868 | 00 [G] |
| 빈 테이블 통계 → 재수집 (1.2~1.4초) | 1,000,000 | — | HASH · 5,868 | 00 [H] |
Q1이 82와 91로 갈리는 것은 적재하면서 인덱스가 새로 지어져 리프가 더 촘촘해졌기 때문이다(같은
IX_C10_DT_AMT). 통계 문제가 아니다. 빈 테이블 통계일 때는 인덱스 통계도 전부 0이라 두 인덱스의 Cost가 똑같이 0이 됐고, 옵티마이저는 커버링이 아닌IX_C10_DT를 골랐다.
일반 INSERT 판(90번, 운영 재현): 계획은 지름길(APPEND NO_GATHER…)과 같다. Q1은 IX_C10_DT + 테이블, Q2는 NESTED LOOPS(E-Rows 1)다. Buffers가 조금 더 큰 것은 일반 INSERT로 쌓은 테이블이 더 크기 때문이다(Q2의 FULL 줄 7,497~7,525 vs 5,829). 적재 직후 STALE_STATS는 YES지만 통계는 그대로 0이다. '오래됐다'는 표시와 '다시 모았다'는 서로 다른 일이다.
Q2가 NL로 넘어가는 문턱 (SET_TABLE_STATS로 행수만 바꿔 EXPLAIN PLAN · 01 [D])
통계 NUM_ROWS | 0 | 1 | 100 | 1,000 | 2,000 | 2,500 | 10,000 | 1,000,000 |
|---|---|---|---|---|---|---|---|---|
| Q2 조인 | NL | NL | NL | NL | NL | HASH | HASH | HASH |
| Cost | 4 | 4 | 12 | 13 | 15 | 16 | 28 | 1,644 |
문턱은 2,000과 2,500 사이로, M_CUS의 행수(2,000) 근처다. 주문이 고객보다 적어 보이면 고객을 한 명씩 찾아가고(NL), 많아 보이면 고객 카드함을 통째로 펼친다(해시). 통계가 100배 틀려도(1만 행) 문턱을 넘지 않으면 계획은 그대로다.
재현 조건 — 일반 INSERT가 오래 걸리는 이유
인덱스 3개를 단 채 일반 INSERT 100만 행 | 인덱스 없이 일반 INSERT | APPEND (+ 힌트) | |
|---|---|---|---|
| 걸린 시간 (연습 DB) | 9분 13초 (다른 작업과 겹친 날 22분 46초). log file switch (checkpoint incomplete) 대기 | 0.91초 | 9~36초 (실측 6회) |
연습 DB의 리두 로그는 10MB짜리 두 개다. 인덱스를 단 채 한 행씩 넣으면 인덱스를 갱신하느라 리두와 언두가 쏟아진다. 오과장이 통계 수집을 뺀 이유(04시 마감)와 뿌리가 같고, 그 청구서는 14화에서 읽는다.
7. 최댓값 밖 (out-of-range)
통계의 최댓값보다 뒤의 값을 물으면 오라클은 "그런 값은 거의 없을 것"으로 본다. 최댓값에서 멀수록 더 그렇다.
등치 — 선형으로 줄어든다
E-Rows = (NUM_ROWS / NDV) × ( 1 − (값 − 최댓값) / (최댓값 − 최솟값) ) (최소 1)ORD_DT = | 최댓값에서 | 손계산 | 실측 E-Rows |
|---|---|---|---|
| 2024-12-30 (최댓값) | 0일 | 913.2 | 913 |
| 2024-12-31 | +1 | 912.4 | 912 |
| 2025-01-30 | +31 | 887.4 | 887 |
| 2025-12-30 | +365 | 608.5 | 609 |
| 2026-12-30 | +730 | 303.9 | 304 |
| 2027-12-29 | +1,094 (= 범위 폭) | 0 → 1 | 1 |
닫힌 범위가 통째로 최댓값 밖이면(2025-01-01 <= ORD_DT < 2025-02-01, 2026-01) 913(하루치)으로 고정됐다.
실제로 데이터가 있을 때 (복사본 · 01 [E])
| 값 | |
|---|---|
통계 수집 후 최댓값 다음 달(2025-01) 한 달치 적재 (27,390행 = 2.7%) | STALE_STATS = NO (STALE_PERCENT 10) |
1월 조회 E-Rows / A-Rows | 913 / 27,390 (30배 과소) |
| 1월 등급별 집계 (Q2 모양) | 통계 그대로: IX_C10_DT 범위 + 테이블 → 5,008~5,014 / 재수집 뒤: FULL + 해시 → 6,031~6,046 |
이 쿼리에서는 틀린
E-Rows로 고른 계획이 오히려 블록을 덜 읽었다. 틀린 추정이 늘 나쁜 계획을 고르는 건 아니다. 다만 맞아서가 아니라 운이 좋아서다. 같은 913이 다른 조인 위로 올라가면 결과는 장담할 수 없다.
STALE 10%의 함정
STALE_PERCENT 기본값이 10%라서 100만 행 테이블은 10만 행이 바뀌어야 자동 수집 대상이 된다. 하루 913건이면 약 110일이 걸린다. 그동안 "이번 달"이나 "어제"를 묻는 화면은 최댓값 밖을 묻는 셈이다.
| 처방 | 언제 |
|---|---|
| 적재 직후 그 배치에서 수집 (정책서 P2) | 날짜가 자라는 테이블의 기본 |
SET_TABLE_PREFS(..., 'STALE_PERCENT', ...)로 낮춘다 | 자동 수집에 맡길 때 |
| (참고) 파티션 테이블은 새 파티션 통계를 복사하거나 설정하는 방법이 따로 있다 | 14화 |
8. 상관 컬럼과 확장 통계
독립 가정이 깨지는 곳 — T_ORD_BIG의 CUS_ID와 ORD_ST
데이터를 보면 (CUS_ID, ORD_ST) 조합이 2,000개로 고객 수와 같다. 고객마다 상태가 하나뿐이다(CANCEL 고객 40명 · PENDING 160명 · COMPLETE 1,800명, 고객당 주문 500건).
WHERE CUS_ID = 'CUS_000001' AND ORD_ST = 'CANCEL'(실제 500):
| 통계 | 손계산 | 실측 E-Rows |
|---|---|---|
| 히스토그램 없음 | 1,000,000 × 1/2,000 × 1/3 | 167 |
ORD_ST FREQUENCY | 1,000,000 × 1/2,000 × 0.02 | 10 ← 더 틀렸다 |
| + 같은 문장 2회차 (Statistics Feedback) | — | 500 (그 커서만) |
컬럼 그룹 (CUS_ID, ORD_ST) · 히스토그램 없이 · ORD_ST도 없이 | 1,000,000 / 2,000 | 500 |
컬럼 그룹 (히스토그램 없음) + ORD_ST FREQUENCY | — | 10 ← 그룹이 무시됐다 |
컬럼 그룹 FREQUENCY (SIZE 2048) + ORD_ST FREQUENCY | 그룹의 그 조합 건수 | 500 |
없는 조합 (CUS_000002, CANCEL)(실제 0)은 그룹(히스토그램 없음)에서 500, 그룹 FREQUENCY에서 250(가장 적은 값의 절반)이었다.
규칙(실측): 그룹을 이루는 열에 히스토그램이 있으면, 그룹에도 히스토그램이 있어야 그룹을 쓴다. 교훈: 치우친 열에 히스토그램을 넣으면 그 열 단독 조건은 고쳐지지만, 상관된 다른 열과 함께 쓰는 조건은 더 정밀하게 틀린다. 히스토그램은 열 하나의 분포다. 열과 열 사이의 관계는 모른다.
02화가 넘긴 것 — "E-Rows가 정확해도 틀리는 경우"
| 조건 | 히스토그램 없음 | ORD_ST FREQUENCY | 실제 |
|---|---|---|---|
| 6월 | 28,336 | 28,336 | 27,390 |
| 6월 AND CANCEL | 9,445 (17배) | 567 | 549 |
6월(고르게 퍼진 ORD_DT)만 물으면 정확했다. 하지만 치우친 열(ORD_ST) 하나가 곱해지는 순간 틀린다. 게다가 이 데이터에서 CANCEL은 1,095일 중 219일에만 있다. 날짜와 상태도 엮여 있는 것이다.
본사의 상관 (ora-dist · 복사본)
| 조건 | 상관 | 독립 가정 | 컬럼 그룹 | 실제 |
|---|---|---|---|---|
SHP_NO = 1003 AND REG_EMP_NO = 1003 | 매장마다 등록 직원 1명 (조합 5) | 20,000 | 100,000 | 100,167 |
SLE_DT = '20230603' AND SLE_DOW = '7' (토요일) | 날짜가 요일을 정한다 (조합 730) | 98 | 685 | 648 |
SLE_DT = '20230603' AND SLE_DOW = '2' (없는 조합) | 98 | 685 | 0 |
히스토그램 없는 컬럼 그룹은 "이 조합이 없다"는 사실을 모른다. 조합 수(NDV)로 나눌 뿐이다.
표현식 통계 — 가공을 지울 수 없을 때
| 조건 | 고정값 (통계 없음) | 표현식 통계 후 | 실제 |
|---|---|---|---|
TO_CHAR(ORD_DT,'YYYYMM') = '202406' (T_ORD_BIG 복사본) | 10,000 (1%) | 27,778 (= 100만 / 36개월) | 27,390 |
TO_DATE(SLE_DT,'YYYYMMDD') >= … AND < … (본사 복사본) | 1,250 (0.25%) | 21,261 | 20,953 |
sql
SELECT DBMS_STATS.CREATE_EXTENDED_STATS(USER, 'T', '(TO_CHAR(ORD_DT,''YYYYMM''))') FROM dual; -- 숨은 가상 열을 만든다
SELECT DBMS_STATS.CREATE_EXTENDED_STATS(USER, 'T', '(CUS_ID,ORD_ST)') FROM dual; -- 컬럼 그룹
EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, 'T', ...) -- 만든 뒤 반드시 다시 모은다
EXEC DBMS_STATS.DROP_EXTENDED_STATS(USER, 'T', '(CUS_ID,ORD_ST)') -- 되돌리기표현식 통계는 추정만 고친다. 인덱스는 여전히 못 탄다(FULL). SQL을 고칠 수 있으면 그쪽이 먼저다(03화 rev03). 확장 통계는 테이블에 숨은 열을 하나 더 만드는 DDL이다. 원본에 걸면 정책서 항목이 된다.
9. 10053으로 검산한다
실습/91_10053_비용검산.sql을 쓴다. 트레이스는 하드 파싱 때만 기록되므로 문장마다 시각을 넣는다(08화와 같은 방식).
| 찾을 줄 | 무엇 | 실측 |
|---|---|---|
Estimated selectivity: … | 조건 하나의 선택도 | Q1 6월 >= 6/1 0.194698 · < 7/1 0.833638 (합 − 1 = 0.028336) / 본사 >= '20230601' 0.057974 · < '20230701' 0.952381 |
Card: Original: … Rounded: … Computed: … | 테이블 카디널리티 | Q1 6월 Rounded: 28336 Computed: 28335.545483 · 본사 6월 Rounded: 5177 Computed: 5177.294490 |
Histogram: Freq #Bkts: 3 … | 히스토그램을 썼다는 표시 (정책 적용 후 CANCEL) | Estimated selectivity: 0.020000 , endpoint value predicate · Rounded: 20000. 히스토그램이 없으면 이 줄 대신 NDV: 3 … Density: 0.333333 · Rounded: 333333 (복사본 실측) |
Access Path: … · resc_io: · Best:: | 경로별 비용 | Q1 커버링 resc_io: 93.000000 · 935페이지 인덱스 resc_io: 9384.000000 |
SORT ressource 아래 Initial runs · Merge passes · IO Cost / pass · Total IO sort cost · Total Temp space used | 정렬 비용 | 2 · 1 · 2,720 · 7,740 (= Blocks to Sort 5,020 + 2,720) · 40,166,000 |
First K Rows: K = … , N = … | ROWNUM이 있을 때의 셈 | K = 9350.00, N = 1000000.00 |
꺼내 읽는 법(Git Bash):
bash
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" <경로>'10. Statistics Feedback과 SQL Plan Directive
| Statistics Feedback | SQL Plan Directive (SPD) | |
|---|---|---|
| 무엇 | 첫 실행에서 E-Rows와 A-Rows가 크게 어긋나면 그 커서에 '다시 최적화' 표시 → 다음 실행 때 실제 건수로 새 자식 커서 | 어긋남을 테이블 · 열 단위로 적어 두는 지시문 |
| 어디에 남나 | V$SQL.IS_REOPTIMIZABLE = Y → 자식 커서 1 · Note: statistics feedback used for this statement | DBA_SQL_PLAN_DIRECTIVES · DBA_SQL_PLAN_DIR_OBJECTS (메모리 → 약 15분마다 디스크) |
| 실측 | CUS_000001 + CANCEL: 1회차 10 → 2회차 500. 주석 한 글자 다른 새 SQL은 다시 10 | 이 버전 기본값 optimizer_adaptive_statistics = FALSE에서는 E-Rows 10 (안 쓴다) · 세션만 TRUE로 바꾸면 Note: 1 Sql Plan Directive used · dynamic sampling (level=AUTO) · 500~555 (표본이라 실행마다 다르다. 실측 500 · 500 · 537 · 555) |
| 한계 | 그 커서에만. 커서가 밀려나면 사라진다 | 기본 설정에서는 적어 두기만 한다 |
| 지우면 | — | 그 테이블을 DROP하면 함께 지워졌다 (실측) |
SPD를 바로 보려면
DBMS_SPD.FLUSH_SQL_PLAN_DIRECTIVE가 필요한데, sqlp 계정으로는 ORA-38171(권한 없음)이 난다.실습/92_SPD_SYSDBA.sql은 SYS로 돈다. ⚠️ 원본 테이블에서 '조건이 둘 이상이고 크게 틀리는' 쿼리를 돌리면 지시문이 원본에 남는다(실측: 6월 AND CANCEL을 원본에서 돌린 뒤T_ORD_BIG에ORD_DT와ORD_ST지시문이 생겼다). 기본 설정에서는 계획에 쓰이지 않지만 흔적은 남는다. 그래서 10화 실습은 그런 쿼리를 전부 복사본에서 돌린다.
11. 09화가 넘긴 것 — STOPKEY 아래의 E-Rows와 정렬 비용
① 포스트잇 계획의 E-Rows 999K vs A-Rows 11
xplan
| Id | Operation | Name | Starts | E-Rows | Cost (%CPU)| A-Rows | Buffers |
|* 1 | COUNT STOPKEY | | 1 | | | 11 | 16 |
| 2 | VIEW | | 1 | 12 | 15 (0)| 11 | 16 |
| 3 | TABLE ACCESS BY INDEX ROWID | T_ORD_BIG | 1 | 999K| 15 (0)| 11 | 16 |
|* 4 | INDEX RANGE SCAN DESCENDING | IX_ORD_BIG_DT_SEQ | 1 | 12 | 3 (0)| 11 | 4 || 값 | |
|---|---|
ROWNUM을 뗀 같은 조건의 카디널리티 | 999,990 (EXPLAIN PLAN) = 손계산 1,000,000 × (1 − (1/1,095)(1 − 988,783/999,999)) |
| 몇 건에서 멈출지 | VIEW 줄 E-Rows 12와 Cost 15에 반영 (First K Rows) |
| 테이블 줄의 999K | 멈추기 전의 카디널리티. 오추정이 아니다 |
SOP 예외:
COUNT STOPKEY아래 줄의E-Rows는A-Rows와 대조하지 않는다. 대조는VIEW줄로 하고, 판단은Cost로 한다. 09화rev01계획(힌트 없음)에서는 반대로 인덱스 줄이 1000, 테이블 줄이 1000K였고, 힌트를 주면 인덱스 줄 1000K, 테이블 줄 1000이었다. 어느 줄에 K를 반영하는지조차 일정하지 않다.
② 934페이지까지 인덱스, 935페이지부터 정렬
EXPLAIN PLAN | 계획 | Cost | IO_COST |
|---|---|---|---|
| 934페이지 (N = 9,340) | INDEX FULL SCAN DESCENDING + 테이블 | 9,376 | 9,374 (= 인덱스 34 + 테이블 9,340) |
| 935페이지 (N = 9,350) | TABLE ACCESS FULL + SORT ORDER BY STOPKEY | 9,378 | 9,346 (= FULL 1,606 + 정렬 7,740) |
| 935페이지 인덱스 강제 | INDEX FULL SCAN DESCENDING + 테이블 | 9,386 | 9,384 |
- 인덱스 쪽: CF가 100만이라 N건마다 테이블 한 블록 → IO ≈ 34 + N. N에 비례한다
- 정렬 쪽: 페이지와 무관하게 9,378. 그중 정렬이 7,766(IO 7,740 + CPU 약 26)
- 10053:
Blocks to Sort: 5020 · Total Rows: 1000000 · Area size: 536576 · Total IO sort cost: 7740 · Total Temp space used: 40166000
09화 기록 정정. 09화 §3에 "Cost에는 정렬에 드는 CPU가 들어 있기 때문이다"라고 적었다. 10053으로 보니 정렬 비용의 대부분은 CPU가 아니라 IO다. 옵티마이저는 100만 행 정렬이 TEMP로 약 40MB 넘칠 것으로 셈했다. 실제 Top-N 정렬(
STOPKEY)은 책상 206K 안에서 끝났다(09화 실측). 정렬 비용 계산에 Top-N이 반영되지 않아 정렬 쪽을 비싸게 보고, 그래서Buffers로는 581페이지에서 이미 비싼 인덱스 쪽을 935페이지까지 고른다.
12. 문자 날짜의 수직선 — ora-dist
03화와 04화가 넘긴 질문은 VARCHAR2(8) 날짜의 E-Rows가 왜 틀리느냐였다.
CF10.SLE_DT · 2023년 6월 · 실제 20,953 · NDV 730 · 최솟값 '20220101' · 최댓값 '20231231'.
sel(>= '20230601') = (20231231 − 20230601) / (20231231 − 20220101) + 1/730 = 630/11130 + 0.001370 = 0.057974
sel(< '20230701') = (20230701 − 20220101) / (20231231 − 20220101) = 10600/11130 = 0.952381
둘 다 = 0.057974 + 0.952381 − 1 = 0.010355 → 500,000 × 0.010355 = 5,177
BETWEEN '20230601' AND '20230630' (닫힌 끝 둘) → 2,67310053의 Estimated selectivity 두 값(0.057974 · 0.952381)과 6자리까지 같다. 숫자 모양의 이 문자열은 수직선 위의 점처럼 계산된 것이다.
| 그 수직선 위에서 | 칸 수 |
|---|---|
한 달 ('20230601' → '20230701') | 100 |
해가 바뀌는 하루 ('20221231' → '20230101') | 8,870 |
전체 ('20220101' → '20231231') | 11,130 |
04화 버거원에서
'20121231'에서'20130101'로 하루 넘어가자E-Rows가 102K → 899K로 뛴 것도 같은 수직선 때문이다(04화 §5).
고치는 길 셋 (복사본 실측)
| 방법 | 6월 E-Rows | 하루('20230601', 실제 706) | 비고 |
|---|---|---|---|
그대로 (SIZE 1) | 5,177 | 685 | 수직선 |
SLE_DT 히스토그램 SIZE 254 → HYBRID | 19,630 ~ 21,780 (5회: 19,630 · 21,780 · 21,299 · 20,270 · 21,309) · BETWEEN 18,339 ~ 19,982 | 538 ~ 812 | 표본(약 5,400~5,600행)으로 만들기 때문에 수집할 때마다 다르다 |
TO_DATE(SLE_DT,'YYYYMMDD') 표현식 통계 (그 식으로 묻는 SQL에만) | 21,261 | — | 03화 가상 컬럼 · FBI와 같은 숫자 |
(근본) 컬럼을 DATE로 | — | — | 03화의 결론. 테이블 설계 변경 |
본사 원본에는 걸지 않았다. 03화와 09화의 본사 숫자가 그 통계 위에 있고, 시스템 주인이 따로 있기 때문이다. 정책서 후보로 유통지원팀에 보낸다.
13. 「통계 수집 정책서」
양식 → 40_보고서와_표준/템플릿/통계수집_정책서.md
| 호 | 대상 | 설정 | 근거 (실측) |
|---|---|---|---|
| 1호 | T_ORD_BIG | 선호도 FOR ALL COLUMNS SIZE 1 FOR COLUMNS SIZE 254 ORD_ST → ORD_ST FREQUENCY | CANCEL 333,333 → 20,000 · 6조각 계획 해시 변화 없음 · 리스크: 상관 AND 167 → 10 · 바인드 → 11화 |
| 2호 | T_ORD_BIG 재적재 배치 | 적재 직후 같은 배치에서 수집 · 응급은 RESTORE_TABLE_STATS | 빈 테이블 통계 → Q1 27,274 · Q2 1,026,852 / 복원 82 · 5,868 |
01~09화 스크립트와
00_자료준비/고객사/L1_ora-hanorder/04_상태복구.sql은T_ORD_BIG을SIZE 1로 명시해서 다시 모은다(명시한 값이 선호도보다 우선한다). 그래서 그것들을 돌린 뒤에는실습/05_정책_적용.sql을 다시 돌려야 한다.
14. 이 화의 측정값 전부
모두 ora-hanorder와 ora-dist에서 잰 실측값이다. 환경: Oracle AI Database 26ai Free 23.26.2.0.0 · optimizer_features_enable 23.1.0 · optimizer_mode ALL_ROWS · optimizer_dynamic_sampling 2 · optimizer_adaptive_statistics FALSE · optimizer_real_time_statistics FALSE · db_file_multiblock_read_count 128 · cpu_count 2 · 시스템 통계 noworkload (CPUSPEEDNW 3,500.5 · IOSEEKTIM 10 · IOTFRSPEED 4,096) · 리두 로그 10MB × 2 · 자동 통계 수집 ENABLED · 전역 METHOD_OPT FOR ALL COLUMNS SIZE AUTO · STALE_PERCENT 10 · 통계 이력 보존 31일 · AUTO_STAT_EXTENSIONS OFF.
원본 (ora-hanorder · 읽기만)
| 무엇 | 값 |
|---|---|
| Q1 rev02 (6월) | IX_ORD_BIG_DT_AMT · 91 · E-Rows 28,336 / 27,390 · Cost 93 · PHV 636720714 |
| Q2 rev01 | HASH · 5,868~5,869 · 174~196 ms · PHV 2292855265 |
| Q3 rev01 | 839~840 · PHV 2231864090 |
| Q4 rev02 (포스트잇 2페이지) | 15~16 · PHV 3610158121 |
| 01화 CANCEL (정책 전 → 후) | E-Rows 333K → 20,000 · A-Rows 20,000 · 5,826 · PHV 914281838 그대로 |
| 정책 적용 수집 | 1.4~3.3초 |
| 하루 · 6월 · 고정값 · 조인 · 최댓값 밖 | §2 · §7 |
복사본 (ora-hanorder) → §6 · §4 · §8
ora-dist 복사본 → §4 실측 ② · §8 · §12
15. 원천 좌표
전체 지도 →
00_자료준비/06_원서와_공식문서_지도.mdPDF 페이지 = 책 페이지 − 2 ·성능고도화II_목차색인.md(공개 사이트에는 없음)
성능고도화 II권 Ch3 — 옵티마이저 원리 (주 원천)
| 주제 | 절 | 책 페이지 |
|---|---|---|
| 옵티마이저 · 비용기반 옵티마이저 | 01 | 362 · 364 |
| 옵티마이저 행동에 영향을 미치는 요소 · 통계정보 | 02 (5) | 379 |
| 옵티마이저의 한계: 부정확한 통계 · 히스토그램의 한계 · 바인드 변수 사용 시 균등분포 가정 · 결합 선택도 산정의 어려움 | 03 (2)~(5) | 385 ~ 387 |
| 통계정보 I: 테이블 · 인덱스 · 컬럼 · 시스템 통계 | 04 | 392 ~ 397 |
| 카디널리티: 선택도 · 카디널리티 · NULL · 조건절 두 개 이상 · 범위검색 | 05 | 403 ~ 413 |
| 히스토그램: 유형 · 도수분포 · 높이균형 · 바인드 변수 · 결합선택도 | 06 | 416 ~ 429 |
| 비용: I/O 비용모델 · CPU 비용모델 | 07 | 434 · 439 |
통계정보 II: 전략적인 통계수집 정책 · DBMS_STATS · 컬럼 히스토그램 수집 · 데이터 샘플링 · 캐싱된 커서 Invalidation · Statistics Preference | 08 | 442 ~ 459 |
원서가 다루는 히스토그램은 도수분포와 높이균형 두 가지다. TOP-FREQUENCY와 HYBRID는 12c 이후에 생긴 종류라 공식문서로 본다.
오라클 공식문서
| 주제 | 매뉴얼 | 장 제목 |
|---|---|---|
| 통계의 종류 · 수집 경로 · 온라인 통계 수집 · 동적 통계 | tgsql | Optimizer Statistics Concepts |
| 히스토그램 4종과 만들어지는 조건 | tgsql | Histograms |
선호도 · 자동 수집 · STALE_PERCENT | tgsql | Managing Optimizer Statistics: Basic Topics |
| 확장 통계 (컬럼 그룹 · 표현식) · 통계 복원 · 잠금 | tgsql | Managing Optimizer Statistics: Advanced Topics |
| 카디널리티 · 선택도 · Statistics Feedback · SQL Plan Directive | tgsql | Query Optimizer Concepts · Adaptive Query Optimization |
DBMS_STATS · DBMS_SPD | arpls | DBMS_STATS · DBMS_SPD |
OPTIMIZER_ADAPTIVE_STATISTICS · OPTIMIZER_DYNAMIC_SAMPLING | refrn | Initialization Parameters |
이 화에서 꼭 확인할 뷰
sql
USER_TAB_STATISTICS -- num_rows · blocks · last_analyzed · stale_stats · user_stats
USER_TAB_COL_STATISTICS -- num_distinct · density · low/high_value · histogram · num_buckets · notes
USER_TAB_HISTOGRAMS -- endpoint_number · endpoint_value · endpoint_repeat_count (DBA_TAB_HISTOGRAMS)
USER_IND_STATISTICS -- 인덱스 통계 (테이블과 따로 바뀐다)
USER_TAB_MODIFICATIONS -- inserts · deletes · truncated
USER_TAB_STATS_HISTORY -- 통계가 바뀐 시각 (RESTORE_TABLE_STATS 의 as_of)
USER_STAT_EXTENSIONS -- 컬럼 그룹 · 표현식 통계
V$SQL -- is_reoptimizable (Statistics Feedback)
DBA_SQL_PLAN_DIRECTIVES / DBA_SQL_PLAN_DIR_OBJECTS다음 화가 여기서 가져가는 것
| 화 | 가져가는 것 |
|---|---|
| 11 | T_ORD_BIG.ORD_ST의 FREQUENCY 히스토그램(정책서 1호). 그 위에서 ORD_ST를 바인드 변수로 받으면 무슨 일이 생기나. 히스토그램은 리터럴일 때의 이야기였다 |
| 14 | 인덱스 3개를 단 채 일반 INSERT 100만 행 = 약 10분(log file switch (checkpoint incomplete)). 오과장의 04시와 통계 수집 단계가 부딪친 이유 |
| 18 | 6조각 원장 재측정 때 통계 상태(선호도 · 히스토그램 · 수집 시각)를 함께 적는다 · 정책서의 재검토 조건이 걸렸는가 |