EXISTS và NOT EXISTS
Bạn sẽ học được
EXISTSlà gì và vì sao danh sáchSELECTbên trong nó không quan trọng.- Dùng
EXISTSnhư semi-join để lọc "có ít nhất một" mà không nhân dòng. - Dùng
NOT EXISTScho câu hỏi "không có". - Thêm điều kiện phụ vào truy vấn con tương quan.
- Vì sao
NOT INcó thể trả rỗng và khi nào nên tránh.
Khái niệm cốt lõi
EXISTS nhận một truy vấn con và trả về đúng/sai: có tồn tại ít nhất một dòng thỏa điều kiện hay không. Nó không trả dữ liệu, chỉ trả câu trả lời.
SELECT p.name
FROM products p
WHERE EXISTS (
SELECT 1 FROM order_items oi WHERE oi.product_id = p.id
)
ORDER BY p.name;Tám dòng — mọi sản phẩm đã từng được bán ít nhất một lần. Giá kê laptop không xuất hiện vì chưa có dòng hàng nào trỏ đến nó.
Hai chi tiết về cú pháp:
SELECT 1là quy ước, không phải yêu cầu.SELECT *,SELECT NULL,SELECT oi.idđều cho kết quả y hệt, vì PostgreSQL chỉ kiểm tra có dòng hay không, không đọc giá trị. ViếtSELECT 1để người đọc hiểu ngay "tôi chỉ cần sự tồn tại".- Truy vấn con thường tương quan: nó tham chiếu
p.idtừ câu ngoài. Vì vậy nó được đánh giá cho từng sản phẩm, và dừng ngay khi tìm được dòng đầu tiên — không quét hết.
Điểm mạnh lớn nhất của EXISTS so với JOIN: không nhân dòng. Câu hỏi "khách nào đã mua hàng Điện tử" nếu viết bằng JOIN sẽ trả mỗi khách nhiều lần — một lần cho mỗi dòng hàng Điện tử họ mua:
-- 12 dòng, nhiều khách lặp lại
SELECT c.name
FROM customers c
JOIN orders o ON o.customer_id = c.id
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id
WHERE p.category = 'Điện tử';Muốn đếm đúng, bạn phải thêm DISTINCT hoặc GROUP BY. Với EXISTS, mỗi khách chỉ được trả một lần vì câu hỏi là "có hay không", không phải "bao nhiêu":
SELECT c.name
FROM customers c
WHERE EXISTS (
SELECT 1
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id
WHERE o.customer_id = c.id AND p.category = 'Điện tử'
)
ORDER BY c.name;Kết quả: 8 khách, không trùng lặp. Dạng truy vấn này gọi là semi-join — nối bán phần, chỉ giữ sự tồn tại.
NOT EXISTS đảo ngược: giữ dòng mà truy vấn con không tìm được gì.
SELECT c.name
FROM customers c
WHERE NOT EXISTS (
SELECT 1 FROM orders o WHERE o.customer_id = c.id
);Kết quả: Hà Bùi — khách duy nhất chưa có đơn.
Vì EXISTS nhận mọi điều kiện, bạn có thể hỏi những câu phức tạp hơn một phép nối đơn giản. "Khách chưa từng có đơn hoàn tất":
SELECT c.name
FROM customers c
WHERE NOT EXISTS (
SELECT 1 FROM orders o
WHERE o.customer_id = c.id AND o.status = 'completed'
)
ORDER BY c.name;Bốn người: Em Võ, Hà Bùi, Inh Đỗ, Nga Lý. Điều kiện status = 'completed' nằm trong truy vấn con — nó thu hẹp định nghĩa "tồn tại", không lọc kết quả cuối.
Cùng ý tưởng với "khách đang có đơn chờ":
SELECT c.name
FROM customers c
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.customer_id = c.id AND o.status = 'pending'
)
ORDER BY c.name;Hai người: Em Võ, Nga Lý.
Bảng chọn công cụ:
| Cách viết | Trả gì | Nhân dòng? | Dùng khi |
|---|---|---|---|
JOIN | Dữ liệu hai bảng | Có | Cần cột của bảng phải |
IN (subquery) | Tập giá trị | Không | Danh sách giá trị nhỏ, không có NULL |
EXISTS | Đúng/sai | Không | Chỉ cần biết "có tồn tại" |
NOT EXISTS | Đúng/sai | Không | Cần "không tồn tại", an toàn với NULL |
Cạm bẫy NOT IN: nếu danh sách con chứa NULL, toàn bộ điều kiện thành NULL và câu trả không dòng nào. Trong bảng khách có hai email NULL (Dũng Phạm, Inh Đỗ), nên:
SELECT COUNT(*) FROM customers
WHERE email NOT IN (SELECT email FROM customers WHERE city = 'Hà Nội');trả 0, dù logic đời thường là "khách ở thành phố khác". NOT EXISTS viết lại an toàn:
SELECT COUNT(*) FROM customers c
WHERE NOT EXISTS (
SELECT 1 FROM customers c2
WHERE c2.city = 'Hà Nội' AND c2.email = c.email
);Kết quả 10 — mọi khách trừ An Nguyễn và Khánh Hồ, hai người Hà Nội có email trùng với chính họ. Khách có email NULL (Dũng Phạm, Inh Đỗ) không "khớp" với ai, kể cả chính họ, nên Dũng vẫn nằm trong kết quả. Đó là ngữ nghĩa đúng của "không có email trùng với khách Hà Nội nào".
Ví dụ chạy được
Khách đã mua Sách SQL thực chiến:
SELECT c.name
FROM customers c
WHERE EXISTS (
SELECT 1
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
WHERE o.customer_id = c.id AND oi.product_id = 6
)
ORDER BY c.name;Bốn khách. Cùng câu hỏi bằng JOIN cần DISTINCT; bằng IN cần danh sách customer_id từ hai lần nối — EXISTS diễn đạt trực tiếp nhất.
Đếm sản phẩm chưa bán được:
SELECT COUNT(*) AS so_san_pham_chua_ban
FROM products p
WHERE NOT EXISTS (
SELECT 1 FROM order_items oi WHERE oi.product_id = p.id
);Kết quả 1 (Giá kê laptop). Đối chiếu: 9 sản phẩm = 8 đã bán + 1 chưa bán.
So sánh semi-join và join:
SELECT 'EXISTS' AS cach, COUNT(*) AS so_dong FROM (
SELECT c.id FROM customers c
WHERE EXISTS (
SELECT 1 FROM orders o JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id
WHERE o.customer_id = c.id AND p.category = 'Gia dụng'
)
) t
UNION ALL
SELECT 'JOIN', COUNT(*) FROM (
SELECT c.id
FROM customers c
JOIN orders o ON o.customer_id = c.id
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id
WHERE p.category = 'Gia dụng'
) t;Kết quả: EXISTS | 8 và JOIN | 16. Cùng tám khách, nhưng JOIN trả 16 dòng vì mỗi khách mua nhiều món Gia dụng. EXISTS trả đúng hạt của câu hỏi — mỗi khách một dòng.
Một phép kiểm chứng hiệu năng đáng nhớ: EXISTS dừng ở dòng khớp đầu tiên. Nếu bảng order_items có chỉ mục trên product_id, PostgreSQL tìm được một dòng trong vài bước, không cần quét toàn bảng. NOT IN thì thường phải dựng toàn bộ danh sách con trước khi so sánh — với danh sách lớn, chênh lệch rất rõ.
Thực hành
- Liệt kê sản phẩm chưa từng được bán bằng
NOT EXISTS. Kết quả: Giá kê laptop. - Đếm khách đã mua hàng Gia dụng bằng
EXISTS. Đáp số 8; đếm bằngJOINmà quênDISTINCTsẽ ra 16. - Tìm khách có ít nhất một đơn
pending. Kết quả: Em Võ, Nga Lý. - Chạy câu
NOT INvới email, rồi viết lại bằngNOT EXISTS. Ghi lại hai kết quả 0 và 10. - Đổi
SELECT 1trong một câuEXISTSthànhSELECT *và xác nhận kết quả không đổi.
Lỗi thường gặp
| Triệu chứng | Nguyên nhân | Cách sửa |
|---|---|---|
| Kết quả trùng lặp | Dùng JOIN thay EXISTS | Đổi sang EXISTS hoặc thêm DISTINCT |
NOT IN trả 0 dòng | Danh sách con chứa NULL | NOT EXISTS hoặc loại NULL |
EXISTS luôn đúng | Truy vấn con không tương quan, không lọc theo dòng ngoài | Thêm WHERE bang_ngoai.id = ... |
| Kết quả thiếu người | Điều kiện phụ đặt ngoài truy vấn con | Chuyển vào WHERE của EXISTS |
Tưởng EXISTS trả cột | Hiểu sai ngữ nghĩa boolean | Chỉ cần biết có dòng hay không |
Dòng thứ ba là lỗi khó thấy: nếu truy vấn con không tham chiếu bảng ngoài và bảng trong có ít nhất một dòng, EXISTS đúng với mọi dòng ngoài. Kết quả trông như "tất cả khách đều thỏa" — và đó chính là dấu hiệu để kiểm tra lại mệnh đề tương quan.
Tóm tắt
EXISTStrả đúng/sai và dừng ở dòng khớp đầu tiên; danh sáchSELECTbên trong không quan trọng.- Dùng
EXISTSnhư semi-join: lọc "có ít nhất một" mà không nhân dòng. NOT EXISTSlà cách an toàn để hỏi "không có", kể cả khi dữ liệu chứaNULL.- Điều kiện phụ đặt trong truy vấn con để định nghĩa lại "tồn tại".
NOT INvới cột nullable có thể trả rỗng mà không báo lỗi — tránh dùng.
Use ← → to move between lessons