Search

REMINDER 07-04. 중복 제거가 불필요하면 UNION ALL을 검토하자

상황: 2023년 3월의 카드(CARD)·현금(CASH) 결제 주문번호와 매장 ID를 하나로 조회한다. 실제 일반 테이블 startdbpg.tr_ord에서 검증했다. 이 테이블의 ord_no는 PK이며, pay_tp는 주문별 단일 결제유형이다.

BAD SQL — 중복 제거가 필요하지 않은데 UNION 사용

SELECT o.ord_no, o.shop_id FROM startdbpg.tr_ord o WHERE o.ord_dtm >= TIMESTAMP '2023-03-01' AND o.ord_dtm < TIMESTAMP '2023-04-01' AND o.pay_tp = 'CARD' UNION SELECT o.ord_no, o.shop_id FROM startdbpg.tr_ord o WHERE o.ord_dtm >= TIMESTAMP '2023-03-01' AND o.ord_dtm < TIMESTAMP '2023-04-01' AND o.pay_tp = 'CASH';
SQL
복사
UNION은 두 SELECT의 결과를 합치면서 전체 출력 컬럼 기준 중복 행을 제거한다. 그러나 주문 한 건이 카드 결제이면서 동시에 현금 결제일 수 없고, 출력한 ord_no도 PK이므로 여기서는 중복 제거가 불필요하다.

GOOD SQL 1 — 중복 제거가 필요 없다면 UNION ALL

SELECT o.ord_no, o.shop_id FROM startdbpg.tr_ord o WHERE o.ord_dtm >= TIMESTAMP '2023-03-01' AND o.ord_dtm < TIMESTAMP '2023-04-01' AND o.pay_tp = 'CARD' UNION ALL SELECT o.ord_no, o.shop_id FROM startdbpg.tr_ord o WHERE o.ord_dtm >= TIMESTAMP '2023-03-01' AND o.ord_dtm < TIMESTAMP '2023-04-01' AND o.pay_tp = 'CASH';
SQL
복사
UNION ALL은 중복 제거를 수행하지 않는다. 다만 동일 테이블을 카드·현금 조건으로 두 번 검색하는 구조는 남아 있다.

GOOD SQL 2 — 이 사례의 최종 권장안: WHERE IN으로 통합

SELECT o.ord_no, o.shop_id FROM startdbpg.tr_ord o WHERE o.ord_dtm >= TIMESTAMP '2023-03-01' AND o.ord_dtm < TIMESTAMP '2023-04-01' AND o.pay_tp IN ('CARD', 'CASH');
SQL
복사
하나의 주문 테이블에서 결제유형 조건만 다르게 조회하므로, WHERE IN으로 통합하면 두 번 검색하지 않고 한 번만 검색할 수 있다. 서로 다른 테이블 또는 독립적인 결과 집합을 결합해야 한다면 이 방식으로 대체할 수 없고 UNION ALL 등을 검토해야 한다.
실제 데이터 확인
•
2023년 3월 CARD 주문: 16,878건
•
2023년 3월 CASH 주문: 704건
•
전체 조회 대상: 17,582건
•
tr_ord.ord_no는 실제 PRIMARY KEY
•
UNION, UNION ALL, WHERE IN 모두 17,582행 반환
•
세 결과의 EXCEPT ALL 양방향 검증: 차이 0건
실제 실행계획 비교 (EXPLAIN (ANALYZE, BUFFERS, COSTS OFF))
비교 항목
BAD: UNION
GOOD 1: UNION ALL
GOOD 2: WHERE IN
반환 행 수
17,582
17,582
17,582
실행시간 (각 1회)
29.931ms
14.934ms
8.819ms
Shared Buffer Hit
524
524
262
주문 테이블 검색
2회
2회
1회
주요 실행 노드
Append → HashAggregate
Append
Index Scan

BAD SQL — : UNION 실제 실행계획

HashAggregate (actual time=24.320..28.198 rows=17582 loops=1) Group Key: o.ord_no, o.shop_id Batches: 1 Memory Usage: 1553kB Buffers: shared hit=524 -> Append (actual time=0.097..15.269 rows=17582 loops=1) Buffers: shared hit=524 -> Index Scan using tr_ord_x51 on tr_ord o (actual time=0.096..8.022 rows=16878 loops=1) Index Cond: ((ord_dtm >= '2023-03-01 00:00:00'::timestamp without time zone) AND (ord_dtm < '2023-04-01 00:00:00'::timestamp without time zone)) Filter: ((pay_tp)::text = 'CARD'::text) Rows Removed by Filter: 704 Buffers: shared hit=262 -> Index Scan using tr_ord_x51 on tr_ord o_1 (actual time=0.035..5.158 rows=704 loops=1) Index Cond: ((ord_dtm >= '2023-03-01 00:00:00'::timestamp without time zone) AND (ord_dtm < '2023-04-01 00:00:00'::timestamp without time zone)) Filter: ((pay_tp)::text = 'CASH'::text) Rows Removed by Filter: 16878 Buffers: shared hit=262 Planning: Buffers: shared hit=173 Planning Time: 0.963 ms Execution Time: 29.931 ms
Plain Text
복사

GOOD SQL 1 — : UNION ALL 실제 실행계획

Append (actual time=0.057..13.668 rows=17582 loops=1) Buffers: shared hit=524 -> Index Scan using tr_ord_x51 on tr_ord o (actual time=0.057..6.741 rows=16878 loops=1) Index Cond: ((ord_dtm >= '2023-03-01 00:00:00'::timestamp without time zone) AND (ord_dtm < '2023-04-01 00:00:00'::timestamp without time zone)) Filter: ((pay_tp)::text = 'CARD'::text) Rows Removed by Filter: 704 Buffers: shared hit=262 -> Index Scan using tr_ord_x51 on tr_ord o_1 (actual time=0.034..4.964 rows=704 loops=1) Index Cond: ((ord_dtm >= '2023-03-01 00:00:00'::timestamp without time zone) AND (ord_dtm < '2023-04-01 00:00:00'::timestamp without time zone)) Filter: ((pay_tp)::text = 'CASH'::text) Rows Removed by Filter: 16878 Buffers: shared hit=262 Planning: Buffers: shared hit=150 Planning Time: 0.817 ms Execution Time: 14.934 ms
Plain Text
복사

GOOD SQL 2 — : WHERE IN 실제 실행계획

Index Scan using tr_ord_x51 on tr_ord o (actual time=0.049..7.651 rows=17582 loops=1) Index Cond: ((ord_dtm >= '2023-03-01 00:00:00'::timestamp without time zone) AND (ord_dtm < '2023-04-01 00:00:00'::timestamp without time zone)) Filter: ((pay_tp)::text = ANY ('{CARD,CASH}'::text[])) Buffers: shared hit=262 Planning: Buffers: shared hit=202 Planning Time: 0.930 ms Execution Time: 8.819 ms
Plain Text
복사
실행계획 읽기: UNION은 Append로 두 집합을 결합한 다음 HashAggregate로 중복을 제거한다. UNION ALL은 중복 제거용 HashAggregate가 없지만, 두 브랜치에서 tr_ord_x51을 각각 사용하여 해당 월의 주문을 반복 검색한다. WHERE IN은 한 번의 Index Scan으로 조회한다. 이 실습에서는 실행시간뿐 아니라 버퍼 접근량도 524 → 262로 줄었다.

핵심

•
중복 제거가 필요 없는 집합 결합은 UNION ALL을 우선 검토하자. 더 나아가 동일 테이블의 OR 조건으로 표현할 수 있다면 WHERE IN 등으로 한 번의 조회로 통합하는 편이 단순할 수 있다.
💡
참고·예외
•
서로 다른 브랜치에 같은 출력 행이 생길 수 있으면 UNION과 UNION ALL의 결과는 달라진다. UNION은 출력된 컬럼의 조합으로 중복을 제거하며, 원본의 PK가 출력되지 않으면 서로 다른 원본 행도 중복으로 판단될 수 있다.
•
중복을 보존해야 하는 업무에서는 UNION ALL을 사용해야 한다.
•
서로 다른 테이블의 결과를 결합하는 경우 WHERE IN은 일반적인 대체안이 아니다.
•
이번 수치는 각각 1회 측정값이다. UNION ALL 또는 WHERE IN이 항상 같은 비율로 빠르다는 뜻은 아니므로 실제 실행계획을 비교해야 한다.