FOR EACH STATEMENT 是唯一可行路径,因 PostgreSQL 硬限制禁止在行级触发器中执行 REFRESH MATERIALIZED VIEW;CONCURRENTLY 刷新需满足物化视图非空、有唯一索引、触发器显式调用三条件;事件须覆盖 INSERT/UPDATE/DELETE/TRUNCATE;高频写入应改用定时刷新或连续聚合。

REFRESH MATERIALIZED VIEW 不能在行级触发器里执行,直接写 FOR EACH ROW 触发器一定会失败。真想用触发器“自动刷新”,必须走语句级 + 函数封装 + 事件全覆盖这条路。
为什么 FOR EACH STATEMENT 是唯一可行路径
PostgreSQL 明确禁止在 FOR EACH ROW 触发器中调用 REFRESH MATERIALIZED VIEW,首次触发就会报错:ERROR: REFRESH MATERIALIZED VIEW cannot be executed from a trigger。这不是权限问题,是内核硬限制。
-
REFRESH是事务级重量操作,设计上就不是为逐行响应准备的 - 批量写入(比如
COPY或单条INSERT ... VALUES (), (), ())会触发 N 次行级刷新,但物化视图内容只变一次,纯属 I/O 浪费 -
FOR EACH STATEMENT保证每个 DML 语句(无论影响几行)只调用一次刷新函数,锁竞争和延迟都可控
CONCURRENTLY 刷新必须满足的三个条件
想让刷新不阻塞查询,REFRESH MATERIALIZED VIEW CONCURRENTLY 不是加个关键词就行——漏掉任意一条都会卡住或报错:
- 物化视图必须已存在且非空(首次刷新得先用普通
REFRESH初始化) - 物化视图上必须建有至少一个
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 是最典型疏漏。UPDATE 和 DELETE 同样会让物化视图过期——比如汇总订单金额的视图,删一条记录,总数就不对了。
- 触发器定义必须写全:
AFTER INSERT OR UPDATE OR DELETE ON base_table - 不要拆成多个触发器(一个管 INSERT、一个管 DELETE),维护成本高且容易漏事件
- 如果基表支持
TRUNCATE(PostgreSQL 12+),建议加上OR TRUNCATE,否则清空后物化视图完全不同步
高频写入场景下触发器反而成瓶颈
当基表每秒 DML 超过 20–30 次(如实时日志表、埋点表),每次变更都刷一次物化视图,CPU 和锁竞争会迅速拖垮系统。
- 此时应放弃触发器,改用
pg_cron定时刷新(例如每 30 秒一次) - 或者直接换方案:用 TimescaleDB 的连续聚合(
WITH (timescaledb.continuous)),后台常驻进程自动增量更新 - 手动维护汇总表 +
INSERT ... ON CONFLICT也是更可控的选择,尤其当聚合逻辑简单(如 COUNT/SUM)且源表有单调递增字段时
真正容易被忽略的是:物化视图刷新永远是全量重算,CONCURRENTLY 只解决读锁问题,不减少底层查询扫描和计算开销——大表 + 复杂 GROUP BY 场景下,刷新本身就会成为性能热点。

















