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

VIP 관리 화면 묶음 세 개가 대용량 주문 테이블을 하루 4,600번 통째로 읽는다

실전DDL한빛유통252 / 700

상황 ​

CRM팀의 VIP 관리 화면에는 SQL 세 개가 있다. Q1 「VIP 목록」(A 등급 20명의 올해 주문 건수와 금액)은 하루 300번, Q2 「고객 최근 주문 10건」은 콜센터가 하루 4,000번, Q3 「상품별 평균 평점」은 하루 50번 돈다. 고객사는 "스칼라 서브쿼리가 있는 SQL마다 인덱스를 하나씩 만들어 달라"라고 요청했다. 박팀장은 인덱스를 최소한으로 정하고, 필요 없는 곳은 왜 필요 없는지 근거를 대라고 했다.

테이블과 인덱스 ​

  • T_ORD_BIG: 1,000,000행 · 5,826블록 · 인덱스 없음 · 고객마다 500건(2,000행 간격으로 흩어져 있다) · 2024년 333,245건(VIP 20명은 3,330건, 1명당 약 167건)
  • M_CUS: 2,000행 · 30블록 · PK_M_CUS (CUS_ID) · CUS_GD A 20명
  • M_ITM: 100행 · 5블록 · PK_M_ITM · T_ITM_EVL: 5,000행 · 48블록 · PK_T_ITM_EVL (ITM_ID, EVL_LST_NO) · 상품당 50건
  • 주문은 하루 약 900건 들어온다 · 통계 최신 · 스칼라 서브쿼리 Unnesting은 켜져 있다

원본 SQL ​

sql
-- Q1 「VIP 목록」 (하루 300 번)
SELECT c.CUS_ID, c.CUS_NM,
       (SELECT COUNT(*) FROM T_ORD_BIG o
         WHERE o.CUS_ID = c.CUS_ID
           AND o.ORD_DT >= DATE '2024-01-01' AND o.ORD_DT < DATE '2025-01-01') AS ord_cnt,
       (SELECT SUM(o.ORD_AMT) FROM T_ORD_BIG o
         WHERE o.CUS_ID = c.CUS_ID
           AND o.ORD_DT >= DATE '2024-01-01' AND o.ORD_DT < DATE '2025-01-01') AS ord_amt
  FROM M_CUS c
 WHERE c.CUS_GD = 'A'
 ORDER BY c.CUS_ID;

-- Q2 「고객 최근 주문 10건」 (하루 4,000 번)
SELECT *
  FROM (SELECT o.ORD_SEQ, o.ORD_DT, o.ORD_ST, o.ORD_AMT
          FROM T_ORD_BIG o
         WHERE o.CUS_ID = :cus
         ORDER BY o.ORD_DT DESC)
 WHERE ROWNUM <= 10;

-- Q3 「상품별 평균 평점」 (하루 50 번)
SELECT i.ITM_ID, i.ITM_NM,
       (SELECT ROUND(AVG(e.EVL_PT), 2) FROM T_ITM_EVL e WHERE e.ITM_ID = i.ITM_ID) AS avg_pt
  FROM M_ITM i
 ORDER BY i.ITM_ID;

실행계획 학습용 예시 ​

xplan
[Q1]
-----------------------------------------------------------------------------------------------
| Id  | Operation              | Name      | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
-----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT       |           |      1 |        |     20 |00:00:00.29 |   11682 |
|   1 |  SORT ORDER BY         |           |      1 |     20 |     20 |00:00:00.29 |   11682 |
|*  2 |   HASH JOIN OUTER      |           |      1 |     20 |     20 |00:00:00.29 |   11682 |
|*  3 |    HASH JOIN OUTER     |           |      1 |     20 |     20 |00:00:00.15 |    5856 |
|*  4 |     TABLE ACCESS FULL  | M_CUS     |      1 |     20 |     20 |00:00:00.01 |      30 |
|   5 |     VIEW               | VW_SSQ_1  |      1 |   2000 |   2000 |00:00:00.14 |    5826 |
|   6 |      HASH GROUP BY     |           |      1 |   2000 |   2000 |00:00:00.14 |    5826 |
|*  7 |       TABLE ACCESS FULL| T_ORD_BIG |      1 |    333K|    333K|00:00:00.05 |    5826 |
|   8 |    VIEW                | VW_SSQ_2  |      1 |   2000 |   2000 |00:00:00.14 |    5826 |
|   9 |     HASH GROUP BY      |           |      1 |   2000 |   2000 |00:00:00.14 |    5826 |
|* 10 |      TABLE ACCESS FULL | T_ORD_BIG |      1 |    333K|    333K|00:00:00.05 |    5826 |
-----------------------------------------------------------------------------------------------
   2 - access("VW_SSQ_2"."ITEM_1"(+)="C"."CUS_ID")
   3 - access("VW_SSQ_1"."ITEM_1"(+)="C"."CUS_ID")
   4 - filter("C"."CUS_GD"='A')
   7 - filter(("O"."ORD_DT">=TO_DATE(' 2024-01-01 00:00:00', 'syyyy-mm-dd hh24:mi:ss') AND "O"."ORD_DT"<TO_DATE(' 2025-01-01 00:00:00', 'syyyy-mm-dd hh24:mi:ss')))
  10 - filter((같은 모양))

[Q2]
-----------------------------------------------------------------------------------------------
| Id  | Operation              | Name      | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
-----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT       |           |      1 |        |     10 |00:00:00.05 |    5826 |
|*  1 |  COUNT STOPKEY         |           |      1 |        |     10 |00:00:00.05 |    5826 |
|   2 |   VIEW                 |           |      1 |    500 |     10 |00:00:00.05 |    5826 |
|*  3 |    SORT ORDER BY STOPKEY|          |      1 |    500 |     10 |00:00:00.05 |    5826 |
|*  4 |     TABLE ACCESS FULL  | T_ORD_BIG |      1 |    500 |    500 |00:00:00.05 |    5826 |
-----------------------------------------------------------------------------------------------
   1 - filter(ROWNUM<=10)
   3 - filter(ROWNUM<=10)
   4 - filter("O"."CUS_ID"=:CUS)

[Q3]
-----------------------------------------------------------------------------------------------
| Id  | Operation              | Name      | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
-----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT       |           |      1 |        |    100 |00:00:00.01 |      53 |
|   1 |  SORT ORDER BY         |           |      1 |    100 |    100 |00:00:00.01 |      53 |
|*  2 |   HASH JOIN OUTER      |           |      1 |    100 |    100 |00:00:00.01 |      53 |
|   3 |    TABLE ACCESS FULL   | M_ITM     |      1 |    100 |    100 |00:00:00.01 |       5 |
|   4 |    VIEW                | VW_SSQ_1  |      1 |    100 |    100 |00:00:00.01 |      48 |
|   5 |     HASH GROUP BY      |           |      1 |    100 |    100 |00:00:00.01 |      48 |
|   6 |      TABLE ACCESS FULL | T_ITM_EVL |      1 |   5000 |   5000 |00:00:00.01 |      48 |
-----------------------------------------------------------------------------------------------
   2 - access("VW_SSQ_1"."ITEM_1"(+)="I"."ITM_ID")

문제 ​

  1. 세 SQL의 일량이 어디서 나오는지 줄 번호와 Buffers로 설명하고, 하루 실행 횟수를 곱해 어느 SQL이 가장 급한지 정하시오.
  2. 인덱스를 최소한으로 설계하고(DDL), 고쳐야 할 SQL은 고치시오. 인덱스가 필요 없는 SQL은 그 이유를 쓰시오.
  3. 인덱스 컬럼 순서와 ORD_AMT를 인덱스에 넣을지를 비교하고, 인덱스를 더하는 대가를 서술하시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자