MariaDB / Query / UNION, UNION ALL

MariaDB의 UNION은 여러 SELECT의 결과를 행 방향으로 합치고 같은 결과 행을 한 번만 남깁니다. UNION ALL은 중복을 포함한 모든 행을 유지합니다. 열을 옆으로 붙이는 JOIN과 다르며, 중복 판정은 특정 키 하나가 아니라 SELECT에 포함한 모든 열의 조합을 기준으로 합니다.

두 결과를 합치는 전체 실습

실습용 learning 데이터베이스에서 다음 준비 SQL을 한 번 실행합니다. CREATE·INSERT·SELECT 권한이 필요하며 같은 이름의 테이블이 이미 있다면 다른 실습 이름을 사용합니다. 중복 행과 같은 id의 다른 내용을 함께 넣어 차이를 비교합니다.

USE learning;
CREATE TABLE union_first_demo (
    id INT NOT NULL, label VARCHAR(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin
);
CREATE TABLE union_second_demo LIKE union_first_demo;
INSERT INTO union_first_demo VALUES (1,'A'), (2,'B'), (2,'B'), (3,'C');
INSERT INTO union_second_demo VALUES (1,'A'), (2,'X'), (4,'D');
SELECT id, label FROM union_first_demo
UNION
SELECT id, label FROM union_second_demo
ORDER BY id, label;

SELECT id, label FROM union_first_demo
UNION ALL
SELECT id, label FROM union_second_demo
ORDER BY id, label;

UNION 결과는 (1,A), (2,B), (2,X), (3,C), (4,D)의 5행입니다. UNION ALL은 (1,A)가 두 번, (2,B)가 두 번 나오고 나머지 세 행까지 총 7행입니다. 같은 SELECT 내부의 중복도 UNION의 최종 중복 제거 대상에 포함됩니다. UNION DISTINCT라고 명시해도 UNION과 같은 뜻입니다.

UNION 공식 문서의 규칙에 따라 반환 열 이름은 첫 SELECT에서 정합니다. ORDER BY가 없으면 위에서 아래로 입력한 순서나 중복 제거 과정의 정렬 순서를 보장하지 않으므로, 독자에게 보여줄 순서는 명시해야 합니다.

선택한 열이 바뀌면 중복도 달라집니다

SELECT id FROM union_first_demo
UNION
SELECT id FROM union_second_demo
ORDER BY id;

SELECT 'first' AS origin, id, label FROM union_first_demo
UNION
SELECT 'second', id, label FROM union_second_demo
ORDER BY origin, id, label;

id만 선택하면 결과는 1·2·3·4의 4행입니다. 원본의 (2,B)와 (2,X)에서 label을 조회하지 않기 때문에 같은 id=2로 합쳐집니다. 반대로 출처 origin을 추가한 둘째 쿼리는 first의 세 행과 second의 세 행, 총 6행입니다. (1,A)도 출처가 다르므로 별개 행이며, first 안의 (2,B) 중복만 합쳐집니다.

UNION은 원본 테이블에서 중복을 삭제하거나 기본 키 충돌을 해결하지 않습니다. 서로 다른 사람이나 주문이 우연히 같은 표시 값을 가질 수 있으므로 이름·금액만 조회해 중복 제거하면 필요한 행이 사라질 수 있습니다. 단순히 건수와 금액을 모두 보존하려는 보고서라면 UNION ALL이 맞는지 먼저 검토합니다.

열 개수·대응 위치·타입 확인

각 SELECT의 열 개수는 같아야 하고, 이름이 아니라 순서대로 대응됩니다. 첫 열이 상품 코드라면 다른 SELECT의 첫 열도 같은 의미의 값이어야 합니다. 열 개수가 같다는 이유만으로 이름과 금액을 섞어도 되는 것은 아닙니다. SELECT *는 스키마 변경 뒤 대응이 달라질 수 있으므로 열을 직접 적습니다.

SELECT CAST(1 AS CHAR(10)) AS display_code, 'first' AS origin
UNION ALL
SELECT '0042', 'second'
ORDER BY origin;

SELECT NULL AS value_text
UNION
SELECT NULL;

SELECT COUNT(*) AS combined_rows
FROM (
    SELECT id, label FROM union_first_demo
    UNION ALL
    SELECT id, label FROM union_second_demo
) AS combined;

첫 조회는 코드 1·0042를 문자로 합칩니다. 숫자와 문자를 섞으면 MariaDB가 결과 타입과 길이를 조정할 수 있지만, 코드의 선행 0·날짜·금액 등 의미가 중요할 때는 CAST와 같은 변환으로 의도를 명시합니다. 문자열 비교와 중복 판정은 문자셋·콜레이션의 영향도 받습니다. 둘째 쿼리의 NULL은 중복 제거에서 하나의 결과 행으로 합쳐지며, 셋째 쿼리는 합친 결과를 파생 테이블로 감싸 전체 행 수 7을 구합니다.

전체 정렬·제한과 개별 SELECT 제한

SELECT id AS item_id, label AS item_label FROM union_first_demo
UNION ALL
SELECT id, label FROM union_second_demo
ORDER BY item_id DESC, item_label DESC
LIMIT 3;

(SELECT id, label FROM union_first_demo
 ORDER BY id DESC, label DESC LIMIT 1)
UNION ALL
(SELECT id, label FROM union_second_demo
 ORDER BY id DESC, label DESC LIMIT 1)
ORDER BY id DESC, label DESC;

첫 결과는 (4,D), (3,C), (2,X)입니다. 끝의 ORDER BY와 LIMIT은 전체 합친 결과에 적용하고 첫 SELECT에서 만든 별칭을 사용합니다. 둘째는 괄호 안의 각 조회에서 한 행씩 고른 뒤 합쳐 (4,D), (3,C)를 반환합니다. 각 부분 조회의 정렬은 어떤 행을 LIMIT으로 고를지 정하며 최종 출력 순서는 마지막 ORDER BY로 지정합니다. 같은 정렬 값이 있는 행의 구별이 중요하다면 실제로 유일한 키까지 정렬 기준에 추가하세요.

ORDER BY의 다중 열 정렬을 적용할 때 ASC·DESC는 열마다 지정합니다. UNION ALL은 중복 제거 작업을 생략할 수 있지만, 정렬·조회량·인덱스·실행 계획에 따라 비용이 달라지므로 “항상 몇 배 빠르다”라고 단정할 수 없습니다. 중복 제거가 필요한지 먼저 결정한 다음 실제 쿼리의 실행 계획과 측정 결과로 조정합니다.