关系型API
- 关系型API
- 为什么需要关系型API?
- Calcite
- 关系API概述
- 程序结构
- 获取运行时
- 获取Table运行时
- 表注册
- 输入表注册
- 输出表注册
- TableSource和TableSink
- TableSource
- TableSink
- 定义查询
- 相互转换
- 将DataStream或DataSet转换为表
- 将表转换为DataStream或DataSet
- 将表转换为DataStream
- 将表转换为DataSet
- 动态表概述
- 流式关系代数
- 动态表
- 持续查询
- 动态表到流的转换
关系型API
为什么需要关系型API?
DataStream或DataSet需要开发任意掌握Java或Scala语言,并精通两套API。随着流处理应用的广泛推广以及Flink在该领域的快速增长,Flink社区决定推出一套同时支持流处理与批处理、并且更简单的API,以降低开发门槛,满足更多用户的使用需求,关系型API由此产生。
关系型API是 Table API (提供类似LINQ(集成查询语言)语法的API)和 SQL(支持标准SQL查询)(是Table API的更高层抽象)的统称。
具有以下优点:
- 它是声明式的,用户不需要提供计算的实现细节,系统决定如何计算。
- 关系型API需要响应的执行引擎,用于翻译SQL,因此我们可在引擎层全局优化查询,而不是在编程语言层优化程序。DataStream和DataSe应用程序很难做到这一点。
- SQL的普及程度远远高于编程语言,尤其是在数据分析领域。
Calcite
Flink没有重复造轮子,是根据SQL解析与优化框架Calcite构建关系型API。
Apach Calcite是为不同计算平台和数据源提供统一动态数据管理服务的高层框架,Calcite在各种数据源上构建标准的SQL语言,并提供多种查询优化方案。此外,Calcite也适合用于流处理场景。
Calcite的目标是为不同计算平台和数据源提供统一的查询引擎(一种方案适应所有需求场景,one size fits all),并以类似SQL查询语言访问不同的数据源。
Apach Calcite执行SQL查询的主要步骤如下:
- 将SQL解析成未经校验额抽象语法树(AST Abstract Syntax Tree),抽象语法树是和语言无关的形式,这和交叉编译的第一步类似。
- Validate:验证AST,主要验证SQL语句是否合法,验证后结果为RelNode树。
- Optimize:优化RelNode树并生成物理执行计划。
- Execute:将物理执行计划转换为特定平台的执行代码,如Flink的DataStream应用程序。
关系API概述
关系型API是以库的形式提供的,在Maven工程中需要加入以下依赖:
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-table-planner_2.11</artifactId>
<version>1.8.0</version>
</dependency>
此外,根据使用的编程语言,需要获取对关系型API的支持:
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-table-api-java-bridge_2.11</artifactId>
<version>1.8.0</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-table-api-scala-bridge_2.11</artifactId>
<version>1.8.0</version>
</dependency>
在内部,部分表生态系统是用Scala实现的。因此,请确保为批处理和流处理应用程序添加以下依赖项:
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-scala_2.11</artifactId>
<version>1.8.0</version>
</dependency>
程序结构
关系型API统一了DataStream API和DataSet API,而且Table API和SQL屏蔽了批处理和流处理的细节,因此Table API和SQL的应用程序结构是一致的。基于关系型API的数据处理应用程序应包括以下6个步骤:
获取运行时
- StreamingExecutionEnvironment:对应流处理
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
- ExecutionEnvironment:对应批处理
ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
获取Table运行时
关系型API统一了DataStream API与DataSet API,因此TableEnvironment根据DataStream和DataSet分别定义了获取Table运行时的方法:
- StreamTableEnvironment:对应流处理
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env)
- BatchTableEnvironment:对应批处理
BatchTableEnvironment tableEev = BatchTableEnvironment.create(env);
作为关系API的核心对象,TableEnvironment承担以下任务:
- 负责表的管理。目录管理Schema的元数据和命名空间。
- SQL查询。Flink规定统一查询中的表不能定义在不同的运行时环境中。
- UDF管理
- 将DataStream或DataSet转换成表
- 管理StreamTableEnvironment对象和BatchTableEnvironment对象的引用。
统一批处理API和流处理API是以增加架构复杂度为代价的,并且关系型API是构建在DataStream API和DataSet API之上的,而不是重复造轮子。
表注册
TableEnvironment维护按照名称进行注册的表目录。有两种类型的表,输入表和输出表:
- 输入表:输入表可以在表API和SQL查询中引用,并提供输入数据。
- 输出表:输出表可用于将表API或SQL查询的结果发送到外部系统。
为什么区分输入表和输出表:
- Flink引擎以pull的方式获取外部数据,但不支持数据更新到Source,这是符合数据分析哲学的。
- 应用程序的目标是获取关系代数的运算结果,但大多数情况下,Schema中并没有结果对应的数据结构。
因此这里的表和关系型数据库中的视图类似。
输入表注册
- 基于已有的表实例
// get a StreamTableEnvironment, works for BatchTableEnvironment equivalently
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
// Table is the result of a simple projection query
Table projTable = tableEnv.scan("X").select(...);
// register the Table projTable as table "projectedX"
tableEnv.registerTable("projectedTable", projTable);
- 基于外部Source
// get a StreamTableEnvironment, works for BatchTableEnvironment equivalently
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
// create a TableSource
TableSource csvSource = new CsvTableSource("/path/to/file", ...);
// register the TableSource as table "CsvTable"
tableEnv.registerTableSource("CsvTable", csvSource);
- 基于外部catalog
// get a StreamTableEnvironment, works for BatchTableEnvironment equivalently
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
// create an external catalog
ExternalCatalog catalog = new InMemoryExternalCatalog();
// register the ExternalCatalog catalog
tableEnv.registerExternalCatalog("InMemCatalog", catalog);
4.基于DataStream或DataSet
// get StreamTableEnvironment
// registration of a DataSet in a BatchTableEnvironment is equivalent
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
DataStream<Tuple2<Long, String>> stream = ...
// register the DataStream as Table "myTable" with fields "f0", "f1"
tableEnv.registerDataStream("myTable", stream);
// register the DataStream as table "myTable2" with fields "myLong", "myString"
tableEnv.registerDataStream("myTable2", stream, "myLong, myString");
输出表注册
由TableSink注册:
// get a StreamTableEnvironment, works for BatchTableEnvironment equivalently
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
// create a TableSink
TableSink csvSink = new CsvTableSink("/path/to/file", ...);
// define the field names and types
String[] fieldNames = {"a", "b", "c"};
TypeInformation[] fieldTypes = {Types.INT, Types.STRING, Types.LONG};
// register the TableSink as table "CsvSinkTable"
tableEnv.registerTableSink("CsvSinkTable", fieldNames, fieldTypes, csvSink);
TableSource和TableSink
TableSource
tableEnvironment
// 连接外部数据源
.connect(...)
// 解析数据源格式
.withFormat(...)
// 定义Schema
.withSchema(...)
// 输出表的更新方式
.inAppendMode()
// 注册表
.registerTableSource("MyTable")
TableSink
tableEnvironment
// 连接外部数据源
.connect(...)
// 解析数据源格式
.withFormat(...)
// 定义Schema
.withSchema(...)
// 输出表的更新方式
.inAppendMode()
// 注册表
.registerTableSink("MyTable")
Kafks示例:
tableEnvironment
// declare the external system to connect to
.connect(
new Kafka()
.version("0.10")
.topic("test-input")
.startFromEarliest()
.property("zookeeper.connect", "localhost:2181")
.property("bootstrap.servers", "localhost:9092")
)
// declare a format for this system
.withFormat(
new Avro()
.avroSchema(
"{" +
" \"namespace\": \"org.myorganization\"," +
" \"type\": \"record\"," +
" \"name\": \"UserMessage\"," +
" \"fields\": [" +
" {\"name\": \"timestamp\", \"type\": \"string\"}," +
" {\"name\": \"user\", \"type\": \"long\"}," +
" {\"name\": \"message\", \"type\": [\"string\", \"null\"]}" +
" ]" +
"}"
)
)
// declare the schema of the table
.withSchema(
new Schema()
.field("rowtime", Types.SQL_TIMESTAMP)
.rowtime(new Rowtime()
.timestampsFromField("timestamp")
.watermarksPeriodicBounded(60000)
)
.field("user", Types.LONG)
.field("message", Types.STRING)
)
// specify the update-mode for streaming tables
.inAppendMode()
// register as source, sink, or both and under a name
.registerTableSource("MyUserTable");
定义查询
Table API 与 SQL查询的格式是不同的:
- Table API
// scan registered Orders table
Table orders = tableEnv.scan("Orders");
// compute revenue for all customers from France
Table revenue = orders
.filter("cCountry === 'FRANCE'")
.groupBy("cID, cName")
.select("cID, cName, revenue.sum AS revSum");
- SQL
Table revenue = tableEnv.sqlQuery(
"SELECT cID, cName, SUM(revenue) AS revSum " +
"FROM Orders " +
"WHERE cCountry = 'FRANCE' " +
"GROUP BY cID, cName"
);
相互转换
应用程序可将DataStream或DataSet转换成Table,以可将Table转换成DataStream或DataSet。
将DataStream或DataSet转换为表
// get StreamTableEnvironment
// registration of a DataSet in a BatchTableEnvironment is equivalent
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
DataStream<Tuple2<Long, String>> stream = ...
// Convert the DataStream into a Table with default fields "f0", "f1"
Table table1 = tableEnv.fromDataStream(stream);
// Convert the DataStream into a Table with fields "myLong", "myString"
Table table2 = tableEnv.fromDataStream(stream, "myLong, myString");
将表转换为DataStream或DataSet
表可以转换为DataStream或DataSet。通过这种方式,可以对Table API或SQL的结果运行自定义DataStream或DataSet程序。
将表转换为DataStream或DataSet时,需要指定DataStream或DataSet的数据类型(要将表的行转换成的数据类型)。通常最方便的转换类型是Row。下面的列表概述了不同选项的特性:
- Row:字段由位置、任意数量的字段、支持null值、没有类型安全访问。
- POJO:字段按名称映射(POJO字段必须命名为Table字段),任意数量的字段,支持null值,类型安全访问。
- Case Class:字段按位置映射,不支持null值,类型安全访问。
- Tuple:字段按位置映射,限制为22(Scala)或25(Java)字段,不支持null值,类型安全访问。
- Atomic Type:Table必须具有单个字段,不支持null值,类型安全访问。
将表转换为DataStream
一个Table是流媒体查询的结果将动态更新,随着新记录到达查询的输入流,它也在发生变化。因此DataStream转换这种动态查询需要对表的更新进行编码。
将Table转换为DataStream有两种模式:
Append Mode(追加模式):只有当动态表通过INSERT修改时,才可以使用此模式,它只进行追加不会对以前发出的结果进行修改。
Retract Mode(重复模式):这种模式总是可以使用。它使用boolean定义是插入还是删除操作。
// get StreamTableEnvironment.
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
// Table with two fields (String name, Integer age)
Table table = ...
// convert the Table into an append DataStream of Row by specifying the class
DataStream<Row> dsRow = tableEnv.toAppendStream(table, Row.class);
// convert the Table into an append DataStream of Tuple2<String, Integer>
// via a TypeInformation
TupleTypeInfo<Tuple2<String, Integer>> tupleType = new TupleTypeInfo<>(
Types.STRING(),
Types.INT());
DataStream<Tuple2<String, Integer>> dsTuple =
tableEnv.toAppendStream(table, tupleType);
// convert the Table into a retract DataStream of Row.
// A retract stream of type X is a DataStream<Tuple2<Boolean, X>>.
// The boolean field indicates the type of the change.
// True is INSERT, false is DELETE.
DataStream<Tuple2<Boolean, Row>> retractStream =
tableEnv.toRetractStream(table, Row.class);
将表转换为DataSet
// get BatchTableEnvironment
BatchTableEnvironment tableEnv = BatchTableEnvironment.create(env);
// Table with two fields (String name, Integer age)
Table table = ...
// convert the Table into a DataSet of Row by specifying a class
DataSet<Row> dsRow = tableEnv.toDataSet(table, Row.class);
// convert the Table into a DataSet of Tuple2<String, Integer> via a TypeInformation
TupleTypeInfo<Tuple2<String, Integer>> tupleType = new TupleTypeInfo<>(
Types.STRING(),
Types.INT());
DataSet<Tuple2<String, Integer>> dsTuple =
tableEnv.toDataSet(table, tupleType);
动态表概述
流式关系代数
SQL的本质是关系代数,而关系代数是定义在有界数据集上的,因此将关系代数运算移植到DataSet上不会遇到理论上的障碍。而移植到DataStream则不易理解:D阿塔Stream处理的对象是无限延伸且在时间轴上动态变化的数据集,而流式关系代数理论并不能建立在数据库技术的基础之上,否则流处理会失去意义。
流处理和关系代数间的区别:
- 执行机制不同:根据CAP理论,可用性是有时限要求的,因此关系型数据库必须及时响应;而流处理是要一直运行下去,没有结束点的。
- 计算结果的形式不同:SQL查询的结果解构事先知晓,并大小有限的;流处理则一直更新计算结果,并以流的形式发送出去,其结构和大小是未知的。
动态表
之所以称为动态表(Dynamic Table),是因为相对于关系型数据库的表,动态表的内容是不可预知且持续变化的,且相对于关系型表的查询,在动态表上的查询会产生持续查询(Continuous Query),这符合Flink流式数据处理编程模型:应用程序被翻译成计算图,其逻辑结构是固定的,数据处理任务是持续 的。
在DataStream上执行SQL查询,处理过程如下:
- 将DataStream转换成动态表
- 在动态表上定义持续查询
- 将持续查询的实时结果转换成动态表
- 将使用动态表表示的查询结果转换成DataStream,这样程序可借助DataStream API进一步转换查询结果。
下面以用户点击行为分析为例进行说明:
[
user: VARCHAR, // 用户名
cTime: TIMESTAMP, // 点击时间
url: VARCHAR // 点击界面的URL
]
为了使用关系查询处理DataStream,必须将其转换为动态表。从概念上讲,DataStream的每个记录都被解释为动态表的一条数据。
用户点击行为的动态表形式如图,下图显示了单击事件流(左侧)如何转换为表(右侧)。随着插入更多的单击流记录,结果表将不断增长:
持续查询
接下来,分析每个用户的累计点击量,SQL语句如下所示:
SELECT USER,COUNT(URL) AS CNT FROM CLIKS GROUP BY USER
Flink会根据记录被观察的顺序持续执行SQL查询,生成动态变化的表,即表的行数会动态变化,某行的内容也会发生改变。
窗口是流处理的重要机制,动态表不会丢弃这个流式数据处理的优良功能。以一小时长度开事件时间窗口,数据保存到.cvs文件中,在每个窗口内分析用户的累计点击次数,结果如图:
动态表到流的转换
动态表可以像常规数据库表一样通过插入、更新和删除不断地进行修改。它可能是一个只有一行的表(不断更新)、一个只进行插入而不进行更新和删除修改的表,或者介于两者之间的任何东西。
当将动态表转换为流或将其写入外部系统时,需要对这些更改进行编码。Flink的Table API和SQL支持三种方式来编码动态表的更改:
- Append-only stream:只有进行插入操作时动态表才会通过发出插入的行将其转换为流。
- Retract stream:Retract stream包含两种类型的消息,insert和delete。通过将INSERT编码为add消息,DELETE编码为retract消息,UPDATE编码为已更新(前一行)的retract消息,以及add消息编码为已更新(新)行,动态表被转换为retract流。下图显示了将动态表转换为收缩流的过程。
- Upsert stream:upsert stream包含两种类型的消息,upsert消息和delete消息。转换为upsert stream的动态表需要惟一键(可以是复合键)。通过将INSERT和UPDATE编码为upsert消息,将DELETE编码为DELETE消息,将具有唯一键的动态表转换为流。为了正确应用消息,流消费操作符需要知道惟一的key属性。与Retract stream的主要区别在于更新是用单个消息编码的,因此更高效。下图显示了将动态表转换为upsert stream的过程。