
同一张订单表COUNT(*)是 6COUNT(amount)却只有 4这不是数据库算错而是空值被跳过了。看完这篇你能把计数、求和、平均值、极值、分组过滤和空值处理一次核对清楚。一、业务场景对账为什么先差在计数运营要三个指标订单总数、已支付订单金额、每个城市的已支付客单价。这三个指标分别对应COUNT、SUM、AVG看起来只是基础统计但只要明细列里有空值口径就容易偏。这三个指标看似相近但统计对象不同订单总数回答“有多少条明细”已支付金额回答“实际收到多少钱”客单价回答“平均每笔已支付订单是多少钱”order_iduser_idcityamountstatuspaid_at1101北京100.00paid2026-09-01 09:00:002101北京200.00paid2026-09-01 10:00:003102上海NULLunpaidNULL4103广州300.00paid2026-09-02 11:00:005104上海50.00refundedNULL6105广州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_amount64650.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(*)统计分组内所有行不跳过0COUNT(1)统计分组内所有行不跳过0COUNT(列)统计该列非空值数量跳过空值0SUM(列)对该列非空值求和跳过空值NULLAVG(列)SUM(列) / COUNT(列)跳过空值NULLMAX(列)返回非空值中的最大值跳过空值NULLMIN(列)返回非空值中的最小值跳过空值NULLMAX、MIN可用于数值、日期和字符串。日期场景中MAX表示最新MIN表示最早字符串结果取决于数据库排序规则。我后来固定排查顺序先看COUNT(*)再看COUNT(列)再看SUM和AVG。前两个数能判断空值数量后两个数能判断分母是否被改。记住COUNT(*)数行COUNT(列)数非空值空值参与前者不参与后者。原理清楚后怎么把业务口径准确写成 SQL四、解决方案把业务口径翻译成聚合 SQL4.1 全表指标和条件指标分开算全表订单数用COUNT(*)已支付金额要先识别status再对amount求和。没有匹配行时SUM返回空值需要用COALESCE转成 0。SELECTCOUNT(*)ASorder_count,COUNT(CASEWHENstatuspaidTHEN1END)ASpaid_order_count,COALESCE(SUM(CASEWHENstatuspaidTHENamountEND),0)ASpaid_amount,AVG(CASEWHENstatuspaidTHENamountEND)ASpaid_avg_amountFROMorders;order_countpaid_order_countpaid_amountpaid_avg_amount63600.00200.0000CASE不匹配时默认返回空值SUM、AVG会跳过这些行。条件计数在命中时返回 1不匹配时不要写ELSE 0条件平均同样不要写ELSE 0否则未支付订单会进入平均值分母。记住条件计数返回 1不匹配返回空值条件平均不要写ELSE 0。4.2 分组统计与分组过滤按城市统计时GROUP BY负责划分分组聚合函数负责计算每个城市的指标。SELECTcity,COUNT(*)ASorder_count,COUNT(CASEWHENstatuspaidTHEN1END)ASpaid_order_count,COALESCE(SUM(CASEWHENstatuspaidTHENamountEND),0)ASpaid_amount,AVG(CASEWHENstatuspaidTHENamountEND)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(CASEWHENstatuspaidTHEN1END)0业务需求放置位置原因只统计已支付订单WHERE聚合前排除无关行只统计指定时间范围WHERE条件来自原始行只保留支付金额大于 200 的城市HAVING条件依赖SUM结果只保留已支付订单数大于 1 的城市HAVING条件依赖COUNT结果记住WHERE过滤行HAVING过滤组依赖聚合结果的条件只能放在HAVING。4.3 极值要和整行信息一起取MAX(amount)只返回一个数值不会自动告诉你对应订单号。要取最高金额对应的整行可以用子查询。SELECTorder_id,user_id,amountFROMordersWHEREstatuspaidANDamount(SELECTMAX(amount)FROMordersWHEREstatuspaid);如果最高金额有多条订单子查询会返回多行。只保留一条可以用排名函数要保留并列结果则用RANK。WITHrankedAS(SELECTorder_id,user_id,amount,RANK()OVER(ORDERBYamountDESC)ASrnkFROMordersWHEREstatuspaid)SELECTorder_id,user_id,amountFROMrankedWHERErnk1;4.4 去重、平均口径与窗口函数写法用途提醒COUNT(DISTINCT user_id)统计去重用户数常见写法SUM(DISTINCT amount)去重后求和业务口径少见慎用AVG(DISTINCT amount)去重后求平均容易偏离业务含义MAX(DISTINCT amount)与 MAX(amount) 结果相同不写 DISTINCTMIN(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(CASEWHENstatuspaidTHENamountEND),0)ASpaid_amount,AVG(CASEWHENstatuspaidTHENamountEND)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 ManualAggregate Function DescriptionsPostgreSQL DocumentationAggregate FunctionsSQLite DocumentationBuilt-in Aggregate Functions