GROUP BY 与聚合
GROUP BY 按完整分组键元组划分输入行,为每组维护聚合,再将每组缩成一行结果。增加键列,可以把已有组细分。当前查询中每个源行恰好属于一组,每组恰好输出一行,这两条规则决定数值与结果粒度。
COUNT(*) 数全部行,COUNT(column)、SUM(column)、AVG(column) 忽略该列的 NULL。NULL 出现时,行数与非空值数便不再相同。
GROUP BY 如何工作
例如按地区统计订单与金额:
SELECT
region,
COUNT(*) AS orders,
COUNT(amount) AS recorded_amounts,
SUM(amount) AS revenue,
AVG(amount) AS average_amount
FROM sales
GROUP BY region;
当前键为 (region);若改成 region、channel,完整键就为二元组,North / online 与 North / store 必须分开。
COUNT(*) 接收组内每行;COUNT(amount) 只计非 NULL 金额;SUM(amount) 加非 NULL 值,全空则为 NULL;AVG(amount) 用非空值总和除以 COUNT(amount),不是 COUNT(*)。四个聚合产生每组一行中的四列,不是四行。
GROUP BY 逐步演示
默认七行按下列顺序输入,键为 region。
| order | region | channel | amount |
|---|---|---|---|
| o1 | North | online | 120 |
| o2 | South | store | 90 |
| o3 | North | store | 80 |
| o4 | West | online | 40 |
| o5 | South | online | 60 |
| o6 | North | online | NULL |
| o7 | West | store | 110 |
- o1 创建 North,COUNT(*)=1、COUNT(amount)=1、SUM=120。
- o2 创建 South,两计数为 1,SUM=90。
- o3 虽不邻接 o1,键仍是 North,更新为两计数 2、SUM=200。
- o4 创建 West,两计数 1、SUM=40。
- o5 回到 South,两计数 2、SUM=150。
- o6 金额 NULL,North 的 COUNT(*) 从 2 到 3,COUNT(amount) 仍为 2,SUM 仍为 200。
- o7 更新 West,两计数 2、SUM=150。
结束后 North 的平均为 200 / 2 = 100,不是除以 3;South、West 都为 150 / 2 = 75。各组输出一行:
| region | COUNT(*) | COUNT(amount) | SUM(amount) | AVG(amount) |
|---|---|---|---|---|
| North | 3 | 2 | 200 | 100 |
| South | 2 | 2 | 150 | 75 |
| West | 2 | 2 | 150 | 75 |
显示次序是讲解顺序,SQL 需要 ORDER BY 才保证排序。画面左侧保留源表,中间把当前行送入组,右侧写结果。前三行说明机制后加快重复分配,NULL 更新单独放慢,以区分两种计数。
分组粒度怎样改变结果
| 分组 | 键 | 组数 |
|---|---|---|
| 聚合但无 GROUP BY | 全局一个组 | 1 |
| GROUP BY region | (region) | 3 |
| GROUP BY region, channel | (region, channel) | 6 |
加 channel 只能细分地区组,不能把原本不同地区合到一起,因为完整键仍含 region。成员变化如下:
| 地区原组 | 完整新键 | 成员 |
|---|---|---|
| North:o1、o3、o6 | (North, online) | o1、o6 |
| North:o1、o3、o6 | (North, store) | o3 |
| South:o2、o5 | (South, store) | o2 |
| South:o2、o5 | (South, online) | o5 |
| West:o4、o7 | (West, online) | o4 |
| West:o4、o7 | (West, store) | o7 |
North 从一个三行组变成两组,子组成员仍合为 o1、o3、o6,没有丢失或重复。其他地区同理,解释了 3→6,计数栏归纳为 1→3→6。
地区粒度每行表示一个地区,地区加渠道粒度每行表示一对值。下游若把地区总数连接到多个渠道行后再求和,会重复计入;必须明确匹配粒度与再聚合规则。
聚合查询无 GROUP BY 时,整个输入形成一个全局组,返回一行且无分组键列,不是返回原始行。
SQL 规定逻辑结果,不指定物理算法。数据库可哈希键、排序、利用索引、流式处理有序输入或并行合并部分聚合,时间和内存不同,逻辑分组相同。使用前应明确一结果行代表什么、完整键是否表达该粒度、NULL 与分母规则、下游连接是否保持对应粒度。
边界情况
- 两行金额全 NULL:COUNT(*)=2、COUNT(amount)=0、SUM=NULL、AVG=NULL。
- 分组键 NULL 可归为同一组,组合键仍比较完整元组。这与等值连接中
NULL = NULL不为 TRUE 不同。 - 空输入带 GROUP BY region:没有组,返回零行。
- 空输入的无 GROUP BY 全局聚合:返回一行,两计数 0,SUM 和 AVG 为 NULL。
- 每行键唯一:每组只有一行,结果行数不减少。
常见错误
- 用 SUM(amount)/COUNT(*) 代替 AVG,遇到 NULL 就错。
- GROUP BY region 却直接选择 channel。一个地区可能有两渠道,没有单一组值;应聚合或放入键。标准允许某些函数依赖投影,方言行为也有差别,需明确依赖。
- 按源行相邻关系分组,同键非相邻也属于同组。
- 每个 NULL 行单独开组,相同完整分组键应归在一起。
- 没有 ORDER BY 却依赖一次运行的稳定顺序。
- 看见画布桶就断言物理计划是哈希聚合,桶只表示成员关系。
模型范围
本课展示逻辑分组、聚合 NULL 行为、粒度和行数。省略排序规则、精度溢出、DISTINCT 与有序集聚合、HAVING、分组集、函数依赖规则、优化器、索引、并行部分聚合、事务可见性、方言行为。实际查询需按数据库的 SQL 与执行计划规则核对。