文章目录
Flink SQL Introduction
声明式 API,也是 Flink 最高层的 API,易于使用。
自动优化,屏蔽 State 的复杂性,自动做到最优处理。
流批统一,一样的 SQL,一样的结果。
应用广泛,ETL,统计分析,实时报表,实时风控。
Aggregation
Window Aggregation
三种不同的 Window Aggregation,TUBLE 滚动窗口、HOP 滑动窗口、SESSION 会话窗口。
其中的参数 t 字段叫做时间属性字段,只有基于时间属性才能去做一些window操作,这里的时间属性分为两种 系统时间 和 事件时间。
示例:统计每小时每个用户的点击次数。
原表为clicks,其中包含三个字段 user, cTime, url,图中的 SQL 使用窗口聚合,对表中不断产生的新数据进行流式计算,计算的结果会在每个小时结束时输出一次。
Group Aggregation
示例:统计从历史到现在每个用户点击的次数
Group Aggregation 是每来一条数据都会计算一次然后更新结果表,所以结果表一般会使用可以反复更新的结果表,推荐使用 MySQL 或者 HBase。
window 聚合与非 window 聚合的区别
自定义函数
自定义标量函数 UDF
定义:
用户定义的标量函数(UDF)将0个、1个或多个标量值映射到一个新的标量值。
实现:
UDF 需要在 ScalarFunction 类中实现 eval 方法。open 方法和 close 方法为可选方法。
注意:
UDF 默认对于相同的输入会有相同的输出,如果 UDF 不能保证相同的输出(例如在 UDF 中调用外部服务,相同的输入值可能返回不同的结果)建议实现override isDeterministic()方法,返回false。否则在某些条件下(例如 UDF 过滤上推)会使输出结果不符合预期。
需求:
自定义一个计算字符串长度的函数。
import org.apache.flink.table.functions.FunctionContext;
import org.apache.flink.table.functions.ScalarFunction;
public class StringLengthUdf extends ScalarFunction {
// 可选,open方法可以不编写。
// 如果编写open方法需要声明'import org.apache.flink.table.functions.FunctionContext;'。
@Override
public void open(FunctionContext context) {
}
public long eval(String a) {
return a == null ? 0 : a.length();
}
public long eval(String b, String c) {
return eval(b) + eval(c);
}
//可选,close方法可以不写。
@Override
public void close() {
}
}
将 jar 放入到 SQL Client 的 lib 下,再在 SQL Client 中创建函数。
## 创建UDF函数
CREATE FUNCTION stringLengthUdf AS 'com.xxx.xxx.StringLengthUdf ';
## 查看UDF函数
SHOW FUNCTION;
自定义聚合函数 UDAF
定义:
自定义聚合函数(UDAF)将多条记录聚合成1条记录。
实现:
实现 AggregateFunction 的核心接口方法。
建议使用Java,因为Scala的数据类型有时会造成不必要的性能损失。
- createAccumulator 和 getValue 方法
createAccumulator 和 getValue 可以定义在 AggregateFunction 抽象类内。
UDAF 必须包含1个 accumulate 方法。
/*
* @param <T> UDAF的输出结果的类型。
* @param <ACC> UDAF的accumulator的类型。accumulator是UDAF计算中用来存放计算中间结果的数据类型。您可以需要根据需要自行设计每个UDAF的accumulator。
*/
public abstract class AggregateFunction<T, ACC> extends UserDefinedFunction {
/*
* 初始化AggregateFunction的accumulator。
* 系统在第一个做aggregate计算之前调用一次这个方法。
*/
public ACC createAccumulator();
/*
* 系统在每次aggregate计算完成后调用这个方法。
*/
public T getValue(ACC accumulator);
}
- accumulator 方法
实现 accumulate 方法,来描述如何计算用户的输入的数据,并更新到 accumulator 中。
accumulate 方法的第一个参数必须是使用 AggregateFunction 的 ACC 类型的 accumulator。在系统运行过程中,底层 runtime 代码会把历史状态 accumulator,和指定的上游数据(支持任意数量,任意类型的数据)做为参数,一起发送给 accumulate 计算。
public void accumulate(ACC accumulator, ...[用户指定的输入参数]...);
createAccumulator、getValue 和 accumulate3个方法一起使用,就能设计出一个最基本的UDAF。但是实时计算一些特殊的场景需要您提供retract和merge两个方法才能完成。
- retract 方法
在实时计算的场景里,很多时候的计算都是对无限流的一个提前的观测值(early firing)。既然有 early firing,就会有对发出的结果的修改,这个操作叫做撤回(retract)。SQL 翻译优化器会帮助您自动判断哪些情况下会产生撤回的数据,哪些操作需要处理带有撤回标记的数据。需要实现一个 retract 方法来处理撤回的数据。
retract 方法是 accumulate 方法的逆操作。例如,count UDAF,在 accumulate 的时,每来一条数据要加1,在 retract 的时候就是要减1。
类似于 accumulate 方法,retract 方法的第1个参数同样必须使用 AggregateFunction 的 ACC 类型的 accumulator。
public void retract(ACC accumulator, ...[您指定的输入参数]...);
- merge 方法
在实时计算中一些场景需要 merge,例如,session window。 由于实时计算具有 out of order 的特性,后输入的数据有可能位于2个原本分开的 session 中间,这样就把2个 session 合为1个 session。此时,需要使用 merge 方法把多个 accumulator 合为1个 accumulator。
merge 方法的第1个参数,必须是使用 AggregateFunction 的 ACC 类型的accumulator,而且第1个 accumulator 是 merge 方法完成之后,状态所存放的地方。
merge 方法的第2个参数是1个 ACC type 的 accumulator 遍历迭代器,里面有可能存在1个或者多个 accumulator。
public void merge(ACC accumulator, Iterable<ACC> its);
需求:
自定义一个Count计数函数。
import org.apache.flink.table.functions.AggregateFunction;
public class CountUdaf extends AggregateFunction<Long, CountUdaf.CountAccum> {
//定义存放count UDAF状态的accumulator的数据的结构。
public static class CountAccum {
public long total;
}
//初始化count UDAF的accumulator。
public CountAccum createAccumulator() {
CountAccum acc = new CountAccum();
acc.total = 0;
return acc;
}
//getValue提供了,如何通过存放状态的accumulator,计算count UDAF的结果的方法。
public Long getValue(CountAccum accumulator) {
return accumulator.total;
}
//accumulate提供了,如何根据输入的数据,更新count UDAF存放状态的accumulator。
public void accumulate(CountAccum accumulator, Object iValue) {
accumulator.total++;
}
public void merge(CountAccum accumulator, Iterable<CountAccum> its) {
for (CountAccum other : its) {
accumulator.total += other.total;
}
}
}
将 jar 放入到 SQL Client 的 lib 下,再在 SQL Client 中创建函数。
## 创建UDF函数
CREATE FUNCTION countUdaf AS 'com.xxx.xxx.CountUdaf ';
## 查看UDF函数
SHOW FUNCTION;
自定义表值函数 UDTF
定义:
与自定义的标量函数类似,自定义的表值函数(UDTF)将0个、1个或多个标量值作为输入参数。与标量函数不同,表值函数可以返回任意数量的行作为输出,而不仅是1个值。返回的行可以由1个或多个列组成。
实现:
UDTF 需要在 TableFunction 类中实现 eval 方法。open 方法和 close 方法可选。
import org.apache.flink.table.functions.FunctionContext;
import org.apache.flink.table.functions.TableFunction;
public class SplitUdtf extends TableFunction<String> {
// 可选, open方法可不编写。若编写,需要添加声明'import org.apache.flink.table.functions.FunctionContext;'。
@Override
public void open(FunctionContext context) {
// ... ...
}
public void eval(String str) {
String[] split = str.split("\\|");
for (String s : split) {
collect(s);
}
}
// 可选,close方法可不编写。
@Override
public void close() {
// ... ...
}
}
UDTF 又有多行返回和多列返回两种不同的需求。
多行返回:UDTF 可以通过多次调用 collect() 实现将1行的数据转为多行返回。
多列返回:UDTF 不仅可以做到1行转多行,还可以1列转多列。如果需要 UDTF 返回多列,只需要将返回值声明成 Tuple 或 Row。
- 返回值为 Tuple
实时计算支持使用 Tuple1 到 Tuple25 ,分别定义1个字段到25个字段。使用 Tuple 时,字段值不能为 null,且最多只能存在25个字段。
示例: 用 Tuple3 将一个字段返回为3个字段。
import org.apache.flink.api.java.tuple.Tuple3;
import org.apache.flink.table.functions.TableFunction;
// 使用Tuple作为返回值,一定要显式声明Tuple的泛型类型, 如此例的String、Long和Integer。
public class ParseUdtf extends TableFunction<Tuple3<String, Long, Integer>> {
public void eval(String str) {
String[] split = str.split(",");
// 以下代码仅作示例,实际业务需要添加更多的校验逻辑。
String first = split[0];
long second = Long.parseLong(split[1]);
int third = Integer.parseInt(split[2]);
Tuple3<String, Long, Integer> tuple3 = Tuple3.of(first, second, third);
collect(tuple3);
}
}
- 返回值为 Row
用 Row 将一个字段返回为3个字段。使用 Row 时,返回的字段值可以是 null,但必须重载实现 getResultType 方法。
import org.apache.flink.table.types.DataType;
import org.apache.flink.table.types.DataTypes;
import org.apache.flink.table.functions.TableFunction;
import org.apache.flink.types.Row;
public class ParseUdtf extends TableFunction<Row> {
public void eval(String str) {
String[] split = str.split(",");
String first = split[0];
long second = Long.parseLong(split[1]);
int third = Integer.parseInt(split[2]);
Row row = new Row(3);
row.setField(0, first);
row.setField(1, second);
row.setField(2, third);
collect(row);
}
@Override
// 如果返回值是Row,则必须重载实现getResultType方法,显式地声明返回的字段类型。
public DataType getResultType(Object[] arguments, Class[] argTypes) {
return DataTypes.createRowType(DataTypes.STRING, DataTypes.LONG, DataTypes.INT);
}
}
注册与使用
将 jar 放入到 SQL Client 的 lib 下,再在 SQL Client 中创建函数。
UDTF 支持 cross join 和 left join,在使用 UDTF 时需要添加 lateral 和 table 关键字。以上述的 ParseUdtf (将一列解析为三列时)为例,需要先注册一个 function 名字。
CREATE FUNCTION parseUdtf AS 'com.xxx.xxx.ParseUdtf';
cross join:左表的每一行数据都会关联上 UDTF 产出的每一行数据,如果 UDTF 不产出任何数据,那么这1行不会输出。
select S.id, S.content, T.a, T.b, T.c
from input_stream as S,
lateral table(parseUdtf(content)) as T(a, b, c);
left join:左表的每一行数据都会关联上 UDTF 产出的每一行数据,如果 UDTF 不产出任何数据,那么这1行的 UDTF 的字段会用 null 值填充,且 left join UDTF 语句后面必须接 on true 参数。
select S.id, S.content, T.a, T.b, T.c
from input_stream as S
left join lateral table(parseUdtf(content)) as T(a, b, c) on true;
以上内容为自阿里云 Flink SQL 文档的学习总结,详细内容参考:https://help.aliyun.com/document_detail/69463.html?spm=a2c4g.11186623.6.667.6cf7579dD6sfZT