Skip to content

可见性映射(VM) ​

在 MVCC 中,一行的可见性要根据快照逐行判断。如果一个页里所有行对所有事务都可见,那么逐行判断就是纯粹的浪费。可见性映射(VM)就是为避免这种浪费而存在的:它用两个比特位,把"整页的可见性结论"缓存下来,从而让 Index Only Scan 和 VACUUM 都能跳过大量无用工作。

前置知识

阅读本节前,建议先了解:空闲空间映射(FSM)

基础概念 ​

可见性映射(VM, Visibility Map) 是每张表旁边的一个辅助文件 _vm,为每一个数据页记录两个标志位:

标志位英文含义
全可见位All-Visible该页中的所有元组对所有事务都可见(页内无死元组、无未提交版本)
全冻结位All-Frozen该页中的所有元组都已被冻结(XID 已替换为 FrozenTransactionId),不再需要为防回卷而清理

一个字节可以记录 4 个页的标志位(每页 2 位)。所以 VM 文件极小:一张 1TB 的表,VM 大约只有 4MB。

术语英文说明
可见性映射VM, Visibility Map_vm 文件,记录页级可见性标志
全可见All-Visible页内所有元组对所有事务可见,可安全跳过可见性检查
全冻结All-Frozen页内所有元组的 XID 已冻结,VACUUM 可整页跳过
死元组Dead Tuple对任何活跃事务都不可见的旧版本元组
冻结Freeze把元组的 xmin 标记为永久有效,避免事务 ID 回卷
仅索引扫描Index-Only Scan只读索引即可回答查询,无需回表读堆页
回表Heap Fetch从索引拿到 ctid 后再去数据页读整个元组

VM 的两个标志位解决两个不同问题,不要混淆:

text
All-Visible 位 —— 为「读」服务
  · Index-Only Scan 看到该页全可见 → 不需要回表读堆页
  · VACUUM 看到该页全可见 → 可跳过,无需检查死元组
  对应收益:查询变快(少读文件)

All-Frozen 位 —— 为「防回卷」服务
  · VACUUM 看到该页全冻结 → 整页跳过,绝不重新检查 XID
  · 是 aggressive VACUUM(防事务 ID 回卷)的主要加速手段
  对应收益:VACUUM 变快,回卷风险降低

核心机制 ​

VM 的二进制布局 ​

text
VM 文件结构(每个字节记录 4 个数据页,每页 2 个比特)

 字节偏移 0 ─────────────────────────────────────────────────────
 ┌────────┬────────┬────────┬────────┐
 │ 页 0   │ 页 1   │ 页 2   │ 页 3   │
 │ bit0-1 │ bit2-3 │ bit4-5 │ bit6-7 │
 └────────┴────────┴────────┴────────┘
    │
    └─► 每页的 2 位含义
        bit 0 : ALL_VISIBLE  (1 = 全可见)
        bit 1 : ALL_FROZEN   (1 = 全冻结)
        其余 6 位保留

 字节偏移 1 ─────────────────────────────────────────────────────
 ┌────────┬────────┬────────┬────────┐
 │ 页 4   │ 页 5   │ 页 6   │ 页 7   │
 └────────┴────────┴────────┴────────┘

由此可得:
  · VM 文件大小 ≈ ceil(数据页数 / 4) 字节
  · 8192 字节的一个 VM 页可覆盖 32768 个数据页(约 256MB 数据)
  · 缺失的 VM 字节一律按「不全可见、不全冻结」处理(保守但正确)

VACUUM 如何维护 VM ​

VM 的唯一维护者就是 VACUUM。它不能在 INSERT/UPDATE/DELETE 时维护,因为写入会让页立刻"不再全可见"。

text
VACUUM 扫描每一页
        │
        ├─ 发现死元组 → 清理 → 该页不可能全可见 → 清除 All-Visible 位
        │
        ├─ 扫描完成,该页无死元组,且所有元组的 xmin 都对所有事务可见
        │       → 设置 All-Visible 位
        │
        ├─ 进一步:该页所有元组都足够老(可安全冻结)
        │       → 设置 All-Frozen 位(暗示也设置 All-Visible 位)
        │
        └─ 该页无法完全清理(有长事务挡住)→ 不设置任何位

写入操作(INSERT/UPDATE/DELETE)对 VM 的影响:
        → 立即清除该页的两个位(页已"脏",不再是全可见)

关键点:任何写入都会立刻清掉 VM 位,所以 VM 的覆盖情况直接反映"这张表有多久没被稳定读取过了"。高频写入的表 VM 覆盖率通常很低。

两个位的设置条件 ​

位设置条件设置者是否要求"无死元组"
All-Visible页内所有元组对所有未结束事务及未来事务都可见VACUUM是
All-Frozen在上条基础上,所有元组的 XID 都老到可以安全冻结VACUUM是(且额外要求 XID 足够老)

注意 All-Frozen 蕴含 All-Visible,反之不成立。

sql
-- 用 pg_visibility 扩展直接观察 VM
CREATE EXTENSION IF NOT EXISTS pg_visibility;

-- 建立演示表
DROP TABLE IF EXISTS vm_demo;
CREATE TABLE vm_demo (
  id   BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  note TEXT NOT NULL
);

INSERT INTO vm_demo (note)
SELECT '备注-' || g FROM generate_series(1, 5000);

-- 插入后立即观察 VM:此时还没 VACUUM,应该几乎全为 false
SELECT
  count(*) FILTER (WHERE all_visible) AS 全可见页数,
  count(*) FILTER (WHERE all_frozen)  AS 全冻结页数,
  count(*)                            AS 总页数
FROM pg_visibility_map('vm_demo');
text
 全可见页数 | 全冻结页数 | 总页数
------------+------------+--------
          0 |          0 |     18
(1 row)

刚插入的数据,页都不是全可见的 —— 因为 VACUUM 还没跑过。

sql
-- 执行 VACUUM 后再看
VACUUM vm_demo;

SELECT
  count(*) FILTER (WHERE all_visible) AS 全可见页数,
  count(*) FILTER (WHERE all_frozen)  AS 全冻结页数,
  count(*)                            AS 总页数,
  round(100.0 * count(*) FILTER (WHERE all_visible) / count(*), 1) AS 全可见覆盖率百分比
FROM pg_visibility_map('vm_demo');
text
 全可见页数 | 全冻结页数 | 总页数 | 全可见覆盖率百分比
------------+------------+--------+--------------------
         18 |          0 |     18 |              100.0
(1 row)

VACUUM 之后全部页都是全可见了,但全冻结仍为 0 —— 因为刚插入的元组 xmin 还很新,未达到冻结条件。

sql
-- 强制冻结(仅演示,生产慎用)
VACUUM (FREEZE) vm_demo;

SELECT
  count(*) FILTER (WHERE all_visible) AS 全可见页数,
  count(*) FILTER (WHERE all_frozen)  AS 全冻结页数,
  count(*)                            AS 总页数
FROM pg_visibility_map('vm_demo');
text
 全可见页数 | 全冻结页数 | 总页数
------------+------------+--------
         18 |         18 |     18
(1 row)

相关参数 ​

参数默认值作用域说明
vacuum_freeze_min_age50000000实例/表级元组 xmin 至少"老"多少事务才可冻结
vacuum_freeze_table_age150000000实例/表级表有多"老"就触发一次全表扫描式 VACUUM(更新 VM)
vacuum_freeze_max_age200000000实例级超过即强制 aggressive VACUUM,防止事务 ID 回卷
enable_indexonlyscanon会话是否允许优化器选择 Index-Only Scan
autovacuum_freeze_max_age200000000实例级autovacuum 触发抗回卷清理的阈值
vacuum_truncateon表级VACUUM 是否截断末尾全空页(影响 VM 与文件大小)
sql
-- 查看冻结与仅索引扫描相关的参数
SELECT name, setting, unit, short_desc
FROM pg_settings
WHERE name IN (
  'vacuum_freeze_min_age',
  'vacuum_freeze_table_age',
  'vacuum_freeze_max_age',
  'enable_indexonlyscan'
)
ORDER BY name;
text
          name           |  setting  | unit |                        short_desc
-------------------------+-----------+------+-----------------------------------------------------------
 enable_indexonlyscan    | on        |      | Enables the planner's use of index-only-scan plans.
 vacuum_freeze_max_age   | 200000000 |      | Age at which to autovacuum a table to prevent transaction ID
                         |           |      | wraparound.
 vacuum_freeze_min_age   | 50000000  |      | Minimum age at which VACUUM should freeze a table row.
 vacuum_freeze_table_age | 150000000 |      | Age at which VACUUM should scan whole table to freeze tuples.
(4 rows)

vacuum_freeze_table_age 与 VM 的关系

这个参数的含义是:"当表的事务年龄(age(relfrozenxid))超过此值时,VACUUM 不再只扫描'可能脏'的页,而是扫描全表并重建 VM"。

这解释了为什么 VM 覆盖率会周期性跳回 100%:一旦表变"老",VACUUM 就会做一次全表扫描,把所有页重新标记为全可见/全冻结。平时 VACUUM 为了省时间只扫描"可能脏"的页(依赖 VM 和 FSM 判断),所以覆盖率会逐渐下降。

详细说明 ​

Index-Only Scan 如何依赖 VM ​

这是 VM 最有价值的应用场景。普通索引扫描需要"两步走",而 Index-Only Scan 只有"一步"——前提是 VM 说这些页全可见:

text
普通索引扫描(Index Scan)
  1. 遍历索引,拿到 ctid(页号 + 行号)
  2. 对每个 ctid:读数据页 → 检查可见性 → 返回需要的列
  ⇒ 索引命中的每一行都要付一次随机堆页 I/O

仅索引扫描(Index-Only Scan)
  1. 遍历索引,索引里已包含查询所需全部列
  2. 检查该 ctid 所在页的 VM 位
       ├─ All-Visible = 1 → 直接返回索引中的值,绝不读堆页 ✔
       └─ All-Visible = 0 → 必须回表,检查可见性(退化为普通扫描)
  ⇒ VM 覆盖率高时,堆页 I/O 几乎降为 0

VACUUM 未跟上的代价:
  ⇒ 每次 Index-Only Scan 都退化成 Index Scan
  ⇒ 查询计划显示 "Heap Fetches" 数值巨大
sql
-- 制造数据并为索引列建立覆盖索引
DROP TABLE IF EXISTS idx_demo;
CREATE TABLE idx_demo (
  id        BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  user_id   BIGINT NOT NULL,
  amount    NUMERIC(12, 2) NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

INSERT INTO idx_demo (user_id, amount, created_at)
SELECT
  (random() * 10000)::bigint,
  (random() * 1000)::numeric(12, 2),
  now() - (random() * interval '365 days')
FROM generate_series(1, 500000);

-- 覆盖索引:包含查询需要的全部列
CREATE INDEX idx_demo_user_amount ON idx_demo (user_id) INCLUDE (amount);

-- 让统计信息与 VM 都跟上
VACUUM ANALYZE idx_demo;

-- 观察 Index-Only Scan 的效果
EXPLAIN (ANALYZE, BUFFERS)
SELECT user_id, amount
FROM idx_demo
WHERE user_id = 1234;
text
                                    QUERY PLAN
----------------------------------------------------------------------------------
 Index Only Scan using idx_demo_user_amount on idx_demo  (cost=0.42..24.15 rows=48 width=12)
                                                          (actual time=0.028..0.075 rows=49 loops=1)
   Index Cond: (user_id = 1234)
   Heap Fetches: 0
   Buffers: shared hit=4
 Planning Time: 0.128 ms
 Execution Time: 0.098 ms
(6 rows)

Heap Fetches: 0 是关键 —— 一次堆页都没有读,索引就回答了整个查询。这正是 VM 全可见带来的收益。

sql
-- 现在做一批更新,破坏 VM 覆盖
UPDATE idx_demo SET amount = amount + 1 WHERE user_id < 5000;

-- 不 VACUUM,直接看计划的变化
EXPLAIN (ANALYZE, BUFFERS)
SELECT user_id, amount
FROM idx_demo
WHERE user_id = 1234;
text
                                    QUERY PLAN
----------------------------------------------------------------------------------
 Index Only Scan using idx_demo_user_amount on idx_demo  (cost=0.42..26.30 rows=48 width=12)
                                                          (actual time=0.031..0.412 rows=49 loops=1)
   Index Cond: (user_id = 1234)
   Heap Fetches: 49
   Buffers: shared hit=12
 Planning Time: 0.121 ms
 Execution Time: 0.436 ms

Heap Fetches 从 0 变成 49(正好等于返回行数)—— Index-Only Scan 已经完全退化成普通索引扫描。执行时间也涨了 4 倍多。修好它只需一次 VACUUM:

sql
VACUUM idx_demo;

EXPLAIN (ANALYZE, BUFFERS)
SELECT user_id, amount
FROM idx_demo
WHERE user_id = 1234;
text
                                    QUERY PLAN
----------------------------------------------------------------------------------
 Index Only Scan using idx_demo_user_amount on idx_demo  (cost=0.42..24.15 rows=48 width=12)
                                                          (actual time=0.026..0.070 rows=49 loops=1)
   Index Cond: (user_id = 1234)
   Heap Fetches: 0
   Buffers: shared hit=4
 Planning Time: 0.114 ms
 Execution Time: 0.092 ms
(6 rows)

Heap Fetches 回到 0。这就是"VACUUM 跟得上"对读性能的直接影响,也是运维中"查询忽然变慢"最常见的隐藏原因之一。

用 pg_visibility 深入观察 ​

sql
-- 查看整张表的 VM 概况(区分三个函数)
SELECT
  'pg_visibility_map' AS 函数,
  count(*) FILTER (WHERE all_visible) AS 全可见页数,
  count(*) FILTER (WHERE all_frozen)  AS 全冻结页数,
  count(*)                            AS 总页数
FROM pg_visibility_map('idx_demo')

UNION ALL

SELECT
  'all_visible / all_frozen 汇总',
  count(*) FILTER (WHERE all_visible),
  count(*) FILTER (WHERE all_frozen),
  count(*)
FROM pg_visibility_map_summary('idx_demo');
text
             函数              | 全可见页数 | 全冻结页数 | 总页数
-------------------------------+------------+------------+--------
 pg_visibility_map             |      10297 |          0 |  10297
 all_visible / all_frozen 汇总 |      10297 |          0 |  10297
(2 rows)
sql
-- 查看单页的 VM 与可见性明细
SELECT
  blkno                  AS 页号,
  all_visible            AS 全可见,
  all_frozen             AS 全冻结,
  pd_lower, pd_upper,
  pd_upper - pd_lower    AS 页内空闲字节
FROM pg_visibility('idx_demo', 0) v
CROSS JOIN LATERAL page_header(get_raw_page('idx_demo', 0))
LIMIT 1;
text
 页号 | 全可见 | 全冻结 | pd_lower | pd_upper | 页内空闲字节
------+--------+--------+----------+----------+--------------
    0 | t      | f      |      156 |     6992 |         6836
(1 row)
sql
-- 找出「应该有数据但没有全可见标记」的页(即需要 VACUUM 的页)
SELECT
  count(*) AS 非全可见页数
FROM pg_visibility_map('idx_demo')
WHERE NOT all_visible;
text
 非全可见页数
--------------
            0
(1 row)

观察写入立刻清除 VM 位 ​

sql
-- 记录更新前的 VM 状态
SELECT count(*) FILTER (WHERE all_visible) AS 更新前全可见页数
FROM pg_visibility_map('vm_demo');

-- 只更新一行
UPDATE vm_demo SET note = '被改过' WHERE id = 1;

-- 再观察:注意具体是哪些页失去了标记
SELECT
  blkno AS 页号,
  all_visible AS 全可见
FROM pg_visibility_map('vm_demo')
WHERE NOT all_visible
ORDER BY blkno;
text
 更新前全可见页数
------------------
               18
(1 row)

 页号 | 全可见
------+--------
    0 | f
(1 row)

只更新了一行,就清掉了整页的 All-Visible 位。这是 VM 的粗粒度本质:它只有"整页"这一档,没有"部分可见"的中间状态。一行变了,整页就得当作"不确定"来对待。

这个特性直接推导出一条重要实践:把只读历史数据与高频写入数据分开存放(分区表、冷热分离),这样只读分区能保持长时间全可见,Index-Only Scan 才能稳定生效。

实战示例 ​

场景:诊断 "Index-Only Scan 为什么不生效" ​

某个报表查询本来是毫秒级,突然涨到几百毫秒。计划中仍然显示 Index Only Scan,但 Heap Fetches 高达 20 万。

sql
-- ---------- 第 1 步:确认计划与 Heap Fetches ----------
EXPLAIN (ANALYZE, BUFFERS)
SELECT user_id, sum(amount) AS 总金额, count(*) AS 单数
FROM idx_demo
WHERE created_at >= now() - interval '30 days'
GROUP BY user_id
HAVING count(*) > 1
ORDER BY 总金额 DESC
LIMIT 20;
text
                                            QUERY PLAN
----------------------------------------------------------------------------------------------------
 Limit  (cost=58234.11..58234.16 rows=20 width=24) (actual time=412.338..412.341 rows=20 loops=1)
   ->  Sort  (cost=58234.11..58236.61 rows=1000 width=24) (actual time=412.330..412.334 rows=20 loops=1)
         Sort Key: (sum(amount)) DESC
         Sort Method: top-N heapsort  Memory: 26kB
         ->  GroupAggregate  (cost=0.42..58184.11 rows=1000 width=24) (actual time=0.075..411.892 rows=4821 loops=1)
               Group Key: user_id
               Filter: (count(*) > 1)
               Rows Removed by Filter: 5183
               ->  Index Only Scan using idx_demo_user_amount on idx_demo  (cost=0.42..54198.20 rows=10000 width=20)
                                                                            (actual time=0.062..376.512 rows=200000 loops=1)
                     Filter: (created_at >= (now() - '30 days'::interval))
                     Rows Removed by Filter: 300000
                     Heap Fetches: 203417       ← 问题在这里
                     Buffers: shared hit=41203 read=1208
 Planning Time: 0.372 ms
 Execution Time: 412.684 ms
sql
-- ---------- 第 2 步:检查 VM 覆盖率 ----------
SELECT
  c.relname                                    AS 表名,
  count(*) FILTER (WHERE v.all_visible)        AS 全可见页数,
  count(*)                                     AS 总页数,
  round(100.0 * count(*) FILTER (WHERE v.all_visible) / count(*), 1) AS 全可见覆盖率百分比
FROM pg_visibility_map('idx_demo') v
CROSS JOIN pg_class c
WHERE c.oid = 'idx_demo'::regclass
GROUP BY c.relname;
text
 表名     | 全可见页数 | 总页数 | 全可见覆盖率百分比
----------+------------+--------+--------------------
 idx_demo |       4200 |  10297 |               40.8
(1 row)

覆盖率只有 40.8% —— 有一半多的页在 VM 中没有标记,因此索引命中的每一行都得回表检查可见性。

sql
-- ---------- 第 3 步:查看是什么在持续破坏 VM ----------
SELECT
  relname             AS 表名,
  n_tup_ins           AS 插入数,
  n_tup_upd           AS 更新数,
  n_tup_del           AS 删除数,
  n_tup_hot_upd       AS HOT更新数,
  n_dead_tup          AS 死元组数,
  n_mod_since_analyze AS 上次分析后修改数,
  last_vacuum,
  last_autovacuum
FROM pg_stat_user_tables
WHERE relname = 'idx_demo';
text
 表名     | 插入数 | 更新数 | 删除数 | HOT更新数 | 死元组数 | 上次分析后修改数 | last_vacuum |   last_autovacuum
----------+--------+--------+--------+-----------+----------+------------------+-------------+---------------------
 idx_demo | 600000 | 300000 |      0 |    300000 |   300000 |           300000 |             | 2026-09-22 06:00:12
(1 row)

诊断清晰:表有 30 万次更新、30 万死元组,但 last_autovacuum 是几小时前 —— autovacuum 的节奏(默认 scale_factor = 0.2)跟不上写入速度,VM 覆盖率因此长期偏低。

sql
-- ---------- 第 4 步:调优并验证 ----------
-- 让 autovacuum 对这个表更积极
ALTER TABLE idx_demo SET (
  autovacuum_vacuum_scale_factor = 0.02,
  autovacuum_vacuum_threshold    = 1000,
  autovacuum_analyze_scale_factor = 0.01
);

-- 手工先整理一次,立即恢复 VM
VACUUM (ANALYZE, VERBOSE) idx_demo;
text
INFO:  vacuuming "public.idx_demo"
INFO:  "idx_demo": found 300000 removable, 300000 nonremovable row versions in 10297 pages
DETAIL:  0 dead row versions cannot be removed yet.
INFO:  "idx_demo": removed 300000 dead row versions in 3612 pages
INFO:  "idx_demo": index scan bypassed: 6685 pages from table (64.92% of total) have 300000 dead item identifiers
INFO:  "idx_demo": index "idx_demo_user_amount" now contains 600000 row versions in 3450 pages
INFO:  "idx_demo": removed 300000 index row versions in 3450 pages
INFO:  analyzing "public.idx_demo"
sql
-- ---------- 第 5 步:确认 VM 恢复,Heap Fetches 归零 ----------
SELECT
  count(*) FILTER (WHERE all_visible) AS 全可见页数,
  count(*)                            AS 总页数,
  round(100.0 * count(*) FILTER (WHERE all_visible) / count(*), 1) AS 全可见覆盖率百分比
FROM pg_visibility_map('idx_demo');

EXPLAIN (ANALYZE, BUFFERS)
SELECT count(*) FROM idx_demo WHERE user_id = 1234;
text
 全可见页数 | 总页数 | 全可见覆盖率百分比
------------+--------+--------------------
      10297 |  10297 |              100.0
(1 row)

                                          QUERY PLAN
---------------------------------------------------------------------------------------------
 Aggregate  (cost=24.23..24.24 rows=1 width=8) (actual time=0.048..0.049 rows=1 loops=1)
   ->  Index Only Scan using idx_demo_user_amount on idx_demo  (cost=0.42..24.15 rows=48 width=0)
                                                               (actual time=0.026..0.040 rows=49 loops=1)
         Index Cond: (user_id = 1234)
         Heap Fetches: 0                    ← 恢复正常
         Buffers: shared hit=4
 Planning Time: 0.110 ms
 Execution Time: 0.075 ms
(7 rows)

注意事项 ​

  • VM 只由 VACUUM 维护,ANALYZE 不更新 VM,INSERT/UPDATE/DELETE 只会清除 VM 位。
  • VM 只有"整页"粒度,改一行会让整页失去全可见标记。这是它的粗粒度本质,不是 bug。
  • Heap Fetches 是判断 VM 是否有效的唯一可靠指标,不要只看计划里有没有 Index Only Scan 字样。
  • _vm 文件不存在或为 0 字节不是错误,只意味着从未 VACUUM 过,Index-Only Scan 会全部退化为回表。
  • 不要手工创建、修改或删除 _vm 文件。
bash
# 不推荐:手工干预 VM 文件
# rm -f $PGDATA/base/16384/16385_vm   # 不推荐:VM 由 VACUUM 负责,手工操作会破坏一致性
  • VACUUM (FREEZE) 不宜在生产随意全表执行,它会强制冻结并可能引发大量 I/O;应让 autovacuum 按 vacuum_freeze_* 参数自然推进。
sql
-- 不推荐:对大表手工全量冻结
-- VACUUM (FREEZE) huge_table;   -- 不推荐:会长时间高强度 I/O,应交给 autovacuum
  • 高频更新的表 VM 覆盖率必然低,这是物理规律。想提升只能靠减少更新或做冷热分离。
  • 分区是提升 VM 覆盖率最有效的手段:只读的历史分区能长期保持 100% 全可见。
  • VACUUM FULL 会重建表与索引,同时重建 VM(新表所有页都会重算),但代价是排他锁与全表重写。

最佳实践 ​

  • 把"VACUUM 是否跟得上"作为一级监控指标,对高频写表单独设置更激进的 autovacuum 参数。
  • 用覆盖索引(INCLUDE)配合 VM 追求 Index-Only Scan,把查询所需列放进索引,才能真正避免回表。
  • 定期用 pg_visibility_map_summary 检查关键表的全可见覆盖率,低于 80% 就该排查 autovacuum 节奏。
  • 关键查询上线前先看 Heap Fetches,它是"Index-Only Scan 是否真生效"的唯一证据。
  • 冷热数据用分区隔离,只读分区让 VM 长期全可见,报表查询收益显著。
  • 不要指望 VM 解决一切读放大,它只对"全列都在索引里"的查询有效。
  • 批量更新后立即 VACUUM,这是让 VM 快速恢复的标准动作。
  • 避免无意义的全表 UPDATE(如"刷新所有行的更新时间戳"),它不是只破坏 VM,还会制造大量死元组与 WAL。
  • 在 SSD/NVMe 上把 autovacuum 调得更激进(缩短 naptime、提高 cost_limit),VM 恢复更快。
  • 理解 All-Visible 与 All-Frozen 的差异,All-Frozen 主要服务抗回卷,别指望它对查询有额外收益。

下一节

继续学习:WAL 文件组织

参考链接 ​