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

(+)로 쓴 POS 리포트를 ANSI 조인으로 옮겼더니 세 개 중 두 개의 숫자가 달라졌다

실전SELECT버거195 / 700

상황 ​

본사가 리포트 도구를 바꾸면서 2012년에 만든 POS 리포트 SQL을 ANSI 조인으로 옮기고 있다. 시범으로 개발자 세 명이 하나씩 손으로 옮겼고, 옮기기 전과 후의 결과를 맞대 보니 숫자가 다른 리포트가 나왔다. 오과장은 "어느 것이 왜 틀렸는지 찾고, 앞으로 수백 개를 옮길 때 쓸 규칙으로 정리해 달라. 일량이 바뀐 것도 있는지 봐 달라"라고 했다. 아래 SQL은 원래 SQL과 옮긴 SQL이고, 실행계획은 2013-06-14로 돌린 것이다.

테이블과 인덱스 ​

  • SHOP 200행 · 5블록 · PK / ITEM 92행 · 5블록 · PK · SALE_YN 'Y' 78개 · 'N' 14개(판매 중지)
  • ORD 1,000,000행 · 22,422블록 · ORD_X01 (ORD_DT, ORD_HMS) · 2013-06-14 주문 1,368건 · 그날 주문이 없는 매장 3곳
  • ORD_ITEM 2,472,717행 · 55,265블록 · ORD_ITEM_X01 (ORD_DT, ORD_HMS) · 2013-06-14 주문상품 3,381줄(하루치는 인덱스 리프 약 12블록, 테이블 약 80블록)
  • 그날 팔린 상품 69개 · 판매 중인데 그날 안 팔린 상품 9개 · 통계 최신

원본 SQL ​

sql
-- [R1] 매장별 오늘 주문 수 (주문 없는 매장은 0)
--   원래 SQL
SELECT s.SHOP_NO, s.SHOP_NM, COUNT(o.ORD_NO) AS ord_cnt
  FROM SHOP s, ORD o
 WHERE o.SHOP_NO(+) = s.SHOP_NO
   AND o.ORD_DT(+)  = :v_dt
 GROUP BY s.SHOP_NO, s.SHOP_NM;
--   옮긴 SQL
SELECT s.SHOP_NO, s.SHOP_NM, COUNT(o.ORD_NO) AS ord_cnt
  FROM SHOP s
  LEFT JOIN ORD o ON o.SHOP_NO = s.SHOP_NO
 WHERE o.ORD_DT = :v_dt
 GROUP BY s.SHOP_NO, s.SHOP_NM;

-- [R2] 판매 중인 상품별 오늘 판매 수량 (안 팔린 상품은 0)
--   원래 SQL
SELECT t.ITEM_ID, t.ITEM_NM, NVL(SUM(i.ORD_ITEM_QTY), 0) AS qty
  FROM ITEM t, ORD_ITEM i
 WHERE i.ITEM_ID(+) = t.ITEM_ID
   AND i.ORD_DT(+)  = :v_dt
   AND t.SALE_YN    = 'Y'
 GROUP BY t.ITEM_ID, t.ITEM_NM;
--   옮긴 SQL
SELECT t.ITEM_ID, t.ITEM_NM, NVL(SUM(i.ORD_ITEM_QTY), 0) AS qty
  FROM ITEM t
  LEFT JOIN ORD_ITEM i ON i.ITEM_ID = t.ITEM_ID AND i.ORD_DT = :v_dt AND t.SALE_YN = 'Y'
 GROUP BY t.ITEM_ID, t.ITEM_NM;

-- [R3] 오늘 주문이 없는 매장 (점검 대상)
--   원래 SQL
SELECT s.SHOP_NO, s.SHOP_NM
  FROM SHOP s, ORD o
 WHERE o.SHOP_NO(+) = s.SHOP_NO
   AND o.ORD_DT(+)  = :v_dt
   AND o.ORD_NO IS NULL;
--   옮긴 SQL
SELECT s.SHOP_NO, s.SHOP_NM
  FROM SHOP s
  LEFT JOIN ORD o ON o.SHOP_NO = s.SHOP_NO AND o.ORD_DT = :v_dt
 WHERE o.ORD_NO IS NULL;

실행계획 학습용 예시 ​

xplan
[R1 · 옮긴 SQL]
-----------------------------------------------------------------------------------------------------------
| Id  | Operation                             | Name    | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
-----------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                      |         |      1 |        |    197 |00:00:00.01 |      45 |
|   1 |  HASH GROUP BY                        |         |      1 |    197 |    197 |00:00:00.01 |      45 |
|*  2 |   HASH JOIN                           |         |      1 |   1370 |   1368 |00:00:00.01 |      45 |
|   3 |    TABLE ACCESS FULL                  | SHOP    |      1 |    200 |    200 |00:00:00.01 |       7 |
|   4 |    TABLE ACCESS BY INDEX ROWID BATCHED| ORD     |      1 |   1370 |   1368 |00:00:00.01 |      38 |
|*  5 |     INDEX RANGE SCAN                  | ORD_X01 |      1 |   1370 |   1368 |00:00:00.01 |       6 |
-----------------------------------------------------------------------------------------------------------
   2 - access("O"."SHOP_NO"="S"."SHOP_NO")
   5 - access("O"."ORD_DT"=:V_DT)

[R2 · 옮긴 SQL]
---------------------------------------------------------------------------------------------------------------------
| Id  | Operation                                 | Name            | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
---------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                          |                 |      1 |        |     92 |00:00:00.03 |    7183 |
|   1 |  HASH GROUP BY                            |                 |      1 |     92 |     92 |00:00:00.03 |    7183 |
|   2 |   NESTED LOOPS OUTER                      |                 |      1 |   3381 |   3404 |00:00:00.03 |    7183 |
|   3 |    TABLE ACCESS FULL                      | ITEM            |      1 |     92 |     92 |00:00:00.01 |       7 |
|   4 |    VIEW                                   | VW_LAT_4D1E92A6 |     92 |     37 |   3381 |00:00:00.03 |    7176 |
|*  5 |     FILTER                                |                 |     92 |        |   3381 |00:00:00.03 |    7176 |
|*  6 |      TABLE ACCESS BY INDEX ROWID BATCHED  | ORD_ITEM        |     78 |     37 |   3381 |00:00:00.03 |    7176 |
|*  7 |       INDEX RANGE SCAN                    | ORD_ITEM_X01    |     78 |   3381 |    264K|00:00:00.01 |     936 |
---------------------------------------------------------------------------------------------------------------------
   5 - filter("T"."SALE_YN"='Y')
   6 - filter("I"."ITEM_ID"="T"."ITEM_ID")
   7 - access("I"."ORD_DT"=:V_DT)

[원래 SQL과 R3 요약]
  R1 원래   HASH JOIN OUTER   45블록  200줄
  R2 원래   HASH JOIN OUTER   87블록   78줄  (ITEM 78행이 build, 그날 주문상품 범위는 한 번)
  R3 원래 · 옮긴 SQL  HASH JOIN ANTI   45블록  3줄 (두 계획이 같다)

문제 ​

  1. 세 리포트마다 옮긴 SQL이 원래 SQL과 결과가 같은지 판정하고, 다르면 몇 줄이 어떻게 달라지는지 쓰시오.
  2. 틀린 것을 바르게 고친 ANSI SQL을 작성하고, R2를 잘못 옮겼을 때 일량이 87블록에서 7,183블록으로 늘어난 이유를 계획으로 설명하시오.
  3. (+) 조건을 ANSI로 옮기는 규칙을 조건의 종류(두 테이블을 잇는 조건, inner 쪽 상수 조건, 보존할 쪽 조건, IS NULL 조건)별로 정리하시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자