聚合 — GROUP BY 实际在做什么
一句话总结
GROUP BY 把多行折叠成组;折叠后只剩能代表整个组的值,因此 SELECT 列表中只能出现分组键或聚合函数。
为什么需要它
“各渠道销售额”或“各状态订单数”这类问题需要的是摘要,而不是单独的行。聚合是把多行折叠为一行的运算,GROUP BY 则定义按什么标准折叠。
它如何工作
记住处理顺序,大多数容易混淆的问题都会迎刃而解。
FROM → WHERE → GROUP BY → 집계 → HAVING → SELECT → ORDER BY → LIMIT
- WHERE 在折叠前生效,HAVING 在折叠后生效。 因此不能写
WHERE count(*) > 5,因为执行到该阶段时,还没有可供计数的分组。 - SELECT 在 GROUP BY 之后执行,所以有些数据库引擎不允许在 HAVING 中使用 SELECT 创建的别名;而 ORDER BY 在 SELECT 之后,因此可以使用别名。
聚合函数会忽略 NULL。avg(price) 会把 price 为 NULL 的行也从分母中排除。因此,许多“平均值不对”的问题都来自 NULL 行。如果希望把它视为 0,就必须明确写成 avg(coalesce(price, 0))。
掌握 FILTER 子句可以让代码更简洁。它适合在同一个组内按不同条件计算多个聚合值。
SELECT channel,
count(*) FILTER (WHERE status = 'paid') AS paid_count,
count(*) FILTER (WHERE status = 'cancelled') AS cancelled_count
FROM orders
GROUP BY channel;
如果改用 WHERE,就要执行两次查询再进行 join。能否在一次扫描内完成,会直接形成性能差异。
按时间聚合时必须明确时区。如果 ordered_at 是带时区的类型,date_trunc('month', ordered_at) 的结果会随服务器当前时区设置而变化。应像 ordered_at AT TIME ZONE 'Asia/Seoul' 这样固定基准,才能保证无论在哪里执行都得到相同结果。
在实际项目中
种子数据中的退货凭证采用负数数量。如果不加思考地使用 sum(quantity),销量会悄悄减少,热门商品排名也可能颠倒。养成聚合前先检查数据符号与缺失值的习惯,能够避免报表事故。
还有一点:计算平均值时,必须始终追问分母是什么。计算各等级人均销售额时,是否把没有订单的客户计入分母,会让结果产生很大差异;两种答案都可能正确,但必须留下明确定义。
下一篇理论要学什么
下一篇将介绍窗口函数:它不折叠行,却能计算排名与累计值。随后在实验中,会同时求出各状态数量、各渠道销售额、月度汇总和热门商品,并观察负数数量如何改变结果。