怎么让sql查询的字段可以不出现在group分组里_Presto中的分组聚合查询流程

669cfab093ed9514d6bac5cdcc2042be.png

1. 综述

Sql中经常会用到group by来进行分组聚合分析,这里的分组指的就是group by语义,而聚合则指的是对每个组内的数据进行怎么的处理,例如sum、count、avg、max/min等,把一个group内的多条数据reduce成一条,本文对Presto中的groupby进行执行计划、执行过程以及源码层面的分析,并简要介绍Presto中aggregation function的实现方法(即常说的UDAF)

2. 最简单的groupby查询

以TPCH中的一个表的group by查询为例来说明:

SELECT 

结果如下:

 returnflag | _col1 |        _col2         |        _col3        
------------+-------+----------------------+---------------------
 A          |   260 |    9938125.219999999 | 0.04019230769230766 
 N          |   501 | 1.8910475029999994E7 |  0.0406187624750499 
 R          |   243 |    8932509.190000005 | 0.04032921810699583

上边这条sql语句涉及到一个比较简单的group by操作,首先对数据进行过滤,过滤条件是“orderkey <1000”,然后对筛选出的数据,根据returnflag进行分组,对每一个分组内的数据计算count、累加total_price、对tax求平均。

3. Presto中group by的执行计划

下图呈现的这个计划图,就是一个presto的逻辑执行计划,数据按照箭头的方向从下往上进行读取、计算和传输,图中小的绿色矩形为一个逻辑执行计划的Node(并非物理执行计划中的算子),比如对于Stage2,在逻辑上需要完成tablescan、filter、project、aggregation等操作,但是在实际的物理执行过程中,出于执行性能方面的考虑,会把tablescan、filter以及project合并在一个物理算子中,减少内存的拷贝,但这并非本文的重点,这里不展开描述。

4e309c1d2c99d239883a5c73a2509fd9.png

从上图可以看出,Presto把这个典型的单表group by操作切分成了3个stage,大的绿色矩形就表示一个stage。Presto中切分stage的原则是有数据需要进行节点间传输则进行一次stage切分,从上图可以看出,在整个执行的过程中对中间数据进行了两次网络传输。下面就来看看每次传输前都做了些什么。

3.1 Stage2

8ec3aa7f3b1f9608d752828dccfceb13.png

Stage2中和groupby相关的算子是ScanFilterAndProjectOperator,从这个算子的名字可以看出这个算子完成了逻辑执行计划(上图中的Stage2表示的矩形框)中的tablescan(数据源读取)、filter(过滤)以及project(投影)三个工作,其中和groupby有直接关系的是project操作,这个操作主要完成对待分组字段的hash值的计算,处理逻辑通过code generation封装,通常我们会看到类似:com_facebook_presto_$gen_PageProcessor_120.class这样的类,这个类并非presto的源码编译出的类,而是通过asm这样的工具在运行时实时生成的。把这个类dump成class文件之后,使用反编译工具反编译后,我们可以看到类似如下的process方法:

public 

因为presot处理数据的单位是page,一个page中每个字段对应一个block,如下图所示:

ee7ff48bb697725d52486dee6b3afe65.png

上述代码片段中的process方法,会遍历page中的所有行,注意这里没有orderkey字段对应的block,因为orderkey字段的作用只是用来过滤的,在ScanFilterAndProjectOpertor处理结束后就直接被丢弃了。其中returnflag字段对应的block2被处理了两次,一次是因为在调用聚合算子进行分组时,需要对改字段进行hash冲突判断,另外一次则是要对returnflag进行hash值计算,用途同样是进行分组。经过project处理的page被传递到了下游处理逻辑aggregation,对应的物理算子是HashAggreratitionOperator。该算子完成主要的groupby逻辑。HashAggreratitionOperator的addInput方法,该方法主要完成对一个Page数据的分组和partial聚合:

  • 对数据分组

由GroupByHash的getGroupIds方法完成,该方法完成的数据分组是指给每行数据标记出其所在的group id,例如下图中的GroupId Block,就是由该方法产生的:

1e02cf46475a546983ae4a6e04d6d98f.png
  • Partial聚合

之所以说是Partial聚合,是因为在分布式计算系统上,数据在导入系统后,相同的字段值被分散在了不同的服务器上,即使在相同的服务器上,由于有数据分区的存在,相同的字段由会分布在不同的分区里。每个计算单元在进行局部数据处理时,就需要先完成同分组字段在局部数据单元内的聚合,这就是partial聚合,减少数据占用的内存以及网络传输的开销。在presto的partial聚合时,可以看到完成该工作的类是类似com_facebook_presto_$gen_DoubleDoubleSumGroupedAccumulator_101.class这样的,这同样也是由asm工具code generation产生的,从类名可以看出,其主要完成输入为Double类型、输出为Double类型的分组累加工具,我们看在partial阶段涉及的主要方法:

public 

方法中的state成员变量是聚合的中间状态,循环体会遍历该Page中的所有行,首先会给state设置当前要聚合的GroupId,然后动态调用state的input方法(Aggregation Function),完成当前groupid的state和当前行的聚合字段值的聚合,该state将持有一个最新的聚合状态。

  • Paitial分组聚合状态(State)输出

在Partial聚合阶段的分组State输出,并不会产生一个真正的Paitial聚合结果,而只是对这个中间聚合状态进行序列化,code generation中对应的代码如下:

public 

其目的主要是为了把这个中间状态通过网络进行传输。这个序列化后的Block组成page后,会被发送到stage2的下一个物理算子PartitionedOutputOperator(live plan中没有显示),该算子根据分组字段的hash值对一个page进行切分,切分后的page序列化之后,放入outputbuffer(类似spark中的shuffle write),等待下游stage的算子通过网络读取。至此,stage2完成。

3.2 Stage1

Stage1是Stage2的下游Stage,该Stage中和groupby相关的物理算子还是HashAggregationOperator,这个Stage中的HashAggregationOperator不同于Stage2中的HashAggregationOperator的地方在于该算子之前有一个ExchangeOperator,该算子通过网络读取上一个Stage写入到outputbuffer中的数据(类似spark中的shuffe read),所以,相同的group key已经被聚集在一起,该stage中的HashAggregationOperator完成的是最终(Final)聚合。

3968a972de1a0c1bc2553ebe2dab0dcb.png
  • 对数据分组

对数据分组的流程类似Partial聚合中的数据分组

  • Final聚合

这个阶段的聚合操作其实不需要再对每行原始数据进行遍历和累加,如上所述,这个Page中的数据,都是Partial聚合阶段的中间聚合状态,所以这一步的主要逻辑就是把这些中间聚合状态进行合并(combine),code generation中的代码片段如下:

public 

从反编译出的代码可以看出,首先对block进行了反序列化,然后调用了Combine(Aggregation Function)方法,对两个State进行合并。

  • Final聚合结果计算

对于上一步合并的state,根据Aggregation Function不同,可能并不是最终的聚合结果,例如求average,需要汇总某个group key的所有的sum值和count值,最后sum除以count才是最终的平均值,所以这里的Final聚合结果计算就是要完成这样的工作:

public 

注意这里动态调用的是output方法,也是Aggregation Function中的一个实现,该方法会完成某个group key最终的聚合结果计算。

3.3 Stage0

由于Stage1中的Final聚合也是在多节点上分布式执行,所以每个节点的计算结果,最终会发送到一个worker节点进行汇总,汇总后的结果会发送到客户端。至此,一个完整的group by查询就结束,整个物理执行过程可以用下图表示:

a65c9076139ec4cd28bef86a0b877fcd.png

这里需要注意的是,在Partial阶段和Final阶段,都可以运行在同一个worker上,这说明Partial和Final的执行,在逻辑上虽然属于不同的stage,但是物理上并非是隔离开的。

4. Presto中的UDAF实现

Presto中的UD*F并非真正意义上的User Defined,Presto目前还没有提供合适的接口让最终用户自己去定义和加载一个Aggregation Function。这里简单讲解一下要在Presto中实现一个UDAF需要做哪些工作。

其实我们在上边也看到了在整个GroupBy执行的过程中,出现了3处Aggregation Function,调用了3个不同的方法,所以我们如果要实现Presto中的UDAF,也就需要实现这三个方法即可:

  • InputFunction 完成原始数据的按组别聚合
  • CombineFunction 对中间聚合状态进行合并
  • OutputFunction 使用最终聚合状态进行

这些方法都会在CodeGeneration的代码中被调用,以Double类型数据求和为例:

  • DoubleSum的InputFunction,完成对原始值的聚合(累加)
@InputFunction
    
  • DoubleSum的CombineFunction,完成对两个不同中间状态的合并(相加)
@CombineFunction
    
  • DoubleSum的OutputFunction,完成对最终状态的计算
@OutputFunction

可以看出,对于sum这种特殊的聚合操作,中间状态的合并结果其实就是最终状态的计算结果了,所以这里只是把这个最终状态写入输出的BlockBuilder中即可,如果是求平均或者是求中值、求均方差等,那么在这里会有额外的计算步骤。

5.总结

本文对Presto中的GroupBy查询流程和UDAF实现进行了简要讲述,都是非常典型的在分布式Sql系统实现流程,但是还没有涉及很多细节的地方,例如GroupBy中的内存管理、并发控制、code gen代码产生、聚合下推优化、多字段分组处理以及Hash分组等,会在后续的文章中详细介绍。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值