Skip to content
튜닝 연습서브쿼리와 쿼리 변환
SUBQ-055

영수증 합계 뷰를 쓰는 화면 셋 가운데 월간 객단가 리포트만 TEMP를 쓰며 1.4초 걸린다

실전SELECT유통290 / 700

상황 ​

지난달 영수증 합계 함수 사건(뷰가 합쳐지면서 함수가 세 번씩 호출된 일) 뒤에 오과장이 코딩 가이드에 "인라인 뷰와 집계 뷰를 조인할 때는 NO_MERGE를 붙인다"라는 조문을 넣었다. 개발팀은 공통 뷰 V_SLE_RCPT(영수증별 품목 수와 금액)를 쓰는 SQL 세 개에 이 힌트를 붙였다. (A) 「영수증 상세」는 영수증 번호로 하루 3,000번, (B) 「매장 일별 영수증」은 매장과 날짜로 하루 2,000번, (C) 「월간 매장 객단가」 리포트는 하루 30번 돈다. (A)와 (B)는 그대로인데 (C)만 0.2초에서 1.4초로 늘고 TEMP를 쓰기 시작했다. 오과장은 조문을 지킬지, 고칠지 숫자로 판단해 달라고 했다.

테이블과 인덱스 ​

  • CF10 판매(영수증): 500,000행 · 4,583블록 · CF10_PK (SLE_NO) · IX_CF10_SLE_DT (SLE_DT) · 하루 약 700건, 2023년 12월 20,864건 · 매장 6곳
  • CF11 판매상품: 1,096,708행 · 8,204블록 · CF11_PK (SLE_NO, ITM_NO) 높이 3 · 영수증 한 장에 평균 2.2줄(한 블록 안) · 12월 판매상품 45,935줄
  • 영수증 50만 장 모두 판매상품이 한 줄 이상 있다 · 세션 PGA 작업 영역은 자동 관리이고, 집계 하나에 쓸 수 있는 메모리는 약 20MB다 · 통계 최신

원본 SQL ​

sql
-- 공통 뷰: 영수증별 품목 수와 금액
CREATE OR REPLACE VIEW V_SLE_RCPT AS
SELECT SLE_NO, COUNT(*) AS ITM_CNT, SUM(SLE_QTY * SLE_UPRC) AS RCPT_AMT
  FROM CF11
 GROUP BY SLE_NO;

-- (A) 영수증 상세 (하루 3,000번)
SELECT /*+ NO_MERGE(v) */ h.SLE_NO, h.SLE_DT, h.SLE_HMS, v.ITM_CNT, v.RCPT_AMT
  FROM CF10 h, V_SLE_RCPT v
 WHERE h.SLE_NO = :sle_no
   AND v.SLE_NO = h.SLE_NO;

-- (B) 매장 일별 영수증 (하루 2,000번)
SELECT /*+ NO_MERGE(v) */ h.SLE_NO, h.SLE_HMS, v.ITM_CNT, v.RCPT_AMT
  FROM CF10 h, V_SLE_RCPT v
 WHERE h.SHP_NO = :shp
   AND h.SLE_DT = :dt
   AND v.SLE_NO = h.SLE_NO;

-- (C) 월간 매장 객단가 (하루 30번)
SELECT /*+ NO_MERGE(v) */ h.SHP_NO, COUNT(*) AS RCPT_CNT, ROUND(AVG(v.RCPT_AMT)) AS AVG_AMT
  FROM CF10 h, V_SLE_RCPT v
 WHERE h.SLE_DT BETWEEN :m1 AND :m2
   AND v.SLE_NO = h.SLE_NO
 GROUP BY h.SHP_NO;

실행계획 학습용 예시 ​

xplan
[C · :m1 = '20231201', :m2 = '20231231']
------------------------------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                            | Name           | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads | Writes | Used-Mem | Used-Tmp|
------------------------------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                     |                |      1 |        |      6 |00:00:01.42 |    8494 |  3318 |   3318 |          |         |
|   1 |  HASH GROUP BY                       |                |      1 |      6 |      6 |00:00:01.42 |    8494 |  3318 |   3318 | 1036K (0)|         |
|*  2 |   HASH JOIN                          |                |      1 |  20833 |  20864 |00:00:01.42 |    8494 |  3318 |   3318 | 2147K (0)|         |
|   3 |    TABLE ACCESS BY INDEX ROWID BATCHED| CF10          |      1 |  20833 |  20864 |00:00:00.01 |     290 |     0 |      0 |          |         |
|*  4 |     INDEX RANGE SCAN                 | IX_CF10_SLE_DT |      1 |  20833 |  20864 |00:00:00.01 |      98 |     0 |      0 |          |         |
|   5 |    VIEW                              | V_SLE_RCPT     |      1 |    500K|    500K|00:00:01.35 |    8204 |  3318 |   3318 |          |         |
|   6 |     HASH GROUP BY                    |                |      1 |    500K|    500K|00:00:01.30 |    8204 |  3318 |   3318 |   20M (1)|     27M |
|   7 |      TABLE ACCESS FULL               | CF11           |      1 |   1096K|   1096K|00:00:00.06 |    8204 |     0 |      0 |          |         |
------------------------------------------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   2 - access("V"."SLE_NO"="H"."SLE_NO")
   4 - access("H"."SLE_DT">=:M1 AND "H"."SLE_DT"<=:M2)

[B · :shp = 1003, :dt = '20230601']
-----------------------------------------------------------------------------------------------------------------
| Id  | Operation                            | Name           | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
-----------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                     |                |      1 |        |    121 |00:00:00.01 |     496 |
|   1 |  NESTED LOOPS                        |                |      1 |    118 |    121 |00:00:00.01 |     496 |
|*  2 |   TABLE ACCESS BY INDEX ROWID BATCHED| CF10           |      1 |    118 |    121 |00:00:00.01 |      12 |
|*  3 |    INDEX RANGE SCAN                  | IX_CF10_SLE_DT |      1 |    706 |    706 |00:00:00.01 |       5 |
|   4 |   VIEW PUSHED PREDICATE              | V_SLE_RCPT     |    121 |      1 |    121 |00:00:00.01 |     484 |
|   5 |    SORT GROUP BY                     |                |    121 |      1 |    121 |00:00:00.01 |     484 |
|   6 |     TABLE ACCESS BY INDEX ROWID      | CF11           |    121 |      2 |    266 |00:00:00.01 |     484 |
|*  7 |      INDEX RANGE SCAN                | CF11_PK        |    121 |      2 |    266 |00:00:00.01 |     363 |
-----------------------------------------------------------------------------------------------------------------
   2 - filter("H"."SHP_NO"=:SHP)
   3 - access("H"."SLE_DT"=:DT)
   7 - access("SLE_NO"="H"."SLE_NO")

[A]  CF10_PK와 CF11_PK로 영수증 한 장 · Buffers 8 · 7번 줄에 해당하는 조건은 access("SLE_NO"=:SLE_NO)

문제 ​

  1. 세 SQL에서 바깥 조건이 뷰 안으로 각각 어떻게 들어갔는지(조건절 이행, JPPD, 들어가지 못함) 실행계획으로 설명하시오. 같은 NO_MERGE인데 (A)와 (B)는 멀쩡하고 (C)만 느려진 이유도 설명하시오.
  2. (C)를 고치고, 고친 뒤의 처리 순서와 일량(Buffers, TEMP, 시간)을 쓰시오. (C)에서 JPPD를 쓰면 어떻게 되는지도 숫자로 쓰시오.
  3. 코딩 가이드의 조문을 어떻게 고칠지 쓰시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자