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

7월 미정산 주문을 개발자 네 명이 뽑았더니 0건, 31건, 58건, 110건이 나왔다

실전SELECT한빛유통277 / 700

상황 ​

재무팀은 매달 초 「월말 정산 대사」로 지난달 결제 완료 주문 가운데 정산 행이 하나도 없는 주문의 건수와 금액을 받는다. 8월 초 7월분을 개발자 네 명이 각자 짠 SQL로 뽑았더니 답이 0건, 31건, 58건, 110건으로 모두 달랐다. 넷 다 0.03초 안팎에 1,536블록을 읽었다. 7월부터 정산 테이블에는 전에 없던 행이 생겼다. 재무팀이 주문 없이 금액만 맞추는 수기 조정 정산을 넣기 시작했고, 금액이 정해지기 전의 정산 보류 행과 부분 취소 재정산(한 주문에 정산 행 두 줄)도 들어온다.

테이블과 인덱스 ​

  • T_ORD: 100,000행 · 896블록 · PK_T_ORD (ORD_SEQ) · 2024년 7월 결제 4,247건 중 COMPLETE 3,836건
  • T_ORD_STLM: 약 10만 행 · 640블록 · 인덱스 없음 · 컬럼 제약 없음(모두 NULL 허용) · 정산 금액 STLM_AMT에는 결제 금액(PAY_AMT)을 넣는다
  • 7월 COMPLETE 3,836건의 정산 상태: 정산 행 한 줄 3,726건 · 보류 행(STLM_AMT NULL) 한 줄 27건 · 재정산 두 줄(금액을 둘로 나눠 적음) 52건 · 정산 행 없음 31건
  • 수기 조정 정산 3행은 ORD_SEQ가 NULL이다 · 통계 최신
  • 재무팀의 정의: 결제 완료 주문 가운데 그 주문의 정산 행이 하나도 없는 주문(보류와 재정산은 정산 행이 있는 것으로 본다)

원본 SQL ​

sql
-- (A) 개발자 1 : NOT IN
SELECT COUNT(*), SUM(o.PAY_AMT)
  FROM T_ORD o
 WHERE o.PAY_DT >= DATE '2024-07-01' AND o.PAY_DT < DATE '2024-08-01'
   AND o.ORD_ST = 'COMPLETE'
   AND o.ORD_SEQ NOT IN (SELECT s.ORD_SEQ FROM T_ORD_STLM s);

-- (B) 개발자 2 : NOT EXISTS
SELECT COUNT(*), SUM(o.PAY_AMT)
  FROM T_ORD o
 WHERE o.PAY_DT >= DATE '2024-07-01' AND o.PAY_DT < DATE '2024-08-01'
   AND o.ORD_ST = 'COMPLETE'
   AND NOT EXISTS (SELECT 1 FROM T_ORD_STLM s WHERE s.ORD_SEQ = o.ORD_SEQ);

-- (C) 개발자 3 : 외부 조인 + IS NULL
SELECT COUNT(*), SUM(o.PAY_AMT)
  FROM T_ORD o
  LEFT OUTER JOIN T_ORD_STLM s
    ON s.ORD_SEQ = o.ORD_SEQ
 WHERE o.PAY_DT >= DATE '2024-07-01' AND o.PAY_DT < DATE '2024-08-01'
   AND o.ORD_ST = 'COMPLETE'
   AND s.STLM_AMT IS NULL;

-- (D) 개발자 4 : MINUS (금액까지 맞춰 본다)
SELECT COUNT(*), SUM(amt)
  FROM (SELECT o.ORD_SEQ, o.PAY_AMT AS amt
          FROM T_ORD o
         WHERE o.PAY_DT >= DATE '2024-07-01' AND o.PAY_DT < DATE '2024-08-01'
           AND o.ORD_ST = 'COMPLETE'
        MINUS
        SELECT s.ORD_SEQ, s.STLM_AMT
          FROM T_ORD_STLM s);

실행계획 학습용 예시 · 주문 건수는 적재 데이터 그대로 ​

xplan
[A] NOT IN · 결과 0건
----------------------------------------------------------------------------------------------------------
| Id  | Operation             | Name       | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Used-Mem |
----------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT      |            |      1 |        |      1 |00:00:00.03 |    1536 |          |
|   1 |  SORT AGGREGATE       |            |      1 |      1 |      1 |00:00:00.03 |    1536 |          |
|*  2 |   HASH JOIN ANTI NA   |            |      1 |     38 |      0 |00:00:00.03 |    1536 | 1335K (0)|
|*  3 |    TABLE ACCESS FULL  | T_ORD      |      1 |   3836 |   3836 |00:00:00.01 |     896 |          |
|   4 |    TABLE ACCESS FULL  | T_ORD_STLM |      1 |    100K|    100K|00:00:00.01 |     640 |          |
   2 - access("O"."ORD_SEQ"="S"."ORD_SEQ")

[B] NOT EXISTS · 결과 31건 · 7,904,300원
|   0 | SELECT STATEMENT      |            |      1 |        |      1 |00:00:00.03 |    1536 |          |
|   1 |  SORT AGGREGATE       |            |      1 |      1 |      1 |00:00:00.03 |    1536 |          |
|*  2 |   HASH JOIN ANTI      |            |      1 |     38 |     31 |00:00:00.03 |    1536 | 1335K (0)|
|*  3 |    TABLE ACCESS FULL  | T_ORD      |      1 |   3836 |   3836 |00:00:00.01 |     896 |          |
|   4 |    TABLE ACCESS FULL  | T_ORD_STLM |      1 |    100K|    100K|00:00:00.01 |     640 |          |
   2 - access("S"."ORD_SEQ"="O"."ORD_SEQ")

[C] 외부 조인 + STLM_AMT IS NULL · 결과 58건
|   0 | SELECT STATEMENT      |            |      1 |        |      1 |00:00:00.03 |    1536 |          |
|   1 |  SORT AGGREGATE       |            |      1 |      1 |      1 |00:00:00.03 |    1536 |          |
|*  2 |   FILTER              |            |      1 |        |     58 |00:00:00.03 |    1536 |          |
|*  3 |    HASH JOIN OUTER    |            |      1 |   3836 |   3888 |00:00:00.03 |    1536 | 1335K (0)|
|*  4 |     TABLE ACCESS FULL | T_ORD      |      1 |   3836 |   3836 |00:00:00.01 |     896 |          |
|   5 |     TABLE ACCESS FULL | T_ORD_STLM |      1 |    100K|    100K|00:00:00.01 |     640 |          |
   2 - filter("S"."STLM_AMT" IS NULL)
   3 - access("S"."ORD_SEQ"="O"."ORD_SEQ")

[D] MINUS · 결과 110건
|   0 | SELECT STATEMENT      |            |      1 |        |      1 |00:00:00.05 |    1536 |          |
|   1 |  SORT AGGREGATE       |            |      1 |      1 |      1 |00:00:00.05 |    1536 |          |
|   2 |   VIEW                |            |      1 |   3836 |    110 |00:00:00.05 |    1536 |          |
|   3 |    MINUS              |            |      1 |        |    110 |00:00:00.05 |    1536 |          |
|   4 |     SORT UNIQUE       |            |      1 |   3836 |   3836 |00:00:00.01 |     896 |  178K (0)|
|*  5 |      TABLE ACCESS FULL| T_ORD      |      1 |   3836 |   3836 |00:00:00.01 |     896 |          |
|   6 |     SORT UNIQUE       |            |      1 |    100K|    100K|00:00:00.04 |     640 | 4122K (0)|
|   7 |      TABLE ACCESS FULL| T_ORD_STLM |      1 |    100K|    100K|00:00:00.01 |     640 |          |

(네 계획 모두 T_ORD 쪽 filter 는 같다 : "O"."ORD_ST"='COMPLETE' AND "O"."PAY_DT">=TO_DATE(' 2024-07-01 …') AND "O"."PAY_DT"<TO_DATE(' 2024-08-01 …'))

문제 ​

  1. 네 SQL의 답이 0건, 31건, 58건, 110건으로 갈린 까닭을 각 SQL의 뜻과 정산 테이블의 새 행(수기 조정, 보류, 재정산)으로 설명하시오.
  2. 재무팀의 정의에 맞는 SQL을 고르고, NOT IN을 써야 한다면 어떻게 고칠지 함께 쓰시오. 고른 SQL이 맞다는 것을 무엇과 대조해 보일지도 쓰시오.
  3. 네 SQL의 일량과 부작용(NULL, 중복, 정렬)을 비교하고, 재무팀이 보류와 재정산 건수도 함께 보고 싶어 할 때 어떻게 할지 서술하시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자