发布(CREATE PUBLICATION)
发布(publication)是逻辑复制在发布端的声明式入口:它回答「哪些表、哪些列、哪些行、哪些类型的变更需要被发出去」。一个发布本质上是一组表的集合,外加一层可选的过滤逻辑(DML 类型过滤、行过滤、列清单)。CREATE PUBLICATION 这条命令本身不建立任何连接、不启动任何进程,也不做任何数据搬运——它只是在当前数据库的系统目录里登记一份「复制范围说明书」,真正消费这份说明书的是订阅端的 subscription。
可以把发布理解为逻辑复制的「生产侧配置」。它与前面学过的流复制形成鲜明对比:流复制没有「选择范围」这个概念,备库天然复制整个集簇的一切;而发布让复制粒度第一次细化到了表、列、行。本页系统讲解 CREATE PUBLICATION / ALTER PUBLICATION 的完整语法、三种发布范围、行过滤与列清单、分区表的特殊行为,以及 REPLICA IDENTITY 这条绕不开的前置约束。
发布定义在某一个数据库内(不是集簇级对象),这是初学者容易忽略的一点:如果你在 app 库创建了发布,订阅端连接的也必须是 app 库。另外,发布与 schema 是两个正交的概念——把表加入发布不影响任何正常的访问方式,同一张表也可以同时加入多个发布。
前置知识
建议先阅读:逻辑复制原理
基础概念
在动手写 SQL 之前,先把与发布相关的术语厘清:
| 概念 | 英文 / 记法 | 说明 |
|---|---|---|
| 发布 | publication | 定义在发布端数据库内的表集合 + 过滤规则,是复制的「范围声明」 |
| 发布端 | publisher | 定义了发布、产生逻辑变更并对外发送的实例 |
| 成员表 | published table | 被加入发布的普通表或分区表,只能持久化基表 |
| 发布操作 | publish 操作 | insert / update / delete / truncate 四种 DML 的组合开关 |
| 行过滤器 | row filter | 每张表一个 WHERE 表达式,表达式为假或 NULL 的行不发布(PG15+) |
| 列清单 | column list | 每张表一份显式列名清单,只发布清单中的列(PG15+) |
| 通过根发布 | publish_via_partition_root | 分区表变更以根表身份发布的开关(PG13+) |
| 复制标识 | replica identity | 决定 UPDATE / DELETE 时 WAL 里记录哪些旧值,是订阅端定位行的依据 |
三个贯穿全页的要点:
- 发布是「声明」而不是「动作」。创建发布不产生任何网络连接或进程,一个发布可以被零个或多个订阅消费。
- 发布只约束 DML。
publish参数、行过滤、列清单都只影响增量 DML 的发布行为,初始数据同步(table sync)不受它们约束(行过滤除外,PG15+ 订阅端初始拷贝会应用行过滤)。 - 发布上没有权限体系。官方文档明确:目前不存在针对 publication 的 GRANT 权限,任何能连上来的订阅都能读取任何发布。因此想用行过滤、列清单向特定订阅者「隐藏」数据是不可靠的,安全防护要放在发布端自己的访问控制上。
发布在整条复制链路中的位置
发布端(publisher) 订阅端(subscriber)
┌──────────────────────────────────────┐ ┌────────────────────────────┐
│ 业务写入 → WAL │ │ 目标表(手工建的 schema) │
│ │ │ 逻辑变更流 │ ▲ │
│ ▼ │ (walsender)│ │ apply worker │
│ 逻辑解码(pgoutput 输出插件) │ ─────────► │ │ │
│ │ │ │ subscription(订阅) │
│ publication:圈定表 + 裁剪变更 │ │ │ │
│ (publish / 行过滤 / 列清单) │ │ 复制原点 pg_<oid> │
└──────────────────────────────────────┘ └────────────────────────────┘发布在这一条链路里承担两个职责:圈定范围(告诉输出插件哪些表、哪些操作需要发送)与裁剪内容(publish、行过滤、列清单在发送前生效)。注意解码本身面向整个 WAL,过滤发生在输出插件层——理解这一点,就能明白为什么「改了发布、订阅端必须刷新」:发布变了,插件吐出来的内容就变了,但订阅端不刷新就感知不到新的成员表。
一个发布只属于一个数据库
CREATE PUBLICATION 在当前数据库中登记对象,发布名只需在当前数据库内唯一。发布端与订阅端通过 libpq 连接串(CONNECTION)指定数据库,两端数据库的名字可以不同,但订阅连接指向哪个库,就只能消费那个库里的发布。多库分发需要为每个库分别建发布与订阅。
同一张表可以加入多个发布
一张表可以同时是多个发布的成员,每个发布可以为它定义不同的 publish 组合、不同的行过滤、不同的列清单。这是「按订阅者定制数据视图」的基础手段:例如同一张 orders 表,向报表库发布全量列,向风控系统只发布部分列加行过滤。
语法与用法
CREATE PUBLICATION 的完整语法(PostgreSQL 18):
CREATE PUBLICATION name
[ FOR ALL TABLES
| FOR publication_object [, ... ] ]
[ WITH ( publication_parameter [= value] [, ... ] ) ]
-- publication_object 的两种形式:
TABLE [ ONLY ] table_name [ * ] [ ( column_name [, ... ] ) ] [ WHERE ( expression ) ] [, ...]
TABLES IN SCHEMA { schema_name | CURRENT_SCHEMA } [, ...]三个 WITH 参数总览:
| 参数 | 类型 | 默认值 | 说明 | 引入版本 |
|---|---|---|---|---|
publish | string | 'insert, update, delete, truncate' | 该发布对外发送哪些 DML 操作 | PG10 |
publish_generated_columns | enum | none | 是否发布存储生成列,取值 none / stored | PG18(语义调整后) |
publish_via_partition_root | boolean | false | 分区表变更是否以根表的标识与结构发布 | PG13 |
几个最小可用的例子:
-- 1. 发布两张表的全部变更
CREATE PUBLICATION pub_users FOR TABLE public.users, public.departments;
-- 2. 全库发布:当前库所有表(含未来新建的表),需要超级用户
CREATE PUBLICATION pub_all FOR ALL TABLES;
-- 3. schema 级发布:发布 production schema 下的所有表(含未来新建的表),PG15+
CREATE PUBLICATION pub_prod FOR TABLES IN SCHEMA production;
-- 4. 只发布 INSERT:常用于「只追加」型下游(审计、埋点)
CREATE PUBLICATION pub_insert_only FOR TABLE public.events
WITH (publish = 'insert');
-- 5. 行过滤:只发布激活状态的部门
CREATE PUBLICATION pub_active FOR TABLE public.departments WHERE (active IS TRUE);
-- 6. 列清单:只发布 users 表的 user_id 与 firstname 两列
CREATE PUBLICATION pub_cols FOR TABLE public.users (user_id, firstname);
-- 7. 混合:表 + schema 可以出现在同一条语句里
CREATE PUBLICATION pub_mix FOR TABLE public.users, TABLES IN SCHEMA production;如果不写 FOR 子句,发布初始为空发布(不含任何表),适合「先建发布、后用 ALTER PUBLICATION 逐步加表」的灰度流程:
CREATE PUBLICATION pub_orders;
ALTER PUBLICATION pub_orders ADD TABLE public.orders, public.order_items;创建成功后可以用 psql 的 \dRp+ 查看发布定义:
\dRp+ pub_users
Publication pub_users
Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root
-------+------------+---------+---------+---------+-----------+-------------------+----------
postgres | f | t | t | t | t | none | f
Tables:
"public.users"
"public.departments"详细说明
FOR TABLE / FOR ALL TABLES / FOR TABLES IN SCHEMA
三种发布范围各有明确的定位与代价:
| 范围 | 语法 | 包含未来新增表 | 权限要求 | 适用 |
|---|---|---|---|---|
| 显式表清单 | FOR TABLE t1, t2 | 否 | 当前数据库 CREATE + 表的 ownership | 生产首选,范围可控、可审计 |
| 全库 | FOR ALL TABLES | 是 | 超级用户 | 全库整体外发、迁移场景 |
| schema 清单 | FOR TABLES IN SCHEMA s1, s2(PG15+) | 是(该 schema 内) | 超级用户 | 按业务域(schema)整体分发 |
FOR TABLE 的细节最多:
- 表名前加
ONLY表示只发布该表本身;不加ONLY则连同它的继承子表一起发布,表名后跟*是显式声明「含后代」的写法。 - 这套
ONLY/*语义不适用于分区表:分区表的分区永远被视为「隐含成员」,不能也不需要显式加入发布。把分区表加入发布后,它的现有与未来所有分区都自动处于发布范围内,直接对分区执行的 DML 也会经由其祖先的发布发出。 WHERE (...)行过滤与(col1, col2)列清单都写在单张表后面(详见下文)。
FOR TABLES IN SCHEMA(PG15 新增)按 schema 圈定范围,支持 CURRENT_SCHEMA 关键字:
-- 发布 marketing 与 sales 两个 schema 下的所有表
CREATE PUBLICATION pub_sales FOR TABLES IN SCHEMA marketing, sales;
-- 发布当前搜索路径所指 schema 下的所有表
CREATE PUBLICATION pub_cur FOR TABLES IN SCHEMA CURRENT_SCHEMA;注意两条硬性约束:行过滤器 / 列清单与 FOR TABLES IN SCHEMA 不能作用于同一张表(发布里既有「带列清单的表」又有「schema 清单」时不支持);schema 级发布下的分区表,其分区无论是否属于该 schema 都被隐含纳入发布。
publish 参数:选择复制哪些 DML
publish 是一个逗号分隔的字符串,允许的操作是 insert、update、delete、truncate,默认 'insert, update, delete, truncate'(全发)。它与订阅端的关系是「发布端先过滤」:发布没开的操作类型,根本不会出现在发给订阅端的逻辑流里。
-- 只发布 update 与 delete(例如下游已有全量数据,只要增量修正)
ALTER PUBLICATION noinsert SET (publish = 'update, delete');
-- 只发布 insert 与 update,不发布 delete(下游做累积归档)
ALTER PUBLICATION pub_archive SET (publish = 'insert, update');几个容易被忽略的行为细节:
- 初始数据同步不看
publish。即使publish = 'insert',订阅端建订阅时的全量拷贝仍会把整表数据搬过去。想要「空下游 + 只增量」,需要先建空表结构再订阅,或订阅完成后手工清理。 TRUNCATE与行过滤、列清单互不相干:行过滤、列清单对TRUNCATE无效,TRUNCATE是否复制只由publish是否含truncate决定。INSERT ... ON CONFLICT与MERGE按最终效果发布:实际插入了就发INSERT,冲突转更新就发UPDATE,什么都没发生就不发布;MERGE对每行按其结果发布对应的INSERT/UPDATE/DELETE。COPY ... FROM的每一行都作为INSERT发布。
复制标识(REPLICA IDENTITY)
发布端要复制 UPDATE / DELETE,WAL 里必须记录「旧行的定位信息」,这份信息由每张表的 REPLICA IDENTITY 决定。官方规则可以浓缩为一句话:发布 UPDATE/DELETE 的表中,每张表都必须有可用的复制标识,否则这些操作在发布端会被直接拒绝。
| 取值 | WAL 记录的定位信息 | 说明 |
|---|---|---|
DEFAULT | 主键列 | 默认值;表有主键时无需任何额外操作 |
USING INDEX index_name | 指定唯一索引的列 | 索引必须是 B-tree、唯一、非部分、非表达式索引,且列为 NOT NULL |
FULL | 整行的所有列 | 无主键时的兜底方案,WAL 与网络开销显著增大 |
NOTHING | 无 | 该表的 UPDATE / DELETE 无法复制,在发布端报错 |
设置方式(详见 ALTER TABLE):
-- 有主键的表:默认即是 DEFAULT,无需操作
ALTER TABLE public.orders REPLICA IDENTITY DEFAULT;
-- 无主键但有唯一索引:用索引代替主键
ALTER TABLE public.orders REPLICA IDENTITY USING INDEX orders_uidx;
-- 无主键且无索引:退而求其次,整行作键
ALTER TABLE public.orders REPLICA IDENTITY FULL;
-- 明确声明只同步 INSERT
ALTER TABLE public.audit_log REPLICA IDENTITY NOTHING;三类表无法复制 UPDATE / DELETE
复制标识为 NOTHING、DEFAULT 但没有主键、或 USING INDEX 指向的索引已被删除的表,一旦出现在发布 UPDATE / DELETE 的发布里,对这些表执行 UPDATE / DELETE 会在发布端直接报错(而不是静默丢失)。INSERT 不受复制标识影响,永远可以发布。
两端配合还有一条对称性要求:发布端复制标识不是 FULL 时,订阅端表必须定义「相同或更少列」的复制标识。另外发布端用 REPLICA IDENTITY FULL 时,订阅端可以用索引加速行定位,候选索引要求是 B-tree 或 Hash、非部分索引、最左字段是普通列而非表达式;如果连这样的索引都没有,订阅端只能全表扫描定位行——所以官方定位 FULL 是「没有其他办法时的兜底」。
REPLICA IDENTITY FULL 与特殊数据类型
表中含有 point、box 这类没有默认 B-tree / Hash 操作符类的类型时,REPLICA IDENTITY FULL 的 UPDATE / DELETE 在订阅端无法匹配到行。官方给出的解决途径仍然是给表配置主键或可用的复制标识。
行过滤器(Row Filter,PG15+)
行过滤器写在单张表后面,语法是 WHERE (expression)(括号必须写)。表达式的取值为 false 或 NULL 的行不会发布:
-- 只发布未完成订单
CREATE PUBLICATION pub_open_orders
FOR TABLE public.orders WHERE (status <> 'done');
-- 一条语句里给不同表配不同过滤条件
CREATE PUBLICATION pub_region
FOR TABLE public.t2 WHERE (e = 99), public.t3 WHERE (g = 10);表达式限制(官方明确列出,写错了会在建发布时报错):
- 只允许简单表达式:不能包含用户自定义函数、用户自定义操作符、用户自定义类型、用户自定义 collation;
- 不能引用系统列(如
ctid、xmin); - 不能使用非 immutable 的内置函数(例如
now()); - 若发布包含
UPDATE/DELETE,WHERE里只能使用复制标识覆盖到的列;只发布INSERT时可用任意列。
行过滤的运行时语义:
| 场景 | 行为 |
|---|---|
TRUNCATE | 行过滤不生效 |
| 表达式执行身份 | 以订阅连接串中指定的角色执行(因此该角色需要能读到相关列) |
| 一个订阅叠加多个发布、同表多个过滤条件 | 各条件之间取 OR,满足任一条件即发布 |
其中一个发布对同表没有过滤(或用了 FOR ALL TABLES / 命中 schema) | 其他条件全部失效,整表发布 |
| 初始数据同步(订阅端 ≥ PG15) | 同样应用行过滤,只拷贝满足条件的行 |
| 初始数据同步(订阅端 < PG15) | 行过滤被忽略,整表拷贝 |
UPDATE 的行为最精妙:行过滤对新旧行各求值一次,并根据结果转换操作类型,以保证订阅端数据与过滤器定义一致:
| 旧行匹配 | 新行匹配 | 实际发布 |
|---|---|---|
| 否 | 否 | 不发布 |
| 否 | 是 | 转换为 INSERT |
| 是 | 否 | 转换为 DELETE |
| 是 | 是 | 正常 UPDATE |
列清单(Column List,PG15+)
列清单跟在表名后的小括号里,指定只发布哪些列:
CREATE PUBLICATION pub_users_min FOR TABLE public.users (user_id, firstname);关键规则:
- 清单里只能是简单列引用,且列的顺序不保留(订阅端按自己的列序接收);
- 订阅端表至少要包含所有被发布的列,但可以多出别的列;
- 发布含
UPDATE/DELETE时,清单必须包含复制标识列;只发布INSERT时可以省略; - 清单可以包含存储生成列(此时无论
publish_generated_columns是否开启都会发布该列); - 对
TRUNCATE无效; - 初始数据同步只拷贝清单内的列(订阅端 < PG15 时例外,会整表拷贝);
- 与
FOR TABLES IN SCHEMA互斥(同一张表不能既通过 schema 发布又带列清单)。
列清单不能当安全边界
官方文档特别警告:不要依赖列清单做数据安全。恶意的订阅者有可能读到未发布的列。真正的数据保护要在发布端做(视图、权限、独立实例等)。同理,发布对象上没有任何权限控制,同一数据库里的其他发布完全可能暴露相同数据。
一个容易踩的坑:「列清单列出全部列」与「不写列清单」并不等价。不写清单时,表后续新增的列会自动进入发布;写了清单则永远只有清单里的列。反过来,一个订阅若同时订阅多个发布,而同一张表在各发布中的列清单不一致,是不受支持的状态(建订阅时会拒绝,但事后修改发布侧列清单仍可能制造出这种局面,导致订阅端报错)。
分区表与 publish_via_partition_root
分区表的默认行为(publish_via_partition_root = false)是:变更以实际发生所在的叶子分区的标识与结构发布。这带来一个硬性要求——发布端的每个分区,在订阅端都必须存在对应的可用目标表(可以是叶子分区、也可以是进一步分区的表、甚至可以是独立普通表)。换句话说,默认模式下订阅端要「镜像」发布端的分区结构。
-- 发布端
CREATE TABLE public.measures (id int, val numeric, ts timestamptz)
PARTITION BY RANGE (ts);
CREATE TABLE public.measures_2026q1 PARTITION OF public.measures
FOR VALUES FROM ('2026-01-01') TO ('2026-04-01');
-- 默认发布:订阅端需要同样存在 measures_2026q1 这张(或对应结构的)表
CREATE PUBLICATION pub_measures FOR TABLE public.measures;publish_via_partition_root = true(PG13+)则改为以根分区表的标识与结构发布,订阅端只需有根表即可——它可以是一张普通的非分区表,也可以是分区结构完全不同的分区表。这是「把分区表收拢成单表下游」的标准做法。
开启后的副作用需要记牢:
- 行过滤与列清单改从根表读取(false 时从各分区自己读取);
- 直接对分区执行的
TRUNCATE不会被复制; - 把一张表
ATTACH进已发布的分区树时,该表已有内容不会被补发,需要自行初始化; - 一个订阅订阅了多个发布、其中任一发布对该分区表开了
publish_via_partition_root = true,就以根表身份发布; - 事后用
ALTER PUBLICATION ... SET修改这个参数是危险操作:它会改变发布身份,可能造成订阅端数据丢失或重复。官方建议:修改后、执行REFRESH PUBLICATION前不要动分区叶子表,且刷新时用copy_data = off。
用 ALTER PUBLICATION 变更发布
ALTER PUBLICATION 提供四类子动作:
-- 1. 增删成员(ADD/DROP),或整体替换成员清单(SET)
ALTER PUBLICATION pub_orders ADD TABLE public.refunds;
ALTER PUBLICATION pub_orders SET TABLE public.orders, public.order_items, public.refunds;
ALTER PUBLICATION pub_orders DROP TABLE public.refunds;
-- 2. schema 层面的增删改(PG15+)
ALTER PUBLICATION pub_sales ADD TABLES IN SCHEMA marketing, sales;
ALTER PUBLICATION pub_sales SET TABLES IN SCHEMA sales;
ALTER PUBLICATION pub_sales DROP TABLES IN SCHEMA marketing;
-- 3. 修改 WITH 参数
ALTER PUBLICATION pub_orders SET (publish = 'insert, update');
ALTER PUBLICATION pub_measures SET (publish_via_partition_root = true);
-- 4. 改名与转移所有权
ALTER PUBLICATION pub_orders RENAME TO pub_orders_v2;
ALTER PUBLICATION pub_orders OWNER TO repl_admin;需要注意:
- 对已被订阅的发布增删表后,订阅端必须执行
ALTER SUBSCRIPTION ... REFRESH PUBLICATION才会生效——发布端的变更不会自动推送。 ADD TABLE/DROP TABLE是事务性的:事务提交时,表会在正确的快照点开始或停止复制。DROP TABLES IN SCHEMA只移除通过 schema 清单纳入的表,不会移除用FOR TABLE/ADD TABLE单独加进来的表。SET修改参数时,未提及的参数保持原值。- 权限:需要是发布的 owner;加表需要表 ownership;
ADD/SET TABLES IN SCHEMA与修改FOR ALL TABLES/FOR TABLES IN SCHEMA发布的 owner 需要超级用户。
不能发布的对象与序列
官方对「什么能进发布」的边界非常明确:
- 可以发布:持久化的普通基表、分区表(含其所有分区)。
- 不能发布:临时表、unlogged 表、外部表(foreign table)、物化视图、普通视图——试图加入会直接报错。
- 序列(sequence)不是可发布对象,且逻辑复制不复制序列状态。
serial/identity列里的「值」作为普通行数据会被复制过去,但订阅端的序列对象本身停在初始位置,不会随发布端的nextval()推进。如果订阅端要承担写角色(例如切换、故障接管),必须手工把序列推到正确位置(详见逻辑复制限制)。 - DDL 不发布:
CREATE TABLE、ALTER TABLE等结构变更不进入逻辑复制流,两端结构必须自行保持同步。 - 生成列:默认不发布;PG18 起可通过
publish_generated_columns = 'stored'发布存储生成列(订阅端低于 18 时初始同步不会拷贝生成列)。也可以把它们显式写进列清单来发布。
权限要求
发布端相关权限汇总(均来自官方文档):
| 动作 | 所需权限 |
|---|---|
CREATE PUBLICATION | 当前数据库的 CREATE 权限(超级用户无需) |
| 把表加入发布 | 表的 ownership |
FOR ALL TABLES / FOR TABLES IN SCHEMA / ADD/SET TABLES IN SCHEMA | 超级用户 |
ALTER PUBLICATION | 发布的 ownership |
| 更改发布 owner | 能 SET ROLE 到新 owner,且新 owner 有数据库 CREATE 权限;全库/schema 发布的新 owner 必须是超级用户 |
除了这些目录操作权限,真正「跑起来」还依赖连接层面的准备:复制连接角色需要 REPLICATION 属性(或超级用户)与 LOGIN,pg_hba.conf 中要有对应的普通连接条目;做初始数据拷贝时该角色还需要对发布表有 SELECT 权限。PG18 起发布端还会校验输出插件是否在 output_plugin_libraries 受信列表内(内置逻辑复制使用的 pgoutput 默认可用)。
系统视图与 psql 工具
发布相关的系统目录与视图:
| 对象 | 作用 |
|---|---|
pg_publication | 所有发布的目录:pubname、pubowner、puballtables、pubinsert / pubupdate / pubdelete / pubtruncate、pubviaroot、pubgencols 等 |
pg_publication_rel | 发布与表的多对多映射,含行过滤表达式(prqual)与列清单(prattrs) |
pg_publication_namespace | 发布与 schema 的映射(schema 级发布) |
pg_publication_tables | 最常用的展开视图:每个「发布 × 表」一行,含 pubname、schemaname、tablename、attnames、rowfilter |
常用检查 SQL:
-- 发布清单与开关
SELECT pubname, puballtables, pubinsert, pubupdate, pubdelete, pubtruncate, pubviaroot
FROM pg_publication;
-- 某个发布实际覆盖了哪些表、什么行过滤
SELECT schemaname, tablename, attnames, rowfilter
FROM pg_publication_tables
WHERE pubname = 'pub_orders';
-- 发布端:观察订阅连接(walsender)
SELECT application_name, state, sent_lsn, write_lsn, flush_lsn
FROM pg_stat_replication
WHERE application_name LIKE 'sub%';psql 侧:\dRp 列出发布,\dRp+ 显示成员表、行过滤与列清单;\d 表名 时如果表在发布里,也会显示 Publications: 一节。
版本演进速查
发布相关能力是逐个大版本补齐的,写迁移脚本与做兼容性判断时按版本对号入座:
| 版本 | 新增能力 |
|---|---|
| PG10 | 逻辑复制基础:CREATE PUBLICATION、publish 参数、FOR ALL TABLES / FOR TABLE |
| PG13 | publish_via_partition_root:分区表以根表身份发布 |
| PG15 | FOR TABLES IN SCHEMA(schema 级发布)、行过滤器(WHERE)、列清单 |
| PG16 | 订阅端并行流式应用(streaming = parallel)、run_as_owner |
| PG17 | 订阅 failover 选项(逻辑槽随物理复制同步到备库)、pg_stat_subscription_stats 增加各 confl_* 冲突计数 |
| PG18 | publish_generated_columns(取值 none / stored)、output_plugin_libraries 受信输出插件列表 |
写 SQL 前先核对订阅端版本:PG15 之前的订阅端在初始同步中会忽略行过滤与列清单(整表拷贝);PG18 之前的订阅端不会在初始同步中拷贝生成列。
实战示例
示例一:为订单库发布多张表(含 DML 过滤)
场景:把订单主表与明细表完整同步给数仓;把审计事件表只同步 INSERT 给归档系统。
-- 发布端:建表与数据
CREATE TABLE public.orders (
order_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id bigint NOT NULL,
amount numeric(12,2) NOT NULL,
status text NOT NULL DEFAULT 'created',
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE public.order_items (
item_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
order_id bigint NOT NULL REFERENCES public.orders(order_id),
sku text NOT NULL,
qty int NOT NULL
);
CREATE TABLE public.audit_events (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
payload jsonb NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
-- 完整发布:全量 DML
CREATE PUBLICATION pub_orders_dw
FOR TABLE public.orders, public.order_items;
-- 归档发布:只要 INSERT
CREATE PUBLICATION pub_audit_archive
FOR TABLE public.audit_events
WITH (publish = 'insert');-- 发布端验证:两个发布各覆盖哪些表
SELECT pubname, schemaname, tablename, rowfilter
FROM pg_publication_tables
ORDER BY pubname, tablename; pubname | schemaname | tablename | rowfilter
------------------+------------+--------------+-----------
pub_audit_archive| public | audit_events |
pub_orders_dw | public | orders |
pub_orders_dw | public | order_items |
(2 rows)注意:虽然 pub_audit_archive 只发布 insert,但订阅它的下游在初始同步时依然会拿到整张 audit_events 的历史数据(初始同步不看 publish)。如果归档系统只想要增量,应先建空表结构、订阅完成后清理,或直接用行过滤约束初始拷贝范围。
示例二:分区表的两种发布方式
-- 发布端:按月分区的度量表
CREATE TABLE public.metrics (
id bigint GENERATED ALWAYS AS IDENTITY,
val numeric NOT NULL,
ts timestamptz NOT NULL
) PARTITION BY RANGE (ts);
CREATE TABLE public.metrics_2026_09 PARTITION OF public.metrics
FOR VALUES FROM ('2026-09-01') TO ('2026-10-01');
CREATE TABLE public.metrics_2026_10 PARTITION OF public.metrics
FOR VALUES FROM ('2026-10-01') TO ('2026-11-01');方式 A(默认):按分区发布,订阅端镜像同样的分区结构:
CREATE PUBLICATION pub_metrics_leaves FOR TABLE public.metrics;
-- 订阅端必须存在 metrics_2026_09、metrics_2026_10(或结构兼容的目标表)方式 B:以根表发布(PG13+),订阅端只需一张普通表:
CREATE PUBLICATION pub_metrics_root
FOR TABLE public.metrics
WITH (publish_via_partition_root = true);
-- 订阅端只需要:
-- CREATE TABLE public.metrics (id bigint, val numeric, ts timestamptz);方式 B 下,直接对分区执行 TRUNCATE 不会复制;给分区表加新分区不需要动发布,新分区的数据会自动以根表身份发布出去。
示例三:行过滤 + 列清单的精细分发
场景:向合作方系统只开放「已支付订单」的订单号、金额与支付时间。
-- 发布端
CREATE PUBLICATION pub_partner
FOR TABLE public.orders (order_id, user_id, amount, created_at)
WHERE (status = 'paid');-- 订阅端:表只需要包含被发布的列,可以另有本地列
CREATE TABLE public.orders (
order_id bigint PRIMARY KEY,
user_id bigint NOT NULL,
amount numeric(12,2) NOT NULL,
created_at timestamptz NOT NULL,
received_at timestamptz DEFAULT now() -- 订阅端本地列,发布端没有
);
CREATE SUBSCRIPTION sub_partner
CONNECTION 'host=publisher.example.com port=5432 dbname=app user=repl password=***'
PUBLICATION pub_partner
WITH (copy_data = true);行为要点:
- 初始同步只会拷贝
status = 'paid'且只含清单四列的数据; - 之后订单从
created改为paid时,旧行不匹配、新行匹配,这次UPDATE会自动转换为INSERT出现在订阅端; - 订单又改回
created时,新发布的则是一条DELETE; - 列清单含
order_id(主键),满足「发布 UPDATE/DELETE 时清单必须含复制标识列」的要求; - 若下游是合作方,务必记得:列清单与行过滤不是安全边界,敏感数据不要放进同一个物理库再靠发布过滤。
示例四:从空发布到成熟发布的生命周期演练
演示「先建发布、灰度加表、最后收口」的完整节奏,这是在线系统上变更发布最稳妥的顺序:
-- 阶段 1:空发布(不改变任何在线行为)
CREATE PUBLICATION pub_billing;
-- 阶段 2:加入第一批表,并先收窄操作类型(下游已有存量,只要增量)
ALTER PUBLICATION pub_billing ADD TABLE public.invoices;
ALTER PUBLICATION pub_billing SET (publish = 'insert');
-- 阶段 3:下游就绪后逐步放开操作类型
ALTER PUBLICATION pub_billing SET (publish = 'insert, update');
-- 阶段 4:加入新表并附带行过滤
ALTER PUBLICATION pub_billing ADD TABLE public.payments WHERE (amount > 0);-- 订阅端:每个阶段之后都要刷新,阶段 4 会触发 payments 的初始同步
ALTER SUBSCRIPTION sub_billing REFRESH PUBLICATION;-- 收尾核查:发布定义与订阅端表清单对齐
SELECT pubname, schemaname, tablename, rowfilter
FROM pg_publication_tables
WHERE pubname = 'pub_billing';
SELECT srrelid::regclass AS "table", srsubstate FROM pg_subscription_rel;顺序设计的门道在阶段 2:先收窄 publish、再加表。如果反过来先加表再改参数,两条语句之间有一个「新表按全量操作发布」的窗口,下游可能在不知情时收到 UPDATE / DELETE。把「建发布 → 收窄 → 加表 → 放开 → 刷新订阅 → 核查」固化成 checklist,发布变更就不再是高危操作。
注意事项
创建发布 ≠ 开始复制。 发布只是声明,没有任何订阅消费它时,它不产生任何流量。排障时先确认「发布存在」再确认「订阅已刷新到最新的发布定义」。
初始数据同步会无视 publish。 想要「不带历史数据的纯增量订阅」,不能只靠 publish = 'insert',要用 copy_data = false 配合手工初始化,或接受全量拷贝。
改了发布,订阅端不会自动知道。 ADD/DROP/SET TABLE 之后,已存在的订阅必须执行 ALTER SUBSCRIPTION ... REFRESH PUBLICATION。把「改发布」和「刷新订阅」写进同一个变更流程,否则很容易出现「发布端以为在复制、订阅端从来没收到」的静默断层。
REPLICA IDENTITY 必须在建发布之前想清楚。 无主键表混进发布 UPDATE/DELETE 的发布里,业务高峰期一条 UPDATE 就会在发布端报错,影响的可是在线写入。无主键表要么补主键、要么 USING INDEX、要么明确 REPLICA IDENTITY FULL 并评估开销。
行过滤表达式的限制比想象中严格。 不能用自定义函数、不能用 now() 等非 immutable 内置函数、发布 UPDATE/DELETE 时只能用复制标识覆盖的列。设计过滤条件时先用 CREATE PUBLICATION 验证一遍再写进迁移脚本。
publish_via_partition_root 改动是高危操作。 它决定变更以「分区」还是「根表」的身份出现,中途切换可能造成订阅端丢数据或重复数据。确需切换时按官方建议:刷新前不动叶子表,REFRESH PUBLICATION 时带 copy_data = off。
FOR ALL TABLES 会吞掉未来的每一张表。 之后新建的任何表(包括临时调试用的表)都会自动进入发布并开始产生流量。生产环境优先用显式清单或 schema 清单。
发布是数据库级对象,备份与迁移要按库粒度对待。 pg_dump 会按库导出发布定义,pg_dumpall --globals-only 里没有它;做实例级迁移时,别指望「导了全局对象就带走了发布」。
最佳实践
- 用显式表清单代替
FOR ALL TABLES,让复制范围可审计、可版本化;范围确实很大时用FOR TABLES IN SCHEMA(PG15+),按业务域划边界。 - 所有参与复制的表都定义主键;确实没有主键的表用
USING INDEX指向唯一索引,REPLICA IDENTITY FULL只作最后兜底,并确认表中没有point/box等无法比较的类型。 - 把发布定义纳入迁移脚本管理:发布的创建、成员变更、参数调整都走代码评审,避免「线上发布与仓库定义漂移」。
- 发布变更与
REFRESH PUBLICATION绑定成同一个变更单,并在变更后核对pg_publication_tables与订阅端pg_subscription_rel的表清单是否一致。 - 按下游需求裁剪
publish:只读归档下游给publish = 'insert';不允许下游删数据的场景去掉delete。少发一类操作就少一类冲突。 - 用行过滤做业务子集分发,用列清单做瘦身分发,但要理解 OR 组合规则与 UPDATE 转换语义;涉及安全的数据隔离不要依赖这两者。
- 分区表优先评估
publish_via_partition_root = true,除非订阅端确实需要镜像分区结构;这样订阅端结构与运维都简单得多。 - 给复用场景的发布起规范名字,例如
pub_<库>_<用途>_<下游>,并保持「一个发布对应一类下游」的粒度,避免多个下游共享发布后无法独立调整过滤条件。 - 上线前用
\dRp+与pg_publication_tables做一次发布体检:确认成员表、行过滤、列清单、publish 组合与设计一致,特别检查无主键表。 - 监控发布端 walsender 与槽的滞留:
pg_stat_replication观察订阅连接,pg_replication_slots监控confirmed_flush_lsn与restart_lsn的差距(详见复制槽),防止订阅离线把pg_wal撑爆。
下一节
参考链接
- PostgreSQL 官方文档:CREATE PUBLICATION
- PostgreSQL 官方文档:ALTER PUBLICATION
- PostgreSQL 官方文档:DROP PUBLICATION
- PostgreSQL 官方文档:Logical Replication Publication
- PostgreSQL 官方文档:Row Filters
- PostgreSQL 官方文档:Column Lists
- PostgreSQL 官方文档:Generated Column Replication
- PostgreSQL 官方文档:pg_publication
- PostgreSQL 官方文档:ALTER TABLE(REPLICA IDENTITY)
- PostgreSQL 官方文档:Logical Replication