Lọc nhóm với HAVING
Bạn sẽ học được
- Lọc nhóm theo kết quả tổng hợp bằng
HAVING. - Phân biệt rõ
WHEREvàHAVINGvà biết khi nào dùng cái nào. - Kết hợp nhiều điều kiện trên nhóm và sắp xếp theo kết quả tổng hợp.
- Dùng
HAVINGkhông cóGROUP BYđể lọc một con số toàn cục. - Tránh các lỗi do đặt sai mệnh đề hoặc dùng bí danh.
Khái niệm cốt lõi
WHERE lọc dòng trước khi gom nhóm. HAVING lọc nhóm sau khi đã gom và tính tổng hợp. Câu hỏi quyết định cách chọn rất đơn giản: điều kiện có nhắc đến hàm tổng hợp (COUNT, SUM, AVG…) hay không? Nếu có, đó là HAVING.
Thành phố có từ hai khách trở lên:
SELECT city, COUNT(*) AS so_khach
FROM customers
GROUP BY city
HAVING COUNT(*) >= 2
ORDER BY so_khach DESC;Bốn dòng: TP. Hồ Chí Minh 4, Hà Nội 3, Đà Nẵng 2, Cần Thơ 2. Hải Phòng bị loại vì chỉ có một khách. COUNT(*) ở đây được tính cho từng nhóm, sau đó HAVING kiểm tra và giữ nhóm đạt ngưỡng.
Đối chiếu với truy vấn dùng WHERE để lọc dòng:
SELECT city, COUNT(*) AS so_khach
FROM customers
WHERE segment = 'retail'
GROUP BY city
ORDER BY city;Hai câu nhìn giống nhau nhưng khác hoàn toàn về ý nghĩa. Câu thứ hai bỏ khách vip và wholesale ra khỏi phép đếm, rồi đếm tất cả thành phố còn lại — kể cả thành phố chỉ còn một khách. Câu thứ nhất giữ mọi khách, đếm theo thành phố, rồi loại thành phố ít khách. Muốn lọc cả hai cách, kết hợp cả hai mệnh đề:
SELECT city, COUNT(*) AS so_khach_retail
FROM customers
WHERE segment = 'retail'
GROUP BY city
HAVING COUNT(*) >= 2
ORDER BY so_khach_retail DESC;Kết quả chỉ còn TP. Hồ Chí Minh (3) và Đà Nẵng (2). Đọc theo thứ tự thực thi: lọc khách retail → gom theo thành phố → bỏ thành phố có dưới hai khách retail. Mỗi mệnh đề làm đúng một việc.
HAVING nhận mọi biểu thức dùng được trong SELECT, kể cả nhiều điều kiện:
SELECT o.customer_id,
COUNT(*) AS so_don,
SUM(oi.quantity * oi.unit_price) AS doanh_thu
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
GROUP BY o.customer_id
HAVING COUNT(DISTINCT o.id) >= 2
AND SUM(oi.quantity * oi.unit_price) > 5000000
ORDER BY doanh_thu DESC;Hai điều kiện kiểm tra hai khía cạnh: khách phải có ít nhất hai đơn và doanh thu trên năm triệu. Chỉ những nhóm thỏa cả hai mới xuất hiện.
Một điểm quan trọng: bí danh SELECT không dùng được trong HAVING. Câu này báo lỗi column "doanh_thu" does not exist:
SELECT o.customer_id, SUM(oi.quantity * oi.unit_price) AS doanh_thu
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
GROUP BY o.customer_id
HAVING doanh_thu > 5000000; -- lỗiLý do nằm ở thứ tự thực thi: HAVING chạy trước SELECT, nên bí danh chưa tồn tại. Hãy lặp lại biểu thức trong HAVING, hoặc bọc truy vấn trong một lớp con rồi lọc ở lớp ngoài — cách thứ hai hữu ích khi biểu thức dài.
HAVING không bắt buộc phải có GROUP BY. Khi đó toàn bộ kết quả được coi là một nhóm duy nhất:
SELECT COUNT(*) AS so_don
FROM orders
HAVING COUNT(*) > 10;Nếu bảng có hơn 10 đơn, trả một dòng 16; nếu không, trả không dòng nào. Đây là cách dùng hiếm nhưng hợp lệ, thỉnh thoảng gặp trong các câu kiểm tra dữ liệu.
Cuối cùng, ORDER BY sau HAVING sắp xếp các nhóm còn lại. Có thể sắp theo bí danh vì ORDER BY chạy sau SELECT:
ORDER BY doanh_thu DESCĐây là lý do bạn thường thấy cùng một biểu thức xuất hiện ở cả SELECT và HAVING, nhưng chỉ ở SELECT và ORDER BY là bí danh.
Ví dụ chạy được
Khách có từ hai đơn trở lên:
SELECT c.name, COUNT(o.id) AS so_don
FROM customers c
JOIN orders o ON o.customer_id = c.id
GROUP BY c.name
HAVING COUNT(o.id) >= 2
ORDER BY so_don DESC, c.name;Bốn dòng: Bình Trần 3; An Nguyễn 2; Chi Lê 2; Giang Đặng 2. Dùng JOIN thay LEFT JOIN nên Hà Bùi (0 đơn) không xuất hiện ngay từ đầu — HAVING không cần loại cô ấy.
Khách có doanh thu trên năm triệu:
SELECT c.name, SUM(oi.quantity * oi.unit_price) AS doanh_thu
FROM customers c
JOIN orders o ON o.customer_id = c.id
JOIN order_items oi ON oi.order_id = o.id
GROUP BY c.name
HAVING SUM(oi.quantity * oi.unit_price) > 5000000
ORDER BY doanh_thu DESC;Hai dòng: Bình Trần 6.750.000 và Giang Đặng 5.490.000. Hai người kế tiếp là Chi Lê 4.270.000 và Minh Dương 4.030.000 — sát ngưỡng, nên hãy thử đổi ngưỡng thành > 4000000 để thấy danh sách mở rộng thêm hai dòng.
Nhóm sản phẩm có giá trung bình trên 500.000:
SELECT category,
COUNT(*) AS so_san_pham,
AVG(price) AS gia_trung_binh
FROM products
GROUP BY category
HAVING AVG(price) > 500000;
ORDER BY gia_trung_binh DESC;Lưu ý câu trên có lỗi cố ý: dấu chấm phẩy đặt sau HAVING khiến ORDER BY thành câu lệnh riêng và báo lỗi. Viết đúng:
SELECT category,
COUNT(*) AS so_san_pham,
AVG(price) AS gia_trung_binh
FROM products
GROUP BY category
HAVING AVG(price) > 500000
ORDER BY gia_trung_binh DESC;Hai nhóm còn lại: Điện tử 863.333,33 (3 sản phẩm) và Gia dụng 672.500 (4 sản phẩm). Gia dụng có giá trung bình 672.500 vì gồm Nồi chiên 1.690.000 kéo lên và ba sản phẩm rẻ hơn ở dưới.
Đếm khách chưa có email theo thành phố, giữ thành phố có ít nhất hai khách:
SELECT city, COUNT(*) AS so_khach_thieu_email
FROM customers
WHERE email IS NULL
GROUP BY city
HAVING COUNT(*) >= 1
ORDER BY city;Kết quả hai dòng: Hà Nội 1 (Dũng Phạm) và TP. Hồ Chí Minh 1 (Inh Đỗ). Hai khách thiếu email nằm ở hai thành phố khác nhau, nên mỗi nhóm chỉ có một người. Truy vấn này cũng cho thấy WHERE email IS NULL lọc trước khi gom, nên COUNT(*) chỉ đếm khách thiếu email, không đếm toàn bộ khách của thành phố.
Lọc kết hợp hai mệnh đề:
SELECT city, COUNT(*) AS so_don_hoan_tat
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.status = 'completed'
GROUP BY city
HAVING COUNT(*) >= 2
ORDER BY so_don_hoan_tat DESC;Kết quả gồm các thành phố có ít nhất hai đơn hoàn tất. Đây là mẫu bạn sẽ dùng khi làm báo cáo theo khu vực: lọc trạng thái ở WHERE, lọc độ lớn nhóm ở HAVING.
Thực hành
- Tìm thành phố có từ hai khách trở lên. Đáp án: TP. Hồ Chí Minh 4, Hà Nội 3, Đà Nẵng 2, Cần Thơ 2.
- Tìm khách có tổng doanh thu trên bốn triệu. Danh sách: Bình Trần, Giang Đặng, Chi Lê, Minh Dương.
- Đếm số đơn theo
statusnhưng chỉ giữ trạng thái có ít nhất hai đơn. Kết quả: completed 13, pending 2 — cancelled bị loại. - Viết truy vấn trong đó
WHERElọc đơncompletedvàHAVINGgiữ khách có trên hai đơn hoàn tất. Giải thích bằng lời thứ tự hai bước lọc.
Lỗi thường gặp
| Triệu chứng | Nguyên nhân | Cách sửa |
|---|---|---|
aggregate functions are not allowed in WHERE | Dùng hàm tổng hợp trong WHERE | Chuyển sang HAVING |
column "..." does not exist trong HAVING | Dùng bí danh SELECT | Lặp lại biểu thức hoặc bọc truy vấn con |
syntax error at or near "ORDER" | Đặt dấu ; trước ORDER BY | Chỉ đặt ; ở cuối câu |
HAVING lọc mất nhóm không mong muốn | Ngưỡng đặt trên dòng thay vì nhóm | Kiểm tra phép đếm dùng COUNT(*) hay COUNT(DISTINCT ...) |
Kết quả khác khi thêm WHERE | WHERE chạy trước, làm thay đổi số dòng của mỗi nhóm | Xác định rõ muốn lọc dòng hay lọc nhóm |
Lỗi áp chót xuất hiện khi nối bảng: COUNT(*) đếm dòng nối (một đơn nhiều món), nên HAVING COUNT(*) >= 2 có thể giữ một khách chỉ có một đơn nhưng đơn đó có hai món. Muốn đếm đơn, dùng COUNT(DISTINCT o.id). Luôn hỏi: con số trong HAVING đang đếm cái gì?
Tóm tắt
WHERElọc dòng trước khi gom;HAVINGlọc nhóm sau khi gom.- Điều kiện có hàm tổng hợp thì phải nằm trong
HAVING. HAVINGnhận nhiều điều kiện nối bằngAND/ORvà cảCOUNT(DISTINCT ...).- Bí danh
SELECTkhông dùng được trongHAVING; phải lặp biểu thức. HAVINGkhông cóGROUP BYcoi toàn bộ kết quả là một nhóm.
Use ← → to move between lessons