Flink SQL Window Join 把时间维度“写进” JOIN 条件里

一、Window Join 是什么?

官方的一句话概括:

Window Join 在 join 条件中引入窗口边界,只在"同一个时间窗口内、key 匹配"的记录之间进行关联。

和普通 Regular Join 相比,Window Join 有两点关键差异:

  1. 有时间窗口约束

    不再是"表里所有历史数据都参与 Join",而是 按窗口切分,每个窗口内的数据局部 Join。

  2. 只在窗口结束时输出结果 & 清理状态

    和普通聚合类似,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;

这里有几个硬性规则

  1. 左右两侧必须是 Windowing TVF 的结果

    比如 TUMBLE/HOP/CUMULATE/SESSION(TABLE ..., DESCRIPTOR(time_col), ...)

  2. JOIN 条件必须包含窗口边界相等

    sql 复制代码
    L.window_start = R.window_start
    AND L.window_end = R.window_end
  3. 左右两侧的窗口类型与参数必须一致

    比如都用 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)
  • 窗口二 [12:05, 12:10)

    • Left:L2(12:06, num=2)
    • Right:R4(12:05, num=4)

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

重点理解几个点:

  • L3R3 能 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 里,通常用 INEXISTS 表达:

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 条件的限制

  • 必须包含

    sql 复制代码
    L.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 窗口的 SLIDESIZE 也要匹配。

6.3 对 Session Window 的限制

  • Session Window Join 在批模式下不支持

  • 当前如果 Window Join 直接跟在 Windowing TVF 后面:

    • 只支持 Tumble / Hop / Cumulate;
    • Session Window 还处于"概念支持 + Beta"状态,优化较少,生产上要慎用并关注版本说明。

6.4 状态压力仍需评估

虽然 Window Join 会在窗口结束后释放状态,但:

  • 窗口越大,峰值状态越高;

  • QPS 高时,窗口内"瞬时状态量"依然可能很恐怖

  • 建议结合:

    • 合理窗口长度;
    • 合理 watermark(允许延迟但别过大);
    • 状态后端优化与资源评估。

七、实践小结与建议

最后用几条经验给这篇收个尾:

  1. 业务语义是"按时间账期对账"的,优先考虑 Window Join

    比 Regular Join 更贴合语义,也更容易控制状态。

  2. 统一用 Windowing TVF 来表达窗口
    TUMBLE/HOP/CUMULATE/SESSION 搭配 window_start/window_end/window_time

    接在后面的 Window Aggregation / Window Join / Window TopN 都能直接复用。

  3. 只关心"是否存在"时用 SEMI / ANTI

    SEMI:窗口内存在就算数;

    ANTI:窗口内完全不存在的才输出。

    这种写法 SQL 语义清晰,也避免了无意义的数据膨胀。

  4. 窗口大小 + Watermark 要一起设计

    • 窗口太小 → 对不上业务(特别是乱序严重时);
    • 窗口太大 → 峰值状态爆炸;
    • Watermark 太小 → 容错差;太大 → 延迟高。
相关推荐
MySQL实战1 小时前
Redis 7.0 新特性之maxmemory-clients:限制客户端内存总使用量
数据库·redis
VX:Fegn08951 小时前
计算机毕业设计|基于springboot + vue校园社团管理系统(源码+数据库+文档)
前端·数据库·vue.js·spring boot·后端·课程设计
北亚数据恢复2 小时前
虚拟机数据恢复—ESXi虚拟机下SqlServer数据库数据恢复案例
数据库
susu10830189112 小时前
使用navicat创建事件event报错You have an error in your SQL syntax
数据库·sql
水力魔方2 小时前
武理排水管网模拟分析系统应用专题5:模型克隆与并行计算
数据库·c++·算法·swmm
oulaqiao2 小时前
幂等性——网络抖动重复支付的解决方法
sql·web app
cike_y2 小时前
Spring-Bean的作用域&Bean的自动装配
java·开发语言·数据库·spring
stella·3 小时前
mysql的时区问题
数据库·mysql·timezone·时区
+VX:Fegn08954 小时前
计算机毕业设计|基于springboot + vueOA工程项目管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·课程设计
wang6021252184 小时前
阿里云存储的下载验证
数据库·阿里云·fastapi