# 物化视图 · 把慢查询的结果冻起来

> 普通 VIEW 只是 SQL 别名，每次查都重算；MATERIALIZED VIEW 把结果物化到磁盘并可加索引，读取 = 直接读物理表，比反复重算快几个量级。代价是数据不实时，要 REFRESH——而 REFRESH CONCURRENTLY 能在不锁读的情况下刷新。

- 作者：David（道雾轩）
- 专栏：PostgreSQL 特性详解（https://daiw.org/manual/postgresql.md）
- 最后更新：2026-07-13
- 原文：https://daiw.org/manual/postgresql/materialized-views
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# 物化视图 · 把慢查询的结果冻起来

有一类查询，算一次很贵（几张大表 JOIN + 聚合），结果却被反复读（BI 仪表盘每次打开都跑一遍）。每次重算，既慢又浪费。物化视图就是这个矛盾的解：**算一次，把结果存下来，之后直接读存好的。**

## VIEW 与 MATERIALIZED VIEW：算不算存下来

先分清两个「视图」：

- **`VIEW`** —— 只是给一段 SQL 起了个名字。每次查询视图，底层 SQL **重新执行一遍**。它省的是「打字」，不省「计算」。
- **`MATERIALIZED VIEW`** —— 把查询结果**物化到磁盘**，变成一张真实的物理表，还能加索引。之后查它 = 直接读这张表，**完全不重算原始查询**。

实测把一份 5000 行的销售数据聚合成日汇总：

```sql
-- 普通视图:每次查都现算这 719 行
CREATE VIEW v_daily_sales AS
  SELECT day, sum(amount) AS total FROM sales GROUP BY day;

-- 物化视图:719 行算一次,存进磁盘(实测占 64 kB)
CREATE MATERIALIZED VIEW mv_daily_sales AS
  SELECT day, sum(amount) AS total FROM sales GROUP BY day;
```

两者查询结果一样（都是 719 行），但物化视图读的是存好的结果，原始 5000 行的聚合一次都不用重跑。数据量从几千变成几千万时，这个差别就是「秒开」和「转圈十秒」的区别。

## 代价：数据不实时，要 REFRESH

天下没有免费的午餐。物化视图的结果是**某一刻的快照**，源数据后来变了，它**不会自动更新**：

```text
往 sales 新增一行(2025-07-15, 99999)
→ 查 mv,那天的 total 仍是旧值(None,没刷新)
→ REFRESH MATERIALIZED VIEW 之后,才变成 99999
```

所以物化视图适合**能容忍一点延迟**的场景：日报、周报、排行榜、仪表盘——晚几分钟无所谓。刷新可以用定时任务（如 `pg_cron`）周期性跑。

## REFRESH CONCURRENTLY：刷新时不锁读

普通 `REFRESH MATERIALIZED VIEW` 有个问题：**刷新期间会锁表**，读也被挡住。对一个「大家随时在看」的仪表盘，这很致命。`REFRESH ... CONCURRENTLY` 解决它：

```sql
-- 前提:物化视图上得有唯一索引
CREATE UNIQUE INDEX ON mv_daily_sales (day);

REFRESH MATERIALIZED VIEW CONCURRENTLY mv_daily_sales;  -- 刷新时照常能读
```

<Callout type="info">
  `CONCURRENTLY` 为什么需要唯一索引？因为它不是「清空重灌」，而是**在后台把新结果算好，再和旧数据逐行比对，只更新有变化的行**。要「逐行比对」，就必须能唯一标识每一行——这正是唯一索引的作用。代价是它比普通 REFRESH 慢（要算新结果 + 做 diff），但换来「刷新期间不阻塞读」，对在线服务几乎是必须的。
</Callout>

## PostgreSQL 的短板：只能全量刷新

得诚实说一个短板：**PostgreSQL 内置的物化视图只支持「全量刷新」**——每次 REFRESH 都把整个查询重算一遍，哪怕源数据只改了一行。它没有 Oracle 那种「增量 / 快速刷新」（只算变化部分）。

<Callout type="warn">
  如果你的物化视图很大、但每次源数据只变一点点，全量刷新会很浪费。解法有两个：① 用 **`pg_ivm`** 扩展做增量物化视图（Incremental View Maintenance，源表一变就增量更新）；② 自己用「普通表 + 触发器 / 定时增量」手搓。选型时把这条记住：**PG 原生物化视图 = 全量刷新**，超大 + 高频更新的场景要另想办法。
</Callout>

## 横向对比

| 能力 | PostgreSQL | MySQL | Oracle | SQL Server |
| --- | --- | --- | --- | --- |
| 物化视图 | **有** | ❌ 无（自己拿表 + 触发器凑） | 有（功能最强） | 索引视图(indexed view) |
| 并发刷新 | `REFRESH CONCURRENTLY` | — | 有 | 自动维护 |
| 增量刷新 | ❌ 需 `pg_ivm` 扩展 | — | **有(fast refresh)** | 自动增量维护 |
| 查询自动改写 | ❌ 无 | — | **有(query rewrite)** | 有限 |

这里 PostgreSQL 不是最强的：**Oracle 的物化视图有增量刷新 + 查询改写**（优化器自动把命中的原始查询改写到物化视图上），更省心。**SQL Server 的索引视图自动维护**（但限制多）。**MySQL 干脆没有**，只能用表 + 触发器 / 定时任务模拟。PostgreSQL 处在中间：有、好用、但要手动刷新、且默认全量。

## 应用场景

- **BI 仪表盘 / 报表**：复杂多表聚合预计算成物化视图，前端秒开；定时(`pg_cron`)每几分钟 `REFRESH CONCURRENTLY`。
- **排行榜 / 榜单**：热度、销量榜算一次存下，读高频、更新低频，天生适配。
- **复杂 JOIN 中间结果**：反复用到的多表连接结果物化掉，下游查询直接读。
- **跨库 / 外部数据快照**：通过 `postgres_fdw` 拉外部数据，物化成本地快照，减少远程往返。

## 小结与下一章

物化视图用「一点延迟」换「读取快几个量级」，是应对「贵查询被反复读」的经典手段。`REFRESH CONCURRENTLY` 让它能在线刷新不挡读。要清醒的是它的短板——原生只支持全量刷新，超大高频场景要靠 `pg_ivm` 或手搓增量。

分区、物化视图都是「让大表 / 慢查询变快」的手段，但前提是——你得先知道**慢在哪**。规模与性能的最后一章，教你用 EXPLAIN 读懂查询计划，这是排查慢查询的第一手段。👉 [EXPLAIN：读懂 PostgreSQL 的执行计划](https://daiw.org/manual/postgresql/explain)。

## 参考文献

- [1] PostgreSQL 官方文档.「41. Materialized Views / SQL-CREATEMATERIALIZEDVIEW.」https://www.postgresql.org/docs/current/rules-materializedviews.html —— 物化视图概念与刷新。
- [2] PostgreSQL 官方文档.「REFRESH MATERIALIZED VIEW.」https://www.postgresql.org/docs/current/sql-refreshmaterializedview.html —— `CONCURRENTLY` 与唯一索引要求。
