Mở đầu — vì sao bài này quan trọng
Nếu bạn từng viết một câu truy vấn WHERE tưởng chừng đúng tuyệt đối, nhưng kết quả trả về lại thiếu hàng, dư hàng, hoặc tệ hơn là âm thầm sai lệch mà không báo lỗi gì — thì bài học này chính là dành cho bạn. Trong công việc của một Business Analyst (BA), 80% giá trị bạn tạo ra nằm ở khả năng lọc đúng dữ liệu. Một báo cáo doanh thu sai chỉ vì bạn quên xử lý giá trị NULL, hay một danh sách khách hàng "đang hoạt động" bị thiếu vì dùng nhầm toán tử — đó không chỉ là lỗi kỹ thuật, đó là lỗi gây ra quyết định kinh doanh sai.
Ở Bài 1, bạn đã làm quen với WHERE ở mức cơ bản: lọc theo một điều kiện đơn giản như status = 'active'. Bài 7 này sẽ đào sâu vào kho vũ khí toán tử của WHERE: từ các phép so sánh, khoảng giá trị với BETWEEN, danh sách với IN, cho đến phần "đáng sợ" nhất với người mới — xử lý NULL. Đây là những kiến thức mà một BA dùng hằng ngày, và cũng là nơi sản sinh ra nhiều lỗi âm thầm nhất trong các báo cáo thực tế.
Hãy coi WHERE như chiếc rây lọc. Rây càng tinh, dữ liệu bạn lấy ra càng sạch và đáng tin. Bắt đầu nhé.
Khái niệm cốt lõi
Bảng toán tử so sánh đầy đủ
Mọi câu WHERE đều xoay quanh một biểu thức trả về đúng (TRUE), sai (FALSE) hoặc không xác định (UNKNOWN). Chỉ những hàng cho kết quả TRUE mới được giữ lại. Dưới đây là bộ toán tử bạn cần thuộc:
| Toán tử | Ý nghĩa | Ví dụ |
|---|---|---|
= | bằng | status = 'active' |
<> hoặc != | khác | status <> 'cancelled' |
<, > | nhỏ hơn, lớn hơn | total_amount > 500000 |
<=, >= | nhỏ hơn/bằng, lớn hơn/bằng | age >= 18 |
BETWEEN ... AND ... | nằm trong khoảng (bao gồm 2 đầu mút) | order_date BETWEEN '2026-01-01' AND '2026-01-31' |
IN (...) | thuộc một danh sách giá trị | city IN ('Hà Nội', 'Đà Nẵng', 'TP.HCM') |
LIKE | khớp mẫu chuỗi | name LIKE 'Nguyễn%' |
IS NULL / IS NOT NULL | kiểm tra giá trị rỗng | phone IS NULL |
AND, OR, NOT | kết hợp/phủ định điều kiện | a AND b, NOT (a OR b) |
<> là chuẩn SQL, != là biến thể được hầu hết hệ quản trị (MySQL, PostgreSQL) chấp nhận. Hai cái tương đương, nhưng nếu muốn an toàn tối đa cho tính di động, hãy dùng <>.BETWEEN — gọn nhưng có cái bẫy
BETWEEN a AND b là cách viết ngắn cho >= a AND <= b. Điểm cần khắc cốt ghi tâm: BETWEEN bao gồm cả hai đầu mút. Tức là price BETWEEN 100 AND 200 sẽ lấy cả 100 và cả 200.
Cái bẫy lớn nhất nằm ở dữ liệu ngày giờ (datetime). Giả sử cột created_at lưu cả ngày lẫn giờ (kiểu TIMESTAMP), thì:
WHERE created_at BETWEEN '2026-01-01' AND '2026-01-31'
Câu này âm thầm bỏ sót gần như toàn bộ dữ liệu của ngày 31/01. Vì sao? Vì '2026-01-31' khi không ghi giờ sẽ được hiểu là '2026-01-31 00:00:00'. Một đơn hàng đặt lúc 2026-01-31 14:30:00 sẽ lớn hơn đầu mút trên và bị loại. Đây là lỗi kinh điển khiến báo cáo doanh thu cuối tháng bị thiếu cả ngày cuối cùng.
Cách viết an toàn cho dữ liệu datetime là dùng nửa khoảng mở:
WHERE created_at >= '2026-01-01' AND created_at < '2026-02-01'
IN — thay thế chuỗi OR dài dòng
IN cho phép kiểm tra một cột có thuộc một danh sách giá trị hay không. Thay vì:
WHERE city = 'Hà Nội' OR city = 'Đà Nẵng' OR city = 'TP.HCM'
bạn viết gọn:
WHERE city IN ('Hà Nội', 'Đà Nẵng', 'TP.HCM')
NOT IN làm điều ngược lại — loại bỏ các giá trị trong danh sách. Nhưng cẩn thận: NOT IN kết hợp với NULL là một trong những cái bẫy nguy hiểm nhất, ta sẽ bàn ngay phần dưới.
IN cũng có thể nhận một câu truy vấn con (subquery), ví dụ WHERE customer_id IN (SELECT customer_id FROM vip_list) — nhưng đó là chủ đề của Bài 15, ở đây ta chỉ dùng IN với danh sách giá trị tĩnh.
NULL — "không biết", chứ không phải "rỗng" hay "bằng 0"
Đây là phần quan trọng nhất của bài. NULL trong SQL không phải là số 0, không phải chuỗi rỗng '', không phải FALSE. NULL mang nghĩa "không xác định / không biết". Và chính bản chất "không biết" này tạo ra logic ba trạng thái (three-valued logic): TRUE, FALSE, và UNKNOWN.
Quy tắc vàng: bất kỳ phép so sánh nào với NULL bằng =, <>, <, > đều trả về UNKNOWN, không bao giờ TRUE.
WHERE phone = NULL -- SAI: không bao giờ trả về hàng nào
WHERE phone IS NULL -- ĐÚNG: lấy các hàng có phone rỗng
WHERE phone = NULL không gây lỗi cú pháp, nó chỉ âm thầm trả về 0 hàng — đây là lý do nhiều BA mới vào nghề "mất" dữ liệu mà không hiểu vì sao. Để kiểm tra NULL, bắt buộc dùng IS NULL hoặc IS NOT NULL.
Cái bẫy NOT IN với NULL
Giả sử bạn muốn lấy tất cả khách hàng KHÔNG nằm trong danh sách bị chặn:
WHERE customer_id NOT IN (SELECT customer_id FROM blacklist)
Nếu trong bảng blacklist có dù chỉ một giá trị customer_id là NULL, thì toàn bộ câu truy vấn này trả về 0 hàng — kể cả khi danh sách đó có hàng nghìn khách hợp lệ. Lý do: NOT IN (1, 2, NULL) được dịch thành <> 1 AND <> 2 AND <> NULL, mà <> NULL luôn là UNKNOWN, kéo cả biểu thức xuống UNKNOWN. Cách phòng tránh: thêm WHERE customer_id IS NOT NULL vào subquery, hoặc dùng NOT EXISTS.
Thứ tự ưu tiên AND/OR và dấu ngoặc
AND được tính trước OR, giống như phép nhân trước phép cộng. Vì vậy:
WHERE city = 'Hà Nội' OR city = 'TP.HCM' AND status = 'active'
không có nghĩa là "ở Hà Nội hoặc TP.HCM, và đang active". Nó được hiểu là "ở Hà Nội (bất kể status), HOẶC (ở TP.HCM và active)". Để đúng ý, luôn dùng ngoặc đơn khi trộn AND với OR:
WHERE (city = 'Hà Nội' OR city = 'TP.HCM') AND status = 'active'
Tình huống thực tế
Tình huống 1: Báo cáo doanh thu tháng bị thiếu ngày cuối (sàn TMĐT kiểu Tiki)
Một BA tại một sàn thương mại điện tử được giao làm báo cáo tổng doanh thu tháng 1/2026. Cô viết:
SELECT SUM(total_amount) AS revenue
FROM orders
WHERE created_at BETWEEN '2026-01-01' AND '2026-01-31'
AND status = 'completed';
Con số trả về là 8,7 tỷ đồng. Nhưng team kế toán phản hồi: doanh thu thực phải là 9,1 tỷ. Sai lệch ~400 triệu, đúng bằng doanh số của ngày 31/01 — ngày cao điểm cuối tháng. Nguyên nhân: cột created_at là TIMESTAMP, và BETWEEN ... '2026-01-31' chỉ lấy đến 2026-01-31 00:00:00, bỏ qua mọi đơn trong ngày.
Cô sửa lại thành nửa khoảng mở:
WHERE created_at >= '2026-01-01' AND created_at < '2026-02-01'
Con số khớp với kế toán. Bài học: với dữ liệu datetime, đừng dùng BETWEEN ngày — dùng >= đầu kỳ AND < đầu kỳ kế tiếp. Đây là lỗi gây thất thoát số liệu phổ biến nhất trong báo cáo của BA.
Tình huống 2: Danh sách khách "không có email" bị rỗng (ví ZaloPay/MoMo mô phỏng)
Một BA ở công ty fintech cần lấy danh sách người dùng chưa cập nhật email để team marketing gửi SMS thay vì email:
SELECT user_id, phone FROM users WHERE email = '';
Kết quả: 0 hàng — vô lý, vì rõ ràng nhiều người chưa điền email. Vấn đề là khi người dùng bỏ trống ô email, hệ thống lưu giá trị NULL chứ không lưu chuỗi rỗng ''. Câu email = '' chỉ bắt được chuỗi rỗng thật sự.
Để bao quát cả hai trường hợp (NULL và chuỗi rỗng), cô viết:
SELECT user_id, phone
FROM users
WHERE email IS NULL OR email = '';
Lần này trả về 142.000 người dùng. Bài học: trong dữ liệu thực tế, "thiếu giá trị" có thể tồn tại dưới hai dạng — NULL và chuỗi rỗng. Một BA giỏi luôn hỏi đội kỹ thuật: "Trường này khi trống thì lưu NULL hay lưu rỗng?" trước khi viết điều kiện lọc.
Tình huống 3: Lọc tài xế ngoài danh sách thưởng bị mất sạch (ride-hailing kiểu Be/Grab)
Một BA muốn lấy danh sách tài xế không thuộc nhóm đã nhận thưởng quý này, để gửi chương trình khuyến khích:
SELECT driver_id, full_name
FROM drivers
WHERE driver_id NOT IN (
SELECT driver_id FROM bonus_recipients
);
Kết quả trả về rỗng dù có hơn 12.000 tài xế. Sau khi kiểm tra, hóa ra bảng bonus_recipients có vài dòng lỗi với driver_id là NULL (do quy trình nhập liệu thủ công). Như đã phân tích, chỉ một NULL trong NOT IN làm hỏng toàn bộ kết quả.
Cô sửa bằng cách lọc NULL ngay trong subquery:
SELECT driver_id, full_name
FROM drivers
WHERE driver_id NOT IN (
SELECT driver_id FROM bonus_recipients
WHERE driver_id IS NOT NULL
);
Bài học: mỗi khi dùng NOT IN với một subquery, luôn tự hỏi "cột này có thể chứa NULL không?". Nếu có, hãy lọc NULL ra trước, hoặc chuyển sang dùng NOT EXISTS vốn miễn nhiễm với cái bẫy này.
Hướng dẫn từng bước
Dưới đây là quy trình tư duy khi xây dựng một mệnh đề WHERE chuẩn:
Bước 1 — Liệt kê các điều kiện bằng ngôn ngữ tự nhiên. Ví dụ: "Đơn hàng trong tháng 1/2026, trạng thái đã hoàn tất hoặc đang giao, giá trị trên 200.000đ, thuộc một trong ba thành phố lớn." Viết ra giấy trước khi gõ SQL.
Bước 2 — Chuyển từng điều kiện sang toán tử phù hợp.
- Khoảng ngày → dùng
>= ... AND < ...(không dùng BETWEEN cho datetime). - "hoặc" giữa các giá trị cùng cột → dùng
IN. - so sánh số →
>,<,>=,<=.
<>, NOT IN, hãy xử lý NULL rõ ràng.Bước 4 — Đặt ngoặc khi trộn AND và OR. Bất cứ khi nào có cả hai, bọc nhóm OR trong ngoặc đơn.
Bước 5 — Ráp lại thành câu hoàn chỉnh:
SELECT order_id, total_amount, created_at, city
FROM orders
WHERE created_at >= '2026-01-01'
AND created_at < '2026-02-01'
AND status IN ('completed', 'shipping')
AND total_amount > 200000
AND city IN ('Hà Nội', 'Đà Nẵng', 'TP.HCM');
Bước 6 — Kiểm tra ngược. Chạy thử với một điều kiện đảo ngược (ví dụ đổi > 200000 thành <= 200000) và xem tổng số hàng của hai lần cộng lại có bằng tổng số hàng gốc không. Đây là cách kiểm tra nhanh rằng bộ lọc không "đánh rơi" hàng nào do NULL.
Lỗi thường gặp & mẹo
Lỗi 1 — Dùng = NULL thay vì IS NULL. Không báo lỗi nhưng luôn trả về 0 hàng. Mẹo: hễ thấy chữ "NULL" thì phản xạ phải là IS/IS NOT, không bao giờ là =/<>.
Lỗi 2 — Dùng BETWEEN cho cột TIMESTAMP. Bỏ sót dữ liệu ngày cuối kỳ. Mẹo: ghi nhớ công thức nửa khoảng mở >= đầu kỳ AND < đầu kỳ kế.
Lỗi 3 — NOT IN gặp NULL trong danh sách. Trả về rỗng toàn bộ. Mẹo: luôn thêm IS NOT NULL vào subquery của NOT IN.
Lỗi 4 — Quên rằng <> cũng loại bỏ NULL. Câu WHERE status <> 'cancelled' sẽ không trả về các hàng có status là NULL, vì NULL <> 'cancelled' là UNKNOWN. Nếu bạn muốn lấy cả những hàng status rỗng, phải viết WHERE status <> 'cancelled' OR status IS NULL.
Lỗi 5 — Trộn AND/OR không ngoặc. Cho ra tập kết quả sai âm thầm. Mẹo: cứ có OR cạnh AND là phải có ngoặc.
Mẹo hiệu năng: với cột văn bản, IN ('a', 'b', 'c') thường nhanh hơn và dễ đọc hơn nhiều so với chuỗi OR dài. Ngoài ra, nếu cột được lập chỉ mục (index), việc dùng =, IN, BETWEEN, >, < sẽ tận dụng được index; còn dùng hàm bao quanh cột (như WHERE YEAR(created_at) = 2026) thì thường làm mất tác dụng index — đây là lý do nên viết khoảng ngày trực tiếp thay vì bọc hàm. Chi tiết về index sẽ ở Bài 24.
Mẹo COALESCE để gán giá trị thay thế: khi muốn coi NULL như một giá trị mặc định trong điều kiện, có thể dùng COALESCE(discount, 0) > 0. Tuy nhiên các hàm như COALESCE, NULLIF, IFNULL là trọng tâm của Bài 23, ở đây bạn chỉ cần biết chúng tồn tại như một lựa chọn xử lý NULL.
Bài tập thực hành
Giả sử có bảng orders với các cột: order_id, customer_id, total_amount, status, city, created_at (kiểu TIMESTAMP), coupon_code (có thể NULL).
- Lấy tất cả đơn hàng trong quý 1/2026 (1/1 đến hết 31/3) có giá trị từ 300.000đ đến 1.000.000đ. Viết hai cách: một dùng
BETWEENcho số tiền, và phần ngày dùng nửa khoảng mở.
- Lấy các đơn ở Hà Nội, TP.HCM hoặc Cần Thơ có trạng thái không phải
'cancelled'— và nhớ bao gồm cả những đơn cóstatuslàNULL.
- Lấy danh sách đơn không sử dụng mã giảm giá (cột
coupon_codetrống dưới mọi hình thức — cả NULL lẫn chuỗi rỗng).
- Cho bảng
vip_customers(customer_id)có thể chứa NULL. Viết câu lấy các đơn của khách hàng không thuộc danh sách VIP, đảm bảo không bị dính bẫyNOT INvới NULL.
- Câu tư duy: giải thích bằng lời tại sao
WHERE total_amount <> 500000lại không trả về các hàng màtotal_amountlàNULL, và viết lại điều kiện để bao gồm cả chúng.
SELECT order_id
FROM orders
WHERE coupon_code IS NULL OR coupon_code = '';
Tóm tắt
Mệnh đề WHERE là chiếc rây lọc quyết định độ sạch của mọi báo cáo bạn làm. Qua bài này, hãy ghi nhớ những điểm cốt lõi:
- Nắm vững bộ toán tử:
=,<>,<,>,<=,>=,BETWEEN,IN, và các phép logicAND/OR/NOT. BETWEENbao gồm cả hai đầu mút và là cái bẫy với dữ liệu datetime — hãy ưu tiên>= đầu kỳ AND < đầu kỳ kế.INthay thế gọn gàng cho chuỗiORdài; nhưngNOT INcực kỳ nguy hiểm khi danh sách chứaNULL.NULLnghĩa là "không biết", không phải 0 hay rỗng. Luôn dùngIS NULL/IS NOT NULL, không bao giờ= NULL. Và nhớ rằng<>cũng âm thầm loại bỏ NULL.- Khi trộn
ANDvớiOR, luôn dùng ngoặc đơn để diễn đạt đúng ý.
NOT IN — đều bắt nguồn từ cùng một gốc rễ: hiểu chưa thấu cách SQL xử lý khoảng giá trị và NULL. Khi bạn nắm chắc những điều này, bạn không chỉ viết được câu truy vấn chạy được, mà còn viết được câu truy vấn đúng — và đó mới là điều phân biệt một BA chuyên nghiệp. Ở bài tiếp theo, ta sẽ bước sang LIKE và regex để khai thác sức mạnh khớp mẫu chuỗi.