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

한정 메뉴 매장별 판매 보고서는 두 번째로 열면 빠른데 아침 첫 조회만 다섯 배 넘게 느리다

중급SELECT버거643 / 700

상황 ​

버거 본사 마케팅팀은 12월 한정 메뉴 「크리스마스 더블 버거」(I091)의 매장별 판매를 캠페인 내내 같은 보고서로 본다. 보고서 SQL은 기간과 메뉴가 글자로 박혀 있어 캠페인 동안 한 글자도 바뀌지 않는다. 팀원 다섯 명이 하루 열 번쯤 연다. 박팀장이 전했다. "아침에 처음 여는 사람만 0.5초 넘게 기다리고, 그다음부터는 0.1초도 안 걸린답니다. 날마다 그래요." POS 원장 통계는 매일 03:00에 기본값으로 다시 모은다.

테이블과 인덱스 ​

  • ORD: 1,000,000행 · 22,422블록 · 날짜순 적재 · 2013년 12월 41,666건 · ORD_PK (ORD_NO) · ORD_X01 (ORD_DT, ORD_HMS)
  • ORD_ITEM: 2,472,717행 · 55,265블록 · 날짜순 적재 · 2013년 12월 103,030줄 · ORD_ITEM_PK (ORD_NO, ITEM_ID) · ORD_ITEM_X01 (ORD_DT, ORD_HMS)
  • 이 문제의 ITEM_ID는 'I091'처럼 네 글자다. 한정 메뉴 I091은 2013년 12월에만 팔렸다(8,300줄). ITEM_ID에는 FREQUENCY 히스토그램이 있다(92종).
  • 통계: 매일 03:00 GATHER_SCHEMA_STATS 기본값(무효화는 AUTO_INVALIDATE)

원본 SQL ​

sql
-- 「한정 메뉴 매장별 판매」 (SQL_ID 3m8qv1x7c2k0p)
SELECT o.SHOP_NO, COUNT(*) AS LINE_CNT, SUM(i.ORD_ITEM_QTY) AS QTY
  FROM ORD_ITEM i, ORD o
 WHERE i.ITEM_ID = 'I091'
   AND i.ORD_DT BETWEEN '20131201' AND '20131231'
   AND o.ORD_NO = i.ORD_NO
   AND o.ORD_DT BETWEEN '20131201' AND '20131231'
 GROUP BY o.SHOP_NO

실행계획 학습용 예시 ​

xplan
[V$SQL · SQL_ID 3m8qv1x7c2k0p · 오늘]
 CHILD  PLAN_HASH_VALUE  IS_REOPTIMIZABLE  EXECUTIONS  BUFFER_GETS  ELAPSED  LAST_LOAD_TIME
     0      1093361258   Y                         1       20,265   0.52초   08:52
     1      4027719634   N                         9       33,363   0.71초   09:10
[V$SQL_SHARED_CURSOR · 자식 1]  USE_FEEDBACK_STATS = Y

[자식 0 · 아침 첫 실행]
Plan hash value: 1093361258

-------------------------------------------------------------------------------------------------------------------
| Id  | Operation                             | Name         | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads  |
-------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                      |              |      1 |        |    200 |00:00:00.52 |   20265 |   2911 |
|   1 |  HASH GROUP BY                        |              |      1 |     15 |    200 |00:00:00.52 |   20265 |   2911 |
|   2 |   NESTED LOOPS                        |              |      1 |     15 |   8300 |00:00:00.50 |   20265 |   2911 |
|   3 |    NESTED LOOPS                       |              |      1 |    352 |   8300 |00:00:00.41 |   19215 |   2804 |
|*  4 |     TABLE ACCESS BY INDEX ROWID BATCHED| ORD_ITEM    |      1 |    352 |   8300 |00:00:00.06 |    2615 |      0 |
|*  5 |      INDEX RANGE SCAN                 | ORD_ITEM_X01 |      1 |   103K |   103K |00:00:00.01 |     312 |      0 |
|*  6 |     INDEX UNIQUE SCAN                 | ORD_PK       |   8300 |      1 |   8300 |00:00:00.33 |   16600 |   2804 |
|*  7 |    TABLE ACCESS BY INDEX ROWID        | ORD          |   8300 |      1 |   8300 |00:00:00.08 |    1050 |    107 |
-------------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   4 - filter("I"."ITEM_ID"='I091')
   5 - access("I"."ORD_DT">='20131201' AND "I"."ORD_DT"<='20131231')
   6 - access("O"."ORD_NO"="I"."ORD_NO")
   7 - filter(("O"."ORD_DT">='20131201' AND "O"."ORD_DT"<='20131231'))

[자식 1 · 두 번째 실행부터]
Plan hash value: 4027719634

-------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                             | Name         | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Used-Mem |
-------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                      |              |      1 |        |    200 |00:00:00.08 |    3707 |          |
|   1 |  HASH GROUP BY                        |              |      1 |    200 |    200 |00:00:00.08 |    3707 | 1105K (0)|
|*  2 |   HASH JOIN                           |              |      1 |    346 |   8300 |00:00:00.07 |    3707 | 1532K (0)|
|*  3 |    TABLE ACCESS BY INDEX ROWID BATCHED| ORD_ITEM     |      1 |   8300 |   8300 |00:00:00.04 |    2615 |          |
|*  4 |     INDEX RANGE SCAN                  | ORD_ITEM_X01 |      1 |   103K |   103K |00:00:00.01 |     312 |          |
|   5 |    TABLE ACCESS BY INDEX ROWID BATCHED| ORD          |      1 |  41666 |  41666 |00:00:00.02 |    1092 |          |
|*  6 |     INDEX RANGE SCAN                  | ORD_X01      |      1 |  41666 |  41666 |00:00:00.01 |     158 |          |
-------------------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   2 - access("O"."ORD_NO"="I"."ORD_NO")
   3 - filter("I"."ITEM_ID"='I091')
   4 - access("I"."ORD_DT">='20131201' AND "I"."ORD_DT"<='20131231')
   6 - access("O"."ORD_DT">='20131201' AND "O"."ORD_DT"<='20131231')

Note
-----
   - statistics feedback used for this statement

문제 ​

  1. 같은 SQL이 아침 첫 실행만 느리고 두 번째부터 빠른 이유를 두 자식 커서와 Note로 설명하시오. 다음 날 아침 다시 느려지는 이유와, 자식 0의 4번 줄 E-Rows 352가 실제 8,300과 다른 이유도 쓰시오.
  2. 첫 실행부터 빠른 계획으로 돌도록 고치시오. 통계(히스토그램, 컬럼 그룹)로 고쳐지는지와 고른 방법의 대가도 쓰시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자