☰
聚合函数:COUNT、SUM、AVG、MAX、MIN 一篇讲透
2026/10/1 20:45:06 网站建设 项目流程

同一张订单表,COUNT(*)是 6,COUNT(amount)却只有 4;这不是数据库算错,而是空值被跳过了。

看完这篇,你能把计数、求和、平均值、极值、分组过滤和空值处理一次核对清楚。

一、业务场景:对账为什么先差在计数

运营要三个指标:订单总数、已支付订单金额、每个城市的已支付客单价。这三个指标分别对应COUNT、SUM、AVG,看起来只是基础统计,但只要明细列里有空值,口径就容易偏。

这三个指标看似相近,但统计对象不同:

  • 订单总数回答“有多少条明细”
  • 已支付金额回答“实际收到多少钱”
  • 客单价回答“平均每笔已支付订单是多少钱”
order_iduser_idcityamountstatuspaid_at
1101北京100.00paid2026-09-01 09:00:00
2101北京200.00paid2026-09-01 10:00:00
3102上海NULLunpaidNULL
4103广州300.00paid2026-09-02 11:00:00
5104上海50.00refundedNULL
6105广州NULLunpaidNULL

这三个指标看起来只是简单计算,为什么一到对账就容易偏?

二、踩坑现象:同一张表为什么算出三个口径

我先跑了一条不带过滤条件的汇总 SQL,想确认全表底数。

SELECTCOUNT(*)ASorder_count,COUNT(amount)ASamount_count,SUM(amount)AStotal_amount,AVG(amount)ASavg_amount,MAX(amount)ASmax_amount,MIN(amount)ASmin_amountFROMorders;
order_countamount_counttotal_amountavg_amountmax_amountmin_amount
64650.00162.5000300.0050.00

如果按650 / 6计算,平均值是 108.33;数据库返回的是 162.5。差额来自两条amount为空值的订单,它们被COUNT(*)统计,却没有进入SUM(amount)和AVG(amount)。

我第一次排查这类问题时,盯着SUM看了很久,后来先查COUNT(amount),才发现真正的问题是非空行数和总行数不一致。

为什么SUM会跳过空值,而COUNT(*)没有跳过?

三、底层原理:聚合什么时候发生,空值什么时候被跳过

聚合函数(Aggregate Function)对一组行进行计算并返回单个值。没有GROUP BY时,整张结果集被视为一个组;有GROUP BY时,每个分组独立计算。

FROM 取表 ↓ WHERE 过滤原始行 ↓ GROUP BY 划分分组 ↓ 聚合函数逐组计算 ↓ HAVING 过滤分组结果 ↓ SELECT 输出列 ↓ ORDER BY 排序

上面的顺序是逻辑执行顺序,用来判断条件该放在哪一层;数据库优化器实际选择的物理执行路径可能不同。

写法统计口径空值处理无匹配行返回
COUNT(*)统计分组内所有行不跳过0
COUNT(1)统计分组内所有行不跳过0
COUNT(列)统计该列非空值数量跳过空值0
SUM(列)对该列非空值求和跳过空值NULL
AVG(列)SUM(列) / COUNT(列)跳过空值NULL
MAX(列)返回非空值中的最大值跳过空值NULL
MIN(列)返回非空值中的最小值跳过空值NULL

MAX、MIN可用于数值、日期和字符串。日期场景中,MAX表示最新,MIN表示最早;字符串结果取决于数据库排序规则。

我后来固定排查顺序:先看COUNT(*),再看COUNT(列),再看SUM和AVG。前两个数能判断空值数量,后两个数能判断分母是否被改。

记住:COUNT(*)数行,COUNT(列)数非空值;空值参与前者,不参与后者。

原理清楚后,怎么把业务口径准确写成 SQL?

四、解决方案:把业务口径翻译成聚合 SQL

4.1 全表指标和条件指标分开算

全表订单数用COUNT(*);已支付金额要先识别status,再对amount求和。没有匹配行时,SUM返回空值,需要用COALESCE转成 0。

SELECTCOUNT(*)ASorder_count,COUNT(CASEWHENstatus='paid'THEN1END)ASpaid_order_count,COALESCE(SUM(CASEWHENstatus='paid'THENamountEND),0)ASpaid_amount,AVG(CASEWHENstatus='paid'THENamountEND)ASpaid_avg_amountFROMorders;
order_countpaid_order_countpaid_amountpaid_avg_amount
63600.00200.0000

CASE不匹配时默认返回空值,SUM、AVG会跳过这些行。条件计数在命中时返回 1,不匹配时不要写ELSE 0;条件平均同样不要写ELSE 0,否则未支付订单会进入平均值分母。

记住:条件计数返回 1,不匹配返回空值;条件平均不要写ELSE 0。

4.2 分组统计与分组过滤

按城市统计时,GROUP BY负责划分分组,聚合函数负责计算每个城市的指标。

SELECTcity,COUNT(*)ASorder_count,COUNT(CASEWHENstatus='paid'THEN1END)ASpaid_order_count,COALESCE(SUM(CASEWHENstatus='paid'THENamountEND),0)ASpaid_amount,AVG(CASEWHENstatus='paid'THENamountEND)ASpaid_avg_amount,MAX(paid_at)ASlatest_paid_atFROMordersGROUPBYcityORDERBYpaid_amountDESC,city;
cityorder_countpaid_order_countpaid_amountpaid_avg_amountlatest_paid_at
北京22300.00200.00002026-09-01 10:00:00
广州21300.00300.00002026-09-02 11:00:00
上海200.00NULLNULL

如果只保留存在已支付订单的城市,在末尾增加HAVING。

HAVINGCOUNT(CASEWHENstatus='paid'THEN1END)>0
业务需求放置位置原因
只统计已支付订单WHERE聚合前排除无关行
只统计指定时间范围WHERE条件来自原始行
只保留支付金额大于 200 的城市HAVING条件依赖SUM结果
只保留已支付订单数大于 1 的城市HAVING条件依赖COUNT结果

记住:WHERE过滤行,HAVING过滤组;依赖聚合结果的条件,只能放在HAVING。

4.3 极值要和整行信息一起取

MAX(amount)只返回一个数值,不会自动告诉你对应订单号。要取最高金额对应的整行,可以用子查询。

SELECTorder_id,user_id,amountFROMordersWHEREstatus='paid'ANDamount=(SELECTMAX(amount)FROMordersWHEREstatus='paid');

如果最高金额有多条订单,子查询会返回多行。只保留一条可以用排名函数;要保留并列结果则用RANK。

WITHrankedAS(SELECTorder_id,user_id,amount,RANK()OVER(ORDERBYamountDESC)ASrnkFROMordersWHEREstatus='paid')SELECTorder_id,user_id,amountFROMrankedWHERErnk=1;

4.4 去重、平均口径与窗口函数

写法用途提醒
COUNT(DISTINCT user_id)统计去重用户数常见写法
SUM(DISTINCT amount)去重后求和业务口径少见,慎用
AVG(DISTINCT amount)去重后求平均容易偏离业务含义
MAX(DISTINCT amount)与 MAX(amount) 结果相同不写 DISTINCT
MIN(DISTINCT amount)与 MIN(amount) 结果相同不写 DISTINCT
业务口径写法
空值按 0 参与平均SUM(COALESCE(score, 0)) / COUNT(*)
加权平均SUM(score * weight) / SUM(weight)
避免整数平均值AVG(CAST(score AS DECIMAL(10,2)))

窗口函数(Window Function)保留明细行,适合在订单明细旁追加用户累计金额。

SELECTorder_id,user_id,amount,SUM(amount)OVER(PARTITIONBYuser_id)ASuser_totalFROMorders;

GROUP BY会把每组合并成一行;OVER(PARTITION BY user_id)不会减少行数,只是在每行旁边追加窗口范围内的聚合结果。

五、实操验证与总结延伸

以下脚本以 MySQL 8.x 为例。你可以先建表并插入示例数据,再执行前面的汇总和分组查询。

DROPTABLEIFEXISTSorders;CREATETABLEorders(order_idINTPRIMARYKEY,user_idINTNOTNULL,cityVARCHAR(20)NOTNULL,amountDECIMAL(10,2),statusVARCHAR(20)NOTNULL,paid_atDATETIME);INSERTINTOorders(order_id,user_id,city,amount,status,paid_at)VALUES(1,101,'北京',100.00,'paid','2026-09-01 09:00:00'),(2,101,'北京',200.00,'paid','2026-09-01 10:00:00'),(3,102,'上海',NULL,'unpaid',NULL),(4,103,'广州',300.00,'paid','2026-09-02 11:00:00'),(5,104,'上海',50.00,'refunded',NULL),(6,105,'广州',NULL,'unpaid',NULL);

执行验证查询。

SELECTCOUNT(*)ASorder_count,COUNT(amount)ASamount_count,COALESCE(SUM(CASEWHENstatus='paid'THENamountEND),0)ASpaid_amount,AVG(CASEWHENstatus='paid'THENamountEND)ASpaid_avg_amount,MAX(amount)ASmax_amount,MIN(amount)ASmin_amountFROMorders;

预期结果如下,不同数据库的小数位数可能不同。

order_count | amount_count | paid_amount | paid_avg_amount | max_amount | min_amount 6 | 4 | 600.00 | 200.0000 | 300.00 | 50.00

分组查询预期结果如下。

city | order_count | paid_order_count | paid_amount | paid_avg_amount 北京 | 2 | 2 | 300.00 | 200.0000 广州 | 2 | 1 | 300.00 | 300.0000 上海 | 2 | 0 | 0.00 | NULL

记住:验证聚合结果时,同时核对行数、非空数、分母和空集返回值;只看 SQL 执行成功没有意义。

聚合函数解决的是一组行到一个值的问题。COUNT回答多少,SUM回答总量,AVG回答平均水平,MAX和MIN回答边界。真正决定结果的不是函数名,而是统计口径。

  • 数所有行用COUNT(*),数非空值用COUNT(列)
  • 空集计数是 0,空集的SUM、AVG、MAX、MIN是空值
  • 条件聚合用CASE,条件平均不要写ELSE 0
  • 行过滤用WHERE,分组过滤用HAVING
  • 极值要带整行时,用子查询、窗口函数或排名函数
  • 大表分组检查分组列和过滤列的联合索引
  • COUNT(DISTINCT)有排序或哈希开销,固定报表可考虑汇总表或物化视图
  • 用EXPLAIN确认是否走索引、是否出现临时表或排序

术语速查表

术语说明
聚合函数(Aggregate Function)对一组行计算并返回单个值
空值(NULL)表示未知,不等同于 0 或空字符串
分组(GROUP BY)按指定列把行划分为多个集合
分组过滤(HAVING)对聚合后的分组结果进行过滤
去重(DISTINCT)对重复值先去重再计算
窗口函数(Window Function)在保留明细行的基础上计算窗口范围内的聚合值
条件聚合(Conditional Aggregation)通过CASE控制哪些行进入聚合函数

参考链接

  • MySQL 8.4 Reference Manual:Aggregate Function Descriptions
  • PostgreSQL Documentation:Aggregate Functions
  • SQLite Documentation:Built-in Aggregate Functions

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询