Skip to content
튜닝 연습조인 튜닝
JOIN-080

힌트로 계획을 고정해 두었다는 「매장 월 상품 판매」가 통계를 새로 모은 주말 뒤로 두 배 넘게 읽는다

중급SELECT유통230 / 700

상황 ​

「매장 월 상품 판매」는 매장 하나와 달 하나를 고르면 그 매장에서 그달 팔린 상품마다 영수증 수와 판매 수량, 금액을 낸다. 점장 여섯 명과 본사 영업팀이 달을 바꿔 가며 하루 300번쯤 연다. 2년 전 선배가 이 SQL에 힌트를 달고 계획이 해시 조인인 것을 확인했다. 힌트 대장에는 "해시 조인으로 고정, 8,682블록"이라고 적혀 있다. 지난 토요일 DBA 강도현이 1년 넘게 갱신되지 않던 판매 테이블들의 통계를 새로 모았다. 월요일부터 이 리포트가 0.17초에서 0.41초로 늘었다. 박팀장은 "힌트를 더 달아서 막을지, 힌트를 떼고 통계와 베이스라인에 맡길지 정해 와라"라고 했다. 아래는 같은 매장의 2023년 6월을 지난주와 이번 주에 돌린 실행계획이다.

테이블과 인덱스 ​

  • CF10 판매: 500,000행 · 4,583블록 · IX_CF10_SLE_DT (SLE_DT) 리프 1,393블록 · 날짜순 적재 · 통계는 매주 모아 왔다
  • CF11 판매상품: 1,096,708행 · 8,204블록 · CF11_PK (SLE_NO, ITM_NO) BLEVEL 2 · 영수증당 평균 2.19줄 · 영수증 번호 순으로 적재
  • CF11 통계: 지난주까지는 2022년 12월 말에 모은 값(548,120행 · 4,110블록)이었고, 지난 토요일 1,096,708행 · 8,204블록으로 바뀌었다
  • 2023년 6월 판매 20,953건 · 이 매장은 3,492건(판매상품 7,656줄, 상품 104종)
  • SLE_DT가 VARCHAR2(8)이라 옵티마이저는 6월 한 달을 5,177건으로, 이 매장의 6월을 863건으로 본다(교재 03화)

원본 SQL ​

sql
SELECT /*+ LEADING(a b) */
       b.ITM_NO,
       COUNT(DISTINCT a.SLE_NO)    AS rcpt_cnt,
       SUM(b.SLE_QTY)              AS sle_qty,
       SUM(b.SLE_QTY * b.SLE_UPRC) AS sle_amt
  FROM CF10 a, CF11 b
 WHERE a.SHP_NO = :v_shp
   AND a.SLE_DT BETWEEN :d1 AND :d2
   AND b.SLE_NO = a.SLE_NO
 GROUP BY b.ITM_NO
 ORDER BY b.ITM_NO

실행계획 학습용 예시 ​

xplan
[지난주 월요일 · 통계 갱신 전]
-------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                              | Name           | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Used-Mem |
-------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                       |                |      1 |        |    104 |00:00:00.17 |    8682 |          |
|   1 |  SORT GROUP BY                         |                |      1 |    104 |    104 |00:00:00.17 |    8682 |  372K (0)|
|*  2 |   HASH JOIN                            |                |      1 |   1890 |   7656 |00:00:00.16 |    8682 | 1421K (0)|
|*  3 |    TABLE ACCESS BY INDEX ROWID BATCHED | CF10           |      1 |    863 |   3492 |00:00:00.02 |     478 |          |
|*  4 |     INDEX RANGE SCAN                   | IX_CF10_SLE_DT |      1 |   5177 |  20953 |00:00:00.01 |      60 |          |
|   5 |    TABLE ACCESS FULL                   | CF11           |      1 |    548K|   1096K|00:00:00.09 |    8204 |          |
-------------------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   2 - access("B"."SLE_NO"="A"."SLE_NO")
   3 - filter("A"."SHP_NO"=:V_SHP)
   4 - access("A"."SLE_DT">=:D1 AND "A"."SLE_DT"<=:D2)

[이번 주 월요일 · 통계 갱신 뒤]
-------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                              | Name           | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Used-Mem |
-------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                       |                |      1 |        |    104 |00:00:00.41 |   18610 |          |
|   1 |  SORT GROUP BY                         |                |      1 |    104 |    104 |00:00:00.41 |   18610 |  372K (0)|
|   2 |   NESTED LOOPS                         |                |      1 |   1890 |   7656 |00:00:00.40 |   18610 |          |
|   3 |    NESTED LOOPS                        |                |      1 |   1890 |   7656 |00:00:00.22 |   10954 |          |
|*  4 |     TABLE ACCESS BY INDEX ROWID BATCHED| CF10           |      1 |    863 |   3492 |00:00:00.02 |     478 |          |
|*  5 |      INDEX RANGE SCAN                  | IX_CF10_SLE_DT |      1 |   5177 |  20953 |00:00:00.01 |      60 |          |
|*  6 |     INDEX RANGE SCAN                   | CF11_PK        |   3492 |      2 |   7656 |00:00:00.15 |   10476 |          |
|   7 |    TABLE ACCESS BY INDEX ROWID         | CF11           |   7656 |      2 |   7656 |00:00:00.17 |    7656 |          |
-------------------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   4 - filter("A"."SHP_NO"=:V_SHP)
   5 - access("A"."SLE_DT">=:D1 AND "A"."SLE_DT"<=:D2)
   6 - access("B"."SLE_NO"="A"."SLE_NO")

Hint Report (identified by operation id / Query Block Name / Object Alias):
Total hints for statement: 1
---------------------------------------------------------------------------

   1 -  SEL$1
         -  LEADING(a b)

Outline Data
-------------
  /*+
      BEGIN_OUTLINE_DATA
      IGNORE_OPTIM_EMBEDDED_HINTS
      OPTIMIZER_FEATURES_ENABLE('19.1.0')
      DB_VERSION('19.1.0')
      ALL_ROWS
      OUTLINE_LEAF(@"SEL$1")
      INDEX_RS_ASC(@"SEL$1" "A"@"SEL$1" ("CF10"."SLE_DT"))
      BATCH_TABLE_ACCESS_BY_ROWID(@"SEL$1" "A"@"SEL$1")
      INDEX(@"SEL$1" "B"@"SEL$1" ("CF11"."SLE_NO" "CF11"."ITM_NO"))
      LEADING(@"SEL$1" "A"@"SEL$1" "B"@"SEL$1")
      USE_NL(@"SEL$1" "B"@"SEL$1")
      NLJ_BATCHING(@"SEL$1" "B"@"SEL$1")
      END_OUTLINE_DATA
  */

문제 ​

  1. 힌트가 있는데도 계획이 바뀐 이유를 Hint Report와 Outline Data로 설명하시오. 통계 갱신이 옵티마이저의 계산을 어떻게 바꿨는지 E-Rows와 CF11의 통계로 쓰시오.
  2. 이 블록의 계획이 통계에 흔들리지 않게 하는 두 길(빈틈없는 힌트 묶음, 힌트를 떼고 통계와 베이스라인에 맡기기)을 비교해 하나를 고르고 SQL로 쓰시오. USE_HASH(b) 하나만 더하면 왜 모자란지도 쓰시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자