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

매장 상품 월 집계를 FORALL로 바꿨는데 논리 읽기가 그대로다

중급MERGE버거442 / 700

상황 ​

버거원 본사의 「매장별 상품 판매」 화면은 집계 테이블 SHOP_ITEM_MON을 읽는다. 이번 달 집계는 매일 밤 다시 계산해 덮어쓴다. 원래는 커서 루프로 (매장, 상품) 조합마다 MERGE를 한 번씩 실행했는데(3.1초), "BULK로 바꾸면 빨라진다"는 말을 듣고 개발자가 BULK COLLECT와 FORALL로 고쳤다. 경과 시간은 2.3초로 조금 줄었지만 세션의 논리 읽기는 95,412 → 93,806으로 거의 그대로다. 개발자: "FORALL이면 한 번에 처리되는 거 아닌가요?"

테이블과 인덱스 ​

  • ORD: 1,000,000행 · 22,422블록 · ORD_X01 (ORD_DT, ORD_HMS) · 2013년 12월 42,408행 · SHOP_NO 200개
  • ORD_ITEM: 2,472,717행 · 55,265블록 · ORD_ITEM_X01 (ORD_DT, ORD_HMS) · 2013년 12월 106,342행(ORD_DT는 ORD에서 복사한 값)
  • SHOP_ITEM_MON(집계, 이 문제용 테이블): SHOP_ITEM_MON_PK (ORD_YM, SHOP_NO, ITEM_ID) · SALE_QTY · LINE_CNT · 24개월 약 430,000행 · 약 1,900블록. 12월 조합은 17,934개이고, 그중 17,760개는 어젯밤 집계로 이미 있다.
  • 통계는 최신이다.

원본 SQL ​

sql
DECLARE
  TYPE t_shop IS TABLE OF SHOP_ITEM_MON.SHOP_NO%TYPE;
  TYPE t_item IS TABLE OF SHOP_ITEM_MON.ITEM_ID%TYPE;
  TYPE t_num  IS TABLE OF NUMBER;
  v_shop t_shop;  v_item t_item;  v_qty t_num;  v_cnt t_num;
BEGIN
  SELECT o.SHOP_NO, i.ITEM_ID, SUM(i.ORD_ITEM_QTY), COUNT(*)
    BULK COLLECT INTO v_shop, v_item, v_qty, v_cnt
    FROM ORD o, ORD_ITEM i
   WHERE i.ORD_NO = o.ORD_NO
     AND o.ORD_DT BETWEEN '20131201' AND '20131231'
     AND i.ORD_DT BETWEEN '20131201' AND '20131231'
   GROUP BY o.SHOP_NO, i.ITEM_ID;

  FORALL k IN 1 .. v_shop.COUNT                    -- "BULK 로 바꾸면 빨라진다"
    MERGE INTO SHOP_ITEM_MON t
    USING (SELECT '201312' AS ORD_YM, v_shop(k) AS SHOP_NO, v_item(k) AS ITEM_ID,
                  v_qty(k) AS QTY, v_cnt(k) AS CNT
             FROM dual) s
       ON (t.ORD_YM = s.ORD_YM AND t.SHOP_NO = s.SHOP_NO AND t.ITEM_ID = s.ITEM_ID)
     WHEN MATCHED THEN UPDATE SET t.SALE_QTY = s.QTY, t.LINE_CNT = s.CNT
     WHEN NOT MATCHED THEN INSERT (ORD_YM, SHOP_NO, ITEM_ID, SALE_QTY, LINE_CNT)
          VALUES (s.ORD_YM, s.SHOP_NO, s.ITEM_ID, s.QTY, s.CNT);
  COMMIT;
END;

실행계획 학습용 예시 ​

xplan
[세션 통계 — 두 판 비교 (12월분 · 17,934조합)]
                               루프 판(지난달까지)     FORALL 판(이번 주)
  경과 시간                    3.1 초                 2.3 초
  recursive calls              18,046                     34
  execute count                17,951                     22
  session logical reads        95,412                 93,806
  db block changes             36,180                 35,940
  redo size                    7.9 MB                 7.7 MB

[V$SQL — FORALL 판의 MERGE]
  ROWS_PROCESSED 17,934 · BUFFER_GETS 89,940 (원소 하나에 약 5블록: PK 탐색 약 3 · 고치기 약 2)

[집계 SELECT — DISPLAY_CURSOR (ALLSTATS LAST)]
----------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                             | Name         | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Used-Mem |
----------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                      |              |      1 |        |  17934 |00:00:00.19 |    3750 |          |
|   1 |  HASH GROUP BY                        |              |      1 |  18400 |  17934 |00:00:00.19 |    3750 | 2318K (0)|
|*  2 |   HASH JOIN                           |              |      1 |    106K|    106K|00:00:00.12 |    3750 | 3021K (0)|
|   3 |    TABLE ACCESS BY INDEX ROWID BATCHED| ORD          |      1 |  42408 |  42408 |00:00:00.02 |    1067 |          |
|*  4 |     INDEX RANGE SCAN                  | ORD_X01      |      1 |  42408 |  42408 |00:00:00.01 |     131 |          |
|   5 |    TABLE ACCESS BY INDEX ROWID BATCHED| ORD_ITEM     |      1 |    106K|    106K|00:00:00.03 |    2683 |          |
|*  6 |     INDEX RANGE SCAN                  | ORD_ITEM_X01 |      1 |    106K|    106K|00:00:00.01 |     381 |          |
----------------------------------------------------------------------------------------------------------------------------

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

문제 ​

  1. FORALL로 바꿨는데 recursive calls만 줄고 논리 읽기가 그대로인 이유를 설명하시오.
  2. 같은 결과를 내도록 다시 작성하고, 고친 뒤의 숫자를 적으시오.
  3. FORALL이 맞는 경우는 언제인지 쓰시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자