聚合函数
查询明细数据时,一行结果通常对应表中的一行记录。但实际业务中,我们还经常要回答这类问题:一共有多少笔订单、销售总额是多少、每个地区的平均订单金额是多少?
SQL 聚合函数(Aggregate Function)就是用来完成这类统计的工具。它会接收多行数据,经过计算后返回一个汇总值。本文使用通用 SQL 介绍最常见的聚合函数,并逐步说明如何配合 GROUP BY 和 HAVING 完成分组统计。
准备示例数据
假设有一张 orders 表:
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer VARCHAR(50) NOT NULL,
region VARCHAR(20) NOT NULL,
category VARCHAR(20) NOT NULL,
amount DECIMAL(10, 2) NOT NULL,
discount DECIMAL(10, 2)
);
表中有以下数据:
id | customer | region | category | amount | discount |
|---|---|---|---|---|---|
| 1 | Alice | 华东 | 图书 | 120.00 | 10.00 |
| 2 | Bob | 华东 | 文具 | 30.00 | NULL |
| 3 | Alice | 华东 | 文具 | 50.00 | 5.00 |
| 4 | Carol | 华西 | 图书 | 200.00 | 20.00 |
| 5 | Dave | 华西 | 文具 | 80.00 | NULL |
| 6 | Carol | 华西 | 图书 | 150.00 | 15.00 |
后面的查询都基于这组数据。示例中的基本写法适用于 MySQL、PostgreSQL 和 SQLite。
五个常用聚合函数
最常见的聚合函数有:
| 函数 | 作用 | 示例问题 |
|---|---|---|
COUNT() | 统计行数或非空值数 | 一共有多少笔订单? |
SUM() | 求和 | 所有订单的总金额是多少? |
AVG() | 求平均值 | 平均订单金额是多少? |
MIN() | 找最小值 | 最低订单金额是多少? |
MAX() | 找最大值 | 最高订单金额是多少? |
COUNT():统计数量
COUNT(*) 统计查询结果中的行数:
SELECT COUNT(*) AS order_count
FROM orders;
结果为:
order_count |
|---|
| 6 |
COUNT(列名) 只统计该列不为 NULL 的行。示例数据中只有 4 笔订单填写了优惠金额:
SELECT COUNT(discount) AS discounted_order_count
FROM orders;
discounted_order_count |
|---|
| 4 |
因此,COUNT(*) 和 COUNT(discount) 回答的不是同一个问题:前者统计所有订单,后者统计填写了 discount 的订单。
如果只想统计不重复的值,可以使用 DISTINCT。例如统计下过订单的客户数:
SELECT COUNT(DISTINCT customer) AS customer_count
FROM orders;
Alice 和 Carol 虽然各有两笔订单,但只会被计算一次,因此结果是 4。
SUM():计算总和
计算所有订单的总金额:
SELECT SUM(amount) AS total_amount
FROM orders;
结果是 630.00。SUM() 通常用于金额、数量、时长等可以相加的数值。
AVG():计算平均值
计算平均订单金额:
SELECT AVG(amount) AS average_amount
FROM orders;
结果是 105.00,因为总金额 630.00 除以 6 笔订单等于 105.00。
MIN() 和 MAX():找最小值与最大值
一次查询可以使用多个聚合函数:
SELECT
MIN(amount) AS minimum_amount,
MAX(amount) AS maximum_amount
FROM orders;
minimum_amount | maximum_amount |
|---|---|
| 30.00 | 200.00 |
MIN() 和 MAX() 不只可以处理数字。在数据库支持的数据类型和排序规则下,它们也可以用于日期或文本,例如查找最早和最晚的下单时间。
没有 GROUP BY 时,所有行属于一组
下面的查询同时计算订单数量、总金额和平均金额:
SELECT
COUNT(*) AS order_count,
SUM(amount) AS total_amount,
AVG(amount) AS average_amount
FROM orders;
虽然 orders 表有 6 行,查询结果却只有 1 行。这是因为没有使用 GROUP BY 时,满足条件的所有行会被当作一个整体进行汇总。
可以把这个过程理解为:
6 行订单明细 -> 聚合计算 -> 1 行统计结果
GROUP BY:分别统计每一组
如果想知道每个地区各有多少笔订单、总金额是多少,就需要先按地区分组:
SELECT
region,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM orders
GROUP BY region;
结果为:
region | order_count | total_amount |
|---|---|---|
| 华东 | 3 | 200.00 |
| 华西 | 3 | 430.00 |
数据库会先把相同 region 的行放进同一组,再对每组分别执行 COUNT() 和 SUM()。最终每个分组产生一行结果。
按多个字段分组
还可以按地区和商品分类共同分组:
SELECT
region,
category,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM orders
GROUP BY region, category
ORDER BY region, category;
此时,只有 region 和 category 都相同的行才属于同一组:
region | category | order_count | total_amount |
|---|---|---|---|
| 华东 | 图书 | 1 | 120.00 |
| 华东 | 文具 | 2 | 80.00 |
| 华西 | 图书 | 2 | 350.00 |
| 华西 | 文具 | 1 | 80.00 |
SELECT 中能放哪些列
使用 GROUP BY 后,SELECT 中通常只能出现:
- 分组字段,例如
region; - 聚合表达式,例如
SUM(amount); - 由分组字段或聚合结果计算出的表达式。
下面的写法存在问题:
SELECT region, customer, SUM(amount)
FROM orders
GROUP BY region;
一个地区中可能有多个客户,数据库无法确定该显示哪个 customer。标准 SQL 会拒绝这类查询;某些数据库配置可能返回一个不确定的值,也不应该依赖这种行为。正确做法是将 customer 加入分组条件,或者从查询结果中移除它。
WHERE:聚合前过滤明细行
如果只想统计图书订单,可以先用 WHERE 过滤数据:
SELECT
region,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM orders
WHERE category = '图书'
GROUP BY region;
WHERE 会先排除文具订单,然后再分组和聚合:
region | order_count | total_amount |
|---|---|---|
| 华东 | 1 | 120.00 |
| 华西 | 2 | 350.00 |
由于 WHERE 执行时聚合结果还没有产生,所以不能在其中直接使用聚合函数:
-- 错误:WHERE 阶段还没有计算出 SUM(amount)
SELECT region, SUM(amount)
FROM orders
WHERE SUM(amount) > 300
GROUP BY region;
需要根据聚合结果过滤分组时,应使用 HAVING。
HAVING:聚合后过滤分组
下面的查询只保留订单总金额大于 300 的地区:
SELECT
region,
SUM(amount) AS total_amount
FROM orders
GROUP BY region
HAVING SUM(amount) > 300;
结果只有华西地区:
region | total_amount |
|---|---|
| 华西 | 430.00 |
WHERE 和 HAVING 的区别可以概括为:
| 子句 | 过滤对象 | 执行时机 | 典型条件 |
|---|---|---|---|
WHERE | 明细行 | 聚合之前 | category = '图书' |
HAVING | 分组结果 | 聚合之后 | SUM(amount) > 300 |
二者也可以同时使用。例如,先排除金额小于 50 的订单,再找出剩余订单总额超过 300 的地区:
SELECT region, SUM(amount) AS total_amount
FROM orders
WHERE amount >= 50
GROUP BY region
HAVING SUM(amount) > 300;
SQL 子句的逻辑处理顺序
SQL 的书写顺序和逻辑处理顺序并不相同。理解下面的顺序,有助于判断某个阶段能使用哪些数据:
| 顺序 | 子句 | 作用 |
|---|---|---|
| 1 | FROM | 确定数据来源 |
| 2 | WHERE | 过滤明细行 |
| 3 | GROUP BY | 将剩余的行分组 |
| 4 | HAVING | 过滤分组结果 |
| 5 | SELECT | 计算并选择最终输出列 |
| 6 | ORDER BY | 对最终结果排序 |
| 7 | LIMIT | 限制返回行数(若有使用) |
例如,要查看销售总额最高的地区,可以这样写:
SELECT region, SUM(amount) AS total_amount
FROM orders
GROUP BY region
ORDER BY total_amount DESC
LIMIT 1;
这里可以在 ORDER BY 中使用别名 total_amount,因为排序发生在 SELECT 结果产生之后。不同数据库对别名可见性的细节可能不同,复杂查询应查看所用数据库的文档。
聚合函数如何处理 NULL
大多数聚合函数会忽略 NULL:
COUNT(discount)只统计非NULL的优惠金额;SUM(discount)只把非NULL的优惠金额相加;AVG(discount)只用非NULL的值计算平均数;MIN(discount)和MAX(discount)也会跳过NULL。
COUNT(*) 是重要的例外:它统计行数,不关心某一列是否为 NULL。
有时业务上需要把 NULL 当作 0,这时可以使用 COALESCE:
SELECT AVG(COALESCE(discount, 0)) AS average_discount
FROM orders;
这条查询把两笔没有优惠金额的订单按 0 参与平均值计算,结果是 8.33 左右。若直接使用 AVG(discount),只会用 4 个非空值计算,结果是 12.50。
两种写法都可能正确,关键是先明确 NULL 在业务中表示“没有优惠”,还是表示“数据未知”。不要为了消除 NULL 而随意替换成 0。
如果过滤后没有任何行,COUNT(*) 返回 0;SUM()、AVG()、MIN() 和 MAX() 通常返回 NULL。需要把空结果显示为 0 时,可以写成:
SELECT COALESCE(SUM(amount), 0) AS total_amount
FROM orders
WHERE region = '不存在的地区';
条件聚合:在一次查询中统计多个条件
条件聚合通常把 CASE 表达式放进聚合函数中。下面的查询一次得到每个地区的图书销售额和文具销售额:
SELECT
region,
SUM(CASE WHEN category = '图书' THEN amount ELSE 0 END) AS book_amount,
SUM(CASE WHEN category = '文具' THEN amount ELSE 0 END) AS stationery_amount
FROM orders
GROUP BY region;
region | book_amount | stationery_amount |
|---|---|---|
| 华东 | 120.00 | 80.00 |
| 华西 | 350.00 | 80.00 |
其思路是:每行先由 CASE 判断是否满足条件,满足时提供订单金额,不满足时提供 0;随后 SUM() 再对每组求和。
统计满足某个条件的行数时也可以使用这个方法:
SELECT
region,
SUM(CASE WHEN amount >= 100 THEN 1 ELSE 0 END) AS large_order_count
FROM orders
GROUP BY region;
这种写法适用于报表中的多项并列指标,而且在常见数据库中都能使用。部分数据库还提供专用的聚合过滤语法,但可移植性需要根据数据库产品确认。
常见错误与注意事项
混淆 COUNT(*) 与 COUNT(列名)
想统计行数时优先使用 COUNT(*)。只有在明确需要排除 NULL 时,才使用 COUNT(列名)。
把聚合条件写进 WHERE
WHERE 过滤明细行,不能直接判断 SUM()、AVG() 等聚合结果。分组后的条件应写在 HAVING 中。
忘记 GROUP BY
如果需求中出现“每个地区”“每位客户”“每个月”等词,通常意味着需要按对应字段 GROUP BY。没有分组时,只会得到整个结果集的一条汇总记录。
直接计算“平均值的平均值”
不同分组的记录数量不同时,简单地把各组平均值再次平均,通常会得到错误结果。例如,一个地区有 2 笔订单,另一个地区有 100 笔订单,两者的平均值不应该拥有相同权重。
需要总体平均值时,应直接对原始明细使用 AVG(),或者用各组的“总和之和除以数量之和”计算加权平均值。
连接后重复统计
聚合前如果进行了表连接,一对多关系可能让原来的行重复出现,从而放大 COUNT() 或 SUM() 的结果。应先检查连接后的明细和行数,再决定使用预聚合、COUNT(DISTINCT ...),还是调整连接条件。关于连接导致的行数变化,可继续阅读表之间的连接。
忽略数值精度与数据库差异
金额通常应使用 DECIMAL 等精确数值类型,不要依赖浮点数保存精确货币值。不同数据库对聚合结果的数据类型、除法规则、别名引用和专用函数支持可能不同,生产代码应以所用数据库的官方文档和实际测试为准。
动手练习
基于示例 orders 表,尝试写出下面三条查询:
- 统计每位客户的订单数量和消费总额,并按消费总额从高到低排序。
- 找出平均订单金额不低于
100的客户。 - 分别统计填写了优惠金额和没有填写优惠金额的订单数量。
参考写法:
-- 练习 1
SELECT
customer,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM orders
GROUP BY customer
ORDER BY total_amount DESC;
-- 练习 2
SELECT
customer,
AVG(amount) AS average_amount
FROM orders
GROUP BY customer
HAVING AVG(amount) >= 100;
-- 练习 3
SELECT
SUM(CASE WHEN discount IS NOT NULL THEN 1 ELSE 0 END) AS filled_count,
SUM(CASE WHEN discount IS NULL THEN 1 ELSE 0 END) AS empty_count
FROM orders;
小结
- 聚合函数把多行数据汇总成统计结果,常用函数有
COUNT()、SUM()、AVG()、MIN()和MAX()。 - 没有
GROUP BY时,所有满足条件的行被视为一组;使用GROUP BY后,每个分组产生一行结果。 WHERE在聚合前过滤明细行,HAVING在聚合后过滤分组。- 除
COUNT(*)外,大多数聚合函数都会忽略NULL;是否用COALESCE转成0应由业务含义决定。 - 使用
CASE配合聚合函数,可以在一次查询中统计多个条件。 - 写聚合查询时,先明确“统计对象、分组维度、过滤阶段”,再检查连接是否改变了原始行数。