<1> look up join
Paimon支持Lookup Join语法,它用于从 Paimon 查询的数据来补充维度字段。要求一个表具有处理时间属性,而另一个表由查找源连接器支持。
Paimon 支持 Flink 中具有主键的表和append-only的表查找联接。以下示例说明了此功能。
要求:Flink是事实表,paimon存的是维度表
1) Flink声明一个事实表(随机生成数据)
CREATE TEMPORARY TABLE Orders (
order_id INT,
total INT,
customer_id INT, -- 关联字段
proc_time AS PROCTIME() -- 处理时间属性(关键) 事件时间语义不支持lookupjoin
) WITH (
'connector' = 'datagen',
'rows-per-second'='1',
'fields.order_id.kind'='sequence',
'fields.order_id.start'='1',
'fields.order_id.end'='1000000',
'fields.total.kind'='random',
'fields.total.min'='1',
'fields.total.max'='1000',
'fields.customer_id.kind'='random',
'fields.customer_id.min'='1',
'fields.customer_id.max'='3'
);
2) paimon声明一个维度表
USE CATALOG fs_catalog;
CREATE TABLE customers (
id INT PRIMARY KEY NOT ENFORCED,
name STRING,
country STRING,
zip STRING
);
# 往维度表中
条数据
INSERT INTO customers VALUES(1,'zs','ch','123'),(2,'ls','ch','456'), (3,'ww','ch','789');
3) 开始lookupjoin
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 -- 关键语法,指定查询维表的事实表时间戳proc_time,确保关联的数据是最新的
ON o.customer_id = c.id;
底层原理: Lookup Join算子会在本地维护一个RocksDB缓存区并实时拉取表的最新更新。查找连接运算符只会提取必要的数据,因此您的过滤条件对于性能非常重要。
如果Orders(主表)的记录Join缺失,是因为customers(维度表)对应的数据还没有准备好。
可以考虑配置Flink的Delayed Retry Strategy For Lookup(lookup的延迟重试策略)
- 维表缓存:Flink 默认缓存维表数据(LRU 策略),减少对 Paimon 的频繁查询。
- 一致性保证:处理时间语义下,维表更新可能延迟生效,关联结果非严格精确(最终一致)。
- 主键优化:若 Paimon 表有主键,Lookup 直接通过主键索引查询,效率接近 O(1);若为追加表,需全表扫描(性能差,不推荐)。
优化:
- 缓存调优
-- 在 Flink SQL 中设置维表缓存参数
SELECT /*+ LOOKUP('table'='c', 'cache'='ALL', 'cache-ttl'='5min') */
FROM orders o
JOIN customers
FOR SYSTEM_TIME AS OF o.proc_time AS c ...
cache:缓存策略(ALL全量缓存,LRU部分缓存)。cache-ttl:缓存过期时间(需短于维表更新周期)。
- 异步查询 启用异步 I/O 减少 Join 延迟:
SET 'table.exec.async-lookup' = 'true';
SET 'table.exec.async-lookup.buffer-capacity' = '1000'; -- 异步缓冲区大小
- 分区剪枝 若 Paimon 表按
country分区,查询时优先过滤分区:
SELECT ... FROM orders o
JOIN customers FOR SYSTEM_TIME AS OF o.proc_time AS c
ON o.customer_id = c.id AND c.country = 'US'; -- 分区过滤条件
<2> Retry Lookup 重试
问题背景:当流表(如 orders)与维表(如 customers)关联时,若维表数据未及时更新(如延迟写入),可能导致关联失败(lookup_miss)。 解决方案:通过 Flink 的 延迟重试策略 对缺失的维表数据进行多次重试。
1. 同步重试配置
SELECT /*+
LOOKUP('table'='c',
'retry-predicate'='lookup_miss', -- 仅在关联缺失时重试
'retry-strategy'='fixed_delay', -- 固定间隔重试
'fixed-delay'='1s', -- 每次重试间隔1秒
'max-attempts'='600' -- 最大重试600次(总等待时间=600*1s=10分钟)
) */
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;
- 适用场景: 维表更新延迟可控(如分钟级),且主表数据允许短暂延迟处理(如离线补数场景)。
- 局限性: 同步阻塞:若某条记录持续重试,后续记录会被阻塞,影响整体吞吐量。
2. 异步重试优化
SELECT /*+
LOOKUP('table'='c',
'retry-predicate'='lookup_miss',
'output-mode'='allow_unordered', -- 允许无序输出(不阻塞后续记录)
'retry-strategy'='fixed_delay',
'fixed-delay'='1s',
'max-attempts'='600'
) */
o.order_id, o.total, c.country, c.zip
FROM orders AS o
JOIN customers /*+
OPTIONS(
'lookup.async'='true', -- 启用异步查询
'lookup.async-thread-number'='16' -- 异步线程数(根据CPU核数调整)
) */
FOR SYSTEM_TIME AS OF o.proc_time AS c
ON o.customer_id = c.id;
-
核心优势:
-
非阻塞处理:异步线程池执行重试,主处理线程继续处理后续记录。
-
无序输出:允许先处理成功的记录提前输出,无需等待重试中的记录。
-
注意事项:
-
CDC流限制:若主表是 CDC 流(如 MySQL Binlog),
allow_unordered会被 Flink 忽略,导致异步失效。 -
解决方案:通过 Paimon 的 Audit Log 系统表 将 CDC 流转换为追加流:
-- 创建基于 Audit Log 的追加流视图
CREATE VIEW customers_append
AS SELECT * FROM customers$audit_log
WHERE rowkind = '+I';
<3> Dynamic partition 动态分区
问题背景:传统数仓中,分区表(如按天分区)的 Lookup Join 通常只需关联最新分区数据。Paimon 通过 max_pt() 特性自动识别最新分区,避免全表扫描。
1. 动态分区表定义
CREATE TABLE customers (
id INT,
name STRING,
country STRING,
zip STRING,
dt STRING, -- 分区字段(如按天分区)
PRIMARY KEY (id, dt) NOT ENFORCED
) PARTITIONED BY (dt);
2. Lookup Join 动态分区查询
SELECT o.order_id, o.total, c.country, c.zip
FROM orders AS o
JOIN customers /*+
OPTIONS(
'lookup.dynamic-partition'='max_pt()', -- 自动选择最新分区
'lookup.dynamic-partition.refresh-interval'='1 h' -- 每1小时刷新最新分区
) */
FOR SYSTEM_TIME AS OF o.proc_time AS c
ON o.customer_id = c.id;
-
工作原理:
-
自动刷新:每隔
refresh-interval时间,查询服务会检测最新分区(如dt='2023-10-01')。 -
分区剪枝:Lookup Join 仅查询最新分区的数据,减少扫描数据量。
-
-
适用场景: 维表按时间分区,且仅需关联最新分区数据(如 T+1 更新的用户画像表)。
<4> Query Service 类似缓存
问题背景:高并发 Lookup Join 场景下,频繁查询 Paimon 表可能导致性能瓶颈。通过启动 常驻 Flink 流作业 作为查询服务,缓存热数据并加速查询。
1. 启动查询服务
-- 启动并行度为4的查询服务
CALL sys.query*service('my*db.customers', 4);
2. Lookup Join 自动优化
当 Query Service 运行时:
-
优先查询服务:Flink Lookup Join 会优先从 Query Service 的内存缓存中获取数据。
-
缓存热数据:Query Service 会预加载 Paimon 表数据到内存,并定期更新(类似物化视图)。
3. 性能对比
| 场景 | 直接查询 Paimon 表 | 使用 Query Service |
|---|---|---|
| 查询延迟 | 10~100 ms | 1~5 ms |
| 吞吐量 | 1k~10k QPS | 10k~100k QPS |
| 适用场景 | 低并发、冷数据 | 高并发、热数据 |
<5> Large Scala Lookup(Fixed Bucket)
默认情况下,Flink Lookup Join 中每个子任务(subtask)会缓存整个维表(customers)的全量数据:
- 若维表数据量极大(如千万 / 亿级),单 subtask 内存无法承载,会导致 OOM 或性能急剧下降;
- Fixed Bucket 类型的 Paimon 表(提前指定分桶数、分桶键的表)天然具备数据分桶特性,可基于分桶优化 Lookup 逻辑。
解决方案:Large Scala Lookup(Fixed Bucket)该优化的核心是基于 Paimon 表的分桶策略,让每个 Flink Subtask 仅缓存维表的一个 / 部分分桶数据,而非全量,从而降低单节点内存压力。
SELECT /*+ LOOKUP('table'='c', 'shuffle'='true') */ # 只针对固定分桶且Flink2.0+的Paimon表
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;
<6> 应用场景
1. 高吞吐低延迟场景
- 异步查询 + Query Service
CALL sys.query_service('my_db.customers', 8);
SELECT /*+
LOOKUP('table'='c', 'output-mode'='allow_unordered') */
o.*, c.*
FROM orders o
JOIN customers /*+ OPTIONS('lookup.async'='true') */
FOR SYSTEM_TIME AS OF o.proc_time AS c
ON o.customer_id = c.id;
2. 维表延迟更新场景
- 异步重试 + Audit Log
CREATE VIEW customers_append AS
SELECT * FROM customers$audit_log WHERE rowkind = '+I';
SELECT /*+
LOOKUP('retry-strategy'='fixed_delay', 'max-attempts'='300') */
o.*, c.*
FROM orders o
JOIN customers_append /*+ OPTIONS('lookup.async'='true') */
FOR SYSTEM_TIME AS OF o.proc_time AS c
ON o.customer_id = c.id;
3. 动态分区场景
- 动态分区 + 定期刷新
SELECT o.order_id, c.country
FROM orders o
JOIN customers /*+
OPTIONS(
'lookup.dynamic-partition'='max_pt()',
'lookup.dynamic-partition.refresh-interval'='5 min'
) */
FOR SYSTEM_TIME AS OF o.proc_time AS c
ON o.customer_id = c.id;
4. 固定分桶、数据量极大场景(Flink2.0+)
- Large Scala Lookup
SELECT /*+ LOOKUP('table'='c', 'shuffle'='true') */ # 只针对固定分桶且Flink2.0+的Paimon表
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;