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

일별 판매 목록 SQL 하나에 자식 커서가 넷이고, 리포트 도구에서만 6,000블록을 읽는다

실전SELECT유통574 / 700

상황 ​

본사 레거시 유통의 「일별 판매 목록」 SQL은 세 프로그램이 같은 문자열을 복사해 쓴다. Java 본사 화면(2011), 매장 단말 프로그램(C#, ODP.NET, 2018), 경영지원팀의 리포트 도구(BI)다. 리포트 도구는 접속할 때 세션의 OPTIMIZER_MODE를 FIRST_ROWS_1로 바꾼다. 지난달 인스턴스의 CURSOR_SHARING을 EXACT로 되돌린 뒤 이 문장이 AWR 「Version Count」 상위에 올랐다. 경영지원팀은 리포트가 느리다고 하고, 본사 화면과 매장 단말은 빠르다. 박팀장이 말했다. "자식이 왜 넷인지 하나씩 가려서, 고칠 것만 고쳐 주세요."

테이블과 인덱스 ​

  • CF10 판매: 500,000행 · 4,583블록 · SLE_NO VARCHAR2(15)는 날짜로 시작한다('20230601-0123') · SLE_NO 순(날짜순)으로 적재 · SLE_DT VARCHAR2(8) 하루 약 706건
  • 인덱스 CF10_PK (SLE_NO): 리프 약 1,700블록 · IX_CF10_SLE_DT (SLE_DT): 리프 1,393블록
  • 통계: 매일 01:00 기본값(AUTO_INVALIDATE) · 히스토그램 없음 · CURSOR_SHARING = EXACT · 바인드 피킹 켬
  • 호출 수(하루): 본사 화면 약 3,500 · 매장 단말 약 9,000 · 리포트 도구 약 600(리포트는 하루치를 모두 받아 엑셀로 내린다)

원본 SQL ​

sql
-- 「일별 판매 목록」 (세 프로그램이 같은 문자열)
SELECT SLE_NO, SLE_HMS, SHP_NO
  FROM CF10
 WHERE SLE_DT = :dt
 ORDER BY SLE_NO

// 매장 단말 (C#, ODP.NET)
var p = new OracleParameter("dt", OracleDbType.Varchar2);
p.Value = dt;
p.Size  = 200;        // "넉넉하게"
cmd.Parameters.Add(p);

-- 리포트 도구 접속 설정 (데이터 원본 초기화 SQL)
ALTER SESSION SET OPTIMIZER_MODE = FIRST_ROWS_1;

실행계획 학습용 예시 ​

xplan
[V$SQL — 15:00 · 자식 넷]
 CHILD  SHAREABLE  OPTIMIZER_MODE  :DT MAX_LENGTH  EXEC   BUF/EXEC  PLAN_HASH    처음 부른 곳 · 시각
     0  N          ALL_ROWS                    32  2,104        13  1932847016   본사 화면 · 전날 09:02
     1  N          ALL_ROWS                    32  1,388        13  1932847016   본사 화면 · 03:12
     2  Y          ALL_ROWS                  2000  9,415        13  1932847016   매장 단말 · 07:30
     3  Y          FIRST_ROWS                  32    612     6,285  2410375521   리포트 도구 · 09:10

[V$SQL_SHARED_CURSOR — Y인 칸 (그 줄의 자식이 새로 생긴 이유)]
   child 1 : ROLL_INVALID_MISMATCH = Y
   child 2 : BIND_LENGTH_UPGRADEABLE = Y
   child 3 : OPTIMIZER_MODE_MISMATCH = Y

[자식 2 (ALL_ROWS) · 한 번]
-------------------------------------------------------------------------------------------------------------------
| Id  | Operation                            | Name           | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Used-Mem |
-------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                     |                |      1 |        |    706 |00:00:00.01 |      13 |          |
|   1 |  SORT ORDER BY                       |                |      1 |    706 |    706 |00:00:00.01 |      13 |  59392 (0)|
|   2 |   TABLE ACCESS BY INDEX ROWID BATCHED| CF10           |      1 |    706 |    706 |00:00:00.01 |      13 |          |
|*  3 |    INDEX RANGE SCAN                  | IX_CF10_SLE_DT |      1 |    706 |    706 |00:00:00.01 |       4 |          |
-------------------------------------------------------------------------------------------------------------------
   3 - access("SLE_DT"=:DT)

[자식 3 (FIRST_ROWS_1) · 리포트 도구 한 번 · 하루치를 끝까지 받는다]
-------------------------------------------------------------------------------------------------------------------
| Id  | Operation                            | Name           | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Used-Mem |
-------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                     |                |      1 |        |    706 |00:00:00.12 |    6285 |          |
|*  1 |  TABLE ACCESS BY INDEX ROWID         | CF10           |      1 |      1 |    706 |00:00:00.12 |    6285 |          |
|   2 |   INDEX FULL SCAN                    | CF10_PK        |      1 |    708 |   500K |00:00:00.04 |    1702 |          |
-------------------------------------------------------------------------------------------------------------------
   1 - filter("SLE_DT"=:DT)

문제 ​

  1. 자식 넷이 각각 왜 생겼는지 Y 칸으로 설명하고, 해로운 자식과 낭비일 뿐인 자식, 정상인 자식으로 가르시오.
  2. 해로운 자식과 낭비인 자식이 생기지 않도록 각 프로그램을 고치시오.
  3. 고친 방법이 미치는 범위와 부작용(리포트 도구의 다른 리포트, 다른 화면)을 쓰시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자