物化视图必须显式建唯一索引,因为concurrently刷新依赖它逐行比对新旧数据;索引须为b-tree、字段全not null、覆盖group by列且顺序一致,类型需明确稳定,否则刷新报错或降级锁表。

为什么物化视图必须显式建唯一索引?
PostgreSQL 的 MATERIALIZED VIEW 本身不支持 PRIMARY KEY 或 UNIQUE 约束,但 REFRESH MATERIALIZED VIEW CONCURRENTLY 强制要求存在唯一索引——没有它,刷新会直接报错:ERROR: cannot refresh materialized view "mv_name" concurrently。这不是可选项,是并发刷新的硬性前提。
- 唯一索引的作用不是防重,而是让 PostgreSQL 能安全比对新旧数据行(通过唯一键逐行匹配 + 差异合并)
- 没有唯一索引时,
CONCURRENTLY 会被静默降级为全量锁表刷新,所有 SELECT 查询阻塞
- 索引字段必须覆盖全部
GROUP BY 列,且顺序需与 GROUP BY 完全一致(如 GROUP BY region, month,索引也得是 (region, month))
唯一索引怎么写才有效?
不能只靠业务逻辑“应该唯一”,PostgreSQL 只认索引定义。常见错误包括字段类型隐式转换、NULL 值干扰、表达式不匹配。
- 确保索引列都声明为
NOT NULL(否则唯一性失效:NULL 不等于 NULL,多行 NULL 会绕过约束)
- 避免用
EXTRACT(YEAR FROM order_date) 这类返回 double precision 的表达式做唯一键——类型不一致会导致索引无法用于并发刷新
- 更稳妥的做法是显式 cast 或用
date_trunc('year', order_date)(返回 timestamp,类型稳定)
- 命名建议按规范:如物化视图叫
mv_sales_summary,索引就叫 mv_sales_summary_region_month_uidx
CONCURRENTLY 会被静默降级为全量锁表刷新,所有 SELECT 查询阻塞GROUP BY 列,且顺序需与 GROUP BY 完全一致(如 GROUP BY region, month,索引也得是 (region, month))- 确保索引列都声明为
NOT NULL(否则唯一性失效:NULL 不等于 NULL,多行 NULL 会绕过约束) - 避免用
EXTRACT(YEAR FROM order_date)这类返回double precision的表达式做唯一键——类型不一致会导致索引无法用于并发刷新 - 更稳妥的做法是显式 cast 或用
date_trunc('year', order_date)(返回timestamp,类型稳定) - 命名建议按规范:如物化视图叫
mv_sales_summary,索引就叫mv_sales_summary_region_month_uidx
示例:
PostgreSQL 18.4 官方 Ubuntu 安装包现已发布,这是目前最新的稳定版本。推荐通过官方 APT 仓库安装:先执行 sudo apt update 更新索引,再运行 sudo apt install postgresql-18 即可完成部署。新版本引入了异步 I/O 子系统,在顺序扫描与 VACUUM 场景下性能提升显著,同时支持 UUID v7 原生生成函数与虚拟生成列。
CREATE UNIQUE INDEX mv_sales_summary_region_month_uidx
ON mv_sales_summary (region, date_trunc('month', order_date));
REFRESH CONCURRENTLY 报错说“索引不存在”或“不唯一”怎么办?
这类报错表面是语法问题,实际多是语义或元数据不匹配。
- 执行
\d mv_sales_summary 确认索引确实存在,且状态为 UNIQUE(不是普通 B-tree)
- 检查索引字段是否全为
NOT NULL:运行 SELECT column_name, is_nullable FROM information_schema.columns WHERE table_name = 'mv_sales_summary' AND column_name IN ('region', 'month');
- 如果物化视图定义里用了别名(如
EXTRACT(YEAR FROM order_date) AS year),索引必须用别名字段名,而不是原始表达式
- 刷新前务必先
ANALYZE mv_sales_summary,否则优化器可能误判唯一性分布,拒绝使用该索引
BRIN 或表达式索引能当唯一索引用吗?
不能。只有 B-tree 支持唯一性约束,BRIN、GIN、GiST 等都不行。
-
CREATE UNIQUE INDEX ... USING brin (...) 会直接报错:ERROR: index method "brin" does not support unique indexes
- 表达式索引可以是唯一的,但前提是表达式结果具备确定性 + 可比较性 + 类型明确,例如:
CREATE UNIQUE INDEX mv_users_lower_email_uidx
ON mv_users (lower(email));
- 但注意:如果源数据里
email 允许 NULL,则 lower(email) 也是 NULL,多行 NULL 会让该索引失去唯一约束效力
\d mv_sales_summary 确认索引确实存在,且状态为 UNIQUE(不是普通 B-tree)NOT NULL:运行 SELECT column_name, is_nullable FROM information_schema.columns WHERE table_name = 'mv_sales_summary' AND column_name IN ('region', 'month');
EXTRACT(YEAR FROM order_date) AS year),索引必须用别名字段名,而不是原始表达式ANALYZE mv_sales_summary,否则优化器可能误判唯一性分布,拒绝使用该索引B-tree 支持唯一性约束,BRIN、GIN、GiST 等都不行。
-
CREATE UNIQUE INDEX ... USING brin (...)会直接报错:ERROR: index method "brin" does not support unique indexes - 表达式索引可以是唯一的,但前提是表达式结果具备确定性 + 可比较性 + 类型明确,例如:
CREATE UNIQUE INDEX mv_users_lower_email_uidx ON mv_users (lower(email));
- 但注意:如果源数据里
email允许 NULL,则lower(email)也是 NULL,多行 NULL 会让该索引失去唯一约束效力
真正容易被忽略的是:唯一索引建完后,必须确保物化视图后续每次 REFRESH 都不会产生重复键——这取决于原始查询逻辑是否真的能保证组合唯一,而不是仅仅依赖索引声明。










