테마
본사가 리포트 도구를 바꾸면서 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블록)-- [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;
[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줄 (두 계획이 같다)
(+) 조건을 ANSI로 옮기는 규칙을 조건의 종류(두 테이블을 잇는 조건, inner 쪽 상수 조건, 보존할 쪽 조건, IS NULL 조건)별로 정리하시오.