什么时Table API 和Flink SQL
Flink对批处理和流处理,提供了统一的上层API
Flink本身时批流同一的处理框架,所以Table API和SQL就是批流统一的上层处理API
TableApI是一套内嵌在java和scala语言中的查询API,它允许以非常直观的方式组合来自一些关系运算符的查询
Flink的SQL支持基于实现了SQL标准的Apache Calcite
两种planner(old&blink)的区别
1.批流统一:Blink将批处理作业,视为流式处理的特殊情况。所以,blink不支持表和DataSet之间的转换,批处理作业将不转换为DataSet应用程序;而是跟流处理一样,转换为dataStream程序来处理。
2.因为批流统一,Blink planner也不支持BatchTableSource,而使用有界的StreamTableSource代替。
3.Blink planner只支持全新的目录,不支持已启用的ExternalCatalog。
4.旧planner和Blink planner的FilterableTableSource实现不兼容。旧的planner会把plannerExpressions下推到filterableTableSource中,而blink planner则会把Expressions下推。
5.基于字符串的键值配置选项仅适用于Blink planner。
6.PannerConfig在两个planner中的实现不同。
7.Blinkplanner会将多个sink优化在同一个DAG中(仅在TableEnvironment上受支持,而在StreamTableEnvironment上不受支持)而旧planner的优化总是将每一个sink放在一个新的DAG中,其中所有DAG彼此独立
8.旧的palnner不支持目录统计,而Blik planner支持。
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1);
//读取数据创建DataStream
val inputStream : DataStream[String] = env.readTextFile("/Users/oumanabuka/Desktop/cem_flink_test/src/main/resources/word.txt")
val dataStream:DataStream[SensorReading]=inputStream
.map(data=>{
val dataArray = data.split(",")
SensorReading(dataArray(0),dataArray(1).toLong,dataArray(2).toDouble)
})
//创建表执行环境
val tableEnv:StreamTableEnvironment = StreamTableEnvironment.create(env)
//基于数据流,转换成一张表,然后进行操作
val dataTable:Table = tableEnv.fromDataStream(dataStream)
//调用table Api 得到转换结果
val resultTable :Table = dataTable
.select("id, temperature")
.filter("id == 'sensor1'")
//或者直接写sql得到转换结果
//因为table没有在sql注册表 可以采用字符串+dataTable的形式 进行类似注册
val resultSqlTable :Table = tableEnv.sqlQuery(
"""
|select
|id ,temperature
|from
|""".stripMargin
+dataTable+
"""
|where id='sensor1'
|""".stripMargin
)
//转换回数据流,打印输出
// val resultStream: DataStream[(String,Double)] = resultTable.toAppendStream
val resultStream: DataStream[(String,Double)] = resultSqlTable.toAppendStream
resultStream.print()
resultTable.printSchema()
env.execute("table example job")
基本程序结构
Table API和SQL的程序结构,与流失处理的程序结构十分类似
//创建表执行环境
val tableEnv:StreamTableEnvironment = StreamTableEnvironment.create(env)
//创建一张表 用于读取数据 source
tableEnv.connect(..).createTemporaryTable("inputTable")
//创建一张表 用于输出数据 sink
tableEnv.connect(..).createTemporaryTable("outputTable")
//通过Table Api 或sql 进行计算
val result = tableEnv.from("inputTable").select(....)
val sqlResult= tableEnv.sqlQuery("select .....")
sqlResult.insertInto("outputTable")
创建表的执行环境 需要将flink流处理的执行环境传入
val tableEnv:StreamTableEnvironment = StreamTableEnvironment.create(env)
TableEnvironment是flink中集成Table APi 和SQL的核心概念,所有对表的操作都基于TableEnvironment
注册Catlog(类似表目录)
在Catlog中注册表
执行SQL查询
注册用户自定义函数(UDF)
创建各版本执行环境
//创建老版本的流式查询环境
val settings :EnvironmentSettings = EnvironmentSettings.newInstance()
.useOldPlanner()
.inStreamingMode()
.build()
val tableEnvironment :StreamTableEnvironment = StreamTableEnvironment.create(env,settings)
//创建老版本的批式查询环境
val batchEnv:ExecutionEnvironment = ExecutionEnvironment.getExecutionEnvironment
val batchTableEnvironment:BatchTableEnvironment = BatchTableEnvironment.create(batchEnv)
//创建blink版本的流式查询环境(blink版本已经批流统一了)
val blSettings = EnvironmentSettings.newInstance()
.useBlinkPlanner()
.inStreamingMode()
.build()
val blTableEnv = StreamTableEnvironment.create(env,blSettings)
//创建blink版本的批式查询环境(blink版本已经批流统一了)
val bsSettings = EnvironmentSettings.newInstance()
.useBlinkPlanner()
.inBatchMode()
.build()
val bbTableEnv = TableEnvironment.create(bsSettings)
TableEnvironment可以注册目录Catalog,并可以基于Catalog注册表
表(Table)是由一个“标识符”(identifier)来指定的 由3部分组成:
Catalog名 数据库名 和对象名
表可以是常规的,也可以是虚拟的(视图View)
常规表(Table)一般可以用来描述外部数据,比如文件,数据库表或消息队列的数据,也可以直接从DataStream转换而来
视图(View)可以从现有的表中创建,通常是tableAPI或者SQL查询的一个结果集
创建表
TableEnvironment可以调用.connect()方法,连接外部系统,并调用.createTemporaryTable()(1.10才有这个方法 1.9 用的是 registerTableSource 或者registerTableSink 注册)方法,在Catalog中注册表
通过withFormat()方法 定义数据格式化 withSchema()定义表结构
分别连接文件系统 和 kafka
//连接到文件系统(CSV)
val filePath="/Users/oumanabuka/Desktop/cem_flink_test/src/main/resources/word.txt"
val inputTable = tableEnv.connect(new FileSystem().path(filePath))
.withFormat(new OldCsv().fieldDelimiter(",")
.field("id", Types.STRING())
.field("temperature", Types.LONG())
.field("timestamp", Types.DOUBLE())) //定义读取数据之后的格式化方法
.withSchema(new Schema()
.field("id", Types.STRING())
.field("temperature", Types.LONG())
.field("timestamp", Types.DOUBLE())
)
.registerTableSource("test")
//转换成流打印输出
val sensorTable :Table = tableEnv.sqlQuery(
"""
|select
|*
|from
|test
|""".stripMargin)
sensorTable.toAppendStream[(String,Long,Double)].print()
//连接到kafka
val prop = new Properties()
prop.setProperty("bootstrap.servers", "127.0.0.1:9092")
prop.setProperty("group.id", "expand_group")
// prop.put("enable.auto.commit", "false");
// props.put("auto.offset.reset", "none");
// prop.put("auto.offset.reset", "earliest"); //当旧数据已经过期,不知道新数据从何处开始时使用
prop.put("session.timeout.ms", "30000");
prop.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
prop.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
tableEnv.connect(new Kafka()
.version("0.11")
.topic("sensor")
.properties(prop)
).withFormat(new OldCsv().fieldDelimiter(",")
.field("id", Types.STRING())
.field("temperature", Types.LONG())
.field("timestamp", Types.DOUBLE())) //定义读取数据之后的格式化方法
.withSchema(new Schema()
.field("id", Types.STRING())
.field("temperature", Types.LONG())
.field("timestamp", Types.DOUBLE())
)
.registerTableSource("test_kafka")
val sensorKafkaTable :Table = tableEnv.sqlQuery(
"""
|select
|*
|from
|test_kafka
|""".stripMargin)
sensorKafkaTable.toAppendStream[(String,Long,Double)].print()
表的输出
表的输出是通过将数据 写入tablesink来实现的
tablesink是一个通用接口,可以支持不同的文件格式,存储数据库和消息队列
输出表最直接的方法,就是通过Table.Insertinto()方法将一个Table写入注册过的TableSink中
注意 聚合操作的结果不能输出到文件 原因是 涉及到一个叫做更新模式的概念
也就是可以简单的理解 没有触发更新操作的 才能输出到文件
更新模式
对于流式查询,需要声明如何在表和外部连接器之间执行转换
与外部系统交换的消息类型,由更新模式(Update Mode)指定
1.追加(Append)模式
表只做插入操作,和外部连接器只交换插入(insert)消息
2.撤回(Retract)模式
表和外部连接器交换添加(Add)和撤回(Retract)消息 (即来了同key的需要改变 聚合值时先删除 之前的结果再查询新的结果)
插入操作(insert)编码为Add消息;删除(Delete)编码为Retract消息 更新(Update)编码为上一条的Retract和下一条的Add消息
更新插入(Upsert)模式
更新和插入都被编码为Upsert消息;删除编码为Delete消息(来了新的数据就直接在原key上 进行更新 而不是撤回再插入)
使用方式 在connect之后 inUpsertMode 但是 文件 kafka 都不支持 所以说 转成datastream 更加合适一点
连接到mysql 有一点不同 就是 flink并没有 jdbc连接器 需要通过 ddl的方式创建连接
val sinkDDL :String ="""
|create table jdbcOutputTable(
id varchar(20) not null ,
|cnt bigint not null
|) with (
|'connector.type'='jdbc',
|'connector.url'=://localhost:3306/test',
|'connector.table'='sesor_count',
|'connector.driver'='com.mysql.jdbc.Drivver',
|'connector.username'='root',
|'connector.password'='123456'
|)
""".stripMargin
tableEnv.sqlUpdate(sinkDDL)
aggResultSqlTable.insertinto("jdbcOutputTable")
将Table转换成DataStream
表可以转换为DataStream或DataSet,这样自定义流处理或批处理程序就可以继续在Table API
或sql查询 的结果上运行了
将表转换为DataStrem时或DataSet时,需要指定生成的数据,即要将表的每一行转换成的数据类型
表作为流式查询的结果,是动态更新的
转换有两种转换模式:追加(Appends)模式和撤回(Retract)模式
查看执行计划
Table ApI提供了一种机制来解释表的罗杰和优化查询计划
查看执行计划,可以通过TableEnvironment.EXPLAIN(TABLE)方法或TableEnvironment.explain()方法完成,返回一个字符串,描述三个计划
优化的逻辑查询计划
优化后的逻辑查询计划
实际执行计划
val explainnation:String = tableEnv.explain(resultTable)
println(explaintion)
flink 满足 流数据提供表查询 提出的概念
动态表(Dynamic Tables)
动态表是Flink对流数据的Table Api 和SQL 支持的核心概念
与表示批处理数据的静态表不同,动态表是随事件变化的
持续查询
动态表可以向静态的批处理表一样进行查询,查询一个动态表会产生持续查询
连续查询永远不会终止,并会生成另一个动态表
流式表查询的处理过程:
1.流被转换为动态表
2.对动态表计算连续查询,生成新的动态表
3.生成的动态表被转换回流
为了处理带有关系查询的流,必须先将其转换为表
从概念上讲,流的每个数据记录,都被解释为对结果表的插入(Insert)修改操作
Table Api 时间特性(Time Attributes)
基于时间的操作(比如Table API和SQL中窗口操作) 需要定义相关的时间语义和时间数据来源的信息
Table可以提供一个逻辑上的时间字段,用于在表处理程序中,指示时间和访问相应的时间戳
时间属性,可以是每个表schema的一部分。一旦定义了时间属性,它就可以作为一个字段引用,并且可以在基于时间的操作中使用
时间属性的行为类似于常规时间戳,可以访问,并且进行计算
定义处理时间(processingtime)
在定义schema期间,使用.proctime指定字段名定义处理时间字段
这个proctime属性只能通过附加逻辑字段,来扩展物理schema
val sensorTable = tableEnv.fromDataStream(dataStream,'id,'temperature,'timestamp,'pt.proctime)
//不使用Datastream 创建时 也可以在 wirhSchema时 指定
.filed("pt",Types.TIMEStamp(3)).proctime
//创建表的DDL时 直接 加一个字段(Blink支持)
pt AS PROCTIME()
定义事件时间(Event time)
//先指定是事件事件
env.setStreamTimeCharacterstic(TimeCharacterstic.EventTime)
//再在datastream设置好watermark 并指定以哪个字段为事件时间 和之前是一样的
.assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[sesorReading](Time.seconds(1)){
override def extractTimestamp(element:SensorReading):Long = element.timestamp*1000L
})
val sensorTable = tableEnv.fromDataStream(dataStream,'id,'temperature,'timestamp.rowtime as 'ts)
//不适用dataStream 也可以定义TableSchame时指定 并指定watermark
.withschame(new Schema()
.field("id",Types.STRING())
.field("timestamp",Types.BIGINT())
.rowtime(
new Rowtime()
.timestampsFromField("timestamp")
.watermarksPeriodicBounded(1000)
)
.field("temperature",DataTYpes.DOUBLE)
)
ddl中使用
rt as TO_TOMESTAMP(FROm_UNIXTIME(ex) ),
watermark for rt as rt - interval '1' second