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

NOT IN을 NOT EXISTS로 바꾼 다음 날부터 관심 상품 푸시 대상 적재가 ORA-01400으로 멈춘다

중급INSERT이커머스272 / 700

이 문제는 교재 18화 졸업 과제와 같은 이커머스 데이터베이스를 씁니다. 『튜너의 1년』을 순서대로 읽고 있다면 졸업 과제를 먼저 풀고 오세요. 아래 「테이블과 인덱스」 칸에 과제의 답 일부(제약 조건, 인덱스, 통계의 실제 상태)가 나옵니다.

상황 ​

마케팅팀의 「관심 상품 푸시」 배치는 매일 05시에 지난 7일 동안 상품을 보고도 주문하지 않은 고객을 PUSH_TARGET에 넣는다. 원래 customer_id NOT IN (SELECT … FROM orders …)으로 짜여 있었는데, 지난주 코드 리뷰에서 "NOT IN은 NULL 때문에 위험하니 NOT EXISTS로 바꾸라"라는 지적을 받고 바꿨다. 바꾼 다음 날부터 배치가 ORA-01400: cannot insert NULL into ("ECOMMERCE"."PUSH_TARGET"."CUSTOMER_ID")로 멈춘다. 그 전 3주 동안은 날마다 200명 안팎이 들어갔다.

테이블과 인덱스 ​

  • product_views 상품 조회 로그: 100,000행 · 367블록 · PK만 있음 · customer_id는 NULL 허용(비로그인 조회는 NULL)
  • 지난 7일 조회 384건 = 로그인 조회 313건(고객 268명) + 비로그인 조회 71건
  • orders: 34,908행 · 193블록 · PK만 있음 · customer_id는 NOT NULL · 같은 7일 주문 134건 · 조회한 고객 268명 가운데 67명이 그 기간에 주문했다(그 고객들의 조회는 87건)
  • PUSH_TARGET 푸시 대상(마케팅팀 표): PUSH_DT DATE · CUSTOMER_ID NUMBER NOT NULL · PK (PUSH_DT, CUSTOMER_ID)
  • 통계 최신 · 아래 계획은 INSERT 안의 SELECT만 따로 돌린 것이다

원본 SQL ​

sql
INSERT INTO PUSH_TARGET (PUSH_DT, CUSTOMER_ID)
SELECT DISTINCT TRUNC(SYSDATE), v.customer_id
  FROM product_views v
 WHERE v.view_date >= TRUNC(SYSDATE) - 7
   AND v.view_date <  TRUNC(SYSDATE)
   AND NOT EXISTS (SELECT 1
                     FROM orders o
                    WHERE o.customer_id = v.customer_id
                      AND o.order_date >= TRUNC(SYSDATE) - 7
                      AND o.order_date <  TRUNC(SYSDATE))

실행계획 학습용 예시 ​

xplan
------------------------------------------------------------------------------------------------------------
| Id  | Operation             | Name          | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Used-Mem |
------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT      |               |      1 |        |    202 |00:00:00.01 |     560 |          |
|   1 |  HASH UNIQUE          |               |      1 |    375 |    202 |00:00:00.01 |     560 | 1287K (0)|
|*  2 |   HASH JOIN RIGHT ANTI|               |      1 |    375 |    297 |00:00:00.01 |     560 | 1453K (0)|
|*  3 |    TABLE ACCESS FULL  | ORDERS        |      1 |    134 |    134 |00:00:00.01 |     193 |          |
|*  4 |    TABLE ACCESS FULL  | PRODUCT_VIEWS |      1 |    384 |    384 |00:00:00.01 |     367 |          |
------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   2 - access("O"."CUSTOMER_ID"="V"."CUSTOMER_ID")
   3 - filter(("O"."ORDER_DATE">=TRUNC(SYSDATE@!)-7 AND "O"."ORDER_DATE"<TRUNC(SYSDATE@!)))
   4 - filter(("V"."VIEW_DATE">=TRUNC(SYSDATE@!)-7 AND "V"."VIEW_DATE"<TRUNC(SYSDATE@!)))

[결과]  202행 · 그 가운데 CUSTOMER_ID 가 NULL 인 행 1개
[바꾸기 전 NOT IN]  2번 줄이 HASH JOIN RIGHT ANTI SNA · A-Rows 226 · 결과 201행 · Buffers 560

문제 ​

  1. 같은 뜻으로 바꿨다는 NOT EXISTS가 NULL 고객 한 줄을 만든 까닭과, 예전 NOT IN은 그 줄을 내지 않은 까닭을 실행계획의 A-Rows와 함께 설명하시오.
  2. 배치가 다시 돌도록 SQL을 고치시오. 같은 일을 MINUS로 쓰면 어떻게 되는지도 쓰시오.
  3. NOT IN과 NOT EXISTS가 같은 답을 내는 조건을 바깥 쪽 NULL과 서브쿼리 쪽 NULL로 나눠 정리하시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자