Flink SQL Join 从 Regular Join 到 Temporal Join 的实战

1. Regular Join:最灵活、也最"重"的 Join

Regular Join 就是大家最熟悉的那种:

sql 复制代码
SELECT *
FROM Orders
INNER JOIN Product
ON Orders.product_id = Product.id;

和离线 SQL 看起来几乎一样,但在 Flink 流式语义下,有几个关键差异:

1.1 流式 Regular Join 的"代价"

在流式查询里,输入是无界数据流,Regular Join 的语义是:

任意一侧来一条新数据或变更,都要和另一侧所有历史和未来记录做 Join(满足 Join 条件的)。

为了支撑这种语义,Flink 必须:

  • 把 Join 两侧的所有记录都放在状态里
  • 保证后面来的数据都能拿到历史数据做 Join。

这意味着什么?------状态可能无限增长

状态大小取决于:

  • 两侧表的去重行数;
  • 多表链式 Join 的中间结果数量。

所以 Flink 文档才反复强调:

一定要结合业务设置合理的 State TTL,否则 Regular Join 可以轻松把你打到 OOM。

1.2 内等值 Join vs 外等值 Join

目前 Flink Regular Join:

  • 支持 Inner Join / Left Join / Right Join / Full Outer Join

  • Join 条件必须是 等值 Join(Equi-Join),也就是:

    • Join 条件中至少有一个 a.col = b.col 之类的等值谓词;
  • 不支持:

    • 纯笛卡尔积;
    • Theta Join(a.x > b.y 这类单纯不等式)作为唯一 Join 条件。

示例:

sql 复制代码
-- 内等值 Join
SELECT *
FROM Orders
INNER JOIN Product
ON Orders.product_id = Product.id;

-- 左外等值 Join
SELECT *
FROM Orders
LEFT JOIN Product
ON Orders.product_id = Product.id;

-- 右外等值 Join
SELECT *
FROM Orders
RIGHT JOIN Product
ON Orders.product_id = Product.id;

-- 全外等值 Join
SELECT *
FROM Orders
FULL OUTER JOIN Product
ON Orders.product_id = Product.id;

1.3 多表 Regular Join:优先考虑 MultiJoin

多表链式 Join 的一个典型坑是:

A JOIN B JOIN C JOIN D,如果直接按写的顺序算,很有可能中间状态巨大。

Flink 提供了一个 MultiJoin 算子,用来对多表 Join 做合并和优化(具体使用可以看官方 tuning 文档),实战建议是:

  • 多表 Join 尽量一次写清楚,让优化器有机会使用 MultiJoin;
  • 明确 Join 条件,不要写出"隐式笛卡尔积"。

2. Interval Join:按时间区间做事件关联

Interval Join 的典型场景是:

"订单和支付/发货根据时间做关联,只允许在一个时间区间内 Join。"

例如:

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;

语义:

  • 订单 ID 相等;
  • 且订单时间在 [ship_time - 4 小时, ship_time] 这个时间范围内。

2.1 Interval Join 的约束

相较于 Regular Join,Interval Join 更"克制":

  • 只支持 append-only + 时间属性 的表;

  • Join 条件要求:

    • 至少一个等值 Join;
    • 再加上时间区间谓词:

合法的时间条件示例:

sql 复制代码
ltime = rtime
ltime >= rtime AND ltime < rtime + INTERVAL '10' MINUTE
ltime BETWEEN rtime - INTERVAL '10' SECOND AND rtime + INTERVAL '5' SECOND

因为时间属性是单调递增(或准单调,考虑乱序 + watermark),Flink 能:

  • 利用 watermark 判断哪些数据的 Join 再也不会被触发;
  • 从状态中安全地清理这些旧记录。

一句话:Interval Join = 带时间窗口约束的 Join,在保证语义的前提下,大幅降低状态压力。

3. Temporal Join:给事实流做"时点维度补全"

Temporal Join(时态 Join)是 Flink 流式数仓里的关键能力之一,本质是:

对一个不断变化的维度表,在某个给定时间点回放出"当时的版本",并和事实流 Join。

这类场景在业务里非常常见:

  • 币种汇率表、商品价格表、用户等级表;
  • 订单/行为在当天看和第二天看,看到的维度信息可能不同。

Flink 提供两类时态 Join:

  1. Event Time Temporal Join:按事件时间上的"过去状态" Join;
  2. Processing Time Temporal Join:按处理时间上的"当前最新状态" Join。

3.1 事件时间时态 Join:还原"当时"的维度状态

典型例子就是"订单 + 汇率":

订单按下单时刻的汇率换算成 USD,而不是按当前最新汇率。

DDL 示例前面已经翻译过,这里只看核心 SQL:

sql 复制代码
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_time 有 watermark;
  • 右侧是版本化汇率表,主键 currencyupdate_time 有 watermark;
  • 对每一条订单,用"订单的事件时间 order_time"去汇率表里查找当时版本
  • 之后无论汇率如何变化,之前的 Join 结果都不会被更新。

注意两点:

  1. Join 条件必须包含右表主键的等值条件:
    orders.currency = currency_rates.currency

  2. watermark 的设置会决定:

    • 等多长时间才认为"某个时间点之前的版本可以清理";
    • 系统在等待乱序事件时的容忍度。

3.2 处理时间时态 Join:以"当前最新值"做维度补全

处理时间时态 Join 更接近"Lookup" 的语义:

每条事实流数据来到时,用当前最新的维度数据来补全字段。

典型用法:

sql 复制代码
SELECT
  o_amount, r_rate
FROM
  Orders,
  LATERAL TABLE (Rates(o_proctime))
WHERE
  r_currency = o_currency;

特点:

  • 不会保留维度表的历史版本;
  • 同一个 key 不同时间 Join,得到的结果可能不同(最新值);
  • 结果是非确定的,更适合"实时看当前状态"的场景,而不是"严格还原历史状态"。

4. Lookup Join:最常用的"流 + 外部维表"补全方式

Lookup Join 基本可以视为"处理时间时态 Join + 外部维表 Connector"。

最常见的场景:

实时行为/订单流 + MySQL/HBase/Redis 中的维度表,做字段补全。

DDL 示例:

sql 复制代码
CREATE TEMPORARY TABLE Customers (
  id INT,
  name STRING,
  country STRING,
  zip STRING
) WITH (
  'connector' = 'jdbc',
  'url' = 'jdbc:mysql://mysqlhost:3306/customerdb',
  'table-name' = 'customers'
);

SELECT o.order_id, o.total, c.country, c.zip
FROM Orders AS o
  JOIN Customers FOR SYSTEM_TIME AS OF o.proc_time AS c
    ON o.customer_id = c.id;

这里面有几个关键信息:

  • Orders 是主流,带 proc_time

  • Customers 是 Lookup 表,由 jdbc connector 提供;

  • FOR SYSTEM_TIME AS OF o.proc_time 表示:

    • 在处理该订单的那一刻,从 MySQL 读一行数据;
    • 后面 MySQL 里的数据变了,也不会回刷历史 Join 结果。

Lookup Join 的典型特性:

  • 延迟取决于外部系统的响应时间;
  • 不保留历史版本,仅使用当前最新记录;
  • Join 条件必须是等值条件,类似主键或索引字段。

5. UNNEST / 数组 & Map & Multiset 展开

在业务里经常会看到 "一行里面嵌一个数组/Map",比如:

  • 一条订单带多个商品;
  • 一条记录里面有一个 Map 或 multiset 统计结构。

UNNEST 就是用来把这些嵌套结构拆平的利器。

5.1 数组展开

sql 复制代码
-- 常量数组展开
SELECT * FROM (VALUES('order_1')), UNNEST(ARRAY['shirt', 'pants', 'hat']);

-- 表字段数组展开
SELECT order_id, product_name
FROM Orders
CROSS JOIN UNNEST(product_names) AS t(product_name);

5.2 WITH ORDINALITY:顺带把下标也拿出来

sql 复制代码
SELECT * 
FROM (VALUES ('order_1'), ('order_2'))
    CROSS JOIN UNNEST(ARRAY['shirt', 'pants', 'hat']) 
        WITH ORDINALITY AS t(product_name, index);
  • index 从 1 开始;
  • 对数组:顺序有保证;
  • 对 Map / multiset:顺序不保证。

Map 展开时:

sql 复制代码
SELECT *
FROM 
    (VALUES('order_1'))
        CROSS JOIN UNNEST(MAP['shirt', 2, 'pants', 1, 'hat', 1]) WITH ORDINALITY;

multiset 展开时,如果某个元素 multiplicity 为 2,就会返回两行。

6. 表函数 Join:一拖多的"动态展开"

表函数 Join + LATERAL TABLE 的语义是:

左表每一行作为参数,调用一次表函数,将返回的多行和这行 Join。

6.1 Inner Join 表函数

如果表函数返回空结果,就直接丢弃这一行:

sql 复制代码
SELECT order_id, res
FROM Orders,
LATERAL TABLE(table_func(order_id)) t(res);

6.2 Left Outer Join 表函数

如果表函数返回空结果,左侧行保留,右侧填 NULL

sql 复制代码
SELECT order_id, res
FROM Orders
LEFT OUTER JOIN LATERAL TABLE(table_func(order_id)) t(res)
  ON TRUE;

常见用法:

  • 对某一字段做拆分、解析(比如 JSON → 多行、多列);
  • 对一行文本切词,输出多行关键词;
  • 对一行调用外部服务,返回多行候选结果。

7. 如何给 Join"选型"?一张表看清楚

场景诉求 推荐 Join 类型
任意两张动态表全量 Join,语义最接近离线 SQL Regular Join
两个事件流按 ID + 时间区间 Join Interval Join
事实流回放某一时刻的维表版本 Event Time Temporal Join
事实流实时按当前最新维表信息做补全 Processing Time Temporal Join / Lookup Join
用数组/Map/multiset 拆平成多行 UNNEST +(可选)WITH ORDINALITY
每行调用一个 UDTF 返回多行并 Join LATERAL TABLE / 表函数 Join

再加一句非常关键的实战经验:

Regular Join 和无脑多表 Join 是生产事故高发区。

在流式场景下,一定要考虑:

  • 状态大小;
  • watermark 与 TTL;
  • 是否可以改写为 Interval Join / Temporal Join / Lookup Join;
  • 或者增加窗口约束,将 Join 范围限制在"业务合理的时间段"。

8. 小结

这篇我们从"语义 + 代价 + 典型场景"的角度,把 Flink SQL 的 Join 家族梳理了一遍:

  • Regular Join:最像离线 SQL 的 Join,但状态压力最大;
  • Interval Join:用时间区间限制 Join,控制状态大小;
  • Temporal Join:按时间版本从维度表里取"当时的值",是流式数仓里非常核心的能力;
  • Lookup Join:事实流实时补充外部维表信息的主力方案;
  • UNNEST & 表函数 Join:负责把半结构化/嵌套数据"拆碎摊平"。
相关推荐
learning-striving2 小时前
eNSP静态路由配置完整实验
网络·智能路由器·ensp·静态路由
路边草随风2 小时前
java实现发布flink k8s application模式作业
java·大数据·flink·kubernetes
zuozewei2 小时前
南方区域虚拟电厂网络安全系列政策
网络·安全·web安全
月亮!2 小时前
智能合约的安全验证实践
网络·人工智能·python·测试工具·安全·自动化·智能合约
Mars.CN2 小时前
obs-websocket 5.x.x Protocol 全中文翻译
网络·websocket·网络协议
曲幽2 小时前
Flask数据库操作进阶:告别裸写SQL,用ORM提升开发效率
python·sql·sqlite·flask·web·sqlalchemy
kyle~2 小时前
Linux---scp 安全文件传输
linux·网络·安全
路边草随风2 小时前
java实现发布flink yarn session模式作业
java·flink·yarn
翼龙云_cloud2 小时前
阿里云渠道商:无影云手机配置虚拟网络(VPC)常见问题有哪些?
网络·阿里云·智能手机