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

점검 도구의 FULL 경보를 없애려고 기간 조회 SQL 세 개에 같은 INDEX 힌트를 넣자는 표준안이 올라왔다

실전SELECT한빛유통101 / 700

상황 ​

「주문 조회」 메뉴에는 기간(하루 ~ 석 달)을 고르는 화면이 셋 있다. 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일을 돌아가며 붙어 있다
  • 2024-06-14 137건 · 2024년 6월 4,110건 · 2024년 2분기 12,467건 · ORD_ST 3종
  • 인덱스 IX_T_ORD_DT (ORD_DT): BLEVEL 1 · 리프 약 265블록 · CLUSTERING_FACTOR 99,986 · 통계 최신
  • 그 밖에 PK_T_ORD (ORD_SEQ) · 주문은 운영에서 계속 INSERT된다(하루 약 137건)

원본 SQL ​

sql
-- 표준안을 적용한 세 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;

실행계획 학습용 예시 · 건수는 적재 스크립트 그대로 ​

xplan
[표준안 · 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)

문제 ​

  1. 표준안대로 힌트를 넣으면 S1과 S3의 한 달 조회가 896 → 4,122블록이 되는 이유를 IX_T_ORD_DT의 BLEVEL, 리프, CLUSTERING_FACTOR로 분해하여 설명하시오. 이 테이블에서 이 인덱스가 FULL보다 싼 기간이 며칠까지인지 구하시오.
  2. S2는 같은 인덱스로 같은 기간을 고르는데 기간이 넓어져도 52블록으로 끝나는 이유를 설명하시오.
  3. 세 SQL의 접근 경로를 각각 정하고, 필요한 인덱스를 설계하여 SQL과 DDL을 작성하시오. 점검 도구의 'FULL 경보'를 어떻게 다룰지와 인덱스의 대가도 설명하시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자