背景及目的
本文简单地介绍了一下如何新建工程,添加代码,打包,上传资源包和注册方法,对初次接触的用户提供帮助。
另外,详解介绍通过UDF来满足不同的计算需求。
UDF 开发流程如下:
UDF概述
UDF全称为User Defined Function,即用户自定义函数。MaxCompute提供很多内建函数来满足您的计算需求,同时您还可以通过创建自定义函数来满足不同的计算需求。
UDF在使用上与普通的 内建函数类似,Java和MaxCompute的数据类型的对应关系,请参见 参数与返回值类型。
如果您使用Maven,可以从 Maven 库中搜索odps-sdk-udf,从而获取不同版本的Java SDK,相关配置信息如下所示:
<dependency>
<groupId>com.aliyun.odps</groupId>
<artifactId>odps-sdk-udf</artifactId>
<version>0.29.10-public</version>
</dependency>
MaxCompute支持的UDF有三种:
User Defined Scalar Function(通常也称之为UDF)
用户自定义标量值函数(User Defined Scalar Function)。其输入与输出是一对一的关系,即读入一行数据,写出一条输出值。
UDTF(User Defined Table Valued Function)
自定义表值函数,是用来解决一次函数调用输出多行数据场景的,也是唯一能返回多个字段的自定义函数。而UDF只能一次计算输出一条返回值。
UDAF(User Defined Aggregation Function)
自定义聚合函数,其输入与输出是多对一的关系, 即将多条输入记录聚合成一条输出值。可以与SQL中的Group By语句联用。具体语法请参见 聚合函数。
Java UDF
数据类型对应关系
MaxCompute Type | Java Type |
---|---|
Tinyint | java.lang.Byte |
Smallint | java.lang.Short |
Int | java.lang.Integer |
Bigint | java.lang.Long |
Float | java.lang.Float |
Double | java.lang.Double |
Decimal | java.math.BigDecimal |
Boolean | java.lang.Boolean |
String | java.lang.String |
Varchar | com.aliyun.odps.data.Varchar |
Binary | com.aliyun.odps.data.Binary |
Datetime | java.util.Date |
Timestamp | java.sql.Timestamp |
Array | java.util.List |
Map | java.util.Map |
Struct | com.aliyun.odps.data.Struct |
UDF
实现UDF需要继承com.aliyun.odps.udf.UDF类,并实现evaluate方法。evaluate方法必须是非static的public方法 。Evaluate方法的参数和返回值类型将作为SQL中UDF的函数签名。这意味着您可以在UDF中实现多个evaluate方法,在调用UDF时,框架会依据UDF调用的参数类型匹配正确的evaluate方法 。
UDF的示例如下:
package org.alidata.odps.udf.examples;
import com.aliyun.odps.udf.UDF;
public final class Lower extends UDF {
public String evaluate(String s) {
if (s == null) {
return null;
}
return s.toLowerCase();
}
}
可以通过实现void setup(ExecutionContext ctx)和void close()来分别实现UDF的初始化和结束代码。
如以下代码,定义了一个有三个overloads的UDF,其中第一个用了array作为参数,第二个用了map作为参数,第三个用了struct。由于第三个overloads了struct作为参数或者返回值,因此要求必须要对UDF class打上 @Resolve annotation,来指定struct的具体类型。
@Resolve("struct,string->string")
public class UdfArray extends UDF {
public String evaluate(List vals, Long len) {
return vals.get(len.intValue());
}
public String evaluate(Map map, String key) {
return map.get(key);
}
public String evaluate(Struct struct, String key) {
return struct.getFieldValue("a") + key;
}
}
用户可以直接将复杂类型传入UDF中:
create function my_index as 'UdfArray' using 'myjar.jar';
select id, my_index(array('red', 'yellow', 'green'), colorOrdinal) as color_name from co
UDAF
实现Java UDAF类需要继承 com.aliyun.odps.udf.Aggregator,并实现如下几个接口:
public abstract class Aggregator implements ContextFunction {
@Override
public void setup(ExecutionContext ctx) throws UDFException {
}
@Override
public void close() throws UDFException {
}
/**
* 创建聚合Buffer
* @return Writable 聚合buffer
*/
abstract public Writable newBuffer();
/**
* @param buffer 聚合buffer
* @param args SQL中调用UDAF时指定的参数,不能为null,但是args里面的元素可以为null,代表对应的输入数据是null
* @throws UDFException
*/
abstract public void iterate(Writable buffer, Writable[] args) throws UDFException;
/**
* 生成最终结果
* @param buffer
* @return Object UDAF的最终结果
* @throws UDFException
*/
abstract public Writable terminate(Writable buffer) throws UDFException;
abstract public void merge(Writable buffer, Writable partial) throws UDFException;
}
其中最重要的是iterate,merge和terminate三个接口,UDAF的主要逻辑依赖于这三个接口的实现。此外,还需要您实现自定义的Writable buffer。
以实现求平均值avg为例,下图简要说明了在MaxCompute UDAF中这一函数的实现逻辑及计算流程:
在上图中,输入数据被按照一定的大小进行分片(有关分片的描述请参见 MapReduce),每片的大小适合一个worker在适当的时间内完成。这个分片大小的设置需要您手动配置完成。
UDAF的计算过程分为两个阶段:
第一阶段:每个worker统计分片内数据的个数及汇总值,您可以将每个分片内的数据个数及汇总值视为一个中间结果。
第二阶段:worker汇总上一个阶段中每个分片内的信息。在最终输出时,r.sum / r.count即是所有输入数据的平均值。
计算平均值的UDAF的代码示例,如下所示:
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import com.aliyun.odps.io.DoubleWritable;
import com.aliyun.odps.io.Writable;
import com.aliyun.odps.udf.Aggregator;
import com.aliyun.odps.udf.UDFException;
import com.aliyun.odps.udf.annotation.Resolve;
@Resolve("double->double")
public class AggrAvg extends Aggregator {
private static class AvgBuffer implements Writable {
private double sum = 0;
private long count = 0;
@Override
public void write(DataOutput out) throws IOException {
out.writeDouble(sum);
out.writeLong(count);
}
@Override
public void readFields(DataInput in) throws IOException {
sum = in.readDouble();
count = in.readLong();
}
}
private DoubleWritable ret = new DoubleWritable();
@Override
public Writable newBuffer() {
return new AvgBuffer();
}
@Override
public void iterate(Writable buffer, Writable[] args) throws UDFException {
DoubleWritable arg = (DoubleWritable) args[0];
AvgBuffer buf = (AvgBuffer) buffer;
if (arg != null) {
buf.count += 1;
buf.sum += arg.get();
}
}
@Override
public Writable terminate(Writable buffer) throws UDFException {
AvgBuffer buf = (AvgBuffer) buffer;
if (buf.count == 0) {
ret.set(0);
} else {
ret.set(buf.sum / buf.count);
}
return ret;
}
@Override
public void merge(Writable buffer, Writable partial) throws UDFException {
AvgBuffer buf = (AvgBuffer) buffer;
AvgBuffer p = (AvgBuffer) partial;
buf.sum += p.sum;
buf.count += p.count;
}
}
使用Writable类型实现Concat的示例如下:
package com.aliyun.odps.udf.example;
import com.aliyun.odps.io.Text;
import com.aliyun.odps.udf.UDF;
public class MyConcat extends UDF {
private Text ret = new Text();
public Text evaluate(Text a, Text b) {
if (a == null || b == null) {
return null;
}
ret.clear();
ret.append(a.getBytes(), 0, a.getLength());
ret.append(b.getBytes(), 0, b.getLength());
return ret;
}
}
UDTF
Java UDTF需要继承 com.aliyun.odps.udf.UDTF类。这个类需要实现4个接口,如下表所示:
接口定义 | 描述 |
---|---|
public void setup(ExecutionContext ctx) throws UDFException | 初始化方法,在UDTF处理输入数据前,调用用户自定义的初始化行为。在每个Worker内setup会被先调用一次。 |
public void process(Object[] args) throws UDFException | 这个方法由框架调用,SQL中每一条记录都会对应调用一次process,process的参数为SQL语句中指定的UDTF输入参数。输入参数以Object[]的形式传入,输出结果通过forward函数输出。用户需要在process函数内自行调用forward,以决定输出数据。 |
public void close() throws UDFExcepti |