Skip to content
튜닝 연습PL/SQL과 사용자 정의 함수
PLSQL-046

ORA-04068 대책을 넣은 뒤로 포인트 적립 API가 네 배 느려지고 야간 대사가 멈췄다

중급PLSQL이커머스696 / 700

이 문제는 교재 18화 졸업 과제와 같은 이커머스 데이터베이스를 씁니다. 『튜너의 1년』을 순서대로 읽고 있다면 졸업 과제를 먼저 풀고 오세요. 아래 「테이블과 인덱스」 칸에 과제의 답 일부(제약 조건, 인덱스, 통계의 실제 상태)가 나옵니다.

상황 ​

이커머스의 포인트 적립 API는 WAS 연결 풀(세션 40개)에서 패키지 pkg_point를 부른다. 10월 8일 14:00에 적립 규칙을 고쳐 패키지 본문을 배포하자, 세션 40개가 처음 부를 때 한 번씩 ORA-04068을 냈다. 그 40건의 적립은 다시 시도되지 않아 고객 문의가 들어왔다. 개발자는 10월 10일 스펙과 본문에 PRAGMA SERIALLY_REUSABLE을 붙였다. 15일 배포 때는 ORA-04068이 나지 않았다. 그런데 API 평균 응답이 1.8ms에서 6.9ms가 됐고, 16일 새벽 야간 포인트 대사가 에러로 멈췄다.

테이블과 인덱스 ​

  • customers: 5,230행 · 51블록 · PK(customer_id) · grade 4종 · 등급은 매일 새벽 재산정한다
  • point_rule(이 문제용): 4행 · PK(grade) · earn_rate 등급별 적립률
  • point_transactions: 45,000행 · 210블록 · PK(tx_id) · tx_id는 시퀀스 seq_tx_id
  • 저녁 피크 1시간에 적립 API가 약 27,400번 불린다. 야간 포인트 대사는 SQL에서 pkg_point.rate_of를 부른다.
  • WAS 연결 풀의 세션은 WAS를 다시 띄울 때까지(보통 2 ~ 3주) 끊기지 않는다.

원본 SQL ​

sql
CREATE OR REPLACE PACKAGE pkg_point IS
  PRAGMA SERIALLY_REUSABLE;                      -- 10/10 추가 : ORA-04068 대책
  PROCEDURE earn(p_customer_id NUMBER, p_amount NUMBER);
  FUNCTION rate_of(p_customer_id NUMBER) RETURN NUMBER;
END;
/
CREATE OR REPLACE PACKAGE BODY pkg_point IS
  PRAGMA SERIALLY_REUSABLE;
  TYPE t_grade IS TABLE OF customers.grade%TYPE INDEX BY PLS_INTEGER;
  TYPE t_rate  IS TABLE OF NUMBER INDEX BY VARCHAR2(20);
  g_grade  t_grade;                              -- 고객 5,230명의 등급 (처음 부를 때 한 번 채운다)
  g_rate   t_rate;                               -- 등급별 적립률
  g_loaded BOOLEAN := FALSE;

  PROCEDURE load IS
  BEGIN
    FOR c IN (SELECT customer_id, grade FROM customers) LOOP
      g_grade(c.customer_id) := c.grade;
    END LOOP;
    FOR r IN (SELECT grade, earn_rate FROM point_rule) LOOP
      g_rate(r.grade) := r.earn_rate;
    END LOOP;
    g_loaded := TRUE;
  END;

  FUNCTION rate_of(p_customer_id NUMBER) RETURN NUMBER IS
  BEGIN
    IF NOT g_loaded THEN load; END IF;
    RETURN g_rate(g_grade(p_customer_id));
  END;

  PROCEDURE earn(p_customer_id NUMBER, p_amount NUMBER) IS
    v_pt NUMBER;
  BEGIN
    v_pt := ROUND(p_amount * rate_of(p_customer_id));
    INSERT INTO point_transactions (tx_id, customer_id, amount, tx_type, created_at)
    VALUES (seq_tx_id.NEXTVAL, p_customer_id, v_pt, 'EARN', SYSDATE);
  END;
END;
/

실행계획 학습용 예시 ​

xplan
[API earn 한 번 · 평균 (V$MYSTAT 증가분)]
                               10/8 이전 (상태 있음)    지금 (SERIALLY_REUSABLE)
  경과                                 1.8 ms                 6.9 ms
  CPU                                  0.6 ms                 5.4 ms
  recursive calls                          3                     58
  session logical reads                    9                     66

[저녁 피크 1시간 — AWR]
  API 호출 27,400번
  SELECT CUSTOMER_ID, GRADE FROM CUSTOMERS     Executions 0 → 27,400 · Buffer Gets 0 → 1,397,400
  SELECT GRADE, EARN_RATE FROM POINT_RULE      Executions 0 → 27,400
  DB CPU                                       41 초 → 176 초

[10/8 14:00 배포 직후 — WAS 로그 (세션마다 한 번, 40건)]
  ORA-04068: existing state of packages has been discarded
  ORA-04061: existing state of package body "ECOMMERCE.PKG_POINT" has been invalidated

[10/16 02:10 야간 포인트 대사]
  SELECT o.order_id, o.customer_id, pkg_point.rate_of(o.customer_id) AS rate FROM orders o WHERE …
  ORA-06534: Cannot access Serially Reusable package "ECOMMERCE.PKG_POINT"

문제 ​

  1. SERIALLY_REUSABLE을 붙인 뒤 API가 느려진 이유와 야간 대사가 멈춘 이유를 설명하시오. 처음에 ORA-04068이 난 이유도 쓰시오.
  2. 본문을 다시 배포해도 ORA-04068이 나지 않으면서 느려지지 않도록 패키지를 고치시오. 고친 뒤의 일량도 쓰시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자