grouping sets操作符简化group by+union all操作

在一个GROUP BY 查询中,根据不同的维度组合进行聚合,等价于将不同维度的GROUP BY结果进行UNION ALL操作。GROUPING SETS就是一种将多个GROUP BY逻辑UNION写在一个HIVE SQL语句中的便利写法。GROUPING SETS会把在单个GROUP BY逻辑中没有参与GROUP BY的那一列置为NULL值,这样聚合出来的结果,未被GROUP BY的列将显示为NULL。

如果说聚合函数(Simple UDAF / Generic UDAF)是HQL聚合数据查询或分析的中枢处理器,那GROUP BY可以说是聚合函数的神经了,GROUP BY收集和传递材料,然后交给聚合函数们去处理。这些材料的组织形式显得尤为重要,它们表达着分析者想要的观察维度或视角,管理着聚合函数们的操作对象。

而分析者经常想要在一次分析中从多个维度去获得分析数据,对包含多个维度或多级层次的分析,上卷(roll up)或下钻(drill down)一类就很有分析价值。

我们有时候可以从最细、最多的粒度去做一个查询,然后把结果集导入Excel这个数据分析利器,用数据透视图标进行“上卷”分析;但有时候也行不通,比如说UV这种需要去重的数据,在Excel里用汇总方式进行上卷,就不是纯粹的UV概念了。

所以,对这种情形,在查询过程中,我们就需要获得已经下钻和上卷的数据;如果只有GROUP BY子句,那我们可以写出按各个维度或层次进行GROUP BY的查询语句,然后再通过UNION子句把结果集拼凑起来,但是这样的查询语句显得冗长、笨拙。

为此,HQL像其它很多SQL实现一样,为我们提供了GROUPINGSETS子句来简化查询语句的编写,以下官方CWiki文档很清晰地表达了GROUPING SETS的功能:

使用方法:

假如现在又如下场景,a,b,num三个字段,现在要求对a,b字段分别进行统计,有三种情况:(a,b)、(a)、(b)。常规写法我们可能会写成:

SELECT a,b,sum(num) AS total_num
FROM DW_AAA.BBB
GROUP BY a,b
UNION ALL
SELECT a,sum(num) AS total_num
FROM DW_AAA.BBB
GROUP BY a
UNION ALL
SELECT b,sum(num) AS total_num
FROM DW_AAA.BBB
GROUP BY b

现在用GROUPING SETS来进行改写:

SELECT a
,b
,sum(num) AS total_num
FROM DW_AAA.BBB
GROUP BY a,b
GROUPING SETS (a,b),(a),(b)
可见代码简洁了很多,并且生成的job数也变少且计算的效率提高了(UNION ALL是多次扫描表)。

下面看一个案例:

t1表中的数据

emi     201801  10000
emi     201802  11000
emi     201803  9000
emi     201901  10000
tommy   201801  12500
tommy   201802  10500
tommy   201803  8900
tommy   201901  9000

字段

t_name                  string                                      
t_month                 string                                      
t_sale                  int  

聚合操作

SELECT t_name,t_month,sum(t_sale) AS total_num
  FROM t1
 GROUP BY t_name,t_month
 UNION ALL
SELECT t_name,null,sum(t_sale) AS total_num
  FROM t1
 GROUP BY t_name
 UNION ALL 
SELECT null,t_month,sum(t_sale) AS total_num
  FROM t1
 GROUP BY t_month;

查询出来的结果

u1.t_name       u1.t_month      u1.total_num
emi     		201801  		10000
emi     		201802  		11000
emi     		201803  		9000
emi     		201901  		10000
tommy   		201801  		12500
tommy   		201802  		10500
tommy   		201803  		8900
tommy   		201901  		9000
NULL    		201801  		22500
NULL    		201802  		21500
NULL    		201803  		17900
NULL    		201901  		19000
emi     		NULL    		40000
tommy   		NULL    		40900

使用grouping sets来改写

SELECT t_name,t_month,sum(t_sale) AS total_num
  FROM t1
 GROUP BY t_name,t_month
 grouping sets ((t_name),(t_month),(t_name,t_month));

结果是

NULL    201801  22500
NULL    201802  21500
NULL    201803  17900
NULL    201901  19000
emi     NULL    40000
emi     201801  10000
emi     201802  11000
emi     201803  9000
emi     201901  10000
tommy   NULL    40900
tommy   201801  12500
tommy   201802  10500
tommy   201803  8900
tommy   201901  9000

对括号去掉

SELECT t_name,t_month,sum(t_sale) AS total_num
  FROM t1
 grouping sets ((t_name),(t_month),(t_name,t_month));

报错,还是要先写group by

SELECT t_name,t_month,sum(t_sale) AS total_num
  FROM t1
 GROUP BY t_name,t_month
 grouping sets (t_name,t_month,(t_name,t_month));

对空值可以处理,来区分本身是null值和还是grouping sets 之后为空的。

结果和没有去括号是一样的。对多个结果进行了

SELECT t_name,t_month,sum(t_sale) AS total_num
  FROM t1
 GROUP BY t_name,t_month
 with rollup;
t_name  t_month total_num
NULL    NULL    80900
emi     NULL    40000
emi     201801  10000
emi     201802  11000
emi     201803  9000
emi     201901  10000
tommy   NULL    40900
tommy   201801  12500
tommy   201802  10500
tommy   201803  8900
tommy   201901  9000

试一下 with cube

SELECT t_name,t_month,sum(t_sale) AS total_num
  FROM t1
 GROUP BY t_name,t_month
 with cube;
t_name  t_month total_num
NULL    NULL    80900
NULL    201801  22500
NULL    201802  21500
NULL    201803  17900
NULL    201901  19000
emi     NULL    40000
emi     201801  10000
emi     201802  11000
emi     201803  9000
emi     201901  10000
tommy   NULL    40900
tommy   201801  12500
tommy   201802  10500
tommy   201803  8900
tommy   201901  9000

采用grouping sets可以很明显的减少job的数量,是一个很棒的方法!用union是4个job,用grouping sets就只有1个了。

Aggregate Query with GROUPING SETS

Equivalent Aggregate Query with GROUP BY

SELECT a, b, SUM© FROM tab1 GROUP BY a, b GROUPING SETS ( (a,b) )

SELECT a, b, SUM© FROM tab1 GROUP BY a, b

SELECT a, b, SUM( c ) FROM tab1 GROUP BY a, b GROUPING SETS ( (a,b), a)

SELECT a, b, SUM( c ) FROM tab1 GROUP BY a, b
UNION
SELECT a, null, SUM( c ) FROM tab1 GROUP BY a

SELECT a,b, SUM( c ) FROM tab1 GROUP BY a, b GROUPING SETS (a,b)

SELECT a, null, SUM( c ) FROM tab1 GROUP BY a
UNION
SELECT null, b, SUM( c ) FROM tab1 GROUP BY b

SELECT a, b, SUM( c ) FROM tab1 GROUP BY a, b GROUPING SETS ( (a, b), a, b, ( ) )

SELECT a, b, SUM( c ) FROM tab1 GROUP BY a, b
UNION
SELECT a, null, SUM( c ) FROM tab1 GROUP BY a, null
UNION
SELECT null, b, SUM( c ) FROM tab1 GROUP BY null, b
UNION
SELECT null, null, SUM( c ) FROM tab1

因为涉及UNION操作,所以为了遵循UNION对参与合并的数据集合的要求,GROUPING SETS会把在单个GROUP BY逻辑中没有参与GROUP BY的那一列置为NULL值,使它成为常量占位列。这样聚合出来的结果,未被GROUP BY的列将显示为NULL。

但是这样的处理也会引起一个歧义性问题,如果我们分析的表有一些列没有NOT NULL约束,那原始数据中,未被GROUP BY的列可能原本就会出现一些NULL值,这样,GROUPING SETS出来的结果,我们没有办法去区分该列显示的NULL值是原始数据出现的NULL值聚合的结果,还是你因为这列没有参与GROUP BY而被置为NULL值的结果。

select
    A,
    B,
    C,
    group_id, 
    count(A)
from
    tableName
group by  --declare columns
    A,
    B,
    C
grouping sets
(
   (A,C),
   (A,B),
   (B,C),
   (C)
)

group_id是为了区分每条输出结果是属于哪一个group by的数据。它是根据group by后面声明的顺序字段是否存在于当前group by中的一个二进制位组合数据。 比如(A,C)的group_id: group_id(A,C) = grouping(A)+grouping(B)+grouping © 的结果就是:二进制:101 也就是5.

select中的字段是完整的A,B,C,但是我们知道由于group by的存在,select 字段本不应该出现非group by字段的,所以这里我们要特别说明,如果解释器发现group by A,C 但是select A,B,C 那么运行时会将所有from 表取出的结果复制一份,B都置为null,也就是在结果中,B都为null。

为了解决这个歧义问题,HQL又为我们提供了一个Grouping__ID函数(请注意函数名中的下划线是两个!);这个函数没有参数,在有GROUPING SETS子句的情况下,把它直接放在SELECT子句中,像其它列一样,独占一列。它返回的结果是一个看起来像整形数值类型,其实是字符串的值,这个值使用了位图策略(bitvector,位向量),即它的二进制形式中的每1位标示着对应列是否参与GROUP BY,如果某一列参与了GROUP BY,对应位就被置为1,否则为0,根据这个位向量值和对应列是否显示为NULL,我们就可以解决上面提到的歧义问题了。

这样一来,Grouping__ID函数返回值的范围由查询的字段数(除去聚合函数产生的列)决定,如果比如有3列,那位向量为3位,最大值为7。CWiki文档提供了下面的示例:

有下面一个表数据:

Column1 (key) Column2 (value)
1 NULL
1 1
2 2
3 3
3 NULL
4 5

我们用这样的查询语句去执行查询:

SELECT key, value, GROUPING__ID, count(*) from T1 GROUP BY key,value WITH ROLLUP

rollup是 cube 的子集,以最左侧的维度为主,从该维度进行层级聚合

将得到如下查询结果:

NULL NULL 0 6
1 NULL 1 2
1 NULL 3 1
1 1 3 1
2 NULL 1 1
3 2 3 1
3 NULL 1 2
3 NULL 3 1
3 3 3 1
4 NULL 1 1
4 5 3 1

官方文档没有明确说明这个位向量和各列的高低位对应关系,但是从示例我们可以看到,这个位向量的低位对应SELECT子句中的第1列(非聚合列),高位对应最后1列(非聚合列)。

上面的查询用到了WITH ROLLUP子句,它对应SQL中的上卷操作,其实它就是GROUPINGSETS的特例,对应上面第一个表格中的第4种情形;根据官方的CWiki文档解释,GROUP BY 子句加上ROLLUP 子句可用于计算从一个维度进行层级聚合的操作:

GROUP BY a, b, c with ROLLUP assumes that the hierarchy is"a" drilling down to “b” drilling down to “c”.

类似地还有WITH CUBE子句,对应SQL中的CUBE操作,它完成对字段列中的所有可能组合(全序集?)进行GROUP BY的功能,正如官方CWiki文档的解释:

GROUP BY a, b, c WITH CUBE 等同于
GROUP BY a, b, c GROUPING SETS ( (a, b, c), (a, b), (b, c), (a, c),(a), (b), ©, ( ))

GROUPING SETS增强了GROUP BY的查询表达能力,ROLLUP和CUBE又增强了GROUPING SETS的查询表达能力,这样一来,GROUP BY的形态也变得多样化了,让我们能够在查询阶段就实现更多的分析角度。

还需留意的是:Hive从0.10.0版本才开始有GROUPING SETS的。

Group By后面可以添加Cube关键字,对group by中指定的多个字段进行任意组合进行group by。

比如:Group By a,b,c with cube 就相当于 group by a,b,c grouping sets ((a,b,c),(a,b),(b,c),(a,c),©,())

Group By后面添加Rollup关键字,可以实现对于group by的各个字段进行从右到左递减的统计。

比如 Group By a,b,c with rollup 就相当于group by a,b,c grouping sets((a,b,c),(a,b),(a),())

有如下店铺销售数据:

现有如下需求:按照店铺id和日期维度汇总订单量

代码如下:

SELECT businessid
,date
,count(DISTINCT orderid) AS ord_num
FROM basic_info_detail a
GROUP BY date,businessid
grouping sets((date,businessid),(businessid))
得到结果如下:

从结果中可以看出,businessid为344981的店铺,其订单量为1174,并且在二月份产单1096单,在3月份为78单。

注:

hive中grouping sets 数量较多时如何处理?

可以使用如下设置来

set hive.new.job.grouping.set.cardinality = 30;

这条设置的意义在于告知解释器,group by之前,每条数据复制量在30份以内。

-- grouping sets 
select 
       order_id,
       departure_date,
       count(*) as cnt
  from ord_test
 where order_id=410341346
 group by order_id,
       departure_date
 grouping sets (order_id,(order_id,departure_date))
;

---- 等价于以下
group by order_id
union all
group by order_id,departure_date

-- cube
select 
       order_id,
       departure_date,
       count(*) as cnt
  from ord_test
 where order_id=410341346
 group by order_id,
       departure_date
 with cube
 ;

---- 等价于以下
select count(*) as cnt from ord_test where order_id=410341346
union all
group by order_id
union all
group by departure_date
union all
group by order_id,departure_date

-- rollup
select 
       order_id,
       departure_date,
       count(*) as cnt
  from ord_test
 where order_id=410341346
 group by order_id,
       departure_date
 with rollup
 ;

---- 等价于以下
select count(*) as cnt from ord_test where order_id=410341346
union all
group by order_id
union all
group by order_id,departure_date

猜你喜欢

转载自blog.csdn.net/u012955829/article/details/106008414