Lập trình · 24/09/2026

SQL EXISTS và JOIN: Lọc dữ liệu mà không nhân bản dòng

Bạn cần danh sách khách hàng đã có đơn thanh toán, nhưng truy vấn lại trả một khách hàng nhiều lần. Nguyên nhân có thể không nằm ở dữ liệu trùng: một phép JOIN đang tạo một dòng cho mỗi đơn phù hợp. Khi yêu cầu chỉ là “có hay không”, EXISTS thường diễn đạt ý định rõ hơn.

SQL EXISTS và JOIN: Lọc dữ liệu mà không nhân bản dòng

Bạn cần danh sách khách hàng đã có đơn thanh toán, nhưng truy vấn lại trả một khách hàng nhiều lần. Nguyên nhân có thể không nằm ở dữ liệu trùng: một phép JOIN đang tạo một dòng cho mỗi đơn phù hợp. Khi yêu cầu chỉ là “có hay không”, EXISTS thường diễn đạt ý định rõ hơn.

1. Xác định một dòng kết quả đại diện cho điều gì

Giả sử có hai bảng: customers với id là khóa chính, và orders với id, customer_id, status. Dữ liệu minh họa gồm khách hàng An có hai đơn paid; Bình có một đơn pending; Chi chưa có đơn nào.

Câu hỏi nghiệp vụ là “khách hàng nào có ít nhất một đơn paid”, vì vậy mỗi dòng kết quả phải đại diện cho một khách hàng, không phải một đơn hàng. Hãy viết yêu cầu này trước khi chọn cú pháp SQL.

2. Vì sao JOIN trả nhiều dòng?

SELECT c.id, c.name
FROM customers AS c
JOIN orders AS o ON o.customer_id = c.id
WHERE o.status = 'paid';

Truy vấn trả An hai lần vì có hai cặp khách hàng–đơn hàng thỏa điều kiện. Đây là hành vi phù hợp với quy tắc JOIN của PostgreSQL, không phải bằng chứng bảng customers bị trùng.

Nếu màn hình thực sự hiển thị từng đơn, hãy chọn thêm o.id và những trường cần thiết. Khi đó hai dòng của An là đúng. Vấn đề chỉ xuất hiện khi ứng dụng tưởng mỗi dòng là một khách hàng riêng biệt rồi đếm hoặc phân trang theo giả định đó.

3. Dùng EXISTS cho điều kiện có ít nhất một dòng

SELECT c.id, c.name
FROM customers AS c
WHERE EXISTS (
    SELECT 1
    FROM orders AS o
    WHERE o.customer_id = c.id
      AND o.status = 'paid'
)
ORDER BY c.id;

EXISTS kiểm tra truy vấn con có trả dòng nào hay không. Ở đây An xuất hiện một lần; Bình và Chi không xuất hiện. SELECT 1 thể hiện rằng ta không cần lấy nội dung đơn hàng làm kết quả của truy vấn ngoài.

Điều kiện o.customer_id = c.id rất quan trọng. Nếu bỏ nó, truy vấn chỉ kiểm tra toàn bộ bảng orders có bất kỳ đơn paid nào; khi có, mọi khách hàng đều có thể vượt qua bộ lọc.

4. DISTINCT không thay thế việc hiểu quan hệ dữ liệu

Với truy vấn JOIN ban đầu, SELECT DISTINCT c.id, c.name cũng có thể tạo danh sách khách hàng mong muốn. Nhưng nếu sau đó thêm o.id vào danh sách cột, từng dòng lại khác nhau và DISTINCT không còn gộp chúng.

Không nên kết luận DISTINCT luôn sai hoặc EXISTS luôn nhanh hơn. Hãy chọn biểu thức phản ánh yêu cầu, sau đó đánh giá kế hoạch thực thi trên dữ liệu đại diện. Số lượng đơn, phân bố trạng thái và chỉ mục đều có thể ảnh hưởng đến hiệu năng.

5. Phân biệt “không có đơn paid” và “không có đơn”

SELECT c.id, c.name
FROM customers AS c
WHERE NOT EXISTS (
    SELECT 1
    FROM orders AS o
    WHERE o.customer_id = c.id
      AND o.status = 'paid'
)
ORDER BY c.id;

Kết quả mong đợi gồm Bình và Chi. Bình có đơn nhưng chưa có đơn paid; Chi chưa có đơn nào. Nếu cần riêng khách hàng hoàn toàn chưa có đơn, bỏ điều kiện status trong truy vấn con.

Đừng đổi yêu cầu “không có đơn paid” thành “có đơn không paid”. Một khách hàng có cả đơn paid lẫn pending đáp ứng câu thứ hai nhưng không đáp ứng câu thứ nhất. Đây là khác biệt nghiệp vụ, không phải lựa chọn phong cách viết SQL.

6. Checklist kiểm thử trước khi sử dụng

  • Khách hàng không có đơn: bị loại bởi EXISTS với paid, được giữ bởi NOT EXISTS với paid.
  • Khách hàng có một đơn paid: xuất hiện một lần.
  • Khách hàng có nhiều đơn paid: vẫn xuất hiện một lần trong truy vấn EXISTS.
  • Khách hàng có cả paid và pending: được giữ bởi EXISTS với paid, bị loại bởi NOT EXISTS với paid.
  • Các điều kiện phạm vi như tenant hoặc quyền truy cập được áp dụng đúng theo mô hình dữ liệu, không chỉ lọc trạng thái.

Các kết quả trên là kỳ vọng từ bộ dữ liệu minh họa, không phải benchmark. Khi chuyển vào dự án, hãy chạy kiểm thử với dữ liệu mẫu, kiểm tra truy vấn đếm và truy vấn phân trang dùng cùng điều kiện. Chỉ thêm hoặc thay đổi chỉ mục sau khi xem kế hoạch và cân nhắc chi phí ghi.

Nguyên tắc dễ nhớ: JOIN khi cần kết hợp các dòng để lấy dữ liệu; EXISTS khi cần giữ dòng ngoài dựa trên việc có dữ liệu liên quan. Bắt đầu bằng ý nghĩa của một dòng kết quả sẽ giúp tránh nhiều lỗi đếm, lọc và phân trang về sau.

Thảo luận

Bình luận 0

Đăng nhập để bình luận

Bạn cần có tài khoản để tham gia thảo luận và trả lời độc giả khác.

Đăng nhậpĐăng ký

Chưa có bình luận. Hãy là người đầu tiên chia sẻ ý kiến.