UNION ALL 을 PARTITION BY 로 변환 (Partition Outer Join)
같은 데이터에 대해 여러 키 (예: 부서별) 의 outer join 결과를 UNION ALL 로 합치던 패턴은
PARTITION BY절을 OUTER JOIN 에 결합 하여 한 번의 쿼리로 풀 수 있습니다. plan 에는MERGE JOIN PARTITION OUTER가 등장하며 dimension 테이블 access 가 1 회로 줄어듭니다.
핵심 정리
- PARTITION OUTER JOIN 의 정의: ANSI SQL 표준 (Oracle 10gR2+) 으로, OUTER JOIN 에
PARTITION BY를 결합하여 각 파티션 키마다 outer 측 row 를 보장 합니다. 시계열 빈 기간 채우기 (dense reporting) 의 표준 패턴입니다. - 변환 효과: UNION ALL 로 N 번 outer join 하던 패턴이 1 번의 PARTITION OUTER JOIN 으로 풀리면서 dimension 테이블 access 가 N → 1 회로 줄어듭니다.
- plan signature:
MERGE JOIN PARTITION OUTER+SORT PARTITION JOIN이 등장하면 partition outer join 이 적용된 것입니다.
시나리오
부서별 (deptno = 10, deptno = 20) 월별 매출을 조회. year_month 는 모든 월을 가지고 있고 dept_sale_history 는 매출이 발생한 월만 가집니다. 매출이 없는 월도 0 으로 채워서 출력해야 하는 dense reporting 시나리오.
| 테이블 | 건수 | 인덱스 |
|---|---|---|
year_month | 4 (2002 년 1~4 월) | pk_year_month (yymm) |
dept_sale_history | 6 | pk_dept_sale_history (deptno, yymm), idx_dept_sale_history_yymm (yymm, deptno) |
테스트 데이터 생성 SQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
-- year_month 테이블 (모든 월)
CREATE TABLE year_month (
yymm VARCHAR2(6),
CONSTRAINT pk_year_month PRIMARY KEY (yymm)
);
INSERT INTO year_month VALUES ('200201');
INSERT INTO year_month VALUES ('200202');
INSERT INTO year_month VALUES ('200203');
INSERT INTO year_month VALUES ('200204');
-- dept_sale_history 테이블 (매출이 발생한 월만)
CREATE TABLE dept_sale_history (
deptno NUMBER,
yymm VARCHAR2(6),
sale_amt NUMBER,
CONSTRAINT pk_dept_sale_history PRIMARY KEY (deptno, yymm)
);
CREATE INDEX idx_dept_sale_history_yymm ON dept_sale_history (yymm, deptno);
-- deptno 10: 200201, 200203
INSERT INTO dept_sale_history VALUES (10, '200201', 1000);
INSERT INTO dept_sale_history VALUES (10, '200203', 1500);
-- deptno 20: 200202, 200204
INSERT INTO dept_sale_history VALUES (20, '200202', 2000);
INSERT INTO dept_sale_history VALUES (20, '200204', 2500);
-- deptno 30 (본 쿼리에서는 제외)
INSERT INTO dept_sale_history VALUES (30, '200201', 3000);
INSERT INTO dept_sale_history VALUES (30, '200203', 3500);
COMMIT;
기대 결과: deptno 10, 20 각각 4 개월 × 2 부서 = 8 행 (매출 없는 월은 sale_amt = 0).
1) 원본 — UNION ALL 로 deptno 별 OUTER JOIN
1
2
3
4
5
6
7
8
9
10
11
SELECT e.deptno, m.yymm, NVL(e.sale_amt, 0) AS sale_amt
FROM year_month m
LEFT OUTER JOIN dept_sale_history e
ON (m.yymm = e.yymm AND e.deptno = 10)
WHERE m.yymm LIKE '2002%'
UNION ALL
SELECT e.deptno, m.yymm, NVL(e.sale_amt, 0) AS sale_amt
FROM year_month m
LEFT OUTER JOIN dept_sale_history e
ON (m.yymm = e.yymm AND e.deptno = 20)
WHERE m.yymm LIKE '2002%';
year_month 를 두 번 access 하면서 각 분기마다 dept_sale_history 와 outer join 합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
--------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU) | Time |
--------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 8 | 288 | 4 (0) | 00:00:01 |
| 1 | UNION-ALL | | | | | |
| 2 | NESTED LOOPS OUTER | | 4 | 144 | 2 (0) | 00:00:01 |
|* 3 | INDEX RANGE SCAN | PK_YEAR_MONTH | 4 | 20 | 2 (0) | 00:00:01 |
| 4 | TABLE ACCESS BY INDEX ROWID BATCHED | DEPT_SALE_HISTORY | 1 | 31 | 0 (0) | 00:00:01 |
|* 5 | INDEX RANGE SCAN | IDX_DEPT_SALE_HISTORY_YYMM | 1 | | 0 (0) | 00:00:01 |
| 6 | NESTED LOOPS OUTER | | 4 | 144 | 2 (0) | 00:00:01 |
|* 7 | INDEX RANGE SCAN | PK_YEAR_MONTH | 4 | 20 | 2 (0) | 00:00:01 |
| 8 | TABLE ACCESS BY INDEX ROWID BATCHED | DEPT_SALE_HISTORY | 1 | 31 | 0 (0) | 00:00:01 |
|* 9 | INDEX RANGE SCAN | IDX_DEPT_SALE_HISTORY_YYMM | 1 | | 0 (0) | 00:00:01 |
--------------------------------------------------------------------------------------------------------------------
Id 3 과 Id 7 가 같은 PK_YEAR_MONTH 인덱스를 두 번 RANGE SCAN 합니다. 부서가 더 늘어날수록 access 가 비례 증가.
2) 수정 — PARTITION BY 로 한 번의 PARTITION OUTER JOIN
1
2
3
4
5
6
SELECT e.deptno, m.yymm, NVL(e.sale_amt, 0) AS sale_amt
FROM year_month m
LEFT OUTER JOIN dept_sale_history e
PARTITION BY (e.deptno) ON (m.yymm = e.yymm)
WHERE m.yymm LIKE '2002%'
AND e.deptno IN (10, 20);
ANSI SQL 표준의 partitioned outer join 구문. PARTITION BY (e.deptno) 가 핵심입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
----------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU) | Time |
----------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 4 | 124 | 7 (29) | 00:00:01 |
| 1 | VIEW | | 4 | 124 | 7 (29) | 00:00:01 |
| 2 | MERGE JOIN PARTITION OUTER | | 4 | 144 | 7 (29) | 00:00:01 |
| 3 | SORT JOIN | | 4 | 20 | 3 (34) | 00:00:01 |
|* 4 | INDEX RANGE SCAN | PK_YEAR_MONTH | 4 | 20 | 2 (0) | 00:00:01 |
|* 5 | SORT PARTITION JOIN | | 4 | 124 | 3 (34) | 00:00:01 |
| 6 | INLIST ITERATOR | | | | | |
| 7 | TABLE ACCESS BY INDEX ROWID BATCHED | DEPT_SALE_HISTORY | 4 | 124 | 2 (0) | 00:00:01 |
|* 8 | INDEX RANGE SCAN | PK_DEPT_SALE_HISTORY | 1 | | 2 (0) | 00:00:01 |
----------------------------------------------------------------------------------------------------------------
Id 2의MERGE JOIN PARTITION OUTER가 partition outer join 의 signature 입니다.Id 5의SORT PARTITION JOIN은deptno파티션 단위로 dim 측을 정렬한 단계.Id 4의PK_YEAR_MONTHaccess 가 1 회만 발생 (원본은 2 회).Id 6~Id 8의INLIST ITERATOR가deptno IN (10, 20)처리로 두 deptno 의 PK 인덱스를 RANGE SCAN 합니다.
분석
일반 LEFT OUTER JOIN 은 outer 측 row 와 매칭되는 inner row 가 없을 때 단일 NULL row 를 반환합니다. PARTITION OUTER JOIN 은 여기서 한 단계 더 나아가 각 파티션 키마다 outer 측 row 를 보장 합니다.
year_month4 행 (모든 월)dept_sale_history의 deptno 10 은 200201, 200203 만 매출 있음- 일반
LEFT JOIN year_month m / dept_sale_history e ON yymm AND deptno=10→ 4 행 (월별로 deptno 10 의 매칭 또는 NULL) - PARTITION BY (deptno) 추가 → 4 행 × 2 부서 = 8 행 (각 부서별로 모든 월이 보장됨)
이것이 dense reporting 의 핵심: dimension (year_month) 의 모든 값이 각 partition (deptno) 별로 빠짐없이 출력.
Time Series 빈 기간 채우기 패턴
dense reporting 의 가장 흔한 사용 사례는 시계열 데이터의 빈 기간 채우기 입니다.
| 시계열 형태 | 일반 LEFT JOIN 결과 | PARTITION OUTER JOIN 결과 |
|---|---|---|
| 부서별 월별 매출 | 매출 있는 월만 | 모든 월 (매출 없으면 0) |
| 직원별 일별 출근 | 출근일만 | 모든 일 (결근일 표시) |
| 상품별 시간대 판매 | 판매 시간대만 | 모든 시간대 (판매 0 표시) |
PARTITION BY 위치의 의미
ANSI SQL 표준 구문은 다음과 같습니다.
1
2
3
-- 형태 1: outer 가 파티션 안 (이번 글의 형태)
year_month m LEFT OUTER JOIN dept_sale_history e
PARTITION BY (e.deptno) ON (m.yymm = e.yymm)
year_month 의 모든 row 가 dept_sale_history.deptno 의 각 파티션마다 매칭됩니다. 즉 year_month × distinct(deptno) cartesian-like 구조 + 매칭.
일반 OUTER JOIN 과 PARTITION OUTER JOIN 비교
m × e (deptno=10) on yymm
deptno 가 매번 달라야 하면
UNION ALL N 회 필요
m × distinct(e.deptno) × e on yymm
^ deptno 파티션마다 m 의 모든 row
deptno 별 outer 보장이 자동
한 번의 join 으로 끝Partition Outer Join 은 dense reporting (빈 기간 / 빈 카테고리 채우기) 의 표준 ANSI SQL 패턴입니다. UNION ALL 로 partition 별 outer join 을 N 번 작성하던 패턴이 보이면 즉시 변환을 검토할 수 있는 강력한 방법.
변환 효과 비교
| 항목 | 원본 (UNION ALL) | 수정 (PARTITION OUTER JOIN) |
|---|---|---|
year_month access | 2 회 | 1 회 |
dept_sale_history access | 2 회 (각 deptno) | 1 회 (INLIST IN 10,20) |
| Plan 형태 | UNION-ALL + NL OUTER × 2 | MERGE JOIN PARTITION OUTER |
| 부서 N 개로 확장 시 | UNION ALL N 회 (linear) | 한 번의 PARTITION OUTER JOIN |
부서 수가 늘어날수록 효과가 비례 증가합니다.
정리
- UNION ALL + 같은 outer 측 + 다른 partition 키 패턴은
LEFT OUTER JOIN ... PARTITION BY (key) ON ...의 ANSI SQL 표준 구문으로 변환할 수 있습니다. - plan signature 는
MERGE JOIN PARTITION OUTER+SORT PARTITION JOIN입니다. 이 두 operation 이 보이면 partition outer join 이 적용된 것입니다. - 활용 영역: 시계열 데이터의 빈 기간 채우기, 카테고리별 dense reporting 등 dimension 의 모든 값을 partition 별로 보장 해야 하는 모든 케이스에 적합합니다.