PostgreSQL原生逻辑复制从10版本开始引入,发布端通过CREATE PUBLICATION声明要复制的表以及DML操作类型,订阅端通过CREATE SUBSCRIPTION连接发布并持续应用变更。早期版本只能做到整表复制,要么全量同步,要么干脆不复制某张表。从PostgreSQL 15开始,发布支持行过滤器和列列表,这才让逻辑复制有了精细控制的基础。但在多租户、多环境或多区域分发场景中,把过滤条件直接写在发布定义里,会带来配置散落、难以审计、切换成本高等问题。

所谓过滤策略适配器,并不是PostgreSQL内置的功能名称,而是一层抽象封装。它把哪些表需要复制、只复制哪些列、满足什么条件的行才复制这些规则统一管理起来,再转换成发布端能够识别的SQL定义。这样可以避免手工维护几十个发布对象,也便于在租户迁移、数据脱敏、跨区域合规等场景中快速调整复制范围。
一、发布端过滤的三种粒度
在讨论适配器之前,先要弄清楚PostgreSQL逻辑复制原生支持哪些过滤能力。发布对象由CREATE PUBLICATION定义,通过FOR TABLE指定要复制的表。如果忽略列列表和行过滤器,发布会把该表所有列以及所有满足复制标识的行变更发送给订阅端。
第一种粒度是操作过滤。创建发布时可以添加WITH (publish = 'insert,update'),只发布INSERT和UPDATE,不发布DELETE。这种过滤适合审计日志只增不改的场景。第二种粒度是列过滤,在表名后用括号列出列名,例如FOR TABLE public.orders (order_id, tenant_id, amount)。列过滤在发布端生效,不会把未列出的列放入复制流,但要求列清单必须包含复制标识列,否则UPDATE和DELETE无法定位订阅端行。第三种粒度是行过滤,在表定义后追加WHERE (...)条件。例如只复制tenant_id = 't_001'的订单。行过滤对初始数据同步和增量变更都生效,如果一行从不符合条件变为符合,会作为INSERT同步;如果UPDATE后不再符合条件,订阅端会收到DELETE。
这里有两个容易踩的坑。行过滤器只能使用稳定表达式,不能包含子查询、序列或用户自定义易失函数,否则发布创建或刷新会报错。另一个坑是,修改发布过滤条件后,已经复制到订阅端但不再满足新条件的数据不会被自动删除,逻辑复制只负责后续增量,历史数据清理需要单独处理。理解了这些限制,就能明白适配层需要承担哪些校验和补偿工作。
二、为什么需要策略适配层
直接维护发布对象在小规模环境里并不复杂,但一旦表数量上升到几十张,过滤条件又和租户、区域、数据类型绑定,散落在多个发布脚本中的WHERE条件就会失控。比如A环境的发布只复制华东地区订单,B环境要增加一个脱敏字段排除规则,C环境需要临时放开某租户,手工修改ALTER PUBLICATION既容易遗漏,也缺乏审计记录。
策略适配层的核心思想是把复制过滤规则从发布DDL中剥离出来,放到结构化的配置表里。每种过滤条件都抽象成一个策略记录,包含目标表、过滤类型、表达式、列清单、启用状态和更新时间。发布端仍然使用原生语法,但生成过程由适配器统一完成。这样无论是开发环境初始化、生产环境变更,还是审计追踪,都只操作同一张策略表,再由工具或函数渲染成合法的发布定义。
适配器还可以承担一些发布原生不提供的校验,例如检查列清单是否包含主键列、过滤表达式是否引用了不存在的列、同一个表是否被多个发布重复订阅等。这些检查如果不做,等到订阅端apply时才会暴露问题,排查成本会高很多。
三、策略配置表与生成函数的实现
下面给出一个最小可用的适配层实现。首先创建策略配置表,分别存储行过滤和列过滤规则。表名、列名使用text类型,表达式以文本形式保存,启用状态用布尔值控制。
CREATE TABLE replication_filter_policy (
policy_id serial PRIMARY KEY,
table_schema text NOT NULL,
table_name text NOT NULL,
filter_type text NOT NULL CHECK (filter_type IN ('row','column')),
filter_expression text,
column_list text[],
enabled boolean DEFAULT true,
updated_at timestamptz DEFAULT now()
);
INSERT INTO replication_filter_policy
(table_schema, table_name, filter_type, filter_expression)
VALUES
('public', 'orders', 'row', 'tenant_id = ''t_001'' AND region = ''CN'''),
('public', 'order_items', 'row', 'created_at > now() - interval ''90 days''');
上面的filter_expression保存的是原生SQL布尔表达式,写入时用两个单引号转义字符串字面量。生成发布定义时,适配器读取所有enabled = true的记录,逐个拼接FOR TABLE片段。表名和列名必须通过format的%I标识符转义,过滤表达式只能做白名单校验后拼接,因为它不是参数化DDL能够处理的部分。
接下来的函数会生成一个完整的CREATE PUBLICATION语句。实际执行前建议先打印确认,再交给数据库执行。
CREATE OR REPLACE FUNCTION generate_publication_sql(p_pub_name text)
RETURNS text LANGUAGE plpgsql AS $$
DECLARE
v_sql text := 'CREATE PUBLICATION ' || p_pub_name || ' FOR TABLE ';
r record;
BEGIN
FOR r IN
SELECT table_schema, table_name, filter_expression, column_list
FROM replication_filter_policy
WHERE enabled
ORDER BY table_schema, table_name
LOOP
v_sql := v_sql || format('%I.%I', r.table_schema, r.table_name);
IF r.column_list IS NOT NULL AND array_length(r.column_list, 1) > 0 THEN
v_sql := v_sql || ' (' || array_to_string(r.column_list, ', ') || ')';
END IF;
IF r.filter_expression IS NOT NULL AND btrim(r.filter_expression) <> '' THEN
v_sql := v_sql || ' WHERE (' || r.filter_expression || ')';
END IF;
v_sql := v_sql || ', ';
END LOOP;
v_sql := rtrim(v_sql, ', ');
v_sql := v_sql || ';';
RETURN v_sql;
END;
$$;
函数中使用了大于号和不等号操作符,代码示例里已经做了HTML转义处理,数据库实际执行时仍会按普通SQL比较操作处理。函数里的array_to_string负责把列名数组拼成括号内的列表,rtrim用来去掉最后一个表片段后面多余的逗号。
如果策略表里既有行过滤也有列过滤,生成语句会优先输出列列表,再输出WHERE条件,这和原生发布语法一致。实际使用时可以配合DROP PUBLICATION IF EXISTS后重建,或者用ALTER PUBLICATION ... SET TABLE进行增量调整。重建发布会导致订阅端重新做初始同步,生产环境更推荐增量修改。
四、动态修改发布过滤与订阅端残留处理
当某条策略发生变化时,适配器需要把新的过滤条件应用到已有发布上。PostgreSQL支持ALTER PUBLICATION的SET TABLE子句来覆盖某个表的列过滤和行过滤。例如要把订单表的过滤条件从t_001切换成t_002,可以执行:
ALTER PUBLICATION pub_orders
SET TABLE public.orders (order_id, tenant_id, amount)
WHERE (tenant_id = 't_002' AND status <> 'archived');
这个语句执行后,新过滤条件只对后续变更生效。对于已经复制到订阅端、但不再符合tenant_id = 't_002'的旧租户数据,PostgreSQL不会主动删除。订阅端那张表可能残留t_001历史行,造成数据范围看起来不一致。处理方式一般有三种:一是确认业务允许后手动在订阅端执行删除;二是暂停订阅并使用REFRESH PUBLICATION配合复制标识重新同步整张表;三是接受残留,但通过视图或下游应用逻辑过滤掉。
另外,如果行过滤条件发生了变化,但表没有复制标识,UPDATE和DELETE无法安全应用。PostgreSQL要求被发布的表必须有主键或唯一索引作为复制标识,否则只支持INSERT复制。适配器在生成发布前应检查pg_class.relreplident,当列为d(default)但实际没有主键时给出告警。特别是在列过滤场景中,列清单里漏掉复制标识列是最常见的创建失败原因。
订阅端还可能出现apply冲突,例如发布端更新的行在订阅端被删除,或者主键冲突。逻辑复制默认会不断重试失败事务,错误信息记录在pg_stat_subscription的last_error字段中。适配器可以做一层轻量监控,定期检查该视图,发现错误后结合策略配置判断是过滤条件变更引起的数据错位,还是业务侧并发操作导致。
五、性能影响与监控指标
行过滤发生在逻辑解码阶段,发布端仍需读取完整的WAL并解析每行变更,再评估WHERE条件。因此它不会减少WAL产生量,但能显著降低网络传输和订阅端写入压力。对于宽表或大字段多的表,列过滤的收益更明显,因为未列出的列不会进入复制流。
过滤表达式越复杂,发布端逻辑解码的CPU消耗越高。尽量避免在行过滤中使用类型转换、正则匹配或大范围函数计算。条件中如果涉及不能下推到解码器优化的表达式,每一行都会被单独求值。可以通过pg_stat_replication观察发送延迟,用pg_replication_slots的spill_txns和spill_bytes判断是否有解码溢出到磁盘。订阅端的pg_stat_subscription则能看到每秒接收和应用的变更量,如果apply速率长期跟不上接收速率,说明订阅端写入成为瓶颈,需要优化索引或调整批处理参数。
最后要强调,过滤策略适配器不是万能的,它解决的是配置集中与动态变更问题,并不能突破逻辑复制本身对DDL不复制、序列不同步、大事务性能差的限制。把这些边界提前纳入设计,适配层才能成为可维护的数据分发底座,而不是引入新故障点的复杂封装。
PostgreSQL逻辑复制行过滤策略适配器修改时间:2026-09-20 05:41:23