GROUPING SETS 在一条查询中构建多层聚合
GROUPING SETS 让一条 SQL 同时生成多种聚合层级。销售报表可以一起返回地区与商品明细汇总、地区小计和总计。结果中混合了不同种类的行,因此,识别每行属于哪个层级,与算对金额同样重要;如果下游把所有结果行都当成普通明细,即使数据库计算正确,报表仍可能出错。 声明报表需要的层级 GROUPING SETS ((region,product),(region),()) 声明三种分组。第一种为每个实际出现的地区与商品组合输出一行,第二种为每个地区输出一行,空集合则汇总全部选中的输入行。这是在声明结果语义,具体执行方式由优化器选择。它可以在不同层级共享工作,但语法本身不保证固定的物理扫描次数,也不会消除排序和内存成本。 ROLLUP(region,product) 是相同前缀集合的简写,并包含空集合。交换两列顺序,会改变小计维度。CUBE(region,product) 还包括只按商品汇总的层级,覆盖维度的全部子集。六个独立维度对应六十四种分组集合,不是只输出六十四行;每种集合还可能包含很多不同分组,所以结果规模可能远大于层级数。 所有层级都汇总 FROM 和 WHERE 选出的总体。如果过滤条件只保留某个月的销售,总计也只代表该月。如果不同层级需要不同输入总体,直接对一个共享输入声明这些集合,可能并不符合需求。比较另一张报表的总额之前,要先确认日期、业务状态等筛选范围一致,不能只对照列名。 用 GROUPING 标记层级 某个分组集合没有使用的维度列,会在输出中填入 SQL NULL。但原始数据本来就可能含有 NULL,因此仅靠哪些列为空,无法可靠判断这是小计还是普通分组。GROUPING(region,product) 返回位掩码,说明当前集合缺少了哪些分组表达式;最右边的参数对应最低有效位。 在这里,零表示两个维度都参与分组,一表示省略商品,三表示两个维度都省略。真实空地区的小计与全表总计,可能同时显示空地区、空商品,但掩码不同。应把这个标记纳入输出契约,只有层级明确表示跨地区汇总时,才生成“所有地区”等展示标签。 表达式是否参与分组,与表达式的值是否非空,是不同问题。GROUPING 报告前者,其参数必须匹配查询中的分组表达式,并不是测试任意值是否为空的普通函数。向下游提供数值层级或其他无歧义标识,比把每个空值都替换为“小计”更可靠,也能保留真实缺失维度的含义。 坏掉的样子 把结果中所有 total 再相加,会让同一笔销售在每个层级重复计入。需要进一步汇总时,应先选择一个层级,或直接取已经算好的总计。平均值和去重计数更不能随意相加:没有按样本数加权的地区平均值,通常不能直接平均成整体平均值;重叠集合的去重数量也不能简单求和。聚合层级不是彼此独立的新增交易。 空分组集合在没有输入行时仍形成一个分组,但后续 HAVING 条件可以把这行过滤掉;没有这类过滤时,其中 count 为零,sum 为 NULL。因此,金额为空不等于查询没有返回总计行。应明确报表如何展示空总体,而不是把空值自动当作执行失败。还要避免无意重复声明分组集合;重复层级可能产生重复聚合行,并不代表多了一份独立证据。 快速验证 把下面内容保存为 grouping.sql,在可丢弃的 PostgreSQL 18 数据库中执行 psql -X -v ON_ERROR_STOP=1 -f grouping.sql。数据特意包含一个真实空地区,脚本用事务包住临时对象,末尾回滚清理。 ...