双向同步需设update_data_source字段防循环,default 0 not null;after insert/update/delete须分写;跨库优先用postgres_fdw;聚合或高频表应避免触发器。

双向同步必须绕开 UPDATE_DATA_SOURCE 字段防循环
双向同步(A 表改 → B 表同步,B 表改 → A 表同步)天然会触发嵌套调用,不加拦截就会无限递归。最常用且有效的拦截手段是引入一个标记字段,比如 UPDATE_DATA_SOURCE,约定:只有值为 0(表示“业务写入”)时才触发同步;触发器自己执行的写入,必须显式设为 1 或其他非零值。
这个字段不能靠应用层“记得填”,必须设为列默认值 + NOT NULL 约束:
ALTER TABLE a_table ADD COLUMN UPDATE_DATA_SOURCE SMALLINT DEFAULT 0 NOT NULL;
否则任意一条绕过应用层的直连 SQL(如 DBA 手动 INSERT、ETL 工具导入)都会让同步逻辑失效。
AFTER INSERT/UPDATE/DELETE 必须分开写,别塞进一个触发器
混写一个触发器处理三种 DML 类型,会导致逻辑混乱、调试困难、运行时报错难定位。比如在 INSERT 场景下读 DELETED 表,PostgreSQL 里根本不存在这个表,直接报错;SQL Server 里 DELETED 为空,但代码没判空就引用,也会崩。
正确做法是按操作类型拆开:
-
AFTER INSERT:只读NEW,同步新增记录到对端表 -
AFTER UPDATE:对比NEW和OLD,只同步真正变化的字段(例如只改了name,就别碰email) -
AFTER DELETE:只读OLD,清理对端对应行(注意外键级联是否已做这事,重复删可能报错)
跨库同步优先用 postgres_fdw,别硬上 dblink
虽然 dblink 能实现跨库写入,但它要求触发器函数里手动拼接连接字符串、管理连接生命周期,容易泄露连接、超时卡死事务。而 postgres_fdw 是 PostgreSQL 官方推荐的跨库访问方式,把远端表映射成本地 FOREIGN TABLE 后,触发器里就能像操作本地表一样 INSERT/UPDATE/DELETE,语义清晰、错误明确、事务自动传播。
关键步骤:
- 源库执行:
CREATE EXTENSION IF NOT EXISTS postgres_fdw; - 创建远程服务:
CREATE SERVER remote_b SERVER postgres_fdw OPTIONS (host '192.168.1.100', port '5432', dbname 'b_db'); - 建用户映射:
CREATE USER MAPPING FOR CURRENT_USER SERVER remote_b OPTIONS (user 'sync_user', password 'xxx'); - 建外部表:
CREATE FOREIGN TABLE b_table (...) SERVER remote_b OPTIONS (table_name 'b_table');
之后触发器函数里直接写 INSERT INTO b_table SELECT ... FROM NEW; 即可。
聚合类或高频小表同步,触发器不是首选
如果同步逻辑涉及 COUNT(*)、SUM() 等聚合计算,或者目标表是日志、点击流这类每秒写入上千条的表,触发器会成为性能瓶颈。并发更新时没加 SELECT ... FOR UPDATE 锁主记录,结果必然错乱;高频写入则让每个事务都卡在触发器里,拖垮整个数据库。
这类场景该换方案:
- 聚合类:提前物化视图,或由定时任务刷新
- 高频小表:走应用层消息队列(如 Kafka),触发器只用于低频、强一致核心表(如用户资料、订单主表)
- 双向多主:直接上
Bucardo,它基于触发器捕获变更,但封装了冲突检测、重试、状态跟踪等完整能力,比手写可靠得多
真正难的从来不是“怎么写触发器”,而是判断“该不该在这里写”。一旦发现要加锁、要查全表、要调远程服务、要处理冲突,就该停下来想:是不是已经超出触发器的设计边界了。










