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

새로 만든 주문 화면 네 개 가운데 세 개가 주문 테이블을 통째로 읽는다

실전DDL한빛유통168 / 700

상황 ​

한빛유통이 상담과 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건 · 평가 번호 순서와 평가일 순서는 다르다
  • 통계 최신

원본 SQL ​

sql
-- [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;

실행계획 학습용 예시 · 행 수는 실데이터 ​

xplan
[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)

문제 ​

  1. 네 SQL마다 드라이빙 테이블과 inner 테이블의 조인 키를 정하고, 지금(PK만 있는 상태)의 계획에서 일량이 어디서 나오는지 줄 번호와 Buffers로 설명하시오.
  2. 네 SQL을 함께 고려한 최소 인덱스 세트를 DDL로 작성하시오. 인덱스마다 컬럼 순서의 근거와 그 인덱스를 쓰는 SQL을 적고, 만들지 않기로 한 인덱스와 그 이유도 쓰시오.
  3. 인덱스 하나(T_ORD (ORD_DT, CUS_ID))로 Q1 ~ Q3을 모두 처리하는 안과 비교해, 하루 일량과 적재 비용으로 판단을 서술하시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자