Skip to content

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,390SQL을 고친다(02·03화) → 못 고치면 표현식 통계 (§8)
COUNT STOPKEY 아래그 줄만 E-Rows가 크다포스트잇 999K / 11대조하지 않는다 (§11)
바인드 변수같은 SQL인데 값마다 맞는 계획이 다르다—11화

2. 선택도와 카디널리티 — 손계산 공식 ​

카디널리티(E-Rows) = NUM_ROWS × 선택도. 선택도는 "조건을 통과하는 비율"이다.

단일 조건 (히스토그램 없음) ​

조건선택도T_ORD_BIG 손계산실측 E-RowsA-Rows
col = 값1 / NDV1,000,000 / 1,095 = 913.2 (ORD_DT)913913
col = 값 (종류 3)1 / NDV1,000,000 / 3 (ORD_ST = 'CANCEL')333,33320,000
col >= a AND col < b(b − a) / (최댓값 − 최솟값) + 1/NDV (닫힌 끝 하나)1,000,000 × (30/1,094 + 1/1,095) = 28,335.528,33627,390
col BETWEEN a AND b(b − a) / (최댓값 − 최솟값) + 2/NDV (닫힌 끝 둘)———
col = 없는 값 (히스토그램 없음)1 / NDV 그대로ORD_ST = 'CLOSED'333,3330

닫힌 끝 하나에 1/NDV. 02화 rev01의 E-Rows 28,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,00003화 TO_CHAR(ORD_DT,'YYYYMM') = :p_ym
f(col) >= 값5%50,000—
f(col) BETWEEN a AND b (양쪽 범위)5% × 5% = 0.25%2,50002화 rev00

조건이 둘 이상 — AND는 곱, OR는 합에서 겹침을 뺀다 ​

결합식실측
A AND Bs(A) × s(B), 둘이 독립이라고 가정6월 AND CANCEL: 28,336 × 1/3 = 9,445 (실제 549)
A OR Bs(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-RowsA-Rows
Q2 T_ORD_BIG ⋈ M_CUS (CUS_ID)1,000,000 × 2,000 / MAX(2,000, 2,000)1,000,0001,000,000
Q3 T_ORD_DET ⋈ T_ITM_EVL (ITM_ID)200,000 × 5,000 / MAX(100, 100)10,000,00010,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 93

02화 §5의 식에서 세 번째 항이 빠진 형태다. 커버링이라 테이블을 읽지 않기 때문이다.


3. 통계는 어디에 있나 ​

통계는 하나가 아니다. 테이블, 열, 인덱스 통계가 따로 저장되고 따로 바뀐다.

통계무엇뷰10화에서 본 것
테이블NUM_ROWS · BLOCKS · AVG_ROW_LEN · STALE_STATS · USER_STATSUSER_TAB_STATISTICS재적재 뒤 NUM_ROWS 0 · BLOCKS 0 (실제 100만)
열NUM_DISTINCT · DENSITY · NUM_NULLS · LOW/HIGH_VALUE · HISTOGRAM · NOTESUSER_TAB_COL_STATISTICSNOTES = STATS_ON_LOAD (온라인 수집)
히스토그램끝점 · 끝점 반복 횟수USER_TAB_HISTOGRAMS · DBA_TAB_HISTOGRAMSORD_ST 끝점 20,000 / 920,000 / 1,000,000
인덱스BLEVEL · LEAF_BLOCKS · CLUSTERING_FACTOR · NUM_ROWSUSER_IND_STATISTICS빈 테이블 통계일 때 인덱스도 전부 0
확장 통계컬럼 그룹 · 표현식 (숨은 가상 열)USER_STAT_EXTENSIONSSYS_STU… 이름의 열
변경 추적마지막 수집 이후 INSERT · DELETE · TRUNCATEUSER_TAB_MODIFICATIONSTRUNCATE 뒤 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), 표본 방식으로 정해진다.

종류만들어지는 조건기억하는 것식 (등치 조건)
FREQUENCYNDV ≤ n값마다 정확한 건수 (끝점 = 누적 건수)그 값의 건수. 없는 값은 가장 적은 값의 절반
TOP-FREQUENCYNDV > n · 표본 AUTO_SAMPLE_SIZE · 상위 n개(최솟값 · 최댓값을 억지로 포함)가 전체의 (1 − 1/n) 이상상위 값의 정확한 건수버킷에 든 값: 그 건수. 나머지: (전체 − 버킷 합) / (NDV − n)
HYBRIDNDV > 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 1NONE333,333333,333333,333333,333
SIZE 254FREQUENCY (버킷 3)20,00080,000900,00010,000
SIZE 2 (AUTO 표본)HYBRID (버킷 2)181,633333,333333,333
SIZE 2 + estimate_percent 100HEIGHT BALANCED (버킷 2)333,333333,333333,333333,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-FREQUENCYSIZE 254 → FREQUENCY
2,000 (최솟값)12,556112,556
2,500533,248533,248533,248
3,000241,587241,587241,587
3,500187,601187,601187,601
4,000 · 4,500 · 5,000 · 7,000 · 8,000 · 10,00012,426 ~ 24,478전부 22,378각각 정확
12,000 (최댓값)18,789118,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,378

SIZE 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_ROWSQ1 계획 · BuffersQ2 계획 · Buffers실습
어제 (적재 전)1,000,000커버링 · 91HASH · 5,86300 [B]
빈 테이블 통계 + APPEND NO_GATHER…0IX_C10_DT + 테이블 · 27,274NL · 1,026,85200 [E]
빈 테이블 통계 + 일반 INSERT (운영과 같은 방식)0 (STALE_STATS YES)IX_C10_DT + 테이블 · 27,899~27,900NL · 1,060K~1,062K90
빈 테이블 통계 + APPEND (힌트 없음)1,000,000 (STATS_ON_LOAD)커버링 · 82HASH · 5,86401 [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_STATS1,000,000커버링 · 82HASH · 5,86800 [G]
빈 테이블 통계 → 재수집 (1.2~1.4초)1,000,000—HASH · 5,86800 [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_ROWS011001,0002,0002,50010,0001,000,000
Q2 조인NLNLNLNLNLHASHHASHHASH
Cost4412131516281,644

문턱은 2,000과 2,500 사이로, M_CUS의 행수(2,000) 근처다. 주문이 고객보다 적어 보이면 고객을 한 명씩 찾아가고(NL), 많아 보이면 고객 카드함을 통째로 펼친다(해시). 통계가 100배 틀려도(1만 행) 문턱을 넘지 않으면 계획은 그대로다.

재현 조건 — 일반 INSERT가 오래 걸리는 이유 ​

인덱스 3개를 단 채 일반 INSERT 100만 행인덱스 없이 일반 INSERTAPPEND (+ 힌트)
걸린 시간 (연습 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.2913
2024-12-31+1912.4912
2025-01-30+31887.4887
2025-12-30+365608.5609
2026-12-30+730303.9304
2027-12-29+1,094 (= 범위 폭)0 → 11

닫힌 범위가 통째로 최댓값 밖이면(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-Rows913 / 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/3167
ORD_ST FREQUENCY1,000,000 × 1/2,000 × 0.0210 ← 더 틀렸다
+ 같은 문장 2회차 (Statistics Feedback)—500 (그 커서만)
컬럼 그룹 (CUS_ID, ORD_ST) · 히스토그램 없이 · ORD_ST도 없이1,000,000 / 2,000500
컬럼 그룹 (히스토그램 없음) + 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,33628,33627,390
6월 AND CANCEL9,445 (17배)567549

6월(고르게 퍼진 ORD_DT)만 물으면 정확했다. 하지만 치우친 열(ORD_ST) 하나가 곱해지는 순간 틀린다. 게다가 이 데이터에서 CANCEL은 1,095일 중 219일에만 있다. 날짜와 상태도 엮여 있는 것이다.

본사의 상관 (ora-dist · 복사본) ​

조건상관독립 가정컬럼 그룹실제
SHP_NO = 1003 AND REG_EMP_NO = 1003매장마다 등록 직원 1명 (조합 5)20,000100,000100,167
SLE_DT = '20230603' AND SLE_DOW = '7' (토요일)날짜가 요일을 정한다 (조합 730)98685648
SLE_DT = '20230603' AND SLE_DOW = '2' (없는 조합)986850

히스토그램 없는 컬럼 그룹은 "이 조합이 없다"는 사실을 모른다. 조합 수(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,26120,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 FeedbackSQL Plan Directive (SPD)
무엇첫 실행에서 E-Rows와 A-Rows가 크게 어긋나면 그 커서에 '다시 최적화' 표시 → 다음 실행 때 실제 건수로 새 자식 커서어긋남을 테이블 · 열 단위로 적어 두는 지시문
어디에 남나V$SQL.IS_REOPTIMIZABLE = Y → 자식 커서 1 · Note: statistics feedback used for this statementDBA_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계획CostIO_COST
934페이지 (N = 9,340)INDEX FULL SCAN DESCENDING + 테이블9,3769,374 (= 인덱스 34 + 테이블 9,340)
935페이지 (N = 9,350)TABLE ACCESS FULL + SORT ORDER BY STOPKEY9,3789,346 (= FULL 1,606 + 정렬 7,740)
935페이지 인덱스 강제INDEX FULL SCAN DESCENDING + 테이블9,3869,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,673

10053의 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,177685수직선
SLE_DT 히스토그램 SIZE 254 → HYBRID19,630 ~ 21,780 (5회: 19,630 · 21,780 · 21,299 · 20,270 · 21,309) · BETWEEN 18,339 ~ 19,982538 ~ 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 FREQUENCYCANCEL 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 rev01HASH · 5,868~5,869 · 174~196 ms · PHV 2292855265
Q3 rev01839~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_원서와_공식문서_지도.md PDF 페이지 = 책 페이지 − 2 · 성능고도화II_목차색인.md (공개 사이트에는 없음)

성능고도화 II권 Ch3 — 옵티마이저 원리 (주 원천) ​

주제절책 페이지
옵티마이저 · 비용기반 옵티마이저01362 · 364
옵티마이저 행동에 영향을 미치는 요소 · 통계정보02 (5)379
옵티마이저의 한계: 부정확한 통계 · 히스토그램의 한계 · 바인드 변수 사용 시 균등분포 가정 · 결합 선택도 산정의 어려움03 (2)~(5)385 ~ 387
통계정보 I: 테이블 · 인덱스 · 컬럼 · 시스템 통계04392 ~ 397
카디널리티: 선택도 · 카디널리티 · NULL · 조건절 두 개 이상 · 범위검색05403 ~ 413
히스토그램: 유형 · 도수분포 · 높이균형 · 바인드 변수 · 결합선택도06416 ~ 429
비용: I/O 비용모델 · CPU 비용모델07434 · 439
통계정보 II: 전략적인 통계수집 정책 · DBMS_STATS · 컬럼 히스토그램 수집 · 데이터 샘플링 · 캐싱된 커서 Invalidation · Statistics Preference08442 ~ 459

원서가 다루는 히스토그램은 도수분포와 높이균형 두 가지다. TOP-FREQUENCY와 HYBRID는 12c 이후에 생긴 종류라 공식문서로 본다.

오라클 공식문서 ​

주제매뉴얼장 제목
통계의 종류 · 수집 경로 · 온라인 통계 수집 · 동적 통계tgsqlOptimizer Statistics Concepts
히스토그램 4종과 만들어지는 조건tgsqlHistograms
선호도 · 자동 수집 · STALE_PERCENTtgsqlManaging Optimizer Statistics: Basic Topics
확장 통계 (컬럼 그룹 · 표현식) · 통계 복원 · 잠금tgsqlManaging Optimizer Statistics: Advanced Topics
카디널리티 · 선택도 · Statistics Feedback · SQL Plan DirectivetgsqlQuery Optimizer Concepts · Adaptive Query Optimization
DBMS_STATS · DBMS_SPDarplsDBMS_STATS · DBMS_SPD
OPTIMIZER_ADAPTIVE_STATISTICS · OPTIMIZER_DYNAMIC_SAMPLINGrefrnInitialization 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

다음 화가 여기서 가져가는 것 ​

화가져가는 것
11T_ORD_BIG.ORD_ST의 FREQUENCY 히스토그램(정책서 1호). 그 위에서 ORD_ST를 바인드 변수로 받으면 무슨 일이 생기나. 히스토그램은 리터럴일 때의 이야기였다
14인덱스 3개를 단 채 일반 INSERT 100만 행 = 약 10분(log file switch (checkpoint incomplete)). 오과장의 04시와 통계 수집 단계가 부딪친 이유
186조각 원장 재측정 때 통계 상태(선호도 · 히스토그램 · 수집 시각)를 함께 적는다 · 정책서의 재검토 조건이 걸렸는가