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


前言

table API和SQL,原本是基于有界数据操作的(其实在流式处理的概念出来之前,我们一般的思维模式都是基于批量数据的操作,产生的概念也都是基于批量操作)。所以在流式处理中,我们要接受一些新的概念,理解一些与常规思维方式不同的思维


提示:以下是本篇文章正文内容,下面案例可供参考

一、流处理和关系代数(表,及 SQL)的区别

关系代数和流处理的区别
可以看到,其实关系代数(主要就是指关系型数据库中的表)和SQL,主要就是针对批处理的,这和流处理有天生的隔阂。流处理是一种新生事物,掌握这些概念不仅仅是掌握一个工具,也是一种全新的思考模式。

二、动态表(Dynamic Tables)

在关系型数据库里面,我们查表得到的结果是一个固定的值,但是在流数据当中,由于数据是连续不断的,我们对流数据做select操作,得到的结果也是随着新数据的到来会不停更新。

随着新数据的到来,我们可以不停的在之前基础上更新结果,这样得到的表,在flink当中叫做动态表。

动态变是flink对sql和table api支持的核心概念,和关系型数据库的静态表不同,动态表是随时间变换的。动态表可以像静态表一样进行查询,不过查询结果会一直变化,因为输入表也是动态的,在不断的变动之中。

三、流式持续查询的过程

下图显示了流、动态表和连续查询的关系:

动态表用于流式处理
流式持续查询的过程为:

  1. 流被转换为动态表。
  2. 对动态表计算连续查询,生成新的动态表。
  3. 生成的动态表被转换回流。

1.将流转换成表(Table)

为了处理带有关系查询的流,必须 将其转换为表。
从概念上讲,流的每个数据 记录 都被解释为对结果表的插入 Insert 修改。 因为流
式持续不断的,而且之前的输出结果无法改变。 本质上,我们 其实 是从一个 、 只 有 插入 操作
的 changelog(更新日志流, 来构建一个表)。
为了更好地说明动态表和持续查询的概念,我们来举一个具体的例子。比如,我们现在的输入数据,就是用户在网站上的访问行为,数据类型(Schema)如下:
[
user: VARCHAR, // 用户名
cTime: TIMESTAMP, // 访问某个URL的时间戳
url: VARCHAR // 用户访问的URL
]

下图显示了如何将访问URL事件流 ,或者叫点击事件流 (左侧)转换为表(右侧)。

将事件流转换为表
随着插入更多的访问事件流记录,生成的表将不断增长。

2.持续查询(Continuous Query)

持续查询,会在动态表上做计算处理,并作为结果生成新的动态表。与批处理查询不同,连续查询从不终止,并根据输入表上的更新更新其结果表。
在任何时间点,连续查询的结果在语义上,等同于在输入表的快照上,以批处理模式执行的同一查询的结果。
在下面的示例中,我们展示了对点击 事件流中 的一个 持续 查询。
这个Query很简单,是一个 分组聚合 做 count统计的 查询。它将用户字段上的 clicks表分组,并统计访问的 url数。图中 显示了随着时间的推移,当 clicks表被其他行更新时如何计算查询。

持续查询演示

3.将动态表转换成流

与常规的数据库表一样,动态表可以通过插入(Insert)、更新 Update)和删除 Delete
更改,进行持续的修改。将动态表转换为流或将其写入外部系统时,需要对这些更改进行编
码。 Flink的 Table API和 SQL支持三种方式对动态表的更改进行编码:

1.仅追加( Append only)流

仅通过插入(Insert)更改,来修改的动态表,可以直接转换为“仅追加”流。这个流中发出的数据,就是动态表中新增的每一行。

2.撤回( Retract)流

Retract流是包含两类消息的流,添加(Add)消息和撤回 (Retract)消息。动态表通过将INSERT 编码为 add消息、 DELETE 编码为 retract消息、 UPDATE编码为被更改行(前一行)的 retract消息和更新后行(新行)的 add消息,转换为 retract流。
下图显示了将动态表转换为Retract流的过程。

动态表转换为retract流
3.upsert(更新插入)流

Upsert流包含两种类型的消息: Upsert消息和 delete消息。转换为 upsert流的动态表
需要 有 唯一 的 键 key 。
通过将INSERT和 UPDATE更改编码为 upsert消息,将 DELETE更改编码为 DELETE消息,
就可以 将具有唯一键 Unique Key 的动态表转换为流。
下图显示了将动态表转换为upsert流的过程。

动态表转换为upsert(更新插入)流
这些概念我们之前都已提到过。需要注意 的是,在代码里将动态表转换为 DataStream时,仅支持
Append和 Retract流。 而向外部系统输出动态表的 TableSink接口 ,则可以有不同的实现,比如之前我们讲到的 ES,就可以有 Upsert模式 。

示例:pandas 是基于NumPy 的一种工具,该工具是为了解决数据分析任务而创建的。

四、时间特性

基于时间的操作(比如 Table API和 SQL中窗口操作 )),需要定义相关的时间语义和时间
数据 来源的信息。 所以, Table可以提供 一个逻辑上的时间字段 ,用于在表 处理 程序中 指
示时间和访问相应的时间戳。
时间属性,可以是每个表schema的一部分。一旦定义了时间属性,它就可以作为一个
字段引用,并且可以在基于时间的操作中使用。
时间属性的行为类似于常规时间戳,可以访问,并且 进行计算。

1.处理时间(Processing Time)

处理时间语义下,允许表处理程序根据机器的本地时间生成结果。它是时间的最简单概念。它既不需要提取时间戳,也不需要生成 watermark。
定义处理时间属性有三种方法:在DataStream转化时直接指定;在定义 Table Schema时指定;在创建表的 DDL中指定。

(1) DataStream转化成 Table时指定

由DataStream转换成表时,可以在后面指定字段名来定义 Schema。在 定义 Schema期
间, 可以 使用 .proctime,定义处理时间字段 。注意,这个proctime属性只能通过附加逻辑字段 ,来扩 展物理 schema。因此,只能在schema定义的末尾定义它。

// 定义好 DataStream 
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) })
// 将 DataStream转换为 Table,并指定时间字段 
val sensorTable = tableEnv.fromDataStream(dataStream, 'id, 'temperature, 'timestamp, 'pt.proctime)

(2) 定义 Table Schema时指定

这种方法其实也很简单,只要在定义Schema的时候,加上 一个新的字段,并指定成
proctime就可以了。
代码如下:

tableEnv.connect( 
new FileSystem().path("..\\sensor.txt")) 
.withFormat(new Csv())
 .withSchema(new Schema() 
 .field("id", DataTypes.STRING()) 
 .field("timestamp", DataTypes.BIGINT()) 
 .field("temperature", DataTypes.DOUBLE()) 
 .field("pt", DataTypes.TIMESTAMP(3)) 
 .proctime() // 指定 pt字段为处理时间
  ) // 定义表结构 
  .createTemporaryTable("inputTable") // 创建临时表

(3) 创建表的 DDL中指定
在创建表的DDL中,增加一个字段 并指定成 proctime 也可以指定当前的时间字段 。
代码如下:

val sinkDDL: String = 
""" 
|create table dataTable ( 
| id varchar(20) not null, 
| ts bigint, 
| temperature double, 
| pt AS PROCTIME()
 |) with (
  | 'connector.type' = 'filesystem', 
  | 'connector.path' = 'file:///D:\\..\\sensor.txt',
   | 'format.type' = 'csv'
    |)
     """.stripMargin 
     tableEnv.sqlUpdate(sinkDDL) // 执行 DDL

注意:运行这段 DDL,必须 使用 Blink Planner。

2.事件时间(Event Time)

事件时间语义,允许表处理程序根据每个记录中包含的时间生成结果。这样即使在有乱
序事件或者延迟事件时,也可以获得正确的结果。
为了处理无序事件,并区分流中的准时和迟到事件;Flink需要从事件数据中,提取时
间戳,并用来推进事件时间的进展( watermark)。

(1) DataStream转化成 Table时指定
在DataStream转换成 Table schema的定义期间,使用 .rowtime可以定义事件时间属性。注意,必须在转换的数据流中分配时间戳和 watermark。
在将数据流转换为表时,有两种定义时间属性的方法。根据指定的 .rowtime字段名是否存在于数据流的架构中,timestamp字段可以:
⚫ 作为新字段追加到 schema
⚫ 替换现有字段
在这两种情况下,定义的事件时间戳字段,都将保存DataStream中事件时间戳的值。

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)
  }) 
  .assignAscendingTimestamps(_.timestamp * 1000L) 
  // 将 DataStream转换为 Table,并指定时间字段 
  val sensorTable = tableEnv.fromDataStream(dataStream, 'id, 'timestamp.rowtime, 'temperature) 
  // 或者,直接追加字段 
  val sensorTable2 = tableEnv.fromDataStream(dataStream, 'id, 'temperature, 'timestamp, 'rt.rowtime)

(2) 定义 Table Schema时指定

这种方法只要在定义Schema的时候, 将事件时间 字段,并指定成 rowtime就可以了。
代码如下:

tableEnv.connect( new FileSystem().path("sensor.txt"))
.withFormat(new Csv()) 
.withSchema(new Schema() 
.field("id", DataTypes.STRING()) 
.field("timestamp", DataTypes.BIGINT()) 
.rowtime( new Rowtime() 
.timestampsFromField("timestamp") 
// 从字段中提取时间戳 
.watermarksPeriodicBounded(1000) 
// watermark延迟1秒 
) .field("temperature", DataTypes.DOUBLE()) ) 
// 定义表结构 
.createTemporaryTable("inputTable")
 // 创建临时表

(3) 创建表的 DDL中指定

事件时间属性是使用 CREATE TABLE DDL中的 WARDMARK语句定义的。 watermark语句 ,定义现有事件时间字段上的 watermark生成表达式,该表达式将事件时间字段标记为事件时间属性。
代码如下:

val sinkDDL: String = 
""" 
|create table dataTable ( 
| id varchar(20) not null, 
| ts bigint, 
| temperature double, 
| rt AS TO_TIMESTAMP( FROM_UNIXTIME(ts) ), 
| watermark for rt as rt - interval '1' second 
|) with (
| 'connector.type' = 'filesystem', 
| 'connector.path' = 'file:///D:\\..\\sensor.txt', 
| 'format.type' = 'csv' 
|) 
""".stripMargin
tableEnv.sqlUpdate(sinkDDL) // 执行 DDL

这里
FROM_UNIXTIME 是系统内置的时间函数,用来将一个整数(秒数)转换成
YYYY MM DD hh:mm:ss 格式(默认,也可以作为第二个 String参数传入)的日期时间
字符串( date time string);然后再用 TO_TIMESTAMP 将其转换成 Timestamp。

代码如下(示例):

import numpy as np

2.读入数据

代码如下(示例):

data = pd.read_csv(
    'https://labfile.oss.aliyuncs.com/courses/1283/adult.data.csv')
print(data.head())

该处使用的url网络请求的数据。


总结

批处理和流处理是不同的思维产物,这一章讲了流式处理的很多概念,主要包括动态表和时间,在学习流处理的框架时,除了学会代码怎么写,还要去理解流式处理的思维,用流的观点来看问题。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值