DHDataHọc
TheoryLesson 3/85 min read+10 XP

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 FROM dùng đúng chỗ.
  • Bẫy NOT IN khi 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.

code
SELECT NULL = NULL;      -- NULL (không phải true)
SELECT NULL <> NULL;     -- NULL
SELECT NULL IS NULL;     -- true

Hệ quả: WHERE city <> 'Hà Nội' không trả về các dòng có city IS NULL. Muốn lấy cả hai:

code
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:

code
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:

code
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àmDùng khi
COALESCEThay thế NULL bằng giá trị dự phòng
NULLIFBiến giá trị sentinel thành NULL
IS DISTINCT FROMSo sánh có tính đến NULL
IS NULL / IS NOT NULLLọc dòng thiếu/đủ

Bẫy NOT IN

Đây là lỗi NULL phổ biến nhất:

code
-- 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:

code
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:

code
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ớ:

  1. COUNT(*) đếm mọi dòng; COUNT(col) đếm dòng có giá trị.
  2. AVG(col) chia cho số dòng có giá trị, không phải tổng số dòng.
  3. SUM trên toàn NULL trả về NULL, không phải 0. Dùng COALESCE(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:

code
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ểmNhược điểm
NULLPhâ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ảnKhô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ụ:

code
ALTER TABLE customers
  ALTER COLUMN email SET NOT NULL;

Với ràng buộc có điều kiện:

code
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

  1. Viết truy vấn đếm khách chưa có đơn bằng NOT EXISTS.
  2. Giải thích vì sao NOT IN với NULL trả về 0 dòng.
  3. Dùng NULLIF để tránh chia cho 0.
  4. So sánh AVG bỏ qua NULL và AVG coi NULL là 0 trên 4 giá trị cho trước.
  5. Thêm ràng buộc CHECK cho một cột ngày có thể NULL.

Lỗi thường gặp

Triệu chứngNguyên nhânCách sửa
NOT IN trả 0 dòngNULL trong danh sách conDùng NOT EXISTS
<> bỏ sót dòng NULLLogic ba giá trịThêm OR col IS NULL hoặc IS DISTINCT FROM
Trung bình lệchAVG bỏ qua NULLChọn rõ chính sách NULL
SUM trả NULLToàn bộ dòng NULLCOALESCE(SUM(x), 0)
Join mất dòngNULL ở khóaKiểm tra trước/sau join
Nhiều NULL trong cột UNIQUEPostgreSQL cho phépThê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 FROM so sánh có tính NULL.
  • NOT IN với NULL là bẫy; dùng NOT EXISTS.
  • Hàm tổng hợp bỏ qua NULL; COUNT(*) khác COUNT(col).
  • Giữ NULL ở dữ liệu gốc, xử lý hiển thị ở lớp trình bày.

Use ← → to move between lessons