Hive中的用户自定义函数UDF
Hive中的自定义函数允许用户扩展HiveQL,是一个非常强大的功能。Hive中具有多种类型的用户自定义函数。show functions命令可以列举出当前Hive会话中的所加载进来的函数,包括内置的以及用户加载的函数。
函数都有自身的使用文档,使用describe function命令就可以展示对应函数基本介绍。
标准函数UDF
用户自定义函数指的是一行数据中的一列或是多列数据作为参数然后返回结果是一个值的函数。这里用一个例子作为说明,我们当前有些数据表中存储的时间戳是以秒为单位的long值,如果想将这个long值转换为当天的hour值,就需要写一个UDF(尽管这个需求可以通过Hive中的标准函数解决...但我们举个例子)。
简单的UDF编写,直接继承org.apache.hadoop.hive.ql.exec.UDF,并实现evaluate方法,其中UDF抽象类中并没有严格要求evaluate的方法签名,这意味着可以使用Java中的重载,这样在实际使用时会根据传输参数的类型选择合适的方法;对于每行输入都会调用到evaluate函数,处理后的结果返回给Hive。
public class ToHourFunction extends UDF{ public String evaluate(String timeMillis) { long longTimeMillis = Long.parseLong(timeMillis); Date date = new Date(longTimeMillis * 1000L); return String.format("%tk", date); } }
对于自定的UDF,为了让其他人在使用时能够得到一些帮助信息,最好添加@Description注解。注解中包含了这个函数的文档说明,用户也需要这个注解来说明自定义UDF如何使用,比如添加下面的注解:
@Description(name = "to_hour", value = "_FUNC_(x) - return long time millis(plus 1000) to current hour", extended = "Example: to_hour(123232343) ")
value中的_FUNC_会被替换成用户为这个函数定义的“临时”函数名称,当添加这个jar包,并添加这个自定义函数,将其命名为to_hour
create temporary function to_hour as 'test.udf.ToHourFunction';
添加完成后,执行show functions命令就会额外添加一个我们的自定义函数,使用describe function to_hour能够显示出函数的具体使用方法(@Description中的内容)
to_hour(x) - return long time millis(plus 1000) to current hour
通过使用describe function extended to_hour可以显示@Descrption中的extended属性内容:
Example: to_hour(123232343)
除了UDF类,Hive还提供了一个对应的称为GenericUDF的类,GenericUDF是更为复杂的抽象概念,但是支持更好的null值处理。在Hive中本身就有很多函数是用GenericUDF实现的,我们可以参考他们的写法,比如GenericUDFAbs就是对一些基本的数据类型(SHORT/BYTE/INT/LONG/DOUBLE/FLOAT/STRING/DECIMAL)取绝对值的函数。
比如上面同样的to_hour函数,我们可以用GenericUDF重写一遍。继承GenericUDF函数之后,需要实现三个方法。
首先,initialize方法作为初始化使用,这个方法的目标是确定函数的参数类型,如果参数的数量以及类型不合法,要抛出异常以给用户必要的提示。to_hour方法中,我们需要界定输入参数必须为long类型,参数数量为1个,输出的结果为int类型。
private IntWritable resultInt = new IntWritable(); private ObjectInspectorConverters.Converter inputConverter; @Override public ObjectInspector initialize(ObjectInspector[] arguments) throws UDFArgumentException { if (arguments.length != 1) { throw new UDFArgumentLengthException( "to_hour requires 1 argument, got " + arguments.length); } if (arguments[0].getCategory() != ObjectInspector.Category.PRIMITIVE) { throw new UDFArgumentException( "to_hour only takes primitive types, got " + arguments[0].getTypeName()); } PrimitiveObjectInspector argumentOIs = (PrimitiveObjectInspector) arguments[0]; PrimitiveObjectInspector.PrimitiveCategory inputType = argumentOIs.getPrimitiveCategory(); if (inputType != PrimitiveObjectInspector.PrimitiveCategory.LONG) { throw new UDFArgumentTypeException(0, "Argument type should be long"); } inputConverter = ObjectInspectorConverters .getConverter(arguments[0], PrimitiveObjectInspectorFactory.writableLongObjectInspector); return PrimitiveObjectInspectorFactory.writableIntObjectInspector; }
方法evaluate的输入是一个DeferredObject[],通过initialize方法初始化的inputConverter将DeferredObject转换成对应的Writable对象,这一步中如果输入参数类型可变的话,这个inputConverter的意义就比较重大了,这一点请参考Hive中Abs函数的实现。后面就是将这个数据写到可重用的IntWritable对象中。
@Override public Object evaluate(DeferredObject[] arguments) throws HiveException { Object valueObj = arguments[0].get(); if (valueObj == null) { return null; } valueObj = inputConverter.convert(valueObj); long mills = ((LongWritable) valueObj).get() * 1000L; String hourString = String.format("%tk", new Date(mills)); int hour = Integer.parseInt(hourString); resultInt.set(hour); return resultInt; }
最后一个需要实现的方法是getDisplayString(),用在Hadoop Task内部,在使用此函数时显示调试信息。
@Override public String getDisplayString(String[] children) { StringBuilder sb = new StringBuilder(); sb.append("to_hour("); if (children.length > 0) { sb.append(children[0]); for (int i = 1; i < children.length; i++) { sb.append(","); sb.append(children[i]); } } sb.append(")"); return sb.toString(); }
截止目前,我们写的函数都只能创建为temp函数,如果想加入到Hive本身作为系统的不变函数使用,就需要修改org.apache.hadoop.hive.ql.exec.FunctionRegistry中的static静态块,将我们的函数加入进去,并重新编译部署hive(当然比较麻烦,不过仅需要替换hive-exec-版本号.jar文件即可)。稍微简单并灵活的方法是,我们写一个公用的shell加载脚本,每个hiveql文件在头上手动加载这个脚本,将公用的函数库加入进去。