for each row触发器一定失败,因postgresql内核硬性禁止在行级触发器中执行refresh materialized view,首次触发即报错;必须用for each statement、显式concurrently及全覆盖insert/update/delete/truncate事件,否则卡死或不同步。

不能用 FOR EACH ROW 触发器刷新物化视图,否则第一次触发就报错 ERROR: REFRESH MATERIALIZED VIEW cannot be executed from a trigger;必须用 FOR EACH STATEMENT + 显式 CONCURRENTLY + 全事件覆盖,否则不是卡死就是数据不同步。
为什么 FOR EACH ROW 一定失败
PostgreSQL 内核硬性禁止在行级触发器中执行 REFRESH MATERIALIZED VIEW,这不是权限或配置问题,是设计层面的阻断。哪怕函数语法正确、权限齐全,只要触发器定义里写了 FOR EACH ROW,首次 INSERT/UPDATE/DELETE 就会直接退出并抛出错误。
- 批量操作(如
INSERT INTO t VALUES (), (), ())会触发 N 次行级刷新,但物化视图内容只变一次,纯属重复 I/O 和锁竞争 -
REFRESH是事务级重量操作,依赖完整快照重建,天然不匹配单行变更粒度 - 函数能创建成功,但调用时才暴露问题——容易误以为“写好了”,上线后突然崩
REFRESH MATERIALIZED VIEW CONCURRENTLY 的三个硬条件
加了 CONCURRENTLY 不等于不锁表。漏掉任意一条,刷新会卡住、报错或退化成全表锁:
- 物化视图必须已存在且非空:首次初始化必须用普通
REFRESH MATERIALIZED VIEW mv_name,不能直接CONCURRENTLY - 物化视图上必须有至少一个
UNIQUE索引,例如CREATE UNIQUE INDEX idx_mv_user_id ON mv_user_summary(user_id) - 触发器函数里必须显式写出
CONCURRENTLY关键字,写成REFRESH MATERIALIZED VIEW mv_name就会锁死查询
最常见错误是建完 MATERIALIZED VIEW 忘记建唯一索引,结果刷新时报 cannot refresh materialized view "xxx" concurrently, because it has no unique index。
触发器事件必须覆盖 INSERT OR UPDATE OR DELETE OR TRUNCATE
只监听 INSERT 是典型疏漏。UPDATE 改聚合字段、DELETE 删记录、TRUNCATE 清空整表,都会让物化视图内容失真。
- 必须写成单一触发器:
AFTER INSERT OR UPDATE OR DELETE OR TRUNCATE ON base_table - 不要拆成多个触发器(比如一个管 INSERT、一个管 DELETE),维护成本高且容易漏事件
- PostgreSQL 12+ 才支持
OR TRUNCATE,旧版本需用EVENT TRIGGER捕获sql_drop事件,或禁止直接TRUNCATE
高频写入场景下触发器反而拖垮系统
当基表每秒 DML 超过 20–30 次(如实时日志、埋点表),每次触发都跑一次 REFRESH,CPU 和锁竞争会迅速飙升。
- 此时应放弃触发器,改用
pg_cron定时刷新(例如每 30 秒一次) - 或者换用
pg_ivm扩展实现真正增量更新,避免全量重算 - 如果聚合逻辑简单(如计数、求和),直接用触发器维护汇总表比物化视图更轻量、更可控
真正难的不是写对语法,而是判断“该不该用触发器”——它适合低频变更、强一致性要求的场景,不是万能自动刷新开关。











