该文章主要是对Flink官网相关内容进行翻译,原文地址:ci.apache.org/projects/fl…
Join是批处理数据处理中常见且易于理解的操作,用于连接两个关系的行。 但是,动态表上的连接语义不太明显甚至令人困惑。
在Flink中有几种方法可以使用Table API或SQL实际执行连接。
常规JOIN
常规联接是最通用的join类型,其中任何新记录或对join输入两侧的任何更改都是可见的,并且会影响整个联接结果。例如,如果左侧有一个新记录,则它将与右侧的所有以前和将来的记录合并在一起。
SELECT * FROM Orders
INNER JOIN Product
ON Orders.productId = Product.id
这些语义允许进行任何类型的更新(insert, update, delete)输入表。
但是,此操作有一个重要的含义:它需要将联接输入的两端始终保持在Flink的状态。因此,如果一个或两个输入表持续增长,资源使用也将无限期增长。
时间窗口JOIN
时间窗口连接由join 定义,该join 检查输入记录的时间属性是否在某些时间限制内,即时间窗口。
SELECT *
FROM
Orders o,
Shipments s
WHERE o.id = s.orderId AND
o.ordertime BETWEEN s.shiptime - INTERVAL '4' HOUR AND s.shiptime
与常规join操作相比,此类连接仅支持具有时间属性的仅追加表(append-only tables)。 由于时间属性是quasi-monontic增加,Flink可以从其状态中删除旧值而不影响结果的正确性。
时态表函数JOIN
具有时态表函数的join将仅追加( append-only)表(左输入/探头侧)与时态表(右输入/构建侧)连接,即随时间变化并跟踪其变化的表。 以下示例显示了一个仅追加(append-only)表的订单,这些订单应与不断变化的货币汇率表RatesHistory连接。Orders是仅追加( append-only)表,表示给定金额和给定货币的付款。 例如,在10:15,订单金额为2欧元。
SELECT * FROM Orders;
rowtime amount currency
======= ====== =========
10:15 2 Euro
10:30 1 US Dollar
10:32 50 Yen
10:52 3 Euro
11:04 5 US Dollar
RatesHistory代表一个不断变化的仅追加货币汇率表,相对于日元(汇率为1)。 例如,欧元兑日元从09:00到10:45的汇率为114.从10:45到11:15,汇率为116。
SELECT * FROM RatesHistory;
rowtime currency rate
======= ======== ======
09:00 US Dollar 102
09:00 Euro 114
09:00 Yen 1
10:45 Euro 116
11:15 Euro 119
11:49 Pounds 108
鉴于我们想要计算所有订单的金额转换为通用货币(日元)。
例如,我们希望使用转换率给定的rowtime转换如下订单 (114)
rowtime amount currency
======= ====== =========
10:15 2 Euro
如果不使用时态表,就需要编写如下查询:
SELECT
SUM(o.amount * r.rate) AS amount
FROM Orders AS o,
RatesHistory AS r
WHERE r.currency = o.currency
AND r.rowtime = (
SELECT MAX(rowtime)
FROM RatesHistory AS r2
WHERE r2.currency = o.currency
AND r2.rowtime <= o.rowtime);
借助时态表函数Rates,可以使用下面sql查询
SELECT
o.amount * r.rate AS amount
FROM
Orders AS o,
LATERAL TABLE (Rates(o.rowtime)) AS r
WHERE r.currency = o.currency
探针端记录的相关时间属性时,来自探针端的每个记录将与构建端表的版本关联。为了支持生成侧表上以前值的更新(覆盖),该表必须定义一个主键。
在我们的示例中,Orders表中的每条记录将与Rates表join,在时间o.rowtime 。 货币(currency)字段已被定义为之前的Rates的主键,并用于连接我们示例中的两个表。 如果查询使用处理时间概念,则在执行操作时,新添加的订单将始终与最新版本的Rates连接。
与常规连接相比,这意味着如果构建端(时态表)有新记录,则不会影响以前的连接结果。 这再次允许Flink限制元素的数量,保持state。
与时间窗口join相比,时态表join不定义时间窗口(时间窗口内的数据将会被join)。 探针端的记录始终与time属性指定的构建端版本连接。 因此,构建方面的记录可能是任意旧的。 随着时间的推移,将从状态中删除先前和不再需要的记录版本(对于给定的主键)。
用法
定义时态表函数后,我们可以使用它。 时态表函数的使用方式与使用普通表函数的方式相同。
以下代码段解决了我们从Orders表转换货币问题:
SQL:
SELECT
SUM(o_amount * r_rate) AS amount
FROM
Orders,
LATERAL TABLE (Rates(o_proctime))
WHERE
r_currency = o_currency
JAVA:
Table result = orders
.join(new Table(tEnv, "rates(o_proctime)"), "o_currency = r_currency")
.select("(o_amount * r_rate).sum as amount");
SCALA:
val result = orders
.join(rates('o_proctime), 'r_currency === 'o_currency)
.select(('o_amount * 'r_rate).sum as 'amount)
注意:对于时态表join,尚未实现在查询配置中定义的状态保留。这意味着,计算查询结果所需的状态可能会无限增长,具体取决于历史记录表的不同主键数量。
处理时间时态连接
使用处理时间属性,不可能将过去的时间属性作为参数传递给时态表函数。 根据定义,它始终是当前时间戳。 因此,处理时间时态表函数的调用将始终返回基础表的最新已知版本,并且基础历史表中的任何更新也将立即覆盖当前值。
仅将构建侧记录的最新版本(相对于定义的主键)保持在该状态。构建端的更新不会影响先前发出的联接结果。
可以将处理时的时态联接视为一种简单的HashMap <K,V>,它存储来自构建端的所有记录。当来自构建端的新记录与先前的记录具有相同的键时,旧值仅被覆盖。总是根据HashMap的最新/当前状态评估来自探测器端的每个记录。
事件时间时态连接
利用事件时间属性(即rowtime属性),可以将过去的时间属性传递给时间表函数。这允许在公共时间点将两个表连接在一起。
与处理时间时态连接相比,时态表不仅保持状态中的构建侧记录的最新版本(相对于定义的主键),而且存储自上一个watermark以来的所有版本(由时间标识)。
例如,根据时态表的概念,在时间12:30:00将附加到探测器侧表的事件时间时间戳为12:30:00的传入行与构建侧表的版本连接在一起。 。因此,传入行仅与时间戳小于或等于12:30:00的行连接,并根据主键应用更新,直到此时为止。
通过事件时间的定义,watermark允许连接操作及时向前移动并丢弃不再需要的构建表的版本,因为不期望具有较低或相等时间戳的传入行。
与时态表JOIN
与时态表的联接将任意表(左侧输入/探针侧)与时态表(右侧输入/构建侧)联接,即随时间变化的外部维度表。
注意:用户不能将任意表用作时态表,而需要使用由LookupableTableSource支持的表。LookupableTableSource只能作为时态表用于时间联接。有关如何定义LookupableTableSource的更多详细信息,请参见how to define LookupableTableSource。
下面的示例显示了Orders流,该流应与不断变化的货币汇率表LatestRates结合在一起。
LatestRates是使用最新汇率实现的维度表。在时间10:15、10:30、10:52,LatestRates的内容如下:
10:15> SELECT * FROM LatestRates;
currency rate
======== ======
US Dollar 102
Euro 114
Yen 1
10:30> SELECT * FROM LatestRates;
currency rate
======== ======
US Dollar 102
Euro 114
Yen 1
10:52> SELECT * FROM LatestRates;
currency rate
======== ======
US Dollar 102
Euro 116 <==== changed from 114 to 116
Yen 1
时间10:15和10:30的LastestRates的内容相等。欧元汇率在10:52从114更改为116。订单是一个仅附加表,代表给定金额和给定货币的付款。例如,在10:15时有一笔2欧元的订单。
SELECT * FROM Orders;
amount currency
====== =========
2 Euro <== arrived at time 10:15
1 US Dollar <== arrived at time 10:30
2 Euro <== arrived at time 10:52
假设我们要计算所有转换为通用货币(日元)的订单金额。例如,我们想使用LatestRates中的最新汇率转换以下订单。结果将是:
amount currency rate amout*rate
====== ========= ======= ============
2 Euro 114 228 <== arrived at time 10:15
1 US Dollar 102 102 <== arrived at time 10:30
2 Euro 116 232 <== arrived at time 10:52
借助时态表联接,我们可以在SQL中将查询表示为:
SELECT
o.amout, o.currency, r.rate, o.amount * r.rate
FROM
Orders AS o
JOIN LatestRates FOR SYSTEM_TIME AS OF o.proctime AS r
ON r.currency = o.currency
探针端的每个记录都将与构建端表的当前版本关联。在我们的示例中,查询使用的是处理时间概念,因此在执行操作时,新附加的订单将始终与最新版本的LatestRates结合在一起。注意,结果对于处理时间不是确定的。
与常规联接相反,尽管在构建方面进行了更改,但时态表表联接的先前结果将不会受到影响。而且,时态表联接运算符非常轻巧,并且不保留任何状态。
与时间窗口联接相比,时态表联接没有定义将要在其中联接记录的时间窗口。在处理时,探测端的记录始终会与构建端的最新版本结合在一起。因此,构建方面的记录可能是任意旧的。
时态表函数join和时态表join都来自相同的动机,但是具有不同的SQL语法和运行时实现:
- 时态表函数join的SQL语法是联接UDTF,而时态表join使用SQL:2011中引入的标准时态表语法。
- 时态表函数join的实现实际上联接了两个流并使它们保持状态,而时态表join仅接收唯一的输入流并根据记录中的键查找外部数据库。
- 时态表函数hoin通常用于联接变更日志流,而时态表join通常用于联接外部表(即维表)。
这种行为使时态表成为一个很好的候选者,可以用关系术语来表示流的富集。
将来,时态表联接将支持时态表功能联接的功能,即支持时态联接变更日志流。
用法
时态表联接的语法如下:
SELECT [column_list]
FROM table1 [AS <alias1>]
[LEFT] JOIN table2 FOR SYSTEM_TIME AS OF table1.proctime [AS <alias2>]
ON table1.column-name1 = table2.column-name1
当前,仅支持INNER JOIN和LEFT JOIN。临时表之后应遵循FOR SYSTEM_TIME AS OF table1.proctime。proctime是table1的处理时间属性。这意味着在连接左表中的每个记录时,它会在处理时为时态表拍摄快照。
例如,在定义时态表之后,我们可以如下使用它。
SELECT
SUM(o_amount * r_rate) AS amount
FROM
Orders
JOIN LatestRates FOR SYSTEM_TIME AS OF o_proctime
ON r_currency = o_currency
注意:
- 仅在Blink planner程序中受支持。
- 它仅在SQL中受支持,而在Table API中尚不支持。
- Flink当前不支持事件时间时态表联接。