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

고객 2,000명 등급 재산정에 3분 38초, 문자 발송 탓이라는데

실전PLSQL한빛유통441 / 700

상황 ​

매달 1일 새벽에 고객 등급 재산정 배치가 돈다. 최근 12개월 주문 금액으로 등급(300만 원 이상 A, 100만 원 이상 B, 그 밖 C)을 다시 매기고, 등급이 바뀐 고객은 이력을 남기고 문자를 보낸다. 1월 1일 실행은 3분 38초가 걸렸고 바뀐 고객은 214명이었다. 박팀장: "고객이 2,000명뿐인데 왜 이렇게 오래 걸립니까? 문자 발송 때문인가요?" 마케팅팀은 발송 패키지는 게이트웨이 계약 때문에 손댈 수 없다고 한다.

테이블과 인덱스 ​

  • T_ORD_BIG: 1,000,000행 · 5,826블록 · 2024년 주문 333,612건. 인덱스는 IX_ORD_BIG_DT_AMT (ORD_DT, ORD_AMT) 하나다(CUS_ID 인덱스 없음).
  • M_CUS: 2,000행 · 30블록 · PK_M_CUS (CUS_ID) · CUS_GD. 1년 동안 주문이 없는 고객도 있을 수 있다(그 고객은 C).
  • T_CUS_GD_HIST(등급 이력, 이 문제용 테이블): CUS_ID · CHG_DT · OLD_GD · NEW_GD. 회차 번호 BAT_ID와 발송 여부 SENT_YN 칸을 더해도 된다.
  • PKG_NOTI.SEND_GRADE_MSG(p_cus_id, p_new_gd): 마케팅팀 패키지. 발송 대기열 T_MSG_OUT에 한 행을 넣고 UTL_HTTP로 문자 게이트웨이를 부른다(한 번 약 35ms). 안에서 커밋하지 않는다. 보낸 문자는 되돌릴 수 없고, 같은 요청이 두 번 오면 게이트웨이는 두 번 보낸다.

원본 SQL ​

sql
PROCEDURE REGRADE(p_base DATE) IS              -- p_base = DATE '2025-01-01' (이번 달 1일)
  v_amt NUMBER;
  v_new M_CUS.CUS_GD%TYPE;
BEGIN
  FOR c IN (SELECT CUS_ID, CUS_GD FROM M_CUS) LOOP
    -- ① 최근 12개월 실적
    SELECT NVL(SUM(ORD_AMT), 0) INTO v_amt
      FROM T_ORD_BIG
     WHERE CUS_ID = c.CUS_ID
       AND ORD_DT >= ADD_MONTHS(p_base, -12)
       AND ORD_DT <  p_base;
    -- ② 등급 판정과 갱신
    v_new := CASE WHEN v_amt >= 3000000 THEN 'A'
                  WHEN v_amt >= 1000000 THEN 'B' ELSE 'C' END;
    IF v_new <> c.CUS_GD THEN
      UPDATE M_CUS SET CUS_GD = v_new WHERE CUS_ID = c.CUS_ID;
      INSERT INTO T_CUS_GD_HIST (CUS_ID, CHG_DT, OLD_GD, NEW_GD)
      VALUES (c.CUS_ID, p_base, c.CUS_GD, v_new);
      -- ③ 알림
      PKG_NOTI.SEND_GRADE_MSG(c.CUS_ID, v_new);
    END IF;
    COMMIT;                                     -- 고객마다 (보낸 문자는 되돌릴 수 없으니까)
  END LOOP;
END;

실행계획 학습용 예시 ​

xplan
[세션 통계 — REGRADE(DATE '2025-01-01') 한 번 (운영 복제본)]
  경과 시간                 3분 38초
  session logical reads     약 11,659,000
  recursive calls           약 4,900
  user commits              2,000

[V$SQL — 루프 안의 문장]
  SQL_TEXT                                                          EXECUTIONS   BUFFER_GETS    ELAPSED
  SELECT NVL(SUM(ORD_AMT), 0) FROM T_ORD_BIG WHERE CUS_ID = :B3 …        2,000    11,652,000   209.6 초
  UPDATE M_CUS SET CUS_GD = :B2 WHERE CUS_ID = :B1                         214           642     0.01 초
  INSERT INTO T_CUS_GD_HIST (…) VALUES (…)                                  214           236     0.01 초
  (PKG_NOTI.SEND_GRADE_MSG 214번 · 그 안의 UTL_HTTP 대기 7.4초)

[루프 안 SELECT 한 번의 실행계획 — DISPLAY_CURSOR (ALLSTATS LAST)]
-------------------------------------------------------------------------------------------
| Id  | Operation          | Name      | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
-------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |           |      1 |        |      1 |00:00:00.10 |    5826 |
|   1 |  SORT AGGREGATE    |           |      1 |      1 |      1 |00:00:00.10 |    5826 |
|*  2 |   TABLE ACCESS FULL| T_ORD_BIG |      1 |    167 |    166 |00:00:00.10 |    5826 |
-------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   2 - filter(("CUS_ID"=:B3 AND "ORD_DT">=ADD_MONTHS(:B2,-12) AND "ORD_DT"<:B1))

문제 ​

  1. 3분 38초가 어디서 나오는지 V$SQL과 실행계획으로 설명하시오.
  2. ①과 ②를 집합 처리로 다시 작성하고, ③은 왜 한 문장으로 바꿀 수 없는지 쓴 뒤 어떻게 처리할지 SQL로 작성하시오.
  3. 커밋을 어디에 둘지, ③ 도중에 실패했다가 다시 돌릴 때 같은 고객에게 문자가 두 번 가지 않게 하는 방법을 설명하시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자