Skip to content
튜닝 연습PL/SQL과 사용자 정의 함수
PLSQL-018

BULK COLLECT와 FORALL을 다 썼는데 recursive calls가 30만 번이다

기초PLSQL이커머스668 / 700

이 문제는 교재 18화 졸업 과제와 같은 이커머스 데이터베이스를 씁니다. 『튜너의 1년』을 순서대로 읽고 있다면 졸업 과제를 먼저 풀고 오세요. 아래 「테이블과 인덱스」 칸에 과제의 답 일부(제약 조건, 인덱스, 통계의 실제 상태)가 나옵니다.

상황 ​

이커머스 추천팀은 매일 새벽 상품 조회 이력을 추천 엔진이 읽는 적재 테이블로 옮긴다. 신입 개발자가 교육에서 배운 대로 BULK COLLECT와 FORALL로 짰다며 리뷰를 부탁했다. 조민우가 한 번 돌리고 세션 통계를 떠 보니 recursive calls가 300,232였다. "천 건씩 묶었으면 몇백 번이어야 하지 않나요?"

테이블과 인덱스 ​

  • product_views: 100,000행 · 367블록 · PK(view_id) · product_id · customer_id · view_date DATE(시분초 있음)
  • rec_view_feed(적재 대상, 이 문제용): view_id · product_id · customer_id · view_ts VARCHAR2(19) · day_tp VARCHAR2(10) · score NUMBER · load_dt DATE · 인덱스 없음 · 배치마다 TRUNCATE
  • PKG_REC_SCORE.DECAY(product_id, view_date): SQL 없이 패키지 배열로 최근성 가중치를 계산하는 PL/SQL 함수. 추천 화면도 같은 함수를 쓴다.

원본 SQL ​

sql
PROCEDURE LOAD_VIEW_FEED IS
  CURSOR c IS SELECT view_id, product_id, customer_id, view_date FROM product_views;
  TYPE t_num IS TABLE OF NUMBER       INDEX BY PLS_INTEGER;
  TYPE t_dt  IS TABLE OF DATE         INDEX BY PLS_INTEGER;
  TYPE t_vc  IS TABLE OF VARCHAR2(20) INDEX BY PLS_INTEGER;
  v_id t_num;  v_pid t_num;  v_cid t_num;  v_dt t_dt;
  v_ts t_vc;   v_dy t_vc;    v_sc t_num;   v_ld t_dt;
BEGIN
  EXECUTE IMMEDIATE 'TRUNCATE TABLE rec_view_feed';
  OPEN c;
  LOOP
    FETCH c BULK COLLECT INTO v_id, v_pid, v_cid, v_dt LIMIT 1000;
    EXIT WHEN v_id.COUNT = 0;
    FOR i IN 1 .. v_id.COUNT LOOP
      SELECT TO_CHAR(v_dt(i), 'YYYY-MM-DD"T"HH24:MI:SS') INTO v_ts(i) FROM dual;
      SELECT DECODE(TO_CHAR(v_dt(i), 'DY', 'NLS_DATE_LANGUAGE=ENGLISH'),
                    'SAT', 'WEEKEND', 'SUN', 'WEEKEND', 'WEEKDAY') INTO v_dy(i) FROM dual;
      SELECT SYSDATE INTO v_ld(i) FROM dual;
      v_sc(i) := PKG_REC_SCORE.DECAY(v_pid(i), v_dt(i));
    END LOOP;
    FORALL i IN 1 .. v_id.COUNT
      INSERT INTO rec_view_feed (view_id, product_id, customer_id, view_ts, day_tp, score, load_dt)
      VALUES (v_id(i), v_pid(i), v_cid(i), v_ts(i), v_dy(i), v_sc(i), v_ld(i));
  END LOOP;
  CLOSE c;
  COMMIT;
END;

실행계획 학습용 예시 ​

xplan
[세션 통계 — LOAD_VIEW_FEED 한 번 (V$MYSTAT 증가분)]
  경과 시간                  1.02 ~ 1.08 초
  CPU used by this session   0.99 초
  recursive calls            300,232
  execute count              300,128
  session logical reads      2,436

[TKPROF — 문장별 요약 (sys=no)]
  SQL                                                            Execute    Fetch      rows   query   elapsed
  SELECT TO_CHAR(:B1 , 'YYYY-MM-DD"T"HH24:MI:SS') FROM DUAL      100,000  100,000   100,000       0      0.27
  SELECT DECODE(TO_CHAR(:B1 , 'DY', …), …) FROM DUAL             100,000  100,000   100,000       0      0.28
  SELECT SYSDATE FROM DUAL                                       100,000  100,000   100,000       0      0.25
  INSERT INTO REC_VIEW_FEED (…) VALUES (:B7 , …)                     100        0   100,000      19      0.09
  SELECT VIEW_ID, PRODUCT_ID, CUSTOMER_ID, VIEW_DATE FROM …            1      101   100,000     468      0.02
  (DUAL 세 문장의 계획은 FAST DUAL · Buffers 0)

문제 ​

  1. 천 건씩 묶었는데도 recursive calls가 30만 번인 이유를 TKPROF로 설명하시오.
  2. 적재 결과가 바뀌지 않게 프로시저를 고치시오. PL/SQL로 옮길 때 그대로 옮길 수 없는 식이 있다면 어떻게 바꿨는지도 쓰시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자