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

「2년 연속 구매 고객」 명단이 쿼리를 손본 다음 날부터 2,000명에서 1,937명으로 줄었다

중급SELECT한빛유통299 / 700

상황 ​

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 ​

sql
-- 원래 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);

실행계획 학습용 예시 ​

xplan
[원래 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번 줄도 같은 조인 조건과 연도 조건이다)

문제 ​

  1. 조민우의 SQL에서 63명이 빠진 이유와, 처음 고친 SQL이 32만 줄을 낸 이유를 INTERSECT와 EXISTS가 행을 같다고 보는 기준의 차이로 설명하시오.
  2. 명단에 맞는 뜻이 어느 쪽인지 정하고, 정렬 없이 그 뜻대로 나오도록 SQL을 고치시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자