Skip to content
튜닝 연습인덱스 스캔 효율과 테이블 액세스
ACC-075

쿠폰 시스템 오픈을 앞두고 결제 화면의 「쓸 수 있는 쿠폰」이 열릴 때마다 쿠폰 80만 장을 다 읽는다

실전DDL한빛유통150 / 700

상황 ​

한빛유통이 쿠폰 시스템을 새로 연다. 결제 화면의 「쓸 수 있는 쿠폰」(S1)이 쿠폰 테이블을 통째로 읽는다는 점검 결과가 나오자, 개발팀은 인덱스 셋을 요청했다. "(CUS_ID, USE_YN, EXP_DT), (USE_YN, EXP_DT), (CUS_ID, EXP_DT)" 조민우는 첫째 대신 '안 쓴 쿠폰만 담는' 함수 기반 인덱스를 쓰자고 덧붙였다. 이 테이블은 읽기보다 쓰기가 많다. 결제 화면에서 쿠폰을 고르면 다른 기기에서 같은 쿠폰을 쓰지 못하게 바로 USE_YN을 'Y'로 잡고, 결제를 끝내지 않거나 다른 쿠폰으로 바꾸거나 주문을 취소하면 'N'으로 되돌린다. 박팀장의 주문은 이렇다. "하루 숫자로 따져서 최소한으로. USE_YN을 인덱스에 넣을지부터 정해 와라."

테이블과 인덱스 ​

  • T_CPN 발급 쿠폰(이 문제에서 쓰는 파생 테이블): 800,000행 · 6,020블록 · CPN_NO CHAR(16) · CUS_ID · ISU_DT · EXP_DT · USE_YN · USE_DT · DC_AMT · 최근 1년 발급분만 두고 발급 순서로 쌓인다(지난 쿠폰은 매달 이력으로 옮긴다)
  • 발급 하루 약 2,200장(캠페인 배치와 실시간 발급) · 유효기간 14일 · 고객 2,000명이라 고객당 약 400장
  • USE_YN: 'Y' 약 96,000장(12%) · 'N' 약 704,000장(88%) · 'N'의 대부분은 쓰지 않은 채 기한이 지난 쿠폰이다(만료 처리는 따로 하지 않는다)
  • 고객 한 명이 가진 기한 안 지난 쿠폰은 평균 15장이고, 그중 약 3장은 이미 썼거나 지금 결제 중에 잡혀 있다
  • 인덱스 PK_T_CPN (CPN_NO): BLEVEL 2 · 리프 약 3,330블록 · 다른 인덱스는 없다 · 통계 최신

원본 SQL ​

sql
-- S1 결제 화면 「쓸 수 있는 쿠폰」 · 하루 약 9,000번
SELECT CPN_NO, DC_AMT, EXP_DT
  FROM T_CPN
 WHERE CUS_ID = :v_cus
   AND USE_YN = 'N'
   AND EXP_DT >= SYSDATE
 ORDER BY DC_AMT DESC;

-- S2 쿠폰 잡기 · 놓기 · 하루 약 2만 6천 번 · 약 2만 5천 7백 번
UPDATE T_CPN SET USE_YN = 'Y', USE_DT = SYSDATE WHERE CPN_NO = :v_cpn AND USE_YN = 'N';
UPDATE T_CPN SET USE_YN = 'N', USE_DT = NULL    WHERE CPN_NO = :v_cpn AND USE_YN = 'Y';

-- S3 앱 「내 쿠폰함」(기한 안 지난 쿠폰과 지난 30일 안에 끝난 쿠폰) · 하루 약 4,000번
SELECT CPN_NO, DC_AMT, EXP_DT, USE_YN
  FROM T_CPN
 WHERE CUS_ID = :v_cus
   AND EXP_DT >= TRUNC(SYSDATE) - 30
 ORDER BY EXP_DT;

-- S4 「만료 임박 알림」 배치 · 매일 07:00 한 번
SELECT CUS_ID, CPN_NO, EXP_DT
  FROM T_CPN
 WHERE USE_YN = 'N'
   AND EXP_DT >= TRUNC(SYSDATE) + 3
   AND EXP_DT <  TRUNC(SYSDATE) + 4;

-- S5 발급 · 하루 약 2,200건
INSERT INTO T_CPN (CPN_NO, CUS_ID, ISU_DT, EXP_DT, USE_YN, DC_AMT)
VALUES (:v_cpn, :v_cus, SYSDATE, TRUNC(SYSDATE) + 14, 'N', :v_amt);

-- 요청서와 조민우 안
CREATE INDEX IX_CPN_CUS_YN_EXP ON T_CPN (CUS_ID, USE_YN, EXP_DT);
CREATE INDEX IX_CPN_YN_EXP     ON T_CPN (USE_YN, EXP_DT);
CREATE INDEX IX_CPN_CUS_EXP    ON T_CPN (CUS_ID, EXP_DT);
CREATE INDEX IX_CPN_UNUSED     ON T_CPN (CASE WHEN USE_YN = 'N' THEN CUS_ID END,
                                         CASE WHEN USE_YN = 'N' THEN EXP_DT END);

실행계획 학습용 예시 · T_CPN은 이 문제에서 정의한 파생 테이블 · UPDATE 한 번의 블록 변경과 redo는 교재 04화 §9 비례 ​

xplan
[S1 · CUS_000123 · 지금 (PK만)]
---------------------------------------------------------------------------------------------------
| Id  | Operation          | Name  | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Used-Mem |
---------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |       |      1 |        |     12 |00:00:00.04 |    6020 |          |
|   1 |  SORT ORDER BY     |       |      1 |      5 |     12 |00:00:00.04 |    6020 | 2048  (0)|
|*  2 |   TABLE ACCESS FULL| T_CPN |      1 |      5 |     12 |00:00:00.04 |    6020 |          |
---------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   2 - filter(("CUS_ID"=:V_CUS AND "USE_YN"='N' AND "EXP_DT">=SYSDATE@!))

[시험 DB에 요청서 인덱스를 만들어 잰 값 · INVISIBLE 인덱스도 DML 때는 고쳐지므로 S2와 S5는 그대로 잴 수 있다]
  인덱스 구성                               S1 Buffers   S2 한 번 (블록 변경 · redo)   S5 한 번 (블록 변경)
  PK만                                         6,020       3회 · 약 380바이트             2회
  PK + (CUS_ID, EXP_DT)                           18       3회 · 약 380바이트             3회
  PK + (CUS_ID, USE_YN, EXP_DT)                   15       7회 · 약 900바이트             3회
  PK + 요청서 셋                                  15      11회 · 약 1,420바이트           5회
  PK + (CUS_ID, EXP_DT) + 조민우 안               15       5회 · 약 640바이트             4회
  조민우 안의 크기 : 항목 약 704,000개 · 리프 약 3,130블록 (PK 3,330블록과 비슷하다)

문제 ​

  1. S1 하나만 보면 요청서의 (CUS_ID, USE_YN, EXP_DT)가 가장 적게 읽는다. 그런데도 USE_YN을 인덱스에 넣을지는 S2를 함께 봐야 하는 이유를, USE_YN이 바뀔 때 인덱스에서 일어나는 일과 하루 숫자로 설명하시오.
  2. 조민우의 함수 기반 인덱스가 이 테이블에서 생각만큼 작지 않은 이유를 쓰고, USE_YN을 테이블에서 거르는 것과 비교해 어떤 대가가 있는지 설명하시오.
  3. S1 ~ S5를 함께 보고 최소 인덱스 세트를 설계해 DDL을 작성하고, SQL마다 어떤 길로 몇 블록을 읽을지 어림하시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자