Apache Flink 漫谈系列(11) - Temporal Table JOIN

开发 开发工具
在《Apache Flink 漫谈系列 - JOIN LATERAL》中提到了Temporal Table JOIN,本篇就向大家详细介绍什么是Temporal Table JOIN。

一、什么是Temporal Table

在《Apache Flink 漫谈系列 - JOIN LATERAL》中提到了Temporal Table JOIN,本篇就向大家详细介绍什么是Temporal Table JOIN。

ANSI-SQL 2011 中提出了Temporal 的概念,Oracle,SQLServer,DB2等大的数据库厂商也先后实现了这个标准。Temporal Table记录了历史上任何时间点所有的数据改动,Temporal Table的工作流程如下:

emporal Table

上图示意Temporal Table具有普通table的特性,有具体独特的DDL/DML/QUERY语法,时间是其核心属性。历史意味着时间,意味着快照Snapshot。

二、ANSI-SQL 2011 Temporal Table示例

我们以一个DDL和一套DML示例说明Temporal Table的原理,DDL定义PK是可选的,下面的示例我们以不定义PK的为例进行说明:

1. DDL 示例

  1. CREATE TABLE Emp 
  2. ENo INTEGER, 
  3. Sys_Start TIMESTAMP(12) GENERATED 
  4. ALWAYS AS ROW Start, 
  5. Sys_end TIMESTAMP(12) GENERATED 
  6. ALWAYS AS ROW END, 
  7. EName VARCHAR(30), 
  8. PERIOD FOR SYSTEM_TIME (Sys_Start,Sys_end) 
  9. ) WITH SYSTEM VERSIONING 

2. DML 示例

(1) INSERT

  1. INSERT INTO Emp (ENo, EName) VALUES (22217, 'Joe') 

说明: 其中Sys_Start和Sys_End是数据库系统默认填充的。

(2) UPDATE

  1. UPDATE Emp SET EName = 'Tom' WHERE ENo = 22217 

说明: 假设是在 2012-02-03 10:00:00 执行的UPDATE,执行之后上一个值"Joe"的Sys_End值由9999-12-31 23:59:59 变成了 2012-02-03 10:00:00, 也就是下一个值"Tom"生效的开始时间。可见我们执行的是UPDATE但是数据库里面会存在两条数据,数据值和有效期不同,也就是版本不同。

(3) DELETE (假设执行DELETE之前的表内容如下)

  1. DELETE FROM Emp WHERE ENo = 22217 

说明: 假设我们是在2012-06-01 00:00:00执行的DELETE,则Sys_End值由9999-12-31 23:59:59 变成了 2012-06-01 00:00:00, 也就是在执行DELETE时候没有真正的删除符合条件的行,而是系统将符合条件的行的Sys_end修改为执行DELETE的操作时间。标识数据的有效期到DELETE执行那一刻为止。

(4) SELECT

  1. SELECT ENo,EName,Sys_Start,Sys_End FROM Emp 
  2. FOR SYSTEM_TIME AS OF TIMESTAMP '2011-01-02 00:00:00' 

说明: 这个查询会返回所有Sys_Start <= 2011-01-02 00:00:00 并且 Sys_end > 2011-01-02 00:00:00 的记录。

三、SQLServer Temporal Table 示例

1. DDL

  1. CREATE TABLE Department 
  2. DeptID int NOT NULL PRIMARY KEY CLUSTERED 
  3. , DeptName varchar(50) NOT NULL 
  4. , ManagerID INT NULL 
  5. , ParentDeptID int NULL 
  6. , SysStartTime datetime2 GENERATED ALWAYS AS ROW Start NOT NULL 
  7. , SysEndTime datetime2 GENERATED ALWAYS AS ROW END NOT NULL 
  8. , PERIOD FOR SYSTEM_TIME (SysStartTime,SysEndTime) 
  9. WITH (SYSTEM_VERSIONING = ON); 

执行上面的语句,在数据库会创建当前表和历史表,如下图:

Department 显示是有版本控制的,历史表是默认的名字,我也可以指定名字如:SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.DepartmentHistory)。

2. DML

(1) INSERT - 插入列不包含SysStartTime和SysEndTime列

  1. INSERT INTO [dbo].[Department] ([DeptID] ,[DeptName] ,[ManagerID] ,[ParentDeptID]) 
  2. VALUES(10, 'Marketing', 101, 1); 

VALUES(10, 'Marketing', 101, 1);

执行之后我们分别查询当前表和历史表,如下图:

我们***条INSERT语句数据值的有效时间是操作那一刻2018-06-06 05:50:20.7913985 到永远 9999-12-31 23:59:59.9999999,但这时刻历史表还没有任何信息。我们接下来进行更新操作。

(2) UPDATE

  1. UPDATE [dbo].[Department] SET [ManagerID] = 501 WHERE [DeptID] = 10 

执行之后当前表信息会更新并在历史表里面产生一条历史信息,如下:

 

注意当前表的SysStartTime意见发生了变化,历史表产生了一条记录,SyStartTIme是原当前表记录的SysStartTime,SysEndTime是当前表记录的SystemStartTime。我们再更新一次:

  1. UPDATE [dbo].[Department] SET [ManagerID] = 201 WHERE [DeptID] = 10 

到这里我们了解到SQLServer里面关于Temporal Table的逻辑是有当前表和历史表来存储数据,并且数据库内部以StartTime和EndTime的方式管理数据的版本。

(3) SELECT

  1. SELECT [DeptID], [DeptName], [SysStartTime],[SysEndTime] 
  2. FROM [dbo].[Department] 
  3. FOR SYSTEM_TIME AS OF '2018-06-06 05:50:21.0000000' ; 

SELECT语句查询的是Department的表,实际返回的数据是从历史表里面查询出来的,查询的底层逻辑就是 SysStartTime <= '2018-06-06 05:50:21.0000000' and SysEndTime > '2018-06-06 05:50:21.0000000' 。

四、Apache Flink Temporal Table

我们不止一次的提到Apache Flink遵循ANSI-SQL标准,Apache Flink中Temporal Table的概念也源于ANSI-2011的标准语义,但目前的实现在语法层面和ANSI-SQL略有差别,上面看到ANSI-2011中使用FOR SYSTEM_TIME AS OF的语法,目前Apache Flink中使用 LATERAL TABLE(TemporalTableFunction)的语法。这一点后续需要推动社区进行改进。

1. 为啥需要 Temporal Table

我们以具体的查询示例来说明为啥需要Temporal Table,假设我们有一张实时变化的汇率表(RatesHistory),如下:

RatesHistory代表了Yen汇率(Yen汇率为1),是不断变化的Append only的汇率表。例如,Euro兑Yen汇率从09:00至10:45的汇率为114。从10点45分到11点15分是116。

假设我们想在10:58输出所有当前汇率,我们需要以下SQL查询来计算结果表:

  1. SELECT * 
  2. FROM RatesHistory AS r 
  3. WHERE r.rowtime = ( 
  4. SELECT MAX(rowtime) 
  5. FROM RatesHistory AS r2 
  6. WHERE rr2.currency = r.currency 
  7. AND r2.rowtime <= '10:58'); 

相应Flink代码如下:

  • 定义数据源-genRatesHistorySource
    1. def genRatesHistorySource: CsvTableSource = { 
    2.  
    3. val csvRecords = Seq
    4. "rowtime ,currency ,rate", 
    5. "09:00:00 ,US Dollar , 102", 
    6. "09:00:00 ,Euro , 114", 
    7. "09:00:00 ,Yen , 1", 
    8. "10:45:00 ,Euro , 116", 
    9. "11:15:00 ,Euro , 119", 
    10. "11:49:00 ,Pounds , 108" 
    11. // 测试数据写入临时文件 
    12. val tempFilePath = 
    13. writeToTempFile(csvRecords.mkString("$"), "csv_source_", "tmp") 
    14.  
    15. // 创建Source connector 
    16. new CsvTableSource( 
    17. tempFilePath, 
    18. Array("rowtime","currency","rate"), 
    19. Array( 
    20. Types.STRING,Types.STRING,Types.STRING 
    21. ), 
    22. fieldDelim = ","
    23. rowDelim = "$"
    24. ignoreFirstLine = true
    25. ignoreComments = "%" 
    26. def writeToTempFile( 
    27. contents: String, 
    28. filePrefix: String, 
    29. fileSuffix: String, 
    30. charset: String = "UTF-8"): String = { 
    31. val tempFile = File.createTempFile(filePrefix, fileSuffix) 
    32. val tmpWriter = new OutputStreamWriter(new FileOutputStream(tempFile), charset) 
    33. tmpWriter.write(contents) 
    34. tmpWriter.close() 
    35. tempFile.getAbsolutePath} 
  • 主程序代码
    1. def main(args: Array[String]): Unit = { 
    2. // Streaming 环境 
    3. val env = StreamExecutionEnvironment.getExecutionEnvironment 
    4. val tEnv = TableEnvironment.getTableEnvironment(env) 
    5.  
    6. //方便我们查出输出数据 
    7. env.setParallelism(1) 
    8.  
    9. val sourceTableName = "RatesHistory" 
    10. // 创建CSV source数据结构 
    11. val tableSource = CsvTableSourceUtils.genRatesHistorySource 
    12. // 注册source 
    13. tEnv.registerTableSource(sourceTableName, tableSource) 
    14.  
    15. // 注册retract sink 
    16. val sinkTableName = "retractSink" 
    17. val fieldNames = Array("rowtime", "currency", "rate") 
    18. val fieldTypes: Array[TypeInformation[_]] = Array(Types.STRING, Types.STRING, Types.STRING) 
    19.  
    20. tEnv.registerTableSink( 
    21. sinkTableName, 
    22. fieldNames, 
    23. fieldTypes, 
    24. new MemoryRetractSink) 
    25.  
    26. val SQL = 
    27. ""
    28. |SELECT * 
    29. |FROM RatesHistory AS r 
    30. |WHERE r.rowtime = ( 
    31. | SELECT MAX(rowtime) 
    32. | FROM RatesHistory AS r2 
    33. | WHERE rr2.currency = r.currency 
    34. | AND r2.rowtime <= '10:58:00' ) 
    35. """.stripMargin 
    36.  
    37. // 执行查询 
    38. val result = tEnv.SQLQuery(SQL) 
    39.  
    40. // 将结果插入sink 
    41. result.insertInto(sinkTableName) 
    42. env.execute() 
  • 执行结果如下图:

结果表格化一下:

Temporal Table的概念旨在简化此类查询,加速它们的执行。Temporal Table是Append Only表上的参数化视图,它把Append Only的表变化解释为表的Changelog,并在特定时间点提供该表的版本(时间版本)。将Applend Only表解释为changelog需要指定主键属性和时间戳属性。主键确定覆盖哪些行,时间戳确定行有效的时间,也就是数据版本,与上面SQL Server示例的有效期的概念一致。

在上面的示例中,currency是RatesHistory表的主键,而rowtime是timestamp属性。

2. 如何定义Temporal Table

在Apache Flink中扩展了TableFunction的接口,在TableFunction接口的基础上添加了时间属性和pk属性。

(1) 内部TemporalTableFunction定义如下:

  1. class TemporalTableFunction private( 
  2. @transient private val underlyingHistoryTable: Table, 
  3. // 时间属性,相当于版本信息 
  4. private val timeAttribute: Expression, 
  5. // 主键定义 
  6. private val primaryKey: String, 
  7. private val resultType: RowTypeInfo) 
  8. extends TableFunction[Row] { 
  9. ...} 

(2) 用户创建TemporalTableFunction方式

在Table中添加了createTemporalTableFunction方法,该方法需要传入时间属性和主键,接口定义如下:

  1. // Creates TemporalTableFunction backed up by this table as a history table. 
  2.  
  3. def createTemporalTableFunction( 
  4. timeAttribute: Expression, 
  5. primaryKey: Expression): TemporalTableFunction = { 
  6. ...} 

用户通过如下方式调用就可以得到一个TemporalTableFunction的实例,代码如下:

  1. val tab = ... 
  2. val temporalTableFunction = tab.createTemporalTableFunction('time, 'pk) 
  3. ... 

3. 案例代码

(1) 需求描述

假设我们有一张订单表Orders和一张汇率表Rates,那么订单来自于不同的地区,所以支付的币种各不一样,那么假设需要统计每个订单在下单时候Yen币种对应的金额。

(2) Orders 数据

(3) Rates 数据

(4) 统计需求对应的SQL

  1. SELECT o.currency, o.amount, r.rate 
  2. o.amount * r.rate AS yen_amount 
  3. FROM 
  4. Orders AS o, 
  5. LATERAL TABLE (Rates(o.rowtime)) AS r 
  6. WHERE r.currency = o.currency 

(5) 预期结果

4. Without connnector 实现代码

  1. object TemporalTableJoinTest { 
  2. def main(args: Array[String]): Unit = { 
  3. val env = StreamExecutionEnvironment.getExecutionEnvironment 
  4. val tEnv = TableEnvironment.getTableEnvironment(env) 
  5. env.setParallelism(1) 
  6. // 设置时间类型是 event-time env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime) 
  7. // 构造订单数据 
  8. val ordersData = new mutable.MutableList[(Long, String, Timestamp)] 
  9. ordersData.+=((2L, "Euro", new Timestamp(2L))) 
  10. ordersData.+=((1L, "US Dollar", new Timestamp(3L))) 
  11. ordersData.+=((50L, "Yen", new Timestamp(4L))) 
  12. ordersData.+=((3L, "Euro", new Timestamp(5L))) 
  13.  
  14. //构造汇率数据 
  15. val ratesHistoryData = new mutable.MutableList[(String, Long, Timestamp)] 
  16. ratesHistoryData.+=(("US Dollar", 102L, new Timestamp(1L))) 
  17. ratesHistoryData.+=(("Euro", 114L, new Timestamp(1L))) 
  18. ratesHistoryData.+=(("Yen", 1L, new Timestamp(1L))) 
  19. ratesHistoryData.+=(("Euro", 116L, new Timestamp(5L))) 
  20. ratesHistoryData.+=(("Euro", 119L, new Timestamp(7L))) 
  21.  
  22. // 进行订单表 event-time 的提取 
  23. val orders = env 
  24. .fromCollection(ordersData) 
  25. .assignTimestampsAndWatermarks(new OrderTimestampExtractor[Long, String]()) 
  26. .toTable(tEnv, 'amount, 'currency, 'rowtime.rowtime) 
  27.  
  28. // 进行汇率表 event-time 的提取 
  29. val ratesHistory = env 
  30. .fromCollection(ratesHistoryData) 
  31. .assignTimestampsAndWatermarks(new OrderTimestampExtractor[String, Long]()) 
  32. .toTable(tEnv, 'currency, 'rate, 'rowtime.rowtime) 
  33.  
  34. // 注册订单表和汇率表 
  35. tEnv.registerTable("Orders", orders) 
  36. tEnv.registerTable("RatesHistory", ratesHistory) 
  37. val tab = tEnv.scan("RatesHistory"); 
  38. // 创建TemporalTableFunction 
  39. val temporalTableFunction = tab.createTemporalTableFunction('rowtime, 'currency) 
  40. //注册TemporalTableFunction 
  41. tEnv.registerFunction("Rates",temporalTableFunction) 
  42.  
  43. val SQLQuery = 
  44. ""
  45. |SELECT o.currency, o.amount, r.rate, 
  46. | o.amount * r.rate AS yen_amount 
  47. |FROM 
  48. | Orders AS o, 
  49. | LATERAL TABLE (Rates(o.rowtime)) AS r 
  50. |WHERE r.currency = o.currency 
  51. |""".stripMargin 
  52.  
  53. tEnv.registerTable("TemporalJoinResult", tEnv.SQLQuery(SQLQuery)) 
  54.  
  55. val result = tEnv.scan("TemporalJoinResult").toAppendStream[Row] 
  56. // 打印查询结果 
  57. result.print() 
  58. env.execute() 

在运行上面代码之前需要注意上面代码中对EventTime时间提取的过程,也就是说Apache Flink的TimeCharacteristic.EventTime 模式,需要调用assignTimestampsAndWatermarks方法设置EventTime的生成方式,这种方式也非常灵活,用户可以控制业务数据的EventTime的值和WaterMark的产生,WaterMark相关内容可以查阅《Apache Flink 漫谈系列(03) - Watermark》。 在本示例中提取EventTime的完整代码如下:

  1. import java.SQL.Timestamp 
  2.  
  3. import org.apache.flink.streaming.api.functions.timestamps.BoundedOutOfOrdernessTimestampExtractor 
  4. import org.apache.flink.streaming.api.windowing.time.Time 
  5.  
  6. class OrderTimestampExtractor[T1, T2] 
  7. extends BoundedOutOfOrdernessTimestampExtractor[(T1, T2, Timestamp)](Time.seconds(10)) { 
  8. override def extractTimestamp(element: (T1, T2, Timestamp)): Long = { 
  9. element._3.getTime 

查看运行结果:

5. With CSVConnector 实现代码

在实际的生产开发中,都需要实际的Connector的定义,下面我们以CSV格式的Connector定义来开发Temporal Table JOIN Demo。

(1) genEventRatesHistorySource

  1. def genEventRatesHistorySource: CsvTableSource = { 
  2.  
  3. val csvRecords = Seq
  4. "ts#currency#rate", 
  5. "1#US Dollar#102", 
  6. "1#Euro#114", 
  7. "1#Yen#1", 
  8. "3#Euro#116", 
  9. "5#Euro#119", 
  10. "7#Pounds#108" 
  11. // 测试数据写入临时文件 
  12. val tempFilePath = 
  13. FileUtils.writeToTempFile(csvRecords.mkString(CommonUtils.line), "csv_source_rate", "tmp") 
  14.  
  15. // 创建Source connector 
  16. new CsvTableSource( 
  17. tempFilePath, 
  18. Array("ts","currency","rate"), 
  19. Array( 
  20. Types.LONG,Types.STRING,Types.LONG 
  21. ), 
  22. fieldDelim = "#"
  23. rowDelim = CommonUtils.line, 
  24. ignoreFirstLine = true
  25. ignoreComments = "%" 
  26. )} 

(2) genRatesOrderSource

  1. def genRatesOrderSource: CsvTableSource = { 
  2.  
  3. val csvRecords = Seq
  4. "ts#currency#amount", 
  5. "2#Euro#10", 
  6. "4#Euro#10" 
  7. // 测试数据写入临时文件 
  8. val tempFilePath = 
  9. FileUtils.writeToTempFile(csvRecords.mkString(CommonUtils.line), "csv_source_order", "tmp") 
  10.  
  11. // 创建Source connector 
  12. new CsvTableSource( 
  13. tempFilePath, 
  14. Array("ts","currency", "amount"), 
  15. Array( 
  16. Types.LONG,Types.STRING,Types.LONG 
  17. ), 
  18. fieldDelim = "#"
  19. rowDelim = CommonUtils.line, 
  20. ignoreFirstLine = true
  21. ignoreComments = "%" 

(3) 主程序代码

  1. /* 
  2.  * Licensed to the Apache Software Foundation (ASF) under one 
  3.  * or more contributor license agreements.  See the NOTICE file 
  4.  * distributed with this work for additional information 
  5.  * regarding copyright ownership.  The ASF licenses this file 
  6.  * to you under the Apache License, Version 2.0 (the 
  7.  * "License"); you may not use this file except in compliance 
  8.  * with the License.  You may obtain a copy of the License at 
  9.  * 
  10.  *     http://www.apache.org/licenses/LICENSE-2.0 
  11.  * 
  12.  * Unless required by applicable law or agreed to in writing, software 
  13.  * distributed under the License is distributed on an "AS IS" BASIS, 
  14.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
  15.  * See the License for the specific language governing permissions and 
  16.  * limitations under the License. 
  17.  */ 
  18.  
  19. package org.apache.flink.book.connectors 
  20.  
  21. import java.io.File 
  22.  
  23. import org.apache.flink.api.common.typeinfo.{TypeInformation, Types} 
  24. import org.apache.flink.book.utils.{CommonUtils, FileUtils} 
  25. import org.apache.flink.table.sinks.{CsvTableSink, TableSink} 
  26. import org.apache.flink.table.sources.CsvTableSource 
  27. import org.apache.flink.types.Row 
  28.  
  29. object CsvTableSourceUtils { 
  30.  
  31.   def genWordCountSource: CsvTableSource = { 
  32.     val csvRecords = Seq
  33.       "words", 
  34.       "Hello Flink", 
  35.       "Hi, Apache Flink", 
  36.       "Apache FlinkBook" 
  37.     ) 
  38.     // 测试数据写入临时文件 
  39.     val tempFilePath = 
  40.       FileUtils.writeToTempFile(csvRecords.mkString("$"), "csv_source_", "tmp") 
  41.  
  42.     // 创建Source connector 
  43.     new CsvTableSource( 
  44.       tempFilePath, 
  45.       Array("words"), 
  46.       Array( 
  47.         Types.STRING 
  48.       ), 
  49.       fieldDelim = "#"
  50.       rowDelim = "$"
  51.       ignoreFirstLine = true
  52.       ignoreComments = "%" 
  53.     ) 
  54.   } 
  55.  
  56.  
  57.   def genRatesHistorySource: CsvTableSource = { 
  58.  
  59.     val csvRecords = Seq
  60.       "rowtime ,currency   ,rate", 
  61.     "09:00:00   ,US Dollar  , 102", 
  62.     "09:00:00   ,Euro       , 114", 
  63.     "09:00:00  ,Yen        ,   1", 
  64.     "10:45:00   ,Euro       , 116", 
  65.     "11:15:00   ,Euro       , 119", 
  66.     "11:49:00   ,Pounds     , 108" 
  67.     ) 
  68.     // 测试数据写入临时文件 
  69.     val tempFilePath = 
  70.       FileUtils.writeToTempFile(csvRecords.mkString("$"), "csv_source_", "tmp") 
  71.  
  72.     // 创建Source connector 
  73.     new CsvTableSource( 
  74.       tempFilePath, 
  75.       Array("rowtime","currency","rate"), 
  76.       Array( 
  77.         Types.STRING,Types.STRING,Types.STRING 
  78.       ), 
  79.       fieldDelim = ","
  80.       rowDelim = "$"
  81.       ignoreFirstLine = true
  82.       ignoreComments = "%" 
  83.     ) 
  84.   } 
  85.  
  86.   def genEventRatesHistorySource: CsvTableSource = { 
  87.  
  88.     val csvRecords = Seq
  89.       "ts#currency#rate", 
  90.       "1#US Dollar#102", 
  91.       "1#Euro#114", 
  92.       "1#Yen#1", 
  93.       "3#Euro#116", 
  94.       "5#Euro#119", 
  95.       "7#Pounds#108" 
  96.     ) 
  97.     // 测试数据写入临时文件 
  98.     val tempFilePath = 
  99.       FileUtils.writeToTempFile(csvRecords.mkString(CommonUtils.line), "csv_source_rate", "tmp") 
  100.  
  101.     // 创建Source connector 
  102.     new CsvTableSource( 
  103.       tempFilePath, 
  104.       Array("ts","currency","rate"), 
  105.       Array( 
  106.         Types.LONG,Types.STRING,Types.LONG 
  107.       ), 
  108.       fieldDelim = "#"
  109.       rowDelim = CommonUtils.line, 
  110.       ignoreFirstLine = true
  111.       ignoreComments = "%" 
  112.     ) 
  113.   } 
  114.  
  115.   def genRatesOrderSource: CsvTableSource = { 
  116.  
  117.     val csvRecords = Seq
  118.       "ts#currency#amount", 
  119.       "2#Euro#10", 
  120.       "4#Euro#10" 
  121.     ) 
  122.     // 测试数据写入临时文件 
  123.     val tempFilePath = 
  124.       FileUtils.writeToTempFile(csvRecords.mkString(CommonUtils.line), "csv_source_order", "tmp") 
  125.  
  126.     // 创建Source connector 
  127.     new CsvTableSource( 
  128.       tempFilePath, 
  129.       Array("ts","currency", "amount"), 
  130.       Array( 
  131.         Types.LONG,Types.STRING,Types.LONG 
  132.       ), 
  133.       fieldDelim = "#"
  134.       rowDelim = CommonUtils.line, 
  135.       ignoreFirstLine = true
  136.       ignoreComments = "%" 
  137.     ) 
  138.   } 
  139.  
  140.  
  141.   /** 
  142.     * Example: 
  143.     * genCsvSink( 
  144.     *   Array[String]("word", "count"), 
  145.     *   Array[TypeInformation[_] ](Types.STRING, Types.LONG)) 
  146.     */ 
  147.   def genCsvSink(fieldNames: Array[String], fieldTypes: Array[TypeInformation[_]]): TableSink[Row] = { 
  148.     val tempFile = File.createTempFile("csv_sink_", "tem") 
  149.     if (tempFile.exists()) { 
  150.       tempFile.delete() 
  151.     } 
  152.     new CsvTableSink(tempFile.getAbsolutePath).configure(fieldNames, fieldTypes) 
  153.   } 
  154.  

运行结果如下 :

6. 内部实现原理

我们还是以订单和汇率关系示例来说明Apache Flink内部实现Temporal Table JOIN的原理,如下图所示:

五、Temporal Table JOIN vs 双流JOIN vs Lateral JOIN

在《Apache Flink 漫谈系列(09) - JOIN算子》中我们介绍了双流JOIN,在《Apache Flink 漫谈系列(10) - JOIN LATERAL 》中我们介绍了 JOIN LATERAL(TableFunction),那么本篇介绍的Temporal Table JOIN和双流JOIN/JOIN LATERAL(TableFunction)有什么本质区别呢?

  • 双流JOIN - 双流JOIN本质很明确是 Stream JOIN Stream,双流驱动。
  • LATERAL JOIN - Lateral JOIN的本质是Steam JOIN Table Function, 是单流驱动。
  • Temporal Table JOIN - Temporal Table JOIN 的本质就是 Stream JOIN Temporal Table 或者 Stream JOIN Table with snapshot。Temporal Table JOIN 特点单流驱动,Temporal Table 是被动查询。

1. Temporal Table JOIN vs LATERAL JOIN

从功能上说Temporal Table JOIN和 LATERAL JOIN都是由左流一条数据获取多行数据,也就是单流驱动,并且都是被动查询,那么Temporal JOIN和LATERAL JOIN最本质的区别是什么呢?这里我们说最关键的一点是 State 的管理,LATERAL JOIN是一个TableFunction,不具备state的管理能力,数据不具备版本特性。而Temporal Table JOIN是一个具备版本信息的数据表。

2. Temporal Table JOIN vs 双流 JOIN

Temporal Table JOIN 和 双流 JOIN都可以管理State,那么他们的本质区别是什么? 那就是计算驱动的差别,Temporal Table JOIN是单边驱动,Temporal Table是被动的查询,而双流JOIN是双边驱动,两边都是主动的进行JOIN计算。

3. Temporal Table JOIN改进

个人认为Apache Flink的Temporal Table JOIN功能不论在语法和语义上面都要遵循ANSI-SQL标准,后期会推动社区在Temporal Table上面支持ANSI-SQL的FOR SYSTEM_TIME AS OF标准语法。改进后的处理逻辑示意图:

其中cache是一种性能考虑的优化,详细内容待社区完善后再细述。

六、小结

本篇结合ANSI-SQL标准和SQL Server对Temporal Table的支持来开篇,然后介绍目前Apache Flink对Temporal Table的支持现状,以代码示例和内部处理逻辑示意图的方式让大家直观体验Temporal Table JOIN的语法和语义。

关于点赞和评论

本系列文章难免有很多缺陷和不足,真诚希望读者对有收获的篇章给予点赞鼓励,对有不足的篇章给予反馈和建议,先行感谢大家!

作者:孙金城,花名 金竹,目前就职于阿里巴巴,自2015年以来一直投入于基于Apache Flink的阿里巴巴计算平台Blink的设计研发工作。

【本文为51CTO专栏作者“金竹”原创稿件,转载请联系原作者】

戳这里,看该作者更多好文

责任编辑:赵宁宁 来源: 51CTO专栏
相关推荐

2022-07-13 12:53:59

数据存储

2018-11-29 09:01:26

Apache FlinJOIN代码

2018-11-20 07:59:43

Apache Flin JOIN算子代码

2019-01-03 10:17:53

Apache FlinTable API代码

2022-06-10 17:26:07

数据集计算

2018-12-29 08:16:32

Apache FlinJOIN代码

2018-10-16 08:54:35

Apache Flin流计算State

2018-09-26 08:44:22

Apache Flin流计算计算模式

2018-09-26 07:50:52

Apache Flin流计算计算模式

2018-10-09 10:55:52

Apache FlinWatermark流计算

2018-11-14 09:01:23

Apache FlinSQL代码

2018-10-22 21:43:39

Apache Flin流计算Fault Toler

2022-07-13 13:03:29

流计算乱序

2019-01-15 08:50:12

Apache FlinKafka分布式

2018-11-07 08:48:31

Apache Flin持续查询流计算

2022-07-12 10:38:25

分布式框架

2018-10-30 14:08:45

Apache Flin流表对偶duality

2020-04-09 11:08:30

PyFlinkJAR依赖

2022-06-20 05:52:27

FlinkTTL流查询

2018-10-30 11:10:05

Flink数据集计算
点赞
收藏

51CTO技术栈公众号