PostgreSQL 的逻辑复制与物理复制有一个关键区别:物理复制必须复制整个实例的所有数据变更,而逻辑复制基于发布与订阅模型,可以在发布端挑选表、行甚至列。这个能力对读写分离非常实用,因为它允许一台主库同时向多个只读节点按业务维度分发不同数据集,而不是让每个只读库都背着完全相同的数据。

一、逻辑复制的过滤能力及语法基础
在 PostgreSQL 15 之前,逻辑复制只能做到表级过滤,也就是通过 CREATE PUBLICATION ... FOR TABLE 指定哪些表参与复制。从 PostgreSQL 15 开始,发布端增加了行过滤和列过滤能力。行过滤通过 WHERE 子句实现,列过滤通过表后的列清单实现。例如发布 orders 表时,只复制 region 等于 north 的行,并且只复制 id、region、amount、created_at 四列,可以这样创建发布:
-- 主库发布端:同时使用行过滤与列过滤 CREATE PUBLICATION pub_biz FOR TABLE orders (id, region, amount, created_at) WHERE (region = 'north' AND created_at >= '2025-01-01');
这里 WHERE 中的过滤条件只作用于该发布表,其他未发布的表不受影响。过滤表达式有比较严格的限制,它不能包含子查询、聚合函数、窗口函数,也不能引用系统列或者其他表,只能基于当前表的列进行判断。表达式的稳定性要求接近 CHECK 约束,否则发布端在解析 WAL 时无法可靠地判断一行是否应该被发送。
列过滤则适合两种场景:一是为了避免把敏感字段复制到只读节点,例如只发布订单号、区域和金额,不发布用户手机号;二是为了减少只读库上的宽表维护成本,订阅端表可以只包含业务需要的列。需要注意的是,订阅端表结构必须包含发布端列出的列,主键或复制标识也必须对应。如果订阅端表还包含发布端没有发布的列,这些列必须允许为空,或者提供默认值,否则逻辑复制的应用进程写入时会报错。
行过滤和列过滤可以组合使用,也可以在已有发布上通过 ALTER PUBLICATION ... SET TABLE 进行调整。但是要特别注意,如果修改过滤条件后,原有订阅不会自动删除已经复制的、现在不再满足条件的行,需要通过业务方式或重新初始化订阅来同步数据。
二、用于读写分离的过滤策略设计
读写分离通常不是简单地把读流量指向一个只读副本,而是希望不同业务、不同团队或不同地区的数据可以落在不同的只读节点上。逻辑复制过滤策略让这种精细化分发变成了可能。以订单系统为例,主库保存全国订单,但华东只读节点只需要华东订单,分析节点只需要已完成订单,审计节点可能需要全部订单但不需要用户敏感字段。此时可以在主库上创建多个发布:
-- 主库为不同只读节点创建不同发布 CREATE PUBLICATION pub_east FOR TABLE orders (id, region, amount, status, created_at) WHERE (region = 'east'); CREATE PUBLICATION pub_done FOR TABLE orders (id, region, amount, created_at) WHERE (status = 'done'); CREATE PUBLICATION pub_audit FOR TABLE orders (id, region, amount, created_at) WHERE (created_at >= '2025-01-01');
三个发布互不影响,各自占用一个复制槽。订阅端根据需要创建对应的 CREATE SUBSCRIPTION,连接串指向主库并指定发布名即可。比如华东只读节点只订阅 pub_east,分析节点只订阅 pub_done。这样每个只读节点的数据量、索引大小和回放开销都会被控制在所需范围内。
从架构层面看,这种方案保留了主库的唯一写入入口,只读节点可以单独开放查询服务。逻辑复制相比物理复制还有一个优势:订阅端在数据库层面不是只读模式,应用复制进程需要写权限,但业务账号可以只授予查询权限。通过角色权限与连接池配置,就能把只读节点真正限制为只读,避免误写造成数据冲突。
不过,行过滤表达式如果依赖经常变化的列,例如订单状态频繁从 pending 更新为 done,可能导致行在发布集之间迁移。如果旧行不再满足过滤条件,UPDATE 或 DELETE 可能无法被正确复制。因此读写分离中的过滤键最好选择稳定不变或极少变化的字段,比如 region、tenant_id、created_at 这类不会被业务随意修改的列。
三、复制标识与旧行匹配机制
行过滤发布面临一个容易被忽略的问题:当 UPDATE 或 DELETE 发生时,发布端需要先找到旧行,判断它是否满足过滤条件,再决定是否向订阅端发送变更。PostgreSQL 查找旧行依赖表的复制标识。如果表有主键,默认复制标识就是主键;如果表没有主键,就需要设置 REPLICA IDENTITY FULL,否则逻辑复制无法处理 UPDATE 和 DELETE。
在有行过滤的情况下,即使表有主键,如果更新操作修改了过滤列,也可能出现新旧行不再同属一个发布集的情况。例如订单从 region = 'east' 更新为 region = 'west',旧行属于 pub_east,新行属于 pub_west。发送给 pub_east 订阅端的可能是一个删除操作,发送给 pub_west 订阅端的是插入操作。这要求发布端能够重建旧行的完整内容,而默认主键复制标识只保留主键列,不足以判断旧行原来是否满足过滤条件。为了这类复杂场景,必须将表设置为 REPLICA IDENTITY FULL:
-- 针对频繁修改过滤列或没有主键的表 ALTER TABLE orders REPLICA IDENTITY FULL;
REPLICA IDENTITY FULL 会把整行的旧值写入 WAL,这会增加主库的 WAL 量和逻辑解码开销,所以在大多数场景下,仍应优先使用主键复制标识,并保持过滤键稳定。如果确实需要修改过滤列,应在测试环境评估 WAL 增长速度与复制延迟。
对于只包含列过滤、没有行过滤的发布,旧行匹配问题与普通逻辑复制一致,只需要主键或唯一索引即可。这再次说明,行过滤虽然灵活,但会带来额外的复制标识要求,架构设计时要提前规划。
四、部署要点、监控与调优
要让逻辑复制过滤策略稳定运行,主库和订阅端的参数需要提前调整。主库必须设置 wal_level = logical,并预留足够的复制槽和 WAL 发送进程。因为每个订阅对应一个复制槽,多订阅架构下 max_replication_slots 和 max_wal_senders 要适当增大。订阅端要关注 max_logical_replication_workers,它决定同时运行多少个逻辑复制应用工作进程。初始同步时还可以通过 max_sync_workers_per_subscription 控制表同步并发度。
监控逻辑复制状态主要依赖三张视图:pg_replication_slots 查看复制槽是否活跃、pg_stat_subscription 查看订阅端接收和应用进度、pg_stat_replication 查看主库上 WAL 发送状态。一个常见问题是复制槽长时间不活跃,导致主库 WAL 无法清理。可以执行下面的 SQL 快速检查:
SELECT slot_name, active, restart_lsn, confirmed_flush_lsn
FROM pg_replication_slots
WHERE database = 'main';
SELECT subname, received_lsn, latest_end_lsn,
last_msg_send_time, last_msg_receipt_time
FROM pg_stat_subscription;
如果发现某个订阅的 received_lsn 长时间不推进,需要检查订阅端 worker 是否报错,或者发布端是否因为过滤条件计算复杂而积压。一般来说,过滤表达式应尽量使用简单比较,避免函数运算;无论是行过滤还是复制标识 FULL,都会增加发布端解码成本,应通过压测确认主库 CPU 和 WAL 写入承受能力。
另外,逻辑复制不会同步 DDL,也不会同步序列的当前值。订阅端需要提前手动创建好表结构,业务上如果使用自增主键,还要结合 UUID 或多个只读库错开序列范围等方式避免冲突。初始化订阅时 copy_data = true 只会复制满足过滤条件的行,订阅完成后修改过滤条件需要额外处理存量数据,这些都属于运维中必须提前约定的细节。
综合来看,PostgreSQL 逻辑复制过滤策略把读写分离从实例级下沉到行级和列级,为多租户、多区域和多用途只读库提供了精细化分发能力。只要规划好复制标识、过滤键和监控方案,就可以在保证主库写入性能的同时,让不同只读节点各取所需。

