联接(Joins)是批处理数据处理中连接两个关系行的常见且易于理解的操作。然而,动态表上连接的语义不太明显,甚至不容易混淆。因此,有两种方法可以使用表API或SQL实际执行连接。
一、Regular Joins(常规联接)
常规联接是最通用的联接类型,其中对联接输入的任何一侧的任何新记录或更改都是可见的,并且会影响整个联接结果。例如,如果左侧有一个新记录,则它将与右侧所有以前和将来的记录联接
SELECT * FROM Orders
INNER JOIN Product
ON Orders.productId =Product.id
这些语义允许任何类型的更新(插入、更新、删除)输入表。
然而,这个操作有一个重要的含义:它要求连接输入的两边永远保持在Flink的状态。因此,如果一个或两个输入表都在持续增长,那么资源使用量也将无限增长。
二、Time-windowed Joins(时间窗口连接)
时间窗口连接由连接谓词定义,该谓词检查输入记录的时间属性是否在特定的时间限制(即时间窗口)内。
SELECT *
FROM
Orders o,
Shipments s
WHERE o.id = s.orderId
AND o.ordertime BETWEEN s.shiptime - INTERVAL ‘4’ HOUR AND s.shiptime
与常规联接操作相比,这种联接只支持具有时间属性的append only表。由于时间属性是准单调递增的,Flink可以在不影响结果正确性的情况下从其状态中删除旧值。
三、Join with a Temporal Table Function(使用临时表函数联接)
带有时态表函数的连接将只追加表(左输入/探测侧)与时态表(右输入/构建侧)连接起来,即,随时间变化并跟踪其变化的表。有关时态表的更多信息,请查看相应的页面。
下面的示例显示了一个只追加的表顺序,该表顺序应与不断变化的货币汇率表RatesHistory连接在一起。
Orders是一个只追加的表,表示对给定金额和给定货币的付款。例如,在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
鉴于我们想计算所有订单换算成一种共同货币(日元)的金额。
例如,我们希望使用给定行时间(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 over RatesHistory,我们可以用SQL来表示这样的查询:
SELECT
o.amount * r.rate AS amount
FROM
Orders AS o,
LATERAL TABLE (Rates(o.rowtime)) AS r
WHERE r.currency = o.currency
探测端的每条记录将在探测端记录的相关时间属性出现时与构建端表的版本联接。为了支持更新(覆盖)生成端表上以前的值,该表必须定义主键。
在我们的示例中,订单中的每个记录都将与time o.rowtime的Rates版本相关联。以前,currency字段被定义为Rates的主键,在我们的示例中用于连接两个表。如果查询使用的是处理时间概念,则在执行操作时,新追加的顺序将始终与最新版本的速率相关联。
与常规联接相比,这意味着如果在生成端有新记录,它将不会影响联接的先前结果。这再次允许Flink限制必须保持状态的元素的数量。
与时间窗口联接相比,临时表联接不定义一个时间窗口,在该时间窗口内记录将被联接。来自探测端的记录总是在time属性指定的时间与构建端的版本联接。因此,构建端的记录可能是任意旧的。随着时间的推移,将从状态中删除记录的以前版本和不再需要的版本(对于给定的主键)。
这种行为使得时态表连接成为用关系术语表示流充实的一个很好的候选者。
- 1、用法
定义了时态表函数之后,我们就可以开始使用它了。时态表函数的使用方法与普通表函数的使用方法相同。
下面的代码片段解决了从Orders表转换货币的激励问题:
SELECT
SUM(o_amount * r_rate) AS amount
FROM
Orders,
LATERAL TABLE (Rates(o_proctime))
WHERE
r_currency = o_currency
注意:查询配置中定义的状态保留尚未为临时连接实现。这意味着计算查询结果所需的状态可能会无限增长,这取决于历史表的不同主键的数量。
-
2、Processing-time Temporal Joins(处理时间时态连接)
对于处理时间属性,不可能将过去时间属性作为参数传递给时态表函数。根据定义,它总是当前的时间戳。因此,处理时间时态表函数的调用将始终返回底层表的最新已知版本,底层历史表中的任何更新也将立即覆盖当前值。
只有生成端记录的最新版本(相对于定义的主键)才保持在该状态。生成端的更新不会影响先前发出的联接结果。
我们可以将处理时间时态连接看作一个简单的HashMap<K,V>来存储构建端的所有记录。当来自构建端的新记录与以前的某个记录具有相同的键时,旧值只会被简单地覆盖。探测端的每条记录总是根据HashMap的最新/当前状态进行计算。 -
3、Event-time Temporal Joins(事件时间时态连接)
使用事件时间属性(即rowtime属性),可以将过去时间属性传递给时态表函数。这允许在同一时间点连接两个表。
与处理时间时态连接相比,时态表不仅将构建侧记录的最新版本(相对于定义的主键)保持在该状态,而且存储自上一个水印以来的所有版本(通过时间标识)。
例如,根据时态表的概念,事件时间戳为12:30:00并附加到探测侧表的传入行在时间12:30:00与构建侧表的版本联接。因此,传入行仅与时间戳小于或等于12:30:00的行连接,并根据主键应用更新,直到此时为止。
根据事件时间的定义,水印允许join操作在时间上向前移动,并放弃不再需要的生成表版本,因为不需要时间戳更低或相等的传入行。
四、Join with a Temporal Table(用临时表连接)
带有时态表的连接将任意表(左输入/探测侧)与时态表(右输入/构建侧)连接,即随时间变化的外部维度表。有关时态表的更多信息,请查看相应的页面。
注意:用户不能将任意表用作临时表,但需要使用由LookupableTableSource支持的表。LookupableTableSource只能作为临时表用于临时联接。有关如何定义LookupableTableSource的详细信息,请参见该页。
下面的示例显示了一个订单流,该订单流应该与不断变化的货币汇率表LatestRates相连接。
LatestRates是以最新速率具体化的维度表。在10:15、10:30、10:52时,迟到者的内容如下:
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时的最后孔含量相等。10点52分,欧元汇率从114变为116。
Orders是一个只追加的表,表示对给定金额和给定货币的付款。例如,在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
鉴于我们想计算所有定单换算成一种共同货币(日元)的金额。
例如,我们想用最新价格转换下列订单。结果将是:
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相连接。注意,对于处理时间来说,结果是不确定的。
与常规连接相比,尽管在构建方面有了更改,但临时表连接的先前结果不会受到影响。另外,临时表连接操作符非常轻量级,并且不保留任何状态。
与时间窗口联接相比,临时表联接不定义将在其中联接记录的时间窗口。来自探测端的记录在处理时总是与构建端的最新版本相连接。因此,构建端的记录可能是任意旧的。
时态表函数连接和时态表连接都来自相同的动机,但它们有不同的SQL语法和运行时实现:
时态表函数join的SQL语法是join UDTF,而时态表join使用SQL:2011中引入的标准时态表语法。
时态表函数连接的实现实际上是将两个流连接起来并保持它们的状态,而时态表连接只是接收唯一的输入流并根据记录中的键查找外部数据库。
时态表函数join通常用于连接changelog流,而时态表join通常用于连接外部表(即维度表)。
这种行为使得时态表连接成为用关系术语表示流充实的一个很好的候选者。
未来,时态表连接将支持时态表函数连接的特性,即支持时态连接变更日志流。
- 1、用法
时态表连接的语法如下:
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
目前只支持内部连接和左连接。从表1.proctime开始的FOR SYSTEM_TIME应在时间表之后。proctime是表1的处理时间属性。这意味着,当连接左表中的每条记录时,它会在处理时获取临时表的快照。
例如,在定义了时态表之后,我们可以如下使用它。
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中受支持,而在表API中还不受支持。
注意Flink目前不支持事件时间时态表连接。