Skip to content
튜닝 연습옵티마이저와 통계
OPT-022

분기 초마다 무너지는 실적 화면을 두고 세 사람이 서로 다른 통계 처방을 냈다

실전PLSQL한빛유통612 / 700

상황 ​

3월 28일 분기 누적 화면 사고(통계의 최댓값 밖 추정) 뒤에 박팀장이 정책을 정하라고 했다. "T_ORD_BIG처럼 날마다 자라는 테이블은 통계를 어떻게 지킬지, 근거를 붙여서." 주문은 매일 01:00 적재 배치가 전날 주문 약 913건을 일반 INSERT로 싣는다. 3월 31일에 손으로 다시 모은 뒤로 통계는 그대로다. 세 사람이 안을 냈다. 오과장은 적재 배치 끝에서 매일 모으자고 했고, 정하은은 STALE_PERCENT를 10에서 1로 낮춰 자동 수집에 맡기자고 했다. 조민우는 인터넷에서 본 방법이라며 분기 첫날마다 ORD_DT의 최댓값을 그 분기 말일로 올려 두자고 했다. 조민우가 4월 14일(월) 복사본에서 세 안을 재 왔다.

테이블과 인덱스 ​

  • T_ORD_BIG: 4월 14일 현재 1,094,039행 · 약 6,373블록 · 매일 01:00에 전날 주문 약 913건
  • 인덱스: IX_ORD_BIG_DT_CUS_AMT (ORD_DT, CUS_ID, ORD_AMT) · IX_ORD_BIG_CUS_DT (CUS_ID, ORD_DT) · IX_ORD_BIG_ST_DT (ORD_ST, ORD_DT)
  • 지금 통계: 3월 31일 10:20 수집 · NUM_ROWS 1,081,257 · ORD_DT 최댓값 2025-03-30 · NUM_DISTINCT 1,184 · 히스토그램 없음
  • 수집 한 번: 테이블과 인덱스 셋 1.4 ~ 3.3초(10화 측정) · 자동 수집 창: 평일 22:00부터 4시간, 토 · 일 06:00부터 20시간
  • M_CUS 2,000행 · 30블록 · M_RGN 8행
  • 날짜로 읽는 핵심 SQL 셋: (A) 분기 누적 지역별 매출(분기 첫날 ~ 다음 분기 첫날) (B) 어제 주문 등급별 집계(TRUNC(SYSDATE) - 1 이상 TRUNC(SYSDATE) 미만) (C) 고객 30일 주문 이력(CUS_ID = :c AND ORD_DT >= TRUNC(SYSDATE) - 30)

원본 SQL ​

sql
-- 지금 : 매일 01:00 주문 적재 (일반 INSERT · 통계는 자동 수집에 맡긴다 · STALE_PERCENT 10)
BEGIN
  INSERT INTO T_ORD_BIG (ORD_SEQ, CUS_ID, ORD_DT, ORD_ST, ORD_AMT)
  SELECT ORD_SEQ, CUS_ID, ORD_DT, ORD_ST, ORD_AMT
    FROM STG_ORD_DAY;                       -- 전날 주문 약 913건
  COMMIT;
END;
/

-- 안 ① 오과장 : 적재 뒤에 매일 모은다
--   DBMS_STATS.GATHER_TABLE_STATS(USER, 'T_ORD_BIG');
-- 안 ② 정하은 : 자동 수집이 더 자주 돌게 한다
--   DBMS_STATS.SET_TABLE_PREFS(USER, 'T_ORD_BIG', 'STALE_PERCENT', '1');
-- 안 ③ 조민우 : 분기 첫날마다 ORD_DT 최댓값을 그 분기 말일로 올려 둔다 (NUM_ROWS · NDV 는 그대로)
--   DBMS_STATS.SET_COLUMN_STATS(USER, 'T_ORD_BIG', 'ORD_DT', srec => ...);

실행계획 학습용 예시 · 복사본 측정 · 수집 시간은 교재 10화 측정 ​

xplan
[(A) 분기 누적 · 4월 14일 · 지금 통계]
------------------------------------------------------------------------------------------------------------
| Id  | Operation                      | Name                  | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT               |                       |      1 |        |      8 |00:00:00.02 |   12180 |
|   1 |  HASH GROUP BY                 |                       |      1 |      8 |      8 |00:00:00.02 |   12180 |
|*  2 |   HASH JOIN                    |                       |      1 |    913 |  11869 |00:00:00.02 |   12180 |
|   3 |    TABLE ACCESS FULL           | M_RGN                 |      1 |      8 |      8 |00:00:00.01 |       4 |
|   4 |    NESTED LOOPS                |                       |      1 |    913 |  11869 |00:00:00.02 |   12176 |
|   5 |     NESTED LOOPS               |                       |      1 |    913 |  11869 |00:00:00.01 |     307 |
|*  6 |      INDEX RANGE SCAN          | IX_ORD_BIG_DT_CUS_AMT |      1 |    913 |  11869 |00:00:00.01 |      57 |
|*  7 |      INDEX UNIQUE SCAN         | PK_M_CUS              |  11869 |      1 |  11869 |00:00:00.01 |     250 |
|   8 |     TABLE ACCESS BY INDEX ROWID| M_CUS                 |  11869 |      1 |  11869 |00:00:00.01 |   11869 |
------------------------------------------------------------------------------------------------------------
   2 - access("R"."RGN_ID"="C"."RGN_ID")
   6 - access("O"."ORD_DT">=TO_DATE(' 2025-04-01 00:00:00', 'syyyy-mm-dd hh24:mi:ss') AND
              "O"."ORD_DT"<TO_DATE(' 2025-07-01 00:00:00', 'syyyy-mm-dd hh24:mi:ss'))
   7 - access("C"."CUS_ID"="O"."CUS_ID")

[복사본 측정 · 4월 14일 · E-Rows / A-Rows]
                   지금(3/31 수집)    ① 매일 수집       ② STALE 1 %         ③ 최댓값 6/30
 (A) 분기 누적       913 / 11,869     11,872 / 11,869   10,046 / 11,869 *   77,180 / 11,869
 (B) 어제            913 / 913           913 / 913         913 / 913         1,762 / 913
 (C) 고객 30일         7 / 14             14 / 14           13 / 14             45 / 14
 (A)의 계획          NL · 12,180        HASH · 91         HASH · 91           HASH · 91
 * ②는 4월 12일(토) 06:00 자동 수집 뒤의 값이다. 바뀐 행이 1%(10,813행)를 넘은 것은 4월 12일 01:00 적재 때였다.
   4월 10일에는 (A)가 913 / 8,217로 NL · 8,434블록이었다.
 ③의 통계는 4월 1일 SET_COLUMN_STATS 뒤 그대로다(NUM_ROWS 1,081,257 · NUM_DISTINCT 1,184).

문제 ​

  1. 세 SQL 가운데 어느 것이 언제 틀리는지, 지금 통계와 세 안의 측정값으로 설명하시오. (B)가 어느 안에서나 맞는 이유도 쓰시오.
  2. 세 안을 구멍(틀리는 기간), 비용, 유지 부담으로 비교하고 하나를 골라 적재 배치를 고쳐 쓰시오.
  3. 고른 안이 막지 못하는 것과 함께 정할 것(커서 무효화 시각, 다른 SQL의 계획 변화)을 쓰시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자