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

판매 뷰로 만든 「상품별 누적 판매」가 쓰지도 않는 판매 헤더 50만 건을 함께 읽는다

실전DDL유통221 / 700

상황 ​

본사 영업기획팀의 BI 도구는 시맨틱 계층의 뷰 V_SLE_DTL 하나로 판매 데이터를 본다. 사용자가 칸을 고르면 BI 도구가 그 칸만 SELECT 목록에 넣어 뷰에 SQL을 보낸다. 자주 쓰는 리포트는 아래 넷이고, Q1을 열면 1초 넘게 걸린다. 선배는 "쓰지 않는 테이블은 옵티마이저가 지우게 할 수 있어. 다만 이 DB에 무엇이 없는지부터 봐"라고 했다. 오과장은 제약을 거는 것은 좋지만 POS 판매 저장이 느려지거나 멈추면 안 된다고 했다.

테이블과 인덱스 ​

  • 뷰 V_SLE_DTL: CF11 b JOIN CF10 a ON a.SLE_NO = b.SLE_NO JOIN CF07 s ON s.SHP_NO = a.SHP_NO JOIN CF01 p ON p.ITM_NO = b.ITM_NO LEFT JOIN CF02 g ON g.CTG_NO = p.CTG_NO · 내보내는 칸: b의 칸 전부와 SLE_AMT(= SLE_QTY * SLE_UPRC), a.SLE_DT, a.SHP_NO, s.SHP_NM, p.ITM_NM, p.PTNR_NO, g.CTG_NM
  • CF11 1,096,708행 · 8,204블록 · CF11_PK (SLE_NO, ITM_NO) / CF10 500,000행 · 4,583블록 · CF10_PK (SLE_NO) · IX_CF10_SLE_DT (SLE_DT) · SHP_NO는 NULL을 허용하지만 NULL인 행은 없다
  • CF07 6행 · CF07_PK (SHP_NO) / CF01 109행 · CF01_PK (ITM_NO) / CF02 18행 · CF02_PK (CTG_NO)
  • 본사 레거시라서 PK 말고는 제약이 없다(FK 0개) · CF11.ITM_NO나 CF10.SHP_NO로 시작하는 인덱스는 없다
  • 어제 점검: 판매 헤더가 없는 판매상품 0줄, 매장 마스터에 없는 판매 0건, 상품 마스터에 없는 판매상품 412줄(2023년 3월 시즌 메뉴 2개를 번호를 바꿔 다시 등록하면서 옛 번호를 CF01에서 지웠다) · 통계 최신

원본 SQL ​

sql
-- [Q1] 상품별 누적 판매 (상품팀, 하루 40번)
SELECT ITM_NO, ITM_NM, SUM(SLE_QTY) AS qty, SUM(SLE_AMT) AS amt
  FROM V_SLE_DTL
 GROUP BY ITM_NO, ITM_NM;

-- [Q2] 매장별 일 매출 (영업기획, 하루 20번)
SELECT SHP_NM, SLE_DT, SUM(SLE_AMT) AS amt
  FROM V_SLE_DTL
 WHERE SLE_DT BETWEEN :v_from AND :v_to
 GROUP BY SHP_NM, SLE_DT;

-- [Q3] 영수증 상세 (매장 POS 재출력, 하루 3,000번)
SELECT ITM_NO, ITM_NM, SLE_QTY, SLE_UPRC, SLE_AMT
  FROM V_SLE_DTL
 WHERE SLE_NO = :v_sle_no;

-- [Q4] 카테고리별 월 매출 (MD, 하루 60번)
SELECT CTG_NM, SUM(SLE_AMT) AS amt
  FROM V_SLE_DTL
 WHERE SLE_DT BETWEEN :v_from AND :v_to
 GROUP BY CTG_NM;

실행계획 학습용 예시 ​

xplan
[Q1] 상품별 누적 판매
------------------------------------------------------------------------------------------------------
| Id  | Operation               | Name | Starts | E-Rows | A-Rows |   A-Time   | Buffers |  Used-Mem |
------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT        |      |      1 |        |     88 |00:00:01.12 |   12796 |           |
|   1 |  HASH GROUP BY          |      |      1 |    109 |     88 |00:00:01.12 |   12796 | 1123K (0) |
|*  2 |   HASH JOIN             |      |      1 |  1096K |  1096K |00:00:00.98 |   12796 | 1290K (0) |
|   3 |    TABLE ACCESS FULL    | CF01 |      1 |    109 |    109 |00:00:00.01 |       5 |           |
|*  4 |    HASH JOIN            |      |      1 |  1096K |  1096K |00:00:00.71 |   12791 |   33M (0) |
|*  5 |     HASH JOIN           |      |      1 |   500K |   500K |00:00:00.18 |    4587 | 1156K (0) |
|   6 |      TABLE ACCESS FULL  | CF07 |      1 |      6 |      6 |00:00:00.01 |       4 |           |
|   7 |      TABLE ACCESS FULL  | CF10 |      1 |   500K |   500K |00:00:00.05 |    4583 |           |
|   8 |     TABLE ACCESS FULL   | CF11 |      1 |  1096K |  1096K |00:00:00.09 |    8204 |           |
------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   2 - access("P"."ITM_NO"="B"."ITM_NO")
   4 - access("A"."SLE_NO"="B"."SLE_NO")
   5 - access("S"."SHP_NO"="A"."SHP_NO")

[Q3] 영수증 상세 · 판매 한 건
-------------------------------------------------------------------------------------------------------------
| Id  | Operation                               | Name    | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
-------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                        |         |      1 |        |      3 |00:00:00.01 |      16 |
|   1 |  NESTED LOOPS                           |         |      1 |      2 |      3 |00:00:00.01 |      16 |
|   2 |   NESTED LOOPS                          |         |      1 |      2 |      3 |00:00:00.01 |      10 |
|   3 |    NESTED LOOPS                         |         |      1 |      1 |      1 |00:00:00.01 |       6 |
|   4 |     TABLE ACCESS BY INDEX ROWID         | CF10    |      1 |      1 |      1 |00:00:00.01 |       4 |
|*  5 |      INDEX UNIQUE SCAN                  | CF10_PK |      1 |      1 |      1 |00:00:00.01 |       3 |
|   6 |     TABLE ACCESS BY INDEX ROWID         | CF07    |      1 |      1 |      1 |00:00:00.01 |       2 |
|*  7 |      INDEX UNIQUE SCAN                  | CF07_PK |      1 |      1 |      1 |00:00:00.01 |       1 |
|   8 |    TABLE ACCESS BY INDEX ROWID BATCHED  | CF11    |      1 |      2 |      3 |00:00:00.01 |       4 |
|*  9 |     INDEX RANGE SCAN                    | CF11_PK |      1 |      2 |      3 |00:00:00.01 |       3 |
|  10 |   TABLE ACCESS BY INDEX ROWID           | CF01    |      3 |      1 |      3 |00:00:00.01 |       6 |
|* 11 |    INDEX UNIQUE SCAN                    | CF01_PK |      3 |      1 |      3 |00:00:00.01 |       3 |
-------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   5 - access("A"."SLE_NO"=:V_SLE_NO)
   7 - access("S"."SHP_NO"="A"."SHP_NO")
   9 - access("B"."SLE_NO"=:V_SLE_NO)
  11 - access("P"."ITM_NO"="B"."ITM_NO")

문제 ​

  1. 네 SQL마다 결과에 쓰는 테이블과, 쓰지 않는데 계획에 남는 테이블을 가리고 남는 이유를 쓰시오. CF02가 Q1과 Q3의 계획에 이미 없는 이유도 설명하시오.
  2. 쓰지 않는 조인이 계획에서 사라지도록 제약을 설계해 DDL로 작성하시오. 어제 점검의 412줄을 어떻게 처리할지 정하고, 그 결정이 리포트 숫자에 주는 영향도 쓰시오.
  3. 제약이 POS 판매 저장과 마스터 관리에 주는 영향을 쓰고, 검증 없이 RELY DISABLE NOVALIDATE로 거는 안과 비교하시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자