집합 연산자
집합 연산자는 두 개 이상의 SELECT 결과를 하나의 결과 집합으로 결합한다. 조인이 컬럼을 옆으로 붙인다면, 집합 연산자는 행을 위아래로 결합한다.
집합 연산자 종류
| 연산자 | 의미 | 중복 제거 |
|---|---|---|
| UNION | 합집합 | 제거 |
| UNION ALL | 합집합 | 제거하지 않음 |
| INTERSECT | 교집합 | 제거 |
| MINUS (Oracle) / EXCEPT (SQL Server, 표준) | 차집합 (앞 결과에서 뒤 결과를 뺀다) | 제거 |
성능 관점
중복을 제거하는 연산자는 내부적으로 정렬이나 해시 작업이 필요해 UNION ALL보다 부담이 크다. 중복이 없다는 것이 보장되거나 중복을 그대로 보고 싶다면 UNION ALL을 쓰는 것이 효율적이다.
사용 조건
- 컬럼 수 일치 각 SELECT 절의 컬럼 수가 같아야 한다.
- 타입 호환 같은 위치의 컬럼끼리 데이터 타입이 호환되어야 한다.
- 컬럼명은 첫 SELECT 결과 컬럼명(헤더)은 첫 번째 SELECT 절의 컬럼명 또는 별칭을 따른다.
- ORDER BY는 맨 끝 한 번 개별 SELECT에 붙이면 오류다. 첫 번째 SELECT의 컬럼명·별칭 또는 위치 번호를 사용한다.
SELECT ENAME AS 이름, SAL FROM EMP WHERE DEPTNO = 10
UNION ALL
SELECT ENAME, SAL FROM EMP WHERE DEPTNO = 20
ORDER BY 이름; -- 또는 ORDER BY 1
시험 포인트: 두 번째 SELECT에 붙인 별칭은 결과 헤더에 반영되지 않는다. 마지막 ORDER BY에서 두 번째 SELECT의 별칭만 사용하면 오류가 날 수 있다.
예제 데이터
두 매장의 판매 상품 목록이다. 배, 포도가 양쪽에 겹친다.
STORE_A
| ITEM |
|---|
| 사과 |
| 배 |
| 포도 |
STORE_B
| ITEM |
|---|
| 배 |
| 포도 |
| 귤 |
UNION과 UNION ALL
- UNION ALL 두 결과를 그대로 이어 붙인다. 겹치는 배, 포도도 두 번씩 나와 6행.
- UNION 중복을 하나씩만 남긴다. 배, 포도가 한 번씩만 나와 4행.
SELECT ITEM FROM STORE_A
UNION ALL
SELECT ITEM FROM STORE_B;
| ITEM |
|---|
| 사과 |
| 배 |
| 포도 |
| 배 |
| 포도 |
| 귤 |
UNION ALL은 두 결과를 그대로 이어 붙이므로 6행이 된다.
SELECT ITEM FROM STORE_A
UNION
SELECT ITEM FROM STORE_B
ORDER BY ITEM;
| ITEM |
|---|
| 귤 |
| 배 |
| 사과 |
| 포도 |
UNION은 중복된 배, 포도를 하나씩만 남겨 4행이 된다.
정렬은 보장되지 않는다
중복 제거 과정에서 정렬된 것처럼 보일 수 있지만 순서가 보장되지는 않는다. 원하는 순서가 있으면 ORDER BY를 명시한다.
시험 포인트: 한 테이블 안에 중복 행이 있어도 UNION은 그것까지 제거한다. 즉 UNION 결과는 항상 중복 없는 집합이다.
INTERSECT
SELECT ITEM FROM STORE_A
INTERSECT
SELECT ITEM FROM STORE_B;
| ITEM |
|---|
| 배 |
| 포도 |
양쪽에 모두 있는 행만 남긴다. 같은 결과를 EXISTS나 IN으로도 만들 수 있다.
SELECT DISTINCT A.ITEM
FROM STORE_A A
WHERE EXISTS (SELECT 1 FROM STORE_B B WHERE B.ITEM = A.ITEM);
MINUS / EXCEPT
-- Oracle
SELECT ITEM FROM STORE_A
MINUS
SELECT ITEM FROM STORE_B;
-- SQL Server
SELECT ITEM FROM STORE_A
EXCEPT
SELECT ITEM FROM STORE_B;
| ITEM |
|---|
| 사과 |
순서를 바꾸면 결과도 바뀐다. STORE_B MINUS STORE_A의 결과는 귤이다.
같은 결과를 NOT EXISTS로 표현할 수 있다.
SELECT DISTINCT A.ITEM
FROM STORE_A A
WHERE NOT EXISTS (SELECT 1 FROM STORE_B B WHERE B.ITEM = A.ITEM);
연산 결과 비교 정리
| 연산 | 결과 | 행 수 |
|---|---|---|
| A UNION ALL B | 사과, 배, 포도, 배, 포도, 귤 | 6 |
| A UNION B | 귤, 배, 사과, 포도 | 4 |
| A INTERSECT B | 배, 포도 | 2 |
| A MINUS B | 사과 | 1 |
| B MINUS A | 귤 | 1 |
UNION, INTERSECT, MINUS는 교환법칙 관점에서 다르다. UNION과 INTERSECT는 순서를 바꿔도 결과 집합이 같지만 MINUS는 달라진다.
집합 연산자와 NULL
NULL끼리는 같은 값
집합 연산자는 중복을 판단할 때 NULL끼리 같은 값으로 취급한다. WHERE 절의 = 비교에서 NULL끼리 같지 않은 것과 다르다.
| 쿼리 | 결과 |
|---|---|
SELECT NULL FROM DUAL UNION SELECT NULL FROM DUAL |
NULL 1행 |
SELECT NULL FROM DUAL UNION ALL SELECT NULL FROM DUAL |
NULL 2행 |
SELECT NULL FROM DUAL INTERSECT SELECT NULL FROM DUAL |
NULL 1행 |
NULL의 일반적인 비교 규칙은 NULL과 속성을 참고한다.
여러 집합 연산자를 함께 쓸 때
SELECT ITEM FROM STORE_A
UNION
SELECT ITEM FROM STORE_B
MINUS
SELECT ITEM FROM STORE_C;
Oracle에서는 위에서 아래로 차례대로 수행된다.
- STORE_A 첫 번째 결과
- UNION STORE_B 합집합을 먼저 만든다
- MINUS STORE_C 그 결과에서 C를 뺀다
- Oracle은 집합 연산자의 우선순위가 모두 같아 위에서 아래로 차례대로 수행한다.
- SQL 표준과 SQL Server는 INTERSECT가 UNION, EXCEPT보다 우선순위가 높다.
- 의도를 분명히 하려면 괄호로 묶어 수행 순서를 명시한다.
시험 포인트: 집합 연산 결과에 GROUP BY나 WHERE를 적용하고 싶으면 전체를 인라인 뷰로 감싼다. 개별 SELECT 안의 WHERE·GROUP BY는 각 SELECT에만 적용된다.
핵심 요약
- UNION과 INTERSECT, MINUS(EXCEPT)는 중복을 제거하고 UNION ALL은 제거하지 않는다.
- 각 SELECT의 컬럼 수가 같고 대응하는 컬럼의 타입이 호환되어야 한다.
- 결과 컬럼명은 첫 번째 SELECT 기준이다.
- ORDER BY는 맨 마지막에 한 번만 쓰며, 정렬 순서는 ORDER BY로만 보장된다.
- MINUS는 순서에 따라 결과가 달라지고 SQL Server·표준에서는 EXCEPT라고 쓴다.
- 집합 연산자는 NULL끼리 같은 값으로 보고 중복을 제거한다.
- 성능상 중복 제거가 필요 없으면 UNION ALL을 사용한다.