Paimon的LookUpJoin

242 阅读4分钟

<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);若为追加表,需全表扫描(性能差,不推荐)。

优化:

  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:缓存过期时间(需短于维表更新周期)。
  1. 异步查询 启用异步 I/O 减少 Join 延迟:
SET 'table.exec.async-lookup' = 'true';
SET 'table.exec.async-lookup.buffer-capacity' = '1000'; -- 异步缓冲区大小
  1. 分区剪枝 若 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 ms1~5 ms
吞吐量1k~10k QPS10k~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;