Flink Procedures 用 SQL 的 `CALL` 跑 Flink Job(实现、类型推断、命名参数、Catalog 集成一篇搞懂)

1. Procedures 是什么,适合做什么

Procedure 可以理解为:SQL 世界里的"存储过程",但执行体可以启动 Flink 作业

典型用途

  • 管理类:生成测试数据、重建/维护某些资源、触发后台作业
  • 数据操作类:一键跑一个数据准备/清洗/校验 Job,并把结果以表的形式返回
  • 平台化:把一堆"运维脚本/管理逻辑"收敛到 Catalog 中,让用户统一用 SQL CALL 调用

2. 实现规则:必须实现 Procedure 接口 + 定义 call(...)

2.1 类要求

  • 实现 org.apache.flink.table.procedures.Procedure
  • 类必须 public、非抽象、全局可访问
  • 不能是匿名类、非 static 内部类

2.2 call(...) 方法要求(最关键)

接口本身不定义方法,你需要自己定义名为 call 的方法:

硬性规则

  • call 必须 public

  • 第一个参数必须是 ProcedureContext

    • context.getExecutionEnvironment() 拿到 StreamExecutionEnvironment
  • 返回类型必须是数组int[]String[]Row[]Long[]

而且 JVM 普通重载规则都适用

  • 支持重载:call(ctx, int) / call(ctx, String)
  • 支持 varargs:call(ctx, Integer...)
  • 支持继承入参:call(ctx, Object)

如果你用 Scala

  • varargs 需要加 scala.annotation.varargs
  • 建议用装箱类型(java.lang.Integer 而不是 Int)以支持 NULL

3. 一个最小可用的 Procedure 示例:生成序列

下面这个示例展示了:Procedure 拿到 StreamExecutionEnvironment,用 fromSequence 跑一个小 Job,然后把结果收集为数组返回。

java 复制代码
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.procedure.ProcedureContext;
import org.apache.flink.table.procedures.Procedure;
import org.apache.flink.util.CloseableIterator;

public class GenerateSequenceProcedure implements Procedure {

  public long[] call(ProcedureContext context, int n) throws Exception {
    return generate(context.getExecutionEnvironment(), n);
  }

  public long[] call(ProcedureContext context, String n) throws Exception {
    return generate(context.getExecutionEnvironment(), Integer.parseInt(n));
  }

  private long[] generate(StreamExecutionEnvironment env, int n) throws Exception {
    long[] sequenceN = new long[n];
    int i = 0;
    try (CloseableIterator<Long> it = env.fromSequence(0, n - 1).executeAndCollect()) {
      while (it.hasNext()) {
        sequenceN[i++] = it.next();
      }
    }
    return sequenceN;
  }
}

要点

  • call 可以重载
  • context.getExecutionEnvironment() 获取执行环境
  • 结果必须是数组

4. 类型推断 Type Inference:为什么你有时必须加注解

Flink Table/SQL 是强类型生态,因此 Procedure 的参数/返回值需要映射成 Flink DataType。

Flink 会通过反射自动推断(Automatic Type Inference),但在以下情况经常需要你"补注解"

  • 小数精度/scale(DECIMAL)
  • 嵌套 Row 类型(ROW<...>)
  • RAW/自定义序列化对象
  • 一个 call 想吃多种输入类型、但希望统一输出类型(用 @ProcedureHint

4.1 @DataTypeHint:给参数或返回值补充类型信息

注意:call 的返回值必须是 T[],如果你给返回值加 @DataTypeHint,其实标注的是 数组元素类型 T

java 复制代码
import org.apache.flink.table.annotation.DataTypeHint;
import org.apache.flink.table.annotation.InputGroup;
import org.apache.flink.table.procedure.ProcedureContext;
import org.apache.flink.table.procedures.Procedure;
import org.apache.flink.types.Row;

import java.math.BigDecimal;
import java.nio.ByteBuffer;
import java.time.Instant;

public class OverloadedProcedure implements Procedure {

  public Long[] call(ProcedureContext context, long a, long b) {
    return new Long[] {a + b};
  }

  public @DataTypeHint("DECIMAL(12, 3)") BigDecimal[] call(ProcedureContext context, double a, double b) {
    return new BigDecimal[] {BigDecimal.valueOf(a + b)};
  }

  @DataTypeHint("ROW<s STRING, t TIMESTAMP_LTZ(3)>")
  public Row[] call(ProcedureContext context, int i) {
    return new Row[] {Row.of(String.valueOf(i), Instant.ofEpochSecond(i))};
  }

  @DataTypeHint(value = "RAW", bridgedTo = ByteBuffer.class)
  public ByteBuffer[] call(
      ProcedureContext context,
      @DataTypeHint(inputGroup = InputGroup.ANY) Object o) {
    return new ByteBuffer[] {MyUtils.serializeToByteBuffer(o)};
  }
}

4.2 @ProcedureHint:把"输入类型 -> 输出类型"的映射说清楚

适合场景

  • 一个 call 想统一处理多输入类型(例如 Object...
  • 多个重载 call 的输出类型一致,想全局声明一次
java 复制代码
import org.apache.flink.table.annotation.DataTypeHint;
import org.apache.flink.table.annotation.ProcedureHint;
import org.apache.flink.table.procedure.ProcedureContext;
import org.apache.flink.table.procedures.Procedure;
import org.apache.flink.types.Row;

@ProcedureHint(output = @DataTypeHint("ROW<s STRING, i INT>"))
public class SumProcedure implements Procedure {

  public Row[] call(ProcedureContext context, int a, int b) {
    return new Row[] {Row.of("Sum", a + b)};
  }

  public Row[] call(ProcedureContext context) {
    return new Row[] {Row.of("Empty args", -1)};
  }
}

更极端的用法:完全用 hint 决定类型,call 只要 JVM 能调用即可。

5. 命名参数 Named Parameters:让 CALL 更可读,也能省参数

调用 procedure 时可以用"命名参数",好处

  • 不怕参数顺序写错
  • 可选参数可省略(默认补 null
  • 可读性强(适合平台化)

通过 @ArgumentHint 标注参数名、类型、是否可选。

参数级别标注示例

java 复制代码
import org.apache.flink.table.annotation.ArgumentHint;
import org.apache.flink.table.annotation.DataTypeHint;
import org.apache.flink.table.procedure.ProcedureContext;
import org.apache.flink.table.procedures.Procedure;

public class NamedParameterProcedure implements Procedure {

  public @DataTypeHint("INT") Integer[] call(
      ProcedureContext context,
      @ArgumentHint(name = "a", isOptional = true) Integer a,
      @ArgumentHint(name = "b") Integer b) {
    return new Integer[] {a + (b == null ? 0 : b)};
  }
}

重要限制

  • 命名参数仅在没有重载、没有可变参数(varargs)时生效
  • @ArgumentHint 已包含 @DataTypeHint,在某些组合场景下不能混用(按文档要求)

6. 把 Procedure 放进 Catalog:getProcedure + listProcedures

Procedure 必须存在于 Catalog 才能被 CALL

你需要在 Catalog 中实现:

  • Catalog.getProcedure(ObjectPath procedurePath):返回 procedure 实例
  • Catalog.listProcedures(String dbName):列出该库下有哪些 procedure

示例(内存 catalog 内置 procedure)

java 复制代码
import org.apache.flink.table.catalog.GenericInMemoryCatalog;
import org.apache.flink.table.catalog.ObjectPath;
import org.apache.flink.table.catalog.exceptions.CatalogException;
import org.apache.flink.table.catalog.exceptions.DatabaseNotExistException;
import org.apache.flink.table.catalog.exceptions.ProcedureNotExistException;
import org.apache.flink.table.procedures.Procedure;

import java.util.*;
import java.util.stream.Collectors;

public class CatalogWithBuiltInProcedure extends GenericInMemoryCatalog {

  private static final Map<ObjectPath, Procedure> PROCEDURE_MAP = new HashMap<>();

  static {
    PROCEDURE_MAP.put(ObjectPath.fromString("system.generate_n"), new GenerateSequenceProcedure());
  }

  public CatalogWithBuiltInProcedure(String name) {
    super(name);
  }

  @Override
  public List<String> listProcedures(String dbName)
      throws DatabaseNotExistException, CatalogException {
    if (!databaseExists(dbName)) {
      throw new DatabaseNotExistException(getName(), dbName);
    }
    return PROCEDURE_MAP.keySet().stream()
        .filter(p -> p.getDatabaseName().equals(dbName))
        .map(ObjectPath::getObjectName)
        .collect(Collectors.toList());
  }

  @Override
  public Procedure getProcedure(ObjectPath procedurePath)
      throws ProcedureNotExistException, CatalogException {
    Procedure p = PROCEDURE_MAP.get(procedurePath);
    if (p != null) {
      return p;
    }
    throw new ProcedureNotExistException(getName(), procedurePath);
  }
}

7. SQL 调用:CALL catalog.db.proc(args...)

注册 Catalog 后就能调用:

java 复制代码
TableEnvironment tEnv = TableEnvironment.create(...);

tEnv.registerCatalog("my_catalog", new CatalogWithBuiltInProcedure("my_catalog"));

// 调用
tEnv.executeSql("CALL my_catalog.`system`.generate_n(5)");

SQL 侧一般就是

  • CALL my_catalog.\system.generate_n(5)
  • 或者用命名参数(如果你的 procedure 支持且没有重载/varargs)

8. 实战建议:什么时候用 Procedure,什么时候别用

推荐用 Procedure

  • 平台里做"管理命令":一键生成数据、触发离线/流式任务、数据质量检查
  • 把复杂逻辑隐藏在 Procedure 里,让用户只写 CALL ...

不太推荐(或要谨慎)

  • 你只是想做查询内的行级/集合级变换:那是 UDF/PTF 的领域
  • Procedure 内部启动长周期作业时,要考虑资源、权限、隔离和可观测性(日志/指标/审计)
相关推荐
Psycho_MrZhang2 小时前
业务应用系统类型和常用名词
大数据
武子康2 小时前
大数据-195 KNN/K近邻算法实战:欧氏距离+投票机制手写实现,含可视化与调参要点
大数据·后端·机器学习
毕小宝2 小时前
Elasticsearch 条件字段为 date 类型时注意事项
大数据·elasticsearch·搜索引擎
刘冲溟2 小时前
解决 idea 编辑sql文件换行后自动缩进的问题
sql·idea·缩进
是阿威啊2 小时前
企业级的RDD、 Spark SQL、DataFrame、Dataset使用场景介绍
大数据·sql·spark
Pocker_Spades_A2 小时前
AI Ping 上线 GLM-4.7 与 MiniMax M2.1:两款国产旗舰模型免费用!
大数据·数据库·人工智能
IvanCodes3 小时前
openGauss 高级特性:优化器、存储引擎与分区管理
数据库·sql·opengauss
简道云平台3 小时前
工程BOM、制造BOM、成本BOM有什么区别?三套 BOM 各自解决什么问题?
大数据·制造·bom
Sui_Network3 小时前
回顾 2025,Sui 技术栈的落地之年
大数据·人工智能·web3·去中心化·区块链