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

재고 이력에 전일 재고 칸을 채우는 UPDATE가 40만 행에 블록 319만 개를 읽는다

기초MERGE유통448 / 700

상황 ​

본사 레거시 유통의 물류팀은 매장 상품별로 하루 사이에 재고가 얼마나 줄었는지 보려고, 재고 이력 CF18_HIS에 전일 재고 칸(PRV_INV_QTY)을 만들었다. 2년 치를 채우는 UPDATE를 개발 서버에서 돌렸더니 11초 넘게 걸렸다. 물류팀은 재고 실사 정정이 들어올 때마다 이 문장을 다시 돌리겠다고 한다. 개발자: "앞 날짜의 재고를 가져오는 서브쿼리가 PK를 타니까 문제없을 줄 알았어요."

테이블과 인덱스 ​

  • CF18_HIS 일별상품재고 이력(이 문제를 위해 둔 파생 테이블, 일반 힙 테이블): CF18과 같은 컬럼(SHP_NO · ITM_NO NUMBER · INV_BS_DT VARCHAR2(8) · INV_QTY)에 PRV_INV_QTY NUMBER(지금은 모두 NULL)를 더했다 · 매장 5곳 × 상품 109개 × 730일 = 397,850행 · 1,720블록
  • PK CF18_HIS_PK (SHP_NO, ITM_NO, INV_BS_DT) · BLEVEL 2. 다른 인덱스는 없다.
  • 매장 상품마다 첫날(2022-01-01)은 앞 날짜가 없다(545행). 칸 자리를 남겨 두고(PCTFREE 20) 만든 테이블이라 칸을 채워도 행 이전은 생기지 않는다. 통계는 최신이다.

원본 SQL ​

sql
UPDATE CF18_HIS h
   SET h.PRV_INV_QTY =
       (SELECT p.INV_QTY                                   -- 같은 매장 · 상품의 바로 앞 날짜 재고
          FROM CF18_HIS p
         WHERE p.SHP_NO    = h.SHP_NO
           AND p.ITM_NO    = h.ITM_NO
           AND p.INV_BS_DT = (SELECT MAX(q.INV_BS_DT)
                                FROM CF18_HIS q
                               WHERE q.SHP_NO    = h.SHP_NO
                                 AND q.ITM_NO    = h.ITM_NO
                                 AND q.INV_BS_DT < h.INV_BS_DT));
COMMIT;

실행계획 학습용 예시 · 이력 테이블은 CF18을 2년 치로 쌓았다고 둔 파생 테이블 ​

xplan
--------------------------------------------------------------------------------------------------------
| Id  | Operation                       | Name        | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
--------------------------------------------------------------------------------------------------------
|   0 | UPDATE STATEMENT                |             |      1 |        |      0 |00:00:11.42 |   3191K |
|   1 |  UPDATE                         | CF18_HIS    |      1 |        |      0 |00:00:11.42 |   3191K |
|   2 |   TABLE ACCESS FULL             | CF18_HIS    |      1 |    397K|    397K|00:00:00.05 |    1720 |
|   3 |   TABLE ACCESS BY INDEX ROWID   | CF18_HIS    |    397K|      1 |    397K|00:00:06.88 |   2784K |
|*  4 |    INDEX UNIQUE SCAN            | CF18_HIS_PK |    397K|      1 |    397K|00:00:05.61 |   2387K |
|   5 |     SORT AGGREGATE              |             |    397K|      1 |    397K|00:00:02.47 |   1194K |
|   6 |      FIRST ROW                  |             |    397K|      1 |    397K|00:00:02.03 |   1194K |
|*  7 |       INDEX RANGE SCAN (MIN/MAX)| CF18_HIS_PK |    397K|      1 |    397K|00:00:01.71 |   1194K |
--------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   4 - access("P"."SHP_NO"=:B1 AND "P"."ITM_NO"=:B2 AND "P"."INV_BS_DT"=
              (SELECT MAX("Q"."INV_BS_DT") FROM "CF18_HIS" "Q" WHERE "Q"."INV_BS_DT"<:B3 AND "Q"."ITM_NO"=:B4 AND "Q"."SHP_NO"=:B5))
   7 - access("Q"."SHP_NO"=:B1 AND "Q"."ITM_NO"=:B2 AND "Q"."INV_BS_DT"<:B3)

[세션 통계]  갱신된 행 397,850 · 경과 11.4초

문제 ​

  1. Buffers 3,191K가 어디서 나오는지 실행계획의 Starts로 설명하시오.
  2. 테이블을 한 번만 읽어 같은 결과를 내도록 다시 작성하시오. 매장 상품의 첫날 행과, 재고를 세지 않아 날짜가 빠진 경우에도 결과가 원본과 같은지 쓰시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자