집합 연산은 두 SELECT 의 결과를 위아래로 합치거나 비교합니다. JOIN 처럼 컬럼이 옆으로 늘어나지 않고, 두 SELECT 의 컬럼 개수와 순서가 그대로 결과의 컬럼이 됩니다.
| 연산 | 의미 | 중복 처리 |
|---|---|---|
| UNION | 합집합 | 중복 제거 |
| UNION ALL | 합집합 | 중복 유지 |
| INTERSECT | 교집합(양쪽에 다 있는 행) | 중복 제거 |
| EXCEPT(MINUS) | 차집합(첫 결과에만 있는 행) | 중복 제거 |
핵심UNION·INTERSECT·EXCEPT 는 모두 기본적으로 중복을 제거합니다. UNION 만 ALL 을 붙여 중복을 유지할 수 있고, 표준 INTERSECT ALL·EXCEPT ALL 은 이 레슨에서 다루는 4개 DB 어디서도 사실상 쓰지 못합니다(2.5절).
두 SELECT 는 컬럼 개수가 같아야 합니다. 첫 번째 SELECT 가 2개 컬럼을 반환하면 두 번째도 2개여야 하고, 하나라도 다르면 실행 전에 오류가 납니다. 컬럼 순서가 같은 자리끼리 비교·결합되므로, 같은 의미의 값을 같은 위치에 놓아야 합니다.
타입도 맞아야 합니다. 문자 컬럼 자리에 숫자 컬럼을 두면 DB 가 한쪽을 다른 쪽 타입으로 변환하려다 실패할 수 있습니다. 3절 예제에서 두 오류를 직접 재현합니다.
주의컬럼 수가 다르면 실행 전에 바로 오류가 나지만, 타입이 어중간하게 맞아떨어지면(숫자를 문자로 변환하는 경우 등) 오류 없이 조용히 이상한 값이 섞일 수 있습니다. 집합 연산을 쓸 때는 각 컬럼이 같은 의미인지 눈으로 한 번 더 확인합니다.
결과 컬럼의 이름(헤더)은 항상 첫 번째 SELECT 의 별칭을 따릅니다. 두 번째 이후 SELECT 에 다른 별칭을 붙여도 결과에는 반영되지 않습니다. ORDER BY 에서 컬럼 이름을 쓸 때도 첫 SELECT 기준의 이름을 써야 합니다.
집합 연산으로 합친 여러 SELECT 전체에 ORDER BY 는 맨 마지막에 딱 한 번만 씁니다. 중간 SELECT 뒤에 ORDER BY 를 붙이면 문법 오류가 납니다. 각 SELECT 를 괄호로 감싸면 문법상으로는 통과하지만, 집합 연산 결과의 행 순서는 다시 섞이므로 안쪽 ORDER BY 는 의미가 없습니다. 정렬은 항상 전체 결과에 맨 끝에서 한 번만 하는 것이 정확하고 간단합니다.
표준 SQL 은 UNION 뿐 아니라 INTERSECT ALL·EXCEPT ALL 도 정의합니다. 양쪽에 겹치는 개수만큼 중복까지 살려서 비교하는 연산이지만, 실제로 쓸 수 있는 DB 가 많지 않습니다. Oracle 은 21c 부터 MINUS ALL·INTERSECT ALL·EXCEPT ALL 을 지원하고, 그 이전 버전과 Tibero 는 ALL 옵션이 없습니다. MSSQL 은 DISTINCT(중복 제거) 결과만 지원합니다.
MySQL 은 8.0.31 부터 ALL 옵션까지 지원합니다. 다만 이 레슨이 다루는 H2 자체가 INTERSECT ALL·EXCEPT ALL 문법을 지원하지 않아, 어떤 모드로 실행해도 오류가 납니다(4절 확인).
= 비교에서는 NULL 과 NULL 을 비교하면 참이 아니라 UNKNOWN 입니다(중급 02 에서 다룬 3값 논리). 반면 집합 연산은 "같은 값의 행인가"를 판단할 때 NULL 과 NULL 을 같은 값으로 취급합니다. NULL 이 섞인 행도 INTERSECT·EXCEPT 로 정상적으로 비교됩니다.
주의
WHERE a.x = b.x로 두 테이블을 비교하면 양쪽 다 NULL 인 행은 매칭되지 않습니다. INTERSECT·EXCEPT 로 비교하면 NULL 도 값으로 취급해 매칭됩니다. 같은 "비교"라도 결과가 다를 수 있다는 뜻이므로, 두 방식을 섞어 쓰지 않습니다.
UNION 은 중복 제거를 위해 내부적으로 정렬하거나 해시 테이블을 만듭니다. 두 결과를 합친 다음 전체를 훑어 중복 행을 골라내는 작업이 추가로 들어간다는 뜻입니다. 반환되는 두 결과에 애초에 중복이 없다고 확신할 수 있으면(예: 서로 다른 연도 데이터, 기본키가 겹치지 않는 테이블) UNION 대신 UNION ALL 을 써서 이 비용을 없앨 수 있습니다.
또 다른 흔한 낭비는 같은 테이블을 조건만 바꿔 두 번 읽고 UNION ALL 로 합치는 패턴입니다. "완료 상태만" 조회와 "취소 상태만" 조회를 UNION ALL 로 합치면 같은 테이블을 두 번 스캔합니다. 두 조건을 OR 나 IN 으로 합치면 결과는 같으면서 스캔은 한 번으로 줄어듭니다.
sql-src/mid_04_set_ops/01_union.sql 끝부분에 두 방식을 나란히 실행해 같은 5행이 나오는 것을 확인해 뒀습니다.
집합 연산으로 같은 테이블을 여러 번 읽고 있다면 먼저 WHERE 조건을 OR·IN·CASE 로 합칠 수 없는지 살펴봅니다. 서로 다른 테이블을 합칠 때만 집합 연산이 필요합니다.