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

화면은 0.01초인데 같은 SQL로 받는 엑셀 다운로드는 40초가 걸린다

실전SELECT한빛유통169 / 700

상황 ​

운영팀의 「기간 주문 조회」 화면은 기간을 넣으면 최신 주문부터 50건을 보여 준다. 같은 화면의 「엑셀 다운로드」 버튼은 똑같은 SQL을 열고 끝까지 내려받는다. 화면은 하루 3만 번, 다운로드는 하루 40번쯤 호출된다. 한 달을 내려받으면 40초가 걸린다는 민원에, 지난주 한 개발자가 힌트를 USE_HASH와 FULL로 바꿨다. 다운로드는 6초가 됐지만 이번에는 화면이 누를 때마다 0.3초씩 걸려 상담원들이 불만이다. 아래는 힌트를 바꾸기 전, 2024년 6월로 조회한 두 경우의 실행계획이다.

테이블과 인덱스 ​

  • T_ORD_BIG: 1,000,000행 · 5,826블록 · IX_ORD_BIG_DT_SEQ (ORD_DT, ORD_SEQ) (09화에서 만든 인덱스) · 한 달 27,390건 (인덱스 리프 약 91블록) · 같은 날짜의 주문은 테이블 전체에 흩어져 있고, 한 달치 블록은 대부분 버퍼 캐시에 없다
  • M_CUS: 2,000행 · 30블록 · PK_M_CUS (CUS_ID)
  • 애플리케이션은 한 번에 50행씩 가져온다 (fetch size 50) · 통계 최신

원본 SQL ​

sql
SELECT /*+ LEADING(o c) USE_NL(c) INDEX_DESC(o IX_ORD_BIG_DT_SEQ) */
       o.ORD_DT, o.ORD_SEQ, o.CUS_ID, c.CUS_NM, c.CUS_GD, o.ORD_ST, o.ORD_AMT
  FROM T_ORD_BIG o, M_CUS c
 WHERE o.ORD_DT >= :v_from
   AND o.ORD_DT <  :v_to
   AND c.CUS_ID = o.CUS_ID
 ORDER BY o.ORD_DT DESC, o.ORD_SEQ DESC

실행계획 학습용 예시 ​

xplan
[화면: 첫 50행을 받고 커서를 닫았을 때]
------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                        | Name              | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads |
------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                 |                   |      1 |        |     50 |00:00:00.01 |     106 |     0 |
|*  1 |  FILTER                          |                   |      1 |        |     50 |00:00:00.01 |     106 |     0 |
|   2 |   NESTED LOOPS                   |                   |      1 |  27390 |     50 |00:00:00.01 |     106 |     0 |
|   3 |    NESTED LOOPS                  |                   |      1 |  27390 |     50 |00:00:00.01 |      56 |     0 |
|   4 |     TABLE ACCESS BY INDEX ROWID  | T_ORD_BIG         |      1 |  27390 |     50 |00:00:00.01 |      53 |     0 |
|*  5 |      INDEX RANGE SCAN DESCENDING | IX_ORD_BIG_DT_SEQ |      1 |  27390 |     50 |00:00:00.01 |       3 |     0 |
|*  6 |     INDEX UNIQUE SCAN            | PK_M_CUS          |     50 |      1 |     50 |00:00:00.01 |       3 |     0 |
|   7 |    TABLE ACCESS BY INDEX ROWID   | M_CUS             |     50 |      1 |     50 |00:00:00.01 |      50 |     0 |
------------------------------------------------------------------------------------------------------------------------

[다운로드: 같은 계획으로 27,390행을 끝까지 받았을 때]
------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                        | Name              | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads |
------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                 |                   |      1 |        |  27390 |00:00:38.21 |   55459 | 26982 |
|*  1 |  FILTER                          |                   |      1 |        |  27390 |00:00:38.21 |   55459 | 26982 |
|   2 |   NESTED LOOPS                   |                   |      1 |  27390 |  27390 |00:00:38.19 |   55459 | 26982 |
|   3 |    NESTED LOOPS                  |                   |      1 |  27390 |  27390 |00:00:37.52 |   28069 | 26982 |
|   4 |     TABLE ACCESS BY INDEX ROWID  | T_ORD_BIG         |      1 |  27390 |  27390 |00:00:37.43 |   27481 | 26982 |
|*  5 |      INDEX RANGE SCAN DESCENDING | IX_ORD_BIG_DT_SEQ |      1 |  27390 |  27390 |00:00:00.01 |      91 |     0 |
|*  6 |     INDEX UNIQUE SCAN            | PK_M_CUS          |  27390 |      1 |  27390 |00:00:00.05 |     588 |     0 |
|   7 |    TABLE ACCESS BY INDEX ROWID   | M_CUS             |  27390 |      1 |  27390 |00:00:00.58 |   27390 |     0 |
------------------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   1 - filter(:V_FROM<:V_TO)
   5 - access("O"."ORD_DT"<:V_TO AND "O"."ORD_DT">=:V_FROM)
   6 - access("C"."CUS_ID"="O"."CUS_ID")

문제 ​

  1. 두 실행계획의 A-Rows와 Buffers, Reads를 비교해, 같은 계획이 화면에서는 빠르고 다운로드에서는 느린 이유를 부분범위 처리로 설명하시오.
  2. 화면과 다운로드를 모두 만족하도록 SQL을 고치시오(두 SQL로 나눠도 된다). 각각의 조인 방식과 예상 Buffers를 쓰시오.
  3. 하나의 SQL로 두 요구를 맞추려는 다른 방법들(FIRST_ROWS(n) 힌트, 커버링 인덱스, fetch size 늘리기)을 비교하고 부작용을 서술하시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자