在一个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