PostgreSQL逻辑复制默认会把发布表中所有满足发布条件的行变更发送到订阅端,但生产环境很少需要同步整张表。例如订单系统可能只需要把华东区订单同步到下游分析库,或者只需要将最近三个月活跃用户的行为日志同步到数仓。这个时候,如果继续全量发布,不仅占用网络带宽,订阅端还要回放大量无关事务,延迟和资源消耗都会上升。PostgreSQL从10版本开始把逻辑复制作为内置能力,并在后续版本中不断完善行过滤与列过滤策略。通过给发布项设置WHERE条件,发布端可以只解码并输出匹配条件的数据行;配合列列表,还可以进一步裁剪传输字段。文章将围绕过滤策略在增量同步中的设计方法、限制和验证过程展开。

一、发布端行过滤与列过滤的配置方式
在PostgreSQL逻辑复制中,发布是逻辑复制的数据源集合。创建发布时可以指定要复制的表,也可以为表添加行过滤表达式。行过滤只作用于逻辑解码阶段,数据库会根据WHERE条件判断一行变更是否需要发送给订阅端。如果一行INSERT或UPDATE的新值不满足条件,这条变更就不会进入复制流。这种过滤发生在发布端,因此订阅端不会收到任何无关数据,减少了网络传输和订阅端写入压力。
一个基础的发布语句如下:
-- 创建只发布北方区域订单的发布 CREATE PUBLICATION pub_orders_north FOR TABLE orders WHERE (region = 'north');
这里WHERE条件中的region必须是表上的普通列。行过滤要求表达式满足发布端约束,例如不能包含子查询,不能使用易变函数,也不能引用其他表。条件表达式会在逻辑解码时被重新执行,所以应保证其结果是稳定的。如果同步任务涉及多张表,可以在同一个发布中为不同表设置不同过滤条件,也可以单独创建多个发布,再让订阅端按需订阅。
除了创建时指定,还可以通过ALTER PUBLICATION修改过滤条件。例如把过滤范围从北方区域扩展到北方和华东区域:
ALTER PUBLICATION pub_orders_north SET TABLE orders WHERE (region IN ('north', 'east'));
修改后,之后产生的增量数据会按照新条件判断。但是已经复制过的数据不会被自动撤销,发布过滤条件也不是时间点快照。如果过滤条件从宽变窄,之前已经发送到订阅端的数据行不会自动删除,需要业务层处理。这个行为在做增量同步和重新划分数据范围时需要特别留意。
行过滤控制哪些行,列过滤控制哪些列。PostgreSQL 15开始支持在发布项中声明列列表。列列表会影响逻辑解码输出的字段集合,订阅端目标表只需要包含这些列。比如只需同步订单ID、客户ID、金额和状态,不同步备注等大字段:
CREATE PUBLICATION pub_orders_light FOR TABLE orders (id, customer_id, amount, status) WHERE (status = 'active');
列过滤和行过滤可以同时使用。列列表中的列必须在表上存在,发布列列表不会改变源表结构,只影响复制流。值得注意的是,如果订阅端目标表包含其他列,这些列需要有默认值,否则INSERT消息会因为缺少列而失败。另外,列列表不是主键替代品,复制标识仍然建议使用主键或唯一索引,否则UPDATE和DELETE可能无法在订阅端匹配目标行。
二、增量同步中过滤条件如何影响行变更语义
行过滤最容易被忽略的是UPDATE语句的复制语义。在发布端,逻辑解码会同时看到更新前的旧行和更新后的新行。发布端会分别用WHERE条件判断旧行和新行,然后决定发送什么类型的变更。假设订阅只关注状态为active的订单,而某行订单状态从active变为closed,那么它就不再属于过滤范围。逻辑复制不会发送一个UPDATE给订阅端,而是发送一个DELETE,因为从订阅端视角看,这条行应该从目标表中消失。反过来,如果状态从closed变为active,订阅端会收到一个INSERT。只有当旧行和新行都满足过滤条件时,才会发送UPDATE。
这个行为对增量同步非常重要。如果订阅端只是想维护一个活动订单表,那么上面的DELETE和INSERT正好符合预期。但如果订阅端还依赖UPDATE的旧值做审计或触发逻辑,就需要额外设计。例如可以在源表添加触发器记录状态变化,或者使用不同的发布策略,把状态字段排除在过滤条件之外。否则订阅端无法区分真正的删除和因过滤条件导致的离开数据集。
下面可以通过一个简单实验验证这个行为。先在源库创建表并发布,然后在订阅端观察收到的变更。假设发布条件为status等于active:
-- 源库
CREATE TABLE orders (
id integer PRIMARY KEY,
customer_id integer,
amount numeric,
status text
);
CREATE PUBLICATION pub_active FOR TABLE orders WHERE (status = 'active');
如果源库执行UPDATE orders SET status = 'closed' WHERE id = 1,且该行原本满足active条件,那么订阅端收到的不是UPDATE,而是DELETE。可以从订阅端目标表的最终结果验证:该行被删除。同理,如果执行UPDATE orders SET status = 'active' WHERE id = 1,且该行原本不满足条件,订阅端会执行INSERT。这个机制可以帮助我们利用过滤条件实现订阅端的动态分区,但前提是理解旧行和新行的两次判断逻辑。
三、初始化快照与复制标识的避坑要点
一个常见理解误区是,创建带行过滤的发布后,初始化订阅也会只复制符合过滤条件的行。实际上,PostgreSQL逻辑复制在初始化同步阶段,默认使用COPY把发布表的全部数据复制到订阅端,行过滤条件不会应用到初始快照。也就是说,如果订阅启动时copy_data为true,目标表会先收到整张表的数据,之后的增量才按照过滤条件发送。对于需要只同步部分历史数据的场景,这个默认行为显然不符合预期。
解决办法通常有两种。一种是在创建订阅时关闭初始数据复制,即设置copy_data为false,然后手工在订阅端建立目标表并导入符合过滤条件的数据。这样可以保证初始数据范围和后续增量范围一致。另一种是先使用其他工具或SQL在源端筛选出历史数据,再导入订阅端,然后创建逻辑复制。创建关闭初始复制的订阅语句如下:
CREATE SUBSCRIPTION sub_orders CONNECTION 'host=127.0.0.1 port=5432 user=repluser dbname=sourcedb' PUBLICATION pub_active WITH (copy_data = false);
连接串中的host、port等需要根据环境替换。使用copy_data=false后,订阅端不会自动建表,也不会自动同步历史数据。因此必须提前在订阅端创建好与发布列匹配的目标表和必要的索引。目标表结构不要求完全相同,但至少包含发布列列表中的所有列,且列名尽量一致,否则映射关系会变得复杂。
复制标识则是另一个关键点。逻辑复制默认使用主键作为行的复制标识,订阅端依靠主键来定位UPDATE和DELETE要修改的行。如果发布表没有主键,也没有唯一索引,UPDATE和DELETE会报错,或者需要设置REPLICA IDENTITY FULL。FULL模式会使用整行旧值做定位,代价较高。行过滤条件如果引用的是非主键列,不影响复制标识本身,但跨过滤边界产生的DELETE和INSERT同样需要主键。如果只发布部分列,列列表中应包含主键列,否则订阅端无法正确回放变更。因此,在设计增量过滤策略时,源表的主键和唯一索引是前提条件。
四、修改过滤策略与验证同步范围
业务需求变化时,需要调整发布的行过滤条件。PostgreSQL支持使用ALTER PUBLICATION ... SET TABLE ... WHERE来修改已有表的过滤表达式。例如现在只需要status为active且region为east的订单:
ALTER PUBLICATION pub_active SET TABLE orders WHERE (status = 'active' AND region = 'east');
修改后,逻辑解码会立即采用新条件,但历史数据不会自动回滚。如果新条件变窄,之前已经复制到订阅端的行可能仍然存在,造成两端范围不一致。这时可以停止订阅并重新初始化目标表,或者在订阅端执行与过滤条件一致的DELETE清理多余数据。更稳妥的方案是使用分区表或分片机制,让每个发布只服务于固定范围,避免频繁修改过滤条件。
验证过滤策略是否生效,可以从几个系统视图入手。pg_publication_tables视图提供了发布表的行过滤表达式和列列表。以下查询可以查看发布pub_active关联的表及其过滤信息:
SELECT
p.pubname,
pt.schemaname,
pt.tablename,
pt.attnames,
pt.rowfilter
FROM pg_publication_tables pt
JOIN pg_publication p ON p.pubname = pt.pubname;
注意pg_publication_tables在不同版本中的字段略有差异,PostgreSQL 15以后才有rowfilter和attnames字段。如果使用较旧版本,可以查看pg_publication_rel获取发布关系,但无法直接读取过滤表达式。还可以在订阅端查询pg_stat_subscription视图,观察接收到的变更数量和应用延迟:
SELECT
subname,
received_lsn,
latest_end_lsn,
last_msg_send_time,
last_msg_receipt_time
FROM pg_stat_subscription;
通过对比源端发布表在过滤条件下的行数和订阅端目标表的行数,可以判断初始数据是否一致。如果有差异,可能是初始快照时copy_data设置错误,或者过滤条件修改后没有清理历史数据。建议在重要同步任务上线前,先在一张测试表上模拟不同UPDATE边界、修改过滤条件等操作,确认日志中的行变更类型符合预期。
PostgreSQL逻辑复制行过滤增量同步修改时间:2026-09-21 22:57:32