Flink 系列第21篇:Flink SQL 函数与 UDF 全解读:类型推导、开发要点与 Module 扩展

Flink SQL 内置了丰富的系统函数,但面对千奇百怪的业务需求,自定义函数(UDF)便成了释放开发能量的关键。从标量函数到聚合函数,再到表值聚合和异步查询,每一个 UDF 类型都有其独特的应用场景与开发规范。而 Module 机制则打通了 Hive 与传统数仓的壁垒,让函数复用变得前所未有的简单。本文将深入 Flink 1.16+ 的函数体系,从类型推导、状态管理到 Module 扩展,一次性讲透 Flink SQL 中关于函数的一切。


一、函数体系概览:四大象限与解析优先级

在 Flink SQL 的世界里,函数并非扁平的集合,而是按照两个维度被精心组织,形成了四个象限。

维度一:来源与命名空间

  • 系统(内置)函数:没有命名空间,直接通过函数名引用,如 CONCATCURRENT_TIMESTAMP
  • Catalog 函数:归属于某个 Catalog 和 Database,拥有完整的命名空间,通过 catalog.db.funcdb.func 引用。

维度二:生命周期

  • 临时函数:由用户创建(如 CREATE TEMPORARY FUNCTION),仅在当前会话生命周期内有效,会话结束即销毁。
  • 持久化函数:存储在 Catalog 中,跨会话持久存在,可被不同作业共享。

这两个维度交叉,形成了 Flink SQL 中的四种函数类型:

  1. 临时性系统内置函数
  2. 系统内置函数(持久化)
  3. 临时性 Catalog 函数(例如 CREATE TEMPORARY FUNCTION 注册的 UDF)
  4. Catalog 函数(例如 CREATE FUNCTION 注册的永久 UDF)

优先级从高到低依次是:临时性系统函数 > 系统内置函数 > 临时性 Catalog 函数 > Catalog 函数。当多个同名函数同时存在时,Flink 会按照此顺序解析,先匹配到的优先使用。

函数引用方式

Flink SQL 支持两种引用方式:

  • 精确引用 :用户明确指定 Catalog 和 Database 名称,例如 mydb.myfunc,精确锁定某个 UDF。
  • 模糊引用 :只指定函数名,如 myfunc,此时 Flink 会按照上述优先级顺序去搜索。

理解这套优先级和引用体系,是避免出现"我的 UDF 为什么没生效"或"为什么系统函数被覆盖了"等问题的第一步。


二、系统内置函数

Flink 自带的系统函数涵盖了字符串处理、数学计算、时间日期、类型转换、条件逻辑、聚合等常见场景。由于数量众多,建议直接查阅官方文档的 系统函数列表,按需检索。


三、自定义函数(UDF):让 SQL 长出触角

当内置函数无法满足复杂的业务逻辑时,自定义函数(UDF)便派上用场。它是一种扩展开发机制,允许你在 SQL 中调用用 Java/Scala/Python 编写的自定义代码,常用于频繁复用或难以用纯 SQL 表达的逻辑。

3.1 UDF 分类全景

Flink 目前提供以下几类 UDF:

类型 英文缩写 输入 → 输出 使用方式
标量函数 UDF 单行 → 单值 SELECT 子句中直接调用
表值函数 UDTF 单行 → 多行(表) 配合 LATERAL TABLE 使用
聚合函数 UDAF 多行 → 单值 GROUP BY 后使用
表值聚合函数 UDTAF 多行 → 多行 仅支持 Table API,SQL 不可用
异步表值函数 Async UDTF 异步查询外部系统,返回多行 用于 Lookup Join 的维表关联

注意 :表值聚合函数(UDTAF)目前只能通过 Table API 的 flatAggregate 使用,无法直接在 SQL 中调用。

3.2 开发 UDF 的通用步骤与核心注意事项

无论开发哪一类 UDF,都遵循一套相似的流程,但其中暗藏着几个决定成败的细节。

步骤一:继承正确的基类
  • 标量函数 → org.apache.flink.table.functions.ScalarFunction
  • 表值函数 → org.apache.flink.table.functions.TableFunction
  • 聚合函数 → org.apache.flink.table.functions.AggregateFunction
  • 表值聚合函数 → org.apache.flink.table.functions.TableAggregateFunction

类必须声明为 public,不能是抽象类、非静态内部类或匿名类,并且必须提供默认构造函数,以便 Flink 在运行时能通过反射进行实例化。

步骤二:实现核心逻辑方法

通常是重写 eval 方法(Scalar/Table/Async)或 accumulate/getValue(Aggregate)。这些方法必须为 public,输入输出类型要清晰。对于标量函数,还可以利用 open()close() 进行资源初始化和释放,它们分别在整个 UDF 的生命周期起始和结束时调用。

UDF 的输入参数和输出结果的类型对于 Flink 的序列化、优化至关重要。Flink 提供了三种方式让程序获取类型信息:

  1. 自动类型推导 :通过反射从函数类及其 eval 方法签名自动提取。例如,public Long eval(long a, long b) 会自动映射为 BIGINT 入参和 BIGINT 出参。这适用于绝大多数简单场景。
  2. 类型注解 :当自动推导不够用时,使用 @DataTypeHint@FunctionHint 显式声明。
    • @DataTypeHint 可注解在方法返回值或参数上,用于指定精度、小数位或复杂类型。例如:

      java 复制代码
      public @DataTypeHint("DECIMAL(12, 3)") BigDecimal eval(double a, double b)
    • @FunctionHint 注解在类上,用于统一声明多个重载方法的输入输出类型,尤其适合使用 Object 通用入参的场景。它可以解耦类型声明和 eval 方法实现,让代码更整洁。

  3. 重写 getTypeInference():这是最灵活的方式,适合需要根据输入参数值动态决定输出类型的场景。实现此方法后,Flink 将完全禁用反射推导,交由开发者自定义类型推导逻辑。

最佳实践 :即使 Flink 可以自动推导,也建议显式对复杂类型(如 DECIMALROWRAW)加上注解,增强代码可读性和健壮性。

注意事项二:确定性 ------ 性能优化的关键

通过重写 isDeterministic() 方法,你可以告诉 Flink 该函数在给定相同输入时,是否总是返回相同的结果。

  • 对于纯函数 (无输入的函数,或数学运算等),默认返回 true。若返回 true 且入参为常量,Flink 会在生成执行计划时直接执行该函数,将结果作为常量嵌入,运行时不再调用,从而大幅提升性能。
  • 对于非纯函数 (如 CURRENT_TIMESTAMP、随机函数、读取外部状态等),必须返回 false,确保每次调用都实时执行。
注意事项三:巧妙运用运行时上下文

通过重写 open(FunctionContext context) 方法,你可以在 UDF 初始化时获取丰富的运行时信息,极大扩展 UDF 的能力。

  • context.getMetricGroup():获取当前子任务的 Metric 组,方便自定义监控指标。
  • context.getCachedFile(name):获取通过分布式缓存上传的文件本地副本路径(如字典文件)。
  • context.getJobParameter(name, defaultValue)获取 Flink 作业的全局参数,这对于让 UDF 行为在不同环境(测试/生产)可配置非常实用。
  • context.getExternalResourceInfos(resourceName):获取外部资源信息。

一个典型应用:在 UDF 中从作业参数读取系数,避免硬编码。

java 复制代码
public class HashCodeFunction extends ScalarFunction {
    private int factor = 12;
    @Override
    public void open(FunctionContext context) {
        String param = context.getJobParameter("hashcode_factor", "12");
        this.factor = Integer.parseInt(param);
    }
    public int eval(String s) {
        return s == null ? 0 : s.hashCode() * factor;
    }
}

然后在 StreamPark 或 Flink 配置中设置 hashcode_factor=31,即可动态调控。


四、五种 UDF 深度实战

4.1 标量函数(Scalar Function)------ 一行进,一行出

标量函数是最基础、最常用的 UDF 类型,类似 SQL 中的内置函数,每处理一行数据,返回一个标量结果。

开发要点:

  • 继承 ScalarFunction
  • 实现一个或多个 publiceval 方法,方法签名直接决定入参与出参类型。

案例:通用哈希函数

java 复制代码
package com.example.udf;
import org.apache.flink.table.annotation.DataTypeHint;
import org.apache.flink.table.annotation.InputGroup;
import org.apache.flink.table.functions.ScalarFunction;

public class HashFunction extends ScalarFunction {
    public int eval(@DataTypeHint(inputGroup = InputGroup.ANY) Object o) {
        return o == null ? 0 : o.hashCode();
    }
}

在 SQL 中注册并使用:

sql 复制代码
CREATE TEMPORARY FUNCTION HashFunction AS 'com.example.udf.HashFunction';
SELECT myField, HashFunction(myField) AS hash_code FROM MyTable;

注意,通过 @DataTypeHint(inputGroup = InputGroup.ANY),该 UDF 可以接受任意类型的字段,灵活性很强。

4.2 表值函数(Table Function)------ 一行进,多行出

表值函数(UDTF)可以将一行数据拆解成多行,再与主表做横向连接,是处理数组、字符串拆分等场景的利器。

开发要点:

  • 继承 TableFunction<T>,泛型 T 表示输出的单行类型(通常为 Row)。
  • eval 方法没有返回值,而是调用 collect(T) 方法将生成的行收集起来。
  • 在 SQL 中必须通过 LATERAL TABLE(function) 配合 JOINLEFT JOIN 使用。

案例:字符串分词并统计长度

java 复制代码
package com.example.udtf;
import org.apache.flink.table.annotation.DataTypeHint;
import org.apache.flink.table.annotation.FunctionHint;
import org.apache.flink.table.functions.TableFunction;
import org.apache.flink.types.Row;

@FunctionHint(output = @DataTypeHint("ROW<word STRING, length INT>"))
public class SplitFunction extends TableFunction<Row> {
    public void eval(String str) {
        if (str != null) {
            for (String s : str.split(" ")) {
                collect(Row.of(s, s.length()));
            }
        }
    }
}

SQL 调用:

sql 复制代码
CREATE TEMPORARY FUNCTION SplitFunction AS 'com.example.udtf.SplitFunction';
SELECT myField, word, length
FROM MyTable
LEFT JOIN LATERAL TABLE(SplitFunction(myField)) AS T(word, length) ON TRUE;

使用 LEFT JOIN LATERAL 可以保证即使 UDTF 没有返回任何行,主表记录也不会丢失。

4.3 聚合函数(Aggregate Function)------ 多行进,一行出

聚合函数(UDAF)将多行数据聚合成一个标量结果,类似于内置的 SUMAVG,但可以嵌入任意复杂的累积逻辑。

开发要点:

  • 继承 AggregateFunction<T, ACC>T 为最终结果类型,ACC 为累加器类型。
  • 必须实现 createAccumulator() 创建累加器;accumulate(ACC, input...) 更新累加器;getValue(ACC) 返回最终结果。
  • 若用于回撤流(如普通 GROUP BY),必须实现 retract();若用于会话/滑动窗口,必须实现 merge()
  • 累加器类必须是 public static 内部类或顶级类,因为 Flink 需要序列化、实例化它。

案例:加权平均值

java 复制代码
public class WeightedAvg extends AggregateFunction<Long, WeightedAvg.WeightedAvgAccumulator> {

    public static class WeightedAvgAccumulator {
        public long sum = 0L;
        public int count = 0;
    }

    @Override
    public WeightedAvgAccumulator createAccumulator() { return new WeightedAvgAccumulator(); }

    public void accumulate(WeightedAvgAccumulator acc, Long value, Integer weight) {
        if (value != null && weight != null) {
            acc.sum += value * weight;
            acc.count += weight;
        }
    }

    public void retract(WeightedAvgAccumulator acc, Long value, Integer weight) {
        if (value != null && weight != null) {
            acc.sum -= value * weight;
            acc.count -= weight;
        }
    }

    public void merge(WeightedAvgAccumulator acc, Iterable<WeightedAvgAccumulator> iter) {
        for (WeightedAvgAccumulator a : iter) {
            acc.sum += a.sum;
            acc.count += a.count;
        }
    }

    @Override
    public Long getValue(WeightedAvgAccumulator acc) {
        return acc.count == 0 ? null : acc.sum / acc.count;
    }
}

SQL 使用:

sql 复制代码
CREATE TEMPORARY FUNCTION WeightedAvg AS 'com.example.udaf.WeightedAvg';
SELECT myField, WeightedAvg(`value`, weight) AS w_avg
FROM MyTable GROUP BY myField;

UDAF 的状态(累加器)会被 Flink 的 Checkpoint 机制持久化,保证精确一次语义。

4.4 表值聚合函数(Table Aggregate Function)------ 多行进,多行出

表值聚合函数(UDTAF)可以看作是 UDAF 和 UDTF 的结合体:输入多行,输出一个多行的表。例如,取某一组数据的 Top-K、去重后展开等。但目前在 SQL API 中无法直接使用,只能通过 Table API 的 flatAggregate 调用。

开发要点:

  • 继承 TableAggregateFunction<T, ACC>
  • 必须实现 accumulate(ACC, input...),以及 emitValue(ACC, Collector<T>)emitUpdateWithRetract(ACC, RetractableCollector<T>)
  • emitValue 每次输出全量结果;emitUpdateWithRetract 则撤回旧值、输出新值,适合增量更新,效率更高。二者若同时存在,系统优先使用 emitUpdateWithRetract

案例:Top2 的两种实现方式

累加器存储当前的前两大值。emitValue 方式:

java 复制代码
public class Top2 extends TableAggregateFunction<Tuple2<Integer, Integer>, Top2Accum> {
    public static class Top2Accum {
        public Integer first = Integer.MIN_VALUE;
        public Integer second = Integer.MIN_VALUE;
    }

    @Override
    public Top2Accum createAccumulator() { return new Top2Accum(); }

    public void accumulate(Top2Accum acc, Integer v) {
        if (v > acc.first) {
            acc.second = acc.first;
            acc.first = v;
        } else if (v > acc.second) {
            acc.second = v;
        }
    }

    public void emitValue(Top2Accum acc, Collector<Tuple2<Integer, Integer>> out) {
        if (acc.first != Integer.MIN_VALUE) out.collect(Tuple2.of(acc.first, 1));
        if (acc.second != Integer.MIN_VALUE) out.collect(Tuple2.of(acc.second, 2));
    }
}

emitUpdateWithRetract 方式:累加器额外保存 oldFirstoldSecond,通过比较新旧值进行增量撤回。

java 复制代码
public void emitUpdateWithRetract(Top2Accum acc, RetractableCollector<Tuple2<Integer, Integer>> out) {
    if (!acc.first.equals(acc.oldFirst)) {
        if (acc.oldFirst != Integer.MIN_VALUE) out.retract(Tuple2.of(acc.oldFirst, 1));
        out.collect(Tuple2.of(acc.first, 1));
        acc.oldFirst = acc.first;
    }
    // 对 second 类似处理...
}

Table API 调用示例:

java 复制代码
Table result = tab.groupBy($("key"))
    .flatAggregate(call("top2", $("a")).as("v", "rank"))
    .select($("key"), $("v"), $("rank"));

尽管 UDTAF 在 SQL 中暂不可用,但理解它的设计模式对于深入掌握 Flink 的状态与撤回机制非常有帮助。

4.5 异步表值函数(Async Table Function)------ 维表关联加速器

异步表值函数主要用于维表 Join 场景。传统的 Lookup Join 是同步的,每条数据到达时都需要去外部系统(如 MySQL、Redis)查询一次,等待返回后才能继续,吞吐量极低。异步表值函数允许同时向外发送多个请求,响应到达后再按顺序输出,极大提高了并发度和吞吐。

它的开发与 Table Function 类似,但继承自 AsyncTableFunction,核心方法是 eval(CompletableFuture<Collection<T>> future, Object... input)。在 SQL 中,通过 LOOKUP TABLE 的配置启用异步,并在 WITH 参数中指定异步函数。

由于异步细节较多,本文不做展开,但需要明确:异步表值函数是优化维表 Join 性能的利器。


五、Module:突破函数边界,融合 Hive 生态

在实际生产中,数仓链路往往同时存在实时(Flink)和离线(Hive)两套引擎。许多企业已经积累了大量 Hive UDF,如果要在 Flink 中重写一遍,不仅工作量大,且难以保证逻辑一致性。Module 机制正是为了解决这一痛点而生。

5.1 Module 是什么?

Module 是 Flink 中可插拔的函数扩展单元。它允许用户将一组函数(来自 Hive、自定义 jar 包等)加载到 TableEnvironment 中,使得这些函数可以像内置系统函数一样在 SQL 和 Table API 中使用。Flink 默认内置了 CoreModule(包含所有标准系统函数),用户还可以加载 HiveModule 或自定义 Module。

5.2 Module 的生命周期与调度规则

Module 可以被加载、卸载、启用和禁用。TableEnvironment 可以同时加载多个 Module,并按加载顺序(或通过 USE MODULES 指定的顺序)解析函数。当两个 Module 中存在同名函数时,顺序在前的优先。

  • 使用 LOAD MODULE 加载,默认即启用。
  • USE MODULES hive, core 可以调整解析顺序,将 Hive 放在 core 之前,这样 Hive 的函数会覆盖 core 中的同名函数。
  • USE MODULES hive 会禁用未列出的模块(如 core),但并未卸载。被禁用的 Module 仍保留在环境中,可通过 SHOW FULL MODULES 查看状态。
  • UNLOAD MODULE 则彻底移除。

分为两种:Hive 内置 UDF 和用户自定义 Hive UDF。

支持 Hive 内置函数
  1. 引入依赖:
xml 复制代码
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-connector-hive_${scala.binary.version}</artifactId>
    <version>${flink.version}</version>
</dependency>
  1. 在代码中加载 HiveModule:
java 复制代码
tEnv.loadModule("hive", new HiveModule("3.1.2"));
  1. 之后,get_json_objectrlike 等 Hive 函数就能直接在当前 Flink SQL 中使用了。
支持用户自定义 Hive UDF

同样基于 HiveModule,但需要额外将包含 Hive UDF 的 JAR 包添加到 classpath,并在 Hive 中注册为永久函数或临时函数。Flink 通过 Hive 的元数据可以识别并加载这些函数,前提是正确配置了 Hive Metastore 和资源。

5.4 Module 的使用演示

SQL CLI 方式:

sql 复制代码
-- 查看已启用的模块
SHOW MODULES;
-- 加载 Hive Module
LOAD MODULE hive WITH ('hive-version' = '3.1.2');
-- 调整顺序让 hive 优先解析
USE MODULES hive, core;
-- 禁用 core
USE MODULES hive;
-- 卸载 hive
UNLOAD MODULE hive;

Java API 方式:

java 复制代码
TableEnvironment tEnv = TableEnvironment.create(settings);
// 加载
tEnv.loadModule("hive", new HiveModule());
// 调整顺序
tEnv.useModules("hive", "core");
// 禁用
tEnv.useModules("hive");
// 卸载
tEnv.unloadModule("hive");

通过 Module,你可以无缝复用已有的 Hive 函数资产,真正实现一套 UDF 服务离线与实时双链路,极大降低开发维护成本。


六、总结

Flink SQL 的函数体系远比想象中立体和灵活。掌握它,关键在于理解以下几条"心法":

  • 优先级与命名空间:弄懂临时/系统/Catalog 函数的解析顺序,才能精准控制函数引用。
  • 类型推导三剑客 :自动推导、注解、getTypeInference(),善用注解让 UDF 既优雅又健壮。
  • 状态与撤回 :UDAF 和 UDTAF 中的累加器是你的"记忆",retractmerge 是适应流式撤销与窗口合并的必备技能。
  • 模块化思维:借助 Module 加载 Hive 函数,实现离线到实时的平滑迁移。
相关推荐
科研前沿1 小时前
2026 数字孪生前沿科技:全景迭代报告 —— 镜像视界生成式孪生(Generative DT)技术白皮书
大数据·人工智能·科技·算法·音视频·空间计算
ID_180079054731 小时前
Python 实现亚马逊商品详情 API 数据准确性校验(极简可用 + JSON 参考)
java·python·json
c++之路2 小时前
C++23概述
java·c++·c++23
Elastic 中国社区官方博客2 小时前
Elastic-caveman : 在不损失 Elastic 最佳效果的情况下,将 AI 响应 tokens 减少64%
大数据·运维·数据库·人工智能·elasticsearch·搜索引擎·全文检索
互联网推荐官2 小时前
上海软件定制开发全流程拆解:需求分析、技术选型与交付管理的工程实践
大数据·数据库·需求分析
samFuB2 小时前
【数据集】分省农林牧渔总产值、农业总产值数据(2007-2024年)
大数据
专注API从业者2 小时前
Open Claw 京东商品监控选品实战:一键抓取、实时监控、高效选品
java·服务器·数据库
摇滚侠3 小时前
DBeaver 导入数据库 导入 SQL 文件 MySQL 备份恢复
java·数据库·mysql
云天AI实战派3 小时前
AI 智能体问题排查指南:ChatGPT、API 调用到 Agent 上线失灵的全流程修复手册
大数据·人工智能·python·chatgpt·aigc