Đế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ỏ quaNULLcònCOUNT(*)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:
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:
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:
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:
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:
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:
SELECT COUNT(*) AS tong_don FROM orders;Kết quả: 16.
Đếm đơn đã hoàn tất:
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:
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:
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:
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:
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:
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
- Đế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. - Đếm số khách theo từng
segmentbằng ba truy vấn riêng. Tổng phải bằng 12. Sau đó nghĩ cách viết gọn bằngGROUP BY. - Đế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.
- Viết truy vấn trả về số đơn
pendingvà số đơncancelledtrong cùng một dòng bằng hai lầnCOUNT(*) 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ứng | Nguyên nhân | Cách sửa |
|---|---|---|
| Đếm ra 0 dù có dữ liệu | Điều kiện WHERE sai hoặc so sánh NULL | Kiểm tra điều kiện, dùng IS NULL |
| Đếm thiếu so với số dòng | Dùng COUNT(cot) trên cột có NULL | Dùng COUNT(*) nếu muốn đếm dòng |
| Khách chưa mua vẫn được đếm là 1 | LEFT JOIN + COUNT(*) | Dùng COUNT(o.id) |
SUM trả NULL thay vì 0 | Không có dòng thỏa điều kiện | Bọc COALESCE(..., 0) khi hiển thị |
| Tên cột kết quả khó đọc | Không đặt bí danh | Thê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ỏ quaNULL;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;
COUNTcho 0,SUM/AVGchoNULL. - Đặ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. COUNTkết hợpGROUP BYcho phân bố theo nhóm.
Dùng ← → để chuyển bài