Flink SQL 之 Aggregation&UDF

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值