PAYATHON 2026

Datastream 的 PostgreSQL DELETE 事件为何只有主键?

数据阿明

在表使用 REPLICA IDENTITY DEFAULT、主键只有 event_id 的前提下,DELETE 事件只有主键值,没有 order_id,符合 PostgreSQL 逻辑复制的预期。示例中的非主键字段为 null,不意味着这些字段在删除前就是数据库中的 NULL。

如果保持当前主键结构不变,不能直接将 REPLICA IDENTITY FULL 作为受支持的 Datastream 解决方案。Google Cloud 当前文档明确要求,有主键的 PostgreSQL 表使用 DEFAULT。下游若必须取得 order_id,通常需要维护主键映射,或在删除事务中另行记录业务事件。Datastream PostgreSQL 支持说明

为什么 DELETE 没有非主键字段?

PostgreSQL 通过 REPLICA IDENTITY 决定逻辑复制所需的旧行信息。DEFAULT 记录主键列的旧值,FULL 则记录整行各列的旧值。PostgreSQL REPLICA IDENTITY 文档

按题目中的表结构和默认配置,删除事件可以携带 event_id 来标识行,但不必携带 order_id、event_type 或时间字段的旧值。NOT NULL 约束限制的是源表数据,不保证 DELETE 消息中一定提供对应字段。

Avro 示例中的 null 应理解为“没有提供该字段的旧值”,不能据此把下游已有的业务字段覆盖为空。

先确认表的实际配置

在源数据库执行以下只读查询:

SELECT
    c.oid::regclass AS table_name,
    c.relreplident AS replica_identity,
    pg_get_constraintdef(p.oid) AS primary_key_definition
FROM pg_class AS c
LEFT JOIN pg_constraint AS p
    ON p.conrelid = c.oid
   AND p.contype = 'p'
WHERE c.oid = 'public.example_events'::regclass;

relreplident 的常见值为:

  • d:DEFAULT。
  • i:USING INDEX。
  • f:FULL。
  • n:NOTHING。

如果结果为 d,且主键定义为 PRIMARY KEY (event_id),就能解释题目中的现象。仅凭提供的 Avro 样例,还不能确认源表实际使用哪一种配置。

能否配置 FULL 或额外的复制索引?

PostgreSQL 支持以下语法:

ALTER TABLE public.example_events REPLICA IDENTITY FULL;

但这条 SQL 能在 PostgreSQL 中执行,不代表它适用于当前 Datastream 链路。按照 Datastream 当前公布的限制,有主键表必须使用 DEFAULT,因此不应将上述命令作为这个问题的修复步骤。

Datastream 文档也介绍了无主键表使用合格唯一索引作为 replica identity 的方式,但这不能直接套用到题目中已有主键的表。文档还说明,Datastream 会将 replica identity 指定的列视作主键,因此改变它也可能影响下游的键语义。Datastream 配置建议与限制

下游怎样取得 order_id?

维护 event_id 到 order_id 的映射

如果下游已经消费初始数据、INSERT 和 UPDATE,可以维护一份持久化映射:

初始数据 / INSERT / UPDATE:
    保存 event_id -> order_id

DELETE:
    用 event_id 查询映射
    取得 order_id 后构造删除消息并完成路由
    在处理成功后更新映射状态

这个方案依赖映射完整且与事件进度一致。需要处理初始数据与增量衔接、重复投递、事件乱序,以及 order_id 更新的情况。映射缺失时应进入补偿流程,不能猜测路由值。

删除后再回源表查询通常无法补齐,因为对应行已经不存在。另外,拿到 order_id 只解决了路由信息问题,并不自动保证同一订单的事件处理顺序。

在删除事务中写入 outbox

如果可以调整删除逻辑,可以把删除前所需的字段写入独立的 outbox 表,再消费该表的 INSERT 事件。以下是简化示例,表名和字段需要按实际业务调整:

CREATE TABLE public.example_event_delete_outbox (
    outbox_id bigserial PRIMARY KEY,
    event_id varchar(255) NOT NULL,
    order_id bigint NOT NULL
);

删除与写入 outbox 放在同一条 SQL 中:

WITH deleted AS (
    DELETE FROM public.example_events
    WHERE event_id = 'EVT_AF_002'
    RETURNING event_id, order_id
)
INSERT INTO public.example_event_delete_outbox (event_id, order_id)
SELECT event_id, order_id
FROM deleted;

下游可以从 outbox 的新增记录读取 event_id 和 order_id,无需依赖原表 DELETE 事件补齐非主键字段。使用前需要把 outbox 纳入 publication 和 Datastream 采集范围,并设计消费去重及清理策略。所有相关删除入口也需要执行相同逻辑,或由数据库触发器统一记录。

评估包含 order_id 的复合主键

如果业务数据模型适合,也可以评估将主键设计为 (event_id, order_id),让两个字段都属于默认复制标识。这会改变源表键结构,需要一并评估外键、event_id 单列唯一性和已有 CDC 消费逻辑。只增加一个普通索引,并不能保证 DELETE 会携带 order_id。

如何验证调整是否有效?

在测试表中执行一次 INSERT、UPDATE 和 DELETE,检查原始事件中的 source_metadata.primary_keys、change_type 及实际字段值。如果使用 outbox,还应验证删除回滚时不会留下对应的 outbox 记录,以及重复消费不会重复产生业务效果。

这些检查用于验证实际链路行为。具体配置是否受支持,仍应以 Datastream 的支持文档为依据。

备注:内容仅供参考。