Chuyển tới nội dung chính

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 name khô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:

  1. Cùng số lượng cột — cả hai truy vấn phải trả về cùng số cột
  2. 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
  3. Tên cột — kết quả sử dụng tên cột từ truy vấn đầu tiên
  4. 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ả:

namecity
AliceHanoi
BobHCMC
CarolHanoi
DeltaHanoi
EpsilonDa Nang
GammaHCMC
  • 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ề — HanoiHCMC xuấ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
mẹo

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

UNIONUNION 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 khiBạn chỉ cần các dòng duy nhấtTrùng lặp không quan trọng hoặc không thể xảy ra
Tương đương vớiSELECT 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 Nang bị loại trừ vì không có khách hàng nào ở đó
cảnh bá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ênkhô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 Nang có nhà cung cấp nhưng không có khách hàng — chỉ nó vượt qua EXCEPT
cảnh báo

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;
mẹo

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

UNIONUNION ALLINTERSECTEXCEPT
Trả vềTất cả dòng duy nhấtTất cả dòngDòng có ở cả haiDòng chỉ có ở truy vấn đầu tiên
Trùng lặp (Duplicates)Loại bỏGiữ nguyênLoại bỏLoại bỏ
Hiệu suất (Performance)Trung bìnhNhanh nhấtChậm nhấtChậm
Hỗ trợ MySQLKhông (dùng JOIN)Không (dùng JOIN)
Tương đương toán họcA ∪ BA ∪ B (kèm trùng lặp)A ∩ BA − 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ả:

namecitysource
AliceHanoiCustomer
BobHCMCCustomer
CarolHanoiCustomer
GammaHCMCSupplier
DeltaHanoiSupplier
EpsilonDa NangSupplier

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;

2. Lọc trước khi kết hợp

Áp dụng mệnh đề WHERE trong từng truy vấn riêng lẻ thay vì lọc sau phép toán tập hợp:

-- Tốt: lọc trong từng truy vấn, ít dữ liệu cần kết hợp
SELECT name, city FROM customers WHERE city = 'Hanoi'
UNION ALL
SELECT name, city FROM suppliers WHERE city = 'Hanoi';

-- Kém hiệu quả: kết hợp tất cả, rồi mới lọc
SELECT * FROM (
SELECT name, city FROM customers
UNION ALL
SELECT name, city FROM suppliers
) combined
WHERE city = 'Hanoi';

3. Đánh chỉ mục (Index) các cột dùng trong phép toán tập hợp

Nếu bạn thường xuyên chạy INTERSECT hoặc EXCEPT trên một cột, hãy đảm bảo cột đó đã được đánh chỉ mục (Indexed):

CREATE INDEX idx_customers_city ON customers(city);
CREATE INDEX idx_suppliers_city ON suppliers(city);

4. Giới hạn sớm (Limit Early)

Sử dụng LIMIT / TOP trong các truy vấn con (Subqueries) khi bạn chỉ cần một mẫu dữ liệu:

-- Chỉ cần 10 mục gần nhất từ mỗi nguồn
(SELECT id, name, created_at FROM customers ORDER BY created_at DESC LIMIT 10)
UNION ALL
(SELECT id, name, created_at FROM suppliers ORDER BY created_at DESC LIMIT 10)
ORDER BY created_at DESC;
mẹo

Khi kết hợp ORDER BY hoặc LIMIT với phép toán tập hợp, hãy bọc từng truy vấn riêng lẻ trong ngoặc đơn (Parentheses). ORDER BY cuối cùng áp dụng cho toàn bộ kết quả đã kết hợp.

Cạm Bẫy Thường Gặp

  • Nhầm lẫn UNION và UNION ALL — UNION loại bỏ trùng lặp (chậm hơn), UNION ALL giữ nguyên (nhanh hơn). Hãy chọn có chủ đích.
  • Số cột không khớp (Column Mismatch) — lỗi runtime phổ biến nhất. Cả hai truy vấn phải trả về cùng số cột với kiểu tương thích.
  • Tên cột gây hiểu lầm — kết quả sử dụng tên cột từ truy vấn đầu tiên. Nếu truy vấn thứ hai có tên cột khác, bạn có thể nhận được tiêu đề gây nhầm lẫn. Hãy dùng bí danh (Alias) trong truy vấn đầu tiên.
  • Vị trí ORDER BYORDER BY chỉ xuất hiện một lần, ở cuối toàn bộ câu lệnh. Đặt nó trong từng truy vấn riêng lẻ (không có ngoặc đơn) sẽ gây lỗi.
  • So sánh NULL trong INTERSECT/EXCEPT — phép toán tập hợp coi hai NULL là bằng nhau, khác với so sánh SQL thông thường nơi NULL = NULL trả về UNKNOWN.
  • Cho rằng kết quả có thứ tự — phép toán tập hợp không đảm bảo thứ tự dòng. Luôn sử dụng ORDER BY nếu thứ tự quan trọng.

Best Practices

  1. Dùng UNION ALL làm mặc định — chỉ chuyển sang UNION khi bạn thực sự cần loại bỏ trùng lặp
  2. Thêm cột nguồn (Source Column) — khi kết hợp các bảng tương tự, thêm cột nhãn để theo dõi nguồn gốc của mỗi dòng
  3. Bọc các truy vấn riêng lẻ trong ngoặc đơn — khi sử dụng ORDER BY hoặc LIMIT cho từng truy vấn
  4. Kiểm tra sự tương thích cột — kiểm tra lại các cột tương ứng có kiểu dữ liệu và ngữ nghĩa tương thích
  5. Sử dụng INTERSECT/EXCEPT để xác thực dữ liệu (Data Validation) — rất phù hợp để tìm sự giao nhau và khác biệt giữa các tập dữ liệu
  6. Cân nhắc các phương án thay thế — đôi khi một truy vấn duy nhất với OR, IN, hoặc JOIN đơn giản và nhanh hơn phép toán tập hợp
-- Đôi khi đơn giản hơn UNION
SELECT * FROM contacts
WHERE type = 'Customer' OR type = 'Supplier';

-- Đôi khi đơn giản hơn INTERSECT
SELECT DISTINCT c.city
FROM customers c
INNER JOIN suppliers s ON c.city = s.city;

Câu Hỏi Phỏng Vấn

1. Sự khác biệt giữa UNION và UNION ALL là gì?

UNIONUNION ALL
Trùng lặp (Duplicates)Loại bỏGiữ nguyên
Hiệu suất (Performance)Chậm hơn (cần loại bỏ trùng lặp)Nhanh hơn (không loại bỏ trùng lặp)
Khi nào dùngCần các dòng duy nhấtTrùng lặp không quan trọng

UNION thực hiện thêm một bước để loại bỏ các dòng trùng lặp (giống như bọc kết quả trong DISTINCT). UNION ALL chỉ đơn thuần nối (Concatenate) các kết quả. Nếu bạn biết trùng lặp là không thể hoặc chấp nhận được, hãy luôn dùng UNION ALL để có hiệu suất tốt hơn.

2. Các quy tắc cho phép toán tập hợp SQL là gì?

  1. Cả hai truy vấn phải trả về cùng số lượng cột
  2. Các cột tương ứng phải có kiểu dữ liệu tương thích (Compatible Data Types)
  3. Kết quả sử dụng tên cột từ truy vấn đầu tiên
  4. ORDER BY chỉ xuất hiện một lần, ở cuối toàn bộ câu lệnh
  5. NULL được coi là bằng nhau cho mục đích loại bỏ trùng lặp (Deduplication)

3. Làm thế nào để tìm các dòng có trong một bảng nhưng không tồn tại ở bảng khác?

Sử dụng EXCEPT (hoặc MINUS trong Oracle):

SELECT product_id FROM products
EXCEPT
SELECT DISTINCT product_id FROM order_items;

Trong MySQL, sử dụng cách thay thế (Workaround):

SELECT p.product_id
FROM products p
LEFT JOIN order_items oi ON p.product_id = oi.product_id
WHERE oi.product_id IS NULL;

4. Khi nào nên dùng INTERSECT thay vì INNER JOIN?

Cả hai đều tìm dữ liệu giao nhau, nhưng hoạt động ở các cấp độ khác nhau:

INTERSECTINNER JOIN
So sánhToàn bộ dòng (Entire Rows)Các cột cụ thể
Trả vềCác dòng duy nhất từ cả hai truy vấnCác cột kết hợp từ cả hai bảng
Trùng lặpTự động loại bỏCó thể tạo trùng lặp (quan hệ một-nhiều / One-to-Many)
Dùng khiSo sánh giá trị toàn bộ dòngCần cột từ cả hai bảng
-- INTERSECT: thành phố có trong cả hai bảng (chỉ giá trị city)
SELECT city FROM customers
INTERSECT
SELECT city FROM suppliers;

-- INNER JOIN: cùng câu hỏi, nhưng có thể thêm nhiều cột hơn
SELECT DISTINCT c.city, c.name AS customer_name, s.name AS supplier_name
FROM customers c
INNER JOIN suppliers s ON c.city = s.city;

5. Có thể dùng ORDER BY với UNION không? Cách dùng?

Có, nhưng chỉ một lần — ở cuối cùng của toàn bộ câu lệnh:

SELECT name, city FROM customers
UNION ALL
SELECT name, city FROM suppliers
ORDER BY city, name; -- áp dụng cho kết quả đã kết hợp

Để sắp xếp từng truy vấn riêng lẻ trước khi kết hợp, hãy bọc chúng trong ngoặc đơn:

(SELECT name, city FROM customers ORDER BY name)
UNION ALL
(SELECT name, city FROM suppliers ORDER BY name)
ORDER BY city; -- sắp xếp cuối cùng của kết quả đã kết hợp

6. Làm thế nào để thêm cột "source" (nguồn) để phân biệt các dòng trong UNION?

Thêm một chuỗi cố định (Literal String) trong mỗi truy vấn:

SELECT name, city, 'Customer' AS source FROM customers
UNION ALL
SELECT name, city, 'Supplier' AS source FROM suppliers;

Đây là một mẫu (Pattern) phổ biến khi gộp dữ liệu từ các bảng tương tự và bạn cần theo dõi mỗi dòng đến từ bảng nào.

Tìm Hiểu Thêm

  • SQL Joins — kết hợp cột từ nhiều bảng
  • SQL Aggregation — GROUP BY, HAVING, và các hàm tổng hợp (Aggregate Functions)
  • Database Indexing — cách chỉ mục (Index) tăng tốc hiệu suất truy vấn