Phép toán tập hợp

← Nhóm dữ liệu - GROUP BY, HAVING, hàm tổng hợp | → GROUPING SETS, CUBE, ROLLUP

Bạn sẽ học được

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:

  1. Các truy vấn phải có cùng số cột.
  2. Các cột tương ứng phải có kiểu dữ liệu tương thích.
  3. Tên cột của kết quả lấy từ truy vấn đầu tiên.

Và:

  1. ORDER BY chỉ được đặt một lần, ở cuối cùng, áp dụng cho toàn bộ kết quả.
  2. Mặc định (không có ALL) là loại bỏ dòng trùng.
  3. 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_filmsmost_popular_films
titleyeartitleyear
The Shawshank Redemption1994An American Pickle2020
The Godfather1972The Godfather1972
The Dark Knight2008The Dark Knight2008
12 Angry Men1957Greyhound2020

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_films
UNION
SELECT * FROM most_popular_films
ORDER 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_films
UNION ALL
SELECT * FROM most_popular_films
ORDER 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ỖI
SELECT * FROM top_rated_films ORDER BY title
UNION
SELECT * FROM most_popular_films;
 
-- ✅ ĐÚNG
SELECT * FROM top_rated_films
UNION
SELECT * FROM most_popular_films
ORDER 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 nguon
FROM top_rated_films
UNION ALL
SELECT title, release_year, 'Phổ biến'
FROM most_popular_films
ORDER BY title, nguon;

3.4. Kiểu dữ liệu phải tương thích

-- ✅ OK: integer và numeric tương thích
SELECT 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_films
INTERSECT
SELECT * FROM most_popular_films
ORDER BY title;
      title      | release_year
-----------------+--------------
 The Dark Knight |         2008
 The Godfather   |         1972
(2 rows)
  ┌──────┐         ┌──────┐
  │      │╱╲╱╲╱╲╱╲ │      │
  │      │ ██████  │      │      ← chỉ phần giao
  │      │╲╱╲╱╲╱╲╱ │      │
  └──────┘         └──────┘

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:

INTERSECTINNER JOIN
So sánh theoToà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ánhTổng cột của cả hai bảng
Dòng trùngBị 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_films
EXCEPT
SELECT * FROM most_popular_films
ORDER 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. EXCEPT không giao hoán

SELECT * FROM most_popular_films
EXCEPT
SELECT * FROM top_rated_films
ORDER BY title;
       title        | release_year
--------------------+--------------
 An American Pickle |         2020
 Greyhound          |         2020

Kết quả hoàn toàn khác. Trong khi đó UNION và INTERSECT có 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 NULL
SELECT film_id FROM film
WHERE film_id NOT IN (SELECT film_id FROM inventory);
 
-- ✅ Luôn đúng, kể cả khi có NULL
SELECT film_id FROM film
EXCEPT
SELECT film_id FROM inventory;

→ Lọc dữ liệu - WHERE, LIMIT, IN, BETWEEN, LIKE, IS NULL


6. Thứ tự ưu tiên khi kết hợp nhiều phép

INTERSECT có độ ưu tiên cao hơn UNION và EXCEPT (giống như * cao hơn +):

-- Postgres hiểu là: A UNION (B INTERSECT C)
SELECT * FROM A
UNION
SELECT * FROM B
INTERSECT
SELECT * FROM C;
 
-- Muốn (A UNION B) INTERSECT C thì phải đóng ngoặc
(SELECT * FROM A UNION SELECT * FROM B)
INTERSECT
SELECT * 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);

Nếu kết quả rỗng, hai bảng giống hệt nhau. → Công thức thực dụng - EXPLAIN, xóa trùng, so sánh bảng

7.2. Gộp báo cáo từ nhiều bảng lịch sử

SELECT thang, doanh_thu FROM bao_cao_2024
UNION ALL
SELECT thang, doanh_thu FROM bao_cao_2025
UNION ALL
SELECT thang, doanh_thu FROM bao_cao_2026
ORDER 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 film
UNION ALL
SELECT film_id, 'replacement_cost', replacement_cost FROM film
ORDER BY film_id, chi_tieu
LIMIT 6;

7.4. Thêm dòng “Tổng cộng” vào báo cáo

SELECT rating, count(*)::text AS so_phim
FROM film
GROUP BY rating
 
UNION ALL
 
SELECT 'TỔNG CỘNG', count(*)::text
FROM film
 
ORDER 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.

Cách gọn hơn cho việc này là ROLLUP. → GROUPING SETS, CUBE, ROLLUP


8. Bảng tổng hợp

PhépKết quảGiao hoán?An toàn với NULL?
UNIONA ∪ B, khử trùng✅✅
UNION ALLA ∪ B, giữ trùng✅✅
INTERSECTA ∩ B✅✅
EXCEPTA − B❌✅

9. Bài tập

  1. Gộp danh sách first_name của bảng customer và actor, không trùng lặp.
  2. Tìm những tên vừa xuất hiện trong customer.first_name vừa trong actor.first_name.
  3. Tìm những tên chỉ có ở actor mà không có ở customer.
  4. 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;
  5. 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.
  6. Dùng EXCEPT tìm các film_id không có bản đĩa nào trong inventory. So sánh với cách dùng NOT EXISTS ở Lọc dữ liệu - WHERE, LIMIT, IN, BETWEEN, LIKE, IS NULL.

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õ.

← Nhóm dữ liệu - GROUP BY, HAVING, hàm tổng hợp | → GROUPING SETS, CUBE, ROLLUP