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

월초 정산 검수 대상 조회가 결제 완료 주문 3,800건을 보는 데 9초가 걸린다

실전SELECT한빛유통303 / 700

상황 ​

정산팀은 매달 초 지난달 결제 완료 주문 가운데 검수할 주문을 뽑는다. 검수 대상은 (가) 단가 50만 원 이상 상품이 들어 있는 주문이거나 (나) 정산 금액이 결제 금액과 다르게 적재된 주문이다. 조회는 9초가 걸리고, 정산팀은 화면이 멈춘 줄 알고 몇 번씩 다시 누른다. 조민우는 OR를 UNION ALL로 나눠 보겠다며 고친 SQL을 시험해 봤는데, 결과가 302건이 아니라 305건이 나왔다. 박팀장은 고친 SQL과 함께, 인덱스로 해결하자는 DBA 의견도 비교해 달라고 했다.

테이블과 인덱스 ​

  • T_ORD 주문: 100,000행 · 896블록 · PK_T_ORD (ORD_SEQ) · 2024년 7월 결제 완료 주문 3,812건
  • T_ORD_DET 주문상세: 200,000행 · 896블록 · PK_T_ORD_DET (ORD_SEQ, ORD_DET_NO) · 주문당 1 ~ 3건 · 단가 50만 원 이상인 줄 9,960개
  • T_ORD_STLM 정산: 100,000행 · 640블록 · 인덱스 없음 · 정산 배치가 밤마다 T_ORD에서 적재한다 · STLM_AMT NOT NULL
  • 7월 결과: (가)에 해당하는 주문 274건 · (나)에 해당하는 주문 31건 · 둘 다 해당하는 주문 3건 · 통계 최신

원본 SQL ​

sql
SELECT o.ORD_SEQ, o.CUS_ID, o.PAY_DT, o.PAY_AMT
  FROM T_ORD o
 WHERE o.PAY_DT >= :d1 AND o.PAY_DT < :d2
   AND o.ORD_ST = 'COMPLETE'
   AND (EXISTS (SELECT 1
                  FROM T_ORD_DET d
                 WHERE d.ORD_SEQ = o.ORD_SEQ
                   AND d.UNT_PRC >= :v_prc)            -- (가) 고가 상품
        OR EXISTS (SELECT 1
                     FROM T_ORD_STLM s
                    WHERE s.ORD_SEQ  = o.ORD_SEQ
                      AND s.STLM_AMT <> o.PAY_AMT))    -- (나) 정산 금액 불일치

실행계획 학습용 예시 ​

xplan
------------------------------------------------------------------------------------------------------------------
| Id  | Operation                              | Name         | Starts | E-Rows | A-Rows |      A-Time | Buffers |
------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                       |              |      1 |        |    302 | 00:00:09.12 |   2276K |
|*  1 |  FILTER                                |              |      1 |        |    302 | 00:00:09.12 |   2276K |
|*  2 |   TABLE ACCESS FULL                    | T_ORD        |      1 |   3790 |   3812 | 00:00:00.01 |     896 |
|*  3 |   TABLE ACCESS BY INDEX ROWID BATCHED  | T_ORD_DET    |   3812 |      1 |    274 | 00:00:00.03 |   11436 |
|*  4 |    INDEX RANGE SCAN                    | PK_T_ORD_DET |   3812 |      2 |   7240 | 00:00:00.01 |    7624 |
|*  5 |   TABLE ACCESS FULL                    | T_ORD_STLM   |   3538 |      1 |     31 | 00:00:09.05 |   2264K |
------------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   1 - filter(( EXISTS (SELECT 0 FROM "T_ORD_DET" "D" WHERE "D"."ORD_SEQ"=:B1 AND "D"."UNT_PRC">=:V_PRC) OR
               EXISTS (SELECT 0 FROM "T_ORD_STLM" "S" WHERE "S"."ORD_SEQ"=:B2 AND "S"."STLM_AMT"<>:B3)))
   2 - filter(("O"."ORD_ST"='COMPLETE' AND "O"."PAY_DT">=:D1 AND "O"."PAY_DT"<:D2))
   3 - filter("D"."UNT_PRC">=:V_PRC)
   4 - access("D"."ORD_SEQ"=:B1)
   5 - filter(("S"."ORD_SEQ"=:B1 AND "S"."STLM_AMT"<>:B2))

문제 ​

  1. 9초가 어디서 나오는지 실행계획의 줄 번호와 Starts, Buffers로 설명하시오.
  2. OR를 UNION ALL로 나눠 결과(302건)가 바뀌지 않게 SQL을 고치시오. 조민우의 SQL이 305건을 낸 이유와, 둘째 갈래에서 첫 갈래를 뺄 때 LNNVL을 쓸 수 없는 이유를 설명하시오.
  3. T_ORD_STLM에 (ORD_SEQ) 인덱스를 만들고 SQL은 그대로 두는 안, 두 서브쿼리를 하나의 조인으로 합치는 안과 비교해 일량과 부작용을 서술하시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자