테마
버거원 본사의 「매장별 상품 판매」 화면은 집계 테이블 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개는 어젯밤 집계로 이미 있다.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;
[세션 통계 — 두 판 비교 (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')