4 분 소요

SQL에서 UNION과 UNION ALL은 두 개 이상의 SELECT 결과를 하나로 결합하는 연산자다. 둘은 유사하지만 중복 처리와 성능에서 중요한 차이가 있으며, 이 차이를 모르면 코테와 실무 양쪽에서 버그와 성능 저하를 유발한다. 이 글에서는 차이점, 문법, 실전 예제를 정리한다.

UNION 과 UNION ALL 에 대한 정리 및 설명

1. UNION

  • UNION 연산자는 두 개 이상의 SELECT 쿼리 결과를 결합하고, 중복된 행을 제거해 반환한다.
  • 즉, 중복을 제거한 유니크한 결과만 반환한다.

  • 특징:
    1. 중복 제거: 결과에서 중복된 행을 제거한다.
    2. 정렬 수행: 중복을 제거하기 위해 내부적으로 정렬 또는 해시 기반 중복 제거 단계를 거치므로, 결과 집합이 크면 상대적으로 더 많은 리소스를 사용한다1.
    3. 컬럼 수와 타입 일치: 결합하는 모든 쿼리의 컬럼 수와 데이터 타입이 동일해야 한다.
  • 문법:
SELECT column_name(s) FROM table1
UNION
SELECT column_name(s) FROM table2;
  • 예제: 두 테이블에서 서로 다른 고객 목록을 결합하기
# `Customers_Online`과 `Customers_Offline` 테이블에서 중복되는 고객을 제거해 유일한 고객 목록을 반환한다.
SELECT customer_id, customer_name FROM Customers_Online
UNION
SELECT customer_id, customer_name FROM Customers_Offline;

2. UNION ALL

  • UNION ALL 연산자는 두 개 이상의 SELECT 쿼리 결과를 결합하며, 중복된 행을 제거하지 않고 모두 반환한다. 따라서 중복된 데이터도 그대로 출력된다.
  • 특징:
    1. 중복 허용: 중복된 행도 그대로 반환한다.
    2. 성능: 중복 제거 단계가 생략되므로 일반적으로 UNION보다 빠르다. 실제 차이는 데이터 양·중복률·인덱스 유무에 따라 달라지므로 특정 배수로 단정할 수 없다.
    3. 컬럼 수와 타입 일치: 결합하는 모든 쿼리의 컬럼 수와 데이터 타입이 동일해야 한다.
  • 문법:
SELECT column_name(s) FROM table1
UNION ALL
SELECT column_name(s) FROM table2;
  • 예제: 두 테이블에서 중복을 허용하여 고객 목록 결합하기
# `Customers_Online`과 `Customers_Offline` 테이블의 고객 목록을 중복된 행까지 포함해 모두 반환한다.
SELECT customer_id, customer_name FROM Customers_Online
UNION ALL
SELECT customer_id, customer_name FROM Customers_Offline;

3. UNION UNION ALL의 차이점

특징 UNION UNION ALL
중복된 행 중복된 행을 제거 중복된 행을 제거하지 않음
성능 중복 제거를 위해 더 많은 리소스 사용 중복 제거를 하지 않으므로 더 빠름
정렬 작업 중복을 제거하기 위해 정렬이 필요함 정렬 작업이 필요하지 않음
사용 목적 중복 없는 결과를 원할 때 사용 모든 데이터를 그대로 출력할 때 사용

4. 사용 시 주의사항

  1. 컬럼 수와 데이터 타입:
    • UNION 또는 UNION ALL을 사용할 때, 결합하는 쿼리의 컬럼 수와 데이터 타입이 동일해야 한다.
    • 각 컬럼의 순서도 동일해야 한다. 예를 들어 첫 번째 컬럼이 숫자 타입이면, 두 번째 쿼리의 첫 번째 컬럼도 숫자 타입이어야 한다.
    • UNION의 중복 제거는 값과 타입(콜레이션)을 기준으로 하므로, 콜레이션이 대소문자를 구분하지 않으면 'a'와 'A'가 같은 행으로 취급되어 하나만 남는다1.
  2. 성능 차이:
    • UNION ALL은 중복을 제거하지 않으므로 일반적으로 더 빠르다.
    • UNION은 중복 제거를 위한 정렬 또는 해시 작업이 추가로 필요해 더 많은 리소스를 쓴다. 데이터 양이 많을수록 두 연산자의 차이가 커진다.
    • 성능 비교는 엔진 버전·데이터 분포·인덱스에 좌우되므로 특정 배수로 단정하지 말고, 실행 계획(EXPLAIN)으로 실제 처리 방식을 확인하는 편이 안전하다.
  3. 중복 처리 여부:
    • UNION은 중복된 데이터를 제거해 반환하므로, 결과에서 중복을 원치 않을 때 유용하다.
    • 반대로, 모든 데이터를 반환하고자 할 때는 UNION ALL을 사용하는 것이 적합하다.
    • ORDER BY는 결합된 전체 결과의 맨 끝에 한 번만 지정할 수 있고, 개별 SELECT에는 붙일 수 없다1.

JOIN

INNER·LEFT OUTER·RIGHT OUTER·FULL OUTER 조인의 결과 집합을 나타낸 도식

출처: Microsoft Learn — Kusto 쿼리 언어 join 연산자 문서 (https://learn.microsoft.com/en-us/kusto/query/join-operator). 표기는 Kusto 기준이며 inner=INNER JOIN, leftouter=LEFT OUTER JOIN, rightouter=RIGHT OUTER JOIN, fullouter=FULL OUTER JOIN, innerunique는 Kusto의 중복 제거 기본값에 대응한다.

JOIN 유형 선택 가이드:
────────────────────────────────────────────────────────────
 양쪽 테이블 모두 일치하는 행만       ──> INNER JOIN
 왼쪽 전체 + 일치 행 (없으면 NULL)    ──> LEFT JOIN
 오른쪽 전체 + 일치 행 (없으면 NULL)  ──> RIGHT JOIN
 양쪽 전체 (미일치 포함)              ──> FULL OUTER (MySQL은 UNION으로 에뮬레이션)
 모든 행 조합 (데카르트 곱)           ──> CROSS JOIN
 같은 테이블을 자기 자신과 결합       ──> SELF JOIN
────────────────────────────────────────────────────────────

CROSS JOIN

  • CROSS JOIN은 한쪽 테이블의 모든 행을 다른 쪽 테이블의 모든 행과 짝지어 출력한다. 결과 행 수는 두 테이블 행 수의 곱이 된다.
  • 콤마(,)로 두 테이블을 이어 쓰는 것도 같은 동작이며, WHERE로 결합 조건을 걸지 않으면 CROSS JOIN과 동일한 결과가 나온다.
-- cross join
SELECT *
    FROM EMP
    CROSS JOIN DEPT;

-- ,로 연결
SELECT *
    FROM EMP, DEPT;

1. 집계 함수와 JOIN의 조합

  • 서브쿼리에서 부서별 최대 급여를 구한 뒤, 이를 JOIN해 해당 급여를 받는 직원을 찾는다.
# 예제: 부서별로 가장 높은 급여를 받는 직원 찾기
SELECT e.employee_name, e.salary, e.department_id
FROM employees e
JOIN (
    SELECT department_id, MAX(salary) AS max_salary
    FROM employees
    GROUP BY department_id
) AS dept_max
ON e.department_id = dept_max.department_id AND e.salary = dept_max.max_salary;

2. SELF JOIN 활용

  • 자기 자신과의 JOIN을 통해 같은 부서에 속하지만 자신은 아닌 다른 직원들을 조회할 수 있다.
# 예제: 직원 테이블에서 같은 부서에 속한 직원을 찾기
SELECT e1.employee_name AS employee, e2.employee_name AS colleague
FROM employees e1
JOIN employees e2
ON e1.department_id = e2.department_id
WHERE e1.employee_id <> e2.employee_id;

3. EXISTS 서브쿼리와 JOIN의 결합

  • EXISTS는 서브쿼리가 행을 하나라도 반환하면 TRUE로 평가된다. 따라서 직원이 있는 부서만 결과에 남는다.
# 예제: 부서에 직원이 존재하는 부서만 조회하기
SELECT d.department_name
FROM departments d
WHERE EXISTS (
    SELECT 1
    FROM employees e
    WHERE e.department_id = d.department_id
);

WHERE vs ON vs HAVING

절 적용 시점 목적
WHERE 조인/그룹화 이전 개별 행 단위 필터링
ON 조인 시점 테이블 간 결합 조건 지정
HAVING 그룹화 이후 집계(그룹) 결과에 대한 조건

1. WHERE 절

  • 목적: 개별 행을 필터링한다.
  • 적용 시점: 테이블에서 데이터를 조인하기 전에 각 행에 조건을 적용한다.
  • 사용 위치: 일반적으로 SELECT, UPDATE, DELETE 문에서 사용한다.
  • 예시:
    # 주문 날짜가 2024년 1월 1일 이후인 행만 선택한다.
    SELECT * 
    FROM Orders 
    WHERE order_date > '2024-01-01';
    

2. ON 절

  • 목적: 테이블 간의 조인 조건을 지정한다.
  • 적용 시점: 두 테이블을 조인할 때 조건을 적용한다.
  • 사용 위치: JOIN 문과 함께 사용한다.
  • 예시:
    # `Orders`와 `Customers` 테이블을 고객 ID를 기준으로 결합한다.
    SELECT * 
    FROM Orders O
    JOIN Customers C 
    ON O.customer_id = C.customer_id;
    

ON 절의 조건은 테이블을 결합하는 시점에 평가된다. INNER JOIN이라면 조건에 맞는 행만 결과에 남고, 맞지 않는 행은 제외된다. 반면 LEFT/RIGHT OUTER JOIN이라면 매칭되지 않은 쪽의 컬럼이 NULL로 채워지고, 기준이 되는 테이블의 행은 그대로 유지된다. 즉 “조건이 맞지 않으면 NULL“이라는 동작은 OUTER JOIN에 해당하고, INNER JOIN에는 적용되지 않는다.

3. HAVING 절

  • 목적: 그룹화된 데이터를 필터링한다.
  • 적용 시점: 그룹화 및 집계된 후 조건을 적용한다.
  • 사용 위치: GROUP BY 문 뒤에 사용한다.
  • 예시:
    # 고객별로 주문 금액을 합산한 뒤, 그 합계가 1000을 초과하는 그룹만 선택한다.
    SELECT customer_id, SUM(order_amount) AS total_spent
    FROM Orders
    GROUP BY customer_id
    HAVING total_spent > 1000;
    
  • 세 절의 비교 요약

    절 주요 목적 적용 시점 사용 위치
    WHERE 개별 행 필터링 조인 전 또는 그룹화 전 SELECT, UPDATE, DELETE
    ON 테이블 간의 조인 조건 지정 조인 시점 JOIN
    HAVING 그룹화된 데이터 필터링 그룹화 및 집계 후 GROUP BY 뒤

한계와 트레이드오프

  • 성능은 조건에 따라 달라진다: UNION ALL이 항상 빠르다고 단정할 수는 없다. 중복 제거 비용은 결과 집합 크기와 중복률에 비례하고, UNION ALL이 만든 중복을 애플리케이션에서 다시 제거하면 총비용이 오히려 커질 수 있다.
  • UNION의 중복 판정은 값·타입·콜레이션에 의존한다: 콜레이션이 대소문자나 후행 공백을 무시하면 서로 다른 문자열이 같은 행으로 취급되어 결과에서 하나만 남는다1.
  • MySQL은 FULL OUTER JOIN을 지원하지 않는다: LEFT/RIGHT JOIN 결과를 UNION으로 합쳐 에뮬레이션해야 하며, 이때 UNION ALL을 쓰면 중복 제거를 별도로 처리해야 한다2.
  • ORDER BY 위치 제약: 결합된 결과 전체에 대해서만 정렬할 수 있고, 개별 SELECT별 정렬은 지원되지 않는다1.
  • 실행 계획 확인 권장: 위 차이는 엔진 버전과 데이터 분포에 따라 달라지므로 특정 수치로 단정하지 말고, EXPLAIN으로 실제 처리 방식을 확인하는 편이 안전하다.

References

  1. MySQL 8.0 Reference Manual — UNION Clause (https://dev.mysql.com/doc/refman/8.0/en/set-operations.html) ↩ ↩2 ↩3 ↩4 ↩5

  2. MySQL 8.0 Reference Manual — JOIN Clause (https://dev.mysql.com/doc/refman/8.0/en/join.html) ↩

댓글남기기