Skip to content
튜닝 연습조인 튜닝
JOIN-034

선물하기를 연 뒤로 CRM의 「고객별 월 주문」 추출이 0.03초에서 18초가 됐다

기초SELECT한빛유통184 / 700

상황 ​

CRM팀은 매달 1일 고객 2,000명 모두에게 지난달 주문 건수와 금액을 붙인 파일을 받는다. 주문이 없는 고객도 0건으로 나와야 해서 아우터 조인으로 썼고, 지금까지는 0.03초 만에 끝났다. 지난달 「선물하기」를 열면서 선물로 받은 주문도 받은 고객의 실적으로 세기로 했다. 개발자는 ON 절의 조인 조건에 OR o.RCV_CUS_ID = c.CUS_ID를 더했고, 현업은 건수와 합계를 검수해 맞다고 했다. 그 뒤로 추출이 18초 걸린다. DB는 19c이고 옵티마이저 설정은 기본값이다. 아래는 2024년 6월로 돌린 실행계획이다.

테이블과 인덱스 ​

  • M_CUS 고객: 2,000행 · 30블록 · PK_M_CUS (CUS_ID)
  • T_ORD 주문: 100,000행 · 896블록 · PK_T_ORD (ORD_SEQ) 말고는 인덱스가 없다 · 통계 최신
  • 이 문제의 T_ORD에는 RCV_CUS_ID(선물 받은 회원) 칸이 있다. 회원이 받는 주문에만 값이 있고, 본인이 받으면 CUS_ID와 같은 번호가 들어간다. 비회원이 주문하면 CUS_ID가 NULL이다
  • 2024년 6월 주문 4,170건: 회원 주문 4,120건(본인 수령 3,700건 · 다른 회원에게 선물 300건 · 비회원에게 선물 120건) · 비회원이 회원에게 보낸 선물 50건
  • 그달 주문했거나 선물을 받은 고객 1,752명 · 원본 결과의 건수 합계 4,470건

원본 SQL ​

sql
SELECT c.CUS_ID, c.CUS_NM,
       COUNT(o.ORD_SEQ)       AS ord_cnt,
       NVL(SUM(o.ORD_AMT), 0) AS ord_amt
  FROM M_CUS c
  LEFT JOIN T_ORD o
    ON (o.CUS_ID = c.CUS_ID OR o.RCV_CUS_ID = c.CUS_ID)
   AND o.ORD_DT >= TO_DATE(:v_ym, 'YYYYMM')
   AND o.ORD_DT <  ADD_MONTHS(TO_DATE(:v_ym, 'YYYYMM'), 1)
 GROUP BY c.CUS_ID, c.CUS_NM
 ORDER BY c.CUS_ID

실행계획 학습용 예시 ​

xplan
------------------------------------------------------------------------------------------------------
| Id  | Operation              | Name            | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT       |                 |      1 |        |   2000 |00:00:18.42 |   1792K |
|   1 |  SORT GROUP BY         |                 |      1 |   2000 |   2000 |00:00:18.42 |   1792K |
|   2 |   NESTED LOOPS OUTER   |                 |      1 |   8340 |   4718 |00:00:18.40 |   1792K |
|   3 |    TABLE ACCESS FULL   | M_CUS           |      1 |   2000 |   2000 |00:00:00.01 |      30 |
|   4 |    VIEW                | VW_LAT_5E1D7B42 |   2000 |      4 |   4470 |00:00:18.37 |   1792K |
|*  5 |     TABLE ACCESS FULL  | T_ORD           |   2000 |      4 |   4470 |00:00:18.35 |   1792K |
------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   5 - filter(("O"."ORD_DT">=TO_DATE(:V_YM,'YYYYMM') AND "O"."ORD_DT"<ADD_MONTHS(TO_DATE(:V_YM,'YYYYMM'),1)
              AND ("O"."CUS_ID"="C"."CUS_ID" OR "O"."RCV_CUS_ID"="C"."CUS_ID")))

문제 ​

  1. 1,792K 블록이 어디서 나오는지 실행계획 4번과 5번 줄의 Starts로 설명하시오. OR를 더하기 전처럼 해시 조인을 쓰지 못한 이유를 조인 조건으로 설명하고, 19c 옵티마이저가 이 OR를 스스로 두 갈래로 나누지 않았다는 것을 계획의 어디에서 알 수 있는지도 쓰시오.
  2. 결과가 바뀌지 않게, 해시 조인을 쓸 수 있는 등치 조인 조건 하나로 SQL을 고치시오. 같은 주문이 한 고객에게 두 번 세어지거나 빠지지 않게 하시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자