Báo cáo có ba loại nguồn đăng ký hiển thị trên giao diện, nhưng truy vấn COUNT(DISTINCT source) chỉ trả về hai. Trước khi thêm một đơn vị vào kết quả, hãy xem nguồn thứ ba có thật sự là một chuỗi hay chỉ là nhãn giao diện dành cho NULL.
Bài này giúp bạn viết rõ hợp đồng của một chỉ số: đếm nguồn đã biết, hay đếm các nhóm kể cả dữ liệu thiếu. Hai câu hỏi hợp lệ nhưng cần hai truy vấn khác nhau.
Điều kiện và phạm vi
Cập nhật 11/11/2026, đối chiếu tài liệu PostgreSQL 17 và SQLite. Các truy vấn bên dưới được chạy bằng SQLite 3.54.0 trên dữ liệu CTE tự tạo, không ghi bảng sản xuất. Chưa chạy PostgreSQL server, ORM hay công cụ BI. Kết quả fixture không chứng minh chi phí thực thi trên bảng lớn.
Theo tài liệu aggregate, COUNT(*) đếm dòng, COUNT(expression) bỏ NULL, còn COUNT(DISTINCT expression) đếm các giá trị khác nhau không phải NULL. Cách đặt alias nên phản ánh đúng điều bạn đang đo.
1. Đặt bốn phép đếm cạnh nhau
Fixture có năm sự kiện: hai từ web, một từ app và hai chưa có nguồn. Chuỗi web lặp lại là cùng một nguồn; NULL biểu diễn giá trị chưa biết trong bài này.
WITH events(source) AS (
VALUES ('web'), ('web'), ('app'), (NULL), (NULL)
)
SELECT COUNT(*) AS total_rows,
COUNT(source) AS known_source_rows,
COUNT(DISTINCT source) AS distinct_known_sources,
COUNT(*) - COUNT(source) AS missing_source_rows
FROM events;
Kết quả fixture là 5, 3, 2, 2. Ba dòng có nguồn không đồng nghĩa ba nguồn khác nhau. Hai dòng thiếu cũng không làm COUNT(DISTINCT source) tăng lên.
Khi review báo cáo, hãy hỏi một người khác đọc alias mà chưa xem SQL. Nếu họ hiểu distinct_known_sources là số nguồn đã có giá trị, tên đó đã giảm một điểm mơ hồ. Nếu sản phẩm gọi đây là “tổng nhóm nguồn”, cần kiểm tra lại định nghĩa trước khi đưa số lên dashboard.
2. Đếm nhóm, có bao gồm NULL
Giả sử giao diện cần một nhóm “Chưa rõ nguồn”. Có thể lấy các nhóm bằng một truy vấn con, sau đó đếm dòng của kết quả đó.
WITH events(source) AS (
VALUES ('web'), ('web'), ('app'), (NULL), (NULL)
), source_groups AS (
SELECT DISTINCT source FROM events
)
SELECT COUNT(*) AS groups_including_missing
FROM source_groups;
Fixture trả về 3. Truy vấn con có web, app và một dòng NULL; lớp ngoài dùng COUNT(*), nên dòng NULL cũng được tính. Không có thao tác đổi NULL thành chuỗi trong dữ liệu gốc.
Để giao diện dễ kiểm tra, bạn có thể tạm xem từng nhóm và số dòng bên trong. Ở fixture này, web có hai dòng, app một dòng, NULL hai dòng. Tổng kích thước các nhóm phải bằng năm. Đây là phép đối chiếu đơn giản giúp phát hiện điều kiện lọc bị đặt sai vị trí.
3. Tránh nhãn giả trùng với dữ liệu thật
Một cách viết ngắn thường gặp là thay NULL bằng một chuỗi rồi đếm distinct. Vấn đề xuất hiện khi chuỗi đó cũng có thể được gửi lên từ hệ thống.
WITH events(source) AS (
VALUES ('unknown'), (NULL)
)
SELECT COUNT(DISTINCT COALESCE(source, 'unknown')) AS merged_groups,
(SELECT COUNT(*) FROM (
SELECT DISTINCT source FROM events
)) AS separate_groups
FROM events;
Kết quả fixture là 1, 2: nhãn thay thế đã gộp hai trường hợp khác nhau. Nếu hợp đồng sản phẩm chủ ý xem hai trường hợp là một thì kết quả có thể phù hợp; nếu không, cách viết ngắn đã làm mất thông tin.
Kiểm tra trường hợp biên
Thêm ba fixture trước khi dùng cho báo cáo thật: toàn bộ nguồn là NULL, chỉ một nguồn lặp nhiều lần, và tập không có dòng. Với tập rỗng, cả hai phép đếm distinct đã biết và đếm nhóm qua truy vấn con đều bằng 0. Không được cộng 1 vô điều kiện để đại diện cho nhóm thiếu.
Đối với chuỗi, hãy xác định riêng chính sách chữ hoa, khoảng trắng và collation. Bài này chỉ dùng các chuỗi ASCII chính xác; không khẳng định web và WEB sẽ được xem như nhau trên mọi schema. Chuẩn hóa chuỗi là quyết định dữ liệu cần được kiểm thử riêng.
Lỗi thường gặp
Đổi alias nhưng giữ truy vấn cũ không làm thay đổi ý nghĩa phép đếm. Tương tự, hiển thị NULL thành “Khác” trên frontend không tự biến nó thành một giá trị khác NULL trong database.
Với một truy vấn có JOIN, cần kiểm tra số dòng đầu vào trước aggregate. Fixture hiện tại không có JOIN, không có phân trang và không đánh giá index. Nếu số dòng tăng sau JOIN, hãy giải quyết quan hệ đó trước khi dùng DISTINCT để che kết quả bất ngờ.
Kết luận
Đếm giá trị đã biết và đếm nhóm bao gồm dữ liệu thiếu là hai chỉ số riêng. Viết rõ định nghĩa, giữ fixture có NULL, rồi chọn phép đếm tương ứng.

