Skip to content
튜닝 연습DML과 배치 튜닝
DML-029

지난달 매장 일 집계를 다시 맞추는 배치가 48만 블록을 읽고, 주문을 지운 날의 숫자는 그대로 남긴다

실전MERGE버거454 / 700

상황 ​

버거원 본사의 매장 화면은 매장 일 집계 테이블(SHOP_DAY)만 읽는다. 매일 밤 전날 하루치를 넣고, 매월 3일 02:00에는 늦게 올라온 POS 주문을 반영하려고 지난달 한 달 치를 다시 맞춘다. 12월분을 다시 맞추는 데 8초 남짓이 걸렸다. 그런데 SH0077 매장의 12월 24일은 테스트 주문 61건을 지웠는데도 화면에 주문 61건이 그대로 나온다. 개발팀은 MERGE로 바꾸자는 쪽과 그 달을 지우고 다시 넣자는 쪽으로 갈렸다. 박팀장은 세 방법을 비교한 근거를 남기라고 했다.

테이블과 인덱스 ​

  • ORD 주문: 1,000,000행 · 22,422블록 · ORD_PK (ORD_NO) · ORD_X01 (ORD_DT, ORD_HMS) · 날짜순 적재 · 2013년 12월 42,408행(하루 약 1,370건, 매장 하루 약 7건)
  • ORD_ITEM 주문상품: 2,472,717행 · 55,265블록 · ORD_ITEM_PK (ORD_NO, ITEM_ID) 높이 3 · ORD_ITEM_X01 (ORD_DT, ORD_HMS) · ORD_DT는 ORD에서 복사해 둔 칸 · 12월분 104,712행
  • SHOP_DAY(매장 일 집계, 이 문제를 위해 둔 테이블): 146,000행 · 524블록 · PK (SHOP_NO, ORD_DT) · ORD_CNT, ITEM_QTY(둘 다 NOT NULL) · 12월분 6,200행(200매장 × 31일)
  • 다시 집계하면 12월에 숫자가 바뀌는 행 214행, 새로 생길 행 3행(그날 첫 주문이 늦게 올라온 매장), 주문이 0건이 되는 행 1행(SH0077의 12월 24일)
  • 매장 화면은 하루 종일 SHOP_DAY를 읽는다. 다른 테이블이 SHOP_DAY를 참조하지는 않는다.

원본 SQL ​

sql
-- 매월 3일 02:00 · 지난달 매장 일 집계 다시 맞추기 (늦게 올라온 POS 반영)
-- ① 이미 있는 날은 고친다
UPDATE SHOP_DAY t
   SET (t.ORD_CNT, t.ITEM_QTY) =
       (SELECT COUNT(DISTINCT o.ORD_NO), SUM(i.ORD_ITEM_QTY)
          FROM ORD o, ORD_ITEM i
         WHERE o.SHOP_NO = t.SHOP_NO
           AND o.ORD_DT  = t.ORD_DT
           AND i.ORD_NO  = o.ORD_NO)
 WHERE t.ORD_DT BETWEEN '20131201' AND '20131231'
   AND EXISTS (SELECT 1 FROM ORD o
                WHERE o.SHOP_NO = t.SHOP_NO AND o.ORD_DT = t.ORD_DT);

-- ② 없던 날은 넣는다
INSERT INTO SHOP_DAY (SHOP_NO, ORD_DT, ORD_CNT, ITEM_QTY)
SELECT o.SHOP_NO, o.ORD_DT, COUNT(DISTINCT o.ORD_NO), SUM(i.ORD_ITEM_QTY)
  FROM ORD o, ORD_ITEM i
 WHERE o.ORD_DT BETWEEN '20131201' AND '20131231'
   AND i.ORD_NO = o.ORD_NO
   AND NOT EXISTS (SELECT 1 FROM SHOP_DAY t
                    WHERE t.SHOP_NO = o.SHOP_NO AND t.ORD_DT = o.ORD_DT)
 GROUP BY o.SHOP_NO, o.ORD_DT;
COMMIT;

실행계획 학습용 예시 ​

xplan
[① UPDATE 위 · ② INSERT 아래]
-------------------------------------------------------------------------------------------------------------------
| Id  | Operation                                | Name         | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
-------------------------------------------------------------------------------------------------------------------
|   0 | UPDATE STATEMENT                         |              |      1 |        |      0 |00:00:06.02 |    420K |
|   1 |  UPDATE                                  | SHOP_DAY     |      1 |        |      0 |00:00:06.02 |    420K |
|*  2 |   HASH JOIN SEMI                         |              |      1 |   6200 |   6199 |00:00:00.03 |    1590 |
|*  3 |    TABLE ACCESS FULL                     | SHOP_DAY     |      1 |   6200 |   6200 |00:00:00.01 |     524 |
|   4 |    TABLE ACCESS BY INDEX ROWID BATCHED   | ORD          |      1 |  42408 |  42408 |00:00:00.02 |    1066 |
|*  5 |     INDEX RANGE SCAN                     | ORD_X01      |      1 |  42408 |  42408 |00:00:00.01 |     117 |
|   6 |   SORT AGGREGATE                         |              |   6199 |      1 |   6199 |00:00:05.81 |    409K |
|   7 |    NESTED LOOPS                          |              |   6199 |     17 |    104K|00:00:05.62 |    409K |
|*  8 |     TABLE ACCESS BY INDEX ROWID BATCHED  | ORD          |   6199 |      7 |  42347 |00:00:03.10 |    236K |
|*  9 |      INDEX RANGE SCAN                    | ORD_X01      |   6199 |   1370 |   8480K|00:00:01.02 |   43393 |
|  10 |     TABLE ACCESS BY INDEX ROWID BATCHED  | ORD_ITEM     |  42347 |      2 |    104K|00:00:02.41 |    174K |
|* 11 |      INDEX RANGE SCAN                    | ORD_ITEM_PK  |  42347 |      2 |    104K|00:00:01.20 |    127K |
-------------------------------------------------------------------------------------------------------------------
|   0 | INSERT STATEMENT                         |              |      1 |        |      0 |00:00:02.10 |   56877 |
|   1 |  LOAD TABLE CONVENTIONAL                 | SHOP_DAY     |      1 |        |      0 |00:00:02.10 |   56877 |
|   2 |   HASH GROUP BY                          |              |      1 |   6200 |      3 |00:00:02.09 |   56855 |
|*  3 |    HASH JOIN                             |              |      1 |    104K|     53 |00:00:02.08 |   56855 |
|*  4 |     HASH JOIN ANTI                       |              |      1 |  42408 |     21 |00:00:00.03 |    1590 |
|   5 |      TABLE ACCESS BY INDEX ROWID BATCHED | ORD          |      1 |  42408 |  42408 |00:00:00.02 |    1066 |
|*  6 |       INDEX RANGE SCAN                   | ORD_X01      |      1 |  42408 |  42408 |00:00:00.01 |     117 |
|*  7 |      TABLE ACCESS FULL                   | SHOP_DAY     |      1 |   6200 |   6200 |00:00:00.01 |     524 |
|   8 |     TABLE ACCESS FULL                    | ORD_ITEM     |      1 |   2472K|   2472K|00:00:01.71 |   55265 |
-------------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
① 2 - access("O"."SHOP_NO"="T"."SHOP_NO" AND "O"."ORD_DT"="T"."ORD_DT")
   3 - filter(("T"."ORD_DT">='20131201' AND "T"."ORD_DT"<='20131231'))
   5 - access("O"."ORD_DT">='20131201' AND "O"."ORD_DT"<='20131231')
   8 - filter("O"."SHOP_NO"=:B1)
   9 - access("O"."ORD_DT"=:B2)
  11 - access("I"."ORD_NO"="O"."ORD_NO")
② 3 - access("I"."ORD_NO"="O"."ORD_NO")
   4 - access("T"."SHOP_NO"="O"."SHOP_NO" AND "T"."ORD_DT"="O"."ORD_DT")
   6 - access("O"."ORD_DT">='20131201' AND "O"."ORD_DT"<='20131231')
   7 - filter(("T"."ORD_DT">='20131201' AND "T"."ORD_DT"<='20131231'))

  ① 갱신 6,199행 · ② 입력 3행 · redo 약 1.6 MB · SH0077 12월 24일 ORD_CNT 61 그대로

문제 ​

  1. 지금 두 문장의 일량이 어디서 나오는지 실행계획의 Starts로 설명하고, SH0077의 12월 24일 행이 왜 그대로 남는지 설명하시오.
  2. (A) 지금 방식, (B) MERGE 한 문장, (C) 12월분을 지우고 다시 넣기를 읽는 양, 고치는 행과 redo, 결과의 정확성, 매장 화면에 주는 영향으로 비교하시오.
  3. 고른 방법을 SQL로 작성하시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자