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

코드 정리 배포 다음 날부터 고객센터 화면의 「취소 급증」 칸만 한 박자 늦게 뜬다

중급PLSQL한빛유통582 / 700

상황 ​

한빛유통 고객센터 화면 오른쪽의 「취소 급증」 칸은 최근 30일 취소 주문의 건수와 금액을 보여 준다. 상담 화면을 열 때마다 갱신되어 하루 약 2만 번 호출된다. 같은 프로시저 PKG_STLM_STAT.status_sum을 정산 대시보드의 「미결」 칸과 「완료」 칸(하루 3,000번씩), 02:00 일 마감 배치(완료 한 번)도 부른다. 2021년에 강도현이 상태값마다 리터럴 문장을 따로 두어 만든 프로시저다. 지난주 개발팀이 정적 분석 도구의 「같은 문자열 리터럴 반복」 경고를 없애려고 상태값을 패키지 상수(PKG_CONST)로 모았다. 배포 다음 날부터 「취소 급증」 칸만 한 박자 늦게 뜨고, 이 프로시저의 SQL이 Top SQL에 올랐다. 개발자는 이렇게 말했다. "분기는 그대로 두고 리터럴을 상수로 바꾸기만 했어요. SQL 코딩 가이드 2조에도 '상수로 쓴다'고 되어 있고요."

테이블과 인덱스 ​

  • T_ORD_BIG: 1,000,000행 · 5,826블록 · ORD_DT DATE(전부 자정) 하루 약 913건 · ORD_ST VARCHAR2(10) COMPLETE 90%, PENDING 8%, CANCEL 2% · FREQUENCY 히스토그램 · 행은 ORD_ST, ORD_DT 순서와 상관없이 흩어져 있다
  • 최근 30일 27,390건 가운데 CANCEL 548건 · PENDING 2,191건 · COMPLETE 24,651건
  • 인덱스 IX_ORD_BIG_ST_DT (ORD_ST, ORD_DT)(이 문제에만 있다): 높이 3 · 리프 약 3,000블록
  • 통계는 매일 01:00에 모은다(no_invalidate => FALSE). 01:00 뒤 이 프로시저를 처음 부르는 것은 02:00 일 마감 배치다.
  • CURSOR_SHARING = EXACT · 바인드 피킹 켬 · PLSQL_OPTIMIZE_LEVEL = 2 · 화면 WAS의 커넥션 풀 30개

원본 SQL ​

sql
-- PKG_CONST (지난주 신설) : 상태값 상수
CREATE OR REPLACE PACKAGE PKG_CONST IS
  c_st_complete  CONSTANT VARCHAR2(10) := 'COMPLETE';
  c_st_pending   CONSTANT VARCHAR2(10) := 'PENDING';
  c_st_cancel    CONSTANT VARCHAR2(10) := 'CANCEL';
END PKG_CONST;

-- PKG_STLM_STAT.status_sum (2021) : 지난주 분기 안의 리터럴을 상수로 바꿨다
PROCEDURE status_sum(p_st IN VARCHAR2, o_cnt OUT NUMBER, o_amt OUT NUMBER) IS
BEGIN
  -- 상태값마다 문장을 따로 둔다. 값마다 맞는 계획이 다르다. (2021 hd.kang)
  CASE p_st
    WHEN PKG_CONST.c_st_cancel THEN
      SELECT COUNT(*), NVL(SUM(ORD_AMT), 0) INTO o_cnt, o_amt
        FROM T_ORD_BIG
       WHERE ORD_DT >= TRUNC(SYSDATE) - 30
         AND ORD_ST = PKG_CONST.c_st_cancel;
    WHEN PKG_CONST.c_st_pending THEN
      SELECT COUNT(*), NVL(SUM(ORD_AMT), 0) INTO o_cnt, o_amt
        FROM T_ORD_BIG
       WHERE ORD_DT >= TRUNC(SYSDATE) - 30
         AND ORD_ST = PKG_CONST.c_st_pending;
    WHEN PKG_CONST.c_st_complete THEN
      SELECT COUNT(*), NVL(SUM(ORD_AMT), 0) INTO o_cnt, o_amt
        FROM T_ORD_BIG
       WHERE ORD_DT >= TRUNC(SYSDATE) - 30
         AND ORD_ST = PKG_CONST.c_st_complete;
  END CASE;
END;

실행계획 학습용 예시 ​

xplan
[배포 전 · V$SQL · 지난주 화요일 · 상태값마다 문장 하나]
 SQL_TEXT (줄임)                  EXECUTIONS  PLAN_HASH   계획                                   BUFFER_GETS/EXEC
 ... AND ORD_ST = 'CANCEL'            20,000  1520374851  INDEX RANGE SCAN (IX_ORD_BIG_ST_DT)                 552
 ... AND ORD_ST = 'PENDING'            3,000  3876610259  TABLE ACCESS FULL                                 5,830
 ... AND ORD_ST = 'COMPLETE'           3,001  3876610259  TABLE ACCESS FULL                                 5,830

[배포 후 · V$SQL · 이번 주 화요일 · 문장 하나에 자식 하나]
 SQL_TEXT : SELECT COUNT(*), NVL(SUM(ORD_AMT), 0) FROM T_ORD_BIG WHERE ORD_DT >= TRUNC(SYSDATE) - 30 AND ORD_ST = :B1
 CHILD 0 · FIRST_LOAD_TIME 02:00:03 (일 마감 배치) · EXECUTIONS 26,001 · PARSE_CALLS 91 · PLAN_HASH 3876610259
 IS_BIND_SENSITIVE Y · IS_BIND_AWARE N · BUFFER_GETS/EXEC 5,830

[자식 0 · 「취소 급증」 칸 한 번 · FORMAT 'ALLSTATS LAST +PEEKED_BINDS']
Plan hash value: 3876610259

------------------------------------------------------------------------------------
| Id  | Operation           | Name      | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |           |      1 |        |      1 |00:00:00.09 |    5830 |
|   1 |  SORT AGGREGATE     |           |      1 |      1 |      1 |00:00:00.09 |    5830 |
|*  2 |   TABLE ACCESS FULL | T_ORD_BIG |      1 |  24651 |    548 |00:00:00.09 |    5830 |
------------------------------------------------------------------------------------

Peeked Binds (identified by name):
----------------------------------
   1 - :B1 (VARCHAR2(32), CSID=873): 'COMPLETE'

Predicate Information (identified by operation id):
---------------------------------------------------
   2 - filter(("ORD_ST"=:B1 AND "ORD_DT">=TRUNC(SYSDATE@!)-30))

문제 ​

  1. 리터럴을 상수로 바꾸기만 했는데 SQL_ID 셋이 하나가 되고 「취소 급증」 칸이 5,830블록을 읽게 된 이유를 위 자료로 설명하시오.
  2. 칸마다 제 계획을 타도록 프로시저를 고치시오. 상태값마다 프로시저를 나눠 리터럴을 쓰는 안, 인자와 CASE는 그대로 두고 분기 안의 SQL에 리터럴을 쓰는 안, 허용 목록으로 검사한 값을 리터럴로 이어 붙이는 동적 SQL 안 가운데 무엇을 골랐는지와 그 이유를 쓰시오.
  3. 같은 일이 다시 생기지 않도록 무엇을 남길지 쓰시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자