本文是《PostgreSQL技术问答》系列文章中的一篇。关于这个系列的由来,可以参阅开篇文章:
《PostgreSQL技术问答-00 Why Postgres》
文章的编号只是一个标识,在系列中没有明确的逻辑顺序和意义。读者进行阅读时,不用太关注这个方面。
本文主要讨论的内容是PostgreSQL中聚合操作的复杂形式,笔者称之为: 复杂聚合。
什么是复杂聚合
复杂聚合不是一个正式的Postgres或者SQL的技术术语,是笔者对于这一类功能和操作的一个总结和命名。在官方技术文档中,其实这一类操作包括三个主要功能,就是Grouping Sets(分组集合),Cube(数据立方)和Rollup(汇总)。
笔者已经在另一篇博文中讨论过和聚合计算相关的问题。其实在里面有一个重要的问题没有涉及,就是普通的聚合计算,都是依据聚合计算所涉及的字段进行汇总的。但现实的业务需求,可能需要对多个聚合字段再次进行汇总或者聚合,常规的聚合计算SQL语句就不能满足这个需求了。当然,开发者可以在外部程序中,根据聚合结果集合进行再次,但我们希望可以在同一个SQL语句和处理中来解决这个问题。这时,就可能需要使用复杂聚合相关的功能了。
下面是一个简单的例子:
sql
// 按照品牌和规格进行汇总时
with D(store, brand,size,sales) as ( values
(1,'Apple',14, 10),
(2,'Apple',14, 8),
(1,'Apple',15, 20),
(1,'Dell',14, 15),
(1,'Dell',15, 5),
(2,'Dell',15, 9)
) select brand, size, sum(sales) sales from D group by 1,2;
brand | size | sales
-------+------+-------
Dell | 14 | 15
Dell | 15 | 14
Apple | 15 | 20
Apple | 14 | 18
(4 rows)
// 只按照品牌进行汇总
with D(store, brand,size,sales) as ( values
(1,'Apple',14, 10),
(2,'Apple',14, 8),
(1,'Apple',15, 20),
(1,'Dell',14, 15),
(1,'Dell',15, 5),
(2,'Dell',15, 9)
) select brand, sum(sales) sales from D group by 1;
brand | sales
-------+-------
Dell | 29
Apple | 38
(2 rows)
这个例子,前面,基于品牌和规格汇总了销售的数量,但如果我们需要进一步的汇总品牌的销售数量,可能就需要进行再次的外部汇总,或者重新只以品牌作为汇总依据进行计算,那就是后面另外一个SQL语句了。
我们当然希望,能使用一个语句和操作就完成这种多层次汇总的操作,就需要用到复杂聚合,这是一种更强大和灵活数据分析模式。其实,在Excel里面也有类似的思想和功能,就是所谓的"数据透视表(Prvot Table)"。PostgreSQL提供了几个相关的功能,来对其进行实现。我们后面进行详细讨论。
什么是Grouping Sets
首先是Grouping Sets,直译为分组集合,就是在聚合计算中,通过指定需要的特定的分组聚合字段,来控制聚合的结果。下面是一个简单的例子:
sql
// 品牌和规格小计,总计
select brand, size, sum(sales) sales from D
group by grouping sets((brand,size),(brand),(size),());
brand | size | sales
-------+------+-------
| | 67
Dell | 14 | 15
Dell | 15 | 14
Apple | 15 | 20
Apple | 14 | 18
Dell | | 29
Apple | | 38
| 14 | 33
| 15 | 34
(9 rows)
// 只看品牌和总计
select brand, sum(sales) sales from D group by grouping sets((brand),());
brand | sales
-------+-------
| 67
Dell | 29
Apple | 38
(3 rows)
注意这里的语法规则。grouping sets关键字跟在group by子句前缀之后,表明使用一个数据集的定义。数据集定义可以使用字段名和字段名的组合,或者使用()表示完全汇总。然后的汇总操作,就会根据这个定义,分别在不同的级别和范围内进行汇总操作计算。开发者可以完全灵活的组合所需要的汇总项目和级别。
Grouping Sets好像已经够用了,为什么需要Rollup和Cube呢
前面看到了Grouping Sets,因为可以自己定义所有可能的分组聚合计算的方式和范围,这已经基本上可以满足任意分组聚合的需求了,为什么Postgres还提供了另外两个Rollup和Cube聚合方式呢?先来看看Rollup和Cube的用法:
sql
// rollup 汇总
select store, brand, size, sum(sales) sales from D group by rollup(store, brand, size);
store | brand | size | sales
-------+-------+------+-------
| | | 67
1 | Apple | 14 | 10
1 | Dell | 15 | 5
2 | Apple | 14 | 8
2 | Dell | 15 | 9
1 | Dell | 14 | 15
1 | Apple | 15 | 20
1 | Dell | | 20
1 | Apple | | 30
2 | Dell | | 9
2 | Apple | | 8
1 | | | 50
2 | | | 17
(13 rows)
// 立方
select store, brand, size, sum(sales) sales from D group by cube(store, brand, size);
store | brand | size | sales
-------+-------+------+-------
| | | 67
1 | Apple | 14 | 10
1 | Dell | 15 | 5
2 | Apple | 14 | 8
2 | Dell | 15 | 9
1 | Dell | 14 | 15
1 | Apple | 15 | 20
1 | Dell | | 20
1 | Apple | | 30
2 | Dell | | 9
2 | Apple | | 8
1 | | | 50
2 | | | 17
| Dell | 14 | 15
| Dell | 15 | 14
| Apple | 15 | 20
| Apple | 14 | 18
| Dell | | 29
| Apple | | 38
2 | | 15 | 9
1 | | 15 | 25
2 | | 14 | 8
1 | | 14 | 25
| | 14 | 33
| | 15 | 34
(25 rows)
上面这个例子,让我们清晰的看到Rollup和Cube的差异。为了更好的说明问题,我们需要引入store这个汇总项目。首先我们可以看到,它们其实都是grouping sets的简化定义方式;Rollup是从定义列表开始出发,逐个项目进行汇总的分解,有一定的层次关系;而Cube,就像它的名字一样,就是从所有的维度来对数据进行监视,会将列表中所有字段进行组合,列举所有可能的汇总方式。
从技术文档上,我们也可以看到,Rollup和Cube都是Grouping sets的等效和简化书写模式,但实例和代码让我们可以更容易理解:
js
ROLLUP ( e1, e2, e3, ... ) 等效于
GROUPING SETS (
( e1, e2, e3, ... ),
...
( e1, e2 ),
( e1 ),
( )
)
CUBE ( a, b, c ) 等效于:
GROUPING SETS (
( a, b, c ),
( a, b ),
( a, c ),
( a ),
( b, c ),
( b ),
( c ),
( )
)
所以,Rollup和Cube其实就是Grouping Sets的语法糖,它们可以简化分组计算的SQL语句书写方式。但如何只需要对特定的字段进行分组,还是需要使用Grouping Sets的。
能对多个聚合字段再进行组合吗
是可以的,这几个复杂聚合,都支持在Group By 子句中的组合使用。例如:
js
GROUP BY a, CUBE (b, c), GROUPING SETS ((d), (e)) 等效于:
GROUP BY GROUPING SETS (
(a, b, c, d), (a, b, c, e),
(a, b, d), (a, b, e),
(a, c, d), (a, c, e),
(a, d), (a, e)
)
可以看到,其实Grouping Sets本身可以处理任意组合的状况,但结合Rollup和Cube可以简化代码和书写。当然道理上,我们应该基于业务需求,只对需要进行计算的维度进行聚合计算。如果对聚合性能不是特别在意的话,最简单的方式就是直接使用Cube(1,2,3..)。
小结
本文探讨了聚合操作Group By的一个遗留和扩展的问题,就是如何对聚合计算结果进行再次高维度聚合的问题。在PostgreSQL中,通过Grouping Set、Rollup和Cube方法,提供了对应的功能,文中举例说明和探讨了这几种方案的联系和差异,以及它们可以适应的不同业务和应用场景。