테마
한빛유통이 상담과 CRM 화면을 개편하면서 아래 네 SQL을 새로 만들었다. 개발 DB에는 PK만 있어서 네 화면 중 세 화면이 T_ORD를 FULL로 읽는다.
박팀장은 "화면마다 하나씩 만들지 말고, 같이 쓸 수 있게 꼭 필요한 만큼만 설계해 달라"라고 했다. 호출 빈도는 업무팀이 추정했다. 아래 실행계획은 Q1이 고객 CUS_000390, Q2가 2024-06-14와 상품 ITM00042, Q3이 2024-06-13, Q4가 상품 하나로 잰 것이다.
T_ORD: 100,000행 · 896블록 · PK_T_ORD (ORD_SEQ)만 있다 · 고객마다 50건(2년) · 하루 137건이 들어온다 · 하루치 주문은 테이블 전체에 흩어져 있다T_ORD_DET: 200,000행 · 896블록 · PK_T_ORD_DET (ORD_SEQ, ORD_DET_NO) · 주문당 1 ~ 3줄 · 상품마다 2,000줄M_CUS: 2,000행 · 30블록 · PK_M_CUS (CUS_ID) · A 등급 20명 / M_ITM: 100행 · 5블록 · PK_M_ITM (ITM_ID)T_ITM_EVL 상품평가: 5,000행 · 48블록 · PK_T_ITM_EVL (ITM_ID, EVL_LST_NO) · 상품마다 50건 · 평가 번호 순서와 평가일 순서는 다르다-- [Q1] 상담: 고객 한 명의 최근 6개월 주문 상세 (하루 5,000번)
SELECT o.ORD_SEQ, o.ORD_DT, o.ORD_ST, d.ORD_DET_NO, i.ITM_NM, d.ORD_QTY, d.UNT_PRC
FROM T_ORD o, T_ORD_DET d, M_ITM i
WHERE o.CUS_ID = :v_cus
AND o.ORD_DT >= :v_from
AND d.ORD_SEQ = o.ORD_SEQ
AND i.ITM_ID = d.ITM_ID
ORDER BY o.ORD_DT DESC, d.ORD_DET_NO;
-- [Q2] 품질: 특정 상품이 들어간 어제 주문과 그 고객 (하루 50번)
SELECT c.CUS_ID, c.CUS_NM, o.ORD_SEQ, o.ORD_DT, d.ORD_QTY
FROM T_ORD o, T_ORD_DET d, M_CUS c
WHERE o.ORD_DT >= :v_dt AND o.ORD_DT < :v_dt + 1
AND d.ORD_SEQ = o.ORD_SEQ
AND d.ITM_ID = :v_itm
AND c.CUS_ID = o.CUS_ID;
-- [Q3] CRM: VIP(A 등급) 고객의 오늘 주문 (10분마다, 하루 144번)
SELECT c.CUS_ID, c.CUS_NM, o.ORD_SEQ, o.ORD_DT, o.ORD_AMT
FROM M_CUS c, T_ORD o
WHERE c.CUS_GD = 'A'
AND o.CUS_ID = c.CUS_ID
AND o.ORD_DT >= :v_dt AND o.ORD_DT < :v_dt + 1;
-- [Q4] 상품 상세: 최근 평가 20건과 평가자 등급 (하루 3만 번)
SELECT e.EVL_DT, e.EVL_PT, c.CUS_NM, c.CUS_GD
FROM T_ITM_EVL e, M_CUS c
WHERE e.ITM_ID = :v_itm
AND c.CUS_ID = e.CUS_ID
ORDER BY e.EVL_DT DESC
FETCH FIRST 20 ROWS ONLY;
[Q1 · 지금]
-----------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-----------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 27 |00:00:00.01 | 964 |
| 1 | SORT ORDER BY | | 1 | 26 | 27 |00:00:00.01 | 964 |
| 2 | NESTED LOOPS | | 1 | 26 | 27 |00:00:00.01 | 964 |
| 3 | NESTED LOOPS | | 1 | 26 | 27 |00:00:00.01 | 937 |
| 4 | NESTED LOOPS | | 1 | 26 | 27 |00:00:00.01 | 935 |
|* 5 | TABLE ACCESS FULL | T_ORD | 1 | 13 | 13 |00:00:00.01 | 896 |
| 6 | TABLE ACCESS BY INDEX ROWID | T_ORD_DET | 13 | 2 | 27 |00:00:00.01 | 39 |
|* 7 | INDEX RANGE SCAN | PK_T_ORD_DET | 13 | 2 | 27 |00:00:00.01 | 26 |
|* 8 | INDEX UNIQUE SCAN | PK_M_ITM | 27 | 1 | 27 |00:00:00.01 | 2 |
| 9 | TABLE ACCESS BY INDEX ROWID | M_ITM | 27 | 1 | 27 |00:00:00.01 | 27 |
-----------------------------------------------------------------------------------------------------------
5 - filter(("O"."CUS_ID"=:V_CUS AND "O"."ORD_DT">=:V_FROM))
[Q2 · 지금]
----------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
----------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 13 |00:00:00.01 | 1335 |
| 1 | NESTED LOOPS | | 1 | 3 | 13 |00:00:00.01 | 1335 |
| 2 | NESTED LOOPS | | 1 | 3 | 13 |00:00:00.01 | 1322 |
| 3 | NESTED LOOPS | | 1 | 3 | 13 |00:00:00.01 | 1307 |
|* 4 | TABLE ACCESS FULL | T_ORD | 1 | 137 | 137 |00:00:00.01 | 896 |
|* 5 | TABLE ACCESS BY INDEX ROWID | T_ORD_DET | 137 | 1 | 13 |00:00:00.01 | 411 |
|* 6 | INDEX RANGE SCAN | PK_T_ORD_DET | 137 | 2 | 274 |00:00:00.01 | 274 |
|* 7 | INDEX UNIQUE SCAN | PK_M_CUS | 13 | 1 | 13 |00:00:00.01 | 15 |
| 8 | TABLE ACCESS BY INDEX ROWID | M_CUS | 13 | 1 | 13 |00:00:00.01 | 13 |
----------------------------------------------------------------------------------------------------------
4 - filter(("O"."ORD_DT">=:V_DT AND "O"."ORD_DT"<:V_DT+1))
5 - filter("D"."ITM_ID"=:V_ITM)
[Q3 · 지금]
---------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
---------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 13 |00:00:00.01 | 926 |
|* 1 | HASH JOIN | | 1 | 2 | 13 |00:00:00.01 | 926 |
|* 2 | TABLE ACCESS FULL | M_CUS | 1 | 20 | 20 |00:00:00.01 | 30 |
|* 3 | TABLE ACCESS FULL | T_ORD | 1 | 137 | 137 |00:00:00.01 | 896 |
---------------------------------------------------------------------------------------
1 - access("O"."CUS_ID"="C"."CUS_ID")
2 - filter("C"."CUS_GD"='A')
3 - filter(("O"."ORD_DT">=:V_DT AND "O"."ORD_DT"<:V_DT+1))
[Q4 · 지금]
-------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 20 |00:00:00.01 | 105 |
|* 1 | VIEW | | 1 | 20 | 20 |00:00:00.01 | 105 |
|* 2 | WINDOW SORT PUSHED RANK | | 1 | 50 | 20 |00:00:00.01 | 105 |
| 3 | NESTED LOOPS | | 1 | 50 | 50 |00:00:00.01 | 105 |
| 4 | NESTED LOOPS | | 1 | 50 | 50 |00:00:00.01 | 55 |
| 5 | TABLE ACCESS BY INDEX ROWID BATCHED | T_ITM_EVL | 1 | 50 | 50 |00:00:00.01 | 52 |
|* 6 | INDEX RANGE SCAN | PK_T_ITM_EVL | 1 | 50 | 50 |00:00:00.01 | 2 |
|* 7 | INDEX UNIQUE SCAN | PK_M_CUS | 50 | 1 | 50 |00:00:00.01 | 3 |
| 8 | TABLE ACCESS BY INDEX ROWID | M_CUS | 50 | 1 | 50 |00:00:00.01 | 50 |
-------------------------------------------------------------------------------------------------------------------
2 - filter(ROW_NUMBER() OVER ( ORDER BY INTERNAL_FUNCTION("E"."EVL_DT") DESC )<=20)
6 - access("E"."ITM_ID"=:V_ITM)
T_ORD (ORD_DT, CUS_ID))로 Q1 ~ Q3을 모두 처리하는 안과 비교해, 하루 일량과 적재 비용으로 판단을 서술하시오.