DHDataHọc
Lý thuyếtBài 17/306 phút đọc+10 XP

Đếm dòng với COUNT

Bạn sẽ học được

  • Dùng COUNT(*), COUNT(cot) và COUNT(DISTINCT cot) đúng mục đích.
  • Hiểu vì sao COUNT(cot) bỏ qua NULL còn COUNT(*) thì không.
  • Đặt bí danh cho cột đếm để kết quả dễ đọc.
  • Biết hàm tổng hợp vẫn trả một dòng khi bảng rỗng.
  • Tránh lỗi đếm sai khi kết hợp LEFT JOIN.

Khái niệm cốt lõi

COUNT là hàm tổng hợp đầu tiên và dùng nhiều nhất. Nó nhận nhiều dòng và trả về một con số.

Ba cách viết, ba ý nghĩa khác nhau:

Cách viếtĐếm gìDùng khi nào
COUNT(*)Số dòngĐếm bản ghi, kể cả dòng có NULL
COUNT(cot)Số dòng có cot khác NULLĐếm dữ liệu thực có
COUNT(DISTINCT cot)Số giá trị khác nhau, bỏ NULLĐếm danh mục, thành phố, khách duy nhất

Sự khác biệt giữa COUNT(*) và COUNT(cot) là nguồn gốc của rất nhiều báo cáo sai. Ví dụ bảng customers có 12 dòng, trong đó hai khách không có email:

code
SELECT COUNT(*) AS so_khach,
       COUNT(email) AS so_co_email,
       COUNT(DISTINCT city) AS so_thanh_pho
FROM customers;

Kết quả một dòng: so_khach = 12, so_co_email = 10, so_thanh_pho = 5. Ba con số trả lời ba câu hỏi khác nhau. Nếu báo cáo cần "số khách đã đăng ký nhận email", dùng cột thứ hai; nếu cần "số khách trong hệ thống", dùng cột thứ nhất. Đổi cột là đổi định nghĩa, không phải đổi cách trình bày.

COUNT(*) không quan tâm giá trị các cột, nó chỉ đếm dòng. Vì vậy COUNT(1), COUNT('x') cho cùng kết quả. Đôi khi bạn thấy COUNT(1) trong tài liệu cũ với lời giải thích "nhanh hơn"; điều đó không đúng trên PostgreSQL hiện đại — optimizer xử lý cả hai như nhau. Hãy dùng COUNT(*) vì nó diễn đạt đúng ý: đếm dòng.

Hàm tổng hợp luôn trả về một dòng, kể cả khi không có dòng nào thỏa điều kiện:

code
SELECT COUNT(*) AS so_dong FROM orders WHERE status = 'refunded';

Kết quả là một dòng với giá trị 0, không phải bảng rỗng. Điều này khác với SELECT ... WHERE thông thường: không có dòng phù hợp thì trả 0 dòng. Với SUM và AVG, bảng rỗng cho NULL, không phải 0 — cần nhớ khi hiển thị báo cáo.

COUNT(*) có thể kết hợp với WHERE để đếm theo điều kiện:

code
SELECT COUNT(*) AS so_don_huy
FROM orders
WHERE status = 'cancelled';

Một dòng, giá trị 1.

Bí danh cột đếm không bắt buộc, nhưng nên đặt. Không có bí danh, tên cột là count — vô nghĩa trong báo cáo và dễ trùng khi có nhiều hàm tổng hợp. Dùng tên mô tả: so_don, so_khach, total. Trong bài luyện, đề có thể yêu cầu đúng tên cột, nên đọc kỹ.

Một bẫy quan trọng: đếm sau LEFT JOIN. Giả sử bạn muốn đếm số đơn của mỗi khách, kể cả khách chưa có đơn:

code
SELECT c.name, COUNT(*) AS so_don
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
GROUP BY c.name;

Với khách chưa có đơn, LEFT JOIN tạo một dòng có các cột bên phải là NULL; COUNT(*) đếm dòng đó là 1, không phải 0. Muốn đếm đúng, dùng COUNT(o.id) — nó bỏ qua NULL:

code
SELECT c.name, COUNT(o.id) AS so_don
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
GROUP BY c.name;

Đây là lỗi kinh điển khi làm báo cáo "khách chưa mua hàng". Nó chỉ xuất hiện khi có LEFT JOIN, nên hãy ghi nhớ: đếm cột của bảng bên phải, không đếm *.

Ví dụ chạy được

Đếm tổng số đơn:

code
SELECT COUNT(*) AS tong_don FROM orders;

Kết quả: 16.

Đếm đơn đã hoàn tất:

code
SELECT COUNT(*) AS don_hoan_tat
FROM orders
WHERE status = 'completed';

Kết quả: 13. Hai đơn pending và một đơn cancelled tạo thành 16.

Đếm thành phố khác nhau:

code
SELECT COUNT(DISTINCT city) AS so_thanh_pho FROM customers;

Kết quả: 5. Danh sách gồm Cần Thơ, Hà Nội, Hải Phòng, TP. Hồ Chí Minh, Đà Nẵng.

Đếm khách có email:

code
SELECT COUNT(email) AS co_email,
       COUNT(*) - COUNT(email) AS khong_email
FROM customers;

Kết quả: co_email = 10, khong_email = 2. Phép trừ này hoạt động vì COUNT(*) đếm mọi dòng còn COUNT(email) bỏ NULL. Đây là cách nhanh để đếm dữ liệu thiếu mà không cần thêm truy vấn.

Đếm theo điều kiện phức tạp:

code
SELECT COUNT(*) AS khach_ha_noi
FROM customers
WHERE city = 'Hà Nội';

Kết quả: 3.

Một ví dụ cho thấy bảng rỗng vẫn trả một dòng:

code
SELECT COUNT(*) AS so_dong,
       SUM(price) AS tong_gia
FROM products
WHERE price > 5000000;

Kết quả: so_dong = 0, tong_gia = NULL. Không sản phẩm nào vượt 5 triệu. COUNT trả 0, SUM trả NULL vì không có giá trị nào để cộng. Khi hiển thị, dùng COALESCE(SUM(price), 0) nếu muốn thấy số 0 thay vì ô trống.

Cuối cùng, đếm theo nhóm — xem trước chương:

code
SELECT status, COUNT(*) AS so_don
FROM orders
GROUP BY status
ORDER BY status;

Ba dòng: cancelled 1, completed 13, pending 2. Đây là cách đếm phân bố, và là nội dung chính của bài GROUP BY ở cuối chương.

Thực hành

  1. Đếm số sản phẩm, số sản phẩm còn hàng (stock > 0), số sản phẩm hết hàng. Ba số phải cộng lại bằng 9.
  2. Đếm số khách theo từng segment bằng ba truy vấn riêng. Tổng phải bằng 12. Sau đó nghĩ cách viết gọn bằng GROUP BY.
  3. Đếm số thành phố khác nhau và số khách có email. So sánh với tổng số khách và giải thích chênh lệch.
  4. Viết truy vấn trả về số đơn pending và số đơn cancelled trong cùng một dòng bằng hai lần COUNT(*) FILTER (WHERE ...) — cú pháp nâng cao, chỉ cần thử và ghi lại kết quả.

Lỗi thường gặp

Triệu chứngNguyên nhânCách sửa
Đếm ra 0 dù có dữ liệuĐiều kiện WHERE sai hoặc so sánh NULLKiểm tra điều kiện, dùng IS NULL
Đếm thiếu so với số dòngDùng COUNT(cot) trên cột có NULLDùng COUNT(*) nếu muốn đếm dòng
Khách chưa mua vẫn được đếm là 1LEFT JOIN + COUNT(*)Dùng COUNT(o.id)
SUM trả NULL thay vì 0Không có dòng thỏa điều kiệnBọc COALESCE(..., 0) khi hiển thị
Tên cột kết quả khó đọcKhông đặt bí danhThêm AS so_dong

Lỗi thứ ba đặc biệt hay gặp trong báo cáo khách hàng, vì nó chỉ xuất hiện ở nhóm "không có dữ liệu liên quan". Cách kiểm tra: so sánh tổng số đơn đếm được với COUNT(*) trên bảng orders. Nếu tổng lớn hơn, bạn đang đếm nhầm các dòng NULL của LEFT JOIN.

Tóm tắt

  • COUNT(*) đếm dòng; COUNT(cot) bỏ qua NULL; COUNT(DISTINCT cot) đếm giá trị khác nhau.
  • Hàm tổng hợp trả một dòng kể cả khi không có dữ liệu; COUNT cho 0, SUM/AVG cho NULL.
  • Đặt bí danh cho cột đếm và dùng tên có nghĩa.
  • Sau LEFT JOIN, đếm cột của bảng phải để tránh đếm dòng rỗng.
  • COUNT kết hợp GROUP BY cho phân bố theo nhóm.

Dùng ← → để chuyển bài