Xử lý NULL có kỷ luật
Bạn sẽ học được
- Logic ba giá trị của SQL và vì sao nó gây bất ngờ.
COALESCE,NULLIF,IS DISTINCT FROMdùng đúng chỗ.- Bẫy
NOT INkhi có NULL. - Hàm tổng hợp bỏ qua NULL và hệ quả.
- Chọn giữa NULL và giá trị mặc định.
Khái niệm cốt lõi
Trong SQL, NULL nghĩa là không biết, không phải "rỗng" hay "0". Mọi so sánh với NULL cho kết quả UNKNOWN, và WHERE chỉ giữ dòng khi điều kiện là TRUE.
SELECT NULL = NULL; -- NULL (không phải true)
SELECT NULL <> NULL; -- NULL
SELECT NULL IS NULL; -- trueHệ quả: WHERE city <> 'Hà Nội' không trả về các dòng có city IS NULL. Muốn lấy cả hai:
SELECT *
FROM customers
WHERE city IS DISTINCT FROM 'Hà Nội';IS DISTINCT FROM coi NULL như một giá trị bình thường: NULL khác 'Hà Nội' → true.
COALESCE và NULLIF
COALESCE(a, b, c) trả về giá trị không NULL đầu tiên:
SELECT
COALESCE(phone, email, 'không có liên hệ') AS lien_he
FROM customers;NULLIF(a, b) trả về NULL nếu a = b. Dùng để biến giá trị vô nghĩa thành NULL trước khi chia hoặc tính trung bình:
SELECT
NULLIF(quantity, 0) AS mau_so_an_toan,
total / NULLIF(quantity, 0) AS don_gia
FROM order_items;Không có NULLIF, phép chia cho 0 gây lỗi. Với cột mà "0" thực chất là "không biết", NULLIF(quantity, 0) còn làm cho các hàm tổng hợp bỏ qua đúng các dòng đó.
| Hàm | Dùng khi |
|---|---|
COALESCE | Thay thế NULL bằng giá trị dự phòng |
NULLIF | Biến giá trị sentinel thành NULL |
IS DISTINCT FROM | So sánh có tính đến NULL |
IS NULL / IS NOT NULL | Lọc dòng thiếu/đủ |
Bẫy NOT IN
Đây là lỗi NULL phổ biến nhất:
-- Nếu orders.customer_id có bất kỳ NULL nào, truy vấn trả về 0 dòng
SELECT *
FROM customers
WHERE id NOT IN (SELECT customer_id FROM orders);Lý do: id NOT IN (1, 2, NULL) tương đương id <> 1 AND id <> 2 AND id <> NULL; vế cuối là UNKNOWN, nên cả điều kiện không bao giờ là TRUE.
Cách sửa — dùng NOT EXISTS hoặc loại NULL:
SELECT c.*
FROM customers c
WHERE NOT EXISTS (
SELECT 1 FROM orders o WHERE o.customer_id = c.id
);NOT EXISTS không bị ảnh hưởng bởi NULL vì nó kiểm tra sự tồn tại của dòng khớp.
Hàm tổng hợp và NULL
Các hàm như SUM, AVG, MIN, MAX, COUNT(col) bỏ qua NULL:
SELECT
COUNT(*) AS so_dong,
COUNT(phone) AS so_co_phone,
AVG(price) AS gia_tb_bo_qua_null
FROM customers
LEFT JOIN orders ON orders.customer_id = customers.id;Ba điểm cần nhớ:
COUNT(*)đếm mọi dòng;COUNT(col)đếm dòng có giá trị.AVG(col)chia cho số dòng có giá trị, không phải tổng số dòng.SUMtrên toàn NULL trả về NULL, không phải 0. DùngCOALESCE(SUM(x), 0)nếu cần 0.
Sự khác biệt giữa AVG bỏ qua NULL và AVG coi NULL là 0 có thể lớn. Ví dụ 4 dòng: 10, 20, NULL, NULL → AVG = 15; nếu coi NULL = 0 thì trung bình = 7,5. Phải chọn theo ý nghĩa nghiệp vụ và ghi rõ.
NULL trong JOIN
JOIN ... ON a.x = b.x không khớp khi một phía NULL, vì NULL = NULL là UNKNOWN. Điều này thường đúng ý (không biết thì không khớp), nhưng gây mất dòng ngoài ý muốn khi NULL là mã hợp lệ.
Kiểm tra số dòng trước và sau join để phát hiện mất dòng:
SELECT
(SELECT COUNT(*) FROM customers) AS truoc,
(SELECT COUNT(*) FROM customers c JOIN orders o ON o.customer_id = c.id) AS sau_join;Chênh lệch lớn không phải lỗi nếu join là một-nhiều, nhưng cần hiểu vì sao.
NULL hay giá trị mặc định
| Cách | Ưu điểm | Nhược điểm |
|---|---|---|
| NULL | Phân biệt "không biết" với "bằng 0" | Cần xử lý ở mọi truy vấn |
| Giá trị mặc định (0, '', 'unknown') | Truy vấn đơn giản | Không phân biệt được thiếu và thật |
Nguyên tắc: giữ NULL cho "không biết" trong dữ liệu gốc. Nếu giao diện cần hiển thị "chưa có", xử lý ở lớp trình bày bằng COALESCE, không ghi giá trị giả vào bảng.
Ngoại lệ: khi giá trị mặc định có nghĩa nghiệp vụ rõ (ví dụ status = 'NEW'), dùng mặc định thay vì NULL.
Ràng buộc và NULL
Trong PostgreSQL, UNIQUE cho phép nhiều NULL (vì hai NULL không bằng nhau). Nếu muốn cấm NULL ở cột khóa nghiệp vụ:
ALTER TABLE customers
ALTER COLUMN email SET NOT NULL;Với ràng buộc có điều kiện:
ALTER TABLE orders
ADD CONSTRAINT ngay_giao_sau_ngay_dat
CHECK (shipped_at IS NULL OR shipped_at >= created_at);Ràng buộc "nếu có thì phải đúng" viết bằng IS NULL OR ....
Thực hành
- Viết truy vấn đếm khách chưa có đơn bằng
NOT EXISTS. - Giải thích vì sao
NOT INvới NULL trả về 0 dòng. - Dùng
NULLIFđể tránh chia cho 0. - So sánh
AVGbỏ qua NULL vàAVGcoi NULL là 0 trên 4 giá trị cho trước. - Thêm ràng buộc
CHECKcho một cột ngày có thể NULL.
Lỗi thường gặp
| Triệu chứng | Nguyên nhân | Cách sửa |
|---|---|---|
NOT IN trả 0 dòng | NULL trong danh sách con | Dùng NOT EXISTS |
<> bỏ sót dòng NULL | Logic ba giá trị | Thêm OR col IS NULL hoặc IS DISTINCT FROM |
| Trung bình lệch | AVG bỏ qua NULL | Chọn rõ chính sách NULL |
SUM trả NULL | Toàn bộ dòng NULL | COALESCE(SUM(x), 0) |
| Join mất dòng | NULL ở khóa | Kiểm tra trước/sau join |
| Nhiều NULL trong cột UNIQUE | PostgreSQL cho phép | Thêm NOT NULL |
Tóm tắt
- NULL là "không biết"; mọi so sánh với NULL cho UNKNOWN.
IS DISTINCT FROMso sánh có tính NULL.NOT INvới NULL là bẫy; dùngNOT EXISTS.- Hàm tổng hợp bỏ qua NULL;
COUNT(*)khácCOUNT(col). - Giữ NULL ở dữ liệu gốc, xử lý hiển thị ở lớp trình bày.
Use ← → to move between lessons