Skip to content
튜닝 연습파싱과 바인드 변수
PARSE-022

튜닝 담당이 재면 920블록인데 운영 화면은 한 번에 5,826블록을 읽는다

중급SELECT한빛유통567 / 700

상황 ​

정산 「기간 주문 목록」 화면은 시작일과 종료일을 받아 최근 순으로 앞의 100건만 보여 준다('더 보기'는 없다). 기본값은 어제 하루이고, 정산팀은 분기 단위로도 연다. 하루 6,000번쯤 호출된다. 조민우가 SQL*Plus에서 재현해 보았다. 날짜를 VAR(VARCHAR2)로 받아 TO_DATE로 바꿔 넣고 어제 하루로 돌리니 920블록이었다. "이 정도면 문제없는데요?" 그런데 DBA가 보낸 Top SQL 목록에는 이 문장이 실행당 5,826블록으로 올라와 있다.

테이블과 인덱스 ​

  • T_ORD_BIG: 1,000,000행 · 5,826블록 · 하루 약 913건 · 행은 ORD_DT 순서와 상관없이 흩어져 있다
  • 인덱스 IX_ORD_BIG_DT (ORD_DT) · IX_ORD_BIG_DT_AMT (ORD_DT, ORD_AMT) · IX_ORD_BIG_DT_SEQ (ORD_DT, ORD_SEQ)
  • 화면(Java)은 날짜를 java.time.LocalDate로 넘긴다(DATE 바인드). 바인드 피킹 켬 · 히스토그램은 ORD_ST에만 있다 · statistics_level = TYPICAL

원본 SQL ​

sql
// PeriodOrderDao.java : 화면은 앞의 100건만 그린다 (setMaxRows)
String sql = "SELECT ORD_SEQ, CUS_ID, ORD_DT, ORD_ST, ORD_AMT FROM T_ORD_BIG "
           + "WHERE ORD_DT >= ? AND ORD_DT < ? ORDER BY ORD_DT DESC, ORD_SEQ DESC";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setObject(1, from);                 // LocalDate → DATE
ps.setObject(2, to.plusDays(1));
ps.setMaxRows(100);

-- 조민우의 재현 (SQL*Plus)
VAR d1 VARCHAR2(10)
VAR d2 VARCHAR2(10)
EXEC :d1 := '2024-06-16'; :d2 := '2024-06-17'
SELECT ORD_SEQ, CUS_ID, ORD_DT, ORD_ST, ORD_AMT FROM T_ORD_BIG
 WHERE ORD_DT >= TO_DATE(:d1, 'YYYY-MM-DD') AND ORD_DT < TO_DATE(:d2, 'YYYY-MM-DD')
 ORDER BY ORD_DT DESC, ORD_SEQ DESC;

실행계획 학습용 예시 ​

xplan
[조민우의 재현 — 텍스트가 다른 새 문장 · 그가 넣은 하루로 태어났다]
---------------------------------------------------------------------------------------------------------------------
| Id  | Operation                            | Name          | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Used-Mem |
---------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                     |               |      1 |        |    913 |00:00:00.01 |     920 |          |
|   1 |  SORT ORDER BY                       |               |      1 |    913 |    913 |00:00:00.01 |     920 |80896  (0)|
|*  2 |   FILTER                             |               |      1 |        |    913 |00:00:00.01 |     920 |          |
|   3 |    TABLE ACCESS BY INDEX ROWID BATCHED| T_ORD_BIG    |      1 |    913 |    913 |00:00:00.01 |     920 |          |
|*  4 |     INDEX RANGE SCAN                 | IX_ORD_BIG_DT |      1 |    913 |    913 |00:00:00.01 |       5 |          |
---------------------------------------------------------------------------------------------------------------------
   2 - filter(TO_DATE(:D2,'YYYY-MM-DD')>TO_DATE(:D1,'YYYY-MM-DD'))
   4 - access("ORD_DT">=TO_DATE(:D1,'YYYY-MM-DD') AND "ORD_DT"<TO_DATE(:D2,'YYYY-MM-DD'))

[운영 커서 — V$SQL · 08:31에 태어났다]
 CHILD  PLAN_HASH    EXECUTIONS  BUFFER_GETS/EXEC  IS_BIND_SENSITIVE  IS_BIND_AWARE  V$SQL_CS_HISTOGRAM
     0  3305918526        4,388             5,826  Y                  N              0:0  1:4,388  2:0

[운영 커서 · 같은 텍스트와 DATE 바인드로 재현(분기 → 하루 순서) · 'ALLSTATS LAST +PEEKED_BINDS' · 하루 실행]
---------------------------------------------------------------------------------------------------------------------
| Id  | Operation                            | Name          | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Used-Mem |
---------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                     |               |      1 |        |    913 |00:00:00.21 |    5826 |          |
|   1 |  SORT ORDER BY                       |               |      1 |  83103 |    913 |00:00:00.21 |    5826 |80896  (0)|
|*  2 |   FILTER                             |               |      1 |        |    913 |00:00:00.21 |    5826 |          |
|*  3 |    TABLE ACCESS FULL                 | T_ORD_BIG     |      1 |  83103 |    913 |00:00:00.20 |    5826 |          |
---------------------------------------------------------------------------------------------------------------------

Peeked Binds (identified by position):
--------------------------------------
   1 - :1 (DATE): 04/01/2024 00:00:00
   2 - :2 (DATE): 07/01/2024 00:00:00

Predicate Information (identified by operation id):
---------------------------------------------------
   2 - filter(:2>:1)
   3 - filter(("ORD_DT">=:1 AND "ORD_DT"<:2))

문제 ​

  1. 조민우의 재현과 운영 화면의 일량이 다른 이유를 설명하시오. 운영의 느린 실행을 보려면 무엇을 보고, 재현할 때는 무엇을 운영과 같게 맞춰야 하는지 쓰시오.
  2. 하루를 넘기든 분기를 넘기든, 어느 값이 먼저 파싱되든 같은 계획이 맞도록 SQL을 고치시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자