테마
CRM팀은 해마다 1월에 「2년 연속 구매 고객」 명단(고객번호, 이름, 휴대폰, 지역)을 뽑아 감사 문자와 우편을 보낸다. 마케팅팀이 이번 달부터 같은 명단을 매일 아침 받겠다고 하자, 조민우는 "집합 연산자는 정렬을 두 번 한다"는 사내 점검 결과를 보고 INTERSECT를 EXISTS로 바꿨다. 처음 고친 SQL은 명단이 32만 줄로 나와 DISTINCT를 붙였고, 그 뒤로는 1,937명이 나온다. 작년 명단은 2,000명이었다.
오과장은 "개인정보 파기 요청으로 휴대폰 번호를 지운 고객이 몇십 명 있다"라고 했다.
T_ORD_BIG: 1,000,000행 · 5,826블록 · 인덱스 없음 · 2023년 333,245건 · 2024년 333,245건 · 고객 2,000명 모두 두 해에 주문이 있다(고객마다 해마다 164 ~ 168건)M_CUS: 2,000행 · 30블록 · PK_M_CUS (CUS_ID) · MBL_NO는 파기 요청을 받은 63명이 NULL이다(NOT NULL 제약 없음) · CUS_NM, RGN_ID는 모두 값이 있다-- 원래 SQL (작년까지 · 2,000명)
SELECT c.CUS_ID, c.CUS_NM, c.MBL_NO, c.RGN_ID
FROM T_ORD_BIG o, M_CUS c
WHERE o.ORD_DT >= DATE '2023-01-01' AND o.ORD_DT < DATE '2024-01-01'
AND c.CUS_ID = o.CUS_ID
INTERSECT
SELECT c.CUS_ID, c.CUS_NM, c.MBL_NO, c.RGN_ID
FROM T_ORD_BIG o, M_CUS c
WHERE o.ORD_DT >= DATE '2024-01-01' AND o.ORD_DT < DATE '2025-01-01'
AND c.CUS_ID = o.CUS_ID;
-- 조민우가 고친 SQL (1,937명)
SELECT DISTINCT c.CUS_ID, c.CUS_NM, c.MBL_NO, c.RGN_ID
FROM T_ORD_BIG o, M_CUS c
WHERE o.ORD_DT >= DATE '2023-01-01' AND o.ORD_DT < DATE '2024-01-01'
AND c.CUS_ID = o.CUS_ID
AND EXISTS (SELECT 1
FROM T_ORD_BIG x, M_CUS d
WHERE x.ORD_DT >= DATE '2024-01-01' AND x.ORD_DT < DATE '2025-01-01'
AND d.CUS_ID = x.CUS_ID
AND d.CUS_ID = c.CUS_ID
AND d.CUS_NM = c.CUS_NM
AND d.MBL_NO = c.MBL_NO
AND d.RGN_ID = c.RGN_ID);
[원래 SQL · INTERSECT]
---------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Used-Mem |
---------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 2000 |00:00:00.52 | 11712 | |
| 1 | INTERSECTION | | 1 | | 2000 |00:00:00.52 | 11712 | |
| 2 | SORT UNIQUE | | 1 | 2000 | 2000 |00:00:00.26 | 5856 | 178K (0)|
|* 3 | HASH JOIN | | 1 | 333K| 333K|00:00:00.13 | 5856 | 1398K (0)|
| 4 | TABLE ACCESS FULL | M_CUS | 1 | 2000 | 2000 |00:00:00.01 | 30 | |
|* 5 | TABLE ACCESS FULL | T_ORD_BIG | 1 | 333K| 333K|00:00:00.05 | 5826 | |
| 6 | SORT UNIQUE | | 1 | 2000 | 2000 |00:00:00.26 | 5856 | 178K (0)|
|* 7 | HASH JOIN | | 1 | 333K| 333K|00:00:00.13 | 5856 | 1398K (0)|
| 8 | TABLE ACCESS FULL | M_CUS | 1 | 2000 | 2000 |00:00:00.01 | 30 | |
|* 9 | TABLE ACCESS FULL | T_ORD_BIG | 1 | 333K| 333K|00:00:00.05 | 5826 | |
---------------------------------------------------------------------------------------------------------------
[조민우의 SQL · EXISTS + DISTINCT]
---------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Used-Mem |
---------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1937 |00:00:00.31 | 11712 | |
| 1 | HASH UNIQUE | | 1 | 2000 | 1937 |00:00:00.31 | 11712 | 1254K (0)|
|* 2 | HASH JOIN RIGHT SEMI | | 1 | 333K| 323K|00:00:00.24 | 11712 | 1543K (0)|
| 3 | VIEW | VW_SQ_1 | 1 | 333K| 333K|00:00:00.11 | 5856 | |
|* 4 | HASH JOIN | | 1 | 333K| 333K|00:00:00.10 | 5856 | 1398K (0)|
| 5 | TABLE ACCESS FULL | M_CUS | 1 | 2000 | 2000 |00:00:00.01 | 30 | |
|* 6 | TABLE ACCESS FULL | T_ORD_BIG | 1 | 333K| 333K|00:00:00.05 | 5826 | |
|* 7 | HASH JOIN | | 1 | 333K| 333K|00:00:00.10 | 5856 | 1398K (0)|
| 8 | TABLE ACCESS FULL | M_CUS | 1 | 2000 | 2000 |00:00:00.01 | 30 | |
|* 9 | TABLE ACCESS FULL | T_ORD_BIG | 1 | 333K| 333K|00:00:00.05 | 5826 | |
---------------------------------------------------------------------------------------------------------------
Predicate Information (조민우의 SQL):
---------------------------------------------------
2 - access("ITEM_1"="C"."CUS_ID" AND "ITEM_2"="C"."CUS_NM" AND "ITEM_3"="C"."MBL_NO" AND "ITEM_4"="C"."RGN_ID")
4 - access("D"."CUS_ID"="X"."CUS_ID")
6 - filter(("X"."ORD_DT">=TO_DATE(' 2024-01-01 00:00:00', 'syyyy-mm-dd hh24:mi:ss') AND
"X"."ORD_DT"<TO_DATE(' 2025-01-01 00:00:00', 'syyyy-mm-dd hh24:mi:ss')))
7 - access("C"."CUS_ID"="O"."CUS_ID")
9 - filter(("O"."ORD_DT">=TO_DATE(' 2023-01-01 00:00:00', 'syyyy-mm-dd hh24:mi:ss') AND
"O"."ORD_DT"<TO_DATE(' 2024-01-01 00:00:00', 'syyyy-mm-dd hh24:mi:ss')))
(원래 SQL의 3번 · 5번, 7번 · 9번 줄도 같은 조인 조건과 연도 조건이다)
INTERSECT와 EXISTS가 행을 같다고 보는 기준의 차이로 설명하시오.