Flink SQL 和 Table API入门教程(二)

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

一、基本的程序结构

  和流式处理的程序结构类似,flinksql和table api也是这样的流程:首先创建环境,然后定义source、transform和sink。
// 创建表的执行环境
val tableEnv = ...
// 创建一张表,用于读取数据
tableEnv.connect(...).createTemporaryTable("inputTable")
// 注册一张表,用于把计算结果输出
tableEnv.connect(...).createTemporaryTable("outputTable")
// 通过 Table API 查询算子,得到一张结果表
val result = tableEnv.from("inputTable").select(...)
// 通过 SQL查询语句,得到一张结果表
val sqlResult = tableEnv.sqlQuery("SELECT ... FROM inputTable ...")
// 将结果表写入输出表中
result.insertInto("outputTable")

二、创建表环境

创建表环境
最简单的方式,就是基于流处理执行环境,调 create方法直接创建:

val tableEnv = StreamTableEnvironment.create(env)

表环境(TableEnvironment 是 flink中 集成 Table API & SQL的核心概念。 它 负责 :

⚫注册 catalog
⚫ 在内部 catalog 中注册表
⚫ 执行 SQL 查询
⚫ 注册用户自定义函数
⚫ 将 DataStream 或 DataSet 转换为表
⚫ 保存对 ExecutionEnvironment 或 StreamExecutionEnvironment 的引用

在创建TableEnv的时候,可以多传入一个 EnvironmentSettings或者 TableConfig参数,
可以用来配置 TableEnvironment的一些特性。比如,配置老版本的流式查询( Flink Streaming Query):

val settings = EnvironmentSettings.newInstance() 
.useOldPlanner() // 使用老版本planner 
.inStreamingMode() // 流处理模式 
.build() 
val tableEnv = StreamTableEnvironment.create(env, settings)

基于老版本的批处理环境(Flink Batch Query):

val batchEnv = ExecutionEnvironment.getExecutionEnvironment 
val batchTableEnv = BatchTableEnvironment.create(batchEnv)

基于blink版本的流处理环境(Blink Streaming Query):

val bsSettings = EnvironmentSettings
                 .newInstance() 
                 .useBlinkPlanner() 
                 .inStreamingMode()
                 .build() 
val bsTableEnv = StreamTableEnvironment.create(env, bsSettings)

val bbSettings = EnvironmentSettings
                 .newInstance() 
                 .useBlinkPlanner() 
                 .inBatchMode()
                 .build() 
val bbTableEnv = TableEnvironment.create(bbSettings)

三、在 Catalog中注册表

1.表( Table)的概念

TableEnvironment可以注册目录 Catalog 并可以基于 Catalog注册表。 它 会维护一个
Catalog Table表之间的 map。
表Table 是由一个“ 标识符 ”来指定的 由 3部分组成: Catalog名、数据库 database
名和对象名 (表名)。如果没有指定目录或数据库,就 使用当前 的 默认值。
表可以是常规的 Table 表),或者虚拟的( View,视图 。 常规表( Table)一般可以
用来描述外部数据,比如文件、数据库表或消息队列的数据,也可以直接从 DataStream转换而来 。 视图可以从现有的表中创建,通常是 table API或 者 SQL查询的 一个 结果。

2.连接到文件系统( Csv格式)

连接外部系统在Catalog中注册表, 直接调用 tableEnv.connect()就可以,里面参数要传
入一个 ConnectorDescriptor 也就是 connector描述器。 对于文件系统 的 connector而言, flink内部已经提供了,就叫做 FileSystem()。

tableEnv 
.connect( new FileSystem()
.path("sensor.txt")) // 定义表数据来源,外部连接 
.withFormat(new OldCsv()) // 定义从外部系统读取数据之后的格式化方法 
.withSchema( new Schema()
.field("id", DataTypes.STRING()) 
.field("timestamp", DataTypes.BIGINT()) 
.field("temperature", DataTypes.DOUBLE()) ) // 定义表结构 .createTemporaryTable("inputTable") // 创建临时表

这是旧版本的csv格式描述器。由于它是非标的,跟外部系统对接并不通用,所以将被
弃用(idea之类的编辑器调用这个api的时候会提示这个即将舍弃),以后会被一个符合 RFC 4180标准 的新 format描述器取代。新的描述器就叫 Csv(),但flink没有直接提供, 需要引入 依赖 flink csv:

<dependency> 
    <groupId>org.apache.flink</groupId> 
    <artifactId>flink-csv</artifactId> 
    <version>1.10.0</version> 
</dependency>

代码非常类似,只需要把withFormat里的 OldCsv改成 Csv就可以了。

3.连接到 Kafka

kafka的连接器 flink kafka connector中, 1.10版本的已经提供了 Table API的 支持。 我们
可以在 connect方法中直接传入一个 叫做 Kafka的 类,这就是 kafka连接器的描述器
ConnectorDescriptor。作为具有流式处理功能的框架,对接kafka可能是最常见的场景了。

tableEnv.connect( 
new Kafka() 
.version("0.11") // 定义kafka的版本 
.topic("sensor") // 定义主题 
.property("zookeeper.connect", "localhost:2181")
.property("bootstrap.servers", "localhost:9092") ) 
.withFormat(new Csv()) 
.withSchema(new Schema() 
.field("id", DataTypes.STRING()) 
.field("timestamp", DataTypes.BIGINT()) 
.field("temperature", DataTypes.DOUBLE()) 
) 
.createTemporaryTable("kafkaInputTable")

当然也可以连接到ElasticSearch、 MySql、 HBase、 Hive等外部系统,实现方式基本上是类似的。

四、表的查询

利用外部系统的连接器
connector,我们可以读写数据,并在环境的 Catalog中注册表。接下来就可以 对表做查询转换了。
Flink给我们提供了两种查询方式: Table API和 SQL。

1.Table API的调用

Table API是集成在 Scala和 Java语言内的查询 API。与 SQL不同, Table API的查询不会
用 字符串 表示,而是在宿主语言中一步一步调用完成 的。
Table API基于代表一张“表”的 Table类,并提供一整套操作处理的方法 API。这些方
法会返回一个新的 Table对象, 这个 对象 就 表示对输入表应用转换操作的结果。有些关系型转换操作,可以由多个方法调用组成,构成链式调用结构。例如table.select(…).filter(…),其中 select(…)表示选择 表中指定的 字段 filter(…)表示筛选条件 。
代码中的实现如下:

val sensorTable: Table = tableEnv.from("inputTable")
val resultTable: Table = senorTable 
.select("id, temperature") 
.filter("id ='sensor_1'")

2.SQL查询

    Flink的 SQL集成 基于 的是 ApacheCalcite,它实现了 SQL标准。在 Flink中,用 常规字符串 来定义 SQL查询语句 。 SQL 查询的结果, 是一个新的 Table。

代码实现如下:

val resultSqlTable: Table = tableEnv
.sqlQuery("select id, temperature from inputTable where id ='sensor_1'")

或者:

val resultSqlTable: Table = tableEnv.sqlQuery( 
""" 
|select id, temperature 
|from inputTable 
|where id = 'sensor_1' 
""".stripMargin)

当然,也可以加上聚合操作,比如我们统计每个 sensor温度数据出现的个数,做个 count统计:

val aggResultTable = sensorTable 
.groupBy('id) 
.select('id, 'id.count as 'count)

SQL的实现:

val aggResultSqlTable = tableEnv.sqlQuery("select id, count(id) as cnt from inputTable group by id")

这里Table API里指定 的字段,前面加了一个单引号 ’,这是 Table API中定义的 Expression类型的写法,可以很方便地表示一个表中的字段。字段可以直接全部 用双引号引起来,也可以用半边单引号 +字段名的方式。以后的代码中,一般都用后一种形式。

五、表的查询将 DataStream 转换成表

Flink允许我们把 Table和 DataStream做转换:我们可以基于一个 DataStream,先流式
地读取数据源,然后 map成样例类,再把它转成 Table。 Table的列字段( column fields),就是样例类里的字段, 这样 就不用再 麻烦地定义 schema了。

1.代码实现

代码中实现非常简单,直接用 tableEnv.fromDataStream()就可以了。 默认转换后的 Table schema 和 DataStream 中的字段定义一一对应,也可以单独指定出来 。
这就允许我们更换字段的顺序、重命名,或者只选取某些字段出 来, 相当于做了一次
map操作(或者 Table API的 select操作) 。

val inputStream: DataStream[String] = env.readTextFile("sensor.txt")
val dataStream: DataStream[SensorReading] = inputStream
.map(data => {
val dataArray = data.split(",")
SensorReading(dataArray(0), dataArray(1).toLong, dataArray(2).toDouble) 
})
val sensorTable: Table = tableEnv.fromDataStream(dataStream) 
val sensorTable2 = tableEnv.fromDataStream(dataStream, 'id, 'timestamp as 'ts)

2.数据类型与 Table schema的对应

在上节的例子中,DataStream 中的数据类型,与表的 Schema 之间的对应关系 是按照 样例类中的 字段名来对应的( name based mapping),所以 还 可以 用 as做 重命名。
另外一种对应方式是,直接按照字段的位置来对应(position based mapping),对应的
过程中,就可以 直接 指定新的字段名了。

基于名称的对应:

val sensorTable = tableEnv.fromDataStream(dataStream, 'timestamp as 'ts, 'id as 'myId, 'temperature)

基于位置的对应:

val sensorTable = tableEnv.fromDataStream(dataStream, 'myId, 'ts)

Flink的 DataStream和 DataSet API支持多种类型。
组合类型,比如 元组(内置 Scala和 Java元组)、 POJO、 Scala case类和 Flink的 Row类型等 ,允许具有多个字段的嵌套数据结构,这些字段可以在 Table的 表达式中访问。其他类型 ,则 被视为原子类型。
元组类型和原子类型,一般用位置对应会好一些;如果非要用名称对应,也是可以的:
元组类型,默认的名称是 “1”, “ 而原子类型,默认名称是 ”f0”。

六、创建临时视图

创建临时视图的第一种方式,就是直接从DataStream转换而来。同样,可以直接对应
字段转换;也可以在转换的时候,指定相应的字段。
代码如下:

tableEnv.createTemporaryView("sensorView", dataStream) tableEnv.createTemporaryView("sensorView", dataStream, 'id, 'temperature, 'timestamp as 'ts)

另外,当然还可以基于 Table创建视图:

tableEnv.createTemporaryView("sensorView", sensorTable)

View和 Table的 Schema完全相同。事实上,在 Table API中,可以认为 View和 Table是等价的。

七、输出表

表的输出,是通过将数据写入TableSink 来实现的 。 TableSink 是一个通用接口,可以
支持不同的文件格式、存储数据库和消息队列 。
具体实现,输出表最直接的方法,就是通过 Table.insertInto() 方法将一个 Table 写入
注册过的 TableSink 中 。

1.输出到文件

代码如下:

// 注册输出表
tableEnv.connect( new FileSystem().path("…\\resources\\out.txt") )
// 定义到文件系统的连接
.withFormat(new Csv())
// 定义格式化方法,Csv格式
.withSchema(new Schema()
.field("id", DataTypes.STRING())
.field("temp", DataTypes.DOUBLE()) ) 
// 定义表结构
.createTemporaryTable("outputTable")
// 创建临时表
resultSqlTable.insertInto("outputTable")

2.更新模式

在流处理过程中,表的处理不像传统定义的那样简单。
对于流式查询,需要声明如何在表和外部处理器之间执行转换。与外部系统交换的消息类型,由更新模式指定。

flink table api中的更新模式有以下三种:

(1)追加模式(Append Mode)
表和外部连接器只交换插入消息

(2)撤回模式(Retracet Mode)

在撤回模式下, 表和外部连接器交换 的是: 添加 Add)和撤回 Retract)消 息。
⚫ 插入( Insert)会被 编码为添加消息;
⚫ 删除( Delete)则 编码为 撤回消息;
⚫ 更新( Update 则会编码为,已更新行(上一行)的撤回 消息 ,和更新 行 (新行
的添加消息。
在此模式下,不能定义key,这一点跟 upsert模式 完全不同。

(3)更新插入模式(Upsert Mode)

在Upsert模式下,动态表和外部连接器交换 Upsert和 Delete消息。
这个模式需要一个唯一的key,通过这个 key可以传递 更新 消息。为了正确应用消息,
外部连接器需要知道这个唯一 key的 属性。
⚫ 插入 Insert 和更新 Update)都 被编码为 Upsert消息;
⚫ 删除 Delete)编码为 Delete信息 。
这种模式和Retract模式的主要区别在于, Update操作是用单个消息编码的,所以 效率
会 更高。

3.输出到kafka

除了输出到文件,flink也可以输出到kafka。flink可以很方便实现kafka进,kafka出。

// 输出到 kafka 
tableEnv.connect( new Kafka()
.version("0.11") 
.topic("sinkTest") 
.property("zookeeper.connect", "localhost:2181") 
.property("bootstrap.servers", "localhost:9092") ) 
.withFormat( new Csv() ) 
.withSchema( new Schema() 
.field("id", DataTypes.STRING()) 
.field("temp", DataTypes.DOUBLE()) ) 
.createTemporaryTable("kafkaOutputTable")
resultTable.insertInto("kafkaOutputTable")

4.输出到ElasticSearch

ElasticSearch的 connector可以在 upsert update+insert,更新插入 模式下操作,这样
就可以使用 Query定义的键 key 与外部系统交换 UPSERT/DELETE消息。
另外,对于“仅追加”( append only)的查询 connector还可以在 append 模式下操作,
这样就可以与外部系统只交换 insert消息。
es目前支持的数据格式,只有 Json,而 flink本身并没有对应的支持,所以还需要引入
依赖:

<dependency> 
   <groupId>org.apache.flink</groupId> 
   <artifactId>flink-json</artifactId> 
   <version>1.10.0</version> 
</dependency>
// 输出到es 
tableEnv.connect( new Elasticsearch()
.version("6") 
.host("localhost", 9200, "http") 
.index("sensor") 
.documentType("temp") ) .inUpsertMode() // 指定是 Upsert 模式
.withFormat(new Json())
.withSchema( new Schema() 
.field("id", DataTypes.STRING()) 
.field("count", DataTypes.BIGINT()) ) 
.createTemporaryTable("esOutputTable") 
aggResultTable.insertInto("esOutputTable")

5.输出到 MySql

Flink专门为 Table API的 jdbc连接提供了 flink jdbc连接器,我们需要先引 入依赖:

<dependency> 
     <groupId>org.apache.flink</groupId>
     <artifactId>flink-jdbc_2.11</artifactId>
      <version>1.10.0</version>
 </dependency>

jdbc连接的代码实现比较特殊,因为没有对应的 java/scala类实现 ConnectorDescriptor
所以不能直接 tableEnv.connect()。不过 Flink SQL留下了执行 DDL的接口 tableEnv.sqlUpdate()。
对于jdbc的创建表 操作,天生就适合直接写 DDL来实现,所以我们的代码可以这样写:

// 输出到 Mysql
val sinkDDL: String =
"""
|create table jdbcOutputTable (
| id varchar(20) not null,
| cnt bigint not null
|) with (
| 'connector.type' = 'jdbc',
| 'connector.url' = 'jdbc:mysql://localhost:3306/test',
| 'connector.table' = 'sensor_count',
| 'connector.driver' = 'com.mysql.jdbc.Driver',
| 'connector.username' = 'root',
| 'connector.password' = '123456'
|)
""".stripMargin
tableEnv.sqlUpdate(sinkDDL) 
aggResultSqlTable.insertInto("jdbcOutputTable")

八、将表转换成 DataStream

表可以转换为DataStream或 DataSet。这样,自定义流处理或批处理程序就可以继续在
Table API或 SQL查询的结果上运行了。
将表转换为
DataStream或 DataSet时,需要指定生成的数据类型,即要将表的每一行转
换成的数据类型。通常,最方便的转换类型就是 Row。当然,因为结果的所有字段类型 都是
明确的,我们也经常会用元组类型来表示。
表作为流式查询的结果,是
动态更新 的 。 所以, 将这种动态查询转换成的数据流 ,同样
需要对表的更新 操作进行编码,进而有不同的转换模式 。
Table API中表到 DataStream有 两种模式:
⚫ 追加模式( Append Mode)用于表只会被插入(
Insert)操作更改的场景 。
⚫ 撤回模式( Retract Mode)用于任何场景。有些类似于更新模式中 Retract模式,它只有 Insert和 Delete两类操作。得到的数据会增加一个Boolean类型的标识位(返回的第一个字段),用它来表示到底是新增的数据( Insert),还是被删除的数据(老数据 Delete) 。

val resultStream: DataStream[Row] = tableEnv.toAppendStream[Row](resultTable)
 val aggResultStream: DataStream[(Boolean, (String, Long))] = tableEnv.toRetractStream[(String, Long)](aggResultTable) 
 resultStream.print("result") 
 aggResultStream.print("aggResult")

所以,没有经过
groupby之类聚合操作,可以 直接 用 toAppendStream 来转换;而 如果
经过了聚合, 有更新操作, 一般 就必须用 toRetractDstream。

九、Query的解释和执行

Table API提供了一种机制来解释( Explain)计算表的逻辑和优化查询计划。这是通过
TableEnvironment.explain table)方法或 TableEnvironment.explain()方法完成的。
explain方法会返回一个字符串,描述三个计划:
⚫ 未优化的逻辑查询计划
⚫ 优化后的逻辑查询计划
⚫ 实际执行计划
我们可以在代码中查看执行计划:

val explaination: String = tableEnv.explain(resultTable) println(explaination)

Query的解释和执行过程,老 planner和 blink planner大体是一致的,又有所不同。整
体来讲, Query都会表示成一个逻辑查询计划,然后分两步解释:

  1. 优化查询计划
  2. 解释成 DataStream 或者 DataSet程序
    而Blink版本是批流统一的,所以所有的 Query,只会被解释成 DataStream程序;另外
    在批处理环境 TableEnvironment下, Blink版本要到 tableEnv.execute()执行调用才开始解释。

这篇内容比较多,需要多去动手运行代码才能看到效果,吃透这一章的内容就可以用flink做一些简单常用的操作了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值