在PostgreSQL中,表的数据以固定大小的页面(默认8KB)存储,页面内部存在已用和未用区域。当表经历大量更新和删除后,页面中会散落许多 dead tuple,形成空闲空间。要准确掌握这些空闲空间的分布,可以借助contrib模块pageinspect直接解析底层页面结构。

pageinspect扩展的准备工作
pageinspect是PostgreSQL自带的一个贡献模块,提供了若干函数用于检查数据库页面的内容。在使用之前,需要以超级用户身份在目标数据库中创建该扩展。如果数据库未安装contrib包,则需先通过操作系统包管理器安装,例如在某些Linux发行版中对应postgresql-contrib软件包。
创建扩展的语句非常简单,但需要注意权限问题。只有具备超级用户权限或者拥有该数据库 CREATE 权限的角色才能执行。扩展创建后,相关的函数如表page_header、heap_page_items就会注册到当前数据库的public模式下,后续查询可以直接调用。
-- 以超级用户连接目标库后执行 CREATE EXTENSION IF NOT EXISTS pageinspect; -- 确认函数已存在 df page_header df heap_page_items
理解页面头与空闲空间的计算原理
每一个PostgreSQL堆页面都有一个页面头结构,其中两个关键字段是pd_lower和pd_upper。pd_lower指向行指针(item pointer)数组的末尾,而pd_upper指向实际元组数据的起始位置。页面中的空闲空间大小,就是pd_upper减去pd_lower所得的差值。当表中删除大量记录但未做VACUUM时,pd_upper不会立即回缩,空闲空间便停留在页面中间。
通过pageinspect提供的page_header函数,可以读取指定页面的头信息。该函数接收一个页面二进制数据作为参数,通常配合get_raw_page函数从表中抽取原始页面。理解这两个字段的关系,是手动计算空闲空间的基础,也比单纯看统计视图更能反映物理存储的真实状态。
-- 读取某表第0号页面的头部信息
SELECT *
FROM page_header(get_raw_page('test_table', 0));
-- 输出示例中的 pd_lower 与 pd_upper 字段
-- pd_lower | pd_upper
-- 232 | 7920
-- 空闲字节 = 7920 - 232 = 7688
逐页统计表的空闲空间
单页查看无法反映整张表的情况,实际运维中往往需要遍历所有页面并求和。PostgreSQL的表由多个物理文件分支组成,每个分支又由从0开始的页面号排列。我们可以借助generate_series生成页面号序列,并结合page_header批量计算。
下面的示例通过关联get_raw_page和page_header,对指定表的前N个页面进行扫描。需要注意的是,若页面号超出实际范围,get_raw_page会报错,因此通常要先从pg_class获取relpages估算总量,或利用异常处理避免中断。统计结果中的空闲总和,能帮助判断表内碎片程度。
-- 假设表名是 test_table,扫描前100个页面
SELECT
blk AS page_num,
(page_header(get_raw_page('test_table', blk))).pd_upper AS upper,
(page_header(get_raw_page('test_table', blk))).pd_lower AS lower,
(page_header(get_raw_page('test_table', blk))).pd_upper -
(page_header(get_raw_page('test_table', blk))).pd_lower AS free_bytes
FROM generate_series(0, 99) AS blk;
-- 汇总全部空闲空间
SELECT sum(
(page_header(get_raw_page('test_table', blk))).pd_upper -
(page_header(get_raw_page('test_table', blk))).pd_lower
) AS total_free
FROM generate_series(0, 99) AS blk;
结合heap_page_items分析碎片类型
page_header只能看到空闲总量,若想区分这些空间是连续的还是被dead tuple割裂的,需要用heap_page_items查看行项。该函数返回页面中每一行的ctid、lp_len、t_xmin、t_xmax等字段。通过t_xmax不为0且未被冻结,可判断出哪些是已删除但未清理的死元组。
当heap_page_items显示大量短小死元组穿插在活元组之间,即便pd_upper与pd_lower差值较大,实际可复用空间也因碎片严重而有限。此时单纯依赖VACUUM可能不够,需要考虑VACUUM FULL或者pg_repack来重整页面。pageinspect因此成为决策前的重要诊断手段。
-- 查看0号页面中所有行项
SELECT lp, lp_off, lp_len, t_xmin, t_xmax, t_data
FROM heap_page_items(get_raw_page('test_table', 0));
-- 筛选死元组(t_xmax 非零表示已被删除或更新)
SELECT count(*) AS dead_rows
FROM heap_page_items(get_raw_page('test_table', 0))
WHERE t_xmax <> 0;
与pgstattuple的对比及使用建议
除了pageinspect,PostgreSQL还提供pgstattuple扩展来估算表级空闲空间。pgstattuple扫描全表并给出大致的dead_percent和free_percent,使用更方便,但它是基于采样或轻量扫描的估算值。pageinspect则是逐页精确解析,适合在怀疑统计信息失真或需要定位具体膨胀页面时使用。
在生产环境中,不建议频繁对大表全量使用pageinspect,因为get_raw_page会读取共享缓冲区或磁盘,且占用会话资源。推荐先通过pgstattuple做全局判断,再针对可疑表用pageinspect抽样少数页面验证。这样既能控制开销,又能获得可靠的空闲空间视图。
| 工具 | 精度 | 性能开销 | 适用场景 |
|---|---|---|---|
| pgstattuple | 估算 | 中 | 快速查看表级膨胀率 |
| pageinspect | 精确 | 高 | 定位具体页面碎片与空闲 |
常见误区与注意事项
一个常见误区是认为pd_upper和pd_lower的差就是“可回收空间”。实际上,若页面中存在未清理的dead tuple,它们占据的空间并不包含在差值里,只有执行VACUUM后,行指针收缩、pd_upper回退,差值才代表真正可插入新行的区域。因此看空闲空间要结合事务状态和清理情况。
另外,pageinspect读取的是当前时刻的页面快照,若数据库正在写入,结果可能随查询过程变化。对关键业务表操作时,应尽量在低峰期进行,并避免长事务持有页面引用。掌握这些细节,才能让pageinspect真正成为查看空闲空间的好帮手。
pageinspectPostgreSQL空闲空间修改时间:2026-08-11 01:15:38