PostgreSQL逻辑复制本身以发布和订阅为基础,发布端从WAL中解码出逻辑变更,通过复制槽发送给订阅端应用。要达到近实时同步,只靠增大workers数量还不够,更有效的是在发布端把不需要的表、行和列提前裁掉。比如一个百张表的业务库只需要同步订单表的最新状态,如果发布包含全部表,WAL的每个事务都会被完整解码并发送,网络和远端apply都会承受无谓压力。

PostgreSQL 10到14的逻辑复制只支持表级过滤,从15版本开始加入行过滤和列列表,形成了表、行、列三层过滤体系。下面从这三层拆开说明,并给出配合复制标识、监控延迟和避坑的完整思路。
一、表、行、列三层过滤策略
表级过滤是最基础的裁剪方式。创建发布时只包含需要同步的表,发布端就不会把其他表的WAL变更发送给订阅端。发布参数publish用来控制要复制的DML类型,默认值是insert、update、delete。如果业务只需要订阅订单表的增量快照,完全可以只发布insert和update,不复制delete。
-- 创建只包含订单相关表的发布 CREATE PUBLICATION pub_orders FOR TABLE orders, order_items WITH (publish = 'insert,update,delete'); -- 后续可以通过ALTER PUBLICATION添加或移除表 ALTER PUBLICATION pub_orders ADD TABLE order_logs; ALTER PUBLICATION pub_orders DROP TABLE order_items;
行级过滤从15版本开始支持,可以在发布中对单表附加WHERE条件。这个条件在发布端执行,只有满足条件的行变更才会被发送。举例来说,如果只需要同步中国区的用户数据,可以这样定义:
-- 仅发布region为cn的用户行
CREATE PUBLICATION pub_cn_users
FOR TABLE users
WHERE (region = 'cn');
-- 对已有发布添加带行过滤的表
ALTER PUBLICATION pub_orders
ADD TABLE orders
WHERE (status NOT IN ('archived'));行过滤条件虽然写法和SQL的WHERE类似,但它是给逻辑解码过程使用的,不是在订阅端应用时再做判断。因此过滤条件应该尽量简单、确定,避免使用不稳定函数或子查询。对于更新操作,发布端会同时检查新旧行,如果新行满足条件但旧行不满足,仍可能发送更新,这个行为需要结合复制标识理解,后面单独说明。
列过滤也是PG15引入的能力。发布表时可以指定列列表,只有列出的列会参与复制,其他列即使发生变化也不会发送。列列表中必须包含主键或复制标识所依赖的列,否则订阅端无法定位要更新的远端行。例如只同步用户表的id、邮箱和更新时间,可以这样写:
-- 只发布users表的三个列 CREATE PUBLICATION pub_user_basic FOR TABLE users (id, email, updated_at) WITH (publish = 'insert,update');
使用列过滤后,订阅端表结构里未发布的列必须允许NULL或有默认值,否则insert会被远端拒绝。三层过滤可以叠加使用,比如发布指定表、只复制某些列、再按行条件裁剪,能够显著减少网络流量和订阅端写入压力。
二、复制标识与过滤条件的联动
逻辑复制处理update和delete时,不是只发送变更后的新行,还需要通过旧行值在订阅端定位要修改或删除的记录。默认情况下,PostgreSQL使用主键作为复制标识。如果表没有主键,就需要设置REPLICA IDENTITY FULL或使用唯一索引来充当复制标识,否则更新和删除无法可靠复制。
-- 使用包含过滤列的唯一索引作为复制标识 ALTER TABLE users REPLICA IDENTITY USING INDEX idx_users_region_id; -- 没有合适索引时使用整行旧值 ALTER TABLE users REPLICA IDENTITY FULL;
当发布中存在行过滤时,复制标识的选择会直接影响到过滤判断的准确性。假设行过滤条件是region = 'cn',而复制标识只包含主键id,不包含region列。那么在用户从region = 'cn'更新为region = 'us'时,发布端需要用旧行的region判断是否命中过滤,但默认主键复制标识不会记录旧行的region值,这就可能导致过滤结果出错,甚至在某些情况下直接报错。为避免这种问题,如果行过滤引用了主键之外的列,最好把复制标识设置为包含这些过滤列的索引,或者在可控范围内使用REPLICA IDENTITY FULL。
初始同步阶段同样会应用过滤规则。创建订阅时如果设置copy_data = true,PostgreSQL会先做一次全量数据拷贝,这个拷贝会尊重发布中的表过滤、行过滤和列过滤。也就是说,如果发布只包含订单表的非归档数据,初始同步就只拷贝这部分数据,而不是先把整张表拉过去再删。这样可以大幅缩短首次同步时间,对近实时目标很有帮助。
三、近实时同步的调优与延迟监控
逻辑复制的延迟通常在毫秒到秒级,但糟糕的参数配置或大事务可能把延迟拖到分钟级甚至更高。发布端首先要确保wal_level为logical,并且预留足够的复制槽和发送进程。订阅端则需要提高逻辑复制工作者数量,让初始同步和应用阶段有更多资源可用。
-- 发布端postgresql.conf关键参数 wal_level = logical max_replication_slots = 10 max_wal_senders = 10 -- 订阅端postgresql.conf关键参数 max_logical_replication_workers = 8 max_sync_workers_per_subscription = 4
从PG16开始,还可以设置max_parallel_apply_workers_per_subscription来允许单个订阅使用多个并行应用工作者,这对大事务的流式应用有明显改善。但要注意,并行应用并不能解决大事务在发布端延迟发送的问题,因为逻辑解码通常在事务提交时才输出完整变更,一个执行几十分钟的大事务会让订阅端一直等不到数据。
监控延迟时,发布端可以直接观察复制槽的confirmed_flush_lsn与当前WAL位置之间的差距,这个差值反映的是尚未被订阅端确认刷写的WAL量。订阅端可以查看pg_stat_subscription中的最新消息时间、结束LSN等信息,辅助判断最近是否还在正常消费。
-- 发布端查看逻辑复制槽延迟
SELECT slot_name,
confirmed_flush_lsn,
pg_current_wal_lsn(),
pg_size_pretty(
pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)
) AS pending_lag
FROM pg_replication_slots
WHERE slot_type = 'logical';
-- 订阅端查看订阅状态
SELECT subname,
received_lsn,
latest_end_lsn,
last_msg_send_time,
last_msg_receipt_time,
latest_end_time
FROM pg_stat_subscription;要让同步接近实时,最实用的经验是把大批量操作拆分成多个小事务提交。比如一次清理一千万行数据,不要放在一个delete中执行,可以按主键范围每批5000到10000行提交一次。这样订阅端能够持续收到变更并应用,复制槽也不会因为长事务迟迟无法推进导致WAL大量累积。初始同步期间,适当调大订阅端的max_sync_workers_per_subscription可以加速全量拷贝,但增量阶段还是要靠发布端的小事务和稳定的网络。
四、过滤策略实践中的几个坑
逻辑复制虽然配置起来简单,但生产环境中有一些行为经常被误解。首先,DDL不会被逻辑复制同步。发布端新建表、修改表结构后,订阅端不会自动执行对应DDL,需要手动在订阅端执行相同的结构变更。发布中添加新表后,还要在订阅端执行ALTER SUBSCRIPTION ... REFRESH PUBLICATION才能让新表进入复制范围。
其次,修改行过滤条件不会自动修正订阅端已有数据。如果把过滤从region = 'cn'收窄为region = 'cn' AND active = true,订阅端已经存在的非活跃记录不会自动删除。反过来放宽过滤条件,原本被过滤掉的行也不会自动补回来。遇到这种情况通常需要重建订阅或使用临时表重新初始化,不能期望发布端自动回放历史数据。
另一个常见问题是序列值不会随逻辑复制同步。如果订阅端将来需要切换为独立写入节点,所有序列的当前值都必须手动同步,否则新插入的行会从旧的序列值开始,产生主键冲突。无主键表更新和删除也无法可靠复制,除非明确设置REPLICA IDENTITY FULL,而FULL又会放大WAL体积,需要结合过滤策略权衡。
最后,行过滤条件最好只使用确定性的列比较,不要加入子查询、随机函数或依赖当前事务时间的表达式。过滤表达式在解码过程中对每一行求值,任何不稳定因素都会让复制行为变得难以预测。列过滤则要检查订阅端表结构,确保未发布的列有默认值或允许NULL,否则insert会持续失败。把这些细节提前处理好,过滤策略才能真正帮助逻辑复制稳定地跑在近实时状态。
PostgreSQL逻辑复制复制过滤近实时同步修改时间:2026-09-24 23:06:55