SQL cho phân tích dữ liệu · Hướng dẫn · 30 tháng 10, 2026 · 11 phút đọc
UNION và UNION ALL trong SQL khác nhau thế nào (kèm INTERSECT và EXCEPT)

UNION và UNION ALL đều xếp chồng kết quả của hai truy vấn lên nhau. Khác biệt duy nhất: UNION loại bỏ các dòng trùng khỏi kết quả gộp, UNION ALL giữ lại mọi dòng. Vì loại bỏ trùng lặp tốn thêm công, UNION ALL cũng nhanh hơn. Quy tắc ngón tay cái cho data analyst: mặc định dùng UNION ALL, và chỉ dùng UNION khi bạn thật sự muốn một danh sách các giá trị khác nhau. Hai toán tử liên quan hoàn thiện bộ này: INTERSECT trả về những dòng có trong cả hai truy vấn, còn EXCEPT trả về những dòng của truy vấn thứ nhất mà truy vấn thứ hai không có.
Mọi ví dụ đều chạy trên cơ sở dữ liệu hãng hàng không của khóa SQL trên Nomto, theo cú pháp PostgreSQL trên DuckDB (engine của phòng học SQL trên Nomto), và mọi bảng kết quả đều là kết quả chạy thật.
Một ví dụ nhỏ cho cả bốn toán tử
Bảng aircrafts có chín mẫu máy bay:
| model | range |
|---|---|
| Boeing 777-300 | 11100 |
| Boeing 767-300 | 7900 |
| Airbus A319-100 | 6700 |
| Airbus A320-200 | 5700 |
| Airbus A321-200 | 5600 |
| Boeing 737-300 | 4200 |
| Sukhoi Superjet-100 | 3000 |
| Bombardier CRJ-200 | 2700 |
| Cessna 208 Caravan | 1200 |
Lấy hai tập chồng lấn nhau: các mẫu tầm xa (tầm bay từ 5.000 km trở lên, năm mẫu) và các mẫu Boeing (ba mẫu). Hai mẫu, 777 và 767, thuộc cả hai tập.
UNION: tất cả, không trùng lặp
SELECT model FROM aircrafts WHERE range >= 5000
UNION
SELECT model FROM aircrafts WHERE model LIKE 'Boeing%'
ORDER BY model;
| model |
|---|
| Airbus A319-100 |
| Airbus A320-200 |
| Airbus A321-200 |
| Boeing 737-300 |
| Boeing 767-300 |
| Boeing 777-300 |
Năm cộng ba là tám, nhưng hai mẫu Boeing có mặt ở cả hai tập chỉ được giữ một lần: sáu dòng.
UNION ALL: tất cả, kể cả dòng trùng
SELECT model FROM aircrafts WHERE range >= 5000
UNION ALL
SELECT model FROM aircrafts WHERE model LIKE 'Boeing%'
ORDER BY model;
| model |
|---|
| Airbus A319-100 |
| Airbus A320-200 |
| Airbus A321-200 |
| Boeing 737-300 |
| Boeing 767-300 |
| Boeing 767-300 |
| Boeing 777-300 |
| Boeing 777-300 |
Tám dòng: UNION ALL chỉ đơn giản nối kết quả thứ hai vào sau kết quả thứ nhất.
INTERSECT: chỉ những dòng có ở cả hai
SELECT model FROM aircrafts WHERE range >= 5000
INTERSECT
SELECT model FROM aircrafts WHERE model LIKE 'Boeing%'
ORDER BY model;
| model |
|---|
| Boeing 767-300 |
| Boeing 777-300 |
EXCEPT: có ở truy vấn thứ nhất nhưng không có ở truy vấn thứ hai
SELECT model FROM aircrafts WHERE range >= 5000
EXCEPT
SELECT model FROM aircrafts WHERE model LIKE 'Boeing%'
ORDER BY model;
| model |
|---|
| Airbus A319-100 |
| Airbus A320-200 |
| Airbus A321-200 |
Khác với ba toán tử còn lại, EXCEPT phụ thuộc vào thứ tự. Đảo hai truy vấn và bạn nhận được các mẫu Boeing không phải tầm xa: chỉ có Boeing 737-300. Oracle gọi toán tử này là MINUS; PostgreSQL, DuckDB, SQL Server và MySQL 8.0.31 trở lên dùng EXCEPT.
UNION cũng loại bỏ trùng lặp bên trong từng truy vấn
Một hiểu lầm phổ biến là UNION chỉ loại những dòng xuất hiện ở cả hai nửa. Thực ra nó loại mọi dòng trùng trong kết quả cuối cùng, kể cả những dòng lặp lại trong cùng một nửa. Lấy các điểm đến từ Sheremetyevo (SVO) và từ Pulkovo ở St Petersburg (LED):
- SVO bay tới 48 sân bay khác nhau, LED tới 24, và 8 sân bay trong số đó được phục vụ từ cả hai nơi.
UNIONcủa hai danh sácharrival_airporttrả về 64 dòng: 48 + 24 − 8.UNION ALLcủa cùng hai truy vấn trả về 4.881 dòng, mỗi chuyến bay một dòng, vì mỗi điểm đến xuất hiện một lần cho mỗi chuyến khởi hành trong lịch bay.
SELECT arrival_airport FROM flights WHERE departure_airport = 'SVO'
INTERSECT
SELECT arrival_airport FROM flights WHERE departure_airport = 'LED'
ORDER BY 1;
| arrival_airport |
|---|
| CSY |
| ESL |
| GOJ |
| KVX |
| NOJ |
| OVS |
| TJM |
| VKT |
Vậy UNION hoạt động như UNION ALL rồi tiếp theo là SELECT DISTINCT. INTERSECT và EXCEPT mặc định cũng trả về các dòng không trùng.
Khi nào UNION ALL là câu trả lời đúng
Biến hai cột thành một trước khi đếm
Mỗi chuyến bay có một sân bay khởi hành và một sân bay đến. Để đếm mọi lượt cất hạ cánh (cất cánh cộng hạ cánh) theo từng sân bay, hãy xếp chồng hai cột thành một rồi nhóm:
SELECT airport, COUNT(*) AS movements
FROM (
SELECT departure_airport AS airport FROM flights
UNION ALL
SELECT arrival_airport FROM flights
) m
GROUP BY airport
ORDER BY movements DESC
LIMIT 5;
| airport | movements |
|---|---|
| DME | 6434 |
| SVO | 5963 |
| LED | 3802 |
| VKO | 3436 |
| OVB | 2110 |
Bây giờ thay UNION ALL bằng UNION và chạy lại (với LIMIT 3):
| airport | movements |
|---|---|
| LED | 1 |
| VOZ | 1 |
| RTW | 1 |
Giờ đây mỗi sân bay chỉ có đúng một “lượt”. UNION đã gộp hàng nghìn dòng giống hệt nhau thành một dòng cho mỗi sân bay trước khi COUNT kịp nhìn thấy chúng. (Vì mọi giá trị đều bằng 1, sân bay nào đứng đầu là ngẫu nhiên và có thể đổi giữa các lần chạy.) Truy vấn chạy không báo lỗi và thoạt nhìn các con số có vẻ hợp lý, đó chính là điều khiến lỗi này nguy hiểm. Bất cứ khi nào bạn xếp chồng các dòng để đếm hay cộng tổng sau đó, thứ bạn cần là UNION ALL.
Bảng tổng hợp từ nhiều nguồn
UNION ALL là cách nhanh để đưa số liệu từ các bảng khác nhau vào một kết quả:
SELECT 'airports' AS metric, COUNT(*) AS value FROM airports
UNION ALL
SELECT 'flights', COUNT(*) FROM flights
UNION ALL
SELECT 'bookings', COUNT(*) FROM bookings
UNION ALL
SELECT 'tickets', COUNT(*) FROM tickets;
| metric | value |
|---|---|
| airports | 104 |
| flights | 33121 |
| bookings | 262788 |
| tickets | 366733 |
Cột nhãn là thứ giữ cho các dòng khác nhau: kể cả khi hai bảng có cùng số đếm, các dòng vẫn khác nhau và cũng sống sót qua UNION. Tuy vậy, nếu không có ORDER BY ở cuối, cơ sở dữ liệu không cam kết trả về các phần theo đúng thứ tự bạn viết. Nếu thứ tự quan trọng, hãy thêm một cột để sắp xếp.
Thêm dòng tổng cộng
Một bảng phân tách với dòng tổng cộng bên dưới, trong cùng một kết quả:
SELECT fare_conditions, COUNT(*) AS segments, ROUND(SUM(amount)) AS revenue
FROM ticket_flights
GROUP BY fare_conditions
UNION ALL
SELECT 'Total', COUNT(*), ROUND(SUM(amount))
FROM ticket_flights
ORDER BY segments;
| fare_conditions | segments | revenue |
|---|---|---|
| Comfort | 17291 | 5661169 |
| Business | 107642 | 55051796 |
| Economy | 920793 | 146956844 |
| Total | 1045726 | 207669809 |
Với các tổng phụ phức tạp hơn, GROUP BY ROLLUP làm được điều tương tự mà không phải lặp lại truy vấn.
Khi nào EXCEPT và INTERSECT hữu ích
EXCEPT là cách gọn để hỏi “còn thiếu gì?”, câu hỏi xuất hiện liên tục khi kiểm tra dữ liệu. Những mẫu máy bay không hề có trong lịch bay:
SELECT aircraft_code FROM aircrafts
EXCEPT
SELECT aircraft_code FROM flights;
| aircraft_code |
|---|
| 320 |
Đó là Airbus A320-200: hãng có nó trong bảng đội bay nhưng không chuyến bay nào dùng nó. Cùng mẫu truy vấn này trả lời các câu hỏi như “hội viên chương trình khách hàng thân thiết chưa từng mua vé” (11.426 trong số 53.000 người ở đây) hay “khách hàng có trong file tháng trước nhưng không có trong file tháng này”.
EXCEPT so sánh cả dòng, nên nó chạy được trên nhiều cột cùng lúc. Để kiểm tra mọi tuyến bay đều có chuyến chiều về, hãy so sánh các cặp (đi, đến) với các cặp (đến, đi): SELECT departure_airport, arrival_airport FROM flights EXCEPT SELECT arrival_airport, departure_airport FROM flights. Với lịch bay này, truy vấn không trả về dòng nào, tức là mọi tuyến đều được khai thác theo cả hai chiều.
Những câu hỏi đó cũng có thể viết bằng NOT EXISTS hoặc LEFT JOIN ... IS NULL (xem phần anti join trong bài về JOIN). EXCEPT ngắn hơn khi bạn chỉ cần các cột khóa. Khi cần thêm các cột khác của những dòng bị thiếu, NOT EXISTS linh hoạt hơn. Một khác biệt cần nhớ: các toán tử tập hợp coi hai giá trị NULL là bằng nhau, còn điều kiện JOIN a.x = b.x không bao giờ khớp NULL.
Quy tắc kết hợp truy vấn
Mọi toán tử tập hợp đều yêu cầu hai vế có cùng hình dạng:
- Cùng số cột. Nếu không, truy vấn báo lỗi. DuckDB thông báo:
Set operations can only apply to expressions with the same number of result columns. - Kiểu dữ liệu tương thích, từng cột một. PostgreSQL từ chối
SELECT range FROM aircrafts UNION SELECT model FROM aircraftsvới thông báo “UNION types integer and character varying cannot be matched”. DuckDB dễ dãi hơn: nó âm thầm chuyển các số sang văn bản và trả về một cột lẫn lộn (6700,Boeing 767-300,1200…). Đừng dựa vào điều đó; hãy ép kiểu tường minh để truy vấn chạy được ở mọi nơi. - Các cột được ghép theo vị trí, không theo tên. Nếu truy vấn thứ nhất chọn
departure_airport, arrival_airportvà truy vấn thứ hai chọnarrival_airport, departure_airport, chúng được xếp thẳng hàng theo đúng thứ tự đó; đây chính là điều mà phép kiểm tra chuyến chiều về ở trên dựa vào, và cũng chính là điều âm thầm làm hỏng báo cáo khi xảy ra do vô tình. - Tên cột lấy từ truy vấn thứ nhất.
SELECT 'x' AS a UNION ALL SELECT 'y' AS btrả về một cột tên làa. ORDER BYvàLIMITở cuối áp dụng cho toàn bộ kết quả. Để sắp xếp hay giới hạn một phần, hãy đặt phần đó trong ngoặc đơn:
(SELECT model, range FROM aircrafts ORDER BY range DESC LIMIT 2)
UNION ALL
(SELECT model, range FROM aircrafts ORDER BY range LIMIT 2);
| model | range |
|---|---|
| Boeing 777-300 | 11100 |
| Boeing 767-300 | 7900 |
| Cessna 208 Caravan | 1200 |
| Bombardier CRJ-200 | 2700 |
Hai mẫu có tầm bay xa nhất và hai mẫu có tầm bay ngắn nhất trong cùng một kết quả.
Khi trộn các toán tử, trong PostgreSQL và SQL chuẩn INTERSECT có độ ưu tiên cao hơn UNION và EXCEPT, nên a UNION b INTERSECT c nghĩa là a UNION (b INTERSECT c). Hãy dùng ngoặc đơn thay vì cố nhớ quy tắc này.
INTERSECT ALL và EXCEPT ALL
PostgreSQL và DuckDB còn hỗ trợ phiên bản ALL của INTERSECT và EXCEPT, đếm các dòng trùng thay vì loại bỏ chúng. Với vế trái là 1, 1, 2:
EXCEPT ALLvới vế phải1trả về1, 2: một trong hai số 1 bị triệt tiêu.INTERSECT ALLvới vế phải1, 1, 1trả về1, 1: bằng số lượng của vế ít hơn.
Chúng hữu ích khi đối chiếu hai bảng từng dòng một, ví dụ so sánh bản trích xuất từ nguồn với bản đã nạp khi dòng trùng là hợp lệ. MySQL và SQL Server hỗ trợ ít biến thể hơn, nên hãy kiểm tra trước khi chuyển truy vấn sang đó.
UNION và JOIN
Cả hai đều kết hợp hai bảng, nhưng theo hai hướng khác nhau. UNION thêm dòng: kết quả rộng bằng mỗi truy vấn và dài bằng cả hai cộng lại. JOIN thêm cột: các dòng từ hai bảng được ghép theo khóa và đặt cạnh nhau. Nếu bạn muốn danh sách chuyến bay kèm mẫu máy bay bên cạnh, đó là JOIN; nếu bạn muốn chuyến đi và chuyến đến trong cùng một danh sách, đó là UNION. JOIN được trình bày chi tiết trong bài JOIN trong SQL.
Câu hỏi thường gặp
UNION và UNION ALL khác nhau thế nào?
UNION trả về các dòng gộp từ hai truy vấn sau khi đã loại bỏ dòng trùng. UNION ALL trả về mọi dòng, kể cả dòng trùng. UNION ALL nhanh hơn vì không cần so sánh các dòng.
UNION ALL có nhanh hơn UNION không?
Có. Để loại bỏ trùng lặp, UNION phải sắp xếp hoặc băm toàn bộ kết quả, tốn thời gian và bộ nhớ trên bảng lớn. UNION ALL chỉ nối thêm. Nếu bạn biết hai phần không thể chồng lấn (ví dụ chúng có nhãn khác nhau hoặc thuộc các khoảng ngày khác nhau), UNION ALL cho ra cùng các dòng mà tốn ít công hơn.
UNION có loại bỏ dòng trùng bên trong một bảng không?
Có. UNION loại bỏ dòng trùng trên toàn bộ kết quả gộp, kể cả những dòng lặp lại bên trong một trong hai truy vấn. Nó hoạt động như UNION ALL rồi tiếp theo là DISTINCT.
Có dùng ORDER BY với UNION được không?
Được, một lần, ở cuối; nó sắp xếp kết quả gộp và có thể tham chiếu tên cột của truy vấn thứ nhất hoặc vị trí cột. Để sắp xếp một phần trước khi gộp, hãy bọc phần đó trong ngoặc đơn với ORDER BY và LIMIT riêng.
EXCEPT và NOT IN khác nhau thế nào?
Cả hai đều tìm những dòng của tập này không có trong tập kia. EXCEPT so sánh cả dòng và coi các NULL là bằng nhau. NOT IN so sánh một cột và không trả về gì cả nếu truy vấn con chứa một NULL, một cái bẫy kinh điển. NOT EXISTS là lựa chọn an toàn nhất nói chung; EXCEPT ngắn nhất khi bạn chỉ cần các cột khóa.
Ngôn ngữ khác: English, Español, Português (Brasil), 简体中文