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

포인트 대사 리포트의 한 줄 안에서 적립, 사용, 소멸과 잔액이 맞지 않는다

중급SELECT이커머스530 / 700

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

상황 ​

이커머스 회계팀은 매월 1일 00:00에 「포인트 대사 리포트」를 받아 포인트 부채를 확인한다. 리포트는 고객마다 적립, 사용, 소멸 합계와 잔액을 한 줄에 보여 준다. 같은 시각에 「포인트 소멸 배치」도 돈다. 회계팀이 물었다. "한 줄 안에서 적립에서 사용과 소멸을 빼면 잔액이 되어야 하는데 37명이 안 맞아요. SQL 한 문장이면 한 시점이라고 들었는데요?" 낮에 다시 뽑으면 모두 맞는다.

테이블과 인덱스 ​

  • customers: 5,230행 · 51블록 · PK customer_id
  • point_transactions: 45,000행 · 210블록 · PK tx_id만 있다 · tx_type은 EARN, USE, EXPIRE · 이 문제에서 amount는 부호 없이 적고 tx_type으로 더할지 뺄지를 정한다
  • 함수 fn_point_sum(고객, 종류)와 fn_point_bal(고객)은 화면 여러 곳이 쓰는 공용 함수다.
  • 「포인트 소멸 배치」: 매월 1일 00:00에 유효기간이 지난 적립을 고객마다 EXPIRE 한 행으로 넣고 고객마다 COMMIT한다. 이번 달 대상은 1,240명이고 약 40초 걸렸다.

원본 SQL ​

sql
-- 공용 함수 (둘 다 point_transactions 를 읽는다)
CREATE OR REPLACE FUNCTION fn_point_sum(p_cus NUMBER, p_tp VARCHAR2) RETURN NUMBER IS
  v NUMBER;
BEGIN
  SELECT NVL(SUM(amount), 0) INTO v
    FROM point_transactions
   WHERE customer_id = p_cus AND tx_type = p_tp;
  RETURN v;
END;

CREATE OR REPLACE FUNCTION fn_point_bal(p_cus NUMBER) RETURN NUMBER IS
  v NUMBER;
BEGIN
  SELECT NVL(SUM(CASE tx_type WHEN 'EARN' THEN amount ELSE -amount END), 0) INTO v
    FROM point_transactions
   WHERE customer_id = p_cus;
  RETURN v;
END;

-- 「포인트 대사 리포트」 (매월 1일 00:00:05)
SELECT c.customer_id, c.name, c.grade,
       fn_point_sum(c.customer_id, 'EARN')   AS earn_amt,
       fn_point_sum(c.customer_id, 'USE')    AS use_amt,
       fn_point_sum(c.customer_id, 'EXPIRE') AS expire_amt,
       fn_point_bal(c.customer_id)           AS bal_amt
  FROM customers c
 ORDER BY c.customer_id;

실행계획 학습용 예시 ​

xplan
[리포트 — ALLSTATS LAST (11월 1일 00:00:05)]
--------------------------------------------------------------------------------------------
| Id  | Operation           | Name      | Starts | E-Rows | A-Rows |    A-Time   | Buffers |
--------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |           |      1 |        |   5230 | 00:00:09.41 |      51 |
|   1 |  SORT ORDER BY      |           |      1 |   5230 |   5230 | 00:00:09.41 |      51 |
|   2 |   TABLE ACCESS FULL | CUSTOMERS |      1 |   5230 |   5230 | 00:00:09.36 |      51 |
--------------------------------------------------------------------------------------------

[함수 안의 SQL — V$SQL (같은 실행)]
SQL_TEXT                                                          EXECUTIONS   BUFFER_GETS   ELAPSED
SELECT NVL(SUM(AMOUNT), 0) FROM POINT_TRANSACTIONS
 WHERE CUSTOMER_ID = :B2 AND TX_TYPE = :B1                           15,690     3,294,900    6.9초
SELECT NVL(SUM(CASE TX_TYPE WHEN 'EARN' THEN AMOUNT ELSE -AMOUNT END), 0)
  FROM POINT_TRANSACTIONS WHERE CUSTOMER_ID = :B1                     5,230     1,098,300    2.3초
  (실행 한 번에 TABLE ACCESS FULL 210블록)

[고객 2,417 의 한 줄 — 함수가 불린 순간]
00:00:07.204   fn_point_sum EARN    → 12,000
00:00:07.205   fn_point_sum USE     →  3,000
00:00:07.206   fn_point_sum EXPIRE  →      0
00:00:07.206   소멸 배치가 고객 2,417 에 EXPIRE 2,500 INSERT · COMMIT
00:00:07.207   fn_point_bal         →  6,500        ← 12,000 - 3,000 - 0 = 9,000 이어야 한 줄이 맞는다

[11월 1일 리포트]  5,230행 · 한 줄 안에서 맞지 않는 고객 37명 · 14:00 에 다시 뽑으면 0명

문제 ​

  1. 리포트가 한 문장인데도 한 줄 안의 숫자가 서로 다른 시점의 것이 되는 이유를 설명하시오. 실행계획의 Buffers 51과 V$SQL의 숫자가 왜 이렇게 다른지도 쓰시오.
  2. 한 줄 안의 숫자가 같은 시점의 것이 되도록 리포트를 고치시오. 고친 뒤에도 회계팀이 알아야 할 점이 있다면 쓰시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자