一、Window Join 是什么?
官方的一句话概括:
Window Join 在 join 条件中引入窗口边界,只在"同一个时间窗口内、key 匹配"的记录之间进行关联。
和普通 Regular Join 相比,Window Join 有两点关键差异:
-
有时间窗口约束
不再是"表里所有历史数据都参与 Join",而是 按窗口切分,每个窗口内的数据局部 Join。
-
只在窗口结束时输出结果 & 清理状态
和普通聚合类似,Window Join 在窗口结束时输出最终结果,并清理该窗口对应的状态,避免无限膨胀。
注意:Window Join 通常是 "Windowing TVF + JOIN" 的组合:
先用
TUMBLE/HOP/CUMULATE/SESSION切成窗口表,再对这些窗口表做 Join。
二、语法模式:Windowing TVF + Window Join
Window Join 的典型写法是这样的(以 Tumble 窗口为例):
sql
SELECT ...
FROM (
SELECT * FROM TABLE(
TUMBLE(TABLE LeftTable, DESCRIPTOR(row_time), INTERVAL '5' MINUTES)
)
) AS L
JOIN (
SELECT * FROM TABLE(
TUMBLE(TABLE RightTable, DESCRIPTOR(row_time), INTERVAL '5' MINUTES)
)
) AS R
ON L.window_start = R.window_start
AND L.window_end = R.window_end
AND L.join_key = R.join_key;
这里有几个硬性规则:
-
左右两侧必须是 Windowing TVF 的结果
比如
TUMBLE/HOP/CUMULATE/SESSION(TABLE ..., DESCRIPTOR(time_col), ...)。 -
JOIN 条件必须包含窗口边界相等
sqlL.window_start = R.window_start AND L.window_end = R.window_end -
左右两侧的窗口类型与参数必须一致
比如都用
TUMBLE,都用INTERVAL '5' MINUTES。
否则 Planner 会直接报错,或者语义完全错误。
三、一个完整例子:看懂 Window Join 的"时间切片"
先定义两个输入表:
sql
CREATE TABLE LeftTable (
row_time TIMESTAMP(3) *ROWTIME*,
num INT,
id STRING,
WATERMARK FOR row_time AS row_time - INTERVAL '1' SECOND
) WITH (...);
CREATE TABLE RightTable (
row_time TIMESTAMP(3) *ROWTIME*,
num INT,
id STRING,
WATERMARK FOR row_time AS row_time - INTERVAL '1' SECOND
) WITH (...);
样例数据如下:
text
LeftTable:
2020-04-15 12:02 | 1 | L1
2020-04-15 12:06 | 2 | L2
2020-04-15 12:03 | 3 | L3
RightTable:
2020-04-15 12:01 | 2 | R2
2020-04-15 12:04 | 3 | R3
2020-04-15 12:05 | 4 | R4
我们按 5 分钟 Tumble 窗口做 FULL OUTER Window Join:
sql
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;
3.1 窗口是如何切的?
Tumble 5 分钟窗口,会把数据切成:
-
窗口一
[12:00, 12:05):- Left:
L1(12:02, num=1),L3(12:03, num=3) - Right:
R2(12:01, num=2),R3(12:04, num=3)
- Left:
-
窗口二
[12:05, 12:10):- Left:
L2(12:06, num=2) - Right:
R4(12:05, num=4)
- Left:
3.2 Join 结果解读
Join 输出如下:
text
L_Num | L_Id | R_Num | R_Id | window_start | window_end
----- | ---- | ----- | ---- | ------------------- | -------------------
1 | L1 | null | null | 2020-04-15 12:00:00 | 2020-04-15 12:05:00
null | null | 2 | R2 | 2020-04-15 12:00:00 | 2020-04-15 12:05:00
3 | L3 | 3 | R3 | 2020-04-15 12:00:00 | 2020-04-15 12:05:00
2 | L2 | null | null | 2020-04-15 12:05:00 | 2020-04-15 12:10:00
null | null | 4 | R4 | 2020-04-15 12:05:00 | 2020-04-15 12:10:00
重点理解几个点:
-
L3与R3能 Join:- 时间均在
[12:00,12:05)窗口; num=3相等;
- 时间均在
-
L1没在右表找到num=1,在 FULL OUTER 的左侧独立输出; -
R2没在左表找到num=2,在 FULL OUTER 的右侧独立输出; -
L2(num=2)和R2(num=2)为什么没 Join?L2在第二个窗口[12:05,12:10);R2在第一个窗口[12:00,12:05);- 虽然 key 一样,但窗口不同,
条件里要求window_start/end相等 → 判定为"不在同一个时间账期",因此不 Join。
这就是 Window Join 的核心语义:
同一窗口 + key 相等 才能配对,不同窗口的同 key 被视为完全不同的业务时间段。
四、Window Join 支持的多种 JOIN 形态
Window Join 语义基于窗口,但从"结果保留哪一侧"角度,它依然支持多种经典 JOIN 形态。
4.1 INNER / LEFT / RIGHT / FULL OUTER Window Join
语法形式:
sql
SELECT ...
FROM L
[LEFT | RIGHT | FULL OUTER] JOIN R
ON L.window_start = R.window_start
AND L.window_end = R.window_end
AND L.key = R.key;
- INNER:窗口内同时在左右两边出现且匹配的记录;
- LEFT / RIGHT:保留对应一侧窗口内所有记录,另一侧没有则填 null;
- FULL OUTER:在窗口粒度上做"全量对账",左右两边都保留。
业务上常见的用法:
- INNER:窗口内所有"成功匹配"的订单/事件;
- LEFT:窗口内所有"左边订单",标记右边是否有对应记录;
- FULL OUTER:做对账报表,后续按空值判断"只在左边 / 只在右边 / 双边都有"。
4.2 SEMI Window Join:窗口内"存在即可"的 Join
对于左表中的记录,只要同一窗口内右表存在至少一条匹配记录,就保留这条左表记录。
在 Flink SQL 里,通常用 IN 或 EXISTS 表达:
sql
-- IN 写法
SELECT *
FROM (
SELECT * FROM TABLE(
TUMBLE(TABLE LeftTable, DESCRIPTOR(row_time), INTERVAL '5' MINUTES)
)
) L
WHERE L.num IN (
SELECT num
FROM (
SELECT * FROM TABLE(
TUMBLE(TABLE RightTable, DESCRIPTOR(row_time), INTERVAL '5' MINUTES)
)
) R
WHERE L.window_start = R.window_start
AND L.window_end = R.window_end
);
或者:
sql
-- EXISTS 写法
SELECT *
FROM (
SELECT * FROM TABLE(
TUMBLE(TABLE LeftTable, DESCRIPTOR(row_time), INTERVAL '5' MINUTES)
)
) L
WHERE EXISTS (
SELECT *
FROM (
SELECT * FROM TABLE(
TUMBLE(TABLE RightTable, DESCRIPTOR(row_time), INTERVAL '5' MINUTES)
)
) R
WHERE L.num = R.num
AND L.window_start = R.window_start
AND L.window_end = R.window_end
);
特点:
- 只关心"在这个窗口里,有没有匹配过",不关心匹配了几条,也不需要右表的字段;
- 用于窗口内存在性判断:比如"窗口内是否支付过"、"是否发生过某种行为"。
4.3 ANTI Window Join:窗口内"完全不存在"的 Join
保留那些在窗口内 找不到任何匹配 的左表记录。
NOT IN 写法:
sql
SELECT *
FROM (
SELECT * FROM TABLE(
TUMBLE(TABLE LeftTable, DESCRIPTOR(row_time), INTERVAL '5' MINUTES)
)
) L
WHERE L.num NOT IN (
SELECT num
FROM (
SELECT * FROM TABLE(
TUMBLE(TABLE RightTable, DESCRIPTOR(row_time), INTERVAL '5' MINUTES)
)
) R
WHERE L.window_start = R.window_start
AND L.window_end = R.window_end
);
NOT EXISTS 写法:
sql
SELECT *
FROM (
SELECT * FROM TABLE(
TUMBLE(TABLE LeftTable, DESCRIPTOR(row_time), INTERVAL '5' MINUTES)
)
) L
WHERE NOT EXISTS (
SELECT *
FROM (
SELECT * FROM TABLE(
TUMBLE(TABLE RightTable, DESCRIPTOR(row_time), INTERVAL '5' MINUTES)
)
) R
WHERE L.num = R.num
AND L.window_start = R.window_start
AND L.window_end = R.window_end
);
典型应用场景:
- 窗口内未支付的订单;
- 窗口内没有被处理/消费的消息;
- 窗口内没有发生某行为的用户(做反向筛选)。
五、Window Join 与其他 Join 的对比
为了选型更清晰,可以简单对比一下几种典型 Join:
| 类型 | 时间条件写法 | 状态规模控制 | 适用场景 |
|---|---|---|---|
| Regular Join | 仅 key 条件 | 无界,靠 TTL 控制 | 小表 + 更新少,或离线/批处理 |
| Interval Join | BETWEEN t1 - x AND t1 + y |
依赖 watermark 清理 | "事件之间时间关系",下单→支付、点击→转化 |
| Temporal Join | FOR SYSTEM_TIME AS OF |
按主键收敛历史版本 | 流 + 维表 / 变更历史表 |
| Window Join | window_start/end 相等 + key 条件 |
按窗口清理 | 按窗口对账 / 窗口内匹配 / 存在性判断 |
归纳一下使用 Window Join 的典型信号:
- 业务语义天然是"每 N 分钟对账 / 统计一次";
- 不关心跨窗口的全局匹配,只关心每个窗口内的匹配情况;
- 希望状态在窗口结束后完全释放,避免 Regular Join 的"长尾状态"。
六、现阶段的限制与坑点
官方文档里明确了几个限制点,实践中很容易踩坑:
6.1 Join 条件的限制
-
必须包含:
sqlL.window_start = R.window_start AND L.window_end = R.window_end -
未来有可能对
TUMBLE/HOP简化为只比较window_start,但目前还是两者都要写。
6.2 Windowing TVF 必须一致
-
左右两侧的 Windowing TVF 必须是同一种:
- 比如,都用
TUMBLE(TABLE ..., DESCRIPTOR(...), INTERVAL '5' MINUTES)
- 比如,都用
-
参数也要一致:
- 比如 HOP 窗口的
SLIDE和SIZE也要匹配。
- 比如 HOP 窗口的
6.3 对 Session Window 的限制
-
Session Window Join 在批模式下不支持;
-
当前如果 Window Join 直接跟在 Windowing TVF 后面:
- 只支持 Tumble / Hop / Cumulate;
- Session Window 还处于"概念支持 + Beta"状态,优化较少,生产上要慎用并关注版本说明。
6.4 状态压力仍需评估
虽然 Window Join 会在窗口结束后释放状态,但:
-
窗口越大,峰值状态越高;
-
QPS 高时,窗口内"瞬时状态量"依然可能很恐怖;
-
建议结合:
- 合理窗口长度;
- 合理 watermark(允许延迟但别过大);
- 状态后端优化与资源评估。
七、实践小结与建议
最后用几条经验给这篇收个尾:
-
业务语义是"按时间账期对账"的,优先考虑 Window Join
比 Regular Join 更贴合语义,也更容易控制状态。
-
统一用 Windowing TVF 来表达窗口
TUMBLE/HOP/CUMULATE/SESSION搭配window_start/window_end/window_time,接在后面的 Window Aggregation / Window Join / Window TopN 都能直接复用。
-
只关心"是否存在"时用 SEMI / ANTI
SEMI:窗口内存在就算数;
ANTI:窗口内完全不存在的才输出。
这种写法 SQL 语义清晰,也避免了无意义的数据膨胀。
-
窗口大小 + Watermark 要一起设计
- 窗口太小 → 对不上业务(特别是乱序严重时);
- 窗口太大 → 峰值状态爆炸;
- Watermark 太小 → 容错差;太大 → 延迟高。