一、Regular Joins
Regular Join是最通用的join类型。在这种join下,join两侧表的任何新纪录或变更都是可见的,并会影响整个join的结果。如下代码,如果左边表有一条新纪录,那么在Product.id相等的情况下,它将和右表之前和之后的所有记录进行join。
SELECT * FROM Orders
INNER JOIN Product
ON Orders.productId = Product.id
疑问:如果有更新,怎么做到更新历史的输出记录呢?答:用retract stream实现,这种join的适用场景是我们需要依赖截止到当前某个表的状态对历史所有结果的影响。举例:如我们要统计每个城市的订单收入,在这个场景中,用户下单时他的填写城市是北京,此时我们会把北京的收入+1,但是用户在下午把他的城市更新成了上海,那么此时需要把北京的收入-1,然后把上海的收入+1。
Regular Join不适用的场景:需要统计当时的状态,而不是用最新的状态去更新历史。如和汇率相关的,我们11:00~11:05卖出的商品是以1:7卖的,11:06~11:10是以1:8卖的,那么此时我们就不能以11:06~11:10的汇率去更新之前的收入,这种场景需要用Temporal Join满足。
1.1 INNER Equi-Join
SELECT *
FROM Orders
INNER JOIN Product
ON Orders.product_id = Product.id
2.1 OUTER Equi-Join
SELECT *
FROM Orders
LEFT JOIN Product
ON Orders.product_id = Product.id
SELECT *
FROM Orders
RIGHT JOIN Product
ON Orders.product_id = Product.id
SELECT *
FROM Orders
FULL OUTER JOIN Product
ON Orders.product_id = Product.id
二、Window Joins
窗口关联就是增加时间维度到关联的条件中。在此过程中,窗口关联将两个流中在同一窗口且符合Join条件的元素Join起来。窗口关联的语义和DataStream中的窗口关联相同。
在流式查询中,与其他连续表上的关联不同,窗口关联不产生中间结果,只在窗口结束时产生一个最终的结果。而且,窗口关联会清除不需要的中间状态。如一个窗口结束时,那这一个窗口中的状态都被清除。
2.1 INNER/LEFT/RIGHT/FULL OUTER
下面展示了 INNER/LEFT/RIGHT/FULL OUTER 窗口关联的语法
SELECT ...
FROM L [LEFT|RIGHT|FULL OUTER] JOIN R -- L and R are relations applied windowing TVF
ON L.window_start = R.window_start AND L.window_end = R.window_end AND ...
示例
SELECT L.num as L_Num, L.id as L_Id, R.num as R_Num, R.id as R_Id,
COALESCE(L.window_start, R.window_start) as window_start,
COALESCE(L.window_end, R.window_end) as window_end
FROM (
SELECT * FROM TABLE(TUMBLE(TABLE LeftTable, DESCRIPTOR(row_time), INTERVAL '5' MINUTES))
) L
FULL JOIN (
SELECT * FROM TABLE(TUMBLE(TABLE RightTable, DESCRIPTOR(row_time), INTERVAL '5' MINUTES))
) R
ON L.num = R.num AND L.window_start = R.window_start AND L.window_end = R.window_end;
结果
+-------+------+-------+------+------------------+------------------+ | L_Num | L_Id | R_Num | R_Id | window_start | window_end | +-------+------+-------+------+------------------+------------------+ | 1 | L1 | null | null | 2020-04-15 12:00 | 2020-04-15 12:05 | | null | null | 2 | R2 | 2020-04-15 12:00 | 2020-04-15 12:05 | | 3 | L3 | 3 | R3 | 2020-04-15 12:00 | 2020-04-15 12:05 | | 2 | L2 | null | null | 2020-04-15 12:05 | 2020-04-15 12:10 | | null | null | 4 | R4 | 2020-04-15 12:05 | 2020-04-15 12:10 | +-------+------+-------+------+------------------+------------------+
三、Interval Joins
返回一个符合Join条件和时间限制的简单笛卡尔积。Interval join需要至少一个equi-join条件和一个join两百年都包含限定时间的条件,判断范围可以用<, >, >=, <= ,也可以用between来判断。
例如,如果我们要统计被接收后四小时以内发货的订单,可以用如下sql
SELECT *
FROM Orders o, Shipments s
WHERE o.id = s.order_id
AND o.order_time BETWEEN s.ship_time - INTERVAL '4' HOUR AND s.ship_time
Interval Join同样会对历史状态进行清除,不会影响结果的正确性。
四、Temporal Joins
时态表的定义看第16章。
4.1 事件时间Temporal Join
基于事件时间的Temporal Join使用任意表(左侧输入/探测端)的每一行与时态表中对应的行进行关联(右侧输入/构建端)。Flink使用SQL:2011标准中的FOR SYSTEM_TIME AS OF语法去执行操作。Temporal join的语法如下:
SELECT [column_list]
FROM table1 [AS <alias1>]
[LEFT] JOIN table2 FOR SYSTEM_TIME AS OF table1.{ proctime | rowtime } [AS <alias2>]
ON table1.column-name1 = table2.column-name1
有了事件时间属性(即:rowtime属性),就能检索到过去某个时间点的值。**这允许在一个共同的时间点上连接这两个表。**版本表将存储自最后一个watermark以来的所有版本(按时间标识)。
例如,假设我们有一个订单表,每个订单都有不同货币的价格。为了正确地将该表统一为单一货币(如美元),每个订单都需要与下单时对应的汇率相关联。
-- Create a table of orders. This is a standard
-- append-only dynamic table.
CREATE TABLE orders (
order_id STRING,
price DECIMAL(32,2),
currency STRING,
order_time TIMESTAMP(3),
WATERMARK FOR order_time AS order_time - INTERVAL '15' SECOND
) WITH (/* ... */);
-- 定义一个汇率的时态表
-- 这个表应该从变化的数据中得到,例如Debezium,一个压缩的kafka topic,或者其他任何定义版本表的地方
CREATE TABLE currency_rates (
currency STRING,
conversion_rate DECIMAL(32, 2),
update_time TIMESTAMP(3) METADATA FROM values.source.timestamp VIRTUAL,
WATERMARK FOR update_time AS update_time - INTERVAL '15' SECOND,
PRIMARY KEY(currency) NOT ENFORCED
) WITH (
'connector' = 'kafka',
'value.format' = 'debezium-json',
/* ... */
);
SELECT
order_id,
price,
orders.currency,
conversion_rate,
order_time
FROM orders
LEFT JOIN currency_rates FOR SYSTEM_TIME AS OF orders.order_time
ON orders.currency = currency_rates.currency;
order_id price currency conversion_rate order_time
======== ===== ======== =============== =========
o_001 11.11 EUR 1.14 12:00:00
o_002 12.51 EUR 1.10 12:06:00
注意:事件时间temporal join是通过左右两侧的watermark触发的;这里的INTERVAl时间减法用于等待后续事件,以确保预期。所以必须确保join两边设置了正确的watermark。
与regular join相比,就算build side(例子中的currency_rates表)发生变更了,之前的temporal table的结果也不会被影响。
4.2 处理时间Temporal Join
基于处理时间的temporal Join使用处理时间属性将数据与外部版本表(如mysql、hbase)的最新版本相关联,这种方式的结果随着时间的变化具有不确定性。
下面这个处理时间 temporal join 示例展示了一个追加表 orders 与 LatestRates 表进行 join。 LatestRates 是一个最新汇率的维表,比如 HBase 表,在 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
LastestRates表的数据在10:15和10:30是相同的。 欧元(Euro)的汇率(rate)在10:52从 114 变更为 116。
Orders表示支付金额的amount和currency的追加表。 例如:在10:15,有一个金额为2 Euro的 order。
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
给出下面这些表,我们希望所有Orders表的记录转换为一个统一的货币。
amount currency rate amount*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
目前,temporal join 还不支持与任意 view/table 的最新版本 join 时使用FOR SYSTEM_TIME AS OF语法。可以像下面这样使用 temporal table function 语法来实现(时态表函数):
SELECT
o_amount, r_rate
FROM
Orders,
LATERAL TABLE (Rates(o_proctime))
WHERE
r_currency = o_currency