跳到主要内容

聚合函数

查询明细数据时,一行结果通常对应表中的一行记录。但实际业务中,我们还经常要回答这类问题:一共有多少笔订单、销售总额是多少、每个地区的平均订单金额是多少?

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)
);

表中有以下数据:

idcustomerregioncategoryamountdiscount
1Alice华东图书120.0010.00
2Bob华东文具30.00NULL
3Alice华东文具50.005.00
4Carol华西图书200.0020.00
5Dave华西文具80.00NULL
6Carol华西图书150.0015.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_amountmaximum_amount
30.00200.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;

结果为:

regionorder_counttotal_amount
华东3200.00
华西3430.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 都相同的行才属于同一组:

regioncategoryorder_counttotal_amount
华东图书1120.00
华东文具280.00
华西图书2350.00
华西文具180.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 会先排除文具订单,然后再分组和聚合:

regionorder_counttotal_amount
华东1120.00
华西2350.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;

结果只有华西地区:

regiontotal_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 的书写顺序和逻辑处理顺序并不相同。理解下面的顺序,有助于判断某个阶段能使用哪些数据:

顺序子句作用
1FROM确定数据来源
2WHERE过滤明细行
3GROUP BY将剩余的行分组
4HAVING过滤分组结果
5SELECT计算并选择最终输出列
6ORDER BY对最终结果排序
7LIMIT限制返回行数(若有使用)

例如,要查看销售总额最高的地区,可以这样写:

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;
regionbook_amountstationery_amount
华东120.0080.00
华西350.0080.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 表,尝试写出下面三条查询:

  1. 统计每位客户的订单数量和消费总额,并按消费总额从高到低排序。
  2. 找出平均订单金额不低于 100 的客户。
  3. 分别统计填写了优惠金额和没有填写优惠金额的订单数量。

参考写法:

-- 练习 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 配合聚合函数,可以在一次查询中统计多个条件。
  • 写聚合查询时,先明确“统计对象、分组维度、过滤阶段”,再检查连接是否改变了原始行数。