MariaDB 강좌 / UNION·UNION ALL로 조회 결과 합치기
UNION은 열 구조가 맞는 여러 조회 결과를 세로로 합치면서 동일한 결과 행을 제거하고, UNION ALL은 중복을 유지합니다. 열 개수·위치·타입을 맞추고 최종 ORDER BY로 전체 순서를 정해야 두 목록의 의미를 혼동하지 않습니다.
JOIN의 가로 연결과 UNION의 세로 합치기 구분하기
고객 이름 옆에 주문 정보를 붙이는 작업은 JOIN이고, 서울 고객 목록과 결제 고객 목록을 하나의 대상 목록으로 모으는 작업은 UNION입니다. 합칠 결과에서 같은 열 위치가 같은 의미를 가져야 합니다. 첫 열은 고객 번호인데 다음 조회의 첫 열은 주문 번호인 결과를 합치면 타입이 같아도 의미가 어긋납니다. 중복 판단은 SELECT로 열 선택하기: 별칭·계산식·DISTINCT의 DISTINCT처럼 선택 열 전체를 기준으로 하며, INNER JOIN으로 서로 연결된 데이터 조회하기의 관계 키에 따른 JOIN과는 결과를 만드는 방향이 다릅니다.
| 기능 | 중복 처리 | 주요 용도 |
|---|---|---|
| UNION | 전체 선택 열이 같은 행 제거 | 겹치는 대상 목록 통합 |
| UNION ALL | 중복 유지 | 각 출처의 발생 내역 보존 |
| JOIN | 조건으로 열·행 조합 | 서로 관련된 정보 연결 |
겹치는 대상 고객을 가진 데이터 준비하기
학습용 MariaDB 서버의 shopcourse에 course_user로 접속합니다. 명령줄 클라이언트와 UTF-8 SQL 파일 실행 방법대로 코드를 UTF-8 파일에 저장하고 SOURCE C:/mariadb-lab/lesson.sql;처럼 실제 파일 경로로 실행할 수 있습니다. SQL 문은 운영체제 터미널이 아닌 MariaDB 연결 안에서 실행합니다.
먼저 SELECT VERSION(), DATABASE();와 SHOW TABLES;로 대상 서버·DB·이름 중복을 확인합니다. 아래 c30_customers, c30_products, c30_orders, c30_items는 이 글의 독립된 연습용 이름입니다. 이미 존재하면 정의와 행을 확인하고, 보관할 자료가 없는 이 글의 실습 테이블임이 확실할 때만 마지막 절의 정리 순서로 지운 뒤 다시 만듭니다. 기존 강좌의 동명 없는 테이블이나 운영 데이터는 삭제하지 않습니다.
USE shopcourse; CREATE TABLE c30_customers ( customer_id INT NOT NULL PRIMARY KEY, customer_name VARCHAR(50) NOT NULL, city VARCHAR(30) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE c30_products ( product_id INT NOT NULL PRIMARY KEY, product_name VARCHAR(100) NOT NULL, category VARCHAR(30) NOT NULL, price DECIMAL(10,2) NOT NULL CHECK (price >= 0), stock INT NOT NULL CHECK (stock >= 0) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE c30_orders ( order_id INT NOT NULL PRIMARY KEY, customer_id INT NOT NULL, ordered_at DATETIME NOT NULL, status VARCHAR(20) NOT NULL, shipping_fee DECIMAL(10,2) NOT NULL DEFAULT 1000, CONSTRAINT fk_c30_order_customer FOREIGN KEY (customer_id) REFERENCES c30_customers(customer_id) ON DELETE RESTRICT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE c30_items ( order_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL CHECK (quantity > 0), unit_price DECIMAL(10,2) NOT NULL CHECK (unit_price >= 0), PRIMARY KEY (order_id, product_id), CONSTRAINT fk_c30_item_order FOREIGN KEY (order_id) REFERENCES c30_orders(order_id) ON DELETE RESTRICT, CONSTRAINT fk_c30_item_product FOREIGN KEY (product_id) REFERENCES c30_products(product_id) ON DELETE RESTRICT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; INSERT INTO c30_customers VALUES (1,'김민수','서울'),(2,'이서연','부산'), (3,'박지우','서울'),(4,'최하늘','대전'); INSERT INTO c30_products VALUES (1,'노트','문구',3500,20),(2,'볼펜','문구',1500,50), (3,'머그컵','생활',8000,12),(4,'스탠드','생활',25000,5); INSERT INTO c30_orders (order_id,customer_id,ordered_at,status,shipping_fee) VALUES (101,1,'2026-01-10 10:00:00','paid',1000), (102,1,'2026-01-20 11:00:00','paid',1000), (103,2,'2026-02-05 12:00:00','paid',1000), (104,3,'2026-02-10 13:00:00','canceled',1000); INSERT INTO c30_items VALUES (101,1,2,3500),(101,2,3,1500),(102,3,1,8000), (103,1,1,3500),(103,3,2,8000),(104,2,1,1500);
서울 고객은 1·3번, paid 주문이 있는 고객은 1·2번입니다. 고객 1은 두 목록에 동시에 속합니다. 안쪽 결제 고객 목록에는 EXISTS를 사용해 주문 두 건인 고객 1도 한 번만 나오게 했습니다.
UNION과 UNION ALL의 행 수 비교하기
SELECT customer_id FROM c30_customers WHERE city='서울' UNION SELECT c.customer_id FROM c30_customers c WHERE EXISTS (SELECT 1 FROM c30_orders o WHERE o.customer_id=c.customer_id AND o.status='paid') ORDER BY customer_id; SELECT customer_id FROM c30_customers WHERE city='서울' UNION ALL SELECT c.customer_id FROM c30_customers c WHERE EXISTS (SELECT 1 FROM c30_orders o WHERE o.customer_id=c.customer_id AND o.status='paid') ORDER BY customer_id;
첫 결과는 1·2·3의 세 행입니다. 둘째 결과는 1·1·2·3의 네 행으로 고객 1의 두 출처가 유지됩니다. UNION ALL은 중복 검사를 생략할 수 있지만, 필요한 중복 제거를 빠르게 보이기 위해 없애면 잘못된 대상 목록이 됩니다. 기능 선택은 결과 의미를 우선합니다. UNION과 UNION ALL 공식 문법을 확인하세요.
출처 열을 추가하면 중복 기준도 달라지기
SELECT customer_id,'서울 고객' AS source_label FROM c30_customers WHERE city='서울' UNION SELECT c.customer_id,'결제 고객' FROM c30_customers c WHERE EXISTS (SELECT 1 FROM c30_orders o WHERE o.customer_id=c.customer_id AND o.status='paid') ORDER BY customer_id,source_label;
고객 1은 서울 고객과 결제 고객이라는 서로 다른 source_label을 가지므로 UNION이어도 두 행이 남습니다. 중복은 고객 번호 하나가 아니라 선택한 열 조합 전체를 기준으로 제거됩니다. 고객당 한 행이 필요하면서 여러 출처를 함께 보여 줘야 한다면 집계 등 별도의 출력 설계를 해야 합니다.
열 수·타입·정렬 위치 맞추기
각 SELECT는 같은 개수의 열을 반환해야 하며 위치별로 호환 가능한 타입이어야 합니다. 결과 열 이름은 기본적으로 첫 조회의 선택 목록을 따르므로 이후 쿼리에서 붙인 별칭이 최종 이름이 된다고 가정하지 않습니다. 숫자와 문자열을 섞어 생기는 타입 변환도 확인하세요.
전체 목록의 순서는 마지막 ORDER BY로 정합니다. 각 분기 안의 ORDER BY만으로 최종 순서가 보장되지 않습니다. 분기별 상위 몇 행을 뽑아야 한다면 괄호와 LIMIT의 범위를 명확히 하고 전체 정렬과 구분합니다. 출처별 건수를 확인할 때는 합친 뒤의 중복 제거가 원래 행 수를 바꾸었는지도 살펴봅니다.
합치기 전 두 결과를 확인하고 정리하기
각 조회를 단독으로 실행해 열 의미와 행 수를 확인한 뒤 UNION ALL 결과부터 대조하면 겹치는 행의 출처를 이해하기 쉽습니다. 그다음 중복 제거가 필요한지 판단합니다. UNION은 원본 테이블의 행을 삭제하거나 두 테이블을 영구적으로 하나로 만드는 명령이 아닙니다.
반복하기 전 현재 DB와 실제 정의·행을 확인하고 필요한 자료를 백업합니다. 보관할 데이터가 없는 이 글의 전용 실습 객체만 아래 순서로 정리합니다. 외래키가 있으면 자식부터 지웁니다. DROP TABLE은 구조와 데이터를 모두 없애며 일반적인 ROLLBACK으로 복원되지 않으므로 취소 가능한 DML 실습과 분리합니다.
DROP TABLE shopcourse.c30_items; DROP TABLE shopcourse.c30_orders; DROP TABLE shopcourse.c30_products; DROP TABLE shopcourse.c30_customers;










