返回首页

09|聚合函数与分组统计

适用版本:MySQL 8.0.x

如果说单表查询和多表 JOIN 解决的是“把哪些行查出来”,那么聚合函数与分组统计解决的就是另一个更贴近业务决策的问题:

  • 一共有多少条订单?
  • 今日支付金额总和是多少?
  • 每个部门有多少人?
  • 哪个城市的客户平均客单价最高?
  • 每个月新增多少用户?
  • 某类商品的退款率是多少?

这些问题都不是简单地返回明细,而是在做汇总、统计、对比、分析。这类能力在后台报表、运营数据面板、BI 查询、监控指标统计中极其常见。

很多人刚接触聚合时,会先学会 COUNT(*)SUM(amount)GROUP BY department_id,然后很快就遇到新的困惑:

  • 为什么有些字段能写在 SELECT 中,有些不行?
  • WHEREHAVING 到底有什么区别?
  • 为什么 COUNT(*)COUNT(column) 结果不一样?
  • 为什么一分组后,结果行数突然变少?
  • 为什么某些统计 SQL 看起来能跑,但实际上结果是错的?

这篇文章就围绕这些问题展开,把 MySQL 8.0 中最常用的聚合与分组统计讲清楚。


一、什么是聚合函数

聚合函数(Aggregate Function)是指:

对多行数据进行汇总计算,并返回一个统计结果的函数。

最常见的聚合函数有:

函数 作用 常见用途
COUNT() 计数 订单数、用户数、记录数
SUM() 求和 销售额、支付金额、库存总数
AVG() 平均值 平均工资、平均客单价
MAX() 最大值 最高价格、最新时间
MIN() 最小值 最低价格、最早时间
GROUP_CONCAT() 组内拼接 合并标签、拼接明细

如果没有 GROUP BY,聚合函数通常会对整个结果集做汇总;如果有 GROUP BY,聚合函数会对每个分组分别计算。


二、示例表结构

后文继续以订单场景举例。假设有如下表:

CREATE TABLE orders (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    order_no VARCHAR(50) NOT NULL,
    customer_id BIGINT NOT NULL,
    city VARCHAR(50) NOT NULL,
    order_status VARCHAR(20) NOT NULL,
    pay_amount DECIMAL(10,2) NOT NULL,
    discount_amount DECIMAL(10,2) DEFAULT 0,
    created_at DATETIME NOT NULL,
    paid_at DATETIME DEFAULT NULL
);

如果你想象自己正在做一个运营后台,这张表已经足够支撑大量常见统计需求。


三、没有 GROUP BY 时:对整个结果集做汇总

1. 统计总订单数

SELECT COUNT(*) AS total_orders
FROM orders;

2. 统计已支付订单总金额

SELECT SUM(pay_amount) AS total_paid_amount
FROM orders
WHERE order_status = 'PAID';

3. 查询最高支付金额与最低支付金额

SELECT MAX(pay_amount) AS max_amount,
       MIN(pay_amount) AS min_amount
FROM orders;

4. 查询平均支付金额

SELECT AVG(pay_amount) AS avg_amount
FROM orders
WHERE order_status = 'PAID';

这类写法的特点是:

  • 结果通常只返回一行
  • 统计对象是筛选后的整个结果集

四、COUNT(*)COUNT(1)COUNT(column) 有什么区别

这是聚合里非常经典的问题。

1. COUNT(*)

表示统计行数,不关心具体列值是否为 NULL

SELECT COUNT(*)
FROM orders;

2. COUNT(column)

表示统计该列NULL 的行数。

SELECT COUNT(paid_at)
FROM orders;

如果有些订单还未支付,paid_atNULL,那么这些行不会被 COUNT(paid_at) 统计进去。

3. 一个简单对比

写法 统计对象
COUNT(*) 全部行数
COUNT(1) 通常等价于全部行数
COUNT(paid_at) paid_at 不为 NULL 的行数

4. 实际使用建议

  • 统计总记录数,优先写 COUNT(*)
  • 明确要统计某字段非空数量时,再写 COUNT(column)

不要把两者混用,否则统计口径会悄悄发生变化。


五、GROUP BY:把明细拆成多个统计组

1. 按城市统计订单数

SELECT city,
       COUNT(*) AS order_count
FROM orders
GROUP BY city;

执行结果的含义是:

  • city 的取值分组
  • 每个城市各算一组
  • 对每组统计订单条数

2. 按订单状态统计金额总和

SELECT order_status,
       SUM(pay_amount) AS total_amount
FROM orders
GROUP BY order_status;

3. 多列分组

如果想看“每个城市下各状态的订单数”:

SELECT city,
       order_status,
       COUNT(*) AS order_count
FROM orders
GROUP BY city, order_status;

这时分组粒度就从“城市”变成了“城市 + 状态”的组合。

4. 怎么理解分组粒度

你可以把 GROUP BY city, order_status 理解成:

  • 北京 + PAID 是一组
  • 北京 + CANCELLED 是一组
  • 上海 + PAID 是一组
  • 上海 + CANCELLED 是一组

分组字段越多,统计粒度通常越细。


六、SELECT 中为什么有些列不能随便写

很多初学者会写出这样的 SQL:

SELECT city,
       order_no,
       COUNT(*) AS order_count
FROM orders
GROUP BY city;

在 MySQL 8.0 默认启用 ONLY_FULL_GROUP_BY 的情况下,这通常会报错。原因是:

  • city 是分组字段,可以出现在 SELECT
  • COUNT(*) 是聚合结果,也可以出现
  • order_no 既不是分组字段,也不是聚合结果
  • 同一个城市下可能有很多订单号,数据库不知道该返回哪一个

正确原则

当使用 GROUP BY 时,SELECT 中的列通常应满足以下之一:

  1. 出现在 GROUP BY
  2. 是聚合函数结果
  3. 在严格可证明函数依赖的特殊场景中可推导,但日常写法里不建议依赖这一点

正确改写示例

SELECT city,
       COUNT(*) AS order_count
FROM orders
GROUP BY city;

如果你真的想看每个城市下最晚一笔订单号,就得先明确规则,例如取最新时间对应的订单号,而不是随手把 order_no 放进去。


七、WHEREHAVING:两个过滤阶段,不是一回事

这是分组统计里最重要的概念之一。

1. WHERE:分组前过滤

SELECT city,
       COUNT(*) AS paid_order_count
FROM orders
WHERE order_status = 'PAID'
GROUP BY city;

含义是:

  1. 先筛出已支付订单
  2. 再按城市分组统计

2. HAVING:分组后过滤

SELECT city,
       COUNT(*) AS order_count
FROM orders
GROUP BY city
HAVING COUNT(*) >= 100;

含义是:

  1. 先按城市分组
  2. 再筛出订单数不少于 100 的城市

3. 一个对照表

子句 执行阶段 典型用途
WHERE 分组前 过滤明细数据
HAVING 分组后 过滤聚合结果

4. 常见组合写法

SELECT city,
       COUNT(*) AS paid_order_count,
       SUM(pay_amount) AS paid_total_amount
FROM orders
WHERE order_status = 'PAID'
GROUP BY city
HAVING SUM(pay_amount) >= 100000;

这里:

  • WHERE 先把“未支付订单”排除
  • HAVING 再筛出支付总额超过 10 万的城市

八、条件聚合:报表统计里非常高频

很多运营报表并不是简单看总数,而是想在同一个结果集中看到多种状态的统计值。这时条件聚合非常有用。

1. 按城市统计总订单数、支付订单数、取消订单数

SELECT city,
       COUNT(*) AS total_orders,
       SUM(CASE WHEN order_status = 'PAID' THEN 1 ELSE 0 END) AS paid_orders,
       SUM(CASE WHEN order_status = 'CANCELLED' THEN 1 ELSE 0 END) AS cancelled_orders
FROM orders
GROUP BY city;

2. 统计不同状态的金额

SELECT city,
       SUM(CASE WHEN order_status = 'PAID' THEN pay_amount ELSE 0 END) AS paid_amount,
       SUM(CASE WHEN order_status = 'REFUNDED' THEN pay_amount ELSE 0 END) AS refunded_amount
FROM orders
GROUP BY city;

3. 为什么条件聚合很实用

因为它能避免你为了不同状态反复查询多次,在同一条 SQL 中就完成结构化统计,非常适合:

  • 运营看板
  • 业务日报
  • 后台概览页
  • 财务口径拆分统计

九、按时间分组:日报、周报、月报的基础

1. 按天统计订单数

SELECT DATE(created_at) AS stat_date,
       COUNT(*) AS order_count
FROM orders
GROUP BY DATE(created_at)
ORDER BY stat_date ASC;

2. 按月统计支付金额

SELECT DATE_FORMAT(paid_at, '%Y-%m') AS stat_month,
       SUM(pay_amount) AS total_paid_amount
FROM orders
WHERE paid_at IS NOT NULL
GROUP BY DATE_FORMAT(paid_at, '%Y-%m')
ORDER BY stat_month ASC;

3. 时间分组的一个实践提醒

DATE(created_at)DATE_FORMAT(paid_at, '%Y-%m') 这样的写法,表达很直观,但在大数据量场景下,如果你还要依赖索引做高性能过滤,通常要把:

  • 时间范围过滤放在 WHERE
  • 时间格式化主要用于展示或分组层

例如:

SELECT DATE(created_at) AS stat_date,
       COUNT(*) AS order_count
FROM orders
WHERE created_at >= '2026-06-01 00:00:00'
  AND created_at <  '2026-07-01 00:00:00'
GROUP BY DATE(created_at)
ORDER BY stat_date ASC;

这样通常比“先对整列做函数,再过滤”更稳一些。


十、DISTINCT 与聚合:去重统计要分清口径

1. 统计去重客户数

SELECT COUNT(DISTINCT customer_id) AS unique_customer_count
FROM orders;

这统计的是“有下单记录的不同客户数”,不是订单总数。

2. 分组后统计每个城市的去重客户数

SELECT city,
       COUNT(DISTINCT customer_id) AS customer_count
FROM orders
GROUP BY city;

3. 为什么这个很重要

很多业务指标名字看起来很像,但含义完全不同:

  • 订单数
  • 下单人数
  • 支付订单数
  • 支付用户数

如果不把“是否去重”想清楚,统计结果就会偏差很大。


十一、字符串聚合:GROUP_CONCAT() 的实用场景

有时你不只是想看数字,还想把同组内的某些文本拼起来。

例如:按城市查看该城市出现过的订单状态。

SELECT city,
       GROUP_CONCAT(DISTINCT order_status ORDER BY order_status SEPARATOR ', ') AS status_list
FROM orders
GROUP BY city;

GROUP_CONCAT() 的典型用途

  • 拼接同组标签
  • 拼接用户角色列表
  • 拼接订单状态集合
  • 辅助导出或调试分析

不过要注意,GROUP_CONCAT() 更适合展示型、辅助型场景,不适合作为严肃业务计算的唯一依据。


十二、分组后的排序与汇总行

1. 按统计结果排序

例如按支付金额从高到低查看各城市排行:

SELECT city,
       SUM(pay_amount) AS total_amount
FROM orders
WHERE order_status = 'PAID'
GROUP BY city
ORDER BY total_amount DESC;

这里 ORDER BY 既可以写别名 total_amount,也可以直接写聚合表达式。

2. 使用 WITH ROLLUP 查看汇总行

MySQL 支持在分组结果后追加总计行。

SELECT city,
       SUM(pay_amount) AS total_amount
FROM orders
WHERE order_status = 'PAID'
GROUP BY city WITH ROLLUP;

结果中通常会多出一行,cityNULL,表示整体汇总。

3. 什么时候适合用 WITH ROLLUP

  • 快速查看分组总计
  • 生成简单汇总报表
  • 临时分析时省去额外再写一条总计 SQL

不过如果你要输出非常正式的报表,通常还是会在应用层对汇总行做更明确的格式化处理。


十三、常见坑:聚合统计最容易错在哪里

1. 把非分组字段随手写进 SELECT

这在 MySQL 8.0 默认严格模式下通常会直接报错,但如果团队有人关闭了相关模式,就可能出现“SQL 能跑但结果并不可靠”的情况。

2. COUNT(column)COUNT(*) 混淆

如果 column 里有 NULL,统计结果就会小于总行数。很多“数量为什么少了”的问题,都是从这里来的。

3. WHEREHAVING 用反

例如把聚合条件写到 WHERE 中:

WHERE COUNT(*) > 10

这是不对的。聚合后的条件应该放在 HAVING 中。

4. 忽略分组粒度

同样是统计金额:

  • 按城市分组
  • 按城市 + 月份分组
  • 按城市 + 月份 + 状态分组

这三种结果完全不是一个粒度。粒度不同,业务含义就不同。

5. 明细 JOIN 后直接聚合,导致统计被放大

比如订单表 JOIN 订单明细表后再统计订单金额,如果一个订单有多条明细,订单金额可能会被重复累计。此时要先想清楚:

  • 我要统计的是订单粒度,还是明细粒度?
  • 是否应该先对明细表汇总,再和订单表关联?

这是多表统计里非常高频的错误来源。

6. 对时间列做函数处理后再过滤,影响查询效率

比如:

WHERE DATE(created_at) = '2026-06-09'

虽然可读,但通常不如范围过滤更稳:

WHERE created_at >= '2026-06-09 00:00:00'
  AND created_at <  '2026-06-10 00:00:00'

十四、实践建议:把统计 SQL 写得更接近业务口径

1. 先明确统计对象是什么

是:

  • 订单数?
  • 支付订单数?
  • 下单人数?
  • 支付用户数?
  • 金额总和?
  • 去重后的金额贡献用户数?

口径不清,再正确的 SQL 也可能统计错业务指标。

2. 先筛选,再分组,再聚合,再排序

很多统计 SQL 其实都可以按这条思路去组织:

  1. FROM 确定数据来源
  2. WHERE 过滤明细
  3. GROUP BY 定义统计粒度
  4. 聚合函数计算指标
  5. HAVING 过滤分组结果
  6. ORDER BY 做排序展示

这个顺序理顺后,写统计 SQL 会稳定很多。

3. 给指标起清晰别名

例如:

  • order_count
  • paid_order_count
  • total_paid_amount
  • avg_order_amount

比起模糊的 cntsum1num2,清晰别名会极大提升 SQL 的可维护性。

4. 复杂报表优先拆层

如果一条统计 SQL 里既有多表 JOIN、又有多种聚合、还要做条件过滤和排序,建议考虑:

  • 先用子查询或 CTE 产生中间结果
  • 再在外层做最终聚合

这样比一口气把所有逻辑揉在一层里更不容易出错。


十五、小结

聚合函数与分组统计,是 SQL 从“查数据”走向“看数据、算数据、分析数据”的关键一步。这篇文章重点覆盖了:

  • COUNTSUMAVGMAXMINGROUP_CONCAT 的基本作用
  • 没有 GROUP BY 时如何对整体结果集做汇总
  • GROUP BY 时如何按不同粒度做统计
  • COUNT(*)COUNT(column) 的差别
  • WHEREHAVING 的职责分工
  • 条件聚合、时间分组、去重统计、汇总行等实战写法
  • 聚合统计里最常见的逻辑错误与实践建议

真正写统计 SQL 时,最重要的从来不是“会不会某个函数”,而是:

  • 统计口径是否清楚
  • 分组粒度是否正确
  • 数据是否被重复计算
  • 过滤条件是在明细层还是分组层生效

把这些问题想明白,聚合函数就不再只是语法点,而会变成你理解和表达业务数据的一种工具。


📝 版权声明:本文为原创技术博客,转载请注明出处。

如文章中存在错误或不准确之处,欢迎在评论区指正,感谢您的阅读与支持!

上一篇

08|子查询与集合操作

下一篇

10| 索引原理与类型