테마
한빛유통이 쿠폰 시스템을 새로 연다. 결제 화면의 「쓸 수 있는 쿠폰」(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년 발급분만 두고 발급 순서로 쌓인다(지난 쿠폰은 매달 이력으로 옮긴다)USE_YN: 'Y' 약 96,000장(12%) · 'N' 약 704,000장(88%) · 'N'의 대부분은 쓰지 않은 채 기한이 지난 쿠폰이다(만료 처리는 따로 하지 않는다)PK_T_CPN (CPN_NO): BLEVEL 2 · 리프 약 3,330블록 · 다른 인덱스는 없다 · 통계 최신-- 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);
[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블록과 비슷하다)