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

같은 '마지막 주문' 뷰를 쓰는데 고객 상세는 0.001초, VIP 목록 스무 줄은 0.2초가 걸린다

실전SELECT한빛유통207 / 700

상황 ​

작년에 만든 뷰 V_CUS_LAST_ORD는 고객마다 마지막 주문 한 건을 돌려준다. 이 뷰를 세 곳이 쓴다. [S1] 고객 상세 화면(고객 한 명, 하루 2만 번), [S2] CRM의 VIP 목록(A 등급 20명, 하루 3천 번), [S3] 야간 배치의 고객 요약 적재(전 고객 2,000명, 하루 한 번)다. S2가 AWR의 읽은 블록 수 상위에 올라왔다. 개발팀은 "S1과 똑같은 뷰를 쓰는데 왜 S2만 느린지 모르겠다"라고 한다. 박팀장은 세 곳을 다 보고 고칠 것과 그대로 둘 것을 숫자로 정해 달라고 했다. 정산팀은 뷰가 같은 시각의 주문 두 건 가운데 어느 것을 '마지막'으로 내는지 정해져 있지 않다는 점도 지적했다.

테이블과 인덱스 ​

  • T_ORD: 100,000행 · 896블록 · PK_T_ORD (ORD_SEQ) · IX_T_ORD_CUS_DT (CUS_ID, ORD_DT) (고객 상담 화면용, BLEVEL 1, 리프 약 330블록) · ORD_DT 시분초 있음 · 고객마다 50건이 테이블 전체에 흩어져 있다
  • M_CUS: 2,000행 · 30블록 · PK_M_CUS (CUS_ID) · A 등급 20명 · CUS_GD에 히스토그램 있음
  • 업무 규칙: 마지막 주문은 주문 시각이 가장 늦은 주문, 같은 시각이면 주문번호가 큰 주문 · 통계 최신

원본 SQL ​

sql
-- 공용 뷰
CREATE OR REPLACE VIEW V_CUS_LAST_ORD AS
SELECT CUS_ID, ORD_SEQ, ORD_DT, ORD_ST, ORD_AMT
  FROM (SELECT o.CUS_ID, o.ORD_SEQ, o.ORD_DT, o.ORD_ST, o.ORD_AMT,
               ROW_NUMBER() OVER (PARTITION BY o.CUS_ID ORDER BY o.ORD_DT DESC) AS RN
          FROM T_ORD o)
 WHERE RN = 1;

-- [S1] 고객 상세: 고객 한 명 (하루 2만 번)
SELECT c.CUS_ID, c.CUS_NM, v.ORD_SEQ, v.ORD_DT, v.ORD_AMT
  FROM M_CUS c, V_CUS_LAST_ORD v
 WHERE c.CUS_ID = :cus
   AND v.CUS_ID = c.CUS_ID;

-- [S2] VIP 목록: A 등급 20명 (하루 3천 번)
SELECT c.CUS_ID, c.CUS_NM, v.ORD_SEQ, v.ORD_DT, v.ORD_ST, v.ORD_AMT
  FROM M_CUS c, V_CUS_LAST_ORD v
 WHERE c.CUS_GD = 'A'
   AND v.CUS_ID = c.CUS_ID
 ORDER BY v.ORD_DT DESC;

-- [S3] 야간 고객 요약: 전 고객 (하루 한 번)
INSERT INTO T_CUS_SUMMARY (CUS_ID, LAST_ORD_SEQ, LAST_ORD_DT, LAST_ORD_AMT)
SELECT CUS_ID, ORD_SEQ, ORD_DT, ORD_AMT FROM V_CUS_LAST_ORD;

실행계획 학습용 예시 ​

xplan
[S1] Buffers 5 · INDEX RANGE SCAN DESCENDING (IX_T_ORD_CUS_DT) → WINDOW NOSORT STOPKEY · 0.001초

[S2]
-----------------------------------------------------------------------------------------------------------------
| Id  | Operation                 | Name   | Starts | E-Rows | A-Rows |   A-Time   | Buffers |  Used-Mem |
-----------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT          |        |      1 |        |     20 |00:00:00.21 |     926 |           |
|   1 |  SORT ORDER BY            |        |      1 |     20 |     20 |00:00:00.21 |     926 |  2048 (0) |
|*  2 |   HASH JOIN               |        |      1 |     20 |     20 |00:00:00.21 |     926 | 1269K (0) |
|*  3 |    TABLE ACCESS FULL      | M_CUS  |      1 |     20 |     20 |00:00:00.01 |      30 |           |
|*  4 |    VIEW                   |        |      1 |    100K|   2000 |00:00:00.20 |     896 |           |
|*  5 |     WINDOW SORT PUSHED RANK|       |      1 |    100K|   2000 |00:00:00.19 |     896 | 4614K (0) |
|   6 |      TABLE ACCESS FULL    | T_ORD  |      1 |    100K|    100K|00:00:00.01 |     896 |           |
-----------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   2 - access("V"."CUS_ID"="C"."CUS_ID")
   3 - filter("C"."CUS_GD"='A')
   4 - filter("RN"=1)
   5 - filter(ROW_NUMBER() OVER ( PARTITION BY "O"."CUS_ID" ORDER BY INTERNAL_FUNCTION("O"."ORD_DT") DESC )<=1)

[S3] Buffers 926 + 적재 · WINDOW SORT PUSHED RANK 100K행 · 0.24초 (하루 한 번)

문제 ​

  1. S1과 S2가 같은 뷰를 쓰는데 일량이 180배 넘게 다른 까닭을 설명하시오. 뷰 안의 ROW_NUMBER가 S2의 조인 조건을 받지 못하는 이유를 포함하시오.
  2. S2를 고객 20명마다 마지막 주문 한 건만 읽도록 고치고, 업무 규칙(같은 시각이면 주문번호가 큰 주문)이 늘 지켜지도록 뷰와 인덱스를 정리하시오.
  3. S3도 S2처럼 고객마다 한 건씩 찾게 바꿀지 판단하고, 세 곳에 대한 결정과 그 대가(인덱스 교체, 다른 화면에 주는 영향)를 숫자로 정리하시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자