Skip to content
튜닝 연습Lock과 동시성 제어
LOCK-021

네 갈래로 나눠 돌린 판매 집계 재계산이 데드락으로 죽고, 다시 돌리니 숫자가 늘었다

실전PLSQL버거516 / 700

상황 ​

버거 본사는 메뉴 코드를 정정할 때마다 「일별 상품 판매 집계」를 2년 치 전부 다시 계산한다. 한 세션으로 43분 걸리던 일을, 지난달 개발팀이 매장 번호로 네 갈래로 나눠 잡 넷이 동시에 돌도록 바꿨다(MOD(매장 번호, 4)). 7월 3일 22:10에 처음 돌렸더니 잡 넷 가운데 셋이 ORA-00060으로 죽었다. 담당자가 다음 날 죽은 잡 셋을 처음부터 다시 돌렸고 이번에는 끝까지 돌았다. 그런데 2012년 초의 판매 수량이 POS 보고서보다 훨씬 많다.

테이블과 인덱스 ​

  • ORD 주문: 1,000,000행 · 22,422블록 · SHOP_NO VARCHAR2 'SH0001' ~ 'SH0200' · ORD_PK (ORD_NO)
  • ORD_ITEM 주문상품: 2,472,717행 · 55,265블록 · ORD_NO · ITEM_ID · ORD_ITEM_QTY · ORD_DT · ORD_ITEM_PK (ORD_NO, ITEM_ID)
  • ITEM_DAY_SUM 일별 상품 판매 집계(이 문제의 파생 테이블): 상품 92개 × 730일 = 67,160행 · 약 250블록 · PK (ITEM_ID, ORD_DT) · SALE_QTY
  • 집계는 본사 보고 화면이 읽기만 한다. 재계산은 마감 뒤(22:00 이후)에 돈다. 같은 시간에 집계를 고치는 다른 작업은 없다.

원본 SQL ​

sql
-- 0단계 (한 번): 집계를 0으로
UPDATE ITEM_DAY_SUM SET SALE_QTY = 0;
COMMIT;

-- 1단계: 잡 넷 (p_part = 0 · 1 · 2 · 3) 을 동시에
PROCEDURE RECALC_PART(p_part NUMBER) IS
  n PLS_INTEGER := 0;
BEGIN
  FOR r IN (SELECT i.ITEM_ID, i.ORD_DT, i.ORD_ITEM_QTY
              FROM ORD o, ORD_ITEM i
             WHERE i.ORD_NO = o.ORD_NO
               AND MOD(TO_NUMBER(SUBSTR(o.SHOP_NO, 3)), 4) = p_part
             ORDER BY o.ORD_NO) LOOP
    UPDATE ITEM_DAY_SUM
       SET SALE_QTY = SALE_QTY + r.ORD_ITEM_QTY
     WHERE ITEM_ID = r.ITEM_ID AND ORD_DT = r.ORD_DT;
    n := n + 1;
    IF MOD(n, 1000) = 0 THEN COMMIT; END IF;
  END LOOP;
  COMMIT;
END;

실행계획 학습용 예시 ​

xplan
[잡 넷 — 7월 3일 22:10 시작 (0단계로 67,160행을 0으로 만든 뒤)]
잡    SID   매장   처리할 줄     결과
J0    330   50곳   618,204     끝 · 23:01 (51분)
J1    301   50곳   619,515     ORA-00060 · 22:14:07 · 52,000줄을 COMMIT 한 뒤 죽음
J2    288   50곳   617,880     ORA-00060 · 22:19:52 · 127,000줄을 COMMIT 한 뒤 죽음
J3    317   50곳   617,118     ORA-00060 · 22:27:30 · 211,000줄을 COMMIT 한 뒤 죽음
(한 세션으로 돌리던 지난달까지: 2,472,717줄 · 43분)

[V$SESSION — 22:12:00 · 넷 다 같은 문장]
SID   EVENT                            SECS   BLOCKING_SESSION
301   enq: TX - row lock contention      2    288
288   enq: TX - row lock contention      1    330
317   enq: TX - row lock contention      3    301
330   (CPU)
하고 있는 문장: UPDATE ITEM_DAY_SUM SET SALE_QTY = SALE_QTY + :B3 WHERE ITEM_ID = :B2 AND ORD_DT = :B1

[J1 (SID 301) 의 트레이스 — Deadlock graph]
Resource Name                       process session holds waits   process session holds waits
TX-000A0015-00001F2C-469D09C8-...        52     317     X              47     301           X
TX-0003001B-00002A61-469D09C8-...        47     301     X              52     317           X
Rows waited on:
  Session 301: ITEM_DAY_SUM · ITEM_ID 12 · ORD_DT '20120115'
  Session 317: ITEM_DAY_SUM · ITEM_ID 7  · ORD_DT '20120115'

[J0 (SID 330) — 세션 통계]
  UPDATE 실행 618,204번 · user commits 619 · enq: TX 기다림 2,914번 · 합계 31분 (경과 51분의 61%)

[다시 돌린 뒤 — POS 보고서와 비교한 판매 수량]
  2012년 1 ~ 2월 1.75배 · 3 ~ 5월 1.5배 · 6 ~ 8월 1.25배 · 9월 초부터 같음

문제 ​

  1. 잡 넷의 결과와 V$SESSION, 트레이스를 근거로 매장 번호로 나눈 잡끼리 왜 서로 기다리고 원을 만드는지 설명하시오. 다시 돌린 뒤 숫자가 늘어난 이유도 쓰시오.
  2. 한 문장으로 다시 계산하기, 잡을 상품 번호로 나누기, 매장으로 나누되 잡마다 먼저 모아서 정해진 순서로 갱신하기를 비교하고 하나를 골라 SQL로 작성하시오.
  3. 고른 방법의 대가와, 실패한 뒤 다시 돌려도 안전한지 쓰시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자