User Defined Function

    UDF 能满足的分析需求分为两种:UDF 和 UDAF。本文中的 UDF 指的是二者的统称。

    1. UDF: 用户自定义函数,这种函数会对单行进行操作,并且输出单行结果。当用户在查询时使用 UDF ,每行数据最终都会出现在结果集中。典型的 UDF 比如字符串操作 concat() 等。
    2. UDAF: 用户自定义的聚合函数,这种函数对多行进行操作,并且输出单行结果。当用户在查询时使用 UDAF,分组后的每组数据最后会计算出一个值并展结果集中。典型的 UDAF 比如集合操作 sum() 等。一般来说 UDAF 都会结合 group by 一起使用。

    这篇文档主要讲述了,如何编写自定义的 UDF 函数,以及如何在 Doris 中使用它。

    如果用户使用 UDF 功能并扩展了 Doris 的函数分析,并且希望将自己实现的 UDF 函数贡献回 Doris 社区给其他用户使用,这时候请看文档 Contribute UDF

    在使用UDF之前,用户需要先在 Doris 的 UDF 框架下,编写自己的UDF函数。在文件中是一个简单的 UDF Demo。

    编写一个 UDF 函数需要以下几个步骤。

    创建对应的头文件、CPP文件,在CPP文件中实现你需要的逻辑。CPP文件中的实现函数格式与UDF的对应关系。

    用户可以把自己的 source code 统一放在一个文件夹下。这里以 udf_sample 为例,目录结构如下:

    非可变参数

    对于非可变参数的UDF,那么两者之间的对应关系很直接。 比如INT MyADD(INT, INT)的UDF就会对应IntVal AddUdf(FunctionContext* context, const IntVal& arg1, const IntVal& arg2)

    1. AddUdf可以为任意的名字,只要创建UDF的时候指定即可。
    2. 实现函数中的第一个参数永远是FunctionContext*。实现者可以通过这个结构体获得一些查询相关的内容,以及申请一些需要使用的内存。具体使用的接口可以参考udf/udf.h中的定义。
    3. 实现函数中从第二个参数开始需要与UDF的参数一一对应,比如IntVal对应INT类型。这部分的类型都要使用const引用。
    4. 返回参数与UDF的参数的类型要相对应。

    可变参数

    对于可变参数,可以参见以下例子,UDFString md5sum(String, ...)对应的 实现函数是StringVal md5sumUdf(FunctionContext* ctx, int num_args, const StringVal* args)

    1. md5sumUdf这个也是可以任意改变的,创建的时候指定即可。
    2. 第一个参数与非可变参数函数一样,传入的是一个FunctionContext*
    3. 可变参数部分由两部分组成,首先会传入一个整数,说明后面还有几个参数。后面传入的是一个可变参数部分的数组。

    类型对应关系

    由于 UDF 实现中依赖了 Doris 的 UDF 框架 , 所以在编译 UDF 函数的时候首先要对 Doris 进行编译,也就是对 UDF 框架进行编译。

    在 Doris 根目录下执行 sh build.sh 就会在 output/udf/ 生成 UDF 框架的静态库文件 headers|libs

    1. ├── output
    2. └── udf
    3. ├── include
    4. ├── uda_test_harness.h
    5. └── udf.h
    6. └── lib
    7. └── libDorisUdf.a
    1. 准备编译 UDF 的 CMakeFiles.txt

      CMakeFiles.txt 用于声明 UDF 函数如何进行编译。存放在源码文件夹下,与用户代码平级。这里以 udf_samples 为例目录结构如下:

      • 需要显示声明引用 libDorisUdf.a
      • 声明 udf.h 头文件位置

      以 udf_sample 为例

      1. # Include udf
      2. include_directories(thirdparty/include)
      3. # Set all libraries
      4. add_library(udf STATIC IMPORTED)
      5. set_target_properties(udf PROPERTIES IMPORTED_LOCATION thirdparty/lib/libDorisUdf.a)
      6. # where to put generated libraries
      7. set(LIBRARY_OUTPUT_PATH "${BUILD_DIR}/src/udf_samples")
      8. # where to put generated binaries
      9. set(EXECUTABLE_OUTPUT_PATH "${BUILD_DIR}/src/udf_samples")
      10. add_library(udfsample SHARED udf_sample.cpp)
      11. target_link_libraries(udfsample
      12. udf
      13. -static-libstdc++
      14. -static-libgcc
      15. )
      16. target_link_libraries(udasample
      17. udf
      18. -static-libstdc++
      19. -static-libgcc
      20. )

    所有文件准备齐后完整的目录结构如下:

    1. ├── thirdparty
    2. │── include
    3. └── udf.h
    4. └── libDorisUdf.a
    5. └── udf_samples
    6. ├── CMakeLists.txt
    7. ├── uda_sample.cpp
    8. ├── uda_sample.h
    9. ├── udf_sample.cpp
    10. └── udf_sample.h

    准备好上述文件就可以直接编译 UDF 了

    在 udf_samples 文件夹下创建一个 build 文件夹,用于存放编译产出。

    在 build 文件夹下运行命令 cmake ../ 生成Makefile,并执行 make 就会生成对应动态库。

    编译完成后的 UDF 动态链接库就生成成功了。在 build/src/ 下,以 udf_samples 为例,目录结构如下:

    1. ├── thirdparty
    2. ├── udf_samples
    3. └── build
    4. └── src
    5. └── udf_samples
    6. ├── libudasample.so
    7. └── libudfsample.so

    通过上述的步骤后,你可以得到 UDF 的动态库(也就是编译结果中的 .so 文件)。你需要将这个动态库放到一个能够通过 HTTP 协议访问到的位置。

    然后登录 Doris 系统,在 mysql-client 中通过 CREATE FUNCTION 语法创建 UDF 函数。你需要拥有ADMIN权限才能够完成这个操作。这时 Doris 系统内部就会存在刚才创建好的 UDF。

    1. CREATE [AGGREGATE] FUNCTION
    2. name ([argtype][,...])
    3. [RETURNS] rettype
    4. PROPERTIES (["key"="value"][,...])

    说明:

    1. PROPERTIES中symbol表示的是,执行入口函数的对应symbol,这个参数是必须设定。你可以通过nm命令来获得对应的symbol,比如nm libudfsample.so | grep AddUdf获得到的_ZN9doris_udf6AddUdfEPNS_15FunctionContextERKNS_6IntValES4_就是对应的symbol。
    2. name: 一个function是要归属于某个DB的,name的形式为dbName.funcName。当dbName没有明确指定的时候,就是使用当前session所在的db作为dbName

    具体使用可以参见 CREATE FUNCTION 获取更详细信息。

    用户使用 UDF 必须拥有对应数据库的 SELECT 权限。

    当你不再需要 UDF 函数时,你可以通过下述命令来删除一个 UDF 函数, 可以参考 。