ODPS2.0重装上阵,优化提高SQL语言表达能力

MaxCompute(原ODPS)是阿里云自主研发的具备业界领先水平的分布式大数据处理平台, 尤为在集团内部获得普遍应用,支撑了多个BU的核心业务。 MaxCompute除了持续优化性能外,也致力于提高SQL语言的用户体验和表达能力,提升广大ODPS开发者的生产力。html

MaxCompute基于ODPS2.0新一代的SQL引擎,显著提高了SQL语言编译过程的易用性与语言的表达能力。咱们在此推出MaxCompute(ODPS2.0)重装上阵系列文章java

MaxCompute自定义函数的参数和返回值不够灵活,是数据开发过程当中时常被说起的问题。Hive 提供给了 GenericUDF 的方式,经过调用一段用户代码,让用户来根据参数类型决定返回值类型。MaxCompute 出于性能、安全性等考虑,没有支持这种方式。可是MaxCompute也提供了许多方式,让您可以灵活地自定义函数。正则表达式

  • 场景1 须要实现一个UDF,能够接受任意类型的输入,可是MaxCompute的UDF不支持泛型,要作一个接受任何类型的函数,就必须为每种类型都写一个evaluate函数。
  • 场景2 MaxCompute的UDAF和UDTF使用@Resolve的注解来指定输入输出类型,没法重载。要作一个接受多种类型的自定义功能,就须要定义多个不一样的函数。
  • 场景3 MaxCompute支持了参数化视图,可以把一些公共的SQL提出来。参数化视图的表值参数要求输入表的列数和类型与视图定义时彻底一致,若是想要写一个可以接受具备类似特征的不一样的表的视图,还没法定义出来。

本文带你们一块儿看看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类型的列。这其中有几点须要注意:

  • 变长部分必需要写在表值参数定义的最后面,即在 * 的后面不容许再有其余列。这也间接致使了一个表值参数中最多只有一个变长列列表。
  • 因为变长部分必须在最后,有的时候输入表的列不必定是按照这种顺序排列的,这时候须要对输入表的列作必定重排,能够以subquery做为参数(参考上面的例子),注意subquery外面要加一层括号。
  • 因为表值参数中变长部分没有名字,所以在视图定义过程当中没办法得到对这部分数据的引用,也就没有办法对这些数据作运算。这个限制是特地设置的,若是须要对变长部分的数据作运算,须要把要运算的列声明在定长部分,而编译器会对调用时传入的参数进行检查。
  • 虽然不能对变长部分作运算,可是 SELECT * 这种通配符的使用依旧能够将变长部分的列传递出去,如上面的例子在paramed_view中将 @a 的全部列返回,虽然建立视图的时候,a中只有key这一列,可是调用视图的时候,编译器推算出@a中还包含了name, age, address,所以视图返回的数据集中也包含这三列,而视图的调用者也能够对着三列进行操做(如 WHERE age < 20)。
  • 表值参数的列与视图声明时指定的定长列部分不必定彻底一致。若是名字不同,编译器会自动作重命名,若是类型不同,编译器会作隐式转换(不能隐式转换则会报错)。

上面提到的第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注解的语法作了许多扩展,如今可以支持必定的灵活性。

  • 参数列表中可使用星号('*'),表示接受任意长度的,任意类型的输入参数。好比 @Resolve('double,*->String'),接受第一个是double,后接任意类型,任意个数的参数列表。这里须要UDF的做者在代码里面本身去判断输入的个数和类型,而后作出相应的动做(能够对比 C 语言里面的 printf 函数来理解)。注意星号用在返回值列表中时,表示的是不一样的含义,在后续第三点中说明。
  • 参数列表中可使用 ANY 关键字,表示任意类型的参数。好比 @Resolve('double,any->string'),接受第一个是double,第二个任意类型的参数列表。注意,ANY在返回值列表中不能使用,也不能在复杂类型的子类型中使用(如不能写ARRAY)。
  • UDTF的返回值可使用星号,表示返回任意多个string类型。这里须要注意,返回值的个数并不是真的是任意多个,而是与调用函数时给出的alias个数有关。好比@Resolve("ANY,ANY->DOUBLE,*"),调用方式是UDTF(x, y) as (a, b, c),这里as后面给出了三个alias (a, b, c),编译器会认定a为double类型(annotation中返回值第一列的类型是给定的),b,c为string类型,而由于这里给出了三个返回值,因此UDTF在forward的时候,也必定要forward长度为3的数组,不然会出现运行时错误。注意这个错误是没法在编译时给出的,所以一般须要UDTF的做者与调用者互相沟通好,调用者在SQL中给出alias个数的时候,必定要按照UDTF的须要来写。因为Aggregator返回值个数固定是1,因此这个功能对UDAF无心义。

用一个例子来讲明。以下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/

本文为阿里云原创内容,未经容许不得转载。

相关文章
相关标签/搜索