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

6월 주문 고객과 상품평 고객을 맞대는 표가 두 테이블을 두 번씩 읽는다

중급SELECT한빛유통190 / 700

상황 ​

CRM팀의 「구매 고객과 평가 고객 대조」는 한 달 동안 주문한 고객과 상품평을 남긴 고객을 한 표로 맞대어 '주문만', '평가만', '둘 다'를 나눈다. 기간을 바꿔 가며 하루 300번쯤 돌린다. 개발자는 처음에 두 인라인 뷰의 조인 조건 양쪽에 (+)를 붙였다가 ORA-01468이 나자, 한쪽씩 아우터 조인한 두 SQL을 UNION으로 이었다. 아래는 2024년 6월의 실행계획이다.

테이블과 인덱스 ​

  • T_ORD: 100,000행 · 896블록 · PK_T_ORD (ORD_SEQ)만 있다 · 한 달 주문은 테이블 전체에 흩어져 있다 · 2024년 6월 4,172건(고객 1,752명)
  • T_ITM_EVL 상품평가: 5,000행 · 48블록 · PK_T_ITM_EVL (ITM_ID, EVL_LST_NO) · EVL_DT에는 인덱스가 없다 · 2024년 6월 211건(고객 200명)
  • 6월에 주문과 평가가 모두 있는 고객 180명 · 평가만 있는 고객 20명 · 통계 최신

원본 SQL ​

sql
SELECT o.CUS_ID, o.ord_cnt, o.ord_amt, NVL(e.evl_cnt, 0) AS evl_cnt
  FROM (SELECT CUS_ID, COUNT(*) AS ord_cnt, SUM(ORD_AMT) AS ord_amt
          FROM T_ORD
         WHERE ORD_DT >= :v_from AND ORD_DT < :v_to
         GROUP BY CUS_ID) o,
       (SELECT CUS_ID, COUNT(*) AS evl_cnt
          FROM T_ITM_EVL
         WHERE EVL_DT >= :v_from AND EVL_DT < :v_to
         GROUP BY CUS_ID) e
 WHERE e.CUS_ID(+) = o.CUS_ID
UNION
SELECT e.CUS_ID, NVL(o.ord_cnt, 0), NVL(o.ord_amt, 0), e.evl_cnt
  FROM (SELECT CUS_ID, COUNT(*) AS evl_cnt
          FROM T_ITM_EVL
         WHERE EVL_DT >= :v_from AND EVL_DT < :v_to
         GROUP BY CUS_ID) e,
       (SELECT CUS_ID, COUNT(*) AS ord_cnt, SUM(ORD_AMT) AS ord_amt
          FROM T_ORD
         WHERE ORD_DT >= :v_from AND ORD_DT < :v_to
         GROUP BY CUS_ID) o
 WHERE o.CUS_ID(+) = e.CUS_ID

실행계획 학습용 예시 ​

xplan
---------------------------------------------------------------------------------------------
| Id  | Operation                | Name      | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
---------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT         |           |      1 |        |   1772 |00:00:00.05 |    1888 |
|   1 |  SORT UNIQUE             |           |      1 |   1952 |   1772 |00:00:00.05 |    1888 |
|   2 |   UNION-ALL              |           |      1 |        |   1952 |00:00:00.04 |    1888 |
|*  3 |    HASH JOIN OUTER       |           |      1 |   1752 |   1752 |00:00:00.02 |     944 |
|   4 |     VIEW                 |           |      1 |   1752 |   1752 |00:00:00.02 |     896 |
|   5 |      HASH GROUP BY       |           |      1 |   1752 |   1752 |00:00:00.02 |     896 |
|*  6 |       TABLE ACCESS FULL  | T_ORD     |      1 |   4170 |   4172 |00:00:00.01 |     896 |
|   7 |     VIEW                 |           |      1 |    200 |    200 |00:00:00.01 |      48 |
|   8 |      HASH GROUP BY       |           |      1 |    200 |    200 |00:00:00.01 |      48 |
|*  9 |       TABLE ACCESS FULL  | T_ITM_EVL |      1 |    208 |    211 |00:00:00.01 |      48 |
|* 10 |    HASH JOIN OUTER       |           |      1 |    200 |    200 |00:00:00.02 |     944 |
|  11 |     VIEW                 |           |      1 |    200 |    200 |00:00:00.01 |      48 |
|  12 |      HASH GROUP BY       |           |      1 |    200 |    200 |00:00:00.01 |      48 |
|* 13 |       TABLE ACCESS FULL  | T_ITM_EVL |      1 |    208 |    211 |00:00:00.01 |      48 |
|  14 |     VIEW                 |           |      1 |   1752 |   1752 |00:00:00.02 |     896 |
|  15 |      HASH GROUP BY       |           |      1 |   1752 |   1752 |00:00:00.02 |     896 |
|* 16 |       TABLE ACCESS FULL  | T_ORD     |      1 |   4170 |   4172 |00:00:00.01 |     896 |
---------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   3 - access("E"."CUS_ID"(+)="O"."CUS_ID")
   6 - filter(("ORD_DT">=:V_FROM AND "ORD_DT"<:V_TO))
   9 - filter(("EVL_DT">=:V_FROM AND "EVL_DT"<:V_TO))
  10 - access("O"."CUS_ID"(+)="E"."CUS_ID")
  13 - filter(("EVL_DT">=:V_FROM AND "EVL_DT"<:V_TO))
  16 - filter(("ORD_DT">=:V_FROM AND "ORD_DT"<:V_TO))

문제 ​

  1. 이 SQL이 두 테이블을 몇 번씩 읽는지 계획의 줄 번호로 짚고, 1번 줄 SORT UNIQUE가 무엇을 하는지 설명하시오.
  2. 두 테이블을 한 번씩만 읽도록 SQL을 고치시오. 결과의 고객 번호 칸이 비지 않게 하는 방법도 쓰시오.
  3. 개발자가 처음 만난 ORA-01468이 왜 났는지 설명하시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자