테마
재무팀은 매달 초 「월말 정산 대사」로 지난달 결제 완료 주문 가운데 정산 행이 하나도 없는 주문의 건수와 금액을 받는다. 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)을 넣는다STLM_AMT NULL) 한 줄 27건 · 재정산 두 줄(금액을 둘로 나눠 적음) 52건 · 정산 행 없음 31건ORD_SEQ가 NULL이다 · 통계 최신-- (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);
[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 …'))