Flink SQL 内置了丰富的系统函数,但面对千奇百怪的业务需求,自定义函数(UDF)便成了释放开发能量的关键。从标量函数到聚合函数,再到表值聚合和异步查询,每一个 UDF 类型都有其独特的应用场景与开发规范。而 Module 机制则打通了 Hive 与传统数仓的壁垒,让函数复用变得前所未有的简单。本文将深入 Flink 1.16+ 的函数体系,从类型推导、状态管理到 Module 扩展,一次性讲透 Flink SQL 中关于函数的一切。
一、函数体系概览:四大象限与解析优先级
在 Flink SQL 的世界里,函数并非扁平的集合,而是按照两个维度被精心组织,形成了四个象限。
维度一:来源与命名空间
- 系统(内置)函数:没有命名空间,直接通过函数名引用,如
CONCAT、CURRENT_TIMESTAMP。 - Catalog 函数:归属于某个 Catalog 和 Database,拥有完整的命名空间,通过
catalog.db.func或db.func引用。
维度二:生命周期
- 临时函数:由用户创建(如
CREATE TEMPORARY FUNCTION),仅在当前会话生命周期内有效,会话结束即销毁。 - 持久化函数:存储在 Catalog 中,跨会话持久存在,可被不同作业共享。
这两个维度交叉,形成了 Flink SQL 中的四种函数类型:
- 临时性系统内置函数
- 系统内置函数(持久化)
- 临时性 Catalog 函数(例如
CREATE TEMPORARY FUNCTION注册的 UDF) - 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 的生命周期起始和结束时调用。
注意事项一:类型推导 ------ 别再让 Flink "猜"了
UDF 的输入参数和输出结果的类型对于 Flink 的序列化、优化至关重要。Flink 提供了三种方式让程序获取类型信息:
- 自动类型推导 :通过反射从函数类及其
eval方法签名自动提取。例如,public Long eval(long a, long b)会自动映射为BIGINT入参和BIGINT出参。这适用于绝大多数简单场景。 - 类型注解 :当自动推导不够用时,使用
@DataTypeHint和@FunctionHint显式声明。-
@DataTypeHint可注解在方法返回值或参数上,用于指定精度、小数位或复杂类型。例如:javapublic @DataTypeHint("DECIMAL(12, 3)") BigDecimal eval(double a, double b) -
@FunctionHint注解在类上,用于统一声明多个重载方法的输入输出类型,尤其适合使用Object通用入参的场景。它可以解耦类型声明和eval方法实现,让代码更整洁。
-
- 重写
getTypeInference():这是最灵活的方式,适合需要根据输入参数值动态决定输出类型的场景。实现此方法后,Flink 将完全禁用反射推导,交由开发者自定义类型推导逻辑。
最佳实践 :即使 Flink 可以自动推导,也建议显式对复杂类型(如
DECIMAL、ROW、RAW)加上注解,增强代码可读性和健壮性。
注意事项二:确定性 ------ 性能优化的关键
通过重写 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。 - 实现一个或多个
public的eval方法,方法签名直接决定入参与出参类型。
案例:通用哈希函数
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)配合JOIN或LEFT 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)将多行数据聚合成一个标量结果,类似于内置的 SUM、AVG,但可以嵌入任意复杂的累积逻辑。
开发要点:
- 继承
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 方式:累加器额外保存 oldFirst 和 oldSecond,通过比较新旧值进行增量撤回。
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则彻底移除。
5.3 如何让 Flink 支持 Hive UDF?
分为两种:Hive 内置 UDF 和用户自定义 Hive UDF。
支持 Hive 内置函数
- 引入依赖:
xml
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-hive_${scala.binary.version}</artifactId>
<version>${flink.version}</version>
</dependency>
- 在代码中加载 HiveModule:
java
tEnv.loadModule("hive", new HiveModule("3.1.2"));
- 之后,
get_json_object、rlike等 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 中的累加器是你的"记忆",
retract和merge是适应流式撤销与窗口合并的必备技能。 - 模块化思维:借助 Module 加载 Hive 函数,实现离线到实时的平滑迁移。