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

같은 SQL을 같은 순서로 스크립트에서 돌리면 4초, 프로시저로 돌리면 21초 걸린다

실전PLSQL한빛유통569 / 700

상황 ​

연말에 1년치 「일별 등급별 매출」을 다시 만드는 재집계 프로시저 P_REAGG_GRADE_DAY는 365일과 등급 셋(A, B, C)을 돌며 하루치 한 등급을 집계해 요약 테이블에 넣는다(1,095번). 오과장이 같은 SELECT를 같은 순서로 SQL*Plus 스크립트에 풀어 돌려 보니 4.3초였는데, 프로시저는 21초가 걸렸다. "프로시저가 뭘 더 하는 것도 아닌데요." 박팀장은 루프를 그대로 둘지, 등급마다 문장을 나눌지, 한 문장으로 바꿀지 비교해서 정해 달라고 했다.

테이블과 인덱스 ​

  • T_ORD_BIG: 1,000,000행 · 5,826블록 · 하루 약 913건 · 행은 ORD_DT 순서와 상관없이 흩어져 있다 · 2023년 약 333,000건
  • M_CUS: 2,000행 · 30블록 · CUS_GD A 20명(1%) · B 180명(9%) · C 1,800명(90%) · FREQUENCY 히스토그램
  • 인덱스 IX_ORD_BIG_DT (ORD_DT) · IX_ORD_BIG_CUS_DT (CUS_ID, ORD_DT)(이 문제용) · PK_M_CUS (CUS_ID)
  • T_GRADE_DAY_SUM(요약): 등급과 날짜마다 한 행 · 인덱스 없음 · statistics_level = TYPICAL · session_cached_cursors = 50

원본 SQL ​

sql
PROCEDURE P_REAGG_GRADE_DAY(p_year IN NUMBER) IS
  v_from DATE := TO_DATE(p_year || '0101', 'YYYYMMDD');
  v_dt   DATE;
  v_cnt  NUMBER;
  v_amt  NUMBER;
BEGIN
  DELETE FROM T_GRADE_DAY_SUM WHERE SUM_DT >= v_from AND SUM_DT < ADD_MONTHS(v_from, 12);
  FOR d IN 0 .. 364 LOOP
    v_dt := v_from + d;
    FOR g IN (SELECT 'A' AS gd FROM dual UNION ALL SELECT 'B' FROM dual UNION ALL SELECT 'C' FROM dual) LOOP
      SELECT COUNT(*), NVL(SUM(o.ORD_AMT), 0)
        INTO v_cnt, v_amt
        FROM M_CUS c, T_ORD_BIG o
       WHERE c.CUS_GD = g.gd
         AND o.CUS_ID = c.CUS_ID
         AND o.ORD_DT = v_dt;
      INSERT INTO T_GRADE_DAY_SUM (SUM_DT, CUS_GD, ORD_CNT, ORD_AMT)
      VALUES (v_dt, g.gd, v_cnt, v_amt);
    END LOOP;
  END LOOP;
  COMMIT;
END;
-- 호출 : EXEC P_REAGG_GRADE_DAY(2023)

실행계획 학습용 예시 · 실행 방식에 따른 ACS 차이는 교재 11화 실측과 같은 방향 ​

xplan
[프로시저 한 번 뒤 · 집계 SELECT · V$SQL]
 CHILD  PARSE_CALLS  EXECUTIONS  BUFFER_GETS  IS_BIND_SENSITIVE  IS_BIND_AWARE  V$SQL_CS_HISTOGRAM
     0            1       1,095    2,556,095  Y                  N              0:730  1:365  2:0

[SQL*Plus로 풀어 돌린 것 · 같은 SELECT 텍스트 · 같은 순서]
 CHILD  PARSE_CALLS  EXECUTIONS  BUFFER_GETS  IS_BIND_AWARE  계획           V$SQL_SHARED_CURSOR
     0            3           3        7,003  N              NESTED LOOPS   —
     1          364         364       36,036  Y              NESTED LOOPS   BIND_EQUIV_FAILURE (A)
     2          364         364      237,328  Y              NESTED LOOPS   BIND_EQUIV_FAILURE (B)
     3          364         364      345,800  Y              HASH JOIN      BIND_EQUIV_FAILURE (C)
 합계 626,167블록 · 4.3초   (프로시저 2,556,095블록 · 21초)

[프로시저 안 · C 등급 하루 한 번 · 'ALLSTATS LAST +PEEKED_BINDS']
-----------------------------------------------------------------------------------------------------------
| Id  | Operation                     | Name              | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
-----------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |                   |      1 |        |      1 |00:00:00.02 |    6252 |
|   1 |  SORT AGGREGATE               |                   |      1 |      1 |      1 |00:00:00.02 |    6252 |
|   2 |   NESTED LOOPS                |                   |      1 |      9 |    822 |00:00:00.02 |    6252 |
|   3 |    NESTED LOOPS               |                   |      1 |      9 |    822 |00:00:00.01 |    5430 |
|*  4 |     TABLE ACCESS FULL         | M_CUS             |      1 |     20 |   1800 |00:00:00.01 |      30 |
|*  5 |     INDEX RANGE SCAN          | IX_ORD_BIG_CUS_DT |   1800 |      1 |    822 |00:00:00.01 |    5400 |
|   6 |    TABLE ACCESS BY INDEX ROWID| T_ORD_BIG         |    822 |      1 |    822 |00:00:00.01 |     822 |
-----------------------------------------------------------------------------------------------------------

Peeked Binds (identified by position):
--------------------------------------
   1 - :B2 (VARCHAR2(32), CSID=873): 'A'
   2 - :B1 (DATE): 01/01/2023 00:00:00

Predicate Information (identified by operation id):
---------------------------------------------------
   4 - filter("C"."CUS_GD"=:B2)
   5 - access("O"."CUS_ID"="C"."CUS_ID" AND "O"."ORD_DT"=:B1)

[SQL*Plus의 자식 3 · C 등급 하루 한 번]
-----------------------------------------------------------------------------------------------------------
| Id  | Operation                     | Name              | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
-----------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |                   |      1 |        |      1 |00:00:00.01 |     950 |
|   1 |  SORT AGGREGATE               |                   |      1 |      1 |      1 |00:00:00.01 |     950 |
|*  2 |   HASH JOIN                   |                   |      1 |    822 |    822 |00:00:00.01 |     950 |
|*  3 |    TABLE ACCESS FULL          | M_CUS             |      1 |   1800 |   1800 |00:00:00.01 |      30 |
|   4 |    TABLE ACCESS BY INDEX ROWID BATCHED| T_ORD_BIG |      1 |    913 |    913 |00:00:00.01 |     920 |
|*  5 |     INDEX RANGE SCAN          | IX_ORD_BIG_DT     |      1 |    913 |    913 |00:00:00.01 |       5 |
-----------------------------------------------------------------------------------------------------------

문제 ​

  1. 같은 SELECT, 같은 순서인데 SQL*Plus에서는 C 등급이 해시 조인으로 바뀌고 프로시저에서는 끝까지 NL로 남은 이유를 위 자료로 설명하시오.
  2. 루프를 그대로 두는 안, 등급마다 문장을 나누는 안, 한 문장으로 바꾸는 안을 일량과 부작용으로 비교하시오.
  3. 고른 안으로 프로시저를 고치시오. 결과(날짜와 등급마다 한 행, 주문이 없으면 0)가 바뀌지 않게 하시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자