테마
연말에 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 = 50PROCEDURE 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)
[프로시저 한 번 뒤 · 집계 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 |
-----------------------------------------------------------------------------------------------------------
SQL*Plus에서는 C 등급이 해시 조인으로 바뀌고 프로시저에서는 끝까지 NL로 남은 이유를 위 자료로 설명하시오.