PostgreSQL 15: 为逻辑复制添加行筛选功能

John Doe 七月 31, 2025

你需要在使用逻辑复制是过滤某些特定的表行吗?现在,PostgreSQL 可以做到这一点了。

非洲大草原上的一头大象

特性提交日志

允许为表的逻辑复制指定行筛选器。

此功能为发布表添加了行筛选功能。定义或修改发布时,可以指定一个可选的 WHERE 子句。不满足该 WHERE 子句的行将被过滤掉。这使得一组表可以被部分复制。行筛选器是按表设置的。只需在表名后指定一个 WHERE 子句,即可添加新的行筛选器。WHERE 子句必须用括号括起来。

对于发布 UPDATE/DELETE 操作的发布,添加到发布中的表,其行筛选器 WHERE 子句只能包含 REPLICA IDENTITY 所涵盖的列。对于发布 INSERT 操作的发布,添加到发布中的表,其行筛选器 WHERE 子句可以使用任何列。如果行筛选器的计算结果为 NULL,则将其视为 “false”。WHERE 子句仅允许使用简单表达式,不允许包含用户定义函数、用户定义运算符、用户定义类型、用户定义的排序规则、非不可变的内置函数或对系统列的引用。这些限制可能会在未来得到解决。

如果选择执行初始表同步,则只有满足行筛选器的数据会被复制到订阅端。如果订阅包含多个发布,且在这些发布中同一个表以不同的 WHERE 子句被发布,则满足任一表达式的行都会被复制。如果订阅端是之前的版本,即使发布端定义了行筛选器,初始的表同步也不会使用这些行筛选器。

行筛选器在发布更改之前应用。如果订阅包含了多个发布,且在这些发布中同一个表以不同的筛选器(针对发布中的相同操作)被发布,则这些表达式会被用 OR 连接起来,因此满足任一表达式的行都会被复制。

这意味着,如果出现以下情况,所有其他筛选器都将变得多余:

(a) 其中一个发布完全没有筛选器;

(b) 其中一个发布是使用 FOR ALL TABLES 创建的;

(c) 其中一个发布是使用 FOR ALL TABLES IN SCHEMA 创建的,且该表属于该 schema。

如果发布包含分区表,发布的参数publish_via_partition_root将决定使用分区的行筛选器(如果该参数为 false,即默认值),还是分区根表的行筛选器。

psql 命令\dRp+\d <表名>可显示出任何行筛选器。

讨论:https://www.postgresql.org/message-id/flat/CAHE3wggb715X%2BmK_DitLXF25B%3DjE6xyNCH4YOwM860JR7HarGQ%40mail.gmail.com

示例

在 PostgreSQL 之前的版本中,你只能将整个表复制到订阅端,无法筛选特定的行。换句话说:你不能复制表中的一部分行。通过本次提交的特性,这一点成为了可能。

让我们先创建两个全新的实例,第一个用作发布端,第二个用作订阅端:

$ mkdir /var/tmp/pub && initdb -D /var/tmp/pub && echo "port=8888" >> /var/tmp/pub/postgresql.auto.conf
$ mkdir /var/tmp/sub && initdb -D /var/tmp/sub && echo "port=9999" >> /var/tmp/sub/postgresql.auto.conf

为了能够使用逻辑复制,需要将wal_level设置为 “logical”,让我们来进行配置,并启动两个实例:

$ echo "wal_level='logical'" >> /var/tmp/pub/postgresql.auto.conf
$ echo "wal_level='logical'" >> /var/tmp/sub/postgresql.auto.conf
$ pg_ctl -D /var/tmp/pub/ start -l /dev/null
$ pg_ctl -D /var/tmp/sub/ start -l /dev/null
$ psql -p 8888 -l
                                  List of databases
   Name    |  Owner   | Encoding |   Collate   |    Ctype    |   Access privileges   
-----------+----------+----------+-------------+-------------+-----------------------
 postgres  | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 | 
 template0 | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 | =c/postgres +
           |          |          |             |             | postgres=CTc/postgres
 template1 | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 | =c/postgres +
           |          |          |             |             | postgres=CTc/postgres
(3 rows)

$ psql -p 9999 -l
                                  List of databases
   Name    |  Owner   | Encoding |   Collate   |    Ctype    |   Access privileges   
-----------+----------+----------+-------------+-------------+-----------------------
 postgres  | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 | 
 template0 | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 | =c/postgres +
           |          |          |             |             | postgres=CTc/postgres
 template1 | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 | =c/postgres +
           |          |          |             |             | postgres=CTc/postgres
(3 rows)

尽管并非真正必需(只要表名和列名匹配),但现在我们要在两个集群中创建完全相同的模式和表:

$ cat c.sql 
create schema a;
create table a.t ( a int primary key, b text, c date );

$ psql -f /home/postgres/c.sql -p 8888

$ psql -f /home/postgres/c.sql -p 9999

假设在发布端的那张表中,我们有一种基于第二列(b列)进行的租户划分:

$ psql -c "insert into a.t select i,'aaa',now() from generate_series(1,1000) i" -p 8888

$ psql -c "insert into a.t select i,'bbb',now() from generate_series(1001,2000) i" -p 8888

$ psql -c "insert into a.t select i,'ccc',now() from generate_series(2001,3000) i" -p 8888

在 PostgreSQL 之前的版本中,对那张表进行逻辑复制时,唯一的选择就是复制整个表。自从有了本次提交的特性,我们可以对一部分行进行筛选:

$ psql -c "h create publication" -p 8888
Command:     CREATE PUBLICATION
Description: define a new publication
Syntax:
CREATE PUBLICATION name
    [ FOR ALL TABLES
      | FOR publication_object [, ... ] ]
    [ WITH ( publication_parameter [= value] [, ... ] ) ]

where publication_object is one of:

    TABLE [ ONLY ] table_name [ * ] [ WHERE ( expression ) ] [, ... ]
    ALL TABLES IN SCHEMA { schema_name | CURRENT_SCHEMA } [, ... ]

假设我们只想复制租户 ‘aaa’ 的数据,我们可以这样做:

$ psql -c "create publication pub1 for table a.t where (b = 'aaa')" -p 8888

当我们在订阅端接收数据时,只会得到该租户的数据:

$ psql -c "create subscription sub1 connection 'port=8888' publication pub1" -p 9999
NOTICE:  created replication slot "sub1" on publisher

$ psql -c "select distinct(b), count(*) from a.t group by b" -p 9999
  b  | count 
-----+-------
 aaa |  1000
(1 row)

非常不错的体验,感谢所有参与的社区人员。

参考

提交日志:https://git.postgresql.org/pg/commitdiff/52e4f0cd472d39d07732b99559989ea3b615be78