Gộp, giao và trừ kết quả của nhiều truy vấn bằng UNION, INTERSECT, EXCEPT.
1. Ba quy tắc chung
Cả ba phép toán đều yêu cầu:
Các truy vấn phải có cùng số cột.
Các cột tương ứng phải có kiểu dữ liệu tương thích.
Tên cột của kết quả lấy từ truy vấn đầu tiên.
Và:
ORDER BY chỉ được đặt một lần, ở cuối cùng, áp dụng cho toàn bộ kết quả.
Mặc định (không có ALL) là loại bỏ dòng trùng.
Các phép này coi hai NULL là bằng nhau khi khử trùng.
2. Dữ liệu ví dụ
DROP TABLE IF EXISTS top_rated_films, most_popular_films;CREATE TABLE top_rated_films ( title VARCHAR NOT NULL, release_year SMALLINT NOT NULL);CREATE TABLE most_popular_films ( title VARCHAR NOT NULL, release_year SMALLINT NOT NULL);INSERT INTO top_rated_films (title, release_year) VALUES ('The Shawshank Redemption', 1994), ('The Godfather', 1972), ('The Dark Knight', 2008), ('12 Angry Men', 1957);INSERT INTO most_popular_films (title, release_year) VALUES ('An American Pickle', 2020), ('The Godfather', 1972), ('The Dark Knight', 2008), ('Greyhound', 2020);
top_rated_films
most_popular_films
title
year
title
year
The Shawshank Redemption
1994
An American Pickle
2020
The Godfather
1972
The Godfather
1972
The Dark Knight
2008
The Dark Knight
2008
12 Angry Men
1957
Greyhound
2020
Chung: The Godfather, The Dark Knight.
3. UNION
Gộp hai tập kết quả, loại bỏ dòng trùng.
SELECT * FROM top_rated_filmsUNIONSELECT * FROM most_popular_filmsORDER BY title;
title | release_year--------------------------+-------------- 12 Angry Men | 1957 An American Pickle | 2020 Greyhound | 2020 The Dark Knight | 2008 The Godfather | 1972 The Shawshank Redemption | 1994(6 rows)
4 + 4 = 8 dòng, nhưng 2 dòng trùng bị khử → 6 dòng.
┌──────┐ ┌──────┐
│██████│█████████│██████│ ← toàn bộ, không trùng lặp
└──────┘ └──────┘
3.1. UNION ALL — giữ nguyên dòng trùng
SELECT * FROM top_rated_filmsUNION ALLSELECT * FROM most_popular_filmsORDER BY title;
title | release_year--------------------------+-------------- 12 Angry Men | 1957 An American Pickle | 2020 Greyhound | 2020 The Dark Knight | 2008 The Dark Knight | 2008 ← xuất hiện 2 lần The Godfather | 1972 The Godfather | 1972 ← xuất hiện 2 lần The Shawshank Redemption | 1994(8 rows)
Luôn ưu tiên UNION ALL khi có thể
UNION phải sắp xếp hoặc băm toàn bộ kết quả để tìm dòng trùng — tốn CPU và bộ nhớ. UNION ALL chỉ nối hai luồng dữ liệu, gần như miễn phí.
Chỉ dùng UNION khi bạn thật sự cần khử trùng. Nếu biết chắc hai tập không giao nhau (ví dụ gộp dữ liệu từ các tháng khác nhau), UNION ALL là lựa chọn đúng.
3.2. ORDER BY phải ở cuối
-- ❌ LỖISELECT * FROM top_rated_films ORDER BY titleUNIONSELECT * FROM most_popular_films;-- ✅ ĐÚNGSELECT * FROM top_rated_filmsUNIONSELECT * FROM most_popular_filmsORDER BY title;
Muốn sắp xếp trong từng nhánh (ví dụ để dùng LIMIT), phải bọc bằng ngoặc:
(SELECT title, release_year FROM top_rated_films ORDER BY release_year LIMIT 2)UNION ALL(SELECT title, release_year FROM most_popular_films ORDER BY release_year LIMIT 2)ORDER BY release_year;
3.3. Đánh dấu nguồn gốc dòng
Mẹo hữu ích khi gộp nhiều nguồn:
SELECT title, release_year, 'Đánh giá cao' AS nguonFROM top_rated_filmsUNION ALLSELECT title, release_year, 'Phổ biến'FROM most_popular_filmsORDER BY title, nguon;
3.4. Kiểu dữ liệu phải tương thích
-- ✅ OK: integer và numeric tương thíchSELECT 1 UNION SELECT 2.5;-- ❌ LỖI: không thể trộn số với chuỗi tùy ýSELECT 1 UNION SELECT 'abc';-- ERROR: invalid input syntax for type integer: "abc"
Khi cần, ép kiểu tường minh:
SELECT 1::text UNION SELECT 'abc';
4. INTERSECT
Trả về các dòng có mặt ở cả hai tập kết quả.
SELECT * FROM top_rated_filmsINTERSECTSELECT * FROM most_popular_filmsORDER BY title;
title | release_year-----------------+-------------- The Dark Knight | 2008 The Godfather | 1972(2 rows)
INTERSECT ALL giữ số bản sao bằng số lần xuất hiện ít hơn giữa hai bên.
4.1. INTERSECT vs INNER JOIN
Hai cách này khác nhau về bản chất:
INTERSECT
INNER JOIN
So sánh theo
Toàn bộ dòng
Điều kiện ON bạn chỉ định
Số cột kết quả
Bằng số cột của một nhánh
Tổng cột của cả hai bảng
Dòng trùng
Bị khử
Có thể nhân lên
NULL = NULL
✅ Coi là bằng
❌ Không khớp
Điểm cuối rất quan trọng: INTERSECT khớp được cả các dòng có NULL, còn JOIN ... ON a.x = b.x thì không.
5. EXCEPT
Trả về các dòng có ở truy vấn thứ nhất nhưng không có ở truy vấn thứ hai.
SELECT * FROM top_rated_filmsEXCEPTSELECT * FROM most_popular_filmsORDER BY title;
title | release_year--------------------------+-------------- 12 Angry Men | 1957 The Shawshank Redemption | 1994(2 rows)
┌──────┐ ┌──────┐
│██████│╱╲╱╲╱╲╱╲ │ │
│██████│ │ │ ← chỉ phần riêng của tập đầu
│██████│╲╱╲╱╲╱╲╱ │ │
└──────┘ └──────┘
5.1. EXCEPTkhông giao hoán
SELECT * FROM most_popular_filmsEXCEPTSELECT * FROM top_rated_filmsORDER BY title;
title | release_year--------------------+-------------- An American Pickle | 2020 Greyhound | 2020
Kết quả hoàn toàn khác. Trong khi đó UNION và INTERSECTcó tính giao hoán.
5.2. EXCEPT an toàn với NULL — khác hẳn NOT IN
Đây là ưu điểm lớn nhất của EXCEPT:
-- ❌ Trả về 0 dòng nếu subquery chứa NULLSELECT film_id FROM filmWHERE film_id NOT IN (SELECT film_id FROM inventory);-- ✅ Luôn đúng, kể cả khi có NULLSELECT film_id FROM filmEXCEPTSELECT film_id FROM inventory;
INTERSECT có độ ưu tiên cao hơnUNION và EXCEPT (giống như * cao hơn +):
-- Postgres hiểu là: A UNION (B INTERSECT C)SELECT * FROM AUNIONSELECT * FROM BINTERSECTSELECT * FROM C;-- Muốn (A UNION B) INTERSECT C thì phải đóng ngoặc(SELECT * FROM A UNION SELECT * FROM B)INTERSECTSELECT * FROM C;
UNION và EXCEPT cùng độ ưu tiên, tính từ trái sang phải.
Luôn đóng ngoặc khi trộn nhiều phép
Rõ ràng hơn cho người đọc và tránh nhầm lẫn quy tắc ưu tiên.
7. Ứng dụng thực tế
7.1. So sánh hai bảng — tìm mọi khác biệt
-- Có ở A mà không có ở B(SELECT 'chỉ có ở A' AS nguon, * FROM bang_a EXCEPT ALL SELECT 'chỉ có ở A', * FROM bang_b)UNION ALL-- Có ở B mà không có ở A(SELECT 'chỉ có ở B', * FROM bang_b EXCEPT ALL SELECT 'chỉ có ở B', * FROM bang_a);
SELECT thang, doanh_thu FROM bao_cao_2024UNION ALLSELECT thang, doanh_thu FROM bao_cao_2025UNION ALLSELECT thang, doanh_thu FROM bao_cao_2026ORDER BY thang;
Dùng UNION ALL vì các bảng chắc chắn không giao nhau.
7.3. Xoay dữ liệu thủ công (unpivot)
Biến các cột thành dòng:
SELECT film_id, 'rental_rate' AS chi_tieu, rental_rate AS gia_tri FROM filmUNION ALLSELECT film_id, 'replacement_cost', replacement_cost FROM filmORDER BY film_id, chi_tieuLIMIT 6;
7.4. Thêm dòng “Tổng cộng” vào báo cáo
SELECT rating, count(*)::text AS so_phimFROM filmGROUP BY ratingUNION ALLSELECT 'TỔNG CỘNG', count(*)::textFROM filmORDER BY (rating = 'TỔNG CỘNG'), rating;
Mẹo ORDER BY (dieu_kien): false (0) sắp trước true (1), nên dòng tổng luôn nằm cuối.
Gộp danh sách first_name của bảng customer và actor, không trùng lặp.
Tìm những tên vừa xuất hiện trong customer.first_name vừa trong actor.first_name.
Tìm những tên chỉ có ở actor mà không có ở customer.
Vì sao hai câu sau cho kết quả khác nhau?
SELECT count(*) FROM (SELECT first_name FROM customer UNION SELECT first_name FROM actor) t;SELECT count(*) FROM (SELECT first_name FROM customer UNION ALL SELECT first_name FROM actor) t;
Viết báo cáo liệt kê số phim theo rating kèm một dòng “Tổng cộng” ở cuối.
SELECT first_name FROM customerUNIONSELECT first_name FROM actorORDER BY first_name;
2.
SELECT first_name FROM customerINTERSECTSELECT first_name FROM actorORDER BY first_name;
3.
SELECT first_name FROM actorEXCEPTSELECT first_name FROM customerORDER BY first_name;
Lưu ý thứ tự: actor EXCEPT customer khác customer EXCEPT actor.
4.UNION ALL trả về 599 + 200 = 799 dòng (tổng số dòng hai bảng). UNION khử mọi tên trùng — cả tên trùng giữa hai bảng lẫn tên trùng trong cùng một bảng (có nhiều khách hàng cùng tên) — nên số nhỏ hơn nhiều.
5.
SELECT rating::text AS xep_hang, count(*) AS so_phimFROM filmGROUP BY ratingUNION ALLSELECT 'TỔNG CỘNG', count(*)FROM filmORDER BY (xep_hang = 'TỔNG CỘNG'), xep_hang;
Cần rating::text vì rating có kiểu mpaa_rating (enum), không tự nối được với chuỗi 'TỔNG CỘNG'.
6.
SELECT film_id FROM filmEXCEPTSELECT film_id FROM inventoryORDER BY film_id;
Trả về 42 dòng, giống hệt cách dùng NOT EXISTS. EXCEPT ngắn gọn hơn khi chỉ cần một cột; NOT EXISTS linh hoạt hơn khi cần lấy nhiều cột từ bảng gốc hoặc có điều kiện phức tạp.
10. Tóm tắt
Ba phép: UNION (hợp), INTERSECT (giao), EXCEPT (hiệu).
Yêu cầu cùng số cột và kiểu tương thích; tên cột lấy từ truy vấn đầu.
Mặc định khử trùng; thêm ALL để giữ nguyên — UNION ALL nhanh hơn nhiều.
ORDER BY chỉ đặt một lần ở cuối; muốn sắp trong nhánh thì đóng ngoặc.
EXCEPT không giao hoán; UNION và INTERSECT thì có.
Các phép này coi NULL = NULL → an toàn hơn NOT IN.
INTERSECT ưu tiên cao hơn UNION/EXCEPT — hãy đóng ngoặc cho rõ.