逻辑复制自PostgreSQL 10正式引入以来,已经成为数据同步、读写分离、在线迁移等场景的主流方案。它基于发布订阅模型:发布端创建publication,订阅端创建subscription拉取WAL中的逻辑变更。但不少使用者在实践中会遇到同步结果不符合预期的情况,比如某些行没同步过去、某些表完全没数据、UPDATE语句丢失等。这些问题大多和过滤策略有关。PostgreSQL的过滤发生在两个层面:发布端决定哪些表、哪些列、哪些行需要发布,订阅端决定应用哪些变更。理解这两个层面的职责边界和行为差异,是准确控制数据流向的前提。

发布端表级过滤:三种发布方式的差异
CREATE PUBLICATION支持三种表级发布方式,行为差异明显。第一种是发布全部表:
-- 发布数据库中所有表(含未来新建的表) CREATE PUBLICATION pub_all FOR ALL TABLES;
这种方式最省事,但灵活性最差,而且无法叠加列过滤和行过滤,FOR ALL TABLES的发布不允许附加WITH子句之外的任何表级选项。第二种是指定表列表:
-- 仅发布两张表 CREATE PUBLICATION pub_tables FOR TABLE orders, customers;
第三种是发布schema下所有表(PostgreSQL 15开始支持):
CREATE PUBLICATION pub_schema FOR TABLES IN SCHEMA sales, marketing;
两者的关键差异在于动态性:FOR TABLE指定的是明确的表对象,后续在sales schema下新建的表不会自动进入FOR TABLE发布,但会自动进入FOR TABLES IN SCHEMA发布;而FOR ALL TABLES则覆盖所有schema的所有表。另一个容易被忽视的差异是REPLICA IDENTITY的要求:当发布中包含UPDATE或DELETE操作时,被发布的表必须有主键或唯一索引,否则发布端会直接报错。如果只发布INSERT,则没有这个限制。这一点在同步没有主键的历史表时尤其要注意。
还有一个运维层面的差异:FOR TABLE发布的表集合可以通过ALTER PUBLICATION动态增删,例如ALTER PUBLICATION pub_tables ADD TABLE products;,而FOR ALL TABLES发布不能再ADD或DROP表,只能整体删除重建。此外,同一个表可以被多个发布同时包含,订阅端会自动去重,不会重复应用变更,这是逻辑复制幂等性的基础。
行级过滤与列过滤:只对指定表发布有效
PostgreSQL 15引入了行过滤(Row Filter)和列过滤(Column List),这是过滤策略中变化最大的部分。先看基本语法:
-- 行过滤:只发布华北区域的订单 CREATE PUBLICATION pub_region FOR TABLE orders WHERE (region = 'north'); -- 列过滤:只发布指定列 CREATE PUBLICATION pub_cols FOR TABLE orders (id, amount, created_at); -- 组合使用 CREATE PUBLICATION pub_combo FOR TABLE orders (id, amount) WHERE (region = 'north');
这里有一个非常重要的限制:行过滤和列过滤只能写在FOR TABLE子句中,不能与FOR ALL TABLES或FOR TABLES IN SCHEMA混用。也就是说,如果你既想覆盖整个schema,又想对个别表加行过滤,必须拆成两个publication,订阅端同时订阅这两个发布即可,PostgreSQL会自动合并处理。
行过滤表达式的求值发生在发布端,且始终使用发布端的当前数据评估,这带来几个微妙的行为差异。第一,INSERT只在插入时评估一次,插入后即使region字段被UPDATE改成south,订阅端也不会自动删除已同步的行。第二,UPDATE的处理遵循一个不对称规则:如果旧行满足过滤条件但新行不满足,该UPDATE在订阅端会被转换为DELETE;如果旧行和新行都满足,正常应用UPDATE;如果旧行不满足,则跳过。第三,DELETE只有旧行满足条件才会被发布。这个不对称性是多机房数据分发场景中最容易踩的坑。
列过滤则要求被过滤掉的列不能是REPLICA IDENTITY的一部分。如果表的主键恰好在列清单之外,创建发布时会报错。列过滤还会影响订阅端表结构:订阅表可以有多余的列(这些列保持订阅端自己的值不被覆盖),但不能缺少被发布的列,否则应用进程会报错并中断。
订阅端过滤:参数与发布端策略的叠加关系
订阅端同样存在控制数据流的手段,主要包括subscribe选项和初始数据拷贝行为。先看创建订阅的标准形式:
CREATE SUBSCRIPTION sub_orders CONNECTION 'host=192.168.1.10 dbname=prod user=repl password=xxx' PUBLICATION pub_region WITH (copy_data = true, enabled = true);
copy_data决定初始同步阶段是否拷贝存量数据。这里有个隐蔽的交互问题:当行过滤与copy_data共存时,PostgreSQL 15之后初始拷贝也会遵守行过滤条件,只复制满足WHERE子句的行。但在PG 15之前没有行过滤概念,不存在这个问题。需要注意的是,如果订阅创建时copy_data为true,后续通过ALTER SUBSCRIPTION ... SET PUBLICATION ... WITH (refresh = true)新增发布,新发布的表也会触发一次数据拷贝,而refresh = false则不会,这个差异常常导致订阅端数据不完整。
enabled参数控制订阅是否整体启用,配合ALTER SUBSCRIPTION sub DISABLE和ENABLE可以在维护窗口暂停数据应用。此外,订阅端还可以通过ALTER SUBSCRIPTION ... SKIP LSN跳过出错的事务,这本质上是一种故障处置手段而非过滤策略,但在排查过滤引起的数据不一致时经常用到。
发布端过滤与订阅端控制的叠加遵循交集原则:只有被发布端选中且订阅端愿意应用的变更才会最终落库。排查数据缺失时应顺着这条链路检查:先用\dRp+查看发布的表清单、行过滤和列过滤定义,再到订阅端用\dRs+确认订阅状态和copy_data设置,最后检查pg_stat_subscription中应用进程的最新LSN,判断数据是被过滤掉了还是根本没有送达。
典型场景下的策略选择建议
对于按区域拆分数据的场景,推荐在发布端使用行过滤,而不是在订阅端写触发器删数据。发布端过滤能显著减少跨网络传输的WAL量,订阅端表也不会产生无谓的写放大。对于敏感字段脱敏场景,列过滤是唯一正确的选择,比如用户表只发布id和昵称,不发布手机号。对于分库分表迁移场景,建议采用多个细粒度publication加同一订阅的组合,便于逐表切换和回滚。
最后提醒一点:行过滤表达式在发布端每个事务都会求值,如果WHERE条件中的函数或表达式代价较高,会直接拖慢发布端的WAL处理速度,甚至造成复制延迟持续增长。过滤条件应尽量基于索引列或简单比较运算。理解了发布端与订阅端各自的过滤职责、表级与行级策略的组合规则,以及UPDATE在行过滤下的转换语义,你就能在复杂的逻辑复制拓扑中准确预测每一条数据的去向。
PostgreSQL逻辑复制发布订阅行级过滤修改时间:2026-08-31 23:17:15