PostgreSQL物化视图无自更新能力,需用FOR EACH STATEMENT触发器调用REFRESH,禁用FOR EACH ROW;CONCURRENTLY刷新须满足非空、唯一索引、显式声明三条件;事件须覆盖INSERT/UPDATE/DELETE/TRUNCATE;高频写入应改用定时或增量方案。

PostgreSQL 里没有“自更新”的物化视图——MATERIALIZED VIEW 本身是静态快照,必须靠外部动作触发刷新。所谓“自动”,本质是用触发器监听基表变更,再调用 REFRESH MATERIALIZED VIEW。但这个机制有硬约束,踩错一步就失效或拖垮性能。
为什么必须用 FOR EACH STATEMENT 而不是 FOR EACH ROW
因为 REFRESH MATERIALIZED VIEW 是语句级操作,不能在行级触发器里执行;PostgreSQL 会直接报错 ERROR: REFRESH MATERIALIZED VIEW cannot be executed from a trigger(如果用了 FOR EACH ROW)。更关键的是性能:一次 INSERT INTO users VALUES (...), (...), (...) 插入 3 行,行级触发器会执行 3 次刷新,而物化视图内容只变一次,纯属浪费。
-
FOR EACH STATEMENT保证每个 DML 语句(无论影响几行)只触发一次刷新 - 批量导入(如
COPY或事务内多行 INSERT)下,语句级能避免 N 倍 I/O 和锁竞争 - 如果误写成
FOR EACH ROW,函数创建会成功,但首次触发时必然报错退出
CONCURRENTLY 刷新的三个前提条件缺一不可
想让刷新不锁表、允许并发读,必须同时满足:
- 物化视图已存在且非空(首次刷新不能用
CONCURRENTLY,得先用普通REFRESH初始化) - 物化视图上建有至少一个
UNIQUE索引(例如CREATE UNIQUE INDEX idx_mv_user_id ON mv_user_summary(user_id)) - 刷新函数里明确写
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_name,漏掉CONCURRENTLY就会锁表
常见错误是建完物化视图忘了加唯一索引,结果刷新时卡住或报 ERROR: cannot refresh materialized view "mv_xxx" concurrently, because it has no unique index。
触发器事件必须覆盖 INSERT OR UPDATE OR DELETE
只监听 INSERT 是最常见的疏漏。UPDATE 和 DELETE 同样会让物化视图数据过期——比如汇总销售额的视图,删掉一条订单记录,总金额就不对了。
- 触发器定义里必须写全:
AFTER INSERT OR UPDATE OR DELETE ON base_table - 不要拆成多个触发器(如一个只管 INSERT,另一个只管 DELETE),维护成本高且容易漏事件
- 如果基表支持
TRUNCATE,也建议加上OR TRUNCATE(PostgreSQL 12+ 支持),否则清空表后物化视图不会同步
高频率写入场景下,触发器方案反而成瓶颈
当基表每秒有几十次以上 DML(比如日志表、实时埋点表),每次变更都刷一次物化视图,会导致:
- 刷新操作堆积,I/O 和 CPU 持续拉满
-
CONCURRENTLY刷新因唯一索引冲突失败(尤其多并发 UPDATE 同一主键范围时) - 事务等待刷新完成,拖慢上游业务响应
这时该换方案:用 pg_cron 定时刷新(如每 5 分钟一次),把“实时性”和“系统负载”做取舍。触发器不是万能解法,它只适合变更频次低、延迟容忍度高的场景(比如配置表、用户档案表)。真正高频更新的物化视图,得靠增量计算或 pg_ivm 这类扩展,而不是原生触发器硬扛。










