Bạn cần lấy những tài khoản không nằm trong danh sách bị chặn. Dữ liệu có ba tài khoản nhưng truy vấn NOT IN không trả hàng nào. Sau khi kiểm tra, danh sách chặn có thêm một giá trị NULL. Đây là tình huống cần hiểu ngữ nghĩa SQL trước khi nghĩ đến index hay cache.
Bài đối chiếu PostgreSQL 17 và tài liệu SQLite ngày 25/10/2026. Fixture SQL được chạy trên SQLite CLI 3.54.0; chưa chạy PostgreSQL trong lượt biên tập. Phần PostgreSQL dựa trên tài liệu chính thức, không phải kết quả thực thi PostgreSQL được suy ra từ SQLite.
1. Tạo tình huống có thể quan sát
Chạy trong database thử và cùng một phiên kết nối. Hai bảng tạm này chỉ dùng cho bài học; không dùng tên bảng nghiệp vụ thật.
CREATE TEMP TABLE demo_accounts (id integer PRIMARY KEY);
CREATE TEMP TABLE demo_blocked (account_id integer);
INSERT INTO demo_accounts (id) VALUES (1), (2), (3);
INSERT INTO demo_blocked (account_id) VALUES (2), (NULL);
SELECT id
FROM demo_accounts
WHERE id NOT IN (SELECT account_id FROM demo_blocked)
ORDER BY id;
Trên fixture SQLite đã chạy, SELECT cuối không trả hàng nào. Nếu bạn chỉ nhìn hai tài khoản 1 và 3 không khớp giá trị 2 rồi kỳ vọng cả hai xuất hiện, bạn đã bỏ qua hàng NULL trong phía bên phải.
Theo PostgreSQL về subquery expressions, khi không tìm thấy giá trị bằng nhau nhưng phía phải có NULL, NOT IN có thể cho kết quả NULL thay vì true. Giá trị đó không được giữ lại như một điều kiện WHERE đúng. Bảng chân trị trong SQLite SQL expressions cũng mô tả trường hợp này.
2. Diễn đạt điều kiện không tồn tại hàng khớp
Nếu nghiệp vụ là “không có hàng chặn nào bằng ID tài khoản”, hãy viết rõ phép đối chiếu trong subquery:
SELECT a.id
FROM demo_accounts AS a
WHERE NOT EXISTS (
SELECT 1
FROM demo_blocked AS b
WHERE b.account_id = a.id
)
ORDER BY a.id;
Với fixture trên, kết quả đã kiểm tra là 1 và 3. EXISTS xét việc subquery có trả hàng hay không. Hàng chặn có account_id bằng NULL không tạo ra phép bằng true với ID 1 hoặc 3, nên không trở thành một hàng khớp trong subquery.
Số 1 trong SELECT 1 thể hiện rằng ta cần sự tồn tại của hàng, không cần lấy giá trị cụ thể từ hàng đó. Đừng thêm hàm có side effect vào subquery để theo dõi “mỗi hàng đã chạy bao nhiêu lần”; đây không phải hợp đồng thực thi mà EXISTS hứa cung cấp.
3. Khi lọc NULL khỏi phía phải là đúng ý định
Bạn cũng có thể giữ NOT IN và loại những giá trị chưa có ID:
SELECT id
FROM demo_accounts
WHERE id NOT IN (
SELECT account_id
FROM demo_blocked
WHERE account_id IS NOT NULL
)
ORDER BY id;
Fixture này cũng trả 1 và 3. Tuy nhiên, cách sửa cần gắn với ý nghĩa dữ liệu: NULL trong bảng chặn là bản ghi nhập dở, dữ liệu không hợp lệ hay một trạng thái nghiệp vụ có chủ đích? Nếu nó không được phép tồn tại, chỉ lọc khi SELECT có thể làm lỗi chất lượng dữ liệu tiếp tục bị che đi.
Tài liệu comparison operators của PostgreSQL mô tả IS NULL và IS NOT NULL. Đừng viết account_id <> NULL để lọc; đó không phải phép kiểm tra NULL mà bạn đang cần.
4. Rà cả cột bên trái
Trong fixture, ID tài khoản là khóa chính và các giá trị mẫu đều có ID. Nếu cột bên trái có thể NULL, NOT IN và NOT EXISTS với phép bằng không mặc nhiên thể hiện cùng một chính sách cho hàng đó. Trước khi thay toàn bộ truy vấn trong codebase, hãy quyết định bản ghi không có ID phải bị loại, được giữ hay bị xem là dữ liệu lỗi.
Nếu sản phẩm không chấp nhận ID trống, ghi điều kiện a.id IS NOT NULL rõ ràng trong truy vấn dùng trên một cột nullable, đồng thời cân nhắc ràng buộc ở nơi lưu dữ liệu. Không dùng một giá trị thay thế như 0 trong COALESCE chỉ vì nó làm kết quả có vẻ đúng; giá trị thay thế có thể trùng với dữ liệu thật.
Kiểm tra trước khi sửa truy vấn thật
Tạo các ca: bảng chặn rỗng, chỉ có ID hợp lệ, có NULL, có ID lặp và cột trái nullable. Ghi danh sách kết quả mong đợi theo nghiệp vụ rồi mới so sánh SQL. Với ID không nullable trong fixture này, bảng chặn rỗng phải giữ cả ba tài khoản; bảng chỉ chặn 2 phải giữ 1 và 3.
Sau khi đúng ngữ nghĩa, mới đánh giá execution plan trên database và dữ liệu đại diện. Bài này không có benchmark để kết luận NOT EXISTS luôn nhanh hơn NOT IN. Cùng một kết quả logic không đảm bảo cùng một plan trên mọi phiên bản hay phân bố dữ liệu.
Kết luận
NULL khiến điều kiện loại trừ cần được viết và kiểm tra cẩn thận hơn phép so sánh danh sách thông thường. Dùng fixture nhỏ để làm rõ chính sách dữ liệu, rồi chọn NOT EXISTS hoặc NOT IN đã lọc NULL theo đúng ý định. Đừng sửa cú pháp trước khi hiểu những hàng nào thực sự cần được giữ lại.


