PostgreSQL增量刷新实为CONCURRENTLY模式下的upsert合并,并非自动差量更新;需唯一索引,仍全量执行查询;真正低延迟需触发器+增量表组合实现。

增量刷新不是“自动差量更新”,而是依赖日志或唯一索引的显式机制
PostgreSQL 原生不支持 Oracle 那种基于 MLOG$ 日志的自动增量刷新。所谓“增量刷新”,在 PostgreSQL 中实际指 REFRESH MATERIALIZED VIEW CONCURRENTLY —— 它并不是只更新变化行,而是利用物化视图上已有的 UNIQUE 索引,把新结果集和旧数据做“upsert”合并:先插入新增/变更行,再删除旧行(需满足唯一约束)。它不读取源表变更日志,也不感知哪几行被改了。
这意味着:
• 你不能省略 UNIQUE 索引,否则 CONCURRENTLY 会报错 ERROR: cannot refresh materialized view "xxx" concurrently because it does not have a unique index
• 即使源表只改了 1 行,这个命令仍要全量执行定义查询,再比对、合并——只是“不锁表”而已
• 真正的“只查变更数据”的增量逻辑,得靠你自己在物化视图的 SELECT 语句里实现,比如加 WHERE updated_at > (SELECT max(updated_at) FROM mv_cache) 这类条件
pg_cron 是最轻量可靠的调度方案,但必须注意权限与连接池配置
用 pg_cron 调度刷新任务,比写 shell 脚本 + psql 或用外部调度器(如 Airflow)更贴合数据库内核,也避免网络延迟和身份认证问题。但它不是装完就能用:
- 安装后需确保
pg_cron的后台进程能连上目标数据库:检查postgresql.conf中shared_preload_libraries = 'pg_cron'已启用,且服务已重启 - 执行
cron.schedule()的用户必须有postgres角色或被显式授予权限:GRANT USAGE ON SCHEMA cron TO your_user; - 默认 cron job 在
postgres数据库下运行,若物化视图在其他库,需用database_name.schema_name.mv_name全限定名,或在对应库中启用扩展 - 避免使用
*通配时间表达式(如'*/5 * * * *'),高并发下易触发多个实例;建议用固定分钟数,例如'5,10,15,20,25,30,35,40,45,50,55 * * * *'
全量 vs 并发刷新:别只看“是否锁表”,要看失败后怎么恢复
很多人选 CONCURRENTLY 只为“不阻塞查询”,却忽略了它的失败行为更难处理:
-
REFRESH MATERIALIZED VIEW(全量)失败时,物化视图内容不变,状态可预测;重试安全 -
REFRESH MATERIALIZED VIEW CONCURRENTLY失败时,可能已部分更新(插入了新行但没删完旧行),导致数据重复或缺失;必须手动清理或重建 - 并发刷新要求目标物化视图所有列都参与唯一索引——如果业务需要按
(user_id, date_trunc('day', created_at))去重,就得建函数索引:CREATE UNIQUE INDEX ON sales_summary (user_id, date_trunc('day', created_at)) - 若物化视图含聚合(如
GROUP BY),且源表存在多对一关系,CONCURRENTLY可能因无法映射到唯一键而报错could not identify rows for update
真正低延迟的增量策略:得绕开物化视图本身,用物化+触发器+时间窗口
当业务要求“5 分钟内看到新订单汇总”,又不想每 5 分钟全刷一次千万级订单表,硬靠 CONCURRENTLY 并不能解决问题。更可行的做法是:
- 把物化视图拆成两层:一层存“历史快照”(每日全量生成),另一层用普通表 +
AFTER INSERT/UPDATE触发器维护“最近 1 小时增量缓存” - 查询时
UNION ALL两者,并加WHERE created_at > NOW() - INTERVAL '1 hour'过滤实时部分 - 定时任务(如 pg_cron)每 5 分钟 truncate 并重填增量表,而非刷新物化视图
- 这样既规避了物化视图刷新的 I/O 峰值,又能控制延迟在分钟级;缺点是逻辑分散,需额外维护触发器和增量表结构
复杂点在于:触发器逻辑必须严格幂等,且不能阻塞源表写入;增量表的分区或 TTL 清理策略得跟上,否则磁盘会悄悄涨满。











