逻辑复制是PostgreSQL基于WAL日志解析实现的数据同步机制,相比物理复制,它最大的优势是可以精确控制哪些表参与复制。不少团队在搭建主从同步或数据分发链路时,都会遇到同一个问题:数据库里有几百张表,真正需要同步到下游的可能只有十来张,全量推送不仅浪费带宽,还可能把敏感数据带到不该去的地方。这篇文章就来详细讲解PostgreSQL逻辑复制的过滤配置方法,涵盖表级过滤、发布端与订阅端的配合,以及PG 15引入的行级过滤特性。

一、理解PUBLICATION的表级过滤机制
逻辑复制的过滤核心在发布端,通过CREATE PUBLICATION语句指定哪些表需要发布。最基本的用法是逐个列出表名:
-- 只发布三张表
CREATE PUBLICATION my_pub FOR TABLE
orders,
customers,
products;这种写法是最精确的过滤方式,发布端只会把这三张表的变更写入发布流,其他表完全不受影响。需要注意的是,表名可以带上模式名,如果表分布在不同的schema下,建议显式写明public.orders、sales.orders这样的完整路径,避免歧义。
另一种写法是发布所有表:
CREATE PUBLICATION my_pub FOR ALL TABLES;
FOR ALL TABLES看似省事,但有两个隐患。第一,新建的表会被自动纳入发布范围,这在表结构频繁变动的业务里容易失控;第二,一旦使用了这个选项,后续就无法再通过ALTER PUBLICATION ... ADD TABLE来调整表清单,只能删掉重建发布。所以生产环境更推荐逐表列举的方式,虽然配置繁琐一些,但行为可预期、可审计。
发布创建好之后,还可以随时增删表:
-- 向已有发布中添加一张表 ALTER PUBLICATION my_pub ADD TABLE inventory; -- 从发布中移除一张表 ALTER PUBLICATION my_pub DROP TABLE products; -- 设置发布内容类型:只发布insert和update ALTER PUBLICATION my_pub SET (publish = 'insert, update');
上面的第三个例子展示了另一个维度的过滤——操作类型过滤。通过publish参数可以限制只发布insert、update、delete或truncate中的某几种操作,比如下游只做新增数据分析、不关心删除动作时,排除delete操作能避免误删数据。
二、订阅端与发布端的配合配置
过滤不只在发布端可以做,订阅端也有一定的控制能力。最典型的场景是同一个数据库上创建了多个发布,订阅端可以选择性订阅其中一部分:
-- 订阅端创建订阅,指定两个发布
CREATE SUBSCRIPTION my_sub
CONNECTION 'host=192.168.1.10 port=5432 dbname=proddb user=repl_user password=xxx'
PUBLICATION my_pub, audit_pub;
-- 后续调整订阅的发布列表
ALTER SUBSCRIPTION my_sub SET PUBLICATION my_pub, audit_pub, log_pub;很多初学者以为订阅端需要预先建好同名表,实际上逻辑复制要求数据同步开始前,订阅端必须已经存在结构相同的表,否则复制会报错。如果希望跳过初始数据拷贝只同步增量,可以加上WITH (copy_data = false)参数,这在表数据量巨大、通过其他方式已经同步过存量数据的场景下非常实用。
还有一个容易被忽略的点是权限。订阅使用的连接账号必须在发布端拥有REPLICATION权限或者超级用户权限,同时对发布的表要有SELECT权限(做初始拷贝时)和REPLICA IDENTITY相关的读权限。权限不足时典型报错是permission denied for table,排查时先检查pg_hba.conf里的replication条目,再核对账号授权。
对于表级过滤的完整性,建议配合复制标识来理解。逻辑复制依赖表的复制标识来定位UPDATE和DELETE对应的行,默认使用主键。如果发布表没有主键,UPDATE操作会报错cannot update table because it does not have a replica identity,此时要么加主键,要么执行ALTER TABLE t REPLICA IDENTITY FULL用整行数据做匹配,不过FULL模式的开销明显更高,生产上应谨慎使用。
三、PG 15的行级过滤(Row Filter)实战
PostgreSQL 15引入了行级过滤功能,可以把过滤粒度从表级细化到行级。语法是在CREATE PUBLICATION的表名后面加上WHERE子句:
-- 只发布status为active的订单
CREATE PUBLICATION active_orders FOR TABLE orders
WHERE (status = 'active');
-- 老发布也可以补加行过滤条件
ALTER PUBLICATION my_pub SET TABLE orders WHERE (region = 'north');行过滤条件在发布端求值,只有满足条件的行才会进入复制流。这意味着过滤逻辑集中在发布端,订阅端不需要做任何配合,多个订阅同一个发布时看到的行集合也是一样的。如果需要让不同订阅看到不同的数据子集,必须创建多个发布,各自指定不同的WHERE条件,这也是实现多租户数据分发的常见做法。
使用行过滤时有几个限制必须清楚。第一,WHERE表达式中只能引用该表自身的列,不能使用子查询、聚合函数或不可变的volatile函数;第二,初始数据拷贝阶段同样会应用过滤条件,所以存量数据也只同步满足条件的部分,这一点和很多人的预期不同;第三,如果UPDATE操作导致某行从满足条件变为不满足条件,发布端会向订阅端发送一条DELETE,反之则发送INSERT,行为是自洽的,但业务设计时要意识到这个特性。
排查行过滤是否生效,可以查看pg_publication_tables视图:
SELECT pubname, schemaname, tablename,
ptfilter.clause AS row_filter
FROM pg_publication p
JOIN pg_publication_rel pr ON p.oid = pr.prpubid
JOIN LATERAL (
SELECT pg_get_expr(pr.prqual, pr.prrelid) AS clause
) ptfilter ON true
JOIN pg_class c ON c.oid = pr.prrelid
JOIN pg_namespace n ON n.oid = c.relnamespace;另外提醒一点,行过滤虽然能减少传输数据量,但发布端仍需要对每条变更求值WHERE表达式,高写入量场景下会带来一定CPU开销。设计过滤条件时应尽量使用有索引支持的简单表达式,避免在大表上做复杂的函数计算。
四、常见问题与排查思路
配置过滤规则时最常见的报错是table name specified more than once,这是在发布表清单中重复列出了同一张表;另一个是订阅端找不到表,报relation does not exist,原因是订阅端缺少对应表或者表在不同schema下,逻辑复制匹配表时严格按照schema加表名进行,跨schema的同名表不会被自动映射。
如果复制卡住不动,可以按顺序检查几个环节:发布端执行SELECT * FROM pg_stat_replication查看wal sender状态;订阅端查询pg_stat_subscription确认apply worker是否正常运行;再看pg_replication_slots中槽位的滞后情况。过滤配置导致的典型问题是发布中移除了某张表,但订阅端不知道,此时需要在订阅端也执行ALTER SUBSCRIPTION my_sub REFRESH PUBLICATION刷新表清单,否则增量数据仍然按旧的表集合传输。
最后总结一下过滤配置的最佳实践:优先使用逐表列举而不是FOR ALL TABLES;行级过滤条件保持简单且可确定;结构变更前先评估复制链路的影响,加表或删表后记得同步刷新订阅;定期监控复制槽的磁盘占用,避免长期滞后的槽位撑爆pg_wal目录。把这些细节做到位,逻辑复制链路就能长期稳定运行了。
PostgreSQL逻辑复制PUBLICATIONSUBSCRIPTION修改时间:2026-09-01 11:03:00