PostgreSQL技术问答-17 复杂聚合

本文是《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方法,提供了对应的功能,文中举例说明和探讨了这几种方案的联系和差异,以及它们可以适应的不同业务和应用场景。

相关推荐
小小小小关同学29 分钟前
Spring Cloud LoadBalancer
后端·spring·spring cloud
鹿子铭33 分钟前
单线程Redis:Redis为什么这么快
数据库·redis
JSON_L1 小时前
MySQL 事务处理
数据库·mysql
Pandaconda2 小时前
【C++ 面试 - 新特性】每日 3 题(六)
开发语言·c++·经验分享·笔记·后端·面试·职场和发展
chanTwo_003 小时前
go--知识点
开发语言·后端·golang
悟空丶1233 小时前
go基础知识归纳总结
开发语言·后端·golang
爱打lan球的程序员3 小时前
redis分布式锁和lua脚本
数据库·redis·分布式
说书客啊3 小时前
计算机毕业设计 | springboot旅行旅游网站管理系统(附源码)
java·数据库·spring boot·后端·毕业设计·课程设计·旅游
hummhumm3 小时前
数据库系统 第46节 数据库版本控制
java·javascript·数据库·python·sql·json·database
ac-er88883 小时前
Flask如何创建并运行数据库迁移
数据库·python·flask