REFRESH MATERIALIZED VIEW
REFRESH MATERIALIZED VIEW 是物化视图的「更新按钮」:它重跑定义查询,把结果替换进已存数据。默认行为是整表重建并持 ACCESS EXCLUSIVE 锁——理解这一点,才能规划好刷新窗口、降低对业务的影响,并在必要时改用并发刷新。本节讲透刷新语义、阻塞影响、刷新策略、进度观察与失败处理。
前置知识
阅读本节前,建议先了解:CREATE MATERIALIZED VIEW、锁与并发
基础概念
刷新的语义
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 的实际用途很少,主要是「临时腾空间」或「标记为待重填」:
-- 清空物化视图内容(等于把它退回未填充状态)
REFRESH MATERIALIZED VIEW mv_x WITH NO DATA;
SELECT ispopulated FROM pg_matviews WHERE matviewname = 'mv_x'; ispopulated
-------------
f
(1 row)刷新策略总览
| 策略 | 触发方式 | 适用 |
|---|---|---|
| 定时刷新 | pg_cron / 系统 crontab | 报表、看板(最常见) |
| 事件触发 | 基表变更后由应用或触发器调用 | 数据变化不频繁但要求较新 |
| 手动刷新 | 运维/开发者按需执行 | 临时排查、一次性汇总 |
| 链式刷新 | 顶层任务按依赖顺序依次刷新 | 多级物化视图 |
语法与用法
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;最简可运行示例:
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;CREATE TABLE
INSERT 0 3
SELECT 2SELECT * FROM mv_host_cpu ORDER BY host; host | avg_cpu | samples
-------+---------+---------
web-1 | 21.25 | 2
web-2 | 55.00 | 1
(2 rows)-- 新增数据
INSERT INTO metrics (host, cpu) VALUES ('web-1', 80.0);
-- 刷新(默认全量)
REFRESH MATERIALIZED VIEW mv_host_cpu;
SELECT * FROM mv_host_cpu ORDER BY host;REFRESH MATERIALIZED VIEW
host | avg_cpu | samples
-------+---------+---------
web-1 | 40.83 | 3
web-2 | 55.00 | 1
(2 rows)详细说明
默认刷新持 ACCESS EXCLUSIVE 锁:会阻塞一切
这是默认刷新最大的风险。ACCESS EXCLUSIVE 是最高级别锁,与包括 SELECT 在内的所有锁模式冲突:
锁冲突矩阵(简化)
ACCESS EXCLUSIVE
SELECT (AccessShare) 冲突 ← 连读都被挡
INSERT/UPDATE/DELETE 冲突
其它 DDL 冲突验证方式:开两个会话,一个 REFRESH 慢查询,另一个 SELECT:
-- 会话 A:刷新一个耗时的物化视图
REFRESH MATERIALIZED VIEW mv_slow;
-- 会话 B:随便读一下(会被阻塞在锁上)
SELECT count(*) FROM mv_slow;-- 会话 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; 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 就是被锁住的典型特征。
降低影响的三个手段:
-- 1) 设锁超时,避免刷新长时间排队(宁可失败,不堵住全局)
SET lock_timeout = '3s';
REFRESH MATERIALIZED VIEW mv_slow;
RESET lock_timeout;
-- 2) 用并发刷新(不阻塞读)
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_slow;
-- 3) 安排在业务低峰刷新期间的执行过程
内部实现上,刷新是「新建临时堆表 → 执行查询 → 替换」(旧内容被丢弃):
REFRESH 内部步骤(示意)
1. 建新的临时存储(relfilenode)
2. 执行定义查询,把结果写入新存储
3. 交换 relfilenode,让物化视图指向新数据
4. 清理旧存储
5. 索引随之重建(普通索引在刷新时整体重建)因此刷新的耗时 ≈ 定义查询的耗时 + 写索引的耗时。定义查询慢,刷新必然慢:
-- 先量一下定义查询本身的耗时
EXPLAIN (ANALYZE, TIMING ON, SUMMARY ON, COSTS OFF)
SELECT host, round(avg(cpu), 2), count(*) FROM metrics GROUP BY host; 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 与结果宽度 |
| 用并发刷新 | 降低锁影响(不减少耗时,反而更慢) |
时间窗口过滤是降刷新成本最有效的手段之一:
-- 只物化最近 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;刷新与事务
两条硬性规则:
REFRESH不能在只读事务中执行。REFRESH不能在事务块里执行CONCURRENTLY(并发刷新不能在事务块内使用)。
-- 错误!只读事务中不能刷新
BEGIN READ ONLY;
REFRESH MATERIALIZED VIEW mv_host_cpu;
COMMIT;ERROR: cannot execute REFRESH MATERIALIZED VIEW in a read-only transaction-- 错误!并发刷新不能在事务块内使用
BEGIN;
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_host_cpu;
COMMIT;ERROR: REFRESH MATERIALIZED VIEW CONCURRENTLY cannot be executed within a transaction block普通刷新可以放在事务里(这样能配合 SET LOCAL lock_timeout):
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 观察:
-- 找出正在执行的刷新
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'; 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 看它等了什么、又被谁等:
-- 刷新持有哪些锁
SELECT l.locktype, l.relation::regclass AS rel, l.mode, l.granted
FROM pg_locks l
WHERE l.pid = 12003
ORDER BY l.granted; locktype | rel | mode | granted
----------+-------------------+---------------------+---------
relation | mv_big | AccessExclusiveLock | t
relation | pg_class | AccessShareLock | t
(2 rows)-- 谁在等这次刷新(被阻塞的会话)
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; pid | query | mode
-------+------------------------------+---------------------
12044 | SELECT count(*) FROM mv_big | AccessShareLock
(1 row)判断「还要多久」
没有原生进度。实用做法是记录历史刷新耗时(见下文监控),用历史均值估算本次剩余时间;必要时对特别大的物化视图拆分成多个小的分片物化视图并行刷新。
批量刷新脚本与依赖顺序
多级物化视图必须按依赖顺序刷新(自底向上):
#!/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 生成刷新清单:
-- 依据 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; relname | depth
------------------+-------
mv_user_spend | 0
mv_top_spenders | 1
mv_daily_report | 1
(3 rows)定时刷新:pg_cron 与系统 crontab
pg_cron 是第三方扩展(不属于 PostgreSQL 内核),能在数据库内定义定时任务:
-- 需要预加载并安装扩展
-- 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$$
);CREATE EXTENSION
schedule
----------------
1
(1 row)-- 查看任务与执行历史
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; jobid | schedule | command | active
-------+--------------+-----------------------------------------------------+--------
1 | */10 * * * * | REFRESH MATERIALIZED VIEW CONCURRENTLY mv_host_cpu | t
(1 row)-- 删除任务
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 | 扩容 / 清理 |
默认刷新的一个重要特性是「失败即保持原内容」(因为整体是原子的):失败不会留下半成品。
-- 验证:刷新失败后内容不变
SELECT count(*) FROM mv_host_cpu; -- 记下这个值
-- 制造失败(示意:超时)
SET lock_timeout = '1ms';
REFRESH MATERIALIZED VIEW mv_host_cpu;
RESET lock_timeout;ERROR: canceling statement due to lock timeout-- 内容仍然是旧的,没有被破坏
SELECT count(*) FROM mv_host_cpu; count
-------
2
(1 row)并发刷新则没有这个保证——它可能失败在中间状态,此时物化视图内容可能是「部分旧 + 部分新」甚至违反唯一约束(见并发刷新)。
刷新与 autovacuum 的交互
刷新会整体重写物化视图的内容,产生大量死元组。autovacuum 会像处理普通表一样处理它:
-- 观察物化视图的膨胀与清理情况
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'; 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 | 处理膨胀与索引膨胀 |
-- 频繁刷新的物化视图:让 autovacuum 更积极
ALTER MATERIALIZED VIEW mv_host_cpu SET (
autovacuum_vacuum_scale_factor = 0.02,
autovacuum_analyze_scale_factor = 0.02
);实战示例
场景:为「主机监控聚合」建立物化视图,实现定时刷新,并对比普通刷新与并发刷新的阻塞差异,最后建立刷新监控。
第 1 步:造一张有写入压力的表
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;DROP TABLE
CREATE TABLE
INSERT 0 1000000
CREATE INDEX
CREATE INDEX
ANALYZE第 2 步:创建物化视图(含唯一索引,为并发刷新做准备)
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;SELECT 200
CREATE INDEX
ANALYZE第 3 步:测量默认刷新耗时
\timing on
REFRESH MATERIALIZED VIEW mv_host_health;REFRESH MATERIALIZED VIEW
Time: 428.517 ms第 4 步:测量并发刷新耗时(对比)
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_host_health;REFRESH MATERIALIZED VIEW
Time: 612.884 ms并发刷新更慢(要对比新旧内容做差异更新),但它不阻塞读。这是核心取舍:
| 刷新方式 | 耗时 | 阻塞读 |
|---|---|---|
| 默认刷新 | 428 ms | 阻塞(ACCESS EXCLUSIVE) |
| 并发刷新 | 613 ms | 不阻塞 |
第 5 步:用会话模拟「刷新期间的读阻塞」
-- 会话 A:普通刷新(模拟)
-- 另开一个终端执行,这里用 psql 的 \! 或直接分两次调用说明
-- 会话 B(在刷新进行期间):
SELECT count(*) FROM mv_host_health;
-- 结果:被阻塞,直到刷新结束才返回在实际操作中可用如下方式观察:
-- 会话 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(); 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 步:建立刷新监控表
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
);用一段可复用的刷新函数记录耗时:
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;
$$;-- 使用它刷新
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; mv_name | duration_ms | row_count | succeeded
-------------------+-------------+-----------+-----------
mv_host_health | 604 | 200 | t
(1 row)第 7 步:调度(示例用 pg_cron)
CREATE EXTENSION IF NOT EXISTS pg_cron;
SELECT cron.schedule(
'refresh-host-health',
'*/5 * * * *',
$$SELECT refresh_mv_logged('mv_host_health', true)$$
);第 8 步:巡检「刷新过旧」的物化视图
-- 每个物化视图最后一次成功刷新的时间与距今时长
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; 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 步:清理
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 是否跟得上。
最佳实践
默认全用并发刷新(前提是建了唯一索引),只在低峰维护窗口用普通刷新:
sqlREFRESH MATERIALIZED VIEW CONCURRENTLY mv_x;刷新前设置
lock_timeout,避免长时间排队:sqlBEGIN; SET LOCAL lock_timeout = '5s'; REFRESH MATERIALIZED VIEW mv_x; COMMIT;刷新后立即
ANALYZE,或用封装函数一并处理:sqlREFRESH MATERIALIZED VIEW mv_x; ANALYZE mv_x;记录每次刷新的耗时与行数到监控表,据此估算窗口与告警:
sqlINSERT INTO mv_refresh_log (mv_name, started_at, finished_at, duration_ms, row_count, succeeded) VALUES (...);按依赖顺序批量刷新,用
pg_depend推导顺序或维护固定清单。把刷新安排在业务低峰,尤其是不支持并发刷新的场景。
优先优化定义查询:加时间窗口、减少 JOIN、在基表建索引,刷新自然变快。
对超大结果集拆分物化(按时间或维度分片),让每次刷新只覆盖一片,降低单次锁与耗时。
给频繁刷新的物化视图调低 autovacuum 阈值,防止膨胀:
sqlALTER MATERIALIZED VIEW mv_x SET (autovacuum_vacuum_scale_factor = 0.02);对刷新加监控告警:刷新失败、耗时突增、新鲜度超阈值都要能通知到人。
下一节
继续学习:并发刷新(CONCURRENTLY)