PostgreSQL 逻辑复制在配置发布时,很多人会自然地认为:只要发布一张有继承关系的父表,所有子表都会跟着一起同步。这个想法对传统 INHERITS 表来说是错的,对声明式分区表来说又基本是对的。差异背后涉及发布集合如何根据表 OID 工作,以及 PostgreSQL 13 之后分区表发布行为的改变。要精确过滤继承表数据,必须分清楚表继承和表分区两类场景,然后选择表级过滤、行级过滤或发布单个分区。下面直接演示配置和验证过程。

先厘清传统继承表和分区表在发布中的差异
传统 INHERITS 继承子表在 PostgreSQL 内部是一个独立的物理表,拥有自己的 OID、自己的存储文件,只是通过系统目录记录了一条继承关系。逻辑复制的发布集合本质上是保存一组表的 OID,发布父表时只把这个父表的 OID 加进去,并不会自动遍历继承树把子表也加进来。这意味着如果你执行 CREATE PUBLICATION pub_inherit FOR TABLE parent;,然后查看 pg_publication_tables,里面只会有 parent,不会有任何 child。
下面的 SQL 可以验证这一点:
CREATE TABLE parent (
id INT PRIMARY KEY,
val TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE child_report () INHERITS (parent);
CREATE TABLE child_temp () INHERITS (parent);
-- 发布只包含父表
CREATE PUBLICATION pub_inherit FOR TABLE parent;
-- 查看发布中包含的表
SELECT schemaname, tablename
FROM pg_publication_tables
WHERE pubname = 'pub_inherit';
查询结果只有 parent 一行。此时向 child_report 插入数据,逻辑复制不会把这条变更发给订阅端。这个特性反过来说也很有用:它提供了一种天然的表级过滤手段,只要不把某张继承子表加进发布,它的数据就不会被复制。
声明式分区表的情况则不同。从 PostgreSQL 13 开始,如果发布一张分区表父表,系统会自动把当前所有分区以及未来新增的分区都纳入发布范围。这个行为让分区表的管理更简单,但也意味着如果你想排除某个分区,不能简单地依靠发布集合来过滤,必须使用行过滤或只发布单个分区。
传统继承表的表级过滤方法
如果目标是只复制父表和部分子表,而不复制另一些子表,那么最佳做法是在创建发布时使用 FOR TABLE 明确列出需要同步的表。不要使用 FOR ALL TABLES,因为它会把当前数据库里所有非系统表都加入发布,包括所有继承子表、普通表以及未来的新表,容易造成数据同步范围失控。
例如有 parent、child_report、child_temp 三张表,现在只想同步 parent 和 child_report,可以这样创建发布:
-- 只发布 parent 和 child_report,排除 child_temp CREATE PUBLICATION pub_filtered FOR TABLE parent, child_report;
如果发布已经存在,则可以用 ALTER PUBLICATION 动态增减表。比如原来发布包含 child_temp,现在要把它过滤掉:
-- 从发布中移除不需要的子表 ALTER PUBLICATION pub_filtered DROP TABLE child_temp; -- 后续需要新增同步表时再添加 ALTER PUBLICATION pub_filtered ADD TABLE child_report;
被 DROP TABLE 移出发布的子表,之后产生的 INSERT、UPDATE、DELETE 都不会被逻辑解码发送给订阅端。但要注意,已经同步到订阅端的历史数据不会因为表被移出发布而自动删除,表级过滤只影响后续变更。如果业务上确实需要删除订阅端残留数据,需要在订阅端手工清理。
当发布端表集合发生变化,尤其是新增了表时,订阅端需要执行 ALTER SUBSCRIPTION ... REFRESH PUBLICATION 来获取最新表结构并启动初始数据同步。否则新加入的表不会自动开始复制。
-- 在订阅端刷新发布表集合 ALTER SUBSCRIPTION sub_filtered REFRESH PUBLICATION;
刷新操作会从发布端读取新增表,创建订阅端缺少的表结构,并通过 COPY 进行初始数据拷贝。如果只想同步结构而不同步数据,还可以使用 WITH (copy_data = false) 参数。
分区表过滤:行过滤与按分区发布的区别
对于声明式分区表,发布父表会递归包含所有分区,因此无法用 ALTER PUBLICATION DROP TABLE 单独排除某一个分区。此时有两种主要思路:一是在发布上定义行过滤条件,让不符合条件的行不发送到订阅端;二是干脆不发布父表,只发布需要同步的具体分区表。
行过滤是 PostgreSQL 15 引入的能力,可以在创建发布时对表设置 WHERE 条件。例如 sales 表按日期分区,如果只想同步 channel 不是 archive 的数据,可以这样定义:
CREATE PUBLICATION pub_sales FOR TABLE sales WHERE (channel <> 'archive');
发布父表 sales 时,这个过滤条件会应用到所有分区上。如果不同分区需要不同的过滤条件,可以先把分区表单独加入发布,再对每个分区设置条件。不过这种方式会让发布集合失去分区父表的自动扩展能力,以后新增的分区需要手工加入发布。使用 publish_via_partition_root 参数也可以改变变更的传递方式,让订阅端以父表为单位接收数据,这样行过滤在父表层面统一生效,而不是分散到各个分区。
另一种做法是只发布某个分区:
-- 只发布 sales_2023 这个分区 CREATE PUBLICATION pub_sales_2023 FOR TABLE sales_2023;
这种方式的优点是精确控制同步范围,缺点是每个分区都要单独维护,而且未来新增分区不会自动进入发布。对于需要长期分区增长的系统,维护成本会明显上升。行过滤的方式虽然不能减少 WAL 产生,但可以保证发布范围稳定,不需要频繁调整表集合。
验证过滤效果与常见的几个坑
配置完成后,可以在订阅端检查数据行数是否符合预期。也可以在发布端查看 pg_publication_tables 是否只包含期望的表,或者使用 psql 的 \dRp+ 元命令查看发布详情和行过滤条件。订阅端的 pg_stat_subscription 视图可以查看复制延迟和最新接收位置,帮助判断变更是否已经到达。
一个特别容易踩的坑是:行过滤条件不会作用于初始数据同步。当订阅第一次创建时,已有数据无论是否满足行过滤条件,都会被完整复制到订阅端。也就是说,即使你在发布上设置了 channel <> 'archive',订阅端的初始表里仍然可能出现 channel 为 archive 的历史行。这些行只有在后续发生更新或删除时才会被过滤掉。因此如果业务要求严格的初始数据一致性,需要在订阅创建前清理源端或订阅端数据。
另一个常见问题是过滤掉继承子表后,订阅端如果还保留着对应的表结构,可能会造成误操作或维护混乱。建议将订阅端的表集合与发布端保持一致,不需要的表及时删除。此外,逻辑复制要求被复制的表有主键或唯一约束,否则 UPDATE 和 DELETE 无法找到目标行。对于继承表,尤其要检查子表是否继承了主键约束,因为 INHERITS 不会自动继承唯一约束和主键约束。
最后,发布表集合变更或行过滤条件调整后,订阅端到底要不要刷新,要看具体变化。添加表、删除表、修改表结构通常需要 REFRESH PUBLICATION;单纯修改行过滤条件不需要刷新即可对后续变更生效,但已经同步的数据仍保持旧状态。理解这些细节,才能在 PostgreSQL 逻辑复制中安全地过滤继承表,避免订阅端出现数据缺口或冗余数据。
PostgreSQL逻辑复制继承表修改时间:2026-09-29 15:56:37