SQL 数据分析 · 指南 · 2026年10月30日 · 9 分钟阅读
SQL 聚合函数:COUNT、SUM、AVG、MIN、MAX 用法与实例

聚合函数把很多行变成一个值。每天都会用到的是这五个:COUNT(多少个)、SUM(合计)、AVG(平均值)、MIN 和 MAX(最小值和最大值)。没有 GROUP BY 时,它们把整张表压缩成一行;有 GROUP BY 时,每组返回一行。大多数意外都可以用两条规则解释:除了 COUNT(*),所有聚合函数都会忽略 NULL;而 COUNT(*)、COUNT(列) 和 COUNT(DISTINCT 列) 数的是三种不同的东西。
下面的例子都在 Nomto SQL 课程的航空公司数据库上运行,使用 PostgreSQL 语法,引擎是 DuckDB(Nomto SQL 练习场背后的引擎),每个结果表都是真实输出。票价单位是美元。
一条查询用上五个聚合函数
ticket_flights 表里每位乘客的每个航段占一行,票价在 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 |
| 函数 | 返回 | 适用类型 |
|---|---|---|
COUNT(*) |
行数 | 任何表 |
COUNT(col) |
非 NULL 值的个数 | 任何类型 |
SUM(col) |
合计 | 数字 |
AVG(col) |
算术平均值 | 数字 |
MIN(col)、MAX(col) |
最小值、最大值 | 数字、文本、日期 |
COUNT(*)、COUNT(列) 和 COUNT(DISTINCT 列) 的区别
这是实际工作中最重要的区别,也是面试高频题:
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(*)数行数,不管行里有什么:33,121 个航班。COUNT(actual_departure)只数这一列不为NULL的行:真正起飞了的只有 16,773 个航班,其余的还没到时间或者被取消了。COUNT(DISTINCT arrival_airport)数不同的值:104 个目的地。NULL不算作一个值。
COUNT(1) 和 COUNT(*) 结果相同,在任何现代数据库里都不会更快。选一种,保持一致就好。
COUNT(DISTINCT ...) 用来回答「有多少个不同的 X」,也就是常说的去重计数。tickets 表每张乘客机票占一行,多张机票可以属于同一个订单:
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 |
要统计两列的不同组合,PostgreSQL 和 DuckDB 不接受 COUNT(DISTINCT a, b)(MySQL、Hive 和 Spark SQL 可以)。通用的做法是数 SELECT DISTINCT 的行数:
SELECT COUNT(*) AS routes
FROM (SELECT DISTINCT departure_airport, arrival_airport FROM flights) r;
| routes |
|---|
| 618 |
在 Hive 和 Spark 这类大数据引擎上,对超大表做 COUNT(DISTINCT) 可能很慢,因为所有不同的值最后要汇总到一起去重。常见的优化是先 GROUP BY 去重再 COUNT(*),就像上面这样;只需要近似值时,可以用 approx_count_distinct(Spark SQL、DuckDB)。
SUM 和 AVG
SUM 求和,AVG 用和除以非 NULL 值的个数。两者都只接受数字。按舱位等级统计,按平均票价排序:
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 |
平均值的平均值陷阱
平均票价是多少?对三个舱位的平均值再求平均,和对所有航段求平均,答案差得很远:
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 |
平均值的平均值让 17,291 个 Comfort 航段和 920,793 个 Economy 航段拥有同样的权重。永远对原始行求平均,或者从分组合计里算 SUM(amount) / SUM(segments)。
整数除法
在 PostgreSQL、SQL Server 和 SQLite 里,整数除以整数会丢掉余数:COUNT(*) / COUNT(*) 这类比率会悄悄变成 0 或 1。DuckDB 的 / 返回小数(7 / 2 是 3.5,7 // 2 是 3),MySQL、Hive 和 Spark SQL 的 / 也返回小数。先乘以 100.0,就像下面的例子那样,在任何数据库里都能得到正确答案。
MIN 和 MAX
MIN 和 MAX 适用于任何可以排序的类型:数字、日期和文本。
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 |
对文本来说,MIN(city) 返回按字母排第一的值(Abakan),MAX(city) 返回最后一个(Yuzhno-Sakhalinsk)。MAX 返回的是值,不是那一行:要看金额最大的是哪个订单,用 ORDER BY total_amount DESC LIMIT 1;需要每组各取一行时,用窗口函数(见 SQL 窗口函数)。
聚合函数和 NULL
SUM、AVG、MIN、MAX和COUNT(col)都会跳过NULL。1、NULL、3 的AVG是 2,不是 4/3。COUNT(*)数所有行,包括含NULL的行。- 在零行上,
COUNT返回 0,而SUM、AVG、MIN和MAX返回NULL。报表上应该显示 0 的话,用COALESCE(SUM(x), 0)包一层。 - 求平均之前把
NULL换成 0(AVG(COALESCE(x, 0)))会改变答案;只有缺失值真的意味着零时才这样做。
SQL NULL 值处理在真实数据上逐一演示了这些规则,包括把未知值当成零之后平均值会偏多少。
聚合函数配合 GROUP BY 和 HAVING
GROUP BY 把行分成组,每个聚合函数在每组上各算一次。HAVING 再按聚合结果筛选组:
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 |
SQL 执行查询的顺序决定了两条规则:
WHERE里不能用聚合函数,因为WHERE在分组之前执行。DuckDB 会报WHERE clause cannot contain aggregates!;按聚合结果筛选要用HAVING。SELECT里的每一列,要么在GROUP BY里,要么包在聚合函数里。 否则数据库不知道该显示组里的哪个值。旧版 MySQL(关闭ONLY_FULL_GROUP_BY时)会随便挑一个值而不报错,这在国内的老项目里是常见的隐患。
详细说明和常见错误见 GROUP BY 和 HAVING 的区别。
条件聚合:FILTER 和 CASE
经常需要在一行里同时算出几个不同条件的计数。FILTER 子句只给某一个聚合函数加条件:
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 在 PostgreSQL(9.4+)、DuckDB 和 SQLite(3.30+)里可用。MySQL、SQL Server 和 Hive 不支持;在那里用 SUM(CASE WHEN ... THEN 1 ELSE 0 END) 或 COUNT(CASE WHEN ... THEN 1 END),结果一样,cancelled_case 列就是证明。MySQL 和 Hive 里还可以写得更短:SUM(IF(status = 'Cancelled', 1, 0))。
同样的写法可以一次算出占比:
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 |
公务舱航段只占约 10%,收入却占 26.5%。更多条件逻辑见 SQL CASE WHEN 用法。
JOIN 之后合计出错
聚合函数拿到什么行就算什么行。如果连接让一行匹配到多行,这一行就会被重复,SUM 会把它加好几次。连接机票之后再对订单金额求和:
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 |
一个三位乘客的订单有三张机票,所以它的金额被加了三次。查询没有报错,数字看起来也挺合理。正确做法是先在各自的粒度上聚合,再连接结果;只需要计数时,用 COUNT(DISTINCT ...)。这种「行数膨胀」的原理见 SQL JOIN 详解。
聚合函数不能嵌套
不能把一个聚合函数直接放进另一个里:SELECT MAX(COUNT(*)) FROM flights GROUP BY departure_airport 会报错「aggregate function calls cannot be nested」。先在子查询或 CTE 里聚合,再对结果聚合:
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 |
平均每个订单 1.4 张机票,最多的有五张。多步聚合怎样写得清楚,见 SQL CTE 用法。
其他常用聚合函数
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, ', ')把多个值拼成一个字符串(MySQL 用GROUP_CONCAT,Hive 和 Spark SQL 用concat_ws(', ', collect_list(col)))。percentile_cont(0.5) WITHIN GROUP (ORDER BY amount)求中位数:这里是 134.00,远低于 198.59 的平均值,因为少数昂贵的公务舱票价把平均值拉高了。DuckDB 还有median(amount),Hive 有percentile和percentile_approx。MySQL 没有内置的分位数聚合函数。BOOL_AND、BOOL_OR检查组内所有行或任意一行是否满足条件(PostgreSQL、DuckDB)。ARRAY_AGG把值收集成数组(Hive 和 Spark SQL 里对应collect_list,去重版是collect_set)。
每个聚合函数加上 OVER (...) 都能当作窗口函数使用,这样会保留每一行而不是把它们压缩掉:累计求和、组内占比就是这样算出来的。
常见问题
SQL 有哪些聚合函数?
标准的五个是 COUNT、SUM、AVG、MIN 和 MAX。大多数数据库还提供更多,比如 STRING_AGG、ARRAY_AGG、STDDEV、VARIANCE 和分位数函数。它们都把一组行变成一个值;有 GROUP BY 时,每组一个值。
COUNT(*) 和 COUNT(列) 有什么区别?
COUNT(*) 数行数。COUNT(列) 数该列不为 NULL 的行数。在 flights 表里,它们对 actual_departure 分别得到 33,121 和 16,773,因为还没起飞的航班没有起飞时间。
聚合函数会忽略 NULL 吗?
会,除了 COUNT(*) 之外都会。AVG 除以非 NULL 值的个数;一列全是 NULL(或者零行)时,SUM 返回 NULL 而不是 0。
WHERE 里能用聚合函数吗?
不能。WHERE 在分组之前筛选行,那时聚合结果还不存在。按聚合结果筛选要用 HAVING,或者在子查询里先算出来,再在外层查询里筛选。
不写 GROUP BY 能用聚合函数吗?
能。没有 GROUP BY 时,整张表(经过 WHERE 之后)就是一个组,结果只有一行。但不能在同一个 SELECT 里把这样的聚合和普通列混在一起。
SQL 怎样统计去重后的数量?
用 COUNT(DISTINCT 列)。多列组合去重计数时,在子查询里数 SELECT DISTINCT a, b 的行数,因为 COUNT(DISTINCT a, b) 只有 MySQL、Hive 和 Spark SQL 支持。
其他语言: English, Español, Português (Brasil), Tiếng Việt