Hive中内置了很多的函数,包含了日常工作需求的字符串处理、日期时间处理等常用函数,在Hive CLI界面中,可以使用show functions
查看全部可用函数,要查看某个函数的作用和用法,可以使用desc function <function_name>
指令:
当内置函数无法满足我们的需求时,Hive提供了可供用户自定义函数的接口,通过实现指定接口,可以创建实现自定义功能的函数。
自定义函数根据输入输出的方式,大致分为三类
函数类型 | 描述 |
---|---|
UDF | 单行进单行出,每一行执行产出一行的结果 |
UDAF | 多行进单行出,对多行数据统计结果 |
UDTF | 单行进多行出,一行数据产生多行结果 |
创建自定义函数需要导入的依赖:
<!--根据实际使用版本调整-->
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>2.6.0</version>
</dependency>
<dependency>
<groupId>org.apache.hive</groupId>
<artifactId>hive-exec</artifactId>
<version>1.1.0</version>
</dependency>
UDF
自定义的UDF类需要继承org.apache.hadoop.hive.ql.exec.UDF
抽象类,并定义一个evaluate
方法,该方法的参数列表就是输入的数据(只能有一行,但是可以有多列),返回值就是我们需要的结果
需要注意的是,该方法并不是重写或实现方法,但名称必选是evaluate
案例需求:格式化姓名,将其改为首字母大写,其余字母全部小写的格式:
import org.apache.hadoop.hive.ql.exec.UDF;
public class FormatName extends UDF {
public String evaluate(String input) {
String lower = input.toLowerCase();
char c = lower.charAt(0);
return lower.replaceFirst(String.valueOf(c), String.valueOf((char) (c - 32)));
}
}
实现代码编写完成后,使用maven工具打包,并上传
接下来,需要添加包含UDF函数的JAR包:add jar <path>;
添加完成后,就可以创建函数了:create [temporary] function <fun_name> as <full_name>;
- 指定函数名称
- 传入自定义UDF类的全类名
此时再使用show functions
指令就可以看到新添加的函数了
试用函数:
UDAF
定义UDAF函数大致需要两部分,第一部分是resolver
类,resolver
类需要继承org.apache.hadoop.hive.ql.udf.GenericUDAFResolver2
抽象类,主要用于对传入的参数进行检查,包括参数的个数和参数的类型,下面是resolver
类的基本结构
public class GenericUDAFHistogramNumeric extends AbstractGenericUDAFResolver {
//LOG对象可以采集过程中的WARN和ERROR信息,并反馈到Hive Log中
static final Log LOG = LogFactory.getLog(GenericUDAFHistogramNumeric.class.getName());
@Override
public GenericUDAFEvaluator getEvaluator(GenericUDAFParameterInfo info) throws SemanticException {
// 在这里检查参数的类型和数量
return new GenericUDAFHistogramNumericEvaluator();
}
public static class GenericUDAFHistogramNumericEvaluator extends GenericUDAFEvaluator {
// 在这里编写UDAF的逻辑代码
}
}
第二部分是evaluator
类。evaluator
类定义在resolver
类的内部,并继承org.apache.hadoop.hive.ql.udf.generic.GenericUDAFEvaluator
抽象类,在本类中编写聚合的逻辑代码。
evaluator
的基本结构:
public static class GenericUDAFHistogramNumericEvaluator extends GenericUDAFEvaluator {
// 对于PARTIAL1和COMPLETE: 传入数据类型的ObejctInspector
private PrimitiveObjectInspector inputOI;
private PrimitiveObjectInspector nbinsOI;
// 对于PARTIAL2和FINAL: 部分聚合结果的ObjectInspectors
private StandardListObjectInspector loi;
@Override
public ObjectInspector init(Mode m, ObjectInspector[] parameters) throws HiveException {
super.init(m, parameters);
}
@Override
public Object terminatePartial(AggregationBuffer agg) throws HiveException {
}
@Override
public Object terminate(AggregationBuffer agg) throws HiveException {
}
@Override
public void merge(AggregationBuffer agg, Object partial) throws HiveException {
}
@Override
public void iterate(AggregationBuffer agg, Object[] parameters) throws HiveException {
}
// Aggregation buffer definition and manipulation methods
static class StdAgg implements AggregationBuffer {
};
@Override
public AggregationBuffer getNewAggregationBuffer() throws HiveException {
}
@Override
public void reset(AggregationBuffer agg) throws HiveException {
}
}
要理解上面的代码结构,首先要了解evaluator
中各个方法是做什么的:
方法 | 作用 |
---|---|
init | Hive调用本方法来创建evaluator 类实例 |
getNewAggregationBuffer | 获得临时存储中间结果的对象 |
iterate | 每传入一行数据,使用本方法的逻辑计算,将本行内容加载到临时缓存中 |
terminatePartial | 获取当前缓存的结果,需要注意,返回的类型只能是java基本类型、数组、包装类、Hadoop Writable类型和集合类(List、Map) |
merge | 合并两个terminatePartial 方法获得的中间结果 |
terminate | 获取要输出的最终结果 |
接下来,看一下聚合的几个过程模式Mode
,Mode
是一个定义在GenericUDAFEvaluator
类中的枚举类
/**
* Mode.
*
*/
public static enum Mode {
/**
* PARTIAL1: from original data to partial aggregation data: iterate() and
* terminatePartial() 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:处理输入数据后合并为中间结果,可以理解为MR中的Map阶段
- PARTIAL2:合并中间结果再以最终结果的形式输入,可以理解为MR中的Shuffle-Combine阶段
- FINAL:合并结果并输出,可以理解为MR中的Reduce阶段
- COMPLETE:处理输入数据后直接输出,可以理解为没有Reduce的Map
下面是自定义avg函数的实现:
import org.apache.hadoop.hive.ql.metadata.HiveException;
import org.apache.hadoop.hive.ql.parse.SemanticException;
import org.apache.hadoop.hive.ql.udf.generic.AbstractGenericUDAFResolver;
import org.apache.hadoop.hive.ql.udf.generic.GenericUDAFEvaluator;
import org.apache.hadoop.hive.ql.udf.generic.GenericUDAFParameterInfo;
import org.apache.hadoop.hive.serde2.objectinspector.*;
import org.apache.hadoop.hive.serde2.objectinspector.primitive.DoubleObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.primitive.LongObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory;
import org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorUtils;
import org.apache.hadoop.io.DoubleWritable;
import org.apache.hadoop.io.LongWritable;
import java.util.ArrayList;
import java.util.List;
public class ThirdAggregation extends AbstractGenericUDAFResolver {
@Override
public GenericUDAFEvaluator getEvaluator(GenericUDAFParameterInfo info) throws SemanticException {
// 在此处添加参数判断
return new MyAvgEvaluator();
}
public static class MyAvgEvaluator extends GenericUDAFEvaluator {
// 存储初始输入数据的格式
PrimitiveObjectInspector inputOI;
// 存储临时缓存的格式,用于merge过程取出数值
StructObjectInspector soi;
StructField sumSF;
StructField countSF;
DoubleObjectInspector sumOI;
LongObjectInspector countOI;
// 保存临时结果
Object[] tmpResult;
@Override
public ObjectInspector init(Mode m, ObjectInspector[] parameters) throws HiveException {
super.init(m, parameters);
if (m == Mode.PARTIAL1 || m == Mode.COMPLETE)
inputOI = (PrimitiveObjectInspector) parameters[0];
else {
soi = (StructObjectInspector) parameters[0];
sumSF = soi.getStructFieldRef("sum");
countSF = soi.getStructFieldRef("count");
sumOI = (DoubleObjectInspector) sumSF.getFieldObjectInspector();
countOI = (LongObjectInspector) countSF.getFieldObjectInspector();
}
if (m == Mode.PARTIAL1 || m == Mode.PARTIAL2) {
tmpResult = new Object[2];
List<String> name = new ArrayList<>();
name.add("sum");
name.add("count");
List<ObjectInspector> soi = new ArrayList<>();
soi.add(PrimitiveObjectInspectorFactory.writableDoubleObjectInspector);
soi.add(PrimitiveObjectInspectorFactory.writableLongObjectInspector);
return ObjectInspectorFactory.getStandardStructObjectInspector(name, soi);
} else
return PrimitiveObjectInspectorFactory.writableDoubleObjectInspector;
}
@Override
public AggregationBuffer getNewAggregationBuffer() throws HiveException {
// 新建一个临时缓存对象并返回
TempBuffer b = new TempBuffer();
reset(b);
return b;
}
@Override
public void reset(AggregationBuffer agg) throws HiveException {
// 将传入的临时缓存对象的sum和count都重置为0
TempBuffer b = (TempBuffer) agg;
b.sum = 0;
b.count = 0;
}
@Override
public void iterate(AggregationBuffer agg, Object[] parameters) throws HiveException {
TempBuffer b = (TempBuffer) agg;
// 取出当前行传入的数值
double v = PrimitiveObjectInspectorUtils.getDouble(parameters[0], inputOI);
b.sum += v;
b.count += 1;
}
@Override
public Object terminatePartial(AggregationBuffer agg) throws HiveException {
// 不可以直接返回TempBuffer对象,要和init中指定的类型相匹配
TempBuffer b = (TempBuffer) agg;
tmpResult[0] = new DoubleWritable(b.sum);
tmpResult[1] = new LongWritable(b.count);
return tmpResult;
}
@Override
public void merge(AggregationBuffer agg, Object partial) throws HiveException {
TempBuffer b = (TempBuffer) agg;
// 取出另一个缓存分区中的数据,两者相加
double sum = sumOI.get(soi.getStructFieldData(partial, sumSF));
long count = countOI.get(soi.getStructFieldData(partial, countSF));
b.sum += sum;
b.count += count;
}
@Override
public Object terminate(AggregationBuffer agg) throws HiveException {
// 返回最终结果,这里的结果也要和init中指定的类型匹配,否则会出现ClassCastException
TempBuffer b = (TempBuffer) agg;
return new DoubleWritable(b.sum / b.count);
}
// 临时缓存结构
static class TempBuffer implements AggregationBuffer {
double sum;
long count;
}
}
}
上述代码中的init()
方法,可以结合源码中的文档理解:
/**
* Initialize the evaluator.
*
* @param m
* The mode of aggregation.
* @param parameters
* The ObjectInspector for the parameters: In PARTIAL1 and COMPLETE
* mode, the parameters are original data; In PARTIAL2 and FINAL
* mode, the parameters are just partial aggregations (in that case,
* the array will always have a single element).
* @return The ObjectInspector for the return value. In PARTIAL1 and PARTIAL2
* mode, the ObjectInspector for the return value of
* terminatePartial() call; In FINAL and COMPLETE mode, the
* ObjectInspector for the return value of terminate() call.
*
* NOTE: We need ObjectInspector[] (in addition to the TypeInfo[] in
* GenericUDAFResolver) for 2 reasons: 1. ObjectInspector contains
* more information than TypeInfo; and GenericUDAFEvaluator.init at
* execution time. 2. We call GenericUDAFResolver.getEvaluator at
* compilation time,
*/
对于PARTIAL1 和COMPLETE模式,parameter参数表示传入的原始数据;对于PARTIAL2 和FINAL模式,parameter参数表示临时的聚合结果(对应代码中的第一个if…else)
对于PARTIAL1 和PARTIAL2模式,应当返回terminatePartial()
方法的返回类型;对于FINAL和COMPLETE模式,应当返回terminate()
方法的返回类型(对应代码中第二个if…else)
完成代码后,打包上传并测试,创建函数时,要指定AbstractGenericUDAFResolver
子类全路径,其余过程与普通UDF相同
hive> select shop_id,myavg(month_sales) from ex_spu group by shop_id;
以上实现为了能清楚的显示逻辑,删去了日志输出和各种类型判断,在正常使用时,一定要在这个的基础上加上对参数个数、类型、数值的判断
总结下,能明白几个函数的作用的话,udaf逻辑还是比较简单的,但是需要注意,hive 使用 ObjectInspector来分析行对象的内部结构以及各个列的结构,因此各个方法内数据的读取一定要使用对应的格式来读取;另一点就是UDAF中会大量使用强制类型转换,一定要清楚被转换的对象是由那个方法传递过来的;以上两点是使用udaf报ClassCastException
的主要原因。
当异常类型为NullPointerException
时,可以优先排查定义的几个成员属性有没有在init
方法中做初始化
UDTF
UDTF与UDAF相反,每次传入一行数据,结果输出多行数据。自定义的UDTF类需要继承org.apache.hadoop.hive.ql.udf.generic.GenericUDTF
类。
UDTF的实现相比UDAF要简单很多,下面我们直接实现一个自定义的UDTF案例:
案例数据:
hive> drop table if exists info;
hive> create table info(id int,f_info string,s_info string);
hive> insert into info values(1,"a,b,c","1,2,3") (2,"d,e,f","4,5"),(3,"g,h,i","6,7,8,9");
可以看到第二列和第三列都是由逗号分隔的多个元素组成,下面我们要输入这两列数据,取两个数据集合的笛卡尔积,每个结果输出一行。
实现代码:
import org.apache.hadoop.hive.ql.exec.UDFArgumentException;
import org.apache.hadoop.hive.ql.metadata.HiveException;
import org.apache.hadoop.hive.ql.udf.generic.GenericUDTF;
import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspectorFactory;
import org.apache.hadoop.hive.serde2.objectinspector.StructObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory;
import java.util.ArrayList;
import java.util.List;
public class CrossExplode extends GenericUDTF {
@Override
public StructObjectInspector initialize(StructObjectInspector argOIs) throws UDFArgumentException {
// 列名集合,因为输入两列就添加两个元素
List<String> name = new ArrayList<>();
name.add("first");
name.add("second");
// 列类型集合,和上面的列名集合对应
List<ObjectInspector> oi = new ArrayList<>();
oi.add(PrimitiveObjectInspectorFactory.javaStringObjectInspector);
oi.add(PrimitiveObjectInspectorFactory.javaStringObjectInspector);
// 如果只有一列输入可以直接返回对应的类型,这里是两列的复合类型
return ObjectInspectorFactory.getStandardStructObjectInspector(name, oi);
}
@Override
public void process(Object[] args) throws HiveException {
// 这里默认输入的两列都至少有一个元素,如果要兼顾含有null的情况,就要对args[n]进行判断
String a1 = args[0].toString(); // 获取输入的第一列数据
String[] arr1 = a1.split(",");
String[] arr2 = args[1].toString().split(","); // 获取输入的第二列数据
String[] res = new String[2]; // 用于存储返回的结果,我们要返回两列,所以用数组表示
for (String value : arr1) { // 双重循环,取出所有组合并返回
res[0] = value;
for (String s : arr2) {
res[1] = s;
forward(res); // forward每调用一次就输出一行结果
}
}
}
@Override
public void close() throws HiveException {
// 释放资源,或者在全部forward执行完成后需要追加输出的,在这个位置定义
}
}
代码定义完成,接下来一样的打包上传操作,测试下结果:
hive> add jar /opt/udf-1.0-SNAPSHOT.jar;
hive> create temporary function cre as 'user_defined_functions.CrossExplode';
有两种使用方式:
第一种:直接在select语句中使用:
hive> select cre(f_info,s_info) from info;
输出结果:
这种不能搭配其他列的数值
第二种:在侧视图中使用:
hive> select id,c1,c2 from info lateral view cre(f_info,s_info) a as c1,c2;
输出结果:
自定义的函数当然也是可以查看执行计划的,我们看下上面两种语句的执行计划:
第一种:
第二种:
根据实际需求,灵活选择