Skip to content
튜닝 연습소트 튜닝과 페이징
SORT-057

「매장 일 실적 보드」를 한 번 열면 주문 인덱스를 1만 8천 번 찾고, 쉰 날은 보드에서 빠진다

실전SELECT버거362 / 700

상황 ​

본사 영업관리팀의 「매장 일 실적 보드」는 고른 달의 200개 매장 × 날짜 칸마다 주문 수, 전일 대비 증감, 월 누적 주문 수, 최근 7일 평균을 보여 준다. 지역 담당 20명과 영업관리팀이 하루 약 300번 연다. 한 번 여는 데 1.2초가 걸리고 DB CPU 상위 SQL 2위다. SH0042 점장이 이렇게 말한다. "리모델링으로 9월 9일부터 11일까지 쉬었는데 보드에 그 사흘이 아예 없어요. 쉰 날도 0으로 보여 주세요." 개발팀이 낸 안은 셋이다. ㉠ 지금 SQL을 두고 인덱스를 더 만든다. ㉡ 분석 함수로 한 번에 계산한다. ㉢ 매장 일 집계 표를 따로 만든다. 박팀장은 "셋을 숫자와 대가로 비교해서 하나를 골라 달라"고 했다.

테이블과 인덱스 ​

  • ORD 주문: 1,000,000행 · 22,422블록 · ORD_DT VARCHAR2(8) · SHOP_NO 매장 200개(매장마다 하루 약 7건) · 주문일 순으로 쌓여 있다.
  • 인덱스: ORD_PK (ORD_NO), ORD_X01 (ORD_DT, ORD_HMS), IX_ORD_SHOP_DT (SHOP_NO, ORD_DT)(지난달 점장 앱 달력 때문에 만들었다. SORT-044). IX_ORD_SHOP_DT에서 한 매장의 한 달(약 210항목)은 리프 한두 블록에 든다.
  • SHOP 매장: 200행 · 5블록. 2013년 9월 주문은 41,100건이고, 주문이 한 건도 없는 매장 × 날짜 칸이 10개 있다(SH0042의 9일 ~ 11일 포함). 그래서 보드의 칸은 6,000개인데 SQL은 5,990줄을 낸다.
  • 통계는 최신이다.

원본 SQL ​

sql
SELECT d.SHOP_NO, d.ORD_DT, d.ord_cnt,
       d.ord_cnt - (SELECT COUNT(*) FROM ORD p
                     WHERE p.SHOP_NO = d.SHOP_NO
                       AND p.ORD_DT  = TO_CHAR(TO_DATE(d.ORD_DT, 'YYYYMMDD') - 1, 'YYYYMMDD')) AS diff_prev,
       (SELECT COUNT(*) FROM ORD c
         WHERE c.SHOP_NO = d.SHOP_NO
           AND c.ORD_DT BETWEEN SUBSTR(d.ORD_DT, 1, 6) || '01' AND d.ORD_DT)              AS cum_cnt,
       (SELECT ROUND(COUNT(*) / 7, 1) FROM ORD w
         WHERE w.SHOP_NO = d.SHOP_NO
           AND w.ORD_DT BETWEEN TO_CHAR(TO_DATE(d.ORD_DT, 'YYYYMMDD') - 6, 'YYYYMMDD')
                            AND d.ORD_DT)                                                AS avg7
  FROM (SELECT SHOP_NO, ORD_DT, COUNT(*) AS ord_cnt
          FROM ORD
         WHERE ORD_DT BETWEEN :ym || '01' AND :ym || '31'
         GROUP BY SHOP_NO, ORD_DT) d
 ORDER BY d.SHOP_NO, d.ORD_DT

실행계획 학습용 예시 ​

xplan
[2013년 9월]
---------------------------------------------------------------------------------------------------------
| Id  | Operation             | Name           | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
---------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT      |                |      1 |        |   5990 |00:00:01.21 |   57522 |
|   1 |  SORT AGGREGATE       |                |   5990 |      1 |   5990 |00:00:00.21 |   17970 |
|*  2 |   INDEX RANGE SCAN    | IX_ORD_SHOP_DT |   5990 |      7 |  41034 |00:00:00.17 |   17970 |
|   3 |  SORT AGGREGATE       |                |   5990 |      1 |   5990 |00:00:00.38 |   19770 |
|*  4 |   INDEX RANGE SCAN    | IX_ORD_SHOP_DT |   5990 |    110 |    636K|00:00:00.31 |   19770 |
|   5 |  SORT AGGREGATE       |                |   5990 |      1 |   5990 |00:00:00.26 |   19170 |
|*  6 |   INDEX RANGE SCAN    | IX_ORD_SHOP_DT |   5990 |     48 |    287K|00:00:00.21 |   19170 |
|   7 |  SORT GROUP BY NOSORT |                |      1 |   6000 |   5990 |00:00:00.02 |     612 |
|*  8 |   INDEX SKIP SCAN     | IX_ORD_SHOP_DT |      1 |  41100 |  41100 |00:00:00.01 |     612 |
---------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   2 - access("P"."SHOP_NO"=:B1 AND "P"."ORD_DT"=TO_CHAR(TO_DATE(:B2,'YYYYMMDD')-1,'YYYYMMDD'))
   4 - access("C"."SHOP_NO"=:B1 AND "C"."ORD_DT">=SUBSTR(:B2,1,6)||'01' AND "C"."ORD_DT"<=:B3)
   6 - access("W"."SHOP_NO"=:B1 AND "W"."ORD_DT">=TO_CHAR(TO_DATE(:B2,'YYYYMMDD')-6,'YYYYMMDD')
              AND "W"."ORD_DT"<=:B3)
   8 - access("ORD_DT">=:YM||'01' AND "ORD_DT"<=:YM||'31')
       filter(("ORD_DT">=:YM||'01' AND "ORD_DT"<=:YM||'31'))

문제 ​

  1. 한 번 여는 데 57,522블록을 읽고 1.2초가 걸리는 까닭을 Starts로 설명하고, 쉰 날이 보드에서 빠지는 까닭을 쓰시오.
  2. ㉠, ㉡, ㉢을 일량과 대가로 비교해 하나를 고르시오.
  3. 고른 안으로 SQL을 작성하시오. 달의 첫 며칠과 쉰 날 앞뒤의 값이 맞게 나오도록 하시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자