+12

Tại sao NULL = NULL lại không đúng?

image.png Bạn đã bao giờ viết một câu query trông có vẻ hoàn hảo, nhưng kết quả trả về lại trống rỗng một cách khó hiểu chưa?

SELECT * FROM users WHERE deleted_at = NULL;

Câu query này chạy thành công, không báo lỗi, nhưng luôn trả về 0 dòng, ngay cả khi trong bảng có hàng trăm user với deleted_at là NULL. Tại sao nhỉ?

Câu trả lời nằm ở một trong những khái niệm gây nhầm lẫn nhất của SQL: NULL. Và đặc biệt là cái sự thật kỳ lạ rằng NULL = NULL lại không trả về TRUE.


Bạn cho chúng mình xin 1 upvote để chúng mình có động lực ra những bài viết thú vị hơn nữa nhé 😄😄😄

Ở đây còn có nhiều điều bất ngờ về SQL hơn nữa: https://sydexa.com/courses/advanced-sql 🤩🤩🤩

Thêm nữa, dưới đây là các hội nhóm mà chúng mình tổng hợp các kiến thức liên quan, bạn cùng tham gia nhé 😍😍😍

Cộng Đồng Backend và System Design Việt Nam: https://www.facebook.com/groups/sydexa

Cộng đồng Frontend: https://www.facebook.com/groups/seniorfrontendvietnam

Kênh TikTok: https://www.tiktok.com/@sydexa.com

Kênh youtube: https://www.youtube.com/@sydexa.official


Vậy NULL thực sự là gì?

image.png Nhiều người mới học SQL hay nghĩ NULL giống như:

  • Số 0
  • Chuỗi rỗng ''
  • Giá trị mặc định
  • "Không có gì"

Tất cả đều sai.

NULL trong SQL có nghĩa là "không biết" (unknown) hoặc "thiếu giá trị" (missing value). Nó không phải là một giá trị cụ thể, nó là sự vắng mặt của giá trị.

Hãy tưởng tượng bạn có một form đăng ký, và một user không điền số điện thoại. Trong database, trường phone của user đó là NULL, không phải vì số điện thoại bằng 0, mà vì chúng ta không biết số điện thoại đó là gì.

Thế tại sao NULL = NULL lại không đúng?

image.png

Đây là phần thú vị. SQL không dùng logic 2 giá trị (TRUE/FALSE) như chúng ta thường nghĩ, mà dùng logic 3 giá trị (Three-Valued Logic):

  • TRUE
  • FALSE
  • UNKNOWN

Khi bạn so sánh NULL = NULL, SQL suy luận như sau:

"Tôi không biết giá trị bên trái là gì. Tôi cũng không biết giá trị bên phải là gì. Vậy làm sao tôi có thể khẳng định được chúng bằng nhau?"

Kết quả là UNKNOWN.

Và trong mệnh đề WHERE, chỉ những dòng có kết quả TRUE mới được trả về. UNKNOWN bị loại bỏ giống như FALSE.

Tương tự, các phép so sánh sau đều trả về UNKNOWN:

NULL = NULL      -- UNKNOWN
NULL != NULL     -- UNKNOWN
NULL = 0         -- UNKNOWN
NULL = ''        -- UNKNOWN
NULL > 100       -- UNKNOWN
NULL + 5         -- NULL (mọi phép toán với NULL đều ra NULL)

Một ví dụ vui để dễ nhớ: Mình hỏi "Tuổi của người X có bằng tuổi của người Y không?" khi cả hai người này bạn đều không biết tuổi nên bạn không thể nói có, cũng không thể nói không. Câu trả lời chỉ có thể là "không biết".

Cách kiểm tra NULL cho đúng

Vì không thể dùng = hay != với NULL, SQL cung cấp hai toán tử đặc biệt:

-- ĐÚNG: Dùng IS NULL
SELECT * FROM users WHERE deleted_at IS NULL;

-- ĐÚNG: Dùng IS NOT NULL
SELECT * FROM users WHERE deleted_at IS NOT NULL;

-- SAI: Sẽ không bao giờ trả về dòng nào
SELECT * FROM users WHERE deleted_at = NULL;

Hãy nhớ kỹ là luôn dùng IS NULLIS NOT NULL khi làm việc với NULL.

Những cái sai lầm phổ biến với NULL

image.png

Thứ nhất là NOT IN với subquery chứa NULL

Đây là một cái sai lầm kinh điển khiến nhiều ae dev phải debug hàng giờ:

-- Bảng orders có một số dòng customer_id là NULL
SELECT * FROM customers
WHERE id NOT IN (SELECT customer_id FROM orders);

Câu query trên trông có vẻ đơn giản, với nội dung là "Lấy ra những khách hàng chưa từng đặt hàng". Nhưng nếu trong ordersbất kỳ dòng nào với customer_id = NULL, câu query sẽ trả về 0 kết quả.

Lý do là NOT IN (1, 2, NULL) thực chất tương đương với:

id != 1 AND id != 2 AND id != NULL

id != NULL luôn cho ra UNKNOWN, khiến toàn bộ biểu thức không bao giờ trả về TRUE.

Sửa lại bằng cách, dùng NOT EXISTS thay vì NOT IN:

SELECT * FROM customers c
WHERE NOT EXISTS (
    SELECT 1 FROM orders o WHERE o.customer_id = c.id
);

Thứ hai là NULL trong hàm aggregate

image.png

Các hàm như COUNT, SUM, AVG xử lý NULL theo cách khác nhau:

-- Giả sử bảng có 100 dòng, trong đó 30 dòng có salary = NULL

SELECT COUNT(*) FROM employees;       -- 100 (đếm tất cả)
SELECT COUNT(salary) FROM employees;  -- 70  (bỏ qua NULL)
SELECT SUM(salary) FROM employees;    -- Tính tổng 70 dòng, bỏ qua NULL
SELECT AVG(salary) FROM employees;    -- Tính trung bình trên tổng của 70 dòng, KHÔNG PHẢI tính trên 100 dòng

Điều này dẫn đến một lỗi rất khó đoán là nếu bạn muốn tính lương trung bình mà coi NULL là 0, thì kết quả AVG(salary) sẽ khác với kết quả mà bạn nghĩ.

Thứ 3 là NULL trong JOIN

Khi JOIN hai bảng trên cột có chứa NULL, các dòng NULL không bao giờ khớp với nhau, kể cả khi cả hai bên đều là NULL:

SELECT * FROM table_a a
JOIN table_b b ON a.code = b.code;
-- Dòng có a.code = NULL sẽ không match với dòng có b.code = NULL

Thứ 4 là NULL trong điều kiện CHECKUNIQUE

Constraint UNIQUE cho phép nhiều dòng có giá trị NULL, vì NULL khác NULL. Điều này có thể gây ngạc nhiên cho bạn, khi bạn nghĩ rằng UNIQUE đảm bảo không trùng lặp.

Cách xử lý NULL trong thực tế

SQL cung cấp một số hàm hữu ích để xử lý NULL:

COALESCE: lấy giá trị non-null đầu tiên

image.png

SELECT
    name,
    COALESCE(phone, email, 'Không có liên lạc') AS contact
FROM users;

Hàm này duyệt từ trái sang phải, trả về giá trị non-null đầu tiên gặp được.

IFNULL / ISNULL / NVL: thay thế NULL bằng giá trị khác

-- MySQL
SELECT IFNULL(salary, 0) FROM employees;

-- SQL Server
SELECT ISNULL(salary, 0) FROM employees;

-- Oracle
SELECT NVL(salary, 0) FROM employees;

-- Chuẩn SQL (chạy mọi nơi)
SELECT COALESCE(salary, 0) FROM employees;

Lời khuyên dành cho bạn là cứ dùng COALESCE cho an toàn, vì nó là chuẩn SQL và hoạt động trên mọi hệ quản trị.

NULLIF thì ngược lại của COALESCE

NULLIF(a, b) trả về NULL nếu a = b, ngược lại trả về a. Rất hữu ích để tránh lỗi chia cho 0:

SELECT revenue / NULLIF(num_orders, 0) AS avg_order_value
FROM sales;

Nếu num_orders = 0, biểu thức trở thành revenue / NULL = NULL thay vì gây lỗi.

Bảng tóm tắt nhanh để nhớ

Tình huống Đúng Sai
Kiểm tra NULL WHERE col IS NULL WHERE col = NULL
Loại trừ với subquery NOT EXISTS NOT IN (nếu có NULL)
Thay thế NULL COALESCE(col, default) Bạn bỏ qua và nghĩ rằng nó không bao giờ là NULL
Tránh chia cho 0 a / NULLIF(b, 0) a / b

Tổng kết lại

NULL là một trong những lý do khiến SQL vừa mạnh mẽ vừa khó chịu. Nó phản ánh chính xác bản chất của dữ liệu thực tế, nơi mà thông tin thường bị thiếu, nhưng đồng thời cũng là nguồn cơn của vô số lỗi mà ta rất khó lường.

Quy tắc vàng để sống chung với NULL: image.png

Lần tới khi câu query của bạn trả về kết quả trống rỗng không lý do, hãy tự hỏi: "Có cột NULL nào đang phá hoại mình không nhỉ?"

Chúc anh em code SQL vui vẻ và ít gặp NULL bug hơn nhé!

Lời nhắn

Bạn cho chúng mình xin 1 upvote để chúng mình có động lực ra những bài viết thú vị hơn nữa nhé 😄😄😄

Ở đây còn có nhiều điều bất ngờ về SQL hơn nữa: https://sydexa.com/courses/advanced-sql 🤩🤩🤩

Thêm nữa, dưới đây là các hội nhóm mà chúng mình tổng hợp các kiến thức liên quan, bạn cùng tham gia nhé 😍😍😍

Cộng Đồng Backend và System Design Việt Nam: https://www.facebook.com/groups/sydexa

Cộng đồng Frontend: https://www.facebook.com/groups/seniorfrontendvietnam

Kênh TikTok: https://www.tiktok.com/@sydexa.com

Kênh youtube: https://www.youtube.com/@sydexa.official



All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí