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

「VIP 주문만」을 켜면 목록이 느려지고, 빠르게 고쳤다는 SQL은 빈 쪽을 보여 준다

실전SELECT한빛유통330 / 700

상황 ​

정산팀의 「주문 관리」는 전체 주문을 최신순(ORD_DT DESC, ORD_SEQ DESC)으로 한 쪽에 20건씩 보여 준다. 「VIP(A 등급) 주문만」을 켜면 1쪽이 0.2초, 10쪽이 2초쯤 걸린다. 정산팀은 쪽 번호를 그대로 쓰고 싶어 한다. 조민우가 "고객 조인을 쪽을 자른 다음으로 옮겼다"며 SQL 2를 올렸는데, 빨라진 대신 1쪽에 주문이 한두 건만 나오거나 아예 비었다. 오과장은 두 SQL을 비교하고 제대로 된 안을 내라고 했다.

테이블과 인덱스 ​

  • T_ORD_BIG: 1,000,000행 · 5,826블록 · 고객당 약 500건 · 같은 날짜의 주문도, 한 고객의 주문도 테이블 곳곳에 흩어져 있다.
  • M_CUS: 2,000행 · 30블록 · PK_M_CUS (CUS_ID) · CUS_GD A 1%(20명) · B 9% · C 90%. A 등급 고객의 주문은 약 10,000건(1%)이다.
  • 인덱스: IX_ORD_BIG_DT_SEQ (ORD_DT, ORD_SEQ)(교재 09화) · IX_ORD_BIG_CUS_DT (CUS_ID, ORD_DT)(CS 화면에서 고객 한 명의 기간 조회에 쓴다).
  • FK T_ORD_BIG.CUS_ID → M_CUS가 켜져 있다. 통계는 최신이고 CUS_GD에 히스토그램은 없다.

원본 SQL ​

sql
-- SQL 1 : 지금 운영 중 (VIP 켜짐)
SELECT *
  FROM (SELECT ROWNUM rn, a.*
          FROM (SELECT o.ORD_SEQ, o.ORD_DT, o.CUS_ID, c.CUS_NM, o.ORD_AMT
                  FROM T_ORD_BIG o, M_CUS c
                 WHERE c.CUS_ID = o.CUS_ID
                   AND c.CUS_GD = 'A'
                 ORDER BY o.ORD_DT DESC, o.ORD_SEQ DESC) a
         WHERE ROWNUM <= :page * 20)
 WHERE rn > (:page - 1) * 20;

-- SQL 2 : 조민우가 올린 안
SELECT x.ORD_SEQ, x.ORD_DT, x.CUS_ID, c.CUS_NM, x.ORD_AMT
  FROM (SELECT *
          FROM (SELECT ROWNUM rn, a.*
                  FROM (SELECT ORD_SEQ, ORD_DT, CUS_ID, ORD_AMT
                          FROM T_ORD_BIG
                         ORDER BY ORD_DT DESC, ORD_SEQ DESC) a
                 WHERE ROWNUM <= :page * 20)
         WHERE rn > (:page - 1) * 20) x,
       M_CUS c
 WHERE c.CUS_ID = x.CUS_ID
   AND c.CUS_GD = 'A'
 ORDER BY x.rn;

실행계획 학습용 예시 ​

xplan
[SQL 1 · 1쪽]   (10쪽은 같은 모양 · 6번 줄 A-Rows 20,130 · Buffers 약 40,400 · 2.05초)
-----------------------------------------------------------------------------------------------------------------
| Id  | Operation                        | Name              | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
-----------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                 |                   |      1 |        |     20 |00:00:00.21 |    4049 |
|*  1 |  VIEW                            |                   |      1 |     20 |     20 |00:00:00.21 |    4049 |
|*  2 |   COUNT STOPKEY                  |                   |      1 |        |     20 |00:00:00.21 |    4049 |
|   3 |    VIEW                          |                   |      1 |  333K  |     20 |00:00:00.21 |    4049 |
|   4 |     NESTED LOOPS                 |                   |      1 |  333K  |     20 |00:00:00.21 |    4049 |
|   5 |      NESTED LOOPS                |                   |      1 |   1000K|   2013 |00:00:00.19 |    2049 |
|   6 |       TABLE ACCESS BY INDEX ROWID| T_ORD_BIG         |      1 |   1000K|   2013 |00:00:00.18 |    2020 |
|   7 |        INDEX FULL SCAN DESCENDING| IX_ORD_BIG_DT_SEQ |      1 |   1000K|   2013 |00:00:00.01 |       9 |
|*  8 |       INDEX UNIQUE SCAN          | PK_M_CUS          |   2013 |      1 |   2013 |00:00:00.01 |      29 |
|*  9 |      TABLE ACCESS BY INDEX ROWID | M_CUS             |   2013 |      1 |     20 |00:00:00.02 |    2000 |
-----------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   1 - filter("RN">(:PAGE-1)*20)
   2 - filter(ROWNUM<=:PAGE*20)
   8 - access("C"."CUS_ID"="O"."CUS_ID")
   9 - filter("C"."CUS_GD"='A')

[SQL 2 · 1쪽]
-------------------------------------------------------------------------------------------------------------------
| Id  | Operation                          | Name              | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
-------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                   |                   |      1 |        |      0 |00:00:00.01 |      45 |
|   1 |  SORT ORDER BY                     |                   |      1 |      7 |      0 |00:00:00.01 |      45 |
|   2 |   NESTED LOOPS                     |                   |      1 |      7 |      0 |00:00:00.01 |      45 |
|   3 |    NESTED LOOPS                    |                   |      1 |     20 |     20 |00:00:00.01 |      25 |
|*  4 |     VIEW                           |                   |      1 |     20 |     20 |00:00:00.01 |      23 |
|*  5 |      COUNT STOPKEY                 |                   |      1 |        |     20 |00:00:00.01 |      23 |
|   6 |       VIEW                         |                   |      1 |   1000K|     20 |00:00:00.01 |      23 |
|   7 |        TABLE ACCESS BY INDEX ROWID | T_ORD_BIG         |      1 |   1000K|     20 |00:00:00.01 |      23 |
|   8 |         INDEX FULL SCAN DESCENDING | IX_ORD_BIG_DT_SEQ |      1 |     20 |     20 |00:00:00.01 |       3 |
|*  9 |     INDEX UNIQUE SCAN              | PK_M_CUS          |     20 |      1 |     20 |00:00:00.01 |       2 |
|* 10 |    TABLE ACCESS BY INDEX ROWID     | M_CUS             |     20 |      1 |      0 |00:00:00.01 |      20 |
-------------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   4 - filter("RN">(:PAGE-1)*20)
   5 - filter(ROWNUM<=:PAGE*20)
   9 - access("C"."CUS_ID"="X"."CUS_ID")
  10 - filter("C"."CUS_GD"='A')

문제 ​

  1. SQL 2가 빈 쪽이나 한두 건짜리 쪽을 보여 주는 까닭을 설명하고, 조인 상대에 조건이 있을 때 "페이징 후 조인"을 쓸 수 없는 이유를 쓰시오.
  2. SQL 1의 1쪽 Buffers 4,049가 어디서 나오는지 쓰고, 쪽이 뒤로 갈수록, 그리고 등급 조건이 B나 C일 때 일량이 어떻게 달라지는지 설명하시오.
  3. 쪽 번호를 유지하면서 A 등급 화면이 몇 쪽째든 비슷한 일량이 되도록 SQL과 인덱스를 고치고, 그 안의 대가와 그 안이 맞지 않는 경우를 쓰시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자