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

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õ WHERE và HAVING và 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 HAVING khô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:

code
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:

code
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 đề:

code
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:

code
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:

code
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ỗi

Lý 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:

code
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:

code
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:

code
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:

code
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:

code
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:

code
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:

code
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 đề:

code
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

  1. 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.
  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.
  3. Đếm số đơn theo status nhưng chỉ giữ trạng thái có ít nhất hai đơn. Kết quả: completed 13, pending 2 — cancelled bị loại.
  4. Viết truy vấn trong đó WHERE lọc đơn completed và HAVING giữ 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ứngNguyên nhânCách sửa
aggregate functions are not allowed in WHEREDùng hàm tổng hợp trong WHEREChuyển sang HAVING
column "..." does not exist trong HAVINGDùng bí danh SELECTLặ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 BYChỉ đặt ; ở cuối câu
HAVING lọc mất nhóm không mong muốnNgưỡng đặt trên dòng thay vì nhómKiểm tra phép đếm dùng COUNT(*) hay COUNT(DISTINCT ...)
Kết quả khác khi thêm WHEREWHERE chạy trước, làm thay đổi số dòng của mỗi nhómXá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

  • WHERE lọc dòng trước khi gom; HAVING lọc nhóm sau khi gom.
  • Điều kiện có hàm tổng hợp thì phải nằm trong HAVING.
  • HAVING nhận nhiều điều kiện nối bằng AND/OR và cả COUNT(DISTINCT ...).
  • Bí danh SELECT không dùng được trong HAVING; phải lặp biểu thức.
  • HAVING không có GROUP BY coi toàn bộ kết quả là một nhóm.

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