postgresql 16物化视图不支持增量更新,refresh materialized view(含concurrently)均为全量重算;concurrently仅通过唯一索引实现upsert式原子合并,需满足索引、非空、分组列覆盖、valid状态及已初始化等全部条件。

PostgreSQL 16 原生不支持增量更新,所谓“增量刷新”是常见误解;所有 REFRESH MATERIALIZED VIEW 操作(含 CONCURRENTLY)都是全量重跑原始 SELECT,不读 WAL、不捕获变更、不跳过任何源表行。
REFRESH MATERIALIZED VIEW CONCURRENTLY 不是增量,是 upsert 合并
- 它执行流程固定为:全量扫描源表 → 全量执行原查询 → 得到新结果集 → 用唯一索引逐行比对旧数据 → 插入新增/变更行 + 删除旧行
- 这个过程不感知哪几行变了,也不依赖时间戳或日志,只是“用新结果覆盖旧状态”的原子合并
- 错误信息
ERROR: cannot refresh materialized view "xxx" concurrently, because it does not have a unique index是硬性拦截,不是可绕过警告
必须满足以下全部条件才能启用 CONCURRENTLY:
- 物化视图上已存在
UNIQUE或PRIMARY KEY索引(不能只靠源表主键) - 索引列全部为
NOT NULL(允许 NULL 的字段需用COALESCE(col, 'placeholder')处理) - 若定义含
GROUP BY a, b,索引必须包含(a, b)全部分组列,顺序一致 - 索引状态为
VALID(新建后可能为INVALID,需先执行VACUUM my_mv或ANALYZE my_mv) - 物化视图已通过普通刷新填充过数据(
WITH NO DATA状态下直接拒绝)
真正能减少刷新开销的,只有 SQL 定义层优化
90% 的刷新耗时卡在原始查询本身。与其纠结“是否并发”,不如检查定义是否可裁剪:
- 避免
DISTINCT ON、窗口函数、random()、now()—— 这些会让CONCURRENTLY直接失效 - 聚合字段类型需与
WHERE条件严格一致,例如EXTRACT(YEAR FROM ts)返回double precision,就不能写WHERE year = 2023(整型),否则隐式转换废掉索引 - 加范围条件把全量扫描转为增量式扫描:
WHERE updated_at > (SELECT COALESCE(MAX(updated_at), '1970-01-01') FROM mv_refresh_state WHERE mv_name = 'my_mv')
并在每次刷新成功后更新mv_refresh_state表中对应记录
调度刷新任务时,pg_cron 是最贴合内核的选择,但配置有坑
- 必须在
postgresql.conf中启用:shared_preload_libraries = 'pg_cron',且重启服务 - 执行
cron.schedule()的用户需被显式授权:GRANT USAGE ON SCHEMA cron TO your_user - 若物化视图不在
postgres库,必须用全限定名:database_name.schema_name.mv_name - 时间表达式避免用
<em>/5 </em> <em> </em> <em></em>,高并发下易触发多个实例;推荐写死分钟点:'5,10,15,20,25,30,35,40,45,50,55 <em> </em> *' -
CONCURRENTLY刷新失败后可能处于半更新状态(已插未删),无法自动回滚,必须人工干预或重建
最容易被忽略的是:即使索引建对、字段非空、列全覆盖,只要物化视图定义里含不可重放表达式(如无 ORDER BY 的 LIMIT、OFFSET、或未加 STABLE 标记的自定义函数),CONCURRENTLY 就会静默退化为全量锁表刷新。










