PostgreSQL 的可见性映射(Visibility Map,简称 VM)是一份与堆表对应的辅助文件,用来记录每个数据页的两个关键状态:页内所有元组是否对所有事务可见,以及页内元组是否已经全部冻结。VACUUM 在执行清理时会读取 VM 位图,跳过那些标记为全可见或全冻结的页面,从而大幅减少扫描成本。但如果想直接查看某个表哪些数据页被打上了这些标记,单看表结构或统计视图是不够的,pg_visibility 扩展正好提供了这一能力。它把底层 VM 文件的内容以关系表的形式暴露出来,让 DBA 可以按数据块编号逐一查看标记状态。

一、VM位图与pg_visibility扩展的关系
VM 位图在物理存储上通常是一个以 _vm 结尾的文件,例如表对应的数据文件 OID 为 16385,那么 VM 文件就是 16385_vm。每个数据页只需要 2 个比特位:一位表示 all-visible,另一位表示 all-frozen。因为一个字节有 8 位,所以每 4 个数据页的信息会合并存储在一个字节里。对于大型表,VM 文件非常小,通常只有几十 KB 到几 MB,却能指导 VACUUM 跳过绝大多数页面。
pg_visibility 是 PostgreSQL 自带的 contrib 扩展,它的实现并不复杂:直接读取 VM 文件,把每一个数据页对应的 2 位解码成布尔值,再封装成函数返回。用户不需要知道 VM 文件的二进制布局,也不需要进入数据目录翻看文件,只要安装扩展并执行 SQL 查询即可。这个扩展提供的核心函数是 pg_visibility_map,它返回每条记录包含块号 blkno、全可见标志 all_visible 和全冻结标志 all_frozen。依赖这些信息,我们可以快速判断一个表里哪些页面已经被 VACUUM 处理得足够干净,哪些页面还需要进一步清理。
二、安装扩展并执行基础查询
在大多数 Linux 发行版的 PostgreSQL 安装中,contrib 模块已经包含在软件包里。安装步骤非常简单,以超级用户身份连接到目标数据库后执行一条 SQL 即可:
CREATE EXTENSION pg_visibility;
创建扩展后,先找一个测试表,或者直接对业务表执行查询。例如要查看 orders 表前 20 个数据块的 VM 状态,可以执行:
SELECT blkno, all_visible, all_frozen
FROM pg_visibility_map('orders')
ORDER BY blkno
LIMIT 20;
输出结果中,all_visible 为 t 表示该页内所有元组对所有事务可见,VACUUM 和 index-only scan 可以安全使用该页;all_frozen 为 t 表示该页内元组已全部冻结,连后续的冻结操作也可以跳过。很多页面会同时为 t,这是最理想的状态。如果发现某些页面两个字段都是 f,说明这些页面还包含不可见元组或未冻结元组,需要等待 VACUUM 处理。
除了逐块查看,pg_visibility 还提供了 pg_visibility_map_summary 函数,能够直接汇总整个表的 VM 标记情况:
SELECT * FROM pg_visibility_map_summary('orders');
该函数返回所有数据页总数、全可见页数、全冻结页数以及未被标记的页数。通过这些统计值,不用逐行检查就能看出表的清理进度。例如一个 10 万页的表,如果全可见页数有 9 万页,说明大部分数据都很稳定;如果只有几千页,则说明这个表可能刚刚经历过大量更新,或者 autovacuum 尚未跟上写入速度。
三、解读VM位图输出与VACUUM行为
VACUUM 在扫描堆表时会先加载 VM 位图,遇到 all-visible 为 true 的页面时,它会判断该页是否可以整体跳过。对于常规 VACUUM 来说,跳过全可见页面能显著减少共享缓冲区和磁盘读取;对于 VACUUM FREEZE 或带有冻结参数的 VACUUM,还会检查 all-frozen 标志,直接跳过已经全部冻结的页面。因此,VM 位图的新鲜程度直接影响 VACUUM 的效率。如果 VM 标记过期,VACUUM 就会重复扫描本可以跳过的页面,浪费 I/O。
我们可以通过对比 pg_visibility_map_summary 的输出与 pg_class 中的 relallvisible 字段来判断 VM 是否有异常。relallvisible 是优化器维护的统计值,表示表中全可见页面的数量,它通常与 VM 中的 all_visible 页数一致。如果两者差距很大,可能意味着 VM 文件损坏或统计信息没有及时更新。此时可以执行一次 ANALYZE 后再观察,或者干脆对表执行一次手动 VACUUM。
此外,pg_visibility 还提供了 pg_visibility 函数(注意与扩展同名),用来查看单个数据块的详细可见性信息,包括页面头部记录的 all-visible 和 all-frozen 标志,方便与 VM 文件中的值做对比。示例:
SELECT * FROM pg_visibility('orders', 10);
如果函数返回的 VM 标志和页面头部标志不一致,说明 VM 文件确实存在问题,需要重建。页面头部标志由 VACUUM 写入,VM 文件由同一过程维护,正常情况下两者应当同步。异常不一致通常出现在崩溃恢复不完整、存储层故障或手工不当操作之后。
四、使用pg_visibility定位问题与优化建议
当怀疑某些页面被 VACUUM 错误跳过时,可以使用 pg_check_visible 和 pg_check_frozen 两个函数进行检查。pg_check_visible 会扫描整表,找出 VM 标记为全可见但实际仍存在不可见元组的页面,返回这些元组的 TID;pg_check_frozen 则专门检查冻结标记与元组实际 xmin 的一致性。例如:
SELECT * FROM pg_check_visible('orders');
SELECT * FROM pg_check_frozen('orders');
这两个查询可能比较耗时,因为它们需要读取并检查实际元组,但能够在 VACUUM 之前发现数据一致性风险。只要返回行数不为零,就代表 VM 位图存在误标,必须通过 VACUUM 或重建 VM 来修复。
修复 VM 位图有两种思路。第一种是让 VACUUM 忽略 VM 强制全表扫描,更新所有标记,命令为 VACUUM (DISABLE_PAGE_SKIPPING) orders。这个选项会让 VACUUM 逐页检查,不使用 VM 跳过任何页面,修复完成后 VM 会被重新写正确。第二种是先清空 VM 文件,再执行普通 VACUUM:pg_truncate_visibility_map('orders') 会删除该表的 VM 文件,之后第一次 VACUUM 会认为所有页面都不可跳过,进而重建全部标记。两种方式都会带来额外的 I/O 开销,区别在于 DISABLE_PAGE_SKIPPING 保留现有 VM 并更新,truncate 则完全丢弃。
从日常运维角度看,定期查看核心表的 VM 汇总数据,有助于评估 autovacuum 参数是否合理。如果发现大量页面长期未标记为全冻结,可以适当调低 autovacuum_freeze_max_age,让冻结操作更频繁;如果 all-visible 页面比例持续偏低,说明表更新频繁,可以考虑调整 autovacuum_vacuum_threshold 和 autovacuum_vacuum_scale_factor,让 VACUUM 更及时地回收空间并更新 VM。通过 pg_visibility 获得直观数据后,这些参数调整就不再盲目了。
pg_visibilityVM位图PostgreSQL修改时间:2026-09-17 23:26:21