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)

Hai hàng hành khách nhập thành một tại cửa ra máy bay, những thẻ lên máy bay trùng nhau được để riêng sang một bên

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):

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:

  1. 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.
  2. 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 aircrafts vớ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.
  3. 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_airport và truy vấn thứ hai chọn arrival_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.
  4. Tên cột lấy từ truy vấn thứ nhất. SELECT 'x' AS a UNION ALL SELECT 'y' AS b trả về một cột tên là a.
  5. ORDER BY và 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:

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), 简体中文