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

콜센터 고객 화면을 열 때마다 TEMP에 주문 33만 건이 쓰였다 지워진다

중급SELECT한빛유통297 / 700

상황 ​

콜센터 「고객 상담」 화면은 위쪽에 고객의 올해 주문 요약(건수, 금액)을, 아래쪽에 올해 최근 주문 10건을 보여 준다. 개발자는 두 부분을 SQL 하나로 받으려고 공통 부분을 WITH 절로 빼 두었고, 요약은 10건의 줄마다 붙어 나오게 했다. 화면은 하루 4,000번 열리고 한 번에 0.7초가 걸린다. DBA는 이 화면이 열릴 때마다 TEMP 사용량이 16MB씩 올라갔다 내려간다고 알려 왔다.

테이블과 인덱스 ​

  • T_ORD_BIG: 1,000,000행 · 5,826블록 · 고객마다 500건(2,000행 간격으로 흩어져 있다) · 2024년 333,245건(고객당 약 167건)
  • 인덱스 IX_ORD_BIG_CUS_DT (CUS_ID, ORD_DT): 이미 있다 · BLEVEL 2
  • M_CUS: 2,000행 · 30블록 · PK_M_CUS (CUS_ID)
  • 통계 최신 · 아래는 고객 CUS_000123으로 연 화면의 실행계획이다

원본 SQL ​

sql
WITH v AS (
  SELECT o.CUS_ID, o.ORD_SEQ, o.ORD_DT, o.ORD_ST, o.ORD_AMT, c.CUS_NM, c.CUS_GD
    FROM T_ORD_BIG o, M_CUS c
   WHERE c.CUS_ID = o.CUS_ID
     AND o.ORD_DT >= DATE '2024-01-01'
     AND o.ORD_DT <  DATE '2025-01-01'
)
SELECT s.CUS_NM, s.CUS_GD, s.ORD_CNT, s.ORD_AMT,
       d.ORD_SEQ, d.ORD_DT, d.ORD_ST, d.ORD_AMT AS AMT
  FROM (SELECT MAX(CUS_NM) AS CUS_NM, MAX(CUS_GD) AS CUS_GD,
               COUNT(*) AS ORD_CNT, SUM(ORD_AMT) AS ORD_AMT
          FROM v
         WHERE CUS_ID = :cus) s,
       (SELECT *
          FROM (SELECT ORD_SEQ, ORD_DT, ORD_ST, ORD_AMT
                  FROM v
                 WHERE CUS_ID = :cus
                 ORDER BY ORD_DT DESC, ORD_SEQ DESC)
         WHERE ROWNUM <= 10) d

실행계획 학습용 예시 ​

xplan
----------------------------------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                                  | Name                      | Starts | E-Rows | A-Rows |      A-Time | Buffers | Reads | Writes |
----------------------------------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                           |                           |      1 |        |     10 | 00:00:00.74 |    9969 |  4072 |   2036 |
|   1 |  TEMP TABLE TRANSFORMATION                 |                           |      1 |        |     10 | 00:00:00.74 |    9969 |  4072 |   2036 |
|   2 |   LOAD AS SELECT (CURSOR DURATION MEMORY)  | SYS_TEMP_0FD9D6624_2A1B3C |      1 |        |      0 | 00:00:00.49 |    5889 |     0 |   2036 |
|*  3 |    HASH JOIN                               |                           |      1 |   333K |   333K | 00:00:00.30 |    5856 |     0 |      0 |
|   4 |     TABLE ACCESS FULL                      | M_CUS                     |      1 |   2000 |   2000 | 00:00:00.01 |      30 |     0 |      0 |
|*  5 |     TABLE ACCESS FULL                      | T_ORD_BIG                 |      1 |   333K |   333K | 00:00:00.09 |    5826 |     0 |      0 |
|   6 |   MERGE JOIN CARTESIAN                     |                           |      1 |     10 |     10 | 00:00:00.25 |    4080 |  4072 |      0 |
|   7 |    VIEW                                    |                           |      1 |      1 |      1 | 00:00:00.12 |    2040 |  2036 |      0 |
|   8 |     SORT AGGREGATE                         |                           |      1 |      1 |      1 | 00:00:00.12 |    2040 |  2036 |      0 |
|*  9 |      VIEW                                  |                           |      1 |   333K |    167 | 00:00:00.12 |    2040 |  2036 |      0 |
|  10 |       TABLE ACCESS FULL                    | SYS_TEMP_0FD9D6624_2A1B3C |      1 |   333K |   333K | 00:00:00.08 |    2040 |  2036 |      0 |
|  11 |    BUFFER SORT                             |                           |      1 |     10 |     10 | 00:00:00.13 |    2040 |  2036 |      0 |
|  12 |     VIEW                                   |                           |      1 |     10 |     10 | 00:00:00.13 |    2040 |  2036 |      0 |
|* 13 |      COUNT STOPKEY                         |                           |      1 |        |     10 | 00:00:00.13 |    2040 |  2036 |      0 |
|  14 |       VIEW                                 |                           |      1 |   333K |     10 | 00:00:00.13 |    2040 |  2036 |      0 |
|* 15 |        SORT ORDER BY STOPKEY               |                           |      1 |   333K |     10 | 00:00:00.13 |    2040 |  2036 |      0 |
|* 16 |         VIEW                               |                           |      1 |   333K |    167 | 00:00:00.12 |    2040 |  2036 |      0 |
|  17 |          TABLE ACCESS FULL                 | SYS_TEMP_0FD9D6624_2A1B3C |      1 |   333K |   333K | 00:00:00.08 |    2040 |  2036 |      0 |
----------------------------------------------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   3 - access("C"."CUS_ID"="O"."CUS_ID")
   5 - 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')))
   9 - filter("CUS_ID"=:CUS)
  13 - filter(ROWNUM<=10)
  15 - filter(ROWNUM<=10)
  16 - filter("CUS_ID"=:CUS)

문제 ​

  1. 이 화면이 열릴 때마다 TEMP를 쓰는 이유를 실행계획의 줄 번호로 설명하시오. 고객 조건(CUS_ID = :cus)이 왜 인덱스로 쓰이지 못했는지도 쓰시오.
  2. 결과가 바뀌지 않게 SQL을 고치시오. /*+ INLINE */ 힌트를 쓰는 안과 비교해 읽는 양을 쓰시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자