SQL UNION
Bạn Sẽ Học Được Gì
Sau khi đọc trang này, bạn sẽ có thể:
- Giải thích phép toán tập hợp (Set Operations) là gì và cách chúng kết hợp các tập kết quả trong SQL
- Viết các truy vấn UNION, UNION ALL, INTERSECT, và EXCEPT
- Hiểu sự khác biệt giữa UNION và UNION ALL và khi nào nên dùng mỗi loại
- Áp dụng các quy tắc tương thích cột (Column Compatibility Rules) chi phối phép toán tập hợp
- Tránh các cạm bẫy về hiệu suất và tính đúng đắn thường gặp
- Chọn phép toán tập hợp phù hợp cho từng bài toán
Vấn Đề: Kết Hợp Kết Quả Từ Nhiều Truy Vấn
Đôi khi bạn cần dữ liệu trải dài trên nhiều bảng hoặc nhiều điều kiện không thể diễn đạt trong một câu lệnh SELECT duy nhất. Các phép toán tập hợp (Set Operations) cho phép bạn kết hợp kết quả của hai hay nhiều truy vấn thành một tập kết quả duy nhất.
customers table: suppliers table:
+----+--------+-------+ +----+--------+-------+
| id | name | city | | id | name | city |
+----+--------+-------+ +----+--------+-------+
| 1 | Alice | Hanoi | | 1 | Gamma | HCMC |
| 2 | Bob | HCMC | | 2 | Delta | Hanoi |
| 3 | Carol | Hanoi | | 3 | Epslon| Da Nang|
+----+--------+-------+ +----+--------+-------+
Những câu hỏi không thể trả lời bằng một truy vấn duy nhất:
- "Cho tôi tất cả liên hệ — cả khách hàng và nhà cung cấp" — cần kết hợp hai bảng
- "Thành phố nào có cả khách hàng và nhà cung cấp?" — cần tìm phần giao nhau
- "Thành phố nào có khách hàng nhưng không có nhà cung cấp?" — cần tìm phần hiệu
Phép toán tập hợp (Set Operations) giải quyết các vấn đề này bằng cách coi kết quả truy vấn như các tập hợp toán học (Mathematical Sets).
Bảng Dữ Liệu Mẫu
Tất cả ví dụ sử dụng hai bảng sau:
CREATE TABLE customers (
id INT PRIMARY KEY,
name VARCHAR(50),
city VARCHAR(50)
);
INSERT INTO customers VALUES
(1, 'Alice', 'Hanoi'),
(2, 'Bob', 'HCMC'),
(3, 'Carol', 'Hanoi');
CREATE TABLE suppliers (
id INT PRIMARY KEY,
name VARCHAR(50),
city VARCHAR(50)
);
INSERT INTO suppliers VALUES
(1, 'Gamma', 'HCMC'),
(2, 'Delta', 'Hanoi'),
(3, 'Epsilon', 'Da Nang');
Điểm đáng chú ý:
- Hanoi xuất hiện ở cả hai bảng (Alice, Carol là khách hàng; Delta là nhà cung cấp)
- HCMC xuất hiện ở cả hai bảng (Bob là khách hàng; Gamma là nhà cung cấp)
- Da Nang chỉ xuất hiện trong bảng suppliers
- Cột
namekhông có giá trị trùng lặp giữa hai bảng — hữu ích cho việc minh họa loại bỏ trùng lặp (Deduplication)
Tổng Quan Phép Toán Tập Hợp
Quy Tắc Cho Phép Toán Tập Hợp
Trước khi đi vào từng phép toán, tất cả các phép toán tập hợp đều tuân theo các quy tắc sau:
- Cùng số lượng cột — cả hai truy vấn phải trả về cùng số cột
- Kiểu dữ liệu tương thích (Compatible Data Types) — các cột tương ứng phải có kiểu tương thích
- Tên cột — kết quả sử dụng tên cột từ truy vấn đầu tiên
- ORDER BY — chỉ xuất hiện một lần, ở cuối cùng, và áp dụng cho toàn bộ kết quả
-- Hợp lệ: cùng số cột, kiểu tương thích
SELECT name, city FROM customers
UNION
SELECT name, city FROM suppliers;
-- Không hợp lệ: số cột khác nhau
SELECT name, city, email FROM customers
UNION
SELECT name, city FROM suppliers; -- ERROR: each UNION query must have the same number of columns
UNION
Trả về tất cả các dòng duy nhất (distinct rows) từ cả hai truy vấn. Các dòng trùng lặp (Duplicate Rows) bị loại bỏ.
SELECT name, city FROM customers
UNION
SELECT name, city FROM suppliers;
Kết quả:
| name | city |
|---|---|
| Alice | Hanoi |
| Bob | HCMC |
| Carol | Hanoi |
| Delta | Hanoi |
| Epsilon | Da Nang |
| Gamma | HCMC |
- 6 dòng được trả về — tất cả các tổ hợp duy nhất
- Vì tất cả tên đều duy nhất giữa hai bảng, không có dòng trùng lặp nào bị loại bỏ
Khi tồn tại dòng trùng lặp
-- Lấy tất cả thành phố từ cả hai bảng
SELECT city FROM customers
UNION
SELECT city FROM suppliers;
Kết quả:
| city |
|---|
| Hanoi |
| HCMC |
| Da Nang |
- 3 dòng được trả về —
HanoivàHCMCxuất hiện ở cả hai bảng nhưng UNION chỉ giữ lại một bản sao - Nếu không dùng UNION (mà dùng UNION ALL), bạn sẽ nhận được 6 dòng
UNION ALL
Trả về tất cả các dòng từ cả hai truy vấn, bao gồm cả trùng lặp. Không có bước loại bỏ trùng lặp (Deduplication).
SELECT city FROM customers
UNION ALL
SELECT city FROM suppliers;
Kết quả:
| city |
|---|
| Hanoi |
| HCMC |
| Hanoi |
| HCMC |
| Hanoi |
| Da Nang |
- 6 dòng được trả về — mọi dòng từ cả hai truy vấn, không loại bỏ trùng lặp
Luôn ưu tiên UNION ALL khi bạn biết trùng lặp là không thể hoặc không đáng kể. UNION loại bỏ trùng lặp bằng cách sắp xếp/băm (Sort/Hash) tập kết quả, gây tốn chi phí. UNION ALL chỉ đơn thuần nối kết quả — nhanh hơn nhiều.
UNION vs UNION ALL
| UNION | UNION ALL | |
|---|---|---|
| Trùng lặp (Duplicates) | Loại bỏ | Giữ nguyên |
| Hiệu suất (Performance) | Chậm hơn (bước loại bỏ trùng lặp) | Nhanh hơn (không loại bỏ trùng lặp) |
| Sử dụng khi | Bạn chỉ cần các dòng duy nhất | Trùng lặp không quan trọng hoặc không thể xảy ra |
| Tương đương với | SELECT DISTINCT ... trên toàn bộ kết quả | Nối (Concatenation) các kết quả |
INTERSECT
Chỉ trả về các dòng xuất hiện ở cả hai kết quả truy vấn.
-- Các thành phố có CẢ khách hàng VÀ nhà cung cấp
SELECT city FROM customers
INTERSECT
SELECT city FROM suppliers;
Kết quả:
| city |
|---|
| Hanoi |
| HCMC |
- Chỉ các thành phố có trong cả hai bảng mới được trả về
Da Nangbị loại trừ vì không có khách hàng nào ở đó
MySQL không hỗ trợ INTERSECT trực tiếp. Sử dụng INNER JOIN hoặc WHERE ... IN thay thế:
-- Cách thay thế INTERSECT trong MySQL
SELECT DISTINCT c.city
FROM customers c
INNER JOIN suppliers s ON c.city = s.city;
EXCEPT
Trả về các dòng từ truy vấn đầu tiên mà không xuất hiện trong truy vấn thứ hai. Còn được gọi là MINUS trong Oracle.
-- Các thành phố có khách hàng nhưng KHÔNG có nhà cung cấp
SELECT city FROM customers
EXCEPT
SELECT city FROM suppliers;
Kết quả:
| city |
|---|
| (trống — tất cả thành phố có khách hàng đều có nhà cung cấp) |
Đảo ngược thứ tự:
-- Các thành phố có nhà cung cấp nhưng KHÔNG có khách hàng
SELECT city FROM suppliers
EXCEPT
SELECT city FROM customers;
Kết quả:
| city |
|---|
| Da Nang |
Da Nangcó nhà cung cấp nhưng không có khách hàng — chỉ nó vượt qua EXCEPT
MySQL không hỗ trợ EXCEPT trực tiếp. Sử dụng LEFT JOIN ... WHERE IS NULL hoặc NOT EXISTS:
-- Cách thay thế EXCEPT trong MySQL
SELECT DISTINCT s.city
FROM suppliers s
LEFT JOIN customers c ON s.city = c.city
WHERE c.city IS NULL;
Trong Oracle, EXCEPT được viết là MINUS. Chúng có chức năng giống hệt nhau.
-- Cú pháp Oracle
SELECT city FROM customers
MINUS
SELECT city FROM suppliers;
So Sánh Các Phép Toán Tập Hợp
| UNION | UNION ALL | INTERSECT | EXCEPT | |
|---|---|---|---|---|
| Trả về | Tất cả dòng duy nhất | Tất cả dòng | Dòng có ở cả hai | Dòng chỉ có ở truy vấn đầu tiên |
| Trùng lặp (Duplicates) | Loại bỏ | Giữ nguyên | Loại bỏ | Loại bỏ |
| Hiệu suất (Performance) | Trung bình | Nhanh nhất | Chậm nhất | Chậm |
| Hỗ trợ MySQL | Có | Có | Không (dùng JOIN) | Không (dùng JOIN) |
| Tương đương toán học | A ∪ B | A ∪ B (kèm trùng lặp) | A ∩ B | A − B |
Ví Dụ Thực Tế
Kết hợp dữ liệu từ các bảng tương tự
Trường hợp sử dụng phổ biến là kết hợp các dòng từ các bảng có cấu trúc tương tự (Similar Structure):
-- Tất cả liên hệ (khách hàng + nhà cung cấp) với nhãn nguồn (Source Label)
SELECT name, city, 'Customer' AS source
FROM customers
UNION ALL
SELECT name, city, 'Supplier' AS source
FROM suppliers;
Kết quả:
| name | city | source |
|---|---|---|
| Alice | Hanoi | Customer |
| Bob | HCMC | Customer |
| Carol | Hanoi | Customer |
| Gamma | HCMC | Supplier |
| Delta | Hanoi | Supplier |
| Epsilon | Da Nang | Supplier |
Gộp báo cáo qua các thời kỳ
-- Doanh số từ Q1 và Q2, kết hợp lại
SELECT product_id, amount, 'Q1' AS quarter FROM sales_q1
UNION ALL
SELECT product_id, amount, 'Q2' AS quarter FROM sales_q2
ORDER BY product_id, quarter;
Tìm bản ghi mồ côi (Orphaned Records)
-- Sản phẩm có trong danh mục nhưng chưa từng được đặt hàng
SELECT product_id FROM products
EXCEPT
SELECT DISTINCT product_id FROM order_items;
Tìm bản ghi hoàn chỉnh vs chưa hoàn chỉnh
-- Người dùng vừa có hồ sơ VÀ có đăng ký hoạt động (Active Subscription)
SELECT user_id FROM profiles
INTERSECT
SELECT user_id FROM subscriptions WHERE status = 'active';
Mẹo Tối Ưu Hiệu Suất
1. Ưu tiên UNION ALL thay vì UNION
UNION thực hiện thao tác DISTINCT ngầm — nó phải sắp xếp hoặc băm (Sort/Hash) toàn bộ tập kết quả để loại bỏ trùng lặp. UNION ALL chỉ đơn thuần nối (Append) kết quả.
-- Chậm: loại bỏ trùng lặp ngầm (Implicit Dedup)
SELECT city FROM customers
UNION
SELECT city FROM suppliers;
-- Nhanh: không loại bỏ trùng lặp (dùng khi biết không có trùng lặp)
SELECT city FROM customers
UNION ALL
SELECT city FROM suppliers;