Skip to content
튜닝 연습옵티마이저와 통계
OPT-037

본사 ERP의 월 판매 명세 추출은 SQL Developer에서는 0.3초인데 ERP에서는 1초가 걸린다

중급SELECT유통627 / 700

상황 ​

본사 유통 ERP는 외부 업체의 패키지다. 업체 설치 지침에 따라 ERP는 로그인하자마자 세션에 ALTER SESSION SET OPTIMIZER_MODE = FIRST_ROWS_10을 건다. 지침을 바꾸면 유지보수 계약에서 벗어나고, SQL은 암호화된 패키지 안에 있어 한 글자도 고칠 수 없다. 매일 아침 본부 담당자 서른 명이 「월 판매 명세」를 내려받는다(지난달 전체, 끝까지 받는다). 선배가 같은 글자의 SQL을 SQL Developer에서 한 번 돌려 보고 말했다. "SQL은 못 고쳐도 계획은 고칠 수 있지."

테이블과 인덱스 ​

  • CF10 판매: 500,000행 · 4,583블록 · SLE_DT VARCHAR2(8) · 2023년 6월 20,953건 · IX_CF10_SLE_DT (SLE_DT) 리프 1,393 · 클러스터링 팩터 4,601(날짜순 적재)
  • CF11 판매상품: 1,096,708행 · 8,204블록 · CF11_PK (SLE_NO, ITM_NO) 높이 3 · 판매 한 건에 평균 2.2줄(같은 블록)
  • CF01 상품: 109행 · 5블록 · CF01_PK (ITM_NO)
  • 인스턴스 OPTIMIZER_MODE는 ALL_ROWS · 통계 최신 · SQL Plan Management 사용 가능(베이스라인은 아직 없다)

원본 SQL ​

sql
SELECT a.SLE_NO, a.SLE_DT, a.SHP_NO, b.ITM_NO, c.ITM_NM, b.SLE_QTY, b.SLE_UPRC
  FROM CF10 a, CF11 b, CF01 c
 WHERE a.SLE_DT BETWEEN :1 AND :2
   AND b.SLE_NO = a.SLE_NO
   AND c.ITM_NO = b.ITM_NO

실행계획 학습용 예시 ​

xplan
[ERP 세션 — '20230601' ~ '20230630' · 끝까지 받은 뒤]
------------------------------------------------------------------------------------------------------------
| Id  | Operation                          | Name           | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                   |                |      1 |        |  45960 |00:00:00.97 |     176K|
|*  1 |  FILTER                            |                |      1 |        |  45960 |00:00:00.97 |     176K|
|   2 |   NESTED LOOPS                     |                |      1 |     10 |  45960 |00:00:00.95 |     176K|
|   3 |    NESTED LOOPS                    |                |      1 |     10 |  45960 |00:00:00.71 |     130K|
|   4 |     NESTED LOOPS                   |                |      1 |     10 |  45960 |00:00:00.52 |   84500 |
|   5 |      NESTED LOOPS                  |                |      1 |     10 |  45960 |00:00:00.30 |   63112 |
|   6 |       TABLE ACCESS BY INDEX ROWID  | CF10           |      1 |      5 |  20953 |00:00:00.02 |     253 |
|*  7 |        INDEX RANGE SCAN            | IX_CF10_SLE_DT |      1 |   2673 |  20953 |00:00:00.01 |      60 |
|*  8 |       INDEX RANGE SCAN             | CF11_PK        |  20953 |      2 |  45960 |00:00:00.21 |   62859 |
|   9 |      TABLE ACCESS BY INDEX ROWID   | CF11           |  45960 |      2 |  45960 |00:00:00.19 |   21388 |
|* 10 |     INDEX UNIQUE SCAN              | CF01_PK        |  45960 |      1 |  45960 |00:00:00.11 |   45960 |
|  11 |    TABLE ACCESS BY INDEX ROWID     | CF01           |  45960 |      1 |  45960 |00:00:00.12 |   45960 |
------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   1 - filter(:1<=:2)
   7 - access("A"."SLE_DT">=:1 AND "A"."SLE_DT"<=:2)
   8 - access("B"."SLE_NO"="A"."SLE_NO")
  10 - access("C"."ITM_NO"="B"."ITM_NO")

[V$SQL · V$SQL_OPTIMIZER_ENV — SQL_ID 6t1d0c4m8z2qk]
 CHILD  optimizer_mode   PLAN_HASH_VALUE  EXECUTIONS  BUFFER_GETS/EXEC  SQL_PLAN_BASELINE
     0  first_rows_10         3871190422          31           176,420  (없음)
     1  all_rows               912774650           1             8,464  (없음)
 (자식 1 은 선배가 SQL Developer 에서 같은 글자로 한 번 돌린 것 · V$SQL_SHARED_CURSOR : OPTIMIZER_MODE_MISMATCH = Y)

[자식 1 — ALL_ROWS 세션]
------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                          | Name           | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Used-Mem |
------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                   |                |      1 |        |  45960 |00:00:00.29 |    8464 |          |
|*  1 |  FILTER                            |                |      1 |        |  45960 |00:00:00.29 |    8464 |          |
|*  2 |   HASH JOIN                        |                |      1 |   5858 |  45960 |00:00:00.28 |    8464 | 1236K (0)|
|   3 |    TABLE ACCESS FULL               | CF01           |      1 |    109 |    109 |00:00:00.01 |       7 |          |
|*  4 |    HASH JOIN                       |                |      1 |   5858 |  45960 |00:00:00.25 |    8457 | 1653K (0)|
|   5 |     TABLE ACCESS BY INDEX ROWID BATCHED| CF10       |      1 |   2673 |  20953 |00:00:00.02 |     253 |          |
|*  6 |      INDEX RANGE SCAN              | IX_CF10_SLE_DT |      1 |   2673 |  20953 |00:00:00.01 |      60 |          |
|   7 |     TABLE ACCESS FULL              | CF11           |      1 |   1096K|   1096K|00:00:00.11 |    8204 |          |
------------------------------------------------------------------------------------------------------------------------
(176K = 176,420 · 130K = 130,460)

문제 ​

  1. 같은 SQL_ID에 자식 커서가 둘이고 계획이 다른 이유를 쓰고, ERP 세션의 계획이 끝까지 받는 추출에 왜 나쁜지 실행계획으로 설명하시오.
  2. SQL과 ERP의 세션 설정을 하나도 바꾸지 않고 ERP 세션이 자식 1의 계획을 쓰게 하시오. 적용됐는지 확인하는 방법과 이 방법을 유지할 때 주의할 점도 쓰시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자