상황: 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이 항상 같은 비율로 빠르다는 뜻은 아니므로 실제 실행계획을 비교해야 한다.


