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

주문 관리 메뉴의 SQL 다섯 개 가운데 무엇을 고칠지 실행계획만 보고 정해 달라고 한다

실전SELECT한빛유통122 / 700

상황 ​

한빛유통은 분기마다 튜닝 리뷰를 연다. 이번에는 「주문 관리」 메뉴의 SQL 다섯 개가 올라왔다. 박팀장은 이렇게 말했다. "다섯 개 다 인덱스를 탄다는데 실행계획에 FULL SCAN, SKIP SCAN 같은 말이 섞여 있어 불안하다. 무엇을 고치고 무엇을 둘지 근거와 함께 정리해 와라." Q1은 하루 수백 번, Q2와 Q3는 하루 몇 번 내려받는 파일, Q4는 하루 수천 번, Q5는 하루 수백 번 호출된다.

테이블과 인덱스 ​

  • T_ORD: 100,000행 · 896블록(블록당 약 112행) · ORD_SEQ 순으로 쌓였다 · ORD_DT DATE(시분초 있음, 2023 ~ 2024, 하루 약 137건) · 같은 날짜의 주문은 ORD_SEQ가 730씩 떨어져 테이블 전체에 흩어져 있다 · 2024-06-14 137건 · 2024년 6월 4,110건
  • CUS_ID NOT NULL 2,000명(고객당 50건, 역시 흩어져 있다) · PAY_TP CARD · CASH · POINT가 1/3씩(하루 안에서도 셋이 고르게 있다)
  • PK_T_ORD (ORD_SEQ): BLEVEL 1 · 리프 약 190블록 · CLUSTERING_FACTOR 896
  • IX_T_ORD_CUS (CUS_ID): BLEVEL 1 · 리프 약 330블록 · CLUSTERING_FACTOR 약 100,000
  • IX_T_ORD_TP_DT (PAY_TP, ORD_DT): BLEVEL 1 · 리프 약 330블록 · CLUSTERING_FACTOR 약 100,000 · 통계 최신
  • 세션의 정렬 작업 영역은 약 20MB까지다

원본 SQL ​

sql
-- Q1 「전체 주문 수」 (메뉴 머리말 · 하루 수백 번)
SELECT COUNT(*) FROM T_ORD;

-- Q2 「주문 목록 받기 · 주문번호순」 (하루 몇 번 · 10만 행 전부)
SELECT ORD_SEQ, CUS_ID, ORD_DT, ORD_AMT
  FROM T_ORD
 ORDER BY ORD_SEQ;

-- Q3 「주문 목록 받기 · 고객순」 (하루 몇 번 · 10만 행 전부 · 미리보기가 느리다고 힌트를 넣었다)
SELECT /*+ FIRST_ROWS(100) */ ORD_SEQ, CUS_ID, ORD_DT, ORD_AMT
  FROM T_ORD
 ORDER BY CUS_ID;

-- Q4 「결제수단별 일 건수」 (하루 수천 번)
SELECT PAY_TP, COUNT(*)
  FROM T_ORD
 WHERE ORD_DT >= TO_DATE(:v_dt, 'YYYYMMDD')
   AND ORD_DT <  TO_DATE(:v_dt, 'YYYYMMDD') + 1
 GROUP BY PAY_TP;

-- Q5 「기간 주문 목록」 (하루 수백 번 · 보통 한 달 · 지난해 누군가 힌트를 넣었다)
SELECT /*+ INDEX_SS(o IX_T_ORD_TP_DT) */
       o.ORD_SEQ, o.CUS_ID, o.ORD_DT, o.PAY_TP, 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;

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

xplan
[Q1]
----------------------------------------------------------------------------------------------
| Id  | Operation             | Name     | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT      |          |      1 |        |      1 |00:00:00.01 |     197 |
|   1 |  SORT AGGREGATE       |          |      1 |      1 |      1 |00:00:00.01 |     197 |
|   2 |   INDEX FAST FULL SCAN| PK_T_ORD |      1 |    100K|    100K|00:00:00.01 |     197 |
----------------------------------------------------------------------------------------------

[Q2]
---------------------------------------------------------------------------------------------------
| Id  | Operation                   | Name     | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
---------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |          |      1 |        |    100K|00:00:00.08 |    1087 |
|   1 |  TABLE ACCESS BY INDEX ROWID| T_ORD    |      1 |    100K|    100K|00:00:00.08 |    1087 |
|   2 |   INDEX FULL SCAN           | PK_T_ORD |      1 |    100K|    100K|00:00:00.02 |     191 |
---------------------------------------------------------------------------------------------------

[Q3]
-------------------------------------------------------------------------------------------------------
| Id  | Operation                   | Name         | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
-------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |              |      1 |        |    100K|00:00:00.21 |     100K|
|   1 |  TABLE ACCESS BY INDEX ROWID| T_ORD        |      1 |    100K|    100K|00:00:00.21 |     100K|
|   2 |   INDEX FULL SCAN           | IX_T_ORD_CUS |      1 |    100K|    100K|00:00:00.02 |     331 |
-------------------------------------------------------------------------------------------------------
(Q3 0번 줄 Buffers 정확한 값 100,331)

[Q4 — 2024-06-14]
----------------------------------------------------------------------------------------------------
| Id  | Operation             | Name           | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
----------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT      |                |      1 |        |      3 |00:00:00.01 |       8 |
|   1 |  SORT GROUP BY NOSORT |                |      1 |      3 |      3 |00:00:00.01 |       8 |
|*  2 |   FILTER              |                |      1 |        |    137 |00:00:00.01 |       8 |
|*  3 |    INDEX SKIP SCAN    | IX_T_ORD_TP_DT |      1 |    137 |    137 |00:00:00.01 |       8 |
----------------------------------------------------------------------------------------------------
   2 - filter(TO_DATE(:V_DT,'YYYYMMDD')+1>TO_DATE(:V_DT,'YYYYMMDD'))
   3 - access("ORD_DT">=TO_DATE(:V_DT,'YYYYMMDD') AND "ORD_DT"<TO_DATE(:V_DT,'YYYYMMDD')+1)
       filter(("ORD_DT">=TO_DATE(:V_DT,'YYYYMMDD') AND "ORD_DT"<TO_DATE(:V_DT,'YYYYMMDD')+1))

[Q5 — 2024년 6월]
------------------------------------------------------------------------------------------------------------------
| Id  | Operation                            | Name           | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                     |                |      1 |        |   4110 |00:00:00.03 |    4129 |
|*  1 |  FILTER                              |                |      1 |        |   4110 |00:00:00.03 |    4129 |
|   2 |   TABLE ACCESS BY INDEX ROWID BATCHED| T_ORD          |      1 |   4110 |   4110 |00:00:00.03 |    4129 |
|*  3 |    INDEX SKIP SCAN                   | IX_T_ORD_TP_DT |      1 |   4110 |   4110 |00:00:00.01 |      19 |
------------------------------------------------------------------------------------------------------------------
   1 - filter(TO_DATE(:V_TO,'YYYYMMDD')+1>TO_DATE(:V_FROM,'YYYYMMDD'))
   3 - access("O"."ORD_DT">=TO_DATE(:V_FROM,'YYYYMMDD') AND "O"."ORD_DT"<TO_DATE(:V_TO,'YYYYMMDD')+1)
       filter(("O"."ORD_DT">=TO_DATE(:V_FROM,'YYYYMMDD') AND "O"."ORD_DT"<TO_DATE(:V_TO,'YYYYMMDD')+1))

[참고 — T_ORD FULL] 896

문제 ​

  1. Q1 ~ Q5가 각각 어떤 인덱스 스캔을 쓰는지, 그 스캔이 쓰인(또는 강제된) 이유를 한두 문장씩 설명하시오.
  2. Q2와 Q3는 같은 INDEX FULL SCAN인데 Buffers가 1,087과 100,331로 갈린다. 그 이유를 딕셔너리의 숫자로 설명하고, 고칠 쪽을 고치시오.
  3. Q4와 Q5는 같은 INDEX SKIP SCAN인데 하나는 두고 하나는 고쳐야 한다. 이유를 설명하고 고칠 SQL을 작성하시오. 고치지 않는 SQL은 왜 그대로 두는지도 쓰시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자