广告位 · 页头(后台-模块管理-新建模块后替换此区域)

PostgreSQL逻辑复制时如何过滤全文搜索相关对象?

PostgreSQL的逻辑复制以发布和订阅为基础,只会把发布中指定表的INSERT、UPDATE、DELETE操作传递到订阅端。文本搜索对象里,tsvector列属于表数据,会随着行变更一起被复制;但TEXT SEARCH DICTIONARY、TEXT SEARCH CONFIGURATION、TEXT SEARCH PARSER这些对象属于数据库元数据,逻辑复制不会自动同步。因此谈到过滤文本搜索对象时,必须同时考虑数据层的筛选和元数据层的手动同步。

PostgreSQL逻辑复制时如何过滤全文搜索相关对象?

一、先厘清逻辑复制对文本搜索对象的实际行为

发布(publication)定义一组要复制的表,订阅(subscription)接收这些表的变更流。官方逻辑复制从PostgreSQL 10开始提供,直到15版本才加入行级过滤能力。常见的CREATE PUBLICATION pub FOR ALL TABLES;会把当前数据库中所有用户表都加入发布,包括那些为全文搜索而单独拆分的表,比如article_fts、log_search等。这些表通常包含一个或多个tsvector列以及配套的GIN索引。

如果发布范围过大,tsvector列的数据会原样进入复制流。对于高频更新的文章、日志或评论表,tsvector可能非常占用空间,订阅端不仅要接收这些向量,还要在本地维护GIN索引,回放成本明显增加。反过来,真正需要的文本搜索配置和字典却不会从发布端传过来,订阅端如果直接执行全文检索,可能会报缺少文本搜索配置的错误。这个差异是很多逻辑复制故障的根源。

所以第一步是判断哪些对象属于数据,哪些属于DDL。表、行、tsvector列、GIN索引属于数据或依附于表的对象;文本搜索模板、解析器、字典、配置属于独立的数据库对象,必须通过其他方式同步。过滤策略不能只用一种手段解决所有问题。

二、表级过滤:只发布需要的表,剔除全文搜索辅助表

最直接的方式是在创建发布时列出需要复制的表,而不是使用FOR ALL TABLES。例如一个电商系统只需要同步订单、客户和产品表,不需要同步独立的全文搜索索引表,可以这样创建发布:

CREATE PUBLICATION pub_core FOR TABLE orders, customers, products;

如果之前已经创建了全表发布,可以通过ALTER PUBLICATION把不需要的表移除:

ALTER PUBLICATION pub_all DROP TABLE article_fts, log_search;

执行后,可以用系统视图确认发布中剩余的表:

SELECT schemaname, tablename
FROM pg_publication_tables
WHERE pubname = 'pub_core';

这种表级过滤简单可靠,但前提是全文搜索数据存储在最宽表之外的独立表里。如果业务表本身就带有一个大tsvector生成列,比如articles表上有一个search_vec列用于站内搜索,仅仅排除整张表并不可行,因为业务数据本身也需要同步。这时就需要进一步使用行级过滤,或者把文本搜索列拆分成单独的表。

拆分方案通常使用一对一关联表,例如articles保存标题、正文、发布时间,articles_fts保存article_id和search_vec。发布时只发布articles,订阅端如果需要全文搜索,再单独构建articles_fts。这种设计也更利于控制WAL流量和订阅端索引维护,是一种较干净的过滤思路。

三、行级过滤器:把不需要的文档行挡在发布端

PostgreSQL 15在发布上支持WHERE子句,允许只复制满足条件的行。对于全文搜索场景,最常见需求是只同步已发布状态的文章,或者排除某些测试文档。例如:

CREATE PUBLICATION pub_articles FOR TABLE articles
WHERE (status = 'published');

这个过滤条件会在发布端执行,不满足条件的行不会进入逻辑复制的变更流。如果后面一行从草稿变成已发布,UPDATE操作会使其进入匹配范围,订阅端会看到一次INSERT;如果一行从已发布变成草稿,订阅端会收到DELETE。逻辑复制协议会自动处理这种跨过滤边界的变更。

有些人希望直接用全文搜索函数来过滤,例如只复制包含某个关键词的文章:

CREATE PUBLICATION pub_fts FOR TABLE articles
WHERE (to_tsvector('english', content) @@ 'postgres'::tsquery);

但这个写法在官方文档中很可能无法通过。行过滤表达式要求使用不可变函数,而to_tsvector的稳定性级别取决于文本搜索配置,通常不是IMMUTABLE,因此发布创建会报错。稳妥做法是先在表上增加一个存储生成列把全文搜索条件结果固定下来,再用这个列做行过滤:

ALTER TABLE articles
ADD COLUMN fts_match boolean
GENERATED ALWAYS AS (to_tsvector('english', content) @@ 'postgres'::tsquery) STORED;
CREATE PUBLICATION pub_fts FOR TABLE articles
WHERE (fts_match = true);

如果生成列也受到函数稳定性限制,可以在写入时由应用层显式维护这个布尔列,或使用触发器更新。行过滤表达式中还禁止子查询、用户自定义函数和部分系统函数,创建发布前最好先用EXPLAIN验证表达式能否在发布端正常规划。

四、订阅端如何补齐文本搜索对象与索引

逻辑复制不会把发布端的DDL发送到订阅端,因此在订阅端必须独立创建相同的文本搜索字典、解析器和配置。一个常见做法是在订阅端维护一套文本搜索对象的DDL脚本,并在创建订阅前执行:

CREATE TEXT SEARCH DICTIONARY simple_dict (
    TEMPLATE = pg_catalog.simple
);
CREATE TEXT SEARCH CONFIGURATION public.article_cfg (
    PARSER = pg_catalog.default
);
ALTER TEXT SEARCH CONFIGURATION article_cfg
    ADD MAPPING FOR asciiword WITH simple_dict;

表结构本身可以通过pg_dump只导出模式来同步,不会携带数据:

pg_dump -h publisher_host -d source_db -t articles --schema-only | psql -h subscriber_host -d target_db

注意这条命令是在操作系统Shell中执行,而不是在PostgreSQL客户端内运行,发布端和订阅端的主机名、数据库名需要根据实际环境替换。如果还要同步多个表,可以在-t参数中重复指定,或者直接使用--schema=public导出整个模式。

订阅端的GIN索引也需要单独创建。逻辑复制同步的是表数据,不会自动同步索引。如果在订阅端执行全文检索,需要手动建立索引:

CREATE INDEX idx_articles_search_vec
ON articles USING gin(search_vec);

如果希望订阅端在数据同步完成后自动生成tsvector,可以使用存储生成列或触发器。比如在订阅端执行:

ALTER TABLE articles
ADD COLUMN search_vec tsvector
GENERATED ALWAYS AS (to_tsvector('article_cfg', title || ' ' || content)) STORED;
CREATE INDEX idx_articles_search_vec
ON articles USING gin(search_vec);

这种方式把全文搜索的计算放在订阅端本地完成,发布端无需复制任何tsvector数据。代价是订阅端在每次INSERT或UPDATE时会计算向量,如果写入量大,需要评估CPU开销。对于发布端已经过滤掉部分行的场景,订阅端只对同步过来的行做计算,成本相对可控。

文本搜索对象过滤的本质,是不要把数据复制和元数据同步混为一谈。表级列表排除全文搜索辅助表,行级过滤器减少不必要的文档行,DDL脚本补齐字典和配置,这三步组合起来,可以在逻辑复制环境中有效控制文本搜索相关的数据流量和同步复杂度。实际落地时,还要注意发布端行过滤函数稳定性限制,以及订阅端索引的独立维护。

广告位 · 文章底部(后台-模块管理替换此区域)