Bạn cần tổng số đơn đã thanh toán nhưng dashboard luôn trả về tổng số đơn. Một lỗi dễ viết là COUNT(CASE WHEN status = 'paid' THEN 1 ELSE 0 END). Nhìn như đang đếm các số 1, nhưng COUNT không có quy tắc như vậy: số 0 vẫn là một giá trị không NULL và vẫn được tính.
Bài cập nhật ngày 08/11/2026, chạy các truy vấn bằng SQLite 3.54.0 tại máy và đối chiếu thêm tài liệu PostgreSQL 17. Chưa chạy PostgreSQL server, ORM hoặc benchmark. Mục tiêu là làm đúng nghĩa của chỉ số trước khi bàn đến tối ưu truy vấn.
Chuẩn bị một bộ dữ liệu có cả trường hợp không khớp
Ví dụ chỉ dùng bảng đơn hàng độc lập, không có JOIN nên mỗi dòng đại diện đúng một đơn. Trong SQLite session mới, chạy:
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
status TEXT
);
INSERT INTO orders (id, status) VALUES
(1, 'paid'), (2, 'pending'), (3, 'paid'),
(4, 'failed'), (5, NULL);
SELECT
COUNT(*) AS total,
COUNT(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) AS wrong_paid,
COUNT(CASE WHEN status = 'paid' THEN 1 END) AS paid,
SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) AS paid_sum
FROM orders;
Kết quả là total=5, wrong_paid=5, paid=2, paid_sum=2. Hãy nhìn năm giá trị trung gian của biểu thức sai: 1, 0, 1, 0, 0. COUNT nhận đủ năm giá trị không NULL nên trả 5. Truy vấn vẫn hợp lệ về cú pháp, vì vậy chỉ kiểm tra query có chạy thành công là chưa đủ.
COUNT cần NULL để bỏ một dòng
Theo SQLite aggregate functions, COUNT(X) tính các giá trị X không NULL. Khi CASE không có nhánh ELSE và điều kiện không đúng, kết quả là NULL. PostgreSQL CASE cũng mô tả quy tắc mặc định ấy.
Trong ví dụ, COUNT(CASE ... THEN 1 END) nhận 1, NULL, 1, NULL, NULL, nên chỉ có hai giá trị được đếm. Bạn cũng có thể viết ELSE NULL cho rõ ý. Dùng SUM lại là hợp đồng khác: cộng các số 1 và 0 để ra số lượng.
Tính nhiều chỉ số trên cùng tập đơn
SELECT
COUNT(*) AS total,
COUNT(CASE WHEN status = 'paid' THEN 1 END) AS paid,
COUNT(CASE WHEN status = 'pending' THEN 1 END) AS pending,
COUNT(CASE WHEN status IS NULL THEN 1 END) AS unknown,
COUNT(*) FILTER (WHERE status = 'failed') AS failed
FROM orders;
Kết quả theo thứ tự là 5, 2, 1, 1, 1. FILTER diễn đạt trực tiếp điều kiện dành riêng cho một aggregate và được SQLite hỗ trợ. Nếu backend dùng engine hoặc phiên bản khác, kiểm tra hỗ trợ cú pháp tại đó trước khi chuyển query; không suy rộng từ lần chạy SQLite.
Các nhánh trong ví dụ loại trừ nhau theo một cột trạng thái. Với chỉ số chồng lấn như “đã thanh toán” và “giá trị cao”, tổng số đếm có thể lớn hơn total. Đó có thể là thiết kế đúng, không nhất thiết là lỗi SQL; cần ghi định nghĩa chỉ số ngay cạnh truy vấn.
Đừng bỏ quên tập đầu vào rỗng
SELECT
COUNT(CASE WHEN status = 'paid' THEN 1 END) AS paid_count,
SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) AS paid_sum,
COALESCE(SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END), 0) AS paid_zero
FROM orders
WHERE id < 0;
Kết quả là 0, NULL, 0. PostgreSQL aggregate functions cũng phân biệt COUNT với SUM khi không có dòng. Việc đã viết ELSE 0 không tạo ra một dòng đầu vào mới cho SUM. Nếu API cam kết trả số nguyên không âm, quyết định rõ có chuyển NULL thành 0 ở query hay tại lớp chuyển đổi dữ liệu.
Checklist khi đưa vào dashboard
Kiểm tra riêng bốn tình huống: có hai đơn paid, không có đơn paid nhưng vẫn có đơn khác, tất cả status đều NULL và hoàn toàn không có đơn. Với mỗi tình huống, ghi số mong đợi trước khi chạy query. Nếu chỉ kiểm tra dữ liệu toàn paid, biểu thức sai ở đầu bài vẫn có thể cho kết quả giống biểu thức đúng.
Khi thêm JOIN sau này, hãy kiểm tra lại đơn vị đếm vì một đơn có thể sinh nhiều dòng. Ví dụ hiện tại chỉ chứng minh cách aggregate xử lý CASE, không chứng minh query mở rộng đã đếm đúng thực thể hay sử dụng index hiệu quả.
Đối với COUNT có điều kiện, câu hỏi hữu ích khi review là: biểu thức trả gì ở dòng không khớp? Trả NULL thì COUNT bỏ qua; trả 0 thì COUNT vẫn tính.

