上一篇我们讲解了基本UDF的编写,这一节我们来看下UDAF[User Defined Aggregation Functions],自定义聚合函数,用来处理输入多行,输出一行的操作,类似MapReduce中Reduce操作。UDAF是需要在hive的sql语句和group by联合使用,hive的group by对于每个分组,只能返回一条记录。
概述
Hive是构建在Hadoop上的数据仓库,我们的sql语句最终都是要变成Mapreduce
函数,只不过hive已经帮助我们写好并隐藏mapreduce
,给我们提供简洁的sql函数,所以我们要结合Mapper
、Combiner
与Reducer
来帮助我们理解UDAF函数。在hadoop集群中有若干台机器,在不同的机器上Mapper
与Reducer
任务独立运行。所以大体上来说,这个UDAF函数读取数据[mapper],聚集一堆mapper输出到部分聚集结果[combiner],并且最终创建一个最终的聚集结果[reducer]。因为我们跨域多个combiner进行聚集,所以我们需要保存部分聚集结果。在实现UDAF时候,Hive提供了两个重要的类org.apache.hadoop.hive.ql.udf.generic.AbstractGenericUDAFResolver
和org.apache.hadoop.hive.ql.udf.generic.GenericUDAFEvaluator
,我们先分析这两个类都做了什么工作,然后通过一个简单的例子来了解如何自定义UDAF。
UDAF重要的类及原理分析
AbstractGenericUDAFResolver
AbstractGenericUDAFResolver
很简单,要覆盖实现getEvaluator
方法,该方法会根据sql传递的参数数据格式指定调用哪个Evaluator进行处理。
public GenericUDAFEvaluator getEvaluator(TypeInfo[] info)
throws SemanticException {
throw new SemanticException(
"This UDAF does not support the deprecated getEvaluator() method.");
}
GenericUDAFEvaluator
所有Evaluator
必须继承抽象类org.apache.hadoop.hive.ql.udf.generic.GenericUDAFEvaluator
。子类必须实现它的一些抽象方法,实现UDAF的逻辑。
ObjectInspector
ObjectInspector
作用主要是解耦数据使用与数据格式,使得数据流在输入输出端切换不同的输入输出格式,不同的Operator上使用不同的格式,可以通过这个抽象类知道上游传递过来的参数类型,从而解耦。一个ObjectInspector
对象本身并不包含任何数据,它只是提供***对数据的存储类型说明***和***对数据对象操作***的统一管理或者是代理。
public interface ObjectInspector extends Cloneable {
String getTypeName();
ObjectInspector.Category getCategory();
public static enum Category {
PRIMITIVE,
LIST,
MAP,
STRUCT,
UNION;
private Category() {
}
}
}
Model
Model
代表了UDAF在Mapreduce的各个阶段,定义如下:
public static enum Mode {
// PARTIAL1: from original data to partial aggregation data: iterate() and t
// erminatePartial() will be called.
PARTIAL1,
// PARTIAL2: from partial aggregation data to partial aggregation data:
// merge() and terminatePartial() will be called.
PARTIAL2,
// FINAL: from partial aggregation to full aggregation: merge() and
// terminate() will be called.
FINAL,
// COMPLETE: from original data directly to full aggregation: iterate() and
// terminate() will be called.
COMPLETE
};
我们来详细分析下各个阶段:
-
PARTIAL1
是map阶段,从原始数据到数据聚合,将会调用iterate
和terminatePartial
方法; -
PARTIAL2
相当于map阶段的Combine
,负责map端合并map的数据,将调用merge
和terminatePartial
方法; -
FINAL
相当于reduce阶段,从部分聚合到完全聚合,将会调用merge
和terminate
方法; -
COMPLETE
这个表示mapreduce中只有map没有reduce,直接在map端就得到结果了,从原始数据直接到完全聚合结果,将会调用iterate
和terminate
方法。
所以,完整的UDAF逻辑是一个mapreduce过程,如果有mapper和reducer,就会经历PARTIAL1(mapper),FINAL(reducer),如果还有combiner,那就会经历PARTIAL1(mapper),PARTIAL2(combiner),FINAL(reducer)。而有一些情况下只有mapper,而没有reducer,所以就会只有COMPLETE阶段,这个阶段直接输入原始数据,出结果。
相关方法
实现UDAF,需要实现以下几个方法:
// 确定各个阶段输入输出参数的数据格式ObjectInspectors
public ObjectInspector init(Mode m, ObjectInspector[] parameters) throws HiveException;
// 保存数据聚集结果的类
abstract AggregationBuffer getNewAggregationBuffer() throws HiveException;
// 重置聚集结果
public void reset(AggregationBuffer agg) throws HiveException;
// map阶段,迭代处理输入sql传过来的列数据
public void iterate(AggregationBuffer agg, Object[] parameters) throws HiveException;
// map与combiner结束返回结果,得到部分数据聚集结果
public Object terminatePartial(AggregationBuffer agg) throws HiveException;
// combiner合并map返回的结果,还有reducer合并mapper或combiner返回的结果。
public void merge(AggregationBuffer agg, Object partial) throws HiveException;
// reducer阶段,输出最终结果
public Object terminate(AggregationBuffer agg) throws HiveException;
UDAF开发步骤
Hive开发UDAF有两个步骤:
- 编写
resolver
类,resolver负责类型检查,操作符重载。类继承org.apache.hadoop.hive.ql.udf.generic.AbstractGenericUDAFResolver
,AbstractGenericUDAFResolver
实现了org.apache.hadoop.hive.ql.udf.generic.GenericUDAFResolver2
,方便统一接口。 - 第二个是编写
evaluator
类。evaluator真正实现UDAF的逻辑。通常来说,实现org.apache.hadoop.hive.ql.udf.generic.GenericUDAFEvaluator
,包括几个必须实现的抽象方法,这几个方法负责完成UDAF所需要处理的逻辑。
Sum实现分析
我们来看下Hive内置的Sum函数是如何实现的。
GenericUDAFSum
根据上面分析的我们首先要实现AbstractGenericUDAFResolver
的getEvaluator
方法,根据数据类型选择不同的Evaluator
:
- 如果是BYTE,SHORT,INT,LONG类型,就是用
GenericUDAFSumLong
; - 如果是TIMESTAMP,FLOAT,DOUBLE,STRING,VARCHAR,CHAR类型,就是用
GenericUDAFSumDouble
; - 如果是DECIMAL类型,就是用
GenericUDAFSumHiveDecimal
; - 如果是其他类型则抛出异常。
public class GenericUDAFSum extends AbstractGenericUDAFResolver {
@Override
public GenericUDAFEvaluator getEvaluator(TypeInfo[] parameters)
throws SemanticException {
if (parameters.length != 1) {
throw new UDFArgumentTypeException(parameters.length - 1, "Exactly one argument is expected.");
}
if (parameters[0].getCategory() != ObjectInspector.Category.PRIMITIVE) {
throw new UDFArgumentTypeException(
0, "Only primitive type arguments are accepted but "
+ parameters[0].getTypeName() + " is passed.");
}
switch (((PrimitiveTypeInfo) parameters[0]).getPrimitiveCategory()) {
case BYTE:
case SHORT:
case INT:
case LONG:
return new GenericUDAFSumLong();
case TIMESTAMP:
case FLOAT:
case DOUBLE:
case STRING:
case VARCHAR:
case CHAR:
return new GenericUDAFSumDouble();
case DECIMAL:
return new GenericUDAFSumHiveDecimal();
case BOOLEAN:
case DATE:
default:
throw new UDFArgumentTypeException(
0, "Only numeric or string type arguments are accepted but "
+ parameters[0].getTypeName() + " is passed.");
}
}
}
可以看出来根据不同的类型选择了不同处理类,我们来分析下GenericUDAFSumLong
的实现
GenericUDAFSumLong
GenericUDAFSumLong
是用来对BYTE,SHORT,INT,LONG类型求和的UDAF,有两个变量,inputOI
是输入的数据,result
是结果数据,初始化是对这两个参数的初始化,另外定义了SumLongAgg
来存储中间结果,里面包含了sum值和是否为空,我们自行实现时候可以自定义自己的AggBuffer。
public static class GenericUDAFSumLong extends GenericUDAFEvaluator {
private PrimitiveObjectInspector inputOI;
private LongWritable result;
@Override
public ObjectInspector init(Mode m, ObjectInspector[] parameters) throws HiveException {
assert (parameters.length == 1);
super.init(m, parameters);
result = new LongWritable(0);
inputOI = (PrimitiveObjectInspector) parameters[0];
return PrimitiveObjectInspectorFactory.writableLongObjectInspector;
}
/** class for storing double sum value. */
@AggregationType(estimable = true)
static class SumLongAgg extends AbstractAggregationBuffer {
boolean empty;
long sum;
@Override
public int estimate() { return JavaDataModel.PRIMITIVES1 + JavaDataModel.PRIMITIVES2; }
}
@Override
public AggregationBuffer getNewAggregationBuffer() throws HiveException {
SumLongAgg result = new SumLongAgg();
reset(result);
return result;
}
@Override
public void reset(AggregationBuffer agg) throws HiveException {
SumLongAgg myagg = (SumLongAgg) agg;
myagg.empty = true;
myagg.sum = 0;
}
}
另外它实现了Mode阶段的不同函数,我们来一一分析。
iter
iterate()
函数读取到每行中列的数值,然后合并到缓存数据结构中。
@Override
public void iterate(AggregationBuffer agg, Object[] parameters) throws HiveException {
assert (parameters.length == 1);
try {
merge(agg, parameters[0]);
} catch (NumberFormatException e) {
if (!warned) {
warned = true;
LOG.warn(getClass().getSimpleName() + " "
+ StringUtils.stringifyException(e));
}
}
}
merge
merge
函数增加部分聚集总数到AggregationBuffer
中。
@Override
public void merge(AggregationBuffer agg, Object partial) throws HiveException {
if (partial != null) {
SumLongAgg myagg = (SumLongAgg) agg;
myagg.sum += PrimitiveObjectInspectorUtils.getLong(partial, inputOI);
myagg.empty = false;
}
}
terminatePartial
terminatePartial()
函数直接调用terminate
,返回AggregationBuffer
中的内容,这里产生了最终结果。
@Override
public Object terminatePartial(AggregationBuffer agg) throws HiveException {
return terminate(agg);
}
terminate
terminate()
函数返回AggregationBuffer
中的内容,这里产生了最终结果。
@Override
public Object terminate(AggregationBuffer agg) throws HiveException {
SumLongAgg myagg = (SumLongAgg) agg;
if (myagg.empty) {
return null;
}
result.set(myagg.sum);
return result;
}
总结
本文讲解了UDAF,介绍了重要的类以及UDAF的运行过程,最后通过分析sum的UDAF实现,来讲解了一个UDAF实现的具体步骤,但是如果想要更好理解UDAF的运行过程,还是需要看一看avg的实现,avg UDAF会对hive的运行流程要控制的更加精细,并判断当前运行的Mode做一定的逻辑处理,我们后面在分析。