PostgreSQL的逻辑复制默认以表为单位传输行变更,但发布端可以定义更细的过滤规则。行过滤表达式决定哪些行进入复制流,列列表决定哪些字段被发送。将二者组合,就能在源头完成一部分数据掩码工作,避免敏感数据出现在订阅端网络流量和存储中。

一、发布端行过滤与列过滤的运行机制
发布(publication)是逻辑复制的出口,创建发布时可以针对表指定列列表和行过滤条件。列列表写在表名后面的圆括号中,只发布这些字段;行过滤条件跟在 WHERE 关键字后,由发布端的 walsender 进程在解码 WAL 之后对每一行变更进行求值。只有表达式结果为真的行才会被传输给订阅端。
CREATE TABLE orders (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
customer_name text,
email text,
phone text,
amount numeric,
order_date date,
region text
);
CREATE PUBLICATION pub_orders_filtered
FOR TABLE orders (id, amount, order_date, region)
WHERE region = 'east' AND amount > 100;
上面的发布只复制四个非敏感列,并且只复制东部地区金额大于 100 的单据。订阅端即使查询 publication 对应的表,也看不到 email、phone 等列,因为列列表把这些字段从复制数据集中裁掉了。初始数据同步阶段同样遵循该规则,而不是先全量复制再过滤。
行过滤表达式在源表行的旧值或新值上计算,具体取决于操作类型。对于 DELETE,表达式作用于旧值;对于 INSERT 和 UPDATE,表达式作用于新值。过滤只影响复制数据,不会影响源表本身。需要特别注意的是,过滤表达式中如果引用了容易变化的函数,可能导致发布端和订阅端对同一行的判断不一致,建议使用稳定且可重复计算的条件。
二、把过滤策略升级为数据掩码
列过滤是最直接的掩码方式。如果某些列完全不希望落入订阅端,就在发布时直接排除,例如手机号、身份证号、银行卡号等。这种方式不会改变源表结构,也不会影响源库的业务读写,目标库中这些列可以直接不存在,或者存在但没有数据。
但有些场景下,订阅端仍需要保留列结构,只是值需要脱敏。例如业务报表需要显示邮箱字段,但真实邮箱不能进入分析库。原生逻辑复制不提供列值转换函数,行过滤表达式也只能决定行的去留,无法直接修改字段内容。要完成这层掩码,常见做法是在订阅端增加触发器,先让原始数据复制到 staging 表或直接复制到目标表,再由触发器把敏感字段替换成固定掩码。
-- 订阅端 orders 表上的掩码触发器示例
CREATE OR REPLACE FUNCTION mask_orders()
RETURNS trigger AS $$
BEGIN
NEW.email := 'masked@ipipp.com';
NEW.phone := '0000000000';
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_mask_orders
BEFORE INSERT OR UPDATE ON orders
FOR EACH ROW EXECUTE FUNCTION mask_orders();
上面的函数与触发器属于订阅端自定义逻辑,与发布端过滤无关,但二者组合能形成更完整的掩码方案。在源库端排除真正不能外发的字段,在目标库端对允许外发但需要脱敏的字段做二次覆盖。这样既减少了网络传输中的敏感数据量,也避免了目标库直接存储明文。
三、配置一个带过滤与掩码的复制链路
假设源库为 inventory_db,目标库为 report_db。第一步在源库创建发布,同时使用行过滤与列过滤。列过滤直接去掉手机号和邮箱,行过滤只保留 active 状态的客户。
-- 源库执行 CREATE PUBLICATION pub_customer_active FOR TABLE customers (id, nickname, level, created_at) WHERE status = 'active';
第二步在目标库创建同结构表。如果列列表只有四列,订阅表只需要包含这四列即可,不需要包含被排除的列。需要注意的是,如果订阅表缺少主键或唯一索引,后续的 UPDATE 和 DELETE 可能因为复制标识不足而失败,所以最好保留主键。
-- 目标库执行
CREATE TABLE customers (
id bigint PRIMARY KEY,
nickname text,
level text,
created_at timestamptz
);
CREATE SUBSCRIPTION sub_customer_active
CONNECTION 'host=primary_db port=5432 dbname=inventory_db user=repl password=secret'
PUBLICATION pub_customer_active;
订阅创建后会先同步一次初始数据,随后进入增量复制。源端执行 INSERT、UPDATE 或 DELETE 时,只有满足 status = 'active' 的行才会被发送。如果一行状态从 active 改成 inactive,订阅端会收到对应的 DELETE;如果从 inactive 改成 active,则收到 INSERT。这就是行过滤在增量阶段的语义。
如果还希望在目标库显示 email 字段但内容脱敏,可以在目标库增加 email 列,并通过前面提到的触发器覆盖值。这样业务查询不需要改列名,但真实邮箱不会进入目标库。同时,源库发布端的列列表已经排除了原始 email 字段,真实的邮箱根本不会进入复制流,形成双重保护。
四、常见限制与性能考量
行过滤不会作用于 TRUNCATE。如果发布端执行 TRUNCATE,订阅端默认会同步该操作,行过滤和列列表无法阻止它。要避免意外清空订阅表,可以在订阅端通过权限控制或触发器限制 truncate 行为,或者不要在发布端直接对发布表执行 truncate。
过滤表达式在发布端计算,每一个变更行都要执行一次判断。对于高写入表,复杂的 CASE、正则或者子查询会明显增加 walsender 进程的 CPU 消耗。建议把过滤条件写成简单表达式,并让源表上有匹配索引帮助定位,尤其在初始数据同步阶段,WHERE 条件会被下推执行扫描,索引能显著减少扫描范围。
最后,发布端和订阅端的权限要分离管理。发布端账户通常只需要 SELECT 权限,订阅端账户只需要连接和写入权限。生产环境中不要把超级用户权限暴露给订阅连接,这本身也是贴合最小权限原则的掩码策略之一。通过列过滤、行过滤、订阅端触发器以及权限控制配合,可以在不引入额外脱敏中间件的情况下,构建一条相对安全的数据复制链路。
PostgreSQL逻辑复制行过滤数据掩码修改时间:2026-09-24 19:22:36