PostgreSQL 从 15 版本开始支持在发布端为表指定行过滤条件(WHERE 子句)和列列表,这个能力让逻辑复制可以只下发部分行和部分列。但在实际运维中,过滤策略可能因为发布被删除重建、元数据误操作或迁移过程中只导出了表结构而丢失。恢复策略的第一步不是立刻重建,而是去系统目录里确认这些表达式原本存在的位置,以及当前还剩下哪些线索。

一、行过滤与列过滤的底层存储机制
发布对象本身只是逻辑复制的元数据,实际的行过滤表达式和列列表分别记录在pg_publication_rel系统表的prqual和prattrs字段中。prqual字段的类型是pg_node_tree,存储的是解析后的表达式树;prattrs字段则是int2vector,表示被发布列在表属性中的编号列表。通过视图pg_publication_tables可以直接看到这些信息,该视图把底层字段转换成更易读的形式,rowfilter列对应行过滤表达式,attnames列对应列名列表。
例如,当我们执行ALTER PUBLICATION sales_pub ADD TABLE public.orders (order_id, customer_id, amount) WHERE (status = 'active');之后,pg_publication_rel中就会为public.orders生成一行记录,prqual保存status = 'active'的表达式树,prattrs保存order_id、customer_id、amount对应的属性编号。理解这个存储位置很重要,因为后续恢复操作本质上就是依据这些字段重新生成DDL。
还要注意发布参数publish的作用。它决定该发布允许同步哪些操作类型,默认包含insert、update和delete,也可以显式加上truncate。过滤策略恢复时不能只看行过滤和列列表,还要确认publish参数是否与原发布一致,否则即使行过滤恢复正确,更新或删除操作也可能没有下发。
SELECT p.pubname,
n.nspname AS schema_name,
c.relname AS table_name,
CASE WHEN pr.prattrs IS NOT NULL
THEN (SELECT string_agg(a.attname, ', ' ORDER BY t.ord)
FROM unnest(pr.prattrs) WITH ORDINALITY AS t(attnum, ord)
JOIN pg_attribute a
ON a.attrelid = pr.prrelid
AND a.attnum = t.attnum)
ELSE NULL
END AS column_list,
pg_get_expr(pr.prqual, pr.prrelid) AS row_filter
FROM pg_publication p
JOIN pg_publication_rel pr ON pr.prpubid = p.oid
JOIN pg_class c ON c.oid = pr.prrelid
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE p.pubname = 'sales_pub';
二、过滤策略丢失的常见原因
最常见的原因是发布被删除后重建。很多清理脚本会直接DROP PUBLICATION再CREATE PUBLICATION,但重建时往往只列出了表名,忘记重新附加WHERE条件和列列表。这种情况在结构变更或复制冲突处理时尤其普遍。一旦重建完成,订阅端会立即按照无过滤的方式开始同步整表数据。
第二个原因是备份恢复的粒度问题。pg_dump默认会导出发布对象,但如果运维使用了仅导出表结构或者手动拼接建表语句的方式,发布相关的CREATE PUBLICATION和ALTER PUBLICATION ... ADD TABLE语句就可能被遗漏。尤其在跨大版本升级时,旧版本的工具可能不认识新语法,导致发布定义没有被正确还原。
第三个原因是直接修改系统目录或使用不完整的高可用切换方案。在逻辑复制场景中,发布端数据库发生故障切换后,如果备用节点没有同步pg_publication_rel中的变更,切换后就会丢过滤条件。手动执行UPDATE pg_publication_rel设置prqual虽然能绕过权限限制,但表达式树格式错误会导致发布无法使用,这也可以视为一种丢失。
最后一个容易忽视的场景是模式或表被重建。发布中的表如果通过DROP TABLE再CREATE TABLE的方式重建,OID会发生变化,原有的pg_publication_rel记录会失效或被清理。即使发布还在,表也需要重新添加,此时过滤条件自然也就不存在了。
三、从系统目录和备份中恢复过滤策略
如果发布仍然存在,只是部分表的过滤条件不完整,可以先通过第一节的查询找到同发布中其他表的过滤表达式,判断是否有模板可参考。如果整个发布都被删除,则需要借助备份文件。pg_dump生成的纯文本SQL中搜索CREATE PUBLICATION即可找到原始定义,包含行过滤和列列表的ALTER语句也会一并导出。例如使用pg_dump命令:
pg_dump -U postgres -d source_db --schema-only --no-owner --no-privileges \
| grep -A 30 "CREATE PUBLICATION"
如果备份是自定义格式,可以先用pg_restore -l列出内容,再用pg_restore -f还原包含发布定义的条目。找到原始DDL后,在目标库中重新执行即可恢复。注意执行顺序:必须先在发布端创建发布,再添加表,否则会报错。如果发布涉及的订阅已经存在,恢复发布定义后需要刷新订阅或让订阅端重新连接。
在没有备份的情况下,可以通过分析逻辑复制槽和WAL来间接判断下游曾经接收过哪些行,但这种方法无法精确恢复表达式,只能作为最后手段。更现实的做法是查看应用层的数据过滤规则,反推出原本的WHERE条件。比如订阅端表中有大量状态为inactive的行从未出现,那么原过滤条件大概率包含status = 'active'这一类判断。
重建过滤策略的常见SQL如下:
-- 创建发布,指定允许的操作类型
CREATE PUBLICATION sales_pub FOR TABLE public.orders
WITH (publish = 'insert,update,delete');
-- 给表添加行过滤和列列表
ALTER PUBLICATION sales_pub ADD TABLE public.orders (order_id, customer_id, amount)
WHERE (status = 'active' AND amount > 100);
如果需要修改已有表的过滤条件,可以使用ALTER PUBLICATION ... SET TABLE ... WHERE ...语法。例如:
ALTER PUBLICATION sales_pub SET TABLE public.orders (order_id, customer_id, amount)
WHERE (status = 'active' AND amount > 100);
四、验证恢复效果并避免再次丢失
恢复完成后不要直接认为问题解决,必须验证订阅端实际收到的数据范围。可以先在发布端插入一条不符合过滤条件的记录,观察订阅端是否没有同步;再更新一条符合过滤条件的记录,确认能正常下发。这种黑盒验证比单纯查询pg_publication_tables更可靠,因为行过滤表达式有时虽然存在,但可能因为类型不匹配或字段不存在而实际不生效。
还可以用如下查询检查发布中每张表的行过滤和列列表是否与预期一致:
SELECT pub.pubname,
n.nspname,
c.relname,
pg_get_expr(pr.prqual, pr.prrelid) AS row_filter,
(SELECT string_agg(a.attname, ', ' ORDER BY t.ord)
FROM unnest(pr.prattrs) WITH ORDINALITY AS t(attnum, ord)
JOIN pg_attribute a
ON a.attrelid = pr.prrelid
AND a.attnum = t.attnum) AS column_list
FROM pg_publication_rel pr
JOIN pg_publication pub ON pub.oid = pr.prpubid
JOIN pg_class c ON c.oid = pr.prrelid
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE pub.pubname = 'sales_pub';
为了防止过滤策略再次丢失,建议把发布定义纳入版本控制,或者定期导出pg_publication_rel的关键字段。可以写一个简单的脚本,每天执行上述查询并把结果保存到文件中。如果团队使用事件触发器,还可以在DROP PUBLICATION或ALTER PUBLICATION DROP TABLE时记录完整定义,为事后恢复提供依据。最后要注意,行过滤和列过滤是发布端的能力,恢复时务必在发布端操作,不要尝试在订阅端通过触发器或别的机制模拟过滤,那样会破坏逻辑复制的语义一致性。
PostgreSQL逻辑复制行过滤策略发布订阅修改时间:2026-09-22 22:12:36