0. 写在前面

Presto Functions 并不能像 Hive UDF 一样动态加载,需要根据 Function 的类型,实现 Presto 内部定义的不同接口,在 Presto 服务启动时进行注册,然后才能在 SQL 执行时进行调用。

1. 函数定义

Presto 内部将 Functions 分为以下三大类:

  • Scalar Function,即标量函数。将传递给它的一个或者多个参数值,进行计算后,返回一个确定类型的标量值。
  • Aggregation Function,即聚合函数。计算从列中取得的值,返回一个单一的值。
  • Window Function,即开窗函数。计算从分组列中取得的值,并返回多个值。

对于不同类型的函数,需要遵循不同的规则进行实现。

1.1 标量函数

Presto 使用注解框架来实现标量函数,标量函数分别需要定义函数名称、输入参数类型和返回结果类型。下面介绍几种开发标量函数常用的注解:

  • @ScalarFunction:用于声明标量函数的名称和别名
  • @Description:用于生成函数的功能描述
  • @SqlType:用于声明函数的返回类型和参数类型
  • @TypeParameter:用于声明类型变量,它所声明的类型变量可以用于函数的返回类型和参数类型,框架在运行时会自动将变量与具体的类型进行绑定
  • @SqlNullable:用于表示函数参数或返回结果可能为NULL。如果方法的参数不使用此注解,当函数参数包含NULL时,则该函数不会被调用,框架自动返回结果NULL。当 Java 代码中用于实现函数的方法的返回值为包装类型时,必须要在实现方法上加上该注解,且该注解无法用于 Java 基础类型

下面用一个简单的is_null函数来具体说明如何使用以上注解进行标量函数开发。

  1. public class ExampleIsNullFunction
  2. {
  3. @ScalarFunction(value = "is_null", alias = "isnull")
  4. @Description("Returns TRUE if the argument is NULL")
  5. @SqlType(StandardTypes.BOOLEAN)
  6. public static boolean isNull(@SqlNullable @SqlType(StandardTypes.VARCHAR) Slice string)
  7. {
  8. return (string == null);
  9. }
  10. }

以上代码实现的is_null函数功能为:判断传入的VARCHAR类型参数是否为NULL,如果为NULL则返回true,否则返回false。其中:

  • @ScalarFunction(value = "is_null", alias = "isnull")声明了函数名为is_null,函数别名为isnull,即在 SQL 中使用is_nullisnull都可以调用该函数
  • @Description("Returns TRUE if the argument is NULL")声明了函数描述,使用show functions命令可以看到函数的描述
  • @SqlType(StandardTypes.BOOLEAN)声明了函数的返回类型为BOOLEAN
  • 因为当函数参数为NULL时,我们不能直接返回NULL,而是要进行判断,所以要加上@SqlNullable避免框架自动返回NULL
  • @SqlType(StandardTypes.VARCHAR)声明了函数的参数类型为VARCHAR

注意到,这里使用了 Java 类型Slice来接收 SQL 中VARCHAR类型的值。框架会自动将 SQL 中的数据类型与“原生容器类型”(Native container type)进行绑定,目前“原生容器类型”只包括:booleanlongdoubleSliceBlockVARCHAR对应的原生容器类型是Slice而不是String,Slice的本质是对byte[]进行了封装,为的是更加高效、自由地对内存进行操作。Block可以简单的理解为对应 SQL 中的数组类型。具体的对应关系和绑定过程涉及 Presto 的类型系统和函数调用过程,不是本文讲解的重点,故在此不作展开。

进一步地,我们想对 is_null函数进行升级,使它能够处理任意类型的参数,这时@TypeParameter注解就派上用场了,函数的实现可以改写为:

  1. @ScalarFunction(value = "is_null", alias = "isnull")
  2. @Description("Returns TRUE if the argument is NULL")
  3. public class ExampleIsNullFunction
  4. {
  5. private IsNullFunctions()
  6. {
  7. }
  8. @TypeParameter("T")
  9. @SqlType(StandardTypes.BOOLEAN)
  10. public static boolean isNullSlice(@SqlNullable @SqlType("T") Slice value)
  11. {
  12. return (value == null);
  13. }
  14. @TypeParameter("T")
  15. @SqlType(StandardTypes.BOOLEAN)
  16. public static boolean isNullLong(@SqlNullable @SqlType("T") Long value)
  17. {
  18. return (value == null);
  19. }
  20. @TypeParameter("T")
  21. @SqlType(StandardTypes.BOOLEAN)
  22. public static boolean isNullDouble(@SqlNullable @SqlType("T") Double value)
  23. {
  24. return (value == null);
  25. }
  26. @TypeParameter("T")
  27. @SqlType(StandardTypes.BOOLEAN)
  28. public static boolean isNullBoolean(@SqlNullable @SqlType("T") Boolean value)
  29. {
  30. return (value == null);
  31. }
  32. @TypeParameter("T")
  33. @SqlType(StandardTypes.BOOLEAN)
  34. public static boolean isNullBlock(@SqlNullable @SqlType("T") Block value)
  35. {
  36. return (value == null);
  37. }
  38. }

可以看到,@TypeParameter的使用有点类似 Java 中泛型的用法,类型变量T在声明完之后就可以在@SqlType注解中使用。在实际的调用过程中,框架会将T与实际 SQL 类型进行绑定,然后再去调用以对应的原生容器类型为参数的实际方法。

1.2 聚合函数

聚合的过程一般涉及多行,有一个累积计算的过程,又由于 Presto 是一个分布式的计算引擎,数据分布在多个节点,所以需要用状态对象来维护和记录中间计算结果。

引入状态之后,Presto 将聚合的过程抽象为三个步骤:

  1. input(state, value)
  2. combine(state1, state2)
  3. output(state, out)

首先,input 阶段分别在不同的 worker 中进行,将行值进行累积计算到state中;combine阶段将上一步得到的state进行两两结合;经过前两步,最终会得到一个state,在output阶段对最终的state进行处理输出。

在实现方面,聚合函数的开发使用了和标量函数类似的注解框架,但是由于状态概念的引入,需要定义一个继承于AccumulatorState接口的状态接口,对于简单的聚合,该接口只需要新增聚合所需的gettersetter,框架会自动生成相关的实现和序列化代码;如果聚合过程中需要记录复杂类型(LISTMAP或自定义的类)的状态,则需要额外实现AccumulatorStateFactory接口和AccumulatorStateSerializer接口,并在状态接口上使用@AccumulatorStateMetadata注解,在注解中指定stateFactoryClassstateSerializerClass

下面以实现求DOUBLE类型的列均值的聚合函数avg_double为例来说明如何进行简单聚合函数的开发。

avg_double的聚合状态只需要记录累积和与加数个数,所以状态接口的定义如下:

  1. public interface LongAndDoubleState
  2. extends AccumulatorState
  3. {
  4. long getLong();
  5. void setLong(long value);
  6. double getDouble();
  7. void setDouble(double value);
  8. }

使用定义好的状态接口进行聚合函数实现:

  1. @AggregationFunction("avg_double")
  2. public class AverageAggregation
  3. {
  4. @InputFunction
  5. public static void input(LongAndDoubleState state, @SqlType(StandardTypes.DOUBLE) double value)
  6. {
  7. state.setLong(state.getLong() + 1);
  8. state.setDouble(state.getDouble() + value);
  9. }
  10. @CombineFunction
  11. public static void combine(LongAndDoubleState state, LongAndDoubleState otherState)
  12. {
  13. state.setLong(state.getLong() + otherState.getLong());
  14. state.setDouble(state.getDouble() + otherState.getDouble());
  15. }
  16. @OutputFunction(StandardTypes.DOUBLE)
  17. public static void output(LongAndDoubleState state, BlockBuilder out)
  18. {
  19. long count = state.getLong();
  20. if (count == 0) {
  21. out.appendNull();
  22. }
  23. else {
  24. double value = state.getDouble();
  25. DOUBLE.writeDouble(out, value / count);
  26. }
  27. }
  28. }

可以看到聚合函数的实现使用了以下注解:

  • @AggregationFunction声明了聚合函数的名称,也可以指定函数的别名
  • @InputFunction@CombineFunction@OutputFunction分别用来标记聚合的三个步骤,其中@OutputFunction注解需要声明聚合函数返回结果的数据类型
  • BlockBuilder类为结果输出类,聚合计算出的最终结果值将通过BlockBuilder进行输出

1.3 窗口函数

窗口函数在查询结果的行上进行计算,执行顺序在HAVING子句之后,ORDER BY子句之前。在 Presto SQL 中,窗口函数的语法形式如下:

  1. windowFunction(arg1,....argn) OVER([PARTITION BY<...>] [ORDER BY<...>] [RANGE|ROWS BETWEEN AND])

由此可见,窗口函数语法由关键字OVER触发,且包含三个子句:

  1. PARTITION BY: 指定输入行分区的规则,类似于聚合函数的GROUP BY子句,不同分区里的计算互不干扰(窗口函数的计算是并发进行的,并发数和partition数量一致),缺省时将所有数据行视为一个分区
  2. ORDER BY: 决定了窗口函数处理输入行的顺序
  3. RANGE|ROWS BETWEEN AND: 指定窗口边界,不常用,缺省时的窗口为当前行所在的分区第一行到当前行

窗口函数的开发需要实现WindowFunction接口,WindowFunction接口中声明了两个方法:

  • void reset(WindowIndex windowIndex): 处理新分区时,都会调用该方法进行初始化,WindowIndex包含了已排序的分区的所有行
  • void processRow(BlockBuilder output, int peerGroupStart, int peerGroupEnd, int frameStart, int frameEnd): 窗口函数的实现方法,BlockBuilder为结果输出类,计算出来的值将通过BlockBuilder进行输出;peerGroupStartpeerGroupEnd为当前处理的行所在的分区的开始和结束的位置;frameStartframeEnd为当前处理行所在的窗口的开始和结束位置。

实现一个返回窗口中第一个值的窗口函数first_value(x)的代码如下:

  1. @WindowFunctionSignature(name = "first_value", typeVariable = "T", returnType = "T", argumentTypes = "T")
  2. public class FirstValueFunction
  3. extends WindowFunction
  4. {
  5. private final int argumentChannel;
  6. private WindowIndex windowIndex;
  7. public FirstValueFunction(List<Integer> argumentChannels)
  8. {
  9. this.argumentChannel = getOnlyElement(argumentChannels);
  10. }
  11. @Override
  12. public void reset(WindowIndex windowIndex)
  13. {
  14. this.windowIndex = windowIndex;
  15. }
  16. @Override
  17. public void processRow(BlockBuilder output, int peerGroupStart, int peerGroupEnd, int frameStart, int frameEnd)
  18. {
  19. if (frameStart < 0) {
  20. output.appendNull();
  21. return;
  22. }
  23. //Outputs a value from the index
  24. windowIndex.appendTo(argumentChannel, frameStart, output);
  25. }
  26. }

其中:

  • @WindowFunctionSignature注解声明了窗口函数的名称,为了处理任意数据类型的字段,这里还声明了类型变量T,并将返回类型和参数类型都指定为T
  • 构造函数中的argumentChannels为参数字段所在列的索引值
  • processRow方法中,每次只需要通过列索引argumentChannel和当前行所在的窗口起始索引frameStart,就能确定窗口中的第一个值

2. 函数注册

Presto 函数由MetadataManager中的FunctionRegistry进行管理,开发的函数要生效必须要先注册到FunctionRegistry中。函数注册是在 Presto 服务启动过程中进行的,有以下两种方式进行函数注册。

2.1 内置函数注册

内置函数指的是 Presto 自带的函数库中的函数,函数的实现位于presto-main模块中,在FunctionRegistry初始化时进行注册。具体的注册过程使用了建造者模式,不同类型的函数注册只需要调用FunctionListBuilder对象对应的方法进行注册,关键代码如下:

  1. FunctionListBuilder builder = new FunctionListBuilder()
  2. .window(RowNumberFunction.class)
  3. .aggregate(ApproximateCountDistinctAggregation.class)
  4. .scalar(RepeatFunction.class)
  5. .function(MAP_HASH_CODE)
  6. ......

2.2 插件函数注册

内置函数满足不了使用需求时,就需要自行开发函数来拓展函数库。开发者自行编写的拓展函数一般通过插件的方式进行注册。PluginManager在安装插件时会调用插件的getFunctions()方法,将获取到的函数集合通过MetadataManageraddFunctions方法进行注册:

  1. public void installPlugin(Plugin plugin)
  2. {
  3. ......
  4. for (Class<?> functionClass : plugin.getFunctions()) {
  5. log.info("Registering functions from %s", functionClass.getName());
  6. metadata.addFunctions(extractFunctions(functionClass));
  7. }
  8. ......
  9. }

所以用做拓展函数库的插件,需要实现getFunctions()方法,来返回拓展的函数集合,例:

  1. public class ExampleFunctionsPlugin
  2. implements Plugin
  3. {
  4. @Override
  5. public Set<Class<?>> getFunctions()
  6. {
  7. return ImmutableSet.<Class<?>>builder()
  8. .add(ExampleNullFunction.class)
  9. .add(IsNullFunction.class)
  10. .add(IsEqualOrNullFunction.class)
  11. .add(ExampleStringFunction.class)
  12. .add(ExampleAverageFunction.class)
  13. .build();
  14. }
  15. }

3. 多说几句

以上介绍的 Presto 函数开发方式可以满足日常大部分函数开发需求, Presto 函数的注册机制,新增和修改函数后,必须要重启服务才能生效,所以目前还不支持真正的用户自定义函数。

其他较为复杂的函数实现,比如变长参数函数的实现涉及调用过程中的函数签名匹配和类型参数绑定,需要用到codeGen进行实现,具体原理由于篇幅有限,在文中没有进行展开讲解,感兴趣的读者可以在评论区留言。

Presto 函数开发的更多相关文章

  1. myeclipse调用loadrunner函数开发测试脚本

    myeclipse调用loadrunner函数开发测试脚本 一.使用myeclipse开发性能测试脚本 1.使用Eclipse新建一个Java工程,将目录%LoadRunner_Home%\class ...

  2. Jmeter(三十二)Jmeter Question 之 “自定义函数开发”

    “技术是业务的支撑”,已经不是第一次听到这句话,因为有各种各样的需求,因此衍生了许多各种各样的技术.共勉! 前面有提到提到过Jmeter的安装目录结构,也提到Jmeter的常用函数功能,有部分工作使用 ...

  3. Excel自定义函数开发手记

    目录 本文使用的版本:Excel 2013 1.打开脚本编辑框 2.插入模块,编写代码 3.测试所写代码是否正确 4.给Excel单元插入自定义函数 5.给函数增加自定义说明 6.设置该自定义函数在E ...

  4. Hive的UDF(用户自定义函数)开发

    当 Hive 提供的内置函数无法满足你的业务处理需要时,此时就可以考虑使用用户自定义函数(UDF:user-defined function). 测试各种内置函数的快捷方法: 创建一个 dual 表 ...

  5. 关于db2中listagg函数开发中的体验

    一.首先解释一下可能会查询的基础问题: 1.1db2 “with ur”是什么意思: 在DB2中,共有四种隔离级:RS,RR,CS,UR.以下对四种隔离级进行一些描述,同时附上个人做试验的结果.隔离级 ...

  6. presto 函数中使用子查询

    我们已知 在sql中子查询可以配合  in 或者 exists 来使用,但是如何把子查询的结果传给函数呢? 场景: 我们有一个  省份表  数据如下: id   province 1    广东 2  ...

  7. Hive 内建操作符与函数开发——深入浅出学Hive

    第一部分:关系运算 Hive支持的关系运算符 •常见的关系运算符 •等值比较: = •不等值比较: <> •小于比较: < •小于等于比较: <= •大于比较: > •大 ...

  8. mysql presto 函数收集

    格式化日期 presto: select  date_format(CURRENT_DATE - INTERVAL '1' month, '%Y-%m') mysql:date_format(DATE ...

  9. 用js立即执行函数开发基于bootstrap-multiselect的联动参数菜单

    代码调用方式如下: data=[{F0:总分类cd,F1:总分类name,F2:大分类cd,F3:大分类name,F4:中分类cd,F5:中分类name,F6:小分类cd,F7:小分类name},.. ...

随机推荐

  1. LeetCode 80,不使用外部空间的情况下对有序数组去重

    本文始发于个人公众号:TechFlow,原创不易,求个关注 今天是LeetCode专题的第49篇文章,我们一起来看LeetCode的第80题,有序数组去重II(Remove Duplicates fr ...

  2. element ui 版本升级

    element ui 版本升级 1. 卸载之前版本 npm uninstall element-ui 2.重新安装element-ui npm i element-ui 3.就如package.jso ...

  3. 删库吧,Bug浪——我们在同一家摸鱼的公司

    那些口口声声, Bug越来越难写人的,应该盯着你们: 像我一样,我盯着你们,满眼恨意. IT积攒了几十年的漏洞, 所有的死机.溢出.404和超时, 像是专门为你们准备的礼物. 圈复杂度.魔鬼变量.内存 ...

  4. JVM垃圾回收概述

    垃圾回收概述 什么是垃圾 什么是垃圾( Garbage) 呢? 垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾. 如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内 ...

  5. No mapping found for HTTP request with URI [/***] in DispatcherServlet with name 'dispatcherServlet'

    相信不少Springboot初学者和我一样,都遇到上边这个提示,明明路径都是对的,但就是找不到对于的页面而404了,这也困扰我很长一段时间,我也是不得其解,百度上也鲜有合理回答,因为以前使用的时候,明 ...

  6. Bank Hacking题解

    题目: 题意: 有一颗树,你可以断开点(第一个随便断,以后只能是和已经断开的点相临的点),每个点有权值,断开之后,经一条边和两条边可以到达的节点权值加一,问到最后出现过的最大的权值. 分析: 为啥断开 ...

  7. 洛谷P2602 [ZJOI2010]数字计数 题解

    题目描述 输入格式 输出格式 输入输出样例 输入样例 1 99 输出样例 9 20 20 20 20 20 20 20 20 20 说明/提示 数据规模与约定 分析 很裸的一道数位DP的板子 定义f[ ...

  8. 使用@AutoConfigureBefore调整配置顺序竟没生效?

    一个人的价值体现在能够帮助多少人.自己编码好,价值能得到很好的体现.若你做出来的东西能够帮助别人开发,大大减少开发的时间,那就功德无量. 作者:A哥(YourBatman) 公众号:BAT的乌托邦(I ...

  9. 零拷贝(Zero-copy) 浅析及其应用

    相信大家都有过面经历,如果跟面试官聊到了操作系统,聊到了文件操作,可能会问你普通的文件读写流程,它有什么缺点,你知道有什么改进的措施.我们经常听说 零拷贝,每次可能只是背诵一些面试要点就过去了,今天我 ...

  10. 一篇文章教会你如何将DOM转换为virtual DOM

    [一.Virtual DOM简介] Virtual DOM是虚拟节点,它通过Javascript的Object对象模拟DOM中的节点,然后通过特定的render方法将其渲染成真实的DOM节点. 浏览器 ...