Skip to content
튜닝 연습서브쿼리와 쿼리 변환
SUBQ-067

월말 마감 주간마다 경영 리포트 화면의 SQL 셋이 논리 읽기 1 ~ 3위를 차지한다

실전SELECT한빛유통302 / 700

상황 ​

경영지원팀의 「월 주문 리포트」 화면은 탭 세 개(R1 등급과 상태별, R2 일별 추이와 합계, R3 상태별 전월 대비)를 한꺼번에 띄운다. 평소에는 하루 10번 남짓 열지만 월말 마감 주간에는 팀장들이 하루 150번쯤 열고, 그 주에는 이 화면의 SQL 셋이 AWR의 SQL ordered by Gets 1 ~ 3위를 차지한다. 박팀장은 "세 SQL을 각각 고치고, 인덱스를 만들지 요약 테이블을 만들지도 정해 달라"라고 했다. 아래는 2024년 6월 리포트를 열었을 때의 실행계획이다.

테이블과 인덱스 ​

  • T_ORD_BIG: 1,000,000행 · 5,826블록 · 이 문제에서는 인덱스가 없다 · ORD_DT DATE(전부 자정) · ORD_ST는 COMPLETE · PENDING · CANCEL 세 가지
  • 2024년 6월 27,390건 · 5월 28,301건 · 주문은 하루 약 900건 들어오고, 화면에서 한 건씩 넣는다
  • M_CUS: 2,000행 · 30블록 · CUS_GD A · B · C
  • 통계 최신 · 화면은 그달 1일과 다음 달 1일(:d1, :d2), 전달 1일(:d0)을 DATE로 넘긴다

원본 SQL ​

sql
-- R1 등급 · 상태별
SELECT c.CUS_GD, 'COMPLETE' AS ORD_ST, COUNT(*) AS CNT, SUM(o.ORD_AMT) AS AMT
  FROM T_ORD_BIG o, M_CUS c
 WHERE c.CUS_ID = o.CUS_ID AND o.ORD_DT >= :d1 AND o.ORD_DT < :d2 AND o.ORD_ST = 'COMPLETE'
 GROUP BY c.CUS_GD
UNION ALL
SELECT c.CUS_GD, 'PENDING', COUNT(*), SUM(o.ORD_AMT)
  FROM T_ORD_BIG o, M_CUS c
 WHERE c.CUS_ID = o.CUS_ID AND o.ORD_DT >= :d1 AND o.ORD_DT < :d2 AND o.ORD_ST = 'PENDING'
 GROUP BY c.CUS_GD
UNION ALL
SELECT c.CUS_GD, 'CANCEL', COUNT(*), SUM(o.ORD_AMT)
  FROM T_ORD_BIG o, M_CUS c
 WHERE c.CUS_ID = o.CUS_ID AND o.ORD_DT >= :d1 AND o.ORD_DT < :d2 AND o.ORD_ST = 'CANCEL'
 GROUP BY c.CUS_GD;

-- R2 일별 추이와 합계 줄
SELECT TO_CHAR(ORD_DT, 'YYYY-MM-DD') AS DT, COUNT(*) AS CNT, SUM(ORD_AMT) AS AMT
  FROM T_ORD_BIG
 WHERE ORD_DT >= :d1 AND ORD_DT < :d2
 GROUP BY ORD_DT
UNION ALL
SELECT '합계', COUNT(*), SUM(ORD_AMT)
  FROM T_ORD_BIG
 WHERE ORD_DT >= :d1 AND ORD_DT < :d2
 ORDER BY 1;

-- R3 상태별 전월 대비
SELECT 'THIS' AS MON, ORD_ST, COUNT(*) AS CNT, SUM(ORD_AMT) AS AMT
  FROM T_ORD_BIG
 WHERE ORD_DT >= :d1 AND ORD_DT < :d2
 GROUP BY ORD_ST
UNION
SELECT 'PREV', ORD_ST, COUNT(*), SUM(ORD_AMT)
  FROM T_ORD_BIG
 WHERE ORD_DT >= :d0 AND ORD_DT < :d1
 GROUP BY ORD_ST;

실행계획 학습용 예시 ​

xplan
[R1]
-----------------------------------------------------------------------------------------------
| Id  | Operation              | Name      | Starts | E-Rows | A-Rows |      A-Time | Buffers |
-----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT       |           |      1 |        |      9 | 00:00:00.33 |   17568 |
|   1 |  UNION-ALL             |           |      1 |        |      9 | 00:00:00.33 |   17568 |
|   2 |   HASH GROUP BY        |           |      1 |      3 |      3 | 00:00:00.12 |    5856 |
|*  3 |    HASH JOIN           |           |      1 |  24539 |  24651 | 00:00:00.11 |    5856 |
|   4 |     TABLE ACCESS FULL  | M_CUS     |      1 |   2000 |   2000 | 00:00:00.01 |      30 |
|*  5 |     TABLE ACCESS FULL  | T_ORD_BIG |      1 |  24539 |  24651 | 00:00:00.10 |    5826 |
|   6 |   HASH GROUP BY        |           |      1 |      3 |      3 | 00:00:00.11 |    5856 |
|*  7 |    HASH JOIN           |           |      1 |   2181 |   2191 | 00:00:00.10 |    5856 |
|   8 |     TABLE ACCESS FULL  | M_CUS     |      1 |   2000 |   2000 | 00:00:00.01 |      30 |
|*  9 |     TABLE ACCESS FULL  | T_ORD_BIG |      1 |   2181 |   2191 | 00:00:00.10 |    5826 |
|  10 |   HASH GROUP BY        |           |      1 |      3 |      3 | 00:00:00.10 |    5856 |
|* 11 |    HASH JOIN           |           |      1 |    545 |    548 | 00:00:00.10 |    5856 |
|  12 |     TABLE ACCESS FULL  | M_CUS     |      1 |   2000 |   2000 | 00:00:00.01 |      30 |
|* 13 |     TABLE ACCESS FULL  | T_ORD_BIG |      1 |    545 |    548 | 00:00:00.10 |    5826 |
-----------------------------------------------------------------------------------------------
   3 - access("C"."CUS_ID"="O"."CUS_ID")
   5 - filter(("O"."ORD_ST"='COMPLETE' AND "O"."ORD_DT">=:D1 AND "O"."ORD_DT"<:D2))
   (9번, 13번 줄은 상태만 PENDING, CANCEL)

[R2]
-----------------------------------------------------------------------------------------------
| Id  | Operation              | Name      | Starts | E-Rows | A-Rows |      A-Time | Buffers |
-----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT       |           |      1 |        |     31 | 00:00:00.21 |   11652 |
|   1 |  SORT ORDER BY         |           |      1 |     31 |     31 | 00:00:00.21 |   11652 |
|   2 |   UNION-ALL            |           |      1 |        |     31 | 00:00:00.21 |   11652 |
|   3 |    HASH GROUP BY       |           |      1 |     30 |     30 | 00:00:00.11 |    5826 |
|*  4 |     TABLE ACCESS FULL  | T_ORD_BIG |      1 |  27266 |  27390 | 00:00:00.10 |    5826 |
|   5 |    SORT AGGREGATE      |           |      1 |      1 |      1 | 00:00:00.10 |    5826 |
|*  6 |     TABLE ACCESS FULL  | T_ORD_BIG |      1 |  27266 |  27390 | 00:00:00.10 |    5826 |
-----------------------------------------------------------------------------------------------
   4 - filter(("ORD_DT">=:D1 AND "ORD_DT"<:D2))
   6 - filter(("ORD_DT">=:D1 AND "ORD_DT"<:D2))

[R3]
-----------------------------------------------------------------------------------------------
| Id  | Operation              | Name      | Starts | E-Rows | A-Rows |      A-Time | Buffers |
-----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT       |           |      1 |        |      6 | 00:00:00.21 |   11652 |
|   1 |  SORT UNIQUE           |           |      1 |      6 |      6 | 00:00:00.21 |   11652 |
|   2 |   UNION-ALL            |           |      1 |        |      6 | 00:00:00.21 |   11652 |
|   3 |    HASH GROUP BY       |           |      1 |      3 |      3 | 00:00:00.11 |    5826 |
|*  4 |     TABLE ACCESS FULL  | T_ORD_BIG |      1 |  27266 |  27390 | 00:00:00.10 |    5826 |
|   5 |    HASH GROUP BY       |           |      1 |      3 |      3 | 00:00:00.10 |    5826 |
|*  6 |     TABLE ACCESS FULL  | T_ORD_BIG |      1 |  28190 |  28301 | 00:00:00.10 |    5826 |
-----------------------------------------------------------------------------------------------
   4 - filter(("ORD_DT">=:D1 AND "ORD_DT"<:D2))
   6 - filter(("ORD_DT">=:D0 AND "ORD_DT"<:D1))

[화면 한 번]  17,568 + 11,652 + 11,652 = 40,872블록 · 월말 주간 하루 150번이면 약 613만 블록

문제 ​

  1. 화면 한 번에 읽는 블록이 어디서 나오는지 SQL마다 줄 번호와 Buffers로 설명하시오.
  2. 세 SQL을 각각 결과가 바뀌지 않게 T_ORD_BIG을 한 번만 읽도록 고치시오. R2의 합계 줄과 R3의 UNION을 어떻게 처리했는지 근거를 쓰시오.
  3. 고친 SQL에 더해 인덱스를 만드는 안과 일 단위 요약 테이블을 만드는 안을 일량과 부작용으로 비교하고 하나를 고르시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자