文章目录
用户自定义表聚合函数(UDTAGG)可以把一行或多行数据(也就是一个表)聚合成另一张表,结果表中可以有多行多列。很明显,这就像表函数和聚合函数的结合体,是一个“多对多”的转换。
自定义表聚合函数需要继承抽象类 TableAggregateFunction。TableAggregateFunction 的结构和原理与 AggregateFunction 非常类似,同样有两个泛型参数<T, ACC>,用一个 ACC 类型的累加器(accumulator)来存储聚合的中间结果。聚合函数中必须实现的三个方法,在TableAggregateFunction 中也必须对应实现:
1) createAccumulator()
创建累加器的方法,与 AggregateFunction 中用法相同。
2) accumulate()
聚合计算的核心方法,与 AggregateFunction 中用法相同。
3) emitValue()
所有输入行处理完成后,输出最终计算结果的方法。
这个方法对应着 AggregateFunction中的 getValue()方法;区别在于 emitValue 没有输出类型,而输入参数有两个:第一个是 ACC类型的累加器,第二个则是用于输出数据的“收集器”out,它的类型为 Collect。所以很明显,表聚合函数输出数据不是直接 return,而是调用 out.collect()方法,调用多次就可以输出多行数据了;这一点与表函数非常相似。
另外,emitValue()在抽象类中也没有定义,无法 override,必须手动实现。表聚合函数得到的是一张表;在流处理中做持续查询,应该每次都会把这个表重新计算输出。如果输入一条数据后,只是对结果表里一行或几行进行了更新(Update),这时我们重新计算整个表、全部输出显然就不够高效了。为了提高处理效率,TableAggregateFunction 还提供了一个 emitUpdateWithRetract()方法,它可以在结果表发生变化时,以“撤回”(retract)老数据、发送新数据的方式增量地进行更新。如果同时定义了 emitValue()和 emitUpdateWithRetract()
两个方法,在进行更新操作时会优先调用 emitUpdateWithRetract()。
表聚合函数相对比较复杂,它的一个典型应用场景就是 Top N 查询。比如我们希望选出一组数据排序后的前两名,这就是最简单的 TOP-2 查询。没有线程的系统函数,那么我们就可以自定义一个表聚合函数来实现这个功能。在累加器中应该能够保存当前最大的两个值,每当来一条新数据就在 accumulate()方法中进行比较更新,最终在 emitValue()中调用两次out.collect()将前两名数据输出。
目前 SQL 中没有直接使用表聚合函数的方式,所以需要使用 Table API 的方式来调用:
public class UdfTest_TableAggregateFunction {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
//1.在创建表的DDL中直接定义时间属性
String creatDDL = "CREATE TABLE clickTable (" +
"user_name STRING," +
"url STRING," +
"ts BIGINT," +
"et AS TO_TIMESTAMP( FROM_UNIXTIME(ts / 1000))," + //事件时间 FROM_UNIXTIME() 能转换为年月日时分秒这样的格式 转换秒
" WATERMARK FOR et AS et - INTERVAL '1' SECOND " + //watermark 延迟一秒
")WITH(" +
" 'connector' = 'filesystem'," +
" 'path' = 'input/clicks.txt'," +
" 'format' = 'csv'" +
")";
tableEnv.executeSql(creatDDL);
//2.注册自定义的表聚合函数
tableEnv.createTemporarySystemFunction("Top2", Top2.class);
//3.调用UDF进行查询转换
String windowAggQuery = "SELECT user_name,COUNT(url) AS cnt,window_start,window_end " +
"FROM TABLE(" +
" TUMBLE(TABLE clickTable,DESCRIPTOR(et),INTERVAL '10' SECOND)" +
")" +
"GROUP BY user_name,window_start,window_end";
Table aggTable = tableEnv.sqlQuery(windowAggQuery);
//FlinkSQL 对表聚合函数支持并不是很好,这里使用TableAPI方式
Table resultTable = aggTable.groupBy($("window_end"))
.flatAggregate(call("Top2", $("cnt")).as("value", "rank"))
.select($("window_end"), $("value"), $("rank"));
//4.转换成流打印输出
tableEnv.toChangelogStream(resultTable).print();
env.execute();
}
//单独定义一个累加器类型,包含了当前最大和第二大的数据
public static class Top2Accumulator {
public Long max;
public Long secondMax;
}
//实现自定义的表聚合函数
public static class Top2 extends TableAggregateFunction<Tuple2<Long, Integer>, Top2Accumulator> {
@Override
public Top2Accumulator createAccumulator() {
//创建累加器(初始化累加器)
Top2Accumulator top2Accumulator = new Top2Accumulator();
top2Accumulator.max = Long.MIN_VALUE;
top2Accumulator.secondMax = Long.MIN_VALUE;
return top2Accumulator;
}
//定义一个更新累加器的方法
public void accumulate(Top2Accumulator accumulate, Long value) {
if (value > accumulate.max) {
accumulate.secondMax = accumulate.max;
accumulate.max = value;
} else if (value > accumulate.secondMax) {
accumulate.secondMax = value;
}
}
//输出结果,获取当前的 Top2
public void emitValue(Top2Accumulator accumulator, Collector<Tuple2<Long, Integer>> out) {
if (accumulator.max != Long.MIN_VALUE) {
out.collect(Tuple2.of(accumulator.max, 1));
}
if (accumulator.secondMax != Long.MIN_VALUE) {
out.collect(Tuple2.of(accumulator.secondMax, 2));
}
}
}
}
这里使用了 flatAggregate()方法,它就是专门用来调用表聚合函数的接口。对 clickTable中数据按 window_end字段进行分组聚合,统计 value 值最大的两个;并将聚合结果的两个字段重命名为 value 和 rank,之后就可以使用 select()将它们提取出来了。