跳转到主要内容

49 物化视图

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

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

本章要点

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

示例

示例 49.1 物化视图
-- 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。因此它非常适合缓存昂贵的查询结果,让许多其他查询读取这些结果。

运行示例

运行方式(需要 customers_example 和 orders_example):

$ 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)

 ...

常见陷阱

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

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

动手练习

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

下载

SQL 文件:materialized-views.sql · 全部示例文件 · 准备工作