默认刷新会获取ACCESS EXCLUSIVE锁,阻塞所有SELECT和DML;加CONCURRENTLY可避免读阻塞,但需唯一索引、非空列、全覆盖分组列三条件,且仍持SHARE UPDATE EXCLUSIVE锁。

物化视图刷新默认会锁表,REFRESH MATERIALIZED VIEW 不能并发
PostgreSQL 原生 REFRESH MATERIALIZED VIEW 语句执行时会获取 ACCESS EXCLUSIVE 锁,阻塞所有读写——哪怕只是查 SELECT 都会被卡住。这不是性能问题,是设计限制:老版本(
所以直接写 REFRESH MATERIALIZED VIEW mv_orders 就等同于“停服维护”,线上系统基本不可行。
PostgreSQL 9.4+ 支持 CONCURRENTLY,但有硬性前提
从 9.4 开始引入 CONCURRENTLY 关键字,但不是加了就生效——它要求物化视图**必须有唯一索引**,且该索引需覆盖所有行(即能唯一标识每一行),否则报错:
ERROR: cannot refresh materialized view "mv_orders" concurrently HINT: Create a unique index with no WHERE clause on one or more columns of the materialized view.
常见错误操作包括:
- 建了普通
INDEX而非UNIQUE INDEX - 唯一索引带
WHERE条件(比如WHERE status = 'active') - 索引列包含可为
NULL的字段(UNIQUE索引对NULL不做唯一约束) - 用
text列做唯一索引但没指定排序规则(如COLLATE "C"),导致比较行为不一致
正确做法示例:
CREATE UNIQUE INDEX ON mv_orders (id);
或(如果 id 可能为空,改用组合主键):
CREATE UNIQUE INDEX ON mv_orders (order_id, created_at) WHERE order_id IS NOT NULL;
并发刷新实际执行逻辑:先比对再更新,不锁全表
REFRESH MATERIALIZED VIEW CONCURRENTLY 不是“边刷边用”,而是分三步:
- 用新查询结果生成临时快照
- 逐行对比旧视图与快照,只
INSERT/UPDATE/DELETE差异行 - 全程只在目标行上加行级锁,不影响其他行的读取
这意味着:
- 刷新耗时明显更长(尤其数据量大、差异多时)
- 无法避免“刷新中看到部分新数据、部分旧数据”的短暂不一致(最终一致性)
- 若刷新期间底层表被大量修改,可能触发重复对比开销,甚至因事务膨胀拖慢整体性能
典型调用方式:
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_orders;
生产环境建议:配合定时任务 + 监控 + 备用方案
并发刷新不是银弹,容易被忽略的关键点:
-
CONCURRENTLY刷新不能在事务块里执行(会报ERROR: REFRESH MATERIALIZED VIEW CONCURRENTLY cannot be executed from a function or multi-statement query) - 没有内置重试机制,失败后不会自动回滚到旧状态——刷新中断后视图处于“半更新”状态,需人工干预
- pg_stat_progress_refresh_matview 视图可查进度,但仅限当前会话;跨会话监控得靠日志或外部轮询
- 若业务对强一致性要求高(比如财务对账),宁可用非并发刷新 + 应用层双写缓存,也不赌
CONCURRENTLY的中间态
真正上线前,务必在测试库跑通完整链路:建唯一索引 → 手动触发 CONCURRENTLY → 模拟并发 SELECT 和底层表 UPDATE → 查看锁等待和延迟毛刺。

















