Skip to content
튜닝 연습소트 튜닝과 페이징
SORT-020

판매 50만 건을 5,000건씩 넘기는 연동 배치, 뒤쪽 묶음일수록 한 번이 길어진다

중급PLSQL유통325 / 700

상황 ​

본사 DW 연동 배치 P_SEND_CF10은 매일 02시에 판매(CF10) 전체를 DW로 보낸다. DW 쪽 수신 API가 한 번에 5,000건까지만 받아서, 개발자는 판매번호 순으로 정렬해 5,000건씩 OFFSET으로 잘라 보냈다. 첫 묶음은 0.01초인데 마지막 묶음은 0.7초가 걸리고, DB 쪽에서만 35초를 쓴다. DW 담당자는 다른 문제도 알려 왔다. "판매 몇 건이 같은 날 두 번 들어오고, 어떤 판매는 그날 빠졌다가 다음 날 들어온다." 판매는 야간에도 들어오고(POS 지연 전송분은 원래 판매일의 판매번호를 받는다), 취소된 판매는 지워진다.

테이블과 인덱스 ​

  • CF10 판매: 500,000행 · 4,583블록 · SLE_NO VARCHAR2(15) ('20231231-0612', 앞 8자리가 판매일자) · SLE_DT · SLE_HMS · SLE_DOW · SHP_NO
  • CF10_PK (SLE_NO): 높이 3 · 리프 약 1,700블록 · 판매번호 순서와 쌓인 순서가 거의 같다(클러스터링 팩터 9,340).
  • DW는 받은 행을 판매번호로 다시 정렬해 적재하므로 보내는 순서는 상관없다. 한 묶음은 5,000건 이하면 된다. API 호출 한 번은 약 0.2초다.
  • UNDO_RETENTION은 900초다. 통계는 최신이다.

원본 SQL ​

sql
PROCEDURE P_SEND_CF10 IS
  TYPE t_tab IS TABLE OF CF10%ROWTYPE;
  v_buf  t_tab;
  v_cnt  NUMBER;
BEGIN
  SELECT COUNT(*) INTO v_cnt FROM CF10;
  FOR k IN 0 .. CEIL(v_cnt / 5000) - 1 LOOP
    SELECT *
      BULK COLLECT INTO v_buf
      FROM CF10
     ORDER BY SLE_NO
     OFFSET k * 5000 ROWS FETCH NEXT 5000 ROWS ONLY;
    DW_API.SEND_CHUNK(v_buf);
  END LOOP;
END;

실행계획 학습용 예시 · TKPROF 요약 ​

xplan
[TKPROF 요약 · P_SEND_CF10 한 번]
SELECT COUNT(*) FROM CF10

call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Execute      1      0.00       0.00          0          0          0           0
Fetch        1      0.05       0.05          0       1712          0           1

SELECT * FROM CF10 ORDER BY SLE_NO OFFSET :B1 * 5000 ROWS FETCH NEXT 5000 ROWS ONLY

call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        1      0.00       0.00          0          0          0           0
Execute    100      0.01       0.01          0          0          0           0
Fetch      100     34.62      35.18          0     557389          0      500000

[마지막 묶음 · k = 99]   (첫 묶음 k = 0 은 같은 모양 · A-Rows 5,000 · Buffers 112)
-----------------------------------------------------------------------------------------------------
| Id  | Operation                      | Name    | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
-----------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT               |         |      1 |        |   5000 |00:00:00.69 |   11037 |
|*  1 |  VIEW                          |         |      1 |   5000 |   5000 |00:00:00.69 |   11037 |
|*  2 |   WINDOW NOSORT STOPKEY        |         |      1 |   5000 |    500K|00:00:00.62 |   11037 |
|   3 |    TABLE ACCESS BY INDEX ROWID | CF10    |      1 |    500K|    500K|00:00:00.41 |   11037 |
|   4 |     INDEX FULL SCAN            | CF10_PK |      1 |   5000 |    500K|00:00:00.08 |    1697 |
-----------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   1 - filter(("from$_subquery$_002"."rowlimit_$$_rownumber"<=CASE  WHEN (:B1*5000>=0) THEN
              FLOOR(TO_NUMBER(TO_CHAR(:B1*5000)))+5000 ELSE 5000 END  AND
              "from$_subquery$_002"."rowlimit_$$_rownumber">:B1*5000))
   2 - filter(ROW_NUMBER() OVER ( ORDER BY "CF10"."SLE_NO")<=CASE  WHEN (:B1*5000>=0) THEN
              FLOOR(TO_NUMBER(TO_CHAR(:B1*5000)))+5000 ELSE 5000 END )

문제 ​

  1. SELECT 한 번이 읽는 양이 묶음 번호에 따라 어떻게 바뀌는지 쓰고, 전체 557,389블록이 나오는 계산 근거를 보이시오.
  2. DW 쪽이 말한 "두 번 들어온 판매"와 "빠진 판매"가 이 배치의 어느 부분 때문인지 설명하시오.
  3. 같은 일을 하면서 CF10을 한 번만 읽고, 묶음 사이에 판매가 들어오거나 지워져도 빠지거나 겹치지 않도록 배치를 고치시오. 고친 방식의 위험도 쓰시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자