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ụ

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(*)đếm dòng, bất kể bên trong có gì: 33.121 chuyến bay.COUNT(actual_departure)đếm những dòng mà cột đó khácNULL: chỉ 16.773 chuyến đã thực sự cất cánh; số còn lại vẫn ở tương lai hoặc đã bị hủy.COUNT(DISTINCT arrival_airport)đếm số giá trị khác nhau: 104 điểm đến.NULLkhông được tính là một giá trị.
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
SUM,AVG,MIN,MAXvàCOUNT(col)bỏ quaNULL.AVGcủa 1,NULL, 3 là 2, không phải 4/3.COUNT(*)đếm mọi dòng, kể cả dòng cóNULL.- Trên tập không có dòng nào,
COUNTtrả về 0, nhưngSUM,AVG,MINvàMAXtrả vềNULL. Hãy bọc chúng trongCOALESCE(SUM(x), 0)khi báo cáo cần hiển thị 0. - Thay
NULLbằng 0 trước khi tính trung bình (AVG(COALESCE(x, 0))) làm đổi kết quả; chỉ làm vậy khi giá trị bị thiếu thực sự có nghĩa là 0.
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:
- Không được dùng hàm tổng hợp trong
WHERE, vìWHEREchạy trước khi nhóm. DuckDB báoWHERE clause cannot contain aggregates!; hãy lọc theo giá trị tổng hợp bằngHAVING. - Mọi cột trong
SELECTphải nằm trongGROUP BYhoặc bên trong một hàm tổng hợp. Nếu không, cơ sở dữ liệu không biết phải hiển thị giá trị nào của nhóm.
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 |
STRING_AGG(col, ', ')nối các giá trị thành một chuỗi (MySQL:GROUP_CONCAT; SQL Server cóSTRING_AGGtừ bản 2017, sắp thứ tự bằngWITHIN GROUP (ORDER BY ...)).percentile_cont(0.5) WITHIN GROUP (ORDER BY amount)cho giá trị trung vị (median): ở đây là 134.00, thấp hơn nhiều so với trung bình 198.59, vì vài vé Business đắt tiền kéo giá trị trung bình lên. DuckDB còn cómedian(amount). MySQL không có hàm tổng hợp phân vị dựng sẵn.BOOL_AND,BOOL_ORkiểm tra một điều kiện có đúng với mọi dòng hay với ít nhất một dòng trong nhóm (PostgreSQL, DuckDB).ARRAY_AGGgom các giá trị thành một mảng.
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), 简体中文