Menu
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Đang tải...

WHERE chuyên sâu: operators, BETWEEN, IN, NULL handling

SQL and Data Analysis cho BA Bài 7/60

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ĩaVí dụ
=bằngstatus = 'active'
<> hoặc !=khácstatus <> 'cancelled'
<, >nhỏ hơn, lớn hơntotal_amount > 500000
<=, >=nhỏ hơn/bằng, lớn hơn/bằngage >= 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')
LIKEkhớp mẫu chuỗiname LIKE 'Nguyễn%'
IS NULL / IS NOT NULLkiểm tra giá trị rỗngphone IS NULL
AND, OR, NOTkết hợp/phủ định điều kiệna AND b, NOT (a OR b)
Lưu ý nhỏ nhưng quan trọng: <> 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 blacklistdù chỉ một giá trị customer_idNULL, 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_atTIMESTAMP, 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_idNULL (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ố → >, <, >=, <=.
Bước 3 — Xác định cột nào có thể chứa NULL. Với mỗi cột trong điều kiện, hỏi: "Cột này có cho phép trống không?" Nếu có và bạn đang dùng <>, 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ó statusNULL, 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 BETWEEN cho 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ó statusNULL.
  • Lấy danh sách đơn không sử dụng mã giảm giá (cột coupon_code trố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ẫy NOT IN với NULL.
  • Câu tư duy: giải thích bằng lời tại sao WHERE total_amount <> 500000 lại không trả về các hàng mà total_amountNULL, và viết lại điều kiện để bao gồm cả chúng.
Gợi ý đáp án câu 3:

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 logic AND/OR/NOT.
  • BETWEEN bao 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ế.
  • IN thay thế gọn gàng cho chuỗi OR dài; nhưng NOT IN cực kỳ nguy hiểm khi danh sách chứa NULL.
  • NULL nghĩa là "không biết", không phải 0 hay rỗng. Luôn dùng IS 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 AND với OR, luôn dùng ngoặc đơn để diễn đạt đúng ý.
Ba tình huống thực tế — báo cáo doanh thu thiếu ngày cuối, danh sách email rỗng, và danh sách tài xế bị mất sạch vì 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.