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

결제 내역 화면은 검색 한 번에 SQL 세 개가 돌고, 셋 다 주문 테이블을 통째로 읽는다

실전DDL한빛유통335 / 700

상황 ​

정산팀의 「결제수단별 결제 내역」은 결제수단과 기간(기본 최근 1개월)을 넣으면 세 가지를 보여 준다. 위에는 「총 n건, 결제 합계 m원」, 아래에는 결제일시 최신순 목록(한 쪽 20건, 쪽 번호), 오른쪽 위에는 「엑셀 받기」 버튼이 있다. 화면은 검색할 때 Q1과 Q2를, 버튼을 누르면 Q3을 보낸다. 검색은 하루 약 3,000번, 엑셀 받기는 약 150번이다. 합계는 월말 대사에 그대로 쓰므로 정확해야 한다. T_ORD에는 PK 말고 인덱스가 없다. 박팀장은 "화면마다 인덱스를 하나씩 만들지 말고 꼭 필요한 만큼만"이라고 했다.

테이블과 인덱스 ​

  • T_ORD 주문: 100,000행 · 896블록 · ORD_SEQ · CUS_ID · ORD_DT · ORD_ST · PAY_DT(결제일시, 시분초 있음, 결제 전에는 NULL) · PAY_TP(결제수단 4종: CARD 70%, BANK 20%, PHONE 7%, POINT 3%) · ORD_AMT · PAY_AMT
  • 하루 주문 약 137건, 2년치다. 최근 1개월 결제는 약 4,100건이고 그중 CARD가 약 2,870건이다.
  • 결제는 대부분 주문 당일에 끝나서 PAY_DT 순서와 테이블에 쌓인 순서가 거의 같다.
  • 인덱스는 PK_T_ORD (ORD_SEQ)뿐이다. 통계는 최신이다.

원본 SQL ​

sql
-- Q1 : 총건수와 결제 합계
SELECT COUNT(*), SUM(PAY_AMT)
  FROM T_ORD
 WHERE PAY_TP = :tp
   AND PAY_DT >= :from_dt AND PAY_DT < :to_dt;

-- Q2 : 목록 (:page 쪽, 20건)
SELECT *
  FROM (SELECT ROWNUM rn, a.*
          FROM (SELECT ORD_SEQ, CUS_ID, PAY_DT, PAY_AMT, ORD_ST
                  FROM T_ORD
                 WHERE PAY_TP = :tp
                   AND PAY_DT >= :from_dt AND PAY_DT < :to_dt
                 ORDER BY PAY_DT DESC) a
         WHERE ROWNUM <= :page * 20)
 WHERE rn > (:page - 1) * 20;

-- Q3 : 엑셀 받기 (기간 전체)
SELECT ORD_SEQ, CUS_ID, PAY_DT, PAY_AMT, ORD_ST
  FROM T_ORD
 WHERE PAY_TP = :tp
   AND PAY_DT >= :from_dt AND PAY_DT < :to_dt
 ORDER BY PAY_DT DESC;

실행계획 학습용 예시 ​

xplan
[Q1 · CARD · 최근 1개월]
---------------------------------------------------------------------------------------------------------------
| Id  | Operation                 | Name  | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Used-Mem |
---------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT          |       |      1 |        |      1 |00:00:00.01 |     896 |          |
|   1 |  SORT AGGREGATE           |       |      1 |      1 |      1 |00:00:00.01 |     896 |          |
|*  2 |   FILTER                  |       |      1 |        |   2871 |00:00:00.01 |     896 |          |
|*  3 |    TABLE ACCESS FULL      | T_ORD |      1 |   2917 |   2871 |00:00:00.01 |     896 |          |
---------------------------------------------------------------------------------------------------------------

[Q2 · 1쪽]
---------------------------------------------------------------------------------------------------------------
| Id  | Operation                 | Name  | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Used-Mem |
---------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT          |       |      1 |        |     20 |00:00:00.02 |     896 |          |
|*  1 |  VIEW                     |       |      1 |     20 |     20 |00:00:00.02 |     896 |          |
|*  2 |   COUNT STOPKEY           |       |      1 |        |     20 |00:00:00.02 |     896 |          |
|   3 |    VIEW                   |       |      1 |   2917 |     20 |00:00:00.02 |     896 |          |
|*  4 |     SORT ORDER BY STOPKEY |       |      1 |   2917 |     20 |00:00:00.02 |     896 | 2048 (0) |
|*  5 |      FILTER               |       |      1 |        |   2871 |00:00:00.01 |     896 |          |
|*  6 |       TABLE ACCESS FULL   | T_ORD |      1 |   2917 |   2871 |00:00:00.01 |     896 |          |
---------------------------------------------------------------------------------------------------------------

[Q3 · 엑셀]
---------------------------------------------------------------------------------------------------------------
| Id  | Operation                 | Name  | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Used-Mem |
---------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT          |       |      1 |        |   2871 |00:00:00.03 |     896 |          |
|   1 |  SORT ORDER BY            |       |      1 |   2917 |   2871 |00:00:00.03 |     896 |  267K (0)|
|*  2 |   FILTER                  |       |      1 |        |   2871 |00:00:00.01 |     896 |          |
|*  3 |    TABLE ACCESS FULL      | T_ORD |      1 |   2917 |   2871 |00:00:00.01 |     896 |          |
---------------------------------------------------------------------------------------------------------------

Predicate Information (세 SQL 공통)
---------------------------------------------------
   FILTER            - filter(:FROM_DT<:TO_DT)
   TABLE ACCESS FULL - filter(("PAY_TP"=:TP AND "PAY_DT">=:FROM_DT AND "PAY_DT"<:TO_DT))
   (Q2) SORT ORDER BY STOPKEY - filter(ROWNUM<=:PAGE*20)

문제 ​

  1. 검색 한 번과 엑셀 받기 한 번이 각각 몇 블록을 읽는지 쓰고, 세 SQL이 모두 테이블 전체를 읽는 까닭을 설명하시오.
  2. 세 SQL을 함께 처리할 인덱스를 설계하고 컬럼 순서의 근거를 쓰시오. 그 인덱스를 제대로 쓰도록 SQL에서 고칠 곳이 있으면 고치시오.
  3. 고친 뒤 세 SQL의 예상 일량을 쓰고, 그 인덱스의 대가와 총건수와 합계를 상한이나 근사값으로 바꾸지 않은 까닭을 서술하시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자