Spark SQL系列------2. Spark SQL Aggregate操作的实现

 在Spark 1.6上,TungstenAggregateIterator实现了一个分区的Iterator。在实际执行的时候分2中情况:

1.要Aggregate的分区数据并不是特别大,在内存中就可以实现Aggregate了

2.要Aggregate的分区数据比较大,在内存中已经不能实现Aggregate,则需要将内存的数据Partial累加之后,spill到磁盘。


Aggregate操作的原理是对table中的某个field,根据某种累加规则进行累加。并且将累加结果存放到内存或者磁盘,如下图所示:


原始数据先根据Aggregate方法和属性生成Aggregate中间结果,Aggregate中间结果再根据结果表达式,计算出Aggregate输出结果。

Spark在执行Aggregate操作的时候,如果分区数据并不是特别大,数据在内存中的存储可以用如下图表示:



BytesToBytesMap.longArray这个大数组存储了Aggregate的中间结果存放的位置。

keyAddress是TaskMemoryManager.encodePageNumberAndOffset方法根据Aggregate中间结果所在的MemoryPage的index和在MemoryPage的偏移量计算出来的。BytesToBytesMap.longArray中的hashcode是根据Aggregate的Key值计算出来的。hashcode主要用于快速查找某个Key是不是已经有Aggregate中间结果了。

在MemoryPage是一块连续的内存,里面存储了Aggregate的中间结果,MemoryPage的recordsInPage(前4个字节)记录这一页存储了多少个中间结果。

1个Aggregate中间结果内容分4个部分:

keylen+valuelen+4  表示中间结果中Aggregate key的长度加上Aggregate后value的长度

keylen   表示key的长度

key的值

value的值

Spark在进行Aggregate的时候,会根据这种存储形式进行Aggregate中间数据的读取和存储。

如果内存足够大,不需要外部排序,这个时候可以将MemoryPage中的数据转化成Iterator,就可以产生结果数据了。Iterator和Aggregate内存存储数据之间的对应关系为:


可见Iterator的数据顺序和在MemoryPage中数据的存储数据是一致的。

由于需要Aggregate的数据太大,在内存中已经无法实现Aggregate,则需要进行外部排序Aggregate。流程如下图所示:



在这个时候,内存中只能进行Partial Aggregate,并且需要将内存的数据排序之后再spill到磁盘,然后跟之前已经spill到磁盘的Aggregate中间结果进行merge。这步merge操作所作的工作是将两个Iterator进行排序合并。这样相同Key的数据在最终的Iterator中就连续再一起了。在产生输出结果的时候,可以调用TungstenAggregator.processCurrentSortedGroup方法,对已经进行Partial Aggregate的中间结果进行ParitialMerge处理了。

数据spill到磁盘之后,存储Aggregate中间结果的的内存会全部释放。

从以上的分析流程可以知道,当内存不够需要将数据排序之后再spill到磁盘,但是在进行排序的时候并没有额外分配内存,而是使用BytesToBytesMap.longArray。排序之后BytesToBytesMap.longArray的数据是有序的,存储了中间结果的顺序,然后按照这个顺序,将Aggregate中间结果spill到磁盘。spill之前BytesToBytesMap.longArray的数据和MemoryPage中中间结果之间的对应关系可以用如下图表示:



从以上分析可知道,Aggregate的时候进行外部排序对性能的影响是很大的,数据需要落地到磁盘,并且还需要从磁盘中加载数据进行Merge,输出结果。


以上是对Spark1.6 TungstenAggregateIterator的分析,可能存在问题,希望其它同学指正。




  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: Spark SQL可以通过自定义聚合函数来实现更加灵活的数据处理。自定义聚合函数可以根据具体的业务需求,对数据进行自定义的聚合操作,例如计算平均值、求和、最大值、最小值等。 要实现自定义聚合函数,需要继承Aggregator类,并实现其抽象方法。Aggregator类包含三个泛型参数,分别为输入数据类型、缓冲区数据类型和输出数据类型。在实现Aggregator类时,需要重写其三个方法:zero、reduce和merge。 其中,zero方法用于初始化缓冲区,reduce方法用于对输入数据进行聚合操作,merge方法用于合并不同分区的缓冲区数据。最后,还需要实现finish方法,用于将缓冲区中的数据转换为输出数据。 完成自定义聚合函数的实现后,可以通过Spark SQL的API将其注册为UDAF(User-Defined Aggregate Function),并在SQL语句中使用。 例如,假设需要计算某个表中某个字段的平均值,可以先定义一个自定义聚合函数: ``` import org.apache.spark.sql.expressions.Aggregator import org.apache.spark.sql.Encoder case class AvgBuffer(var sum: Double = 0.0, var count: Int = 0) class Avg extends Aggregator[Double, AvgBuffer, Double] { def zero: AvgBuffer = AvgBuffer() def reduce(buffer: AvgBuffer, data: Double): AvgBuffer = { buffer.sum += data buffer.count += 1 buffer } def merge(buffer1: AvgBuffer, buffer2: AvgBuffer): AvgBuffer = { buffer1.sum += buffer2.sum buffer1.count += buffer2.count buffer1 } def finish(buffer: AvgBuffer): Double = buffer.sum.toDouble / buffer.count def bufferEncoder: Encoder[AvgBuffer] = Encoders.product def outputEncoder: Encoder[Double] = Encoders.scalaDouble } ``` 然后,将其注册为UDAF: ``` val avg = new Avg spark.udf.register("myAvg", avg) ``` 最后,在SQL语句中使用该自定义聚合函数: ``` SELECT myAvg(salary) FROM employee ``` ### 回答2: Spark SQL是一款开源的分布式计算框架,它支持使用SQL语言进行数据查询和分析,同时可以与Hadoop、Hive等大数据技术进行无缝集成。Spark SQL中的自定义聚合函数,是指用户自己定义一些聚合函数,然后将它们应用到Spark SQL的查询中,从而实现更加灵活和高效的数据分析功能。 在Spark SQL实现自定义聚合函数,需要遵循以下几个步骤: 1.创建自定义聚合函数类 首先需要创建一个类,该类继承自Aggregator,并实现其中定义的抽象方法。这些方法包括两个泛型:输入类型和累加器类型。输入类型为需要进行聚合的数据类型,累加器类型为处理一个分区的聚合结果类型。 例如,如果我们需要自定义一个计算平均值的聚合函数,那么可以创建一个类如下: class Average extends Aggregator[Double, (Double, Int), Double] { //初始化累加器方法 def zero: (Double, Int) = (0.0, 0) //聚合方法,输入数据类型为Double def reduce(acc: (Double, Int), x: Double): (Double, Int) = (acc._1 + x, acc._2 + 1) //合并累加器方法 def merge(acc1: (Double, Int), acc2: (Double, Int)):(Double, Int) = (acc1._1 + acc2._1, acc1._2 + acc2._2) //输出结果类型为Double类型 def finish(acc: (Double, Int)): Double = acc._1 / acc._2 } 在这个例子中,我们定义了一个计算平均值的聚合函数,其中输入数据类型为Double,累加器类型为一个元组(Double, Int),表示聚合结果的累加器分别包含总和和个数,输出结果类型为Double。 2.注册聚合函数 在创建完自定义聚合函数类后,需要使用SparkSession的udf方法来将它注册为一个UDAF(用户自定义聚合函数)。参看以下代码: val average = new Average().toColumn.name("average") spark.udf.register("average", average) 这里,我们将Average类实例化,然后使用toColumn方法将其转换为一个Column,使用name方法为该列命名为"average"。最后,使用SparkSession的udf方法将该列注册为一个UDAF,命名为"average"。 3.应用聚合函数 当聚合函数注册完毕后,就可以在查询中使用聚合函数进行数据分析了。参看以下代码: val data = Seq((1, 2.0), (1, 2.0), (2, 3.0), (2, 4.0), (2, 3.0)).toDF("group", "value") data.groupBy("group").agg(expr("average(value)") as "avg").show() //输出如下: //+-----+----+ //|group| avg| //+-----+----+ //| 1| 2.0| //| 2| 3.3| //+-----+----+ 在这个例子中,我们使用了数据帧来模拟一组数据,其中包含group和value两个字段。以下查询语句将数据按照group字段进行分组,并使用预先定义的聚合函数"average"计算每组的平均数。最后,使用show()方法展示查询结果。 总而言之,通过自定义聚合函数,可以为Spark SQL增加更多的聚合功能,从而使数据分析处理更加灵活和高效。 ### 回答3: Spark SQL是一个基于SparkSQL查询工具,可以将结构化和半结构化数据导入到数据仓库中。在Spark SQL实现自定义聚合函数非常重要,因为聚合函数是大型数据分析中最重要的部分之一。下面,我们将讨论如何在Spark SQL实现自定义聚合函数。 Spark SQL中的聚合函数 在Spark SQL中,聚合函数是SQL查询语句中用于计算一个数据集中值的函数。这些函数包括最小值,最大值,求和,平均值和计数函数等。 由于Spark SQL是用Scala编写的,因此我们可以在其上下文中定义和使用Scala函数。但是,为了使函数能够在SQL查询中使用,我们需要将它们转换为聚合函数。 定义聚合函数 要定义聚合函数,我们需要定义一个包含聚合函数的类并扩展Aggregator trait。该类必须定义三个类型:输入类型,中间类型和输出类型。 输入类型指的是需要在聚合函数中使用的数据类型。在本例中,我们将使用一个整数类型的输入数据。 中间类型指的是在计算过程中使用的数据类型。这个类型可以是任何类型,只要它们可以相加,并在最后输出结果。在本例中,我们将中间类型定义为一个二元组类型。 输出类型指最终聚合函数的结果类型。因此,我们将输出类型定义为一个double类型的数据。 现在,我们可以定义一个具有以上规则的自定义聚合函数: import org.apache.spark.sql.expressions._ import org.apache.spark.sql._ import org.apache.spark.sql.functions._ import org.apache.spark.sql.types._ object MyAggregator extends Aggregator[Int, (Int, Int), Double] { override def zero: (Int, Int) = (0, 0) override def reduce(b: (Int, Int), a: Int): (Int, Int) = (b._1 + a, b._2 + 1) override def merge(b1: (Int, Int), b2: (Int, Int)): (Int, Int) = (b1._1 + b2._1, b1._2 + b2._2) override def finish(r: (Int, Int)): Double = r._1.toDouble / r._2 override def bufferEncoder: Encoder[(Int, Int)] = Encoders.product[(Int, Int)] override def outputEncoder: Encoder[Double] = Encoders.scalaDouble } 解释: zero方法返回一个中间类型的初始值。在这个例子中,我们使用(0, 0)作为初始值。 reduce 方法使用输入类型的值和中间类型的值并返回一个新的中间类型的值。 merge方法将两个中间类型的值合并成一个中间类型的值。 finish方法将最终的中间类型的值转换为输出类型的值。 bufferEncoder和outputEncoder方法分别定义缓冲区类型和输出类型的编码器。 使用自定义函数 一旦自定义聚合函数定义完成,我们可以在SQL查询中使用它。假设我们有以下数据集: +---+ |num| +---+ | 1| | 2| | 3| | 4| | 5| +---+ 我们可以使用以下查询来使用我们的自定义聚合函数并计算平均数: val df = Seq(1, 2, 3, 4, 5).toDF("num") df.agg(MyAggregator.toColumn.name("avg")).show() 输出: +---+ |avg| +---+ |3.0| +---+ 总结 Spark SQL中自定义聚合函数的过程稍微有些困难,但是一旦我们定义了自定义聚合函数,我们就可以将其用作SQL查询中的任何其他聚合函数。而且在使用它时,我们可以拥有无限的灵活性来定义任何形式的自定义聚合函数。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值