PostgreSQL 16无内核级增量刷新能力,所有REFRESH变体均为全量重跑原始SELECT;CONCURRENTLY仅通过唯一索引实现upsert合并,不跳过源表任何行,也不感知变更,必须预建NOT NULL、覆盖分组列的VALID唯一索引且物化视图已初始化填充。

PostgreSQL 16 根本没有增量刷新的内核能力
它不读 WAL、不捕获变更、不依赖日志表,REFRESH MATERIALIZED VIEW 所有变体(包括 CONCURRENTLY)都是全量重跑原始 SELECT 查询。所谓“增量”只是外部误传——官方文档从没提过“incremental refresh”,所有性能优化都落在查询定义本身。
CONCURRENTLY 不是增量,是 upsert 合并
加了 CONCURRENTLY 后仍会:全量扫描源表 → 全量执行原 SQL → 得到新结果集 → 用唯一索引比对旧数据 → 插入新增行、删除旧行。这过程不跳过任何源行,也不感知哪几行变了。
- 必须提前在物化视图上建
UNIQUE INDEX,否则直接报错:ERROR: cannot refresh materialized view "xxx" concurrently, because it does not have a unique index - 索引列必须全部
NOT NULL,且覆盖全部分组列(如GROUP BY a, b就得建(a, b),顺序不能反) - 刚建完索引可能状态是
INVALID,需先执行VACUUM my_mv或ANALYZE my_mv才被识别 - 物化视图若处于
WITH NO DATA状态(即空壳),CONCURRENTLY会直接拒绝,必须先用普通刷新填一次
真正能减少刷新开销的,只有 SQL 定义层
刷新耗时 90% 以上卡在原始查询本身。与其折腾刷新参数,不如检查:
- 是否用了
DISTINCT ON、窗口函数、random()、now()?这些会让CONCURRENTLY直接失效 - 聚合字段类型是否和
WHERE条件严格一致?比如EXTRACT(YEAR FROM ts)返回double precision,就不能写WHERE year = 2023(整型),否则隐式转换废掉索引 - 能否加
WHERE updated_at > (SELECT max(updated_at) FROM mv_cache)把全量查源表变成范围扫描?这是唯一可控的“伪增量”路径
想真增量,只能靠外部扩展或手动逻辑
PostgreSQL 内核不提供基于变更捕获的增量机制。目前可行方案只有:
-
pg_matview:把物化视图当普通表 + 触发器记录变更 + 手写INSERT ON CONFLICT,但它不自动推导变化行,全靠你维护逻辑 - 手动维护位点:比如在
matview_refresh_state表里存最新updated_at,每次刷新前读取,再拼进查询条件 - 金仓 KingbaseES 等兼容版数据库才原生支持增量刷新,标准 PostgreSQL 16 没这个功能
最容易被忽略的是:即使索引建对、字段非空、列全覆盖,只要物化视图定义里含不可重放表达式(如无 ORDER BY 的 DISTINCT ON),CONCURRENTLY 就会在比对阶段失败,且错误信息可能不直接暴露根本原因。

















