Skip to content

REFRESH MATERIALIZED VIEW ​

REFRESH MATERIALIZED VIEW 是物化视图的「更新按钮」:它重跑定义查询,把结果替换进已存数据。默认行为是整表重建并持 ACCESS EXCLUSIVE 锁——理解这一点,才能规划好刷新窗口、降低对业务的影响,并在必要时改用并发刷新。本节讲透刷新语义、阻塞影响、刷新策略、进度观察与失败处理。

前置知识

阅读本节前,建议先了解:CREATE MATERIALIZED VIEW、锁与并发

基础概念 ​

刷新的语义 ​

text
REFRESH MATERIALIZED VIEW mv;

  1. 对 mv 加 ACCESS EXCLUSIVE 锁(阻塞一切读写,包括 SELECT)
  2. 重新执行 mv 的定义查询,读取基表的最新数据
  3. 用结果替换掉 mv 中已有的全部内容
  4. 释放锁

三个必须记住的点:

事实说明
刷新读的是最新数据定义查询重新执行,读到基表当前已提交的数据
默认是全量替换不是增量更新,旧内容全部丢弃
持 ACCESS EXCLUSIVE 锁刷新期间连 SELECT 都会被阻塞

两个 WITH DATA 语境要分清 ​

WITH DATA / WITH NO DATA 在 CREATE 和 REFRESH 里的含义一致,但用法不同:

语句WITH DATA(默认)WITH NO DATA
CREATE MATERIALIZED VIEW创建并立即填充创建空壳(ispopulated = false)
REFRESH MATERIALIZED VIEW重新填充(正常刷新)清空内容,变成未填充状态

REFRESH ... WITH NO DATA 的实际用途很少,主要是「临时腾空间」或「标记为待重填」:

sql
-- 清空物化视图内容(等于把它退回未填充状态)
REFRESH MATERIALIZED VIEW mv_x WITH NO DATA;

SELECT ispopulated FROM pg_matviews WHERE matviewname = 'mv_x';
text
 ispopulated
-------------
 f
(1 row)

刷新策略总览 ​

策略触发方式适用
定时刷新pg_cron / 系统 crontab报表、看板(最常见)
事件触发基表变更后由应用或触发器调用数据变化不频繁但要求较新
手动刷新运维/开发者按需执行临时排查、一次性汇总
链式刷新顶层任务按依赖顺序依次刷新多级物化视图

语法与用法 ​

text
REFRESH MATERIALIZED VIEW [ CONCURRENTLY ] name
    [ WITH [ NO ] DATA ]

-- 默认(全量 + ACCESS EXCLUSIVE 锁)
REFRESH MATERIALIZED VIEW mv_name;

-- 并发刷新(需唯一索引,不阻塞读)
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_name;

-- 清空内容
REFRESH MATERIALIZED VIEW mv_name WITH NO DATA;

-- 事务控制要点
BEGIN;
SET LOCAL lock_timeout = '5s';
REFRESH MATERIALIZED VIEW mv_name;
COMMIT;

最简可运行示例:

sql
CREATE TABLE metrics (
  id      bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  host    text NOT NULL,
  cpu     numeric(5,2) NOT NULL,
  ts      timestamptz NOT NULL DEFAULT now()
);

INSERT INTO metrics (host, cpu) VALUES ('web-1', 12.5), ('web-1', 30.0), ('web-2', 55.0);

CREATE MATERIALIZED VIEW mv_host_cpu AS
SELECT host, round(avg(cpu), 2) AS avg_cpu, count(*) AS samples
FROM metrics GROUP BY host;
text
CREATE TABLE
INSERT 0 3
SELECT 2
sql
SELECT * FROM mv_host_cpu ORDER BY host;
text
 host  | avg_cpu | samples
-------+---------+---------
 web-1 |   21.25 |       2
 web-2 |   55.00 |       1
(2 rows)
sql
-- 新增数据
INSERT INTO metrics (host, cpu) VALUES ('web-1', 80.0);

-- 刷新(默认全量)
REFRESH MATERIALIZED VIEW mv_host_cpu;

SELECT * FROM mv_host_cpu ORDER BY host;
text
REFRESH MATERIALIZED VIEW
 host  | avg_cpu | samples
-------+---------+---------
 web-1 |   40.83 |       3
 web-2 |   55.00 |       1
(2 rows)

详细说明 ​

默认刷新持 ACCESS EXCLUSIVE 锁:会阻塞一切 ​

这是默认刷新最大的风险。ACCESS EXCLUSIVE 是最高级别锁,与包括 SELECT 在内的所有锁模式冲突:

text
锁冲突矩阵(简化)
                 ACCESS EXCLUSIVE
SELECT (AccessShare)      冲突  ← 连读都被挡
INSERT/UPDATE/DELETE      冲突
其它 DDL                  冲突

验证方式:开两个会话,一个 REFRESH 慢查询,另一个 SELECT:

sql
-- 会话 A:刷新一个耗时的物化视图
REFRESH MATERIALIZED VIEW mv_slow;

-- 会话 B:随便读一下(会被阻塞在锁上)
SELECT count(*) FROM mv_slow;
sql
-- 会话 C:观察阻塞
SELECT
  pid,
  state,
  wait_event_type,
  wait_event,
  left(query, 60) AS query
FROM pg_stat_activity
WHERE datname = current_database() AND pid <> pg_backend_pid()
ORDER BY state;
text
  pid  |   state  | wait_event_type |  wait_event  |              query
-------+----------+-----------------+--------------+----------------------------------
 12003 | active   |                 |              | REFRESH MATERIALIZED VIEW mv_slow
 12044 | active   | Lock            | relation     | SELECT count(*) FROM mv_slow
(2 rows)

wait_event_type = Lock / wait_event = relation 就是被锁住的典型特征。

降低影响的三个手段:

sql
-- 1) 设锁超时,避免刷新长时间排队(宁可失败,不堵住全局)
SET lock_timeout = '3s';
REFRESH MATERIALIZED VIEW mv_slow;
RESET lock_timeout;

-- 2) 用并发刷新(不阻塞读)
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_slow;

-- 3) 安排在业务低峰

刷新期间的执行过程 ​

内部实现上,刷新是「新建临时堆表 → 执行查询 → 替换」(旧内容被丢弃):

text
REFRESH 内部步骤(示意)
  1. 建新的临时存储(relfilenode)
  2. 执行定义查询,把结果写入新存储
  3. 交换 relfilenode,让物化视图指向新数据
  4. 清理旧存储
  5. 索引随之重建(普通索引在刷新时整体重建)

因此刷新的耗时 ≈ 定义查询的耗时 + 写索引的耗时。定义查询慢,刷新必然慢:

sql
-- 先量一下定义查询本身的耗时
EXPLAIN (ANALYZE, TIMING ON, SUMMARY ON, COSTS OFF)
SELECT host, round(avg(cpu), 2), count(*) FROM metrics GROUP BY host;
text
                                QUERY PLAN
--------------------------------------------------------------------------
 HashAggregate (actual time=12.343..12.345 rows=100 loops=1)
   Group Key: host
   ->  Seq Scan on metrics (actual time=0.015..6.212 rows=200000 loops=1)
 Planning Time: 0.187 ms
 Execution Time: 12.456 ms

优化刷新的第一步永远是优化这条查询(基表索引、减少扫描行数、预过滤)。

刷新耗时优化:先治查询本身 ​

手段效果
在基表上建索引定义查询扫描更快,刷新更短
缩小结果集(加时间窗口过滤)写入量小,刷新更快
减少 JOIN 与聚合层数直接减少计算
分区表 + 只刷最近分区(自定义逻辑)用增量替代全量
只依赖必要的列减少 IO 与结果宽度
用并发刷新降低锁影响(不减少耗时,反而更慢)

时间窗口过滤是降刷新成本最有效的手段之一:

sql
-- 只物化最近 90 天,结果集小、刷新快
CREATE MATERIALIZED VIEW mv_recent_metrics AS
SELECT host, date_trunc('hour', ts) AS hour, avg(cpu) AS avg_cpu
FROM metrics
WHERE ts >= now() - interval '90 days'
GROUP BY host, hour;

刷新与事务 ​

两条硬性规则:

  1. REFRESH 不能在只读事务中执行。
  2. REFRESH 不能在事务块里执行 CONCURRENTLY(并发刷新不能在事务块内使用)。
sql
-- 错误!只读事务中不能刷新
BEGIN READ ONLY;
REFRESH MATERIALIZED VIEW mv_host_cpu;
COMMIT;
text
ERROR:  cannot execute REFRESH MATERIALIZED VIEW in a read-only transaction
sql
-- 错误!并发刷新不能在事务块内使用
BEGIN;
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_host_cpu;
COMMIT;
text
ERROR:  REFRESH MATERIALIZED VIEW CONCURRENTLY cannot be executed within a transaction block

普通刷新可以放在事务里(这样能配合 SET LOCAL lock_timeout):

sql
BEGIN;
SET LOCAL lock_timeout = '5s';
REFRESH MATERIALIZED VIEW mv_host_cpu;
COMMIT;

如果刷新失败,整个事务回滚,物化视图保持原内容——这是默认刷新少有的「原子性」优势(并发刷新则可能失败在中间状态,见并发刷新)。

用 pg_stat_activity 观察刷新进度 ​

PostgreSQL 没有针对普通刷新的专用进度视图(pg_stat_progress_* 系列里没有 REFRESH MATERIALIZED VIEW),只能通过 pg_stat_activity 观察:

sql
-- 找出正在执行的刷新
SELECT
  pid,
  now() - query_start AS running_for,
  state,
  wait_event_type,
  wait_event,
  query
FROM pg_stat_activity
WHERE query ILIKE 'REFRESH MATERIALIZED VIEW%'
  AND state <> 'idle';
text
  pid  | running_for  | state  | wait_event_type | wait_event |                query
-------+--------------+--------+-----------------+------------+-------------------------------------
 12003 | 00:00:24.512 | active | IO              | DataFileRead | REFRESH MATERIALIZED VIEW mv_big
(1 row)

配合 pg_locks 看它等了什么、又被谁等:

sql
-- 刷新持有哪些锁
SELECT l.locktype, l.relation::regclass AS rel, l.mode, l.granted
FROM pg_locks l
WHERE l.pid = 12003
ORDER BY l.granted;
text
 locktype |        rel        |        mode         | granted
----------+-------------------+---------------------+---------
 relation | mv_big            | AccessExclusiveLock | t
 relation | pg_class          | AccessShareLock     | t
(2 rows)
sql
-- 谁在等这次刷新(被阻塞的会话)
SELECT a.pid, a.query, l.mode
FROM pg_locks l
JOIN pg_stat_activity a ON a.pid = l.pid
WHERE l.relation = 'mv_big'::regclass AND NOT l.granted;
text
  pid  |            query             |        mode
-------+------------------------------+---------------------
 12044 | SELECT count(*) FROM mv_big  | AccessShareLock
(1 row)

判断「还要多久」

没有原生进度。实用做法是记录历史刷新耗时(见下文监控),用历史均值估算本次剩余时间;必要时对特别大的物化视图拆分成多个小的分片物化视图并行刷新。

批量刷新脚本与依赖顺序 ​

多级物化视图必须按依赖顺序刷新(自底向上):

bash
#!/usr/bin/env bash
# refresh_matviews.sh —— 按依赖顺序批量刷新
set -euo pipefail

PSQL="psql -X -q -d appdb -v ON_ERROR_STOP=1"

# 顺序:越靠底层越先刷
MATVIEWS=(
  "mv_user_spend"        # 依赖 fact_orders
  "mv_top_spenders"      # 依赖 mv_user_spend
  "mv_daily_report"      # 依赖 mv_user_spend
)

for mv in "${MATVIEWS[@]}"; do
  echo "[$(date '+%F %T')] refreshing ${mv} ..."
  start=$(date +%s%3N)
  ${PSQL} -c "SET lock_timeout='30s'; REFRESH MATERIALIZED VIEW CONCURRENTLY ${mv};"
  end=$(date +%s%3N)
  echo "[$(date '+%F %T')] ${mv} done in $((end - start)) ms"
done

用「从依赖图自动推导顺序」的 SQL 生成刷新清单:

sql
-- 依据 pg_depend 推导依赖深度,生成刷新顺序
WITH RECURSIVE mv_deps AS (
  SELECT c.oid, c.relname, 0 AS depth
  FROM pg_class c
  WHERE c.relkind = 'm'
    AND NOT EXISTS (
      SELECT 1 FROM pg_depend d
      JOIN pg_rewrite r ON r.oid = d.objid
      JOIN pg_class src ON src.oid = d.refobjid
      WHERE r.ev_class = c.oid AND src.relkind IN ('m', 'v')
    )
  UNION ALL
  SELECT c.oid, c.relname, m.depth + 1
  FROM pg_class c
  JOIN pg_depend d ON d.refobjid = c.oid
  JOIN pg_rewrite r ON r.oid = d.objid AND r.ev_class <> c.oid
  JOIN mv_deps m ON m.oid = r.ev_class
  WHERE c.relkind = 'm'
)
SELECT relname, depth FROM mv_deps ORDER BY depth, relname;
text
     relname      | depth
------------------+-------
 mv_user_spend    |     0
 mv_top_spenders  |     1
 mv_daily_report  |     1
(3 rows)

定时刷新:pg_cron 与系统 crontab ​

pg_cron 是第三方扩展(不属于 PostgreSQL 内核),能在数据库内定义定时任务:

sql
-- 需要预加载并安装扩展
-- shared_preload_libraries = 'pg_cron'  (在 postgresql.conf 中)
CREATE EXTENSION IF NOT EXISTS pg_cron;

-- 每 10 分钟刷新一次
SELECT cron.schedule(
  'refresh-mv-host-cpu',
  '*/10 * * * *',
  $$REFRESH MATERIALIZED VIEW CONCURRENTLY mv_host_cpu$$
);
text
CREATE EXTENSION
    schedule
----------------
               1
(1 row)
sql
-- 查看任务与执行历史
SELECT jobid, schedule, command, active FROM cron.job;
SELECT jobid, status, start_time, end_time, return_message
FROM cron.job_run_details ORDER BY start_time DESC LIMIT 5;
text
 jobid |   schedule   |                       command                       | active
-------+--------------+-----------------------------------------------------+--------
     1 | */10 * * * * | REFRESH MATERIALIZED VIEW CONCURRENTLY mv_host_cpu | t
(1 row)
sql
-- 删除任务
SELECT cron.unschedule('refresh-mv-host-cpu');

如果不用扩展,用系统 crontab + psql 也能达到同样效果(见上文批量脚本)。

刷新失败后的处理 ​

刷新失败有多种原因,处理方式各异:

失败原因报错特征处理
锁等待超时canceling statement due to lock timeout改用并发刷新,或换时间窗口
被手动取消canceling statement due to user request重试
定义查询报错(如除零)业务函数错误修查询
并发刷新缺唯一索引cannot refresh materialized view ... concurrently创建唯一索引
磁盘满No space left on device扩容 / 清理

默认刷新的一个重要特性是「失败即保持原内容」(因为整体是原子的):失败不会留下半成品。

sql
-- 验证:刷新失败后内容不变
SELECT count(*) FROM mv_host_cpu;   -- 记下这个值
-- 制造失败(示意:超时)
SET lock_timeout = '1ms';
REFRESH MATERIALIZED VIEW mv_host_cpu;
RESET lock_timeout;
text
ERROR:  canceling statement due to lock timeout
sql
-- 内容仍然是旧的,没有被破坏
SELECT count(*) FROM mv_host_cpu;
text
 count
-------
     2
(1 row)

并发刷新则没有这个保证——它可能失败在中间状态,此时物化视图内容可能是「部分旧 + 部分新」甚至违反唯一约束(见并发刷新)。

刷新与 autovacuum 的交互 ​

刷新会整体重写物化视图的内容,产生大量死元组。autovacuum 会像处理普通表一样处理它:

sql
-- 观察物化视图的膨胀与清理情况
SELECT
  relname,
  n_live_tup,
  n_dead_tup,
  last_vacuum,
  last_autovacuum,
  last_analyze,
  last_autoanalyze
FROM pg_stat_user_tables
WHERE relname = 'mv_host_cpu';
text
   relname    | n_live_tup | n_dead_tup | last_vacuum |   last_autovacuum   | last_analyze | last_autoanalyze
--------------+------------+------------+-------------+---------------------+--------------+------------------
 mv_host_cpu  |          2 |      12000 |             | 2026-09-28 10:55:01 |              | 2026-09-28 10:55:02
(1 row)

死元组数量会随刷新次数累积,因此:

建议原因
保持 autovacuum 开启自动回收死元组与更新统计
刷新后主动 ANALYZE刷新不保证统计信息新鲜
频繁刷新的大物化视图调低 autovacuum_vacuum_scale_factor让 autovacuum 更早介入
必要时 VACUUM / REINDEX处理膨胀与索引膨胀
sql
-- 频繁刷新的物化视图:让 autovacuum 更积极
ALTER MATERIALIZED VIEW mv_host_cpu SET (
  autovacuum_vacuum_scale_factor = 0.02,
  autovacuum_analyze_scale_factor = 0.02
);

实战示例 ​

场景:为「主机监控聚合」建立物化视图,实现定时刷新,并对比普通刷新与并发刷新的阻塞差异,最后建立刷新监控。

第 1 步:造一张有写入压力的表

sql
DROP TABLE IF EXISTS host_metrics CASCADE;

CREATE TABLE host_metrics (
  id   bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  host text NOT NULL,
  cpu  numeric(5,2) NOT NULL,
  mem  numeric(5,2) NOT NULL,
  ts   timestamptz NOT NULL DEFAULT now()
);

INSERT INTO host_metrics (host, cpu, mem, ts)
SELECT
  'host-' || lpad((1 + (g % 200))::text, 3, '0'),
  round((random() * 100)::numeric, 2),
  round((random() * 100)::numeric, 2),
  now() - (g || ' seconds')::interval
FROM generate_series(1, 1000000) AS g;

CREATE INDEX idx_host_metrics_host ON host_metrics (host);
CREATE INDEX idx_host_metrics_ts   ON host_metrics (ts);
ANALYZE host_metrics;
text
DROP TABLE
CREATE TABLE
INSERT 0 1000000
CREATE INDEX
CREATE INDEX
ANALYZE

第 2 步:创建物化视图(含唯一索引,为并发刷新做准备)

sql
CREATE MATERIALIZED VIEW mv_host_health AS
SELECT
  host,
  round(avg(cpu), 2) AS avg_cpu,
  round(max(cpu), 2) AS max_cpu,
  round(avg(mem), 2) AS avg_mem,
  count(*)           AS samples,
  max(ts)            AS last_seen
FROM host_metrics
WHERE ts >= now() - interval '1 day'
GROUP BY host;

CREATE UNIQUE INDEX uq_mv_host_health ON mv_host_health (host);
ANALYZE mv_host_health;
text
SELECT 200
CREATE INDEX
ANALYZE

第 3 步:测量默认刷新耗时

sql
\timing on

REFRESH MATERIALIZED VIEW mv_host_health;
text
REFRESH MATERIALIZED VIEW
Time: 428.517 ms

第 4 步:测量并发刷新耗时(对比)

sql
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_host_health;
text
REFRESH MATERIALIZED VIEW
Time: 612.884 ms

并发刷新更慢(要对比新旧内容做差异更新),但它不阻塞读。这是核心取舍:

刷新方式耗时阻塞读
默认刷新428 ms阻塞(ACCESS EXCLUSIVE)
并发刷新613 ms不阻塞

第 5 步:用会话模拟「刷新期间的读阻塞」

sql
-- 会话 A:普通刷新(模拟)
-- 另开一个终端执行,这里用 psql 的 \! 或直接分两次调用说明

-- 会话 B(在刷新进行期间):
SELECT count(*) FROM mv_host_health;
-- 结果:被阻塞,直到刷新结束才返回

在实际操作中可用如下方式观察:

sql
-- 会话 A 的执行
REFRESH MATERIALIZED VIEW mv_host_health;

-- 会话 C 立即查询阻塞情况
SELECT pid, state, wait_event_type, wait_event, left(query, 50) AS q
FROM pg_stat_activity
WHERE query ILIKE '%mv_host_health%' AND pid <> pg_backend_pid();
text
  pid  | state  | wait_event_type | wait_event |                        q
-------+--------+-----------------+------------+----------------------------------
 12003 | active | IO              | DataFileRead | REFRESH MATERIALIZED VIEW mv_host_health
 12044 | active | Lock            | relation     | SELECT count(*) FROM mv_host_health

第 6 步:建立刷新监控表

sql
CREATE TABLE IF NOT EXISTS mv_refresh_log (
  id           bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  mv_name      text        NOT NULL,
  started_at   timestamptz NOT NULL,
  finished_at  timestamptz NOT NULL,
  duration_ms  integer     NOT NULL,
  row_count    bigint,
  succeeded    boolean     NOT NULL,
  error_msg    text
);

用一段可复用的刷新函数记录耗时:

sql
CREATE OR REPLACE FUNCTION refresh_mv_logged(p_mv text, p_concurrently boolean DEFAULT true)
RETURNS void
LANGUAGE plpgsql AS $$
DECLARE
  t0    timestamptz := clock_timestamp();
  t1    timestamptz;
  n     bigint;
  sql   text;
BEGIN
  IF p_concurrently THEN
    sql := format('REFRESH MATERIALIZED VIEW CONCURRENTLY %I', p_mv);
  ELSE
    sql := format('REFRESH MATERIALIZED VIEW %I', p_mv);
  END IF;

  BEGIN
    EXECUTE sql;
    EXECUTE format('SELECT count(*) FROM %I', p_mv) INTO n;
    t1 := clock_timestamp();

    INSERT INTO mv_refresh_log (mv_name, started_at, finished_at, duration_ms, row_count, succeeded)
    VALUES (p_mv, t0, t1, (extract(epoch FROM (t1 - t0)) * 1000)::int, n, true);
  EXCEPTION WHEN OTHERS THEN
    t1 := clock_timestamp();
    INSERT INTO mv_refresh_log (mv_name, started_at, finished_at, duration_ms, row_count, succeeded, error_msg)
    VALUES (p_mv, t0, t1, (extract(epoch FROM (t1 - t0)) * 1000)::int, NULL, false, SQLERRM);
    RAISE;
  END;
END;
$$;
sql
-- 使用它刷新
SELECT refresh_mv_logged('mv_host_health', true);

SELECT mv_name, duration_ms, row_count, succeeded
FROM mv_refresh_log ORDER BY id DESC LIMIT 1;
text
      mv_name      | duration_ms | row_count | succeeded
-------------------+-------------+-----------+-----------
 mv_host_health    |         604 |       200 | t
(1 row)

第 7 步:调度(示例用 pg_cron)

sql
CREATE EXTENSION IF NOT EXISTS pg_cron;

SELECT cron.schedule(
  'refresh-host-health',
  '*/5 * * * *',
  $$SELECT refresh_mv_logged('mv_host_health', true)$$
);

第 8 步:巡检「刷新过旧」的物化视图

sql
-- 每个物化视图最后一次成功刷新的时间与距今时长
SELECT
  m.matviewname,
  max(l.finished_at)                          AS last_refresh,
  now() - max(l.finished_at)                  AS staleness,
  avg(l.duration_ms)::int                     AS avg_ms
FROM pg_matviews m
LEFT JOIN mv_refresh_log l ON l.mv_name = m.matviewname AND l.succeeded
WHERE m.schemaname = 'public'
GROUP BY m.matviewname
ORDER BY last_refresh NULLS FIRST;
text
   matviewname   |         last_refresh         |   staleness   | avg_ms
-----------------+------------------------------+---------------+--------
 mv_host_health  | 2026-09-28 11:00:04.21+08    | 00:02:13.45   |    604
(1 row)

第 9 步:清理

sql
DROP MATERIALIZED VIEW mv_host_health;
DROP FUNCTION refresh_mv_logged(text, boolean);
DROP TABLE host_metrics;
DROP TABLE mv_refresh_log;

注意事项 ​

  • 默认刷新持 ACCESS EXCLUSIVE 锁,连 SELECT 都会被阻塞。 这是最常见的生产事故来源。

    sql
    -- 危险!刷新期间该物化视图的读全部被阻塞
    REFRESH MATERIALIZED VIEW mv_big;
  • REFRESH ... WITH NO DATA 会清空内容,之后查询会报 has not been populated。

  • REFRESH 不能在只读事务中执行。

    sql
    -- 错误!cannot execute REFRESH MATERIALIZED VIEW in a read-only transaction
    BEGIN READ ONLY; REFRESH MATERIALIZED VIEW mv_x; COMMIT;
  • 并发刷新不能在事务块内使用。

    sql
    -- 错误!cannot be executed within a transaction block
    BEGIN; REFRESH MATERIALIZED VIEW CONCURRENTLY mv_x; COMMIT;
  • 没有内置增量刷新。 每次 REFRESH 都是全量重算,pg_ivm 属于第三方扩展。

  • 刷新耗时 ≈ 定义查询耗时。 只优化刷新命令本身是没用的,要优化底层查询与索引。

  • 刷新不会自动 ANALYZE。 统计信息可能陈旧,建议刷新后手动执行。

  • 多级物化视图必须自底向上刷新,否则上层基于旧数据。

  • lock_timeout 是个好习惯,但要接受「超时失败」:宁可在低峰重试,也不要排队堵住整个实例。

  • pg_stat_progress_* 里没有普通刷新的进度视图,别指望有原生进度百分比。

  • 刷新会积累死元组,频繁刷新的大物化视图要关注 autovacuum 是否跟得上。

最佳实践 ​

  • 默认全用并发刷新(前提是建了唯一索引),只在低峰维护窗口用普通刷新:

    sql
    REFRESH MATERIALIZED VIEW CONCURRENTLY mv_x;
  • 刷新前设置 lock_timeout,避免长时间排队:

    sql
    BEGIN;
    SET LOCAL lock_timeout = '5s';
    REFRESH MATERIALIZED VIEW mv_x;
    COMMIT;
  • 刷新后立即 ANALYZE,或用封装函数一并处理:

    sql
    REFRESH MATERIALIZED VIEW mv_x;
    ANALYZE mv_x;
  • 记录每次刷新的耗时与行数到监控表,据此估算窗口与告警:

    sql
    INSERT INTO mv_refresh_log (mv_name, started_at, finished_at, duration_ms, row_count, succeeded) VALUES (...);
  • 按依赖顺序批量刷新,用 pg_depend 推导顺序或维护固定清单。

  • 把刷新安排在业务低峰,尤其是不支持并发刷新的场景。

  • 优先优化定义查询:加时间窗口、减少 JOIN、在基表建索引,刷新自然变快。

  • 对超大结果集拆分物化(按时间或维度分片),让每次刷新只覆盖一片,降低单次锁与耗时。

  • 给频繁刷新的物化视图调低 autovacuum 阈值,防止膨胀:

    sql
    ALTER MATERIALIZED VIEW mv_x SET (autovacuum_vacuum_scale_factor = 0.02);
  • 对刷新加监控告警:刷新失败、耗时突增、新鲜度超阈值都要能通知到人。

下一节

继续学习:并发刷新(CONCURRENTLY)

参考链接 ​