如果用户需要同时流计算、批处理的场景下,用户需要维护两套业务代码,开发人员也要维护两套技术栈,非常不方便。
Flink社区很早就设想过将批数据看作一个有界流数据,将批处理看作流计算的一个特例,从而实现流批统一,Flink社区的开发人员在多轮讨论后,基本敲定了Flink未来的技术架构。
Apache Flink 有两种关系型 API 来做流批统一处理:Table API 和 SQL。
Table API 是用于 Scala 和 Java 语言的查询API,它可以用一种非常直观的方式来组合使用选取、过滤、join 等关系型算子。
Flink SQL 是基于 Apache Calcite 来实现的标准 SQL。这两种 API 中的查询对于批(DataSet)和流(DataStream)的输入有相同的语义,也会产生同样的计算结果。
Table API 和 SQL 两种 API 是紧密集成的,以及 DataStream 和 DataSet API。你可以在这些 API 之间,以及一些基于这些 API 的库之间轻松的切换。比如,你可以先用 CEP 从 DataStream 中做模式匹配,然后用 Table API 来分析匹配的结果;或者你可以用 SQL 来扫描、过滤、聚合一个批式的表,然后再跑一个 Gelly 图算法 来处理已经预处理好的数据。
注意:Table API 和 SQL 现在还处于活跃开发阶段,还没有完全实现所有的特性。不是所有的 [Table API,SQL] 和 [流,批] 的组合都是支持的(1.12之前)。
1 核心概念
Flink 的 Table API 和 SQL 是流批统一的 API。这意味着TableAPI & SQL在无论有限的批式输入还是无限的流式输入下,都具有相同的语义。因为传统的关系代数以及 SQL 最开始都是为了批式处理而设计的,关系型查询在流式场景下不如在批式场景下容易理解。
1.1 动态表和连续查询
动态表(Dynamic Tables)是Flink的支持流数据的Table API和SQL的核心概念。
与表示批处理数据的静态表不同,动态表是随时间变化的。可以像查询静态批处理表一样查询它们。查询动态表将生成一个连续查询(Continuous Query)。一个连续查询永远不会终止,结果会生成一个动态表。查询不断更新其(动态)结果表,以反映其(动态)输入表上的更改。
需要注意的是,连续查询的结果在语义上总是等价于以批处理模式在输入表快照上执行的相同查询的结果。
- 将流转换为动态表。
- 在动态表上计算一个连续查询,生成一个新的动态表。
- 生成的动态表被转换回流。
1.2 在流上定义表
为了使用关系查询处理流,必须将其转换成Table
。从概念上讲,流的每条记录都被解释为对结果表的 INSERT
操作。
// 初始数据格式
[
user: VARCHAR, // 用户名
cTime: TIMESTAMP, // 访问 URL 的时间
url: VARCHAR // 用户访问的 URL
]
下图显示了单击事件流(左侧)如何转换为表(右侧)。当插入更多的单击流记录时,结果表的数据将不断增长。
1.2.1 连续查询
在动态表上计算一个连续查询,并生成一个新的动态表。与批处理查询不同,连续查询从不终止,并根据其输入表上的更新更新其结果表。
在任何时候,连续查询的结果在语义上与以批处理模式在输入表快照上执行的相同查询的结果相同。
- 当查询开始,
clicks
表(左侧)是空的。 - 当第一行数据被插入到
clicks
表时,查询开始计算结果表。第一行数据[Mary,./home]
插入后,结果表(右侧,上部)由一行[Mary, 1]
组成。 - 当第二行
[Bob, ./cart]
插入到clicks
表时,查询会更新结果表并插入了一行新数据[Bob, 1]
。 - 第三行
[Mary, ./prod?id=1]
将产生已计算的结果行的更新,[Mary, 1]
更新成[Mary, 2]
。 - 最后,当第四行数据加入
clicks
表时,查询将第三行[Liz, 1]
插入到结果表中。
2 Flink Table API
2.1 依赖
<!--老版官方提供的依赖没有融合blink的-->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-table-planner_${scala.binary.version}</artifactId>
<version>${flink.version}</version>
</dependency>
<!--blink二次开发之后的依赖-->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-table-planner-blink_${scala.binary.version}</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-csv</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-json</artifactId>
<version>${flink.version}</version>
</dependency>
2.2 基本使用:表与DataStream的混合使用
package com.atguigu.flink.tableAPI;
import com.atguigu.flink.source.WaterSensor;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.types.Row;
import static org.apache.flink.table.api.Expressions.$;
public class Test01_BasicUse {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
DataStreamSource<WaterSensor> waterSensorStream = env.fromElements(new WaterSensor("sensor_1", 1000L, 10),
new WaterSensor("sensor_1", 2000L, 20),
new WaterSensor("sensor_2", 3000L, 30),
new WaterSensor("sensor_1", 4000L, 40),
new WaterSensor("sensor_1", 5000L, 50),
new WaterSensor("sensor_2", 6000L, 60));
// 1 创建表的执行环境
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
// 2 创建表 将流转换成动态表,表的字段名从pojo的属性名自动抽取
Table table = tableEnv.fromDataStream(waterSensorStream);
// 3 对动态表进行查询
Table resultTable = table
.where($("id").isEqual("sensor_1"))
.select($("id"), $("ts"), $("vc"));
// 4 把动态表转换成流
DataStream<Row> resultStream = tableEnv.toAppendStream(resultTable, Row.class);
resultStream.print();
env.execute();
}
}
+I[sensor_1, 1000, 10]
+I[sensor_1, 2000, 20]
+I[sensor_1, 4000, 40]
+I[sensor_1, 5000, 50]
2.3 基本使用:聚集操作
package com.atguigu.flink.tableAPI;
import com.atguigu.flink.source.WaterSensor;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.types.Row;
import static org.apache.flink.table.api.Expressions.$;
public class Test02_Agg {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
DataStreamSource<WaterSensor> waterSensorStream =
env.fromElements(new WaterSensor("sensor_1", 1000L, 10),
new WaterSensor("sensor_1", 2000L, 20),
new WaterSensor("sensor_2", 3000L, 30),
new WaterSensor("sensor_1", 4000L, 40),
new WaterSensor("sensor_1", 5000L, 50),
new WaterSensor("sensor_2", 6000L, 60));
// 1 创建表的执行环境
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
// 2 创建表,将流转换为动态表,表的字段名从pojo的属性名自动转换
Table table = tableEnv.fromDataStream(waterSensorStream);
// 3 对动态表进行查询
Table resultTable = table
.where($("vc").isGreaterOrEqual(20))
.groupBy($("id"))
.aggregate($("vc").sum().as("vc_sum"))
.select($("id"), $("vc_sum"));
// 4 把动态转化为流,如果涉及到数据的更新和改变,要用到撤回流,多了一个boolean标记
DataStream<Tuple2<Boolean, Row>> resultStream = tableEnv.toRetractStream(resultTable, Row.class);
resultStream.print();
env.execute();
}
}
(true,+I[sensor_1, 20])
(true,+I[sensor_2, 30])
(false,-U[sensor_1, 20])
(true,+U[sensor_1, 60])
(false,-U[sensor_1, 60])
(true,+U[sensor_1, 110])
(false,-U[sensor_2, 30])
(true,+U[sensor_2, 90])
2.4 表到流的转换
动态表可以像普通数据库表一样通过INSERT、UPDATE和DELETE来不断修改。它可能是一个只有一行、不断更新的表,也可能是一个insert-only的表,没有UPDATE和 DELETE修改,或者介于两者之间的其他表。
在将动态表转换为流或将其写入外部系统时,需要对这些更改进行编码。Flink的 Table API和SQL支持三种方式来编码一个动态表的变化:
2.4.1 Append-only流(只追加流)
仅通过 INSERT
操作修改的动态表可以通过输出插入的行转换为流。
2.4.2 Retract流(撤回流)
retract流包含两种类型的 message:add messages 和 retract messages 。
将INSERT
操作编码为 add message,将DELETE
操作编码为 retract message
将UPDATE
操作编码为更新(先前)行的 retract message 和更新(新)行的 add message
通过以上两种编码的变换操作,将动态表转换为 retract 流。下图显示了将动态表转换为 retract 流的过程。
2.4.3 Upsert流
upsert 流包含两种类型的 message: upsert messages 和delete messages。转换为 upsert 流的动态表需要(可能是组合的)唯一键。
通过将 INSERT
和 UPDATE
操作编码为 upsert message,将 DELETE
操作编码为 delete message ,将具有唯一键的动态表转换为流。消费流的算子需要知道唯一键的属性,以便正确地应用 message。与 retract 流的主要区别在于 UPDATE
操作是用单个 message 编码的,因此效率更高。下图显示了将动态表转换为 upsert 流的过程。
2.5 通过Connector声明读入数据
前面是先得到流,再转换成动态表,其实动态表也可以直接连接到数据。
2.5.1 File source
sensor-sql.txt
sensor_1,1000,10
sensor_1,2000,20
sensor_2,3000,30
sensor_1,4000,40
sensor_1,5000,50
sensor_2,6000,60
package com.atguigu.flink.tableAPI;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.DataTypes;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.table.descriptors.Csv;
import org.apache.flink.table.descriptors.FileSystem;
import org.apache.flink.table.descriptors.Schema;
import org.apache.flink.types.Row;
import static org.apache.flink.table.api.Expressions.$;
public class Test03_FileSource {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
// 2 创建表
// 2.1 表的元数据信息
Schema schema = new Schema()
.field("id", DataTypes.STRING())
.field("ts", DataTypes.BIGINT())
.field("vc", DataTypes.INT());
// 2.2 连接文件,并创建一个临时表,其实就是一个动态表
tableEnv.connect(new FileSystem().path("input/sensor-sql_1.txt"))
.withFormat(new Csv().fieldDelimiter(',').lineDelimiter("\n"))
.withSchema(schema)
.createTemporaryTable("sensor");
// 3 做成表对象,然后对动态表进行查询
Table sensorTable = tableEnv.from("sensor");
Table resultTable = sensorTable
.groupBy($("id"))
.select($("id"), $("id").count().as("cnt"));
// 4 把动态表转化为流,如果涉及到数据的更新,要用到撤回流
DataStream<Tuple2<Boolean, Row>> resultStream = tableEnv.toRetractStream(resultTable, Row.class);
resultStream.print();
env.execute();
}
}
通过connect直接将外部系统抽象成动态表,作为数据源:
- 调用connect方法,传入一个外部系统的描述器,还有一些参数
- 调用withFormat方法,指定数据的存储格式:列分隔符、行分隔符等等
- 调用withSchema方法,指定表的结构信息:列名、列的类型
- 调用createTemporaryTable方法,创建一张临时表,并且指定表名
2.5.2 Kafka Source
package com.atguigu.flink.tableAPI;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.DataTypes;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.table.descriptors.*;
import org.apache.flink.types.Row;
import static org.apache.flink.table.api.Expressions.$;
public class Test04_KafkaSource {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
// 2 创建表
// 2.1 表的元数据信息
Schema schema = new Schema()
.field("id", DataTypes.STRING())
.field("ts", DataTypes.BIGINT())
.field("vc", DataTypes.INT());
// 2.2 连接Kafka,并创建一个临时表,其实是一个动态表
tableEnv
.connect(new Kafka()
.version("universal") //kafka通用版本
.topic("sensor")
.startFromLatest()
.property("group.id", "bigdata")
.property("bootstrap.servers", "hadoop102:9092,hadoop103:9092,hadoop104:9092"))
.withFormat(new Json())
.withSchema(schema)
.createTemporaryTable("sensor");
// 3 做成表对象,然后对动态表进行查询
Table sensorTable = tableEnv.from("sensor");
Table resultTable = sensorTable
.groupBy($("id"))
.select($("id"), $("id").count().as("cnt"));
// 4 把动态表转化为流,如果涉及到数据的更新,要用到撤回流
DataStream<Tuple2<Boolean, Row>> resultStream = tableEnv.toRetractStream(resultTable, Row.class);
resultStream.print();
env.execute();
}
}
kafka-console-producer.sh --broker-list hadoop102:9092 --topic sensor
{"id":"sensor_1","ts":"1","vc":"1"}
{"id":"sensor_1","ts":"2","vc":"2"}
2.6 通过Connector声明写出数据
2.6.1 File Sink
package com.atguigu.flink.tableAPI;
import com.atguigu.flink.source.WaterSensor;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.DataTypes;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.table.descriptors.*;
import org.apache.flink.types.Row;
import static org.apache.flink.table.api.Expressions.$;
public class Test05_ToFileSystem {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
DataStreamSource<WaterSensor> waterSensorStream =
env.fromElements(new WaterSensor("sensor_1", 1000L, 10),
new WaterSensor("sensor_1", 2000L, 20),
new WaterSensor("sensor_2", 3000L, 30),
new WaterSensor("sensor_1", 4000L, 40),
new WaterSensor("sensor_1", 5000L, 50),
new WaterSensor("sensor_2", 6000L, 60));
// 2.2
Table sensorTable = tableEnv.fromDataStream(waterSensorStream);
Table resultTable = sensorTable
.where($("id").isEqual("sensor_1"))
.select($("id"), $("ts"), $("vc"));
// 2.1 输出表的元数据信息
Schema schema = new Schema()
.field("id", DataTypes.STRING())
.field("ts", DataTypes.BIGINT())
.field("vc", DataTypes.INT());
tableEnv
.connect(new FileSystem().path("output/sensor_id.txt"))
.withFormat(new Csv().fieldDelimiter('|'))
.withSchema(schema)
.createTemporaryTable("sensor");
resultTable.executeInsert("sensor");
}
}
env.execute()方法会去分析代码,生成一些 graph,但是我们代码中没有调用算子,所以会报错,可以直接不用。
2.6.2 Kafka Sink
package com.atguigu.flink.tableAPI;
import com.atguigu.flink.source.WaterSensor;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.DataTypes;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.table.descriptors.*;
import static org.apache.flink.table.api.Expressions.$;
public class Test05_ToKafkaSink {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
DataStreamSource<WaterSensor> waterSensorStream =
env.fromElements(new WaterSensor("sensor_1", 1000L, 10),
new WaterSensor("sensor_1", 2000L, 20),
new WaterSensor("sensor_2", 3000L, 30),
new WaterSensor("sensor_1", 4000L, 40),
new WaterSensor("sensor_1", 5000L, 50),
new WaterSensor("sensor_2", 6000L, 60));
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
Table sensorTable = tableEnv.fromDataStream(waterSensorStream);
Table resultTable = sensorTable
.where($("id").isEqual("sensor_1"))
.select($("id"), $("ts"), $("vc"));
Schema schema = new Schema()
.field("id", DataTypes.STRING())
.field("ts", DataTypes.BIGINT())
.field("vc", DataTypes.INT());
tableEnv
.connect(new Kafka()
.version("universal")
.topic("sensor")
.sinkPartitionerRoundRobin()
.property("bootstrap.servers", "hadoop102:9092,hadoop103:9092,hadoop104:9092"))
.withFormat(new Json())
.withSchema(schema)
.createTemporaryTable("sensor");
resultTable.executeInsert("sensor");
}
}
kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic sensor
2.7 其他Connector用法
参考官方文档: https://ci.apache.org/projects/flink/flink-docs-release-1.12/zh/dev/table/connect.html
3 Flink SQL
3.1 基本使用
3.1.1 查询未注册的表
package com.atguigu.flink.SQL;
import com.atguigu.flink.source.WaterSensor;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.types.Row;
public class Test_01_BaseUse {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
DataStreamSource<WaterSensor> waterSensorStream =
env.fromElements(new WaterSensor("sensor_1", 1000L, 10),
new WaterSensor("sensor_1", 2000L, 20),
new WaterSensor("sensor_2", 3000L, 30),
new WaterSensor("sensor_1", 4000L, 40),
new WaterSensor("sensor_1", 5000L, 50),
new WaterSensor("sensor_2", 6000L, 60));
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
// 使用SQL查询未注册的表
Table inputTable = tableEnv.fromDataStream(waterSensorStream);
Table resultTable = tableEnv.sqlQuery("select * from " + inputTable + " where id='sensor_1'");
tableEnv.toAppendStream(resultTable, Row.class).print();
env.execute();
}
}
3.1.2 查询已注册的表
package com.atguigu.flink.SQL;
import com.atguigu.flink.source.WaterSensor;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.types.Row;
public class Test_02_BaseUse_2 {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
DataStreamSource<WaterSensor> waterSensorStream =
env.fromElements(new WaterSensor("sensor_1", 1000L, 10),
new WaterSensor("sensor_1", 2000L, 20),
new WaterSensor("sensor_2", 3000L, 30),
new WaterSensor("sensor_1", 4000L, 40),
new WaterSensor("sensor_1", 5000L, 50),
new WaterSensor("sensor_2", 6000L, 60));
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
// 使用SQL查询未注册的表
Table inputTable = tableEnv.fromDataStream(waterSensorStream);
/* // 方法一
// 把表注册为一个临时视图
tableEnv.createTemporaryView("sensor",inputTable);
// 在临时表中匹配数据
Table resultTable = tableEnv.sqlQuery("select * from sensor where id='sensor_1'");*/
// 方法二
// 直接从流,转换成表,并注册表名(没有 Table的对象)
tableEnv.createTemporaryView("inputTable", waterSensorStream);
// 这种方式,如果需要table对象,可以直接从表名获取
Table sensorTable = tableEnv.from("inputTable");
Table resultTable = tableEnv.sqlQuery("select * from " + sensorTable + " where id='sensor_1'");
tableEnv.toAppendStream(resultTable, Row.class).print();
env.execute();
}
}
3.2 Kafka到Kafka
使用sql从Kafka读数据, 并写入到Kafka中
package com.atguigu.flink.SQL;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
public class Test03_KafkaToKafka {
public static void main(String[] args) {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
// 1. 注册SourceTable: source_sensor
tableEnv.executeSql("create table source_sensor (id string, ts bigint, vc int) with("
+ "'connector' = 'kafka',"
+ "'topic' = 'topic_source_sensor',"
+ "'properties.bootstrap.servers' = 'hadoop102:9092,hadoop103:9092,hadoop104:9092',"
+ "'properties.group.id' = 'atguigu',"
+ "'scan.startup.mode' = 'latest-offset',"
+ "'format' = 'csv'"
+ ")");
// 2. 注册SinkTable: sink_sensor
tableEnv.executeSql("create table sink_sensor(id string, ts bigint, vc int) with("
+ "'connector' = 'kafka',"
+ "'topic' = 'topic_sink_sensor',"
+ "'properties.bootstrap.servers' = 'hadoop102:9092,hadoop103:9092,hadoop104:9092',"
+ "'format' = 'csv'"
+ ")");
// 3. 从SourceTable 查询数据, 并写入到 SinkTable
tableEnv.executeSql("insert into sink_sensor select * from source_sensor where id='sensor_1'");
}
}
# 启动kafka生产者生产数据
kafka-console-producer.sh --broker-list hadoop102:9092 --topic topic_source_sensor
# 启动kafka消费者消费数据
kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic topic_sink_sensor
使用 sql 关联外部系统:
语法:create table 表名 (字段名 字段类型,字段名 字段类型.....) with ( 参数名=参数值,参数名=参数值.....)
注意: 去官网查看 参数名 有哪些,有一些是必需的,有一些是可选的https://ci.apache.org/projects/flink/flink-docs-release-1.12/dev/table/connectors/kafka.html
4 时间属性
像窗口(在Table API和SQL)这种基于时间的操作,需要有时间信息。因此,Table API中的表就需要提供逻辑时间属性来表示时间,以及支持时间相关操作。
4.1 处理时间
4.1.1 DataStream到Table转换时定义
处理时间属性可以在schema定义的时候用 .proctime 后缀来定义。处理时间属性一定不能定义在一个已有字段上,所以它只能定义在schema定义的最后(1.13b版本已经可以)
package com.atguigu.flink.SQL;
import com.atguigu.flink.source.WaterSensor;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import static org.apache.flink.table.api.Expressions.$;
public class Test04_Proctime_1 {
public static void main(String[] args) {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
DataStreamSource<WaterSensor> waterSensorStream = env.fromElements(new WaterSensor("sensor_1", 1000L, 10),
new WaterSensor("sensor_1", 2000L, 20),
new WaterSensor("sensor_2", 3000L, 30),
new WaterSensor("sensor_1", 4000L, 40),
new WaterSensor("sensor_1", 5000L, 50),
new WaterSensor("sensor_2", 6000L, 60));
// 创建表的执行环境
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
Table sensorTable = tableEnv.fromDataStream(waterSensorStream, $("id"), $("ts"), $("vc"), $("pt").proctime());
sensorTable.execute().print();
}
}
4.1.2 在创建表的DDL中定义
package com.atguigu.flink.SQL;
import com.atguigu.flink.source.WaterSensor;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.TableResult;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import static org.apache.flink.table.api.Expressions.$;
public class Test05_Proctime_2 {
public static void main(String[] args) {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
tableEnv.executeSql("create table sensor(id string,ts bigint,vc int,pt_time as PROCTIME()) with("
+ "'connector' = 'filesystem',"
+ "'path' = 'input/sensor-sql_1.txt',"
+ "'format' = 'csv'"
+ ")");
TableResult result = tableEnv.executeSql("select * from sensor");
result.print();
}
}
4.2 事件时间
事件时间允许程序按照数据中包含的时间来处理,这样可以在有乱序或者晚到的数据的情况下产生一致的处理结果。它可以保证从外部存储读取数据后产生可以复现(replayable)的结果。
除此之外,事件时间可以让程序在流式和批式作业中使用同样的语法。在流式程序中的事件时间属性,在批式程序中就是一个正常的时间字段。
为了能够处理乱序的事件,并且区分正常到达和晚到的事件,Flink 需要从事件中获取事件时间并且产生 watermark(watermarks)。
4.2.1 DataStream到Table转换时定义
事件时间属性可以用.rowtime
后缀在定义 DataStream schema 的时候来定义。时间戳和watermark在这之前一定是在DataStream
上已经定义好了。
在从DataStream
到Table
转换时定义事件时间属性有两种方式。取决于用 .rowtime
后缀修饰的字段名字是否是已有字段,事件时间字段可以是:
- 在 schema 的结尾追加一个新的字段。
- 替换一个已经存在的字段。
不管在哪种情况下,事件时间字段都表示DataStream
中定义的事件的时间戳。
package com.atguigu.flink.SQL;
import com.atguigu.flink.source.WaterSensor;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import java.time.Duration;
import static org.apache.flink.table.api.Expressions.$;
public class Test06_Proctime_3 {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
SingleOutputStreamOperator<WaterSensor> waterSensorStream = env
.fromElements(new WaterSensor("sensor_1", 1000L, 10),
new WaterSensor("sensor_1", 2000L, 20),
new WaterSensor("sensor_2", 3000L, 30),
new WaterSensor("sensor_1", 4000L, 40),
new WaterSensor("sensor_1", 5000L, 50),
new WaterSensor("sensor_2", 6000L, 60))
.assignTimestampsAndWatermarks(
WatermarkStrategy
.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(5))
.withTimestampAssigner(((element, recordTimestamp) -> element.getTs()))
);
Table result = tableEnv
// 用一个额外的字段作为时间时间属性
//.fromDataStream(waterSensorStream, $("id"), $("ts"), $("vc"), $("et").rowtime());
// 使用已有的字段作为时间属性
.fromDataStream(waterSensorStream, $("id"), $("ts").rowtime(), $("vc"));
result.execute().print();
env.execute();
}
}
4.2.2 在创建表的DDL中定义
事件时间属性可以用 WATERMARK 语句在 CREATE TABLE DDL 中进行定义。WATERMARK 语句在一个已有字段上定义一个 watermark 生成表达式,同时标记这个已有字段为时间属性字段。
package com.atguigu.flink.SQL;
import com.atguigu.flink.source.WaterSensor;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import java.time.Duration;
import static org.apache.flink.table.api.Expressions.$;
public class Test07_Proctime_4 {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
StreamTableEnvironment tEnv = StreamTableEnvironment.create(env);
// 作为事件的时间字段必须是timestamp(3) 类型,所以根据Long类型的ts计算出来一个t
tEnv.executeSql("create table sensor(" +
"id string," +
"ts bigint," +
"vc int, " +
"t as to_timestamp(from_unixtime(ts/1000, 'yyyy-MM-dd HH:mm:ss'))," +
"watermark for t as t - interval '5' second)" +
"with(" +
"'connector' = 'filesystem'," +
"'path' = 'input/sensor-sql_1.txt'," +
"'format' = 'csv'" +
")");
tEnv.sqlQuery("select * from sensor").execute().print();
}
}
说明:
-
把一个现有的列定义为一个为表标记事件时间的属性。该列的类型必须为
TIMESTAMP(3)
,且是 schema 中的顶层列,它也可以是一个计算列。如何将一个时间戳转为TIMESTAMP(3)方式有以下两种
-
方式一:在1.12版本之后都可以用
t as to_timestamp(from_unixtime(ts/1000,'yyyy-MM-dd HH:mm:ss'))
-
方式二:在1.13版本之后可以用
t as to_timestamp_ltz(ts,3)
-
-
严格递增时间戳
WATERMARK FOR rowtime_column AS rowtime_column
-
递增时间戳
WATERMARK FOR rowtime_column AS rowtime_column - INTERVAL '0.001' SECOND
-
乱序时间戳
WATERMARK FOR rowtime_column AS rowtime_column - INTERVAL 'string' timeUnit
-
当发现时区所导致的时间问题时,可设置本地使用的时区
Configuration configuration = tableEnv.getConfig().getConfiguration(); configuration.setString("table.local-time-zone", "GMT");
参考官网https://ci.apache.org/projects/flink/flink-docs-release-1.12/dev/table/sql/create.html#watermark
5 窗口(Window)
时间语义,要配合窗口操作才能发挥作用。最主要的用途,当然就是开窗口然后根据时间段做计算了。
下面我们就来看看Table API和SQL中,怎么利用时间字段做窗口操作。
在Table API和SQL中,主要有两种窗口:Group Windows 和 Over Windows。
5.1 Table API 中使用窗口
5.1.1 Group Windows
分组窗口(Group Windows)会根据时间或行计数间隔,将行聚合到有限的组(Group)中,并对每个组的数据执行一次聚合函数。
Table API中的Group Windows都是使用 Window(w:GroupWindow)子句定义的,并且必须由as子句指定一个别名。为了按窗口对表进行分组,窗口的别名必须在group by子句中,像常规的分组字段一样引用。
滚动窗口
package com.atguigu.flink.SQL.window;
import com.atguigu.flink.source.WaterSensor;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.Session;
import org.apache.flink.table.api.Slide;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.Tumble;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import java.time.Duration;
import static org.apache.flink.table.api.Expressions.$;
import static org.apache.flink.table.api.Expressions.lit;
public class Test01_GroupWindows_1 {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
SingleOutputStreamOperator<WaterSensor> waterSensorStream = env.fromElements(new WaterSensor("sensor_1", 1000L, 10),
new WaterSensor("sensor_1", 2000L, 20),
new WaterSensor("sensor_2", 3000L, 30),
new WaterSensor("sensor_1", 4000L, 40),
new WaterSensor("sensor_1", 5000L, 50),
new WaterSensor("sensor_2", 6000L, 60))
.assignTimestampsAndWatermarks(
WatermarkStrategy
.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(5))
.withTimestampAssigner(((element, recordTimestamp) -> element.getTs()))
);
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
Table table = tableEnv
.fromDataStream(waterSensorStream, $("id"), $("ts").rowtime(), $("vc"));
table
// 定义滚动窗口并给窗口起一个别名
// 滚动窗口
//.window(Tumble.over(lit(10).second()).on($("ts")).as("w"))
// 滑动窗口
//.window(Slide.over(lit(10).second()).every(lit(5).second()).on($("ts")).as("w"))
// 会话窗口
.window(Session.withGap(lit(6).second()).on($("ts")).as("w"))
// 窗口必须出现的分组字段中
.groupBy($("id"), $("w"))
.select($("id"), $("w").start(), $("w").end(), $("vc").sum())
.execute()
.print();
env.execute();
}
}
开窗四部曲:
- 窗口类型 .window(
Tumble
.over(lit(10).second()).on($(“ts”)).as(“w”))- 窗口相关参数,比如窗口大小 .window(Tumble.
over(lit(10).second())
.on($(“ts”)).as(“w”))- 指定时间字段 .window(Tumble.over(lit(10).second()).
on($("ts"))
.as(“w”))- 窗口别名 .window(Tumble.over(lit(10).second()).on($(“ts”)).
as("w")
)
滑动窗口
.window(Slide.over(lit(10).second()).every(lit(5).second()).on($("ts")).as("w"))
会话窗口
.window(Session.withGap(lit(6).second()).on($("ts")).as("w")
5.1.2 Over Windows
Over window聚合是标准SQL中已有的(Over子句),可以在查询的SELECT子句中定义。Over window 聚合,会针对每个输入行,计算相邻行范围内的聚合。
Table API提供了Over类,来配置Over窗口的属性。可以在事件时间或处理时间,以及指定为时间间隔、或行计数的范围内,定义Over windows。
无界的over window是使用常量指定的。也就是说,时间间隔要指定UNBOUNDED_RANGE,或者行计数间隔要指定UNBOUNDED_ROW。而有界的over window是用间隔的大小指定的。
Unbounded Over Windows
package com.atguigu.flink.SQL.window;
import com.atguigu.flink.source.WaterSensor;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.Over;
import org.apache.flink.table.api.Session;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import java.time.Duration;
import static org.apache.flink.table.api.Expressions.*;
public class Test02_OverWindows {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
SingleOutputStreamOperator<WaterSensor> waterSensorStream = env.fromElements(new WaterSensor("sensor_1", 1000L, 10),
new WaterSensor("sensor_1", 4000L, 40),
new WaterSensor("sensor_1", 2000L, 20),
new WaterSensor("sensor_2", 3000L, 30),
new WaterSensor("sensor_1", 5000L, 50),
new WaterSensor("sensor_2", 6000L, 60))
.assignTimestampsAndWatermarks(
WatermarkStrategy
.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(1))
.withTimestampAssigner(((element, recordTimestamp) -> element.getTs()))
);
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
Table table = tableEnv
.fromDataStream(waterSensorStream, $("id"), $("ts").rowtime(), $("vc"));
table
// Unbounded Over Windows
// 使用 UNBOUNDED_ROW
//window(Over.partitionBy($("id")).orderBy($("ts")).preceding(UNBOUNDED_ROW).as("w"))
// 使用 UNBOUNDED_RANGE
//.window(Over.partitionBy($("id")).orderBy($("ts")).preceding(UNBOUNDED_RANGE).as("w"))
// Bounded Over Windows
// 当时间时间向前算3s得到一个窗口
//.window(Over.partitionBy($("id")).orderBy($("ts")).preceding(lit(3).second()).as("w"))
// 当行向前推算2行算一个窗口
.window(Over.partitionBy($("id")).orderBy($("ts")).preceding(rowInterval(2L)).as("w"))
.select($("id"), $("ts"), $("vc").sum().over($("w")).as("sum_vc"))
.execute()
.print();
env.execute();
}
}
5.2 SQL API中使用窗口
5.2.1 Group Windows
SQL查询的分组窗口是通过GROUP BY
子句定义的。类似于使用常规GROUP BY
语句的查询,窗口分组语句的GROUP BY
子句中带有一个窗口函数为每个分组计算出一个结果。以下是批处理表和流处理表支持的分组窗口函数:
分组窗口函数 | 描述 |
---|---|
TUMBLE(time_attr, interval) | 定义滚动时间窗口。 滚动时间窗口将行分配给具有固定持续时间(间隔)的不重叠的连续窗口。例如,一个5分钟的滚动窗口以5分钟的间隔对行进行分组。滚动窗口可以在事件时间(流+批处理)或处理时间(流)上定义。 |
HOP(time_attr, interval, interval) | 定义一个跳跃时间窗口(在Table API中称为滑动窗口)。 跳变时间窗口具有固定的持续时间(第二个间隔参数)和通过指定的跳变间隔(第一个间隔参数)进行跳变。如果跳距小于窗口大小,则跳转窗口重叠。因此,可以将行分配给多个窗口。例如,一个15分钟大小的跳转窗口和5分钟的跳转间隔将每行分配给3个15分钟大小的不同窗口,这些窗口在5分钟的间隔中进行评估。跳跃窗口可以在事件时间(流+批处理)或处理中定义。 |
SESSION(time_attr, interval) | 定义会话时间窗口。 会话时间窗口没有固定的持续时间,但其边界由不活动的时间间隔定义,即,如果在定义的间隔时间段内未出现任何事件,则关闭会话窗口。例如,间隔30分钟的会话窗口在30分钟不活动后观察到一行时开始(否则该行将被添加到现有窗口),如果在30分钟内未添加任何行,则关闭该窗口。会话窗口可以在事件时间(流+批处理)或处理时间(流)上工作。 |
package com.atguigu.flink.SQL.window;
import com.atguigu.flink.source.WaterSensor;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.Over;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import java.time.Duration;
import static org.apache.flink.table.api.Expressions.$;
import static org.apache.flink.table.api.Expressions.rowInterval;
public class Test03_GroupWindows {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
// 作为事件时间的字段必须是timestamp类型,所以根据Long类型的ts计算出一个t
tableEnv.executeSql("create table sensor(" +
"id string," +
"ts bigint," +
"vc int, " +
"t as to_timestamp(from_unixtime(ts/1000,'yyyy-MM-dd HH:mm:ss'))," +
"watermark for t as t - interval '5' second)" +
"with(" +
"'connector' = 'filesystem'," +
"'path' = 'input/sensor-sql_1.txt'," +
"'format' = 'csv'" +
")");
tableEnv
/* .sqlQuery(
"select id, "
+ " TUMBLE_START(t, INTERVAL '1' minute) as wStart, "
+ " TUMBLE_END(t, INTERVAL '1' minute) as wEnd, "
+ " SUM(vc) sum_vc "
+ "FROM sensor "
+ "GROUP BY TUMBLE(t, INTERVAL '1' minute), id"
)*/
.sqlQuery(
"SELECT id, " +
" hop_start(t, INTERVAL '1' minute, INTERVAL '1' hour) as wStart, " +
" hop_end(t, INTERVAL '1' minute, INTERVAL '1' hour) as wEnd, " +
" SUM(vc) sum_vc " +
"FROM sensor " +
"GROUP BY hop(t, INTERVAL '1' minute, INTERVAL '1' hour), id"
)
.execute()
.print();
}
}
5.2.2 Over Windows
package com.atguigu.flink.SQL.window;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
public class Test04_OverWindows {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
// 作为事件时间的字段必须是timestamp类型,所以根据Long类型的ts计算出一个t
tableEnv.executeSql("create table sensor(" +
"id string," +
"ts bigint," +
"vc int, " +
"t as to_timestamp(from_unixtime(ts/1000,'yyyy-MM-dd HH:mm:ss'))," +
"watermark for t as t - interval '5' second)" +
"with(" +
"'connector' = 'filesystem'," +
"'path' = 'input/sensor-sql_1.txt'," +
"'format' = 'csv'" +
")");
tableEnv
/* .sqlQuery(
"select " +
"id," +
"vc," +
"sum(vc) over(partition by id order by t rows between 1 PRECEDING and current row)" +
"from sensor"
)*/
.sqlQuery(
"select " +
"id," +
"vc," +
"count(vc) over w," +
"sum(vc) over w " +
"from sensor " +
"window w as (partition by id order by t rows between 1 PRECEDING and current row)"
)
.execute()
.print();
}
}
6 函数(Functions)
Flink Table 和 SQL 内置了很多 SQL 中支持的函数:如果无法满足需要,还可以实现用户自定义的函数(UDF)来解决。
6.1 系统内置函数
Flink Table API 和 SQL为用户提供了一组用于数据转换的内置函数。SQL中支持的很多函数,Table API和SQL都已经做了实现,其它还在快速开发扩展中。
以下是一些典型函数的举例,全部的内置函数,可以参考官网介绍
比较函数
SQL:
value1 = value2
value1 > value2
Table API:
ANY1 === ANY2
ANY1 > ANY2
逻辑函数
SQL:
boolean1 OR boolean2
boolean IS FALSE
NOT boolean
Table API:
BOOLEAN1 || BOOLEAN2
BOOLEAN.isFalse
!BOOLEAN
算术函数
SQL:
numeric1 + numeric2
POWER(numeric1, numeric2)
Table API:
NUMERIC1 + NUMERIC2
NUMERIC1.power(NUMERIC2)
字符串函数
SQL:
string1 || string2
UPPER(string)
CHAR_LENGTH(string)
Table API:
STRING1 + STRING2
STRING.upperCase()
STRING.charLength()
时间函数
SQL:
DATE string
TIMESTAMP string
CURRENT_TIME
INTERVAL string range
Table API:
STRING.toDate
STRING.toTimestamp
currentTime()
NUMERIC.days
NUMERIC.minutes
聚合函数
SQL:
COUNT(*)
SUM([ ALL | DISTINCT ] expression)
RANK()
ROW_NUMBER()
Table API:
FIELD.count
FIELD.sum0
6.2 UDF
用户定义函数(User-defined Functions,UDF)是一个重要的特性,因为它们显著地扩展了查询(Query)的表达能力。一些系统内置函数无法解决的需求,我们可以用UDF来自定义实现。
6.2.1 注册用户自定义函数UDF
在大多数情况下,用户定义的函数必须先注册,然后才能在查询中使用。不需要专门为Scala 的 Table API 注册函数。
函数通过调用 registerFunction() 方法在 TableEnvironment 中注册。当用户定义的函数被注册时,它被插入到 TableEnvironment 的函数目录中,这样 Table API 或 SQL 解析器就可以识别并正确地解释它。
6.2.2 标量函数(Scalar Functions)
用户定义的标量函数,可以将0、1或多个标量值,映射到新的标量值。
为了定义标量函数,必须在 org.apache.flink.table.functions 中扩展基类 Scalar Function,并实现(一个或多个)求值(evaluation,eval)方法。标量函数的行为由求值方法决定,求值方法必须公开声明并命名为eval(直接public声明,没有override)。求值方法的参数类型和返回类型,确定了标量函数的参数和返回类型。
package com.atguigu.flink.SQL.functions;
import com.atguigu.flink.source.WaterSensor;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.table.functions.ScalarFunction;
import static org.apache.flink.table.api.Expressions.$;
import static org.apache.flink.table.api.Expressions.call;
public class Test01_ScakarFunctions {
public static void main(String[] args) {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
// 读取元素得到DataStream
DataStreamSource<WaterSensor> waterSensorDataStreamSource = env.fromElements(new WaterSensor("sensor_1", 1000L, 10),
new WaterSensor("sensor_1", 2000L, 20),
new WaterSensor("sensor_2", 3000L, 30),
new WaterSensor("sensor_1", 4000L, 40),
new WaterSensor("sensor_1", 5000L, 50),
new WaterSensor("sensor_2", 6000L, 60));
// 将流转换为动态表
Table table = tableEnv.fromDataStream(waterSensorDataStreamSource);
//todo 1 不注册函数直接使用
//table.select(call(MyLenth.class, $("id").as("id"))).execute().print();
//todo 2 先注册后使用
tableEnv.createTemporarySystemFunction("MyLenth", MyLenth.class);
//tableEnv.executeSql("select MyLenth(id) from " + table).print();
//todo 3 TableAPI
table.select(call("MyLenth", $("id"))).execute().print();
}
public static class MyLenth extends ScalarFunction {
public int eval(String value) {
return value.length();
}
}
}
6.2.3 表函数(Table Functions)
与用户定义的标量函数类似,用户定义的表函数,可以将0、1或多个标量值作为输入参数;与标量函数不同的是,它可以返回任意数量的行作为输出,而不是单个值。
为了定义一个表函数,必须扩展 org.apache.flink.table.functions 中的基类 TableFunction 并实现(一个或多个)求值方法。表函数的行为由其求值方法决定,求值方法必须是 public 的,并命名为 eval。求值方法的参数类型,决定表函数的所有有效参数。
返回表的类型由 TableFunction 的泛型类型确定。求值方法使用 protected collect(T)方法发出输出行。
在 Table API 中,Table 函数需要与 .joinLateral或 .leftOuterJoinLateral 一起使用。
joinLateral 算子,会将外部表中的每一行,与表函数(TableFunction,算子的参数是它的表达式)计算得到的所有行连接起来。
而 leftOuterJoinLateral 算子,则是左外连接,它同样会将外部表中的每一行与表函数计算生成的所有行连接起来;并且,对于表函数返回的是空表的外部行,也要保留下来。
在SQL中,则需要使用 Lateral Table(),或者带有 ON TRUE 条件的左连接。
下面的代码中,我们将定义一个表函数,在表环境中注册它,并在查询中调用它。
package com.atguigu.flink.SQL.functions;
import com.atguigu.flink.source.WaterSensor;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.annotation.DataTypeHint;
import org.apache.flink.table.annotation.FunctionHint;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.table.functions.ScalarFunction;
import org.apache.flink.table.functions.TableFunction;
import org.apache.flink.types.Row;
import static org.apache.flink.table.api.Expressions.$;
import static org.apache.flink.table.api.Expressions.call;
public class Test02_TableFunctions {
public static void main(String[] args) {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
// 读取元素得到DataStream
DataStreamSource<WaterSensor> waterSensorDataStreamSource = env.fromElements(new WaterSensor("sensor_1", 1000L, 10),
new WaterSensor("sensor_1", 2000L, 20),
new WaterSensor("sensor_2", 3000L, 30),
new WaterSensor("sensor_1", 4000L, 40),
new WaterSensor("sensor_1", 5000L, 50),
new WaterSensor("sensor_2", 6000L, 60));
Table table = tableEnv.fromDataStream(waterSensorDataStreamSource);
//todo 1 先注册后使用
tableEnv.createTemporarySystemFunction("split", SplitFunction.class);
// SQL
//tableEnv.executeSql("select id, word from " + table + ", lateral table(split(id))").print();
// TableAPI
table
.joinLateral(call("split",$("id")))
.select($("id"),$("word"))
.execute()
.print();
}
// 自定义UDTF函数将传入的id按照下划线炸裂成两条数据
// hint暗示,主要作用为类型推断时使用
@FunctionHint(output = @DataTypeHint("Row<word STRING>"))
public static class SplitFunction extends TableFunction<Row> {
public void eval(String str) {
for (String s: str.split("_")) {
collect(Row.of(s));
}
}
}
}
6.2.4 聚合函数(Aggregate Functions)
用户自定义聚合函数(User-Defined Aggregate Functions,UDAGGs)可以把一个表中的数据,聚合成一个标量值。用户定义的聚合函数,是通过继承 AggregateFunction 抽象类实现的。
上图中显示了一个聚合的例子。
假设现在有一张表,包含了各种饮料的数据。该表由三列(id、name和price)、五行组成数据。现在我们需要找到表中所有饮料的最高价格,即执行max()聚合,结果将是一个数值。
AggregateFunction的工作原理如下。
- 首先,它需要一个累加器,用来保存聚合中间结果的数据结构(状态)。可以通过调用 AggregateFunction的createAccumulator() 方法创建空累加器。
- 随后,对每个输入行调用函数的 accumulate() 方法来更新累加器。
- 处理完所有行后,将调用函数的 getValue() 方法来计算并返回最终结果。
AggregationFunction要求必须实现的方法:
createAccumulator()
accumulate()
getValue()
除了上述方法之外,还有一些可选择实现的方法。其中一些方法,可以让系统执行查询更有效率,而另一些方法,对于某些场景是必需的。例如,如果聚合函数应用在会话窗口(session group window)的上下文中,则 merge() 方法是必需的。
retract()
merge()
resetAccumulator()
接下来我们写一个自定义 AggregateFunction,计算一下每个 WaterSensor 中 VC 的平均值。
package com.atguigu.flink.SQL.functions;
import com.atguigu.flink.source.WaterSensor;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.annotation.DataTypeHint;
import org.apache.flink.table.annotation.FunctionHint;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.table.functions.AggregateFunction;
import org.apache.flink.table.functions.FunctionRequirement;
import org.apache.flink.table.functions.TableFunction;
import org.apache.flink.types.Row;
import java.util.Set;
import static org.apache.flink.table.api.Expressions.$;
import static org.apache.flink.table.api.Expressions.call;
public class Test03_AggregateFunctions {
public static void main(String[] args) {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
// 读取元素得到DataStream
DataStreamSource<WaterSensor> waterSensorDataStreamSource = env.fromElements(new WaterSensor("sensor_1", 1000L, 10),
new WaterSensor("sensor_1", 2000L, 20),
new WaterSensor("sensor_2", 3000L, 30),
new WaterSensor("sensor_1", 4000L, 40),
new WaterSensor("sensor_1", 5000L, 50),
new WaterSensor("sensor_2", 6000L, 60));
Table table = tableEnv.fromDataStream(waterSensorDataStreamSource);
//todo 先注册后使用
tableEnv.createTemporarySystemFunction("myavg", MyAvg.class);
//todo SQL
//tableEnv.executeSql("select id, myavg(vc) from " + table + " group by id").print();
//todo TableAPI
table.groupBy($("id"))
.select($("id"), call("myavg", $("vc")))
.execute()
.print();
}
// 定义一个类做累加器,并声明总数和总个数这两个值
public static class MyAvgAccumulator {
public long sum = 0;
public int count = 0;
}
// 自定义UDAF函数,求每个Wat儿Sensor中VC的平均值
public static class MyAvg extends AggregateFunction<Double, MyAvgAccumulator> {
@Override
public Double getValue(MyAvgAccumulator accumulator) {
return accumulator.sum * 1D / accumulator.count;
}
// 创建累加器
@Override
public MyAvgAccumulator createAccumulator() {
return new MyAvgAccumulator();
}
// 做累加操作
public void accumulate(MyAvgAccumulator acc, Integer vc) {
acc.sum += vc;
acc.count += 1;
}
}
}
6.2.5 表聚合函数(Table Aggregate Functions)
用户定义的表聚合函数(User-Defined Table Aggregate Functions,UDTAGGs),可以把一个表中数据,聚合为具有多行和多列的结果表。这跟 AggregateFunction 非常类似,只是之前聚合结果是一个标量值,现在变成了一张表。
比如现在我们需要找到表中所有WaterSensor的前2个最高水位线,即执行 top2() 表聚合。
用户定义的表聚合函数,是通过继承 TableAggregateFunction 抽象类来实现的。
TableAggregateFunction的工作原理如下。
- 首先,它同样需要一个累加器(Accumulator),它是保存聚合中间结果的数据结构。通过调用 TableAggregateFunction 的 createAccumulator() 方法可以创建空累加器。
- 随后,对每个输入行调用函数的 accumulate() 方法来更新累加器。
- 处理完所有行后,将调用函数的 emitValue() 方法来计算并返回最终结果。
TableAggregationFunction要求必须实现的方法:
createAccumulator()
getValue()
除了上述方法之外,还有一些可选择实现的方法。
retract()
merge()
resetAccumulator()
emitValue()
emitUpdateWithRetract()
接下来我们写一个自定义TableAggregateFunction,用来提取每个WaterSensor最高的两个水位值。
package com.atguigu.flink.SQL.functions;
import com.atguigu.flink.source.WaterSensor;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.table.functions.AggregateFunction;
import org.apache.flink.table.functions.TableAggregateFunction;
import org.apache.flink.util.Collector;
import static org.apache.flink.table.api.Expressions.$;
import static org.apache.flink.table.api.Expressions.call;
public class Test04_TableAggregateFunctions {
public static void main(String[] args) {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
// 读取元素得到DataStream
DataStreamSource<WaterSensor> waterSensorDataStreamSource = env.fromElements(new WaterSensor("sensor_1", 1000L, 10),
new WaterSensor("sensor_1", 2000L, 20),
new WaterSensor("sensor_2", 3000L, 30),
new WaterSensor("sensor_1", 4000L, 40),
new WaterSensor("sensor_1", 5000L, 50),
new WaterSensor("sensor_2", 6000L, 60));
Table table = tableEnv.fromDataStream(waterSensorDataStreamSource);
//todo 先注册后使用
tableEnv.createTemporarySystemFunction("Top2", Top2.class);
//todo TableAPI
table
.groupBy($("id"))
.flatAggregate(call("Top2", $("vc")).as("top", "rank"))
.select($("id"), $("top"), $("rank"))
.execute()
.print();
}
// 定义一个类当作累加器,并声明第一和第二这两个值
public static class vcTop2 {
public Integer first = Integer.MIN_VALUE;
public Integer second = Integer.MIN_VALUE;
}
// 自定义UDATF函数(多进多出),求每个WaterSensor中最高的两个水位值
public static class Top2 extends TableAggregateFunction<Tuple2<Integer, Integer>, vcTop2> {
// 创建累加器
@Override
public vcTop2 createAccumulator() {
return new vcTop2();
}
// 比较数据,如果当前数据大于累加器中存在的数据则替换,并将累加器中的数据往下赋值
public void accumulate(vcTop2 acc, Integer value) {
if (value > acc.first) {
acc.second = acc.first;
acc.first = value;
} else if (value > acc.second) {
acc.second = value;
}
}
// 计算(排名)
public void emitValue(vcTop2 acc, Collector<Tuple2<Integer, Integer>> out) {
// emit the value and rank
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));
}
}
}
}
7 Catalog
Catalog 提供了元数据信息,例如数据库、表、分区、视图以及数据库或其他外部系统中存储的函数和信息。
数据处理最关键的方面之一是管理元数据。元数据可以是临时的,例如临时表、或者通过 TableEnvironment 注册的 UDF。元数据也可以是持久化的,例如 Hive Metastore 中的元数据。Catalog 提供了一个统一的 API,用于管理元数据,并使其可以从 Table API 和 SQL 查询语句中来访问。
前面用到 Connector 其实就是在使用 Catalog。
7.1 Catalog类型
- GenericInMemoryCatalog(临时的):是基于内存实现的 Catalog,所有元数据只在 session 的生命周期内可用。
- JdbcCatalog:JdbcCatalog 使得用户可以将 Flink 通过 JDBC 协议连接到关系数据库。PostgresCatalog 是当前实现的唯一一种 JDBC Catalog。
- HiveCatlog(用的最多):
HiveCatalog
有两个用途:作为原生 Flink 元数据的持久化存储,以及作为读写现有Hive元数据的接口。 Flink的Hive 文档提供了有关设置HiveCatalog
以及访问现有Hive元数据的详细信息。
7.2 HiveCatalog
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-hive_2.12</artifactId>
<version>1.13.0</version>
</dependency>
<!-- Hive Dependency -->
<dependency>
<groupId>org.apache.hive</groupId>
<artifactId>hive-exec</artifactId>
<version>3.1.2</version>
<exclusions>
<exclusion>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
</exclusion>
</exclusions>
</dependency>
# 在hive-site.xml中添加配置
<!-- 指定存储元数据要连接的地址 -->
<property>
<name>hive.metastore.uris</name>
<value>thrift://hadoop102:9083</value>
</property>
# 在hadoop102启动hive元数据
nohup hive --service metastore >/dev/null 2>&1 &
// 连接Hive
//设置用户权限
System.setProperty("HADOOP_USER_NAME", "atguigu");
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
StreamTableEnvironment tEnv = StreamTableEnvironment.create(env);
String name = "myhive"; // Catalog 名字
String defaultDatabase = "flink_test"; // 默认数据库
String hiveConfDir = "c:/conf"; // hive配置文件的目录. 需要把hive-site.xml添加到该目录
// 1. 创建HiveCatalog
HiveCatalog hive = new HiveCatalog(name, defaultDatabase, hiveConfDir);
// 2. 注册HiveCatalog
tEnv.registerCatalog(name, hive);
// 3. 把 HiveCatalog: myhive 作为当前session的catalog
tEnv.useCatalog(name);
tEnv.useDatabase("flink_test");
tEnv.sqlQuery("select * from stu").execute().print();
8 SQLClient
启动完一个yarn-session, 然后启动另一个sql客户端。
bin/sql-client.sh embedded
8.1 建立到Kafka的连接
下面创建一个流表从Kafka读取数据
copy “flink-sql-connector-kafka_2.11-1.13.3.jar” 依赖到 flink 的lib 目录下 flink-sql-connector-kafka_2.11-1.13.3.jar 下载地址: https://repo.maven.apache.org/maven2/org/apache/flink/flink-sql-connector-kafka_2.11/1.13.3/flink-sql-connector-kafka_2.11-1.13.3.jar
<!-- https://mvnrepository.com/artifact/org.apache.flink/flink-sql-connector-kafka -->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-sql-connector-kafka_2.11</artifactId>
<version>1.13.3</version>
<scope>provided</scope>
</dependency>
create table sensor(id string, ts bigint, vc int)
with(
'connector'='kafka',
'topic'='sensor',
'properties.bootstrap.servers'='hadoop102:9092',
'properties.group.id'='atguigu',
'format'='csv',
'scan.startup.mode'='latest-offset'
)
select * from sensor;
# 向Kafka写入数据:先开启一个生产者
kafka-console-producer.sh --broker-list hadoop102:9092 --topic sensor
# 写入csv格式的数据:
sensor_1,1,1
8.2 建立到MySQL的连接
<!-- https://mvnrepository.com/artifact/org.apache.flink/flink-connector-jdbc -->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-jdbc_2.11</artifactId>
<version>1.13.3</version>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>