MaxCompute(原ODPS)是阿里云自主研发的具备业界领先水平的分布式大数据处理平台, 尤为在集团内部获得普遍应用,支撑了多个BU的核心业务。 MaxCompute除了持续优化性能外,也致力于提高SQL语言的用户体验和表达能力,提升广大ODPS开发者的生产力。html
MaxCompute基于ODPS2.0新一代的SQL引擎,显著提高了SQL语言编译过程的易用性与语言的表达能力。咱们在此推出MaxCompute(ODPS2.0)重装上阵系列文章java
MaxCompute自定义函数的参数和返回值不够灵活,是数据开发过程当中时常被说起的问题。Hive 提供给了 GenericUDF 的方式,经过调用一段用户代码,让用户来根据参数类型决定返回值类型。MaxCompute 出于性能、安全性等考虑,没有支持这种方式。可是MaxCompute也提供了许多方式,让您可以灵活地自定义函数。正则表达式
本文带你们一块儿看看MaxCompute对这些你们关心的问题都作了哪些改进。json
参数化视图数组
问题安全
参数化视图是MaxCompute本身设计的一种视图。容许用户定义参数,从而可以大大视图代码的复用率。不少用户都利用这一功能,将一些公共SQL提取到视图中,造成公共SQL代码池。分布式
参数化视图在声明过程当中具备局限性:参数类型,长度都是固定的。尤为是参数化视图容许传入表值参数,表值参数要求形参与实参在列的个数和类型上都一致。这一点限制了许多使用场景,以下面的例子:ide
CREATE VIEW paramed_view (@a TABLE(key bigint)) AS SELECT @a.* FROM @a JOIN dim on a.key = dim.key;函数
这个例子封装了一段使用dim表来过滤输入表的逻辑,原本这个是个通用的逻辑,任何包含key这一列的表,均可以用来作输入表。可是因为定义视图时只能肯定输入中包含key列,所以声明的参数类型只包含这一列。致使了视图的调用者传递的表参数必须只能有一列,而返回的数据集也只包含一列,这显然与这个视图的设计初衷不合。性能
改进
最新的MaxCompute版本对参数化视图作了一些改进,能够大大提高参数化视图定义的灵活性。
首先,参数化视图的参数可使用ANY关键字,表示任意类型。如
CREATE VIEW paramed_view (@a ANY) AS SELECT * FROM src WHERE case when @a is null then key1 else key2 end = key3;
这里定义的视图,第一个参数能够接受任意类型。注意ANY类型不能参与如 '+', 'AND' 之类的须要明确类型才能作的运算。ANY类型更可能是在TABLE参数中作passthrough列,如
CREATE VIEW paramed_view (@a TABLE(name STRING, id ANY, age BIGINT)) AS SELECT * FROM @a WHRER name = 'foo' and age < 25; -- 调用示例 SELECT * FROM param_view((SELECT name, id, age from students));
上面的视图接受一个表值参数,可是并不关心这个表的第二列,那么这个列能够直接定义为ANY类型。参数化视图在调用时,每次都会根据输入参数的实际类型从新推算返回值类型。好比上面的视图,当输入的表是 TABLE(c1 STRING, c2 DOUBLE, c3 BIGINT),那么输出的数据集的第二列也会自动变成DOUBLE类型,让视图的调用者可使用任何可用于DOUBLE类型的操做来操做这一列。
须要注意的一点是,咱们用CREATE VIEW建立了视图后,能够用DESC来获取视图的描述,这个描述中会包含视图的返回类型信息。可是因为视图的返回类型是在调用的时候从新推算的,从新推算出来的类型可能与建立视图时推导出来的不一致。一个例子就是上面的ANY类型。
在ANY以外,参数化视图中的表值参数还支持了*,表示任意多个列。这个 * 能够带类型,也可使用ANY类型。如
CREATE VIEW paramed_view (@a TABLE(key STRING, * ANY), @b TABLE(key STRING, * STRING)) AS SELECT a.* FROM @a JOIN @b ON a.key = b.key; -- 调用示例 SELECT name, address FROM param_view((SELECT school, name, age, address FROM student), school) WHERE age < 20;
上面这个视图接受两个表值参数,第一个表值参数第一列是string类型,后面能够是任意多个任意类型的列,而第二个表值参数的第一列是string,后面能够是任意多个STRING类型的列。这其中有几点须要注意:
上面提到的第4点很是有用,一方面保证了调用视图是输入参数的灵活性,另外一方面又不下降数据的信息量。好好利用可以很大程度上增长公共代码的复用率。
下面是一个调用示例。该例子使用的视图是:
CREATE VIEW paramed_view (@a TABLE(key STRING, * ANY), @b TABLE(key STRING, * STRING)) AS SELECT a.* FROM @a JOIN @b ON a.key = b.key;
在MaxCompute Studio中调用,能够享受语法高亮和错误提示等功能。执行的调用代码以下:
执行的状态图以下:
放大执行过程仔细观察,图中能够发现几点有意思的地方:
上述执行输出的结果以下:
+------+---------+ | name | address | +------+---------+ | 小明 | 杭州 | +------+---------+
其余用法
常常有用户误用参数化视图,将参数化视图的参数当作是宏替换参数来使用。这里说明一下。参数化视图其实是函数调用,而不是宏替换。以下面的例子:
CREATE VIEW paramed_view(@a TABLE(key STRING, value STRING), @b STRING) AS SELECT * FROM @a ORDER BY @b; -- 调用示例 select * from paramed_view(src, 'key');
上面的例子中,用户的指望是 ORDER BY @b 被宏替换为 ORDER BY key,即根据输入参数,决定了按照key列作排序。然而,实际上参数@b是做为一个值来传递的,ORDER BY @b 至关于 ORDER BY 'key',即 ORDER BY一个字符串常量('key')而不是一列。要想实现"让调用者决定排序列"这一功能,能够考虑下述作法。
CREATE VIEW orderByFirstCol(@a TABLE(columnForOrder ANY, * ANY)) AS SELECT `(columnForOrder)?+.+` FROM (SELECT * FROM @a ORDER BY columnForOrder) t; -- 调用示例 select * from orderByFirstCol((select key, * from src));
上面的例子,要求调用者将要排序的列放在第一列,因而在调用的时候使用子查询将src的须要排序的列抽取到最前面。视图返回的 (columnForOrder)?+.+ 是一个正则通配符,匹配columnForOrder以外的全部列,列表达式使用正则表达式可参考SELECT语法介绍>列表达式关于正则表达式的说明。
UDF:函数重载方式
问题
MaxCompute 的 UDF 使用重载 evalaute 方法的方式来重载函数,以下面的UDF定义了两个重载,当输入是 String 类型时,输出String类型,输入是BIGINT类型时,输出DOUBLE类型。
public UDFClass extends UDF { public String evaluate(String input) { return input + "123"; } public Double evaluate(Long input) { return input + 123.0; } }
这种方式当然能解决一些问题,但有必定的局限性。好比不支持泛型,要作一个接受任何类型的函数,就必须为每种类型都写一个evaluate函数。有的时候重载甚至是不能实现的,好比ARRAY 和 ARRAY 的重载是作不到的。
public UDFClass extends UDF { public String evaluate(List<Long> input) { return input.size(); } // 这里会报错,由于在java类型擦除后,这个函数和 String evaluate(List<Long> input) 的参数是同样的 public Double evaluate(List<Double> input) { input.size(); } // UDF 不支持下面这种定义方式 public String evaluate(List<Object> input) { return input.size(); } }
PYTHON UDF 或 UDTF 在不提供 Resolve 注解(annotation)的时候,会根据参数个数决定输入参数,也支持变长,所以很是灵活。但也由于过于灵活,编译器没法静态找到某些错误。好比
class Substr(object):
def evaluate(self, a, b): return a\[b:\];
上面的函数接受两个参数,从实现上看,第一个参数须要是STRING类型,第二个参数应该是整形。而这个限制须要用户在调用时本身去把握。即便用户传错了参数,编译器也没有办法报错。同时,这种方式定义的UDF返回值类型只能是STRING,不够灵活。
改进
要解决上面的问题。能够考虑使用UDT。 UDT常常被简单在调用JDK中的方法的时候使用,好比 java.util.Objects.toString(x) 将任何对象 x 转成STRING类型。可是在自定义函数方面一样也有很好的用途。 UDT支持泛型,支持类继承,支持变长等功能,让定义函数更方便。以下面的例子:
public class UDTClass { // 这个函数接受一个数值类型(能够是 TINYINT, SMALLINT, INT, BIGINT, FLOAT, DOUBLE 以及任何以Number为基类的UDT),返回DOUBLE public static Double doubleValue(Number input) { return input.doubleValue(); } // 这个方法,接受一个数值类型参数和一个任意类型的参数,返回值类型与第二个参数的类型相同 public static <T extends Number, R> R nullOrValue(T a, R b) { return a.doubleValue() > 0 ? b : null; } // 这个方法接受一个任意元素类型的array或List,返回BIGINT public static Long length(java.util.List<? extends Object> input) { return input.size(); } // 注意这个在不作强制转换的状况下参数只能接受 UDT 的 java.util.Map<Object, Object> 对象。若是须要传入任何map对象,好比 map<bigint,bigint> 能够考虑: // 1. 定义函数时使用java.util.Map<? extends Object, ? extends Object> // 2. 调用时强转,好比 UDTClass.mapSize(cast(mapObj as java.util.Map<Object, Object>)) public static Long mapSize(java.util.Map<Object, Object> input) { return input.size(); } }
UDT 可以提供灵活的函数定义方式。可是有的时候UDF 须要经过 com.aliyun.odps.udf.ExecutionContext(在setup方法中传入)来获取一些上下文。如今UDT也能够经过 com.aliyun.odps.udt.UDTExecutionContext.get() 方法来或者这样的一个 ExecutionContext 对象。
Aggregator 与 UDTF:Annotation方式
问题
MaxCompute 的 UDAF 和 UDTF 使用Resolve注解来决定函数Signature。好比下面的方式定义了一个UDTF,该UDTF接受一个BIGINT参数,返回DOUBLE类型。
@com.aliyun.odps.udf.annotation.Resolve("BIGINT->DOUBLE") public class UDTFClass extends UDTF { ... }
这种方式的局限性很明显,输入参数和输出参数都是固定的,没办法重载。
改进
MaxCompute对Resolve注解的语法作了许多扩展,如今可以支持必定的灵活性。
用一个例子来讲明。以下UDTF:
import com.aliyun.odps.udf.UDFException; import com.aliyun.odps.udf.UDTF; import com.aliyun.odps.udf.annotation.Resolve; import org.json.JSONException; import org.json.JSONObject; @Resolve("STRING,*->STRING,*") public class JsonTuple extends UDTF { private Object[] result = null; @Override public void process(Object[] input) throws UDFException { if (result == null) { result = new Object[input.length]; } try { JSONObject obj = new JSONObject((String)input[0]); for (int i = 1; i < input.length; i++) { // 返回值要求变长部分都是STRING result[i] = String.valueOf(obj.get((String)(input[i]))); } result[0] = null; } catch (JSONException ex) { for (int i = 1; i < result.length; i++) { result[i] = null; } result[0] = ex.getMessage(); } forward(result); } }
这个UDTF的返回值个数会根据输入参数的个数来决定。输出参数的第一个是一个JSON文本,后面是须要从JSON中解析的key。返回值第一个是解析JSON过程当中的出错信息,若是没有出错,则后续根据输入的key依次输出从json中解析出来的内容。使用示例以下。
-- 根据输入参数的个数定制输出alias个数 SELECT my_json_tuple(json, ’a‘, 'b') as exceptions, a, b FROM jsons; -- 变长部分能够一列都没有 SELECT my_json_tuple(json) as exceptions, a, b FROM jsons; -- 下面这个SQL会出现运行时错误,由于alias个数与实际输出个数不符 -- 注意编译时没法发现这个错误 SELECT my_json_tuple(json, 'a', 'b') as exceptions, a, b, c FROM jsons;
上面虽然作出了许多扩展,可是这些扩展并不必定能知足全部的需求。这时候依然能够考虑使用UDT。UDT也是能够用来实现Aggregator和UDTF的功能的。详细能够参考UDT示例文档,“聚合操做的实现示例” 及 “表值函数的实现示例” 的内容。
总结
MaxCompute自定义函数的函数原型不够灵活,在数据开发过程当中带来诸多不便利,本文列举了各类函数定义方式存在的问题与解决方案,但愿对你们有帮助,同时也告诉你们MaxCompute一直在努力为你们提供更好的服务。
上云就看云栖号:更多云资讯,上云案例,最佳实践,产品入门,访问:https://yqh.aliyun.com/
本文为阿里云原创内容,未经容许不得转载。