# 物化视图

> 在物化视图中存储查询结果并刷新它们，同时管理读者访问。

---

LLMS 索引： [llms.txt](/llms.txt)

---

*物化视图*是将结果存储在磁盘上的视图。与普通视图不同，它不会在每次读取时重新运行查询，而是返回已保存的行。代价在于结果会过时：在你执行 `REFRESH` 之前，看到的都是上次刷新时的结果。物化视图是 PostgreSQL 中缓存昂贵聚合、搜索索引和预计算报表的标准方式。

## 本章要点 {#learn}

- 创建和刷新物化视图
- `REFRESH MATERIALIZED VIEW` 与 `REFRESH MATERIALIZED VIEW CONCURRENTLY` 的区别
- 为物化视图建立索引
- 检测物化视图的陈旧程度
- 何时使用物化视图、普通视图或缓存表

## 示例 {#example}

```sql {num="49.1" caption="物化视图"}
-- A materialized view: aggregate per customer
DROP MATERIALIZED VIEW IF EXISTS customer_spend_mv;
CREATE MATERIALIZED VIEW customer_spend_mv AS
SELECT c.id              AS customer_id,
       c.name            AS customer_name,
       count(o.id)       AS order_count,
       COALESCE(sum(o.total), 0) AS total_spent,
       max(o.id)         AS last_order_id
FROM customers_example c
LEFT JOIN orders_example o ON o.customer_id = c.id
GROUP BY c.id, c.name;

-- Query it like a table
SELECT * FROM customer_spend_mv ORDER BY total_spent DESC;

-- Index it (a matview is a real table, so it supports indexes)
CREATE UNIQUE INDEX customer_spend_mv_pk ON customer_spend_mv (customer_id);
CREATE INDEX customer_spend_mv_total_idx ON customer_spend_mv (total_spent);

-- Add another order, then refresh
INSERT INTO orders_example VALUES (200, 1, 25.00);

-- Plain REFRESH locks the matview against reads for the duration
REFRESH MATERIALIZED VIEW customer_spend_mv;

-- CONCURRENTLY lets readers continue. Requires a UNIQUE index on the matview.
REFRESH MATERIALIZED VIEW CONCURRENTLY customer_spend_mv;

SELECT * FROM customer_spend_mv ORDER BY total_spent DESC;

-- Find matviews and when they were last refreshed (PostgreSQL keeps the relation modification time)
SELECT schemaname, matviewname, hasindexes, ispopulated
FROM pg_matviews
WHERE schemaname = 'public';

DROP MATERIALIZED VIEW IF EXISTS customer_spend_mv;
```

物化视图是一张表，只有在你要求时，PostgreSQL 才会将它与查询同步。`REFRESH MATERIALIZED VIEW name` 会重新运行底层的 `SELECT` 并替换存储的行。默认情况下，刷新会获取 `ACCESS EXCLUSIVE` 锁——刷新期间读者不能查询。

`REFRESH MATERIALIZED VIEW CONCURRENTLY` 更新物化视图时不会阻塞读者。代价在于物化视图必须已经填充，并且存在覆盖所有行的 `UNIQUE` 索引，且索引基于列名而不是表达式。它会重新运行完整查询，比对并协调结果；这不是增量视图维护。对于需要保持可查询的物化视图，这是正确的选择。

物化视图是磁盘上的真实表——你可以在其上创建索引、对其运行 `ANALYZE`，甚至运行 `VACUUM`。因此它非常适合缓存昂贵的查询结果，让许多其他查询读取这些结果。

## 运行示例 {#run}

运行方式（需要 `customers_example` 和 `orders_example`）：

```text
$ psql -f source/materialized-views.sql postgres
DROP MATERIALIZED VIEW
SELECT 3
 customer_id | customer_name | order_count | total_spent | last_order_id
-------------+---------------+-------------+-------------+---------------
           1 | Alice         |           3 |      179.00 |            10
           2 | Bob           |           0 |           0 |
           3 | Carol         |           0 |           0 |
(3 rows)

CREATE INDEX
CREATE INDEX
INSERT 0 1
REFRESH MATERIALIZED VIEW
REFRESH MATERIALIZED VIEW
 customer_id | customer_name | order_count | total_spent | last_order_id
-------------+---------------+-------------+-------------+---------------
           1 | Alice         |           4 |      204.00 |           200
           2 | Bob           |           0 |           0 |
           3 | Carol         |           0 |           0 |
(3 rows)

 ...
```

## 常见陷阱 {#pitfalls}

- 物化视图在刷新前都是陈旧的。如果忘记刷新，报表会悄悄落后。请设置定时任务或调度器。
- `REFRESH ... CONCURRENTLY` 至少需要一个 `UNIQUE` 索引。没有这样的索引时，PostgreSQL 会报错。请在你视为物化视图“主键”的列上添加唯一索引（通常是源表主键中的某一列）。
- 外键不能引用物化视图，刷新时触发器也不会触发——这些都是它与普通表的实际差异。
- `REFRESH` 会从头开始运行原始的 `SELECT`。如果该查询很慢，刷新也会很慢。若要增量刷新，目前需要自行实现或使用 `pg_ivm` 扩展。

> [!TIP] 提示
> 将物化视图用于仪表盘：每晚预先聚合一次“昨天的指标”，之后仪表盘即可即时读取。若要求亚分钟级新鲜度，实时写入路径更新或普通视图通常更合适。

> [!NOTE] 动手练习
> 在脚本最后的 `DROP MATERIALIZED VIEW` 之前运行此实验，或者先重新创建视图及其索引。向 `orders_example` 添加几行。查询物化视图——它仍显示旧的总额。运行 `REFRESH MATERIALIZED VIEW customer_spend_mv;` 并再次查询。然后再添加几行，在另一个会话正在读取时使用 `CONCURRENTLY` 形式。

## 下载 {#downloads}

SQL 文件：[materialized-views.sql](/examples/source/materialized-views.sql) · [全部示例文件](/examples/examples.zip) · [准备工作](/#prerequisites)
