SQL cho phân tích dữ liệu · Hướng dẫn · 30 tháng 10, 2026 · 12 phút đọc

GROUP BY và HAVING trong SQL: khác nhau giữa WHERE và HAVING, kèm ví dụ

Hành lý trên băng chuyền đi qua một chiếc phễu và được chia thành các nhóm riêng

GROUP BY và HAVING không phải hai lựa chọn thay thế nhau: chúng làm hai việc khác nhau trong cùng một truy vấn. GROUP BY gộp các dòng thành nhóm (mỗi sân bay, mỗi ngày, mỗi hạng ghế một dòng kết quả) để các hàm tổng hợp như COUNT và AVG được tính theo từng nhóm. HAVING lọc các nhóm đó sau khi đã có giá trị tổng hợp. WHERE là mảnh ghép thứ ba: nó lọc từng dòng riêng lẻ trước khi việc nhóm diễn ra. Vậy quy tắc ngắn gọn là: điều kiện trên một cột gốc đặt trong WHERE; điều kiện trên một giá trị tổng hợp (COUNT(*) > 1000, AVG(amount) > 300) đặt trong HAVING.

Mọi ví dụ bên dưới đều chạy trên cơ sở dữ liệu hãng hàng không của khóa SQL trên Nomto: 33.121 chuyến bay giữa 104 sân bay trong mùa hè 2026, cùng đơn đặt chỗ, vé và chặng bay. Tiền tính bằng đô la Mỹ. Bạn có thể dán từng truy vấn vào Sân tập SQL của Nomto và tự kiểm tra kết quả.

GROUP BY làm gì

Khi không có GROUP BY, hàm tổng hợp biến cả bảng thành một dòng. Khi có nó, bạn nhận được một dòng cho mỗi giá trị khác nhau của các cột nhóm.

SELECT status, COUNT(*) AS flights
FROM flights
GROUP BY status
ORDER BY flights DESC;
status flights
Arrived 16707
Scheduled 15383
On Time 518
Cancelled 414
Departed 58
Delayed 41

Sáu trạng thái, sáu dòng. Mỗi dòng là một nhóm, và COUNT(*) được tính riêng trong từng nhóm.

Bạn có thể nhóm theo nhiều cột, và theo biểu thức. Đây là số ghế theo từng mẫu máy bay, kèm số ghế hạng thương gia được đếm có điều kiện trong cùng một lượt:

SELECT aircraft_code,
       COUNT(*) AS seats,
       SUM(CASE WHEN fare_conditions = 'Business' THEN 1 ELSE 0 END) AS business
FROM seats
GROUP BY aircraft_code
ORDER BY seats DESC;
aircraft_code seats business
773 402 30
763 222 30
321 170 28
320 140 20
733 130 12
319 116 20
SU9 97 12
CR2 50 0

(Mẫu thứ chín, CN1, có 12 ghế và không có hạng thương gia.)

HAVING làm gì

HAVING giữ lại hoặc loại bỏ cả nhóm. Nó được tính sau khi đã có giá trị tổng hợp, nên có thể tham chiếu tới chúng.

SELECT departure_airport, COUNT(*) AS departures
FROM flights
WHERE status = 'Arrived'
GROUP BY departure_airport
HAVING COUNT(*) > 1000
ORDER BY departures DESC;
departure_airport departures
DME 1618
SVO 1503

Hãy đọc theo thứ tự mà cơ sở dữ liệu thực hiện: lấy mọi chuyến bay, chỉ giữ những chuyến đã hạ cánh (WHERE), nhóm theo sân bay khởi hành, đếm từng nhóm, chỉ giữ những nhóm có hơn 1.000 dòng (HAVING). Hai sân bay ở Moscow, Domodedovo và Sheremetyevo, vượt qua điều kiện.

HAVING có thể dùng bất kỳ hàm tổng hợp nào, không chỉ hàm bạn đã chọn trong SELECT. Mẫu máy bay nào hoàn toàn không có hạng thương gia?

SELECT aircraft_code, COUNT(*) AS seats
FROM seats
GROUP BY aircraft_code
HAVING SUM(CASE WHEN fare_conditions = 'Business' THEN 1 ELSE 0 END) = 0
ORDER BY seats DESC;
aircraft_code seats
CR2 50
CN1 12

Đó là Bombardier CRJ-200 và Cessna 208 Caravan.

Khác nhau giữa WHERE và HAVING

WHERE HAVING
Lọc gì từng dòng nhóm
Chạy khi nào trước GROUP BY sau GROUP BY và tổng hợp
Dùng được hàm tổng hợp không có
Dùng được cột gốc mọi cột chỉ cột đã nhóm (hoặc bên trong hàm tổng hợp)
Dùng được khi không có GROUP BY có có, toàn bộ kết quả là một nhóm

Hệ quả thực tế: điều kiện trong WHERE thay đổi dữ liệu đi vào hàm tổng hợp; điều kiện trong HAVING thì không. So sánh hai truy vấn sau về giá chặng bay.

Giá trung bình theo hạng ghế, chỉ giữ những chặng có giá trên $300:

SELECT fare_conditions, COUNT(*) AS segments, ROUND(AVG(amount), 2) AS avg_price
FROM ticket_flights
WHERE amount > 300
GROUP BY fare_conditions
ORDER BY avg_price DESC;
fare_conditions segments avg_price
Business 63467 739.73
Economy 90837 500.99
Comfort 6919 474.92

Giá trung bình theo hạng ghế, chỉ giữ những hạng có giá trung bình trên $300:

SELECT fare_conditions, COUNT(*) AS segments, ROUND(AVG(amount), 2) AS avg_price
FROM ticket_flights
GROUP BY fare_conditions
HAVING AVG(amount) > 300
ORDER BY avg_price DESC;
fare_conditions segments avg_price
Business 107642 511.43
Comfort 17291 327.41

Truy vấn thứ nhất trả lời câu hỏi “trong mỗi hạng, một chặng đắt tiền trung bình giá bao nhiêu”; mọi giá trị trung bình đều bị đẩy lên vì các chặng rẻ đã bị loại, và hạng phổ thông (Economy) thậm chí vượt cả hạng Comfort. Truy vấn thứ hai trả lời “hạng nào đắt tính trung bình”; ở đây giá trung bình là con số thật (của Economy là $159,60, nên nó bị loại). Cùng các cột, cùng con số 300, nhưng là hai câu hỏi hoàn toàn khác nhau.

Khi đặt ở đâu cũng được, hãy dùng WHERE

Điều kiện trên một cột đã nhóm về mặt kỹ thuật có thể đặt ở cả hai chỗ:

SELECT departure_airport, COUNT(*) AS departures
FROM flights
WHERE departure_airport IN ('SVO', 'DME', 'VKO')
GROUP BY departure_airport
ORDER BY departures DESC;
departure_airport departures
DME 3217
SVO 2981
VKO 1719

Chuyển điều kiện sang HAVING departure_airport IN ('SVO', 'DME', 'VKO') vẫn trả về đúng ba dòng đó. Nhưng phiên bản HAVING phải nhóm và đếm cả 104 sân bay trước, rồi mới bỏ đi 101 nhóm. Nhiều bộ tối ưu hóa sẽ tự đẩy điều kiện như vậy xuống, nhưng bạn không nên trông chờ vào điều đó; hơn nữa WHERE còn cho người đọc thấy ý định của bạn: những dòng này nằm ngoài phạm vi ngay từ đầu.

Thứ tự SQL thực thi một truy vấn

Bạn viết SELECT trước tiên, nhưng cơ sở dữ liệu xử lý các mệnh đề theo thứ tự logic sau:

  1. FROM và JOIN: tạo tập dòng
  2. WHERE: loại bỏ dòng
  3. GROUP BY: tạo nhóm
  4. tính các hàm tổng hợp cho từng nhóm
  5. HAVING: loại bỏ nhóm
  6. SELECT: tính các cột kết quả và bí danh
  7. DISTINCT
  8. ORDER BY
  9. LIMIT / OFFSET

Gần như mọi lỗi với GROUP BY / HAVING đều suy ra được từ danh sách này:

Các lỗi thường gặp

1. Dùng hàm tổng hợp trong WHERE

SELECT departure_airport, COUNT(*)
FROM flights
WHERE COUNT(*) > 1000
GROUP BY departure_airport;

Truy vấn này báo lỗi. PostgreSQL thông báo “aggregate functions are not allowed in WHERE”. Hãy chuyển điều kiện sang HAVING.

2. Cột được chọn không được nhóm cũng không được tổng hợp

SELECT departure_airport, arrival_airport, COUNT(*)
FROM flights
GROUP BY departure_airport;

Truy vấn này cũng báo lỗi: “column arrival_airport must appear in the GROUP BY clause or be used in an aggregate function”. Một sân bay khởi hành có nhiều sân bay đến, nên cơ sở dữ liệu không thể chọn một giá trị cho mỗi nhóm. Hãy thêm arrival_airport vào GROUP BY (mỗi tuyến một dòng) hoặc tổng hợp nó, ví dụ COUNT(DISTINCT arrival_airport).

PostgreSQL có một ngoại lệ: nếu bạn nhóm theo khóa chính của một bảng, bạn được chọn các cột khác của bảng đó. Nhóm theo b.book_ref cho phép chọn b.total_amount. Liệt kê cả hai trong GROUP BY thì chạy được ở mọi nơi, nên các truy vấn ở đây đều viết như vậy:

SELECT b.book_ref, COUNT(*) AS tickets, b.total_amount
FROM bookings b
JOIN tickets t ON t.book_ref = b.book_ref
GROUP BY b.book_ref, b.total_amount
HAVING COUNT(*) >= 5
ORDER BY b.total_amount DESC
LIMIT 5;
book_ref tickets total_amount
65A6EA 5 10656.00
7734A5 5 8291.00
148C46 5 8135.00
070682 5 8012.00
8DA112 5 6150.00

Trong toàn bộ cơ sở dữ liệu chỉ có 23 đơn đặt chỗ có từ năm vé trở lên.

3. Xếp hạng nhóm nhỏ cạnh nhóm lớn

Độ trễ cất cánh trung bình theo sân bay, chỉ tính các chuyến đã thực sự cất cánh:

SELECT departure_airport,
       ROUND(AVG(EXTRACT(EPOCH FROM actual_departure - scheduled_departure) / 60), 1) AS avg_delay_min,
       COUNT(*) AS departed
FROM flights
WHERE status IN ('Arrived', 'Departed')
GROUP BY departure_airport
ORDER BY avg_delay_min DESC
LIMIT 3;
departure_airport avg_delay_min departed
ULY 24.4 97
BQS 22.8 31
LPK 22.7 22

Những sân bay “tệ nhất” này chỉ có từ 22 đến 97 chuyến cất cánh, nên vài lần trễ dài đã chi phối giá trị trung bình. Đặt một kích thước nhóm tối thiểu trong HAVING giúp so sánh công bằng hơn:

SELECT departure_airport,
       COUNT(*) AS departed,
       ROUND(AVG(EXTRACT(EPOCH FROM actual_departure - scheduled_departure) / 60), 1) AS avg_delay_min
FROM flights
WHERE status IN ('Arrived', 'Departed')
GROUP BY departure_airport
HAVING COUNT(*) >= 300
ORDER BY avg_delay_min DESC
LIMIT 5;
departure_airport departed avg_delay_min
SVX 348 14.7
VKO 870 13.4
BZK 306 13.2
KJA 358 13.1
LED 960 12.6

Chú ý sự phân công: WHERE quyết định chuyến nào được tính là đã cất cánh, HAVING quyết định sân bay nào có đủ số chuyến.

4. Dùng COUNT(*) khi ý bạn là COUNT(column)

COUNT(*) đếm số dòng; COUNT(column) đếm số giá trị khác NULL. Trong truy vấn có nhóm, sự khác biệt này thường chính là điều quan trọng:

SELECT status, COUNT(*) AS flights, COUNT(actual_departure) AS with_departure
FROM flights
GROUP BY status
ORDER BY flights DESC;
status flights with_departure
Arrived 16707 16707
Scheduled 15383 0
On Time 518 0
Cancelled 414 8
Departed 58 58
Delayed 41 0

Tám chuyến bị hủy lại có ghi giờ cất cánh, một điểm lạ của dữ liệu mà bạn sẽ không bao giờ nhận ra nếu chỉ dùng COUNT(*). Thêm HAVING COUNT(actual_departure) > 0 vào truy vấn này sẽ giữ lại ba nhóm chứ không phải sáu.

5. Nhóm theo một biểu thức ở chỗ này nhưng không ở chỗ kia

Nếu bạn nhóm theo ngày, hãy nhóm theo đúng biểu thức mà bạn chọn:

SELECT CAST(scheduled_departure AS date) AS day, COUNT(*) AS cancelled
FROM flights
WHERE status = 'Cancelled'
GROUP BY CAST(scheduled_departure AS date)
HAVING COUNT(*) >= 15
ORDER BY day;
day cancelled
2026-09-07 18
2026-09-08 21
2026-09-09 51
2026-09-10 287

Nếu nhóm theo mốc thời gian scheduled_departure gốc, mỗi phút cất cánh sẽ thành một nhóm, và HAVING sẽ không tìm thấy gì. (Đống chuyến bị hủy dồn ở cuối lịch bay đáng được xem kỹ trước khi ai đó báo cáo nó như một cuộc khủng hoảng: 403 trong số 414 chuyến bị hủy nằm trong lịch tháng 9 tương lai.)

HAVING không có GROUP BY

Nếu truy vấn có hàm tổng hợp và HAVING nhưng không có GROUP BY, toàn bộ kết quả được coi là một nhóm. Truy vấn trả về một dòng hoặc không dòng nào:

SELECT COUNT(*) AS flights
FROM flights
HAVING COUNT(*) > 30000;
flights
33121

Đổi ngưỡng thành 40.000 và bạn nhận được 0 dòng. Điều này đôi khi hữu ích khi kiểm tra dữ liệu: một truy vấn chỉ trả về dòng khi có gì đó sai.

Danh sách kiểm tra nhanh

Nếu có JOIN, hãy kiểm tra xem số dòng có bị nhân lên không trước khi tin bất kỳ SUM hay COUNT nào; bài JOIN trong SQL giải thích chuyện này. Khi bạn cần một giá trị tổng hợp bên cạnh mỗi dòng thay vì gộp dòng, thứ bạn cần là hàm cửa sổ, không phải GROUP BY. Muốn tự luyện ngay, hãy làm các bài 4 đến 8 trong bài tập SQL có lời giải.

Câu hỏi thường gặp

Có dùng được HAVING mà không có GROUP BY không?

Có. Toàn bộ kết quả trở thành một nhóm, và truy vấn trả về một dòng hoặc không gì cả, tùy vào điều kiện trong HAVING.

HAVING có chậm hơn WHERE không?

Với điều kiện trên giá trị tổng hợp thì không có lựa chọn nào khác, nên tốc độ không phải vấn đề. Với điều kiện trên cột gốc, WHERE cho phép cơ sở dữ liệu loại dòng trước khi nhóm, không bao giờ chậm hơn và thường nhanh hơn. Hãy dùng WHERE mỗi khi điều kiện không cần hàm tổng hợp.

Có dùng được bí danh cột trong HAVING không?

Trong PostgreSQL và SQL chuẩn thì không: HAVING được tính trước SELECT, nên lúc đó bí danh chưa tồn tại. Hãy viết lại biểu thức. MySQL và một số hệ quản trị khác cho phép dùng bí danh như một phần mở rộng.

WHERE và HAVING có dùng chung trong một truy vấn được không?

Được, và thường xuyên như vậy: WHERE chọn những dòng đi vào hàm tổng hợp, HAVING chọn những nhóm được giữ lại. Các ví dụ về chuyến cất cánh và độ trễ ở trên đều dùng cả hai.

GROUP BY và DISTINCT khác nhau thế nào?

SELECT DISTINCT status FROM flights và SELECT status FROM flights GROUP BY status trả về cùng sáu giá trị. GROUP BY còn cho phép tính giá trị tổng hợp theo nhóm và lọc nhóm bằng HAVING; DISTINCT chỉ loại bỏ các dòng trùng.

Ngôn ngữ khác: Español, Português (Brasil), English, 简体中文