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

Hàm tổng hợp trong SQL: COUNT, SUM, AVG, MIN, MAX kèm ví dụ

Nhiều thẻ lên máy bay nhỏ đổ dồn vào một con số lớn trên màn hình sân bay

Hàm tổng hợp (aggregate function) nhận nhiều dòng và trả về một giá trị. Năm hàm bạn sẽ dùng hằng ngày là COUNT (bao nhiêu), SUM (tổng), AVG (trung bình), MIN và MAX (nhỏ nhất và lớn nhất). Không có GROUP BY, chúng gộp cả bảng thành một dòng; có GROUP BY, chúng trả về một dòng cho mỗi nhóm. Hai quy tắc giải thích phần lớn những bất ngờ: mọi hàm trừ COUNT(*) đều bỏ qua NULL, và COUNT(*), COUNT(column), COUNT(DISTINCT column) đếm ba thứ khác nhau.

Mọi ví dụ chạy trên cơ sở dữ liệu hãng hàng không của khóa SQL trên Nomto, với cú pháp PostgreSQL trên DuckDB (động cơ của Sân tập SQL trên Nomto), và mọi bảng kết quả đều là kết quả thật. Giá vé tính bằng đô la Mỹ.

Năm hàm tổng hợp trong một truy vấn

ticket_flights có một dòng cho mỗi hành khách trên mỗi chặng bay, với giá vé trong cột amount:

SELECT COUNT(*)              AS segments,
       SUM(amount)           AS revenue,
       ROUND(AVG(amount), 2) AS avg_fare,
       MIN(amount)           AS min_fare,
       MAX(amount)           AS max_fare
FROM ticket_flights;
segments revenue avg_fare min_fare max_fare
1045726 207669809.00 198.59 30.00 2033.00
hàm trả về dùng với
COUNT(*) số dòng mọi bảng
COUNT(col) số giá trị khác NULL mọi kiểu
SUM(col) tổng số
AVG(col) trung bình cộng số
MIN(col), MAX(col) nhỏ nhất, lớn nhất số, chuỗi, ngày tháng

COUNT(*), COUNT(cột) và COUNT(DISTINCT cột)

Đây là khác biệt quan trọng nhất trong thực tế:

SELECT COUNT(*)                        AS flights,
       COUNT(actual_departure)         AS departed,
       COUNT(DISTINCT arrival_airport) AS destinations,
       COUNT(DISTINCT aircraft_code)   AS models_used
FROM flights;
flights departed destinations models_used
33121 16773 104 8

COUNT(1) cho cùng kết quả với COUNT(*) và không nhanh hơn ở bất kỳ hệ quản trị hiện đại nào. Chọn một cách và dùng nhất quán.

COUNT(DISTINCT ...) là cách trả lời câu hỏi “có bao nhiêu X khác nhau”. Bảng tickets có một dòng cho mỗi vé hành khách, và nhiều vé có thể thuộc cùng một đơn đặt chỗ:

SELECT COUNT(*)                      AS tickets,
       COUNT(contact_data->>'email') AS with_email,
       COUNT(DISTINCT book_ref)      AS bookings
FROM tickets;
tickets with_email bookings
366733 199083 262788

Để đếm số tổ hợp khác nhau của hai cột, PostgreSQL và DuckDB không chấp nhận COUNT(DISTINCT a, b) (MySQL thì có). Thay vào đó, hãy đếm số dòng của một SELECT DISTINCT:

SELECT COUNT(*) AS routes
FROM (SELECT DISTINCT departure_airport, arrival_airport FROM flights) r;
routes
618

SUM và AVG

SUM cộng lại, AVG chia tổng đó cho số giá trị khác NULL. Cả hai chỉ nhận số. Theo từng hạng vé, sắp theo giá vé trung bình:

SELECT fare_conditions,
       COUNT(*)              AS segments,
       ROUND(SUM(amount))    AS revenue,
       ROUND(AVG(amount), 2) AS avg_fare,
       MIN(amount)           AS min_fare,
       MAX(amount)           AS max_fare
FROM ticket_flights
GROUP BY fare_conditions
ORDER BY avg_fare DESC;
fare_conditions segments revenue avg_fare min_fare max_fare
Business 107642 55051796 511.43 91.00 2033.00
Comfort 17291 5661169 327.41 199.00 476.00
Economy 920793 146956844 159.6 30.00 745.00

Cái bẫy trung bình của các trung bình

Giá vé trung bình là bao nhiêu? Lấy trung bình của ba giá trị trung bình theo hạng vé cho ra con số khác hẳn so với lấy trung bình trên mọi chặng:

SELECT ROUND(AVG(avg_fare), 2) AS avg_of_averages,
       (SELECT ROUND(AVG(amount), 2) FROM ticket_flights) AS true_average
FROM (
  SELECT fare_conditions, AVG(amount) AS avg_fare
  FROM ticket_flights
  GROUP BY fare_conditions
) t;
avg_of_averages true_average
332.81 198.59

Trung bình của các trung bình cho 17.291 chặng hạng Comfort cùng trọng số với 920.793 chặng hạng Economy. Hãy luôn tính trung bình trên dữ liệu gốc, hoặc tính SUM(amount) / SUM(segments) từ các tổng đã nhóm.

Phép chia số nguyên

Trong PostgreSQL, SQL Server và SQLite, chia một số nguyên cho một số nguyên sẽ bỏ phần dư: các tỷ lệ kiểu COUNT(*) / COUNT(*) âm thầm trở thành 0 hoặc 1. Phép / của DuckDB trả về số thập phân (7 / 2 là 3,5; 7 // 2 là 3), và / của MySQL cũng vậy. Nhân với 100.0 trước, như trong các ví dụ bên dưới, cho kết quả đúng ở mọi nơi. Đây là lỗi rất hay gặp khi một truy vấn viết trên PostgreSQL hay DuckDB được chuyển sang SQL Server ở công ty.

MIN và MAX

MIN và MAX dùng được với mọi kiểu có thể sắp xếp: số, ngày tháng và chuỗi.

SELECT MIN(book_date)    AS first_booking,
       MAX(book_date)    AS last_booking,
       MIN(total_amount) AS smallest,
       MAX(total_amount) AS largest
FROM bookings;
first_booking last_booking smallest largest
2026-06-17 14:05:00 2026-08-11 18:00:00 34.00 12045.00

Với chuỗi, MIN(city) trả về giá trị đứng đầu theo bảng chữ cái (Abakan) và MAX(city) giá trị đứng cuối (Yuzhno-Sakhalinsk). Với dữ liệu tiếng Việt, “đứng đầu” phụ thuộc collation, như bài ORDER BY trong SQL cho thấy. MAX trả về giá trị, không phải dòng: muốn biết đơn nào có số tiền lớn nhất, hãy dùng ORDER BY total_amount DESC LIMIT 1, hoặc một hàm cửa sổ khi cần cho từng nhóm (xem hàm cửa sổ trong SQL).

Hàm tổng hợp và NULL

Bài NULL trong SQL minh họa từng điểm trên bằng dữ liệu thật, kể cả việc trung bình dịch đi bao nhiêu khi giá trị chưa biết bị đổi thành 0.

Hàm tổng hợp với GROUP BY và HAVING

GROUP BY chia dòng thành các nhóm, và mỗi hàm tổng hợp được tính một lần cho mỗi nhóm. Sau đó HAVING lọc các nhóm theo một giá trị tổng hợp:

SELECT departure_airport, COUNT(*) AS departures
FROM flights
GROUP BY departure_airport
HAVING COUNT(*) >= 1500
ORDER BY departures DESC;
departure_airport departures
DME 3217
SVO 2981
LED 1900
VKO 1719

Hai quy tắc suy ra từ thứ tự SQL thực thi truy vấn:

Chi tiết, kèm các lỗi thường gặp, có trong bài GROUP BY và HAVING trong SQL.

Tổng hợp có điều kiện: FILTER và CASE

Thường bạn muốn nhiều phép đếm với điều kiện khác nhau trên cùng một dòng. Mệnh đề FILTER áp một điều kiện cho riêng một hàm tổng hợp:

SELECT COUNT(*) FILTER (WHERE status = 'Cancelled') AS cancelled,
       COUNT(*) FILTER (WHERE status = 'Delayed')   AS delayed,
       SUM(CASE WHEN status = 'Cancelled' THEN 1 ELSE 0 END) AS cancelled_case,
       ROUND(100.0 * COUNT(*) FILTER (WHERE status = 'Cancelled') / COUNT(*), 2) AS cancelled_pct
FROM flights;
cancelled delayed cancelled_case cancelled_pct
414 41 414 1.25

FILTER chạy được trong PostgreSQL (9.4+), DuckDB và SQLite (3.30+). MySQL và SQL Server không hỗ trợ; ở đó hãy dùng SUM(CASE WHEN ... THEN 1 ELSE 0 END) hoặc COUNT(CASE WHEN ... THEN 1 END), cho cùng con số, như cột cancelled_case cho thấy.

Cùng mẫu đó tính tỷ trọng trên tổng chỉ trong một lần quét:

SELECT ROUND(SUM(amount)) AS revenue,
       ROUND(SUM(amount) FILTER (WHERE fare_conditions = 'Business')) AS business_revenue,
       ROUND(100.0 * SUM(amount) FILTER (WHERE fare_conditions = 'Business') / SUM(amount), 1) AS business_pct
FROM ticket_flights;
revenue business_revenue business_pct
207669809 55051796 26.5

Hạng Business chiếm khoảng 10% số chặng nhưng 26,5% doanh thu. Thêm logic điều kiện trong bài CASE WHEN trong SQL.

Tổng sai sau khi JOIN

Hàm tổng hợp đếm bất cứ dòng nào chúng nhận được. Một phép JOIN ghép một dòng với nhiều dòng sẽ lặp lại dòng đó, và SUM cộng nó nhiều lần. Tổng tiền đặt chỗ được cộng sau khi JOIN với bảng vé:

SELECT SUM(b.total_amount) AS wrong_revenue,
       (SELECT SUM(total_amount) FROM bookings) AS right_revenue
FROM bookings b
JOIN tickets t ON t.book_ref = b.book_ref;
wrong_revenue right_revenue
343206192.00 207669809.00

Một đơn đặt chỗ có ba hành khách thì có ba vé, nên tổng tiền của nó bị cộng ba lần. Truy vấn chạy không lỗi và con số trông hợp lý. Hãy tổng hợp từng bảng ở đúng cấp của nó trước, rồi mới JOIN các kết quả, hoặc dùng COUNT(DISTINCT ...) khi bạn chỉ cần đếm. Bài JOIN trong SQL giải thích kỹ hiện tượng nhân dòng này.

Lồng hàm tổng hợp

Bạn không thể đặt trực tiếp một hàm tổng hợp bên trong hàm khác: SELECT MAX(COUNT(*)) FROM flights GROUP BY departure_airport báo lỗi “aggregate function calls cannot be nested”. Hãy tổng hợp trong truy vấn con hoặc CTE, rồi tổng hợp kết quả đó:

SELECT AVG(n) AS avg_tickets_per_booking, MAX(n) AS max_tickets
FROM (SELECT book_ref, COUNT(*) AS n FROM tickets GROUP BY book_ref) t;
avg_tickets_per_booking max_tickets
1.3955469808362635 5

Trung bình mỗi đơn có 1,4 vé, và đơn lớn nhất có năm vé. Xem bài CTE trong SQL để viết các phép tổng hợp nhiều bước cho dễ đọc.

Các hàm tổng hợp hữu ích khác

SELECT split_part(model, ' ', 1) AS maker,
       COUNT(*)   AS models,
       MAX(range) AS max_range,
       STRING_AGG(model, ', ' ORDER BY range DESC) AS line_up
FROM aircrafts
GROUP BY maker
ORDER BY max_range DESC;
maker models max_range line_up
Boeing 3 11100 Boeing 777-300, Boeing 767-300, Boeing 737-300
Airbus 3 6700 Airbus A319-100, Airbus A320-200, Airbus A321-200
Sukhoi 1 3000 Sukhoi Superjet-100
Bombardier 1 2700 Bombardier CRJ-200
Cessna 1 1200 Cessna 208 Caravan

Mọi hàm tổng hợp cũng dùng được như một hàm cửa sổ với OVER (...), khi đó các dòng riêng lẻ được giữ lại thay vì bị gộp: đó là cách tính tổng lũy kế và tỷ trọng trong nhóm.

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

SQL có những hàm tổng hợp nào?

Năm hàm chuẩn là COUNT, SUM, AVG, MIN và MAX. Hầu hết hệ quản trị bổ sung thêm, như STRING_AGG, ARRAY_AGG, STDDEV, VARIANCE và các hàm phân vị. Tất cả đều biến một tập dòng thành một giá trị, cho từng nhóm nếu có GROUP BY.

COUNT(*) và COUNT(cột) khác nhau thế nào?

COUNT(*) đếm dòng. COUNT(column) đếm những dòng mà cột đó khác NULL. Trong bảng flights, chúng cho ra 33.121 và 16.773 với actual_departure, vì những chuyến chưa cất cánh không có giờ cất cánh.

Hàm tổng hợp có bỏ qua NULL không?

Có, tất cả trừ COUNT(*). AVG chia cho số giá trị khác NULL, và SUM của một cột toàn NULL (hoặc của tập không có dòng nào) trả về NULL, không phải 0.

Có dùng hàm tổng hợp trong WHERE được không?

Không. WHERE lọc dòng trước khi nhóm, nên lúc đó các giá trị tổng hợp chưa tồn tại. Hãy dùng HAVING để lọc theo giá trị tổng hợp, hoặc tính nó trong truy vấn con rồi lọc ở truy vấn ngoài.

Có dùng hàm tổng hợp mà không có GROUP BY được không?

Được. Không có GROUP BY, cả bảng (sau WHERE) là một nhóm, và bạn nhận được một dòng duy nhất. Bạn không thể trộn các giá trị tổng hợp đó với cột thường trong cùng một SELECT.

Làm sao đếm số giá trị khác nhau trong SQL?

COUNT(DISTINCT column). Với tổ hợp của nhiều cột, hãy đếm số dòng của SELECT DISTINCT a, b trong một truy vấn con, vì COUNT(DISTINCT a, b) chỉ chạy được trong MySQL.

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