concurrently 刷新物化视图必须先创建满足条件的唯一索引,且物化视图需已通过普通刷新填充数据;索引须为valid状态、定义在视图上、含全部分组列、所有列not null,否则报错“does not have a unique index”。

CONCURRENTLY 刷新报错:没有唯一索引
直接执行 REFRESH MATERIALIZED VIEW CONCURRENTLY 一定会失败,除非你先在物化视图上建好符合要求的唯一索引。错误信息固定为:ERROR: cannot refresh materialized view "xxx" concurrently, because it does not have a unique index。这不是配置问题,也不是权限问题,是硬性前提——没索引,命令根本不会开始执行。
必须注意三点:
- 索引得显式创建在物化视图上,
CREATE UNIQUE INDEX ON mv_name (col),源表有主键或唯一约束也不算数 - 所有索引列必须
NOT NULL;如果字段允许 NULL,得用COALESCE(col, 'placeholder')或类似方式兜底 - 聚合类物化视图(含
GROUP BY a, b)必须把全部分组列都放进索引,只建(a)会逻辑错乱
哪些索引能被 CONCURRENTLY 识别
PostgreSQL 16 不接受“看起来像唯一”的索引,只认满足全部条件的:UNIQUE 或 PRIMARY KEY 类型、VALID 状态、定义在物化视图本身、不含函数表达式(比如 (UPPER(name)))、不带 WHERE 条件。
常见误判:
-
UNIQUE CONSTRAINT不等于索引——某些迁移后或分区表场景下,约束未触发隐式索引创建,必须手动CREATE UNIQUE INDEX - 刚建完索引可能状态是
INVALID,需等一次VACUUM或ANALYZE后才生效 - 物化视图处于
WITH NO DATA状态(空壳)时,CONCURRENTLY也会拒绝执行——必须先用普通刷新填充一次
并发刷新期间的真实干扰点
它不锁表,但不是无感操作。实际运行中容易出问题的地方集中在写冲突和资源上:
- 刷新过程中允许
SELECT,也允许大多数 DML,但如果业务写入恰好与新数据产生唯一键冲突,会直接报错:ERROR: duplicate key value violates unique constraint - 最后阶段尝试原子替换时,若某行已被业务
UPDATE过,会失败并提示:could not lock updated tuple in materialized view - 每次刷新都要额外磁盘空间存临时副本;每分钟刷一次,在大数据量下容易触发空间告警
- 性能通常比普通刷新慢 2–3 倍,因为要全量查源表 + 差集计算 + 两次索引扫描
物化视图定义阶段就要考虑索引结构
别等到刷新失败才回头改结构。设计时就该让“主键语义”稳定、非空、可索引:
- 优先选业务天然唯一、不可为空、极少变更的字段组合,比如
tenant_id + event_id,而不是name这类易变字段 - 避免冗余索引列,例如
GROUP BY category_id, category_name时,索引用(category_id)就够了,加category_name只增体积不增效果 - 源表真没自然键?可在定义里加伪主键:
row_number() OVER () AS mv_rowid,再对其建唯一索引
真正容易被忽略的是:索引建对了、字段非空、列全覆盖,但物化视图刚创建还没填充过数据,CONCURRENTLY 依然会拒之门外——必须先用普通刷新跑一次 REFRESH MATERIALIZED VIEW mv_name。











