PostgreSQL 在更新和删除行时遵循 MVCC 机制,旧版本行不会立即从数据文件中消失,而是被标记为不可见,等待后续的清理动作。清理完成后,页面内部会形成大小不一的空洞,这些空洞正是下一次插入可以复用的空间。为了在插入新元组时避免全表扫描,PostgreSQL 使用空闲空间映射(Free Space Map,FSM)提前记录哪些页面还有多少可用空间。pg_freespacemap 扩展做的就是把这些内部数据转换成 DBA 可以查询的视图和函数。

理解 FSM 的更新逻辑,对正确使用 pg_freespacemap 很有帮助。FSM 并不是实时逐字节更新的,而是在 VACUUM 或某些自动清理任务扫描页面后更新。因此查询结果反映的是上一次维护操作结束时的状态,而不是执行查询那一瞬间的精确值。它更适合做趋势分析和膨胀评估,而不是精确审计。
FSM 与 pg_freespacemap 的工作机制
空闲空间映射本质上是一个树状结构,PostgreSQL 用它来记录每个数据页中可用空间的近似值。当一个页面被 VACUUM 清理后,系统会计算该页还剩多少连续或非连续空间,并将这个值写入 FSM。后续插入新元组时,执行器会先查询 FSM,找到空间足够的页面,而不是从表的第一页开始逐个检查。对于大表来说,这种机制能把插入操作的重复扫描成本降得很低。
pg_freespacemap 并不直接读取磁盘上的 FSM 文件,而是通过 PostgreSQL 内部函数实时返回 FSM 中记录的数据。扩展中包含一个函数 pg_freespace(rel regclass, blkno bigint),它接收表名或 OID 以及页面号,返回该页面的可用空间字节数。另一个视图 pg_freespacemap 则一次性列出所有关系的 FSM 条目,字段包括表空间 OID、数据库 OID、文件节点、页面号和可用空间大小。对于想从宏观上观察整个数据库空闲空间分布的 DBA,视图会比函数更方便。
需要注意的是,pg_freespacemap 返回的 avail 字段是 smallint 类型,表示可用空间字节数。FSM 在内部会对空间大小进行分级和取整,因此这个数字是近似值,而不是页面上精确的空闲字节数。对于默认 8KB 页面,这个近似值已经足够指导插入操作;但如果你希望审计精确空间,应该结合 pgstattuple 查看更细粒度的数据。
安装扩展并执行基础查询
安装过程比较简单,在目标数据库中执行 CREATE EXTENSION pg_freespacemap; 即可。该扩展通常由 PostgreSQL 官方安装包自带,不需要额外下载。安装完成后,可以先查看视图的前几条记录来确认数据是否存在。
CREATE EXTENSION pg_freespacemap; SELECT * FROM pg_freespacemap LIMIT 10;
如果关注某一个具体表的页面分布,可以使用函数 pg_freespace 逐页检查。下面的查询假设 public.orders 表前 1000 个页面,列出可用空间大于 0 的页面,并按可用空间倒序排列。
SELECT blkno,
pg_freespace('public.orders', blkno) AS avail
FROM generate_series(0, 999) AS blkno
WHERE pg_freespace('public.orders', blkno) > 0
ORDER BY avail DESC;
这种逐页查询适合检查某个对象的局部空间状态,但如果需要大范围排查,反复调用函数会对性能产生影响。此时应优先使用 pg_freespacemap 视图,一次性获取所有关系的 FSM 数据,再通过关联条件过滤目标表。
用空闲空间数据定位膨胀对象
在数据库维护工作中,一个常见需求是找出哪些表或索引存在大量空闲空间。空闲空间多并不一定代表对象膨胀严重,因为空页可能在下次插入时被复用,但如果一个表长期存在大量离散空闲片段,同时物理文件又很大,就说明空间利用率偏低,可能需要考虑 VACUUM FULL 或 REINDEX 来重建结构。
下面的查询把 pg_freespacemap 视图与 pg_class 关联起来,统计每个关系包含的空闲页面数量和总空闲字节数,并和关系本身的物理大小做对比。结果按总空闲字节数降序排列,排在前面的对象通常值得进一步检查。
SELECT c.relname,
count(f.blkno) AS free_block_count,
sum(f.avail) AS total_free_bytes,
pg_size_pretty(sum(f.avail)) AS free_size,
pg_size_pretty(pg_relation_size(c.oid)) AS table_size
FROM pg_freespacemap f
JOIN pg_class c ON c.relfilenode = f.relfilenode
GROUP BY c.relname, c.oid
ORDER BY total_free_bytes DESC;
执行这类统计时,需要注意 pg_class 的 relfilenode 是物理文件标识,它在某些操作后可能发生变化。例如 TRUNCATE、VACUUM FULL 或某些重建操作会为新文件分配新的 relfilenode,而 FSM 中记录的旧文件节点可能残留。如果发现关联结果中有大量孤立的 relfilenode,通常说明有过重建操作,此时应结合 pg_relation_filenode() 函数确认当前的物理文件节点。
使用限制与误判规避
pg_freespacemap 是观察 FSM 的窗口,而不是查询实际页面空闲情况的精确工具。FSM 的更新依赖 VACUUM 和自动清理任务,因此在长时间未执行 VACUUM 的表上,视图可能显示不出已经被释放但尚未被 FSM 记录的空间。反过来,如果页面在 FSM 更新后又写入了新数据,视图中的可用空间值也可能高于实际情况。把它的结果作为精确审计依据会带来误判。
另外,该扩展对系统表和支持列存储的表可能不会返回有意义的数据。对于索引,FSM 同样存在,但索引页的空闲空间回收规则与堆表不同,通常更应该结合 pgstattuple 来评估索引膨胀。pg_freespacemap 更适合快速发现哪些表的空闲空间总量异常,而不是替代 pgstattuple 或 pg_stat_all_tables 中的统计信息。
实际排查中,建议先使用 pg_freespacemap 做粗粒度筛选,再对重点对象执行 VACUUM (VERBOSE) 或 pgstattuple 进行确认。这样既能控制维护成本,又能避免仅凭近似数据做出重建表或重建索引的决策。

