PostgreSQL 16 不支持原生增量刷新,REFRESH MATERIALIZED VIEW CONCURRENTLY 仍为全量执行查询,仅通过唯一索引实现 upsert 合并;真增量需人工设计 WHERE 条件+外部状态表管理。

PostgreSQL 16 原生不支持真正意义上的“增量刷新”——它没有 Oracle 那种基于物化视图日志(MLOG$)的自动差量捕获能力。所谓“增量”,在社区版 PostgreSQL 中,是靠人工设计查询逻辑 + CONCURRENTLY 机制模拟出来的效果,不是数据库自动识别变更行。
REFRESH MATERIALIZED VIEW CONCURRENTLY 不等于增量更新
很多人误以为加了 CONCURRENTLY 就是只刷变化数据。事实相反:
-
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_name仍会全量执行定义查询(比如SELECT ... FROM orders WHERE updated_at > '2025-01-01'),只是用唯一索引做 upsert 合并:插入新行、删除旧行 - 必须存在覆盖所有列或关键列的
UNIQUE索引,否则报错:ERROR: cannot refresh materialized view "xxx" concurrently because it does not have a unique index - 即使源表只改了 1 行,这个命令也得重跑整个 SELECT —— 它不读 WAL,也不查变更标记
- 如果你的物化视图定义里没带时间/版本过滤条件,那每次
CONCURRENTLY刷的仍是全量结果集,只是“不锁表”而已
真增量要靠 WHERE 条件 + 外部状态管理
想让刷新只处理新增/修改的数据,必须把“增量逻辑”写进物化视图的 AS SELECT ... 语句里。常见做法是依赖时间戳或序列字段:
- 确保源表有可靠递增字段,如
updated_at或version;不能用created_at(无法捕获更新) - 物化视图定义中显式加过滤,例如:
CREATE MATERIALIZED VIEW mv_orders_daily AS SELECT date_trunc('day', updated_at), COUNT(*) FROM orders WHERE updated_at > (SELECT COALESCE(MAX(ts), '1970-01-01') FROM mv_refresh_log WHERE name = 'mv_orders_daily') GROUP BY 1 - 配套建一张
mv_refresh_log表记录每次刷新的最大时间点,并在刷新后UPDATE它——这步不能省,否则下次还是全刷 - 注意事务隔离:
SELECT MAX(ts)和后续REFRESH必须在同一个事务里,否则可能漏掉并发写入
用 pg_cron 调度时必须注意权限与限定名
pg_cron 是目前最贴近内核的调度方案,但配置稍有不慎就会静默失败:
- 确认
shared_preload_libraries = 'pg_cron'已写入postgresql.conf并重启服务 - 执行
SELECT cron.schedule(...)的用户需有USAGE ON SCHEMA cron权限:GRANT USAGE ON SCHEMA cron TO your_user - 如果物化视图不在
postgres库,必须用全限定名:REFRESH MATERIALIZED VIEW CONCURRENTLY otherdb.public.mv_name,或先连到目标库再启用扩展 - 避免用
'*/5 * * * *'这类通配表达式;高负载下易触发多个并发刷新任务,建议用离散分钟列表:'5,15,25,35,45,55 * * * *'
CONCURRENTLY 刷新失败后状态不可预测
这是最容易被忽略的风险点:
- 全量刷新
REFRESH MATERIALIZED VIEW失败,物化视图内容不变,重试安全 -
CONCURRENTLY失败时,可能已插入部分新行但未删完旧行,导致重复或缺失——你无法通过系统视图判断哪些行被改过 - 没有内置回滚机制;出错后要么手动
DELETE冲突数据,要么重建:REFRESH MATERIALIZED VIEW mv_name WITH NO DATA; REFRESH MATERIALIZED VIEW CONCURRENTLY mv_name - 生产环境建议搭配日志表 +
pg_stat_activity监控,捕获refresh类型的长时间运行语句










