테마
「주문 조회」 메뉴에는 기간(하루 ~ 석 달)을 고르는 화면이 셋 있다. S1 「기간 매출 요약」은 정산팀 대시보드가 하루 2천 번쯤 부르고, 기간은 보통 한 달이다. S2 「기간 주문 목록」은 상담원이 하루 5천 번쯤 열고 첫 화면 50건만 본다. S3 「엑셀 다운로드」는 하루 수십 번이고 주로 한 달 ~ 석 달이다.
새로 들인 SQL 점검 도구가 S1과 S3에 'TABLE ACCESS FULL 경보'를 띄우자, 개발 리더가 표준안을 올렸다. "기간 조회는 인덱스를 타야 한다. 세 SQL 모두 /*+ INDEX(o IX_T_ORD_DT) */를 넣는다." 박팀장은 "SQL마다 따로, 숫자로 판단해 와라"라고 했다.
T_ORD: 100,000행 · 896블록(블록당 약 112행) · ORD_SEQ 순으로 쌓였다 · ORD_DT DATE(시분초 있음, 2023 ~ 2024) · 하루 약 137건 · ORD_DT는 주문 번호와 상관없이 730일을 돌아가며 붙어 있다ORD_ST 3종IX_T_ORD_DT (ORD_DT): BLEVEL 1 · 리프 약 265블록 · CLUSTERING_FACTOR 99,986 · 통계 최신PK_T_ORD (ORD_SEQ) · 주문은 운영에서 계속 INSERT된다(하루 약 137건)-- 표준안을 적용한 세 SQL (FROM T_ORD o)
-- S1 「기간 매출 요약」
SELECT /*+ INDEX(o IX_T_ORD_DT) */
o.ORD_ST, COUNT(*) AS cnt, SUM(o.ORD_AMT) AS amt
FROM T_ORD o
WHERE o.ORD_DT >= TO_DATE(:v_from, 'YYYYMMDD')
AND o.ORD_DT < TO_DATE(:v_to, 'YYYYMMDD') + 1
GROUP BY o.ORD_ST;
-- S2 「기간 주문 목록」 첫 화면
SELECT /*+ INDEX(o IX_T_ORD_DT) */
o.ORD_SEQ, o.CUS_ID, o.ORD_DT, o.ORD_ST, o.ORD_AMT
FROM T_ORD o
WHERE o.ORD_DT >= TO_DATE(:v_from, 'YYYYMMDD')
AND o.ORD_DT < TO_DATE(:v_to, 'YYYYMMDD') + 1
ORDER BY o.ORD_DT DESC
FETCH FIRST 50 ROWS ONLY;
-- S3 「엑셀 다운로드」
SELECT /*+ INDEX(o IX_T_ORD_DT) */
o.ORD_SEQ, o.CUS_ID, o.ORD_DT, o.ORD_ST, o.PAY_DT, o.PAY_TP, o.ORD_AMT, o.PAY_AMT
FROM T_ORD o
WHERE o.ORD_DT >= TO_DATE(:v_from, 'YYYYMMDD')
AND o.ORD_DT < TO_DATE(:v_to, 'YYYYMMDD') + 1
ORDER BY o.ORD_DT;
[표준안 · S1 · 2024년 6월]
-------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 3 |00:00:00.02 | 4122 |
| 1 | HASH GROUP BY | | 1 | 3 | 3 |00:00:00.02 | 4122 |
|* 2 | FILTER | | 1 | | 4110 |00:00:00.02 | 4122 |
| 3 | TABLE ACCESS BY INDEX ROWID BATCHED| T_ORD | 1 | 4110 | 4110 |00:00:00.02 | 4122 |
|* 4 | INDEX RANGE SCAN | IX_T_ORD_DT | 1 | 4110 | 4110 |00:00:00.01 | 12 |
-------------------------------------------------------------------------------------------------------------
2 - filter(TO_DATE(:V_TO,'YYYYMMDD')+1>TO_DATE(:V_FROM,'YYYYMMDD'))
4 - access("O"."ORD_DT">=TO_DATE(:V_FROM,'YYYYMMDD') AND "O"."ORD_DT"<TO_DATE(:V_TO,'YYYYMMDD')+1)
[표준안 · S2 · 2024년 6월 · 첫 화면 50건]
-------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 50 |00:00:00.01 | 52 |
|* 1 | VIEW | | 1 | 50 | 50 |00:00:00.01 | 52 |
|* 2 | WINDOW NOSORT STOPKEY | | 1 | 4110 | 50 |00:00:00.01 | 52 |
|* 3 | FILTER | | 1 | | 50 |00:00:00.01 | 52 |
| 4 | TABLE ACCESS BY INDEX ROWID | T_ORD | 1 | 4110 | 50 |00:00:00.01 | 52 |
|* 5 | INDEX RANGE SCAN DESCENDING | IX_T_ORD_DT | 1 | 4110 | 50 |00:00:00.01 | 2 |
-------------------------------------------------------------------------------------------------------------
1 - filter("from$_subquery$_002"."rowlimit_$$_rownumber"<=50)
2 - filter(ROW_NUMBER() OVER ( ORDER BY INTERNAL_FUNCTION("O"."ORD_DT") DESC )<=50)
[측정 요약 · Buffers] 표준안(INDEX 힌트) 힌트 없음(지금, 옵티마이저 선택)
S1 하루 / 한 달 / 석 달 139 / 4,122 / 12,501 139 / 896 / 896 (한 달부터 FULL)
S2 첫 화면 50건 (기간과 상관없이) 52 52 (지금도 인덱스 역순)
S3 하루 / 한 달 / 석 달 139 / 4,122 / 12,501 139 / 896 / 896 (FULL + SORT ORDER BY)