Skip to content
튜닝 연습조인 튜닝
JOIN-030

2년 치 월별 매장 매출을 다시 만드는 배치가 판매상품 테이블을 24번 통째로 읽는다

중급PLSQL유통180 / 700

상황 ​

지난주 판매 데이터 정정이 있어서 2022년 1월부터 2023년 12월까지의 「월별 매장 매출」 요약을 다시 만들어야 한다. 처음 작성자는 "한 번에 하면 무거우니 한 달씩 나눠서" 월 루프를 짰다. 배치는 40초 가까이 걸리고, 그동안 다른 배치의 디스크 읽기가 느려진다. 조민우는 "한 달 SQL은 해시 조인이고 한 번에 1초 남짓이라 괜찮아 보인다"라고 한다.

테이블과 인덱스 ​

  • CF10 판매: 500,000행 · 4,583블록 · CF10_PK (SLE_NO) · IX_CF10_SLE_DT (SLE_DT) (03화) · 한 달 약 2만 1천 건 · 판매 매장 5곳
  • CF11 판매상품: 1,096,708행 · 8,204블록 · CF11_PK (SLE_NO, ITM_NO) · 배치 시간대에는 버퍼 캐시에 오래 남지 않아 읽을 때마다 디스크에서 읽는다
  • CF_SHP_MON_SLS 월별 매장 매출 요약 (이 문제에서 쓰는 파생 테이블): SLE_YM · SHP_NO · SLE_CNT · SLE_AMT · 120행 (24개월 × 5곳) · 인덱스 없음
  • 통계 최신 · 아래는 루프 한 바퀴(2023년 6월)의 실행계획과 배치 전체의 TKPROF 요약

원본 SQL ​

sql
BEGIN
  DELETE FROM CF_SHP_MON_SLS WHERE SLE_YM BETWEEN '202201' AND '202312';
  FOR m IN (SELECT TO_CHAR(ADD_MONTHS(DATE '2022-01-01', LEVEL - 1), 'YYYYMM') AS ym
              FROM DUAL CONNECT BY LEVEL <= 24) LOOP
    INSERT INTO CF_SHP_MON_SLS (SLE_YM, SHP_NO, SLE_CNT, SLE_AMT)
    SELECT m.ym, a.SHP_NO, COUNT(DISTINCT a.SLE_NO), SUM(b.SLE_QTY * b.SLE_UPRC)
      FROM CF10 a, CF11 b
     WHERE a.SLE_DT LIKE m.ym || '%'
       AND b.SLE_NO = a.SLE_NO
     GROUP BY a.SHP_NO;
  END LOOP;
  COMMIT;
END;

실행계획 학습용 예시 · 한 달 실행계획은 교재 03화 실측 규모 ​

xplan
[루프 한 바퀴 · 2023년 6월]
------------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                              | Name           | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads |
------------------------------------------------------------------------------------------------------------------------------
|   0 | INSERT STATEMENT                       |                |      1 |        |      0 |00:00:01.63 |    8566 |  8074 |
|   1 |  LOAD TABLE CONVENTIONAL               | CF_SHP_MON_SLS |      1 |        |      0 |00:00:01.63 |    8566 |  8074 |
|   2 |   SORT GROUP BY                        |                |      1 |      5 |      5 |00:00:01.63 |    8552 |  8074 |
|*  3 |    HASH JOIN                           |                |      1 |   5865 |  45935 |00:00:01.52 |    8552 |  8074 |
|   4 |     TABLE ACCESS BY INDEX ROWID BATCHED| CF10           |      1 |   2673 |  20953 |00:00:00.02 |     478 |     0 |
|*  5 |      INDEX RANGE SCAN                  | IX_CF10_SLE_DT |      1 |   2673 |  20953 |00:00:00.01 |      62 |     0 |
|   6 |     TABLE ACCESS FULL                  | CF11           |      1 |   1096K|   1096K|00:00:01.21 |    8074 |  8074 |
------------------------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   3 - access("B"."SLE_NO"="A"."SLE_NO")
   5 - access("A"."SLE_DT" LIKE :B1||'%')
       filter("A"."SLE_DT" LIKE :B1||'%')

[TKPROF · 루프 안의 INSERT]
call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        1      0.00       0.00          0          0          0           0
Execute     24      5.02      39.14     193776     205248        171         120
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total       25      5.02      39.14     193776     205248        171         120

문제 ​

  1. 한 달 실행계획과 TKPROF 요약을 함께 보고, 배치 전체의 일량이 어디서 나오는지 설명하시오. "한 달씩 나누면 가볍다"는 판단이 이 SQL에서 왜 틀렸는지도 서술하시오.
  2. 같은 결과를 내도록 배치를 고치시오. 한 번에 처리할 때 메모리가 감당되는지 근거를 쓰고, 루프를 두고 고치는 다른 안과 비교하시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자