Skip to content
튜닝 연습소트 튜닝과 페이징
SORT-043

매출 분석 개편 SQL 네 개 가운데 셋이 대용량 주문을 통째로 읽고, 고객 구매 달력 적재는 TEMP까지 쓴다

실전DDL한빛유통348 / 700

상황 ​

영업기획팀의 「매출 분석」 개편으로 T_ORD_BIG을 읽는 SQL 네 개(S1 ~ S4)가 새로 붙는다. 화면 S1은 하루 약 300번, 정산팀 화면 S3은 하루 약 600번, 야간 적재 S2와 월 리포트 S4는 한 번씩 돈다. 개발 DB에서 잰 실행계획을 보면 S1, S2, S4가 테이블을 통째로 읽고 S2는 TEMP까지 쓴다. 박팀장의 주문은 이렇다. "인덱스는 지금 있는 두 개에서 늘리지 않는 것이 목표다. SQL마다 정렬이나 묶기가 사라지는지 남는지 적고, 남기는 것은 왜 남겨도 되는지 써 달라."

테이블과 인덱스 ​

  • T_ORD_BIG: 1,000,000행 · 5,826블록 · ORD_SEQ, CUS_ID, ORD_ST, ORD_AMT는 NOT NULL이고 ORD_DT는 NULL 허용(실제 0건) · ORD_DT는 전부 자정이고 하루 약 913건 · 2024년 4분기 83,941건 · 고객 2,000명, 고객당 약 500건 · 같은 날짜의 주문도 한 고객의 주문도 테이블 곳곳에 흩어져 있다
  • 지금 있는 인덱스: IX_ORD_BIG_DT_SEQ (ORD_DT, ORD_SEQ)(09화, 주문 장부의 키 기반 페이징이 쓴다) · BLEVEL 2 · 리프 3,344블록 · CF 1,000,000 / IX_ORD_BIG_CUS_DT (CUS_ID, ORD_DT)(CS 화면의 고객 한 명 기간 조회가 쓴다) · BLEVEL 2 · 리프 약 3,000블록
  • T_CUS_DAY(고객 구매 달력, 이 문제용 테이블): 매일 밤 TRUNCATE 뒤 S2로 다시 채운다. 고객과 날짜의 조합은 약 80만 개다.
  • 02시의 work area 상한은 약 20MB다. 통계는 최신이다.

원본 SQL ​

sql
-- S1 「분기 일자별 상태별 매출」 (화면, 하루 약 300번)
SELECT ORD_DT, ORD_ST, COUNT(*) AS CNT, SUM(ORD_AMT) AS AMT
  FROM T_ORD_BIG
 WHERE ORD_DT >= :q_from AND ORD_DT < :q_to
 GROUP BY ORD_DT, ORD_ST
 ORDER BY ORD_DT, ORD_ST;

-- S2 「고객 구매 달력」 적재 (야간 한 번)
INSERT /*+ APPEND */ INTO T_CUS_DAY (CUS_ID, ORD_DT, CNT, AMT)
SELECT CUS_ID, ORD_DT, COUNT(*), SUM(ORD_AMT)
  FROM T_ORD_BIG
 GROUP BY CUS_ID, ORD_DT;

-- S3 「일 주문 장부」 (정산팀 화면, 하루 약 600번)
SELECT ORD_SEQ, CUS_ID, ORD_AMT
  FROM T_ORD_BIG
 WHERE ORD_DT >= :d AND ORD_DT < :d + 1
 ORDER BY ORD_SEQ;

-- S4 「고객별 연간 매출 순위」 (월 리포트)
SELECT CUS_ID, SUM(ORD_AMT) AS AMT
  FROM T_ORD_BIG
 WHERE ORD_DT >= DATE '2024-01-01' AND ORD_DT < DATE '2025-01-01'
 GROUP BY CUS_ID
 ORDER BY AMT DESC;

실행계획 학습용 예시 ​

xplan
[S1 · 2024년 4분기]
------------------------------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                            | Name              | Starts | E-Rows | A-Rows |   A-Time   | Buffers |  OMem |  1Mem | Used-Mem | Used-Tmp|
------------------------------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                     |                   |      1 |        |    276 |00:00:00.21 |    5826 |       |       |          |         |
|   1 |  SORT GROUP BY                       |                   |      1 |    276 |    276 |00:00:00.21 |    5826 | 31744 | 31744 |28672  (0)|         |
|*  2 |   TABLE ACCESS FULL                  | T_ORD_BIG         |      1 |  83941 |  83941 |00:00:00.12 |    5826 |       |       |          |         |
------------------------------------------------------------------------------------------------------------------------------------------------
   2 - filter(("ORD_DT">=:Q_FROM AND "ORD_DT"<:Q_TO))

[S2]
------------------------------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                            | Name              | Starts | E-Rows | A-Rows |   A-Time   | Buffers |  OMem |  1Mem | Used-Mem | Used-Tmp|
------------------------------------------------------------------------------------------------------------------------------------------------
|   0 | INSERT STATEMENT                     |                   |      1 |        |      0 |00:00:06.31 |    5941 |       |       |          |         |
|   1 |  LOAD AS SELECT                      | T_CUS_DAY         |      1 |        |      0 |00:00:06.31 |    5941 |       |       |          |         |
|   2 |   OPTIMIZER STATISTICS GATHERING     |                   |      1 |   1000K|    802K|00:00:05.12 |    5833 |       |       |          |         |
|   3 |    HASH GROUP BY                     |                   |      1 |   1000K|    802K|00:00:04.97 |    5833 |    46M|  6118K|   20M (1)|      39M|
|   4 |     TABLE ACCESS FULL                | T_ORD_BIG         |      1 |   1000K|   1000K|00:00:00.19 |    5826 |       |       |          |         |
------------------------------------------------------------------------------------------------------------------------------------------------

[S3 · 하루]
------------------------------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                            | Name              | Starts | E-Rows | A-Rows |   A-Time   | Buffers |  OMem |  1Mem | Used-Mem | Used-Tmp|
------------------------------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                     |                   |      1 |        |    913 |00:00:00.01 |     918 |       |       |          |         |
|   1 |  SORT ORDER BY                       |                   |      1 |    913 |    913 |00:00:00.01 |     918 | 59392 | 59392 |53248  (0)|         |
|   2 |   TABLE ACCESS BY INDEX ROWID BATCHED| T_ORD_BIG         |      1 |    913 |    913 |00:00:00.01 |     918 |       |       |          |         |
|*  3 |    INDEX RANGE SCAN                  | IX_ORD_BIG_DT_SEQ |      1 |    913 |    913 |00:00:00.01 |       5 |       |       |          |         |
------------------------------------------------------------------------------------------------------------------------------------------------
   3 - access("ORD_DT">=:D AND "ORD_DT"<:D+1)

[S4]
------------------------------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                            | Name              | Starts | E-Rows | A-Rows |   A-Time   | Buffers |  OMem |  1Mem | Used-Mem | Used-Tmp|
------------------------------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                     |                   |      1 |        |   2000 |00:00:00.15 |    5826 |       |       |          |         |
|   1 |  SORT ORDER BY                       |                   |      1 |   2000 |   2000 |00:00:00.15 |    5826 | 83968 | 83968 |74752  (0)|         |
|   2 |   HASH GROUP BY                      |                   |      1 |   2000 |   2000 |00:00:00.15 |    5826 |  1201K|  1201K| 1421K (0)|         |
|*  3 |    TABLE ACCESS FULL                 | T_ORD_BIG         |      1 |    334K|    333K|00:00:00.09 |    5826 |       |       |          |         |
------------------------------------------------------------------------------------------------------------------------------------------------
   3 - filter(("ORD_DT">=TO_DATE(' 2024-01-01 00:00:00', 'syyyy-mm-dd hh24:mi:ss') AND
              "ORD_DT"<TO_DATE(' 2025-01-01 00:00:00', 'syyyy-mm-dd hh24:mi:ss')))

문제 ​

  1. S1 ~ S4가 지금 무엇 때문에 테이블을 통째로 읽거나 정렬하고 묶는지 SQL마다 실행계획의 줄로 설명하시오.
  2. 인덱스 개수를 두 개로 유지하면서 S1 ~ S4의 일량을 줄이는 인덱스를 DDL로 쓰고, SQL마다 그 인덱스를 어떻게 쓰는지와 정렬이나 묶기가 사라지는지 남는지 쓰시오.
  3. 남기는 정렬은 왜 남겨도 되는지, 바꾼 인덱스가 원래 쓰던 화면과 적재에 무엇을 바꾸는지 쓰시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자