Bài 7: Xử lý NULL đúng cách – Tránh "sập bẫy" logic và hiệu năng

Dưới đây là nội dung chi tiết cho Bài 7: Xử lý NULL đúng cách – Phân tích sâu "bẫy" logic và hiệu năng, khám phá từng lớp nguyên nhân và giải pháp triệt để:


Hiểu sâu bản chất NULL và cách database xử lý giá trị "vô hình"


1. NULL là gì? Tại sao nó phức tạp?

1.1. Định nghĩa

  • NULL: Không phải là giá trị, mà là trạng thái "không xác định" hoặc "thiếu giá trị".

  • Khác biệt cốt lõi: NULL = NULLKHÔNG đúng (trả về NULL thay vì TRUE).

1.2. Ví dụ minh họa

SELECT NULL = NULL;      -- Kết quả: NULL  
SELECT NULL IS NULL;     -- Kết quả: TRUE  
SELECT 1 + NULL;         -- Kết quả: NULL

2. Phân tích sâu "bẫy" logic khi xử lý NULL

2.1. So sánh với NULL dùng =

Ví dụ sai:

-- Lọc khách hàng chưa có số điện thoại  
SELECT * FROM customers WHERE phone = NULL;  -- Sai! Luôn trả về 0 rows

Cách đúng:

SELECT * FROM customers WHERE phone IS NULL;

Nguyên nhân:

  • Ba giá trị logic (Three-Valued Logic):

    • TRUE / FALSE / UNKNOWN (NULL).

    • WHERE chỉ trả về rows có điều kiện TRUE.

2.2. NULL trong phép JOIN

Ví dụ:

SELECT *  
FROM employees e  
LEFT JOIN bonuses b ON e.id = b.employee_id  
WHERE b.amount > 1000;  -- Loại bỏ các employees không có bonus (b.amount IS NULL)

Kết quả sai: Những người chưa có bonus (b.amount IS NULL) bị loại khỏi kết quả.

Cách đúng:

SELECT *  
FROM employees e  
LEFT JOIN bonuses b ON e.id = b.employee_id  
WHERE (b.amount > 1000 OR b.amount IS NULL);

3. Ảnh hưởng của NULL đến hiệu năng

3.1. Index và NULL

  • B-tree Index: Thường không lưu trữ entries cho rows có giá trị NULL (tùy database).

  • Partial Index (PostgreSQL): Tạo index chỉ cho rows không NULL.

Ví dụ:

-- Tạo index bỏ qua NULL  
CREATE INDEX idx_customers_phone ON customers(phone)  
WHERE phone IS NOT NULL;  

-- Truy vấn tận dụng index  
SELECT * FROM customers  
WHERE phone = '0123456789';  -- Sử dụng index  

SELECT * FROM customers  
WHERE phone IS NULL;         -- Full Scan (không có trong index)

3.2. Thống kê dữ liệu

  • Cardinality Estimate: Số lượng NULLs ảnh hưởng đến ước lượng của Optimizer.

  • Ví dụ: Nếu 90% rows có phone IS NULL, query WHERE phone IS NOT NULL có thể bị ước lượng sai → Chọn plan không tối ưu.


4. Giải pháp xử lý NULL toàn diện

4.1. Hàm hỗ trợ NULL

  • COALESCE: Thay thế NULL bằng giá trị mặc định.

      SELECT COALESCE(phone, 'N/A') AS phone FROM customers;
    
  • NULLIF: Chuyển giá trị cụ thể thành NULL.

      SELECT NULLIF(description, '') AS description FROM products;  -- Chuyển '' thành NULL
    
  • IS DISTINCT FROM (PostgreSQL): So sánh cả NULL.

      SELECT * FROM table WHERE col IS DISTINCT FROM 0;  -- Bao gồm cả col IS NULL
    

4.2. Thiết kế schema thông minh

  • NOT NULL Constraint: Giảm NULL từ gốc nếu dữ liệu luôn yêu cầu giá trị.

      CREATE TABLE users (  
        id INT PRIMARY KEY,  
        email VARCHAR(255) NOT NULL  -- Tránh NULL trong email  
      );
    
  • Giá trị mặc định: Thay thế NULL bằng giá trị hợp lý.

      CREATE TABLE orders (  
        status VARCHAR(20) DEFAULT 'PENDING' NOT NULL  
      );
    

5. Case Study: Tối ưu hóa query chứa NULL

5.1. Bối cảnh

  • Bảng sales có 10 triệu rows, cột discount (20% rows có discount IS NULL).

  • Truy vấn:

      SELECT product_id, SUM(price * quantity) AS total  
      FROM sales  
      WHERE discount IS NULL OR discount = 0  
      GROUP BY product_id;
    
  • Hiệu năng ban đầu: 8 giây (Full Table Scan).

5.2. Tối ưu hóa

Bước 1: Thêm Partial Index cho discount:

CREATE INDEX idx_sales_discount ON sales(discount)  
WHERE discount IS NULL OR discount = 0;

Bước 2: Viết lại query sử dụng Expression Filter:

SELECT product_id, SUM(price * quantity) AS total  
FROM sales  
WHERE discount IS NULL OR discount = 0  
GROUP BY product_id;

Execution Plan mới:

  • Index Scan trên idx_sales_discount → Chỉ quét 20% dữ liệu.

  • Thời gian: 0.9 giây (cải thiện ~9x).


6. Bài tập thực hành

Viết lại các truy vấn sau để xử lý NULL an toàn:

  1. Truy vấn gốc (dễ mất dữ liệu):

     SELECT * FROM orders  
     WHERE cancelled_at = NULL;
    
  2. Truy vấn gốc (sai logic):

     SELECT AVG(rating) FROM reviews  
     WHERE rating != 0;  -- Loại bỏ cả rating = NULL
    

Gợi ý đáp án:
1.

SELECT * FROM orders  
WHERE cancelled_at IS NULL;
SELECT AVG(rating) FROM reviews  
WHERE rating IS NOT NULL AND rating != 0;

7. Tổng kết

  • NULL là "ẩn số" cần được đặc biệt lưu ý trong cả logic và hiệu năng.

  • Luôn dùng IS NULL/IS NOT NULL thay vì phép so sánh thông thường.

  • Tối ưu hóa với Partial Index và hàm hỗ trợ để giảm tác động tiêu cực của NULL.

  • Thiết kế schema chặt chẽ để hạn chế NULL từ gốc.

Làm chủ NULL giúp bạn tránh những lỗi "khó hiểu" và xây dựng hệ thống robust hơn!


Preview bài tiếp theo:
Bài 8: JOIN an toàn và hiệu quả – Tránh tích Đề-các và tối ưu hóa phép kết nối.