PostgreSQL把所有表和索引数据都存放在固定大小的数据页中,默认每个页8KB。对性能调优和故障排查来说,能够直接看到页内部的物理布局往往非常关键,比如判断死元组占了多少空间、B-tree索引的叶子页里到底存了什么、元组的t_xmin和t_xmax事务ID是多少。pageinspect就是官方contrib中专门用来做这件事的扩展,它提供一组SQL可调用的函数,把页的二进制内容解析成结构化的行返回。这篇文章详细介绍它的安装、核心函数和几个典型应用场景。

一、先理解数据页的基本布局
在动手调用函数之前,有必要先了解一个堆表页的内部结构。一个8KB的页从前往后大致分为四部分:页头PageHeaderData(24字节)、指针数组ItemIdData、空闲空间,以及从页尾向前增长的元组数据区。页头记录了pd_lsn(最后一次修改该页的WAL日志位置)、pd_checksum(校验和)、pd_lower(空闲空间起始偏移)和pd_upper(空闲空间结束偏移)等关键字段。
指针数组中的每一项(也称为line pointer)占4字节,指向页尾区域中的一个元组。指针和它指向的元组之间可能存在空隙,这就是为什么VACUUM之后页内空间会碎片化。每个元组自己还有一个23字节左右的元组头HeapTupleHeader,包含t_xmin(插入事务ID)、t_xmax(删除事务ID)、t_cid、t_infomask等字段,这些信息在pageinspect的输出里都能直接看到。
B-tree索引页的结构又不太一样,它有专门的PageOpaqueData区域存放在页尾,记录该页是叶子页还是内部页、左右兄弟页的指针等。理解这些差异后,再看函数的输出字段就不会一头雾水。
二、安装扩展并读取原始页
pageinspect属于contrib扩展,主流发行版和云数据库一般都自带,直接创建即可:
CREATE EXTENSION pageinspect;
所有分析都从get_raw_page函数开始,它负责把指定表的某个页原样读出来,返回bytea类型。它有两种调用形式:一种是三参数版本,指定表名、fork号和页号;另一种是两参数版本,默认读取main fork。
-- 读取表test_table的第0页(main fork)
SELECT get_raw_page('test_table', 0);
-- 指定fork:0是main,1是free space map,2是visibility map
SELECT get_raw_page('test_table', 1, 0);
有个细节需要注意:get_raw_page默认以普通用户身份调用时走的是buffer读取,它会检查调用者对该表是否有SELECT权限。另外它读的是共享缓冲区中的当前状态,如果你想在数据库崩溃后从一份拷贝的数据目录里离线分析页,可以考虑pg_filedump工具或者带绝对路径参数的get_raw_page变体(由pg_filedump生态提供,某些发行版中不可用)。
三、核心函数详解与实战
1. page_header:查看页头信息
拿到raw page之后,最外层的分析就是page_header,它解析出页头的所有字段:
SELECT * FROM page_header(get_raw_page('test_table', 0));
输出中值得重点看的字段包括:pd_lsn可以判断该页最后一次被修改对应的WAL位置,用于排查复制延迟或时间点恢复问题;pd_lower减去24再除以4就是指针数量,也就是该页大致的元组数;pd_upper到pd_special之间是实际的元组数据区。如果pd_lower和pd_upper非常接近,说明这个页快满了。
2. heap_page_items:解析堆页元组
这是最常用的函数,它把堆页中的每个元组信息展开成一行:
SELECT lp, lp_off, lp_flags, t_xmin, t_xmax,
t_ctid, t_infomask, to_hex(t_infomask) AS infomask_hex,
t_len
FROM heap_page_items(get_raw_page('test_table', 0))
ORDER BY lp;
lp是页内指针编号,lp_off是元组在页内的字节偏移,lp_flags标识指针状态(0未使用、1正常、2重定向、3死元组)。t_xmin和t_xmax是事务可见性判断的核心,配合t_infomask可以判断一个元组处于什么状态。比如t_infomask的第8位(0x0100)是XMIN_COMMITTED,如果该位已设置,说明xmin事务已提交且已被冻结提示。
实际排查场景中,一个典型用法是统计某个表所有页中死元组的比例,从而判断VACUUM是否落后。可以先拿到表的页数,然后抽样若干页做统计:
-- 获取表总页数
SELECT relpages FROM pg_class WHERE relname = 'test_table';
-- 抽样查看第100页中的死元组
SELECT count(*) FILTER (WHERE lp_flags = 3) AS dead_pointers,
count(*) AS total
FROM heap_page_items(get_raw_page('test_table', 100));
3. bt_page_items:深入B-tree索引页
索引分析使用bt_page_items,它返回索引项的ctid、itemoffset、data以及索引列的实际值。通过bt_metaph和bt_page_stats还能拿到元页信息和页级统计:
-- 查看B-tree索引元页信息
SELECT * FROM bt_metaph('idx_test_name');
-- 查看索引根页或某页的统计信息
SELECT * FROM bt_page_stats('idx_test_name', 1);
-- 查看叶子页中的索引项
SELECT itemoffset, ctid, itemlen, data, t_data
FROM bt_page_items('idx_test_name', 3);
LIMIT 10;
bt_page_stats输出中的live_items和dead_items能直接反映索引项的活跃度,avg_item_size可以帮助估算索引膨胀程度。如果你怀疑某个索引膨胀严重,可以抽样多个页,把dead_items占比和avg_item_size与建索引时的预期做对比,膨胀明显时用REINDEX CONCURRENTLY重建即可。
四、典型应用场景与注意事项
第一个场景是索引膨胀诊断。当表中频繁发生更新时,B-tree叶子页会出现大量死项,页分裂也可能产生填充率很低的页。结合bt_page_stats和pgstattatpartition类的统计信息,可以量化膨胀比例,决定是否需要重建索引。
第二个场景是事务可见性排查。当遇到数据"神秘消失"或长事务阻塞VACUUM的问题时,通过heap_page_items查看相关元组的t_xmin、t_xmax和t_infomask,配合pg_xact_status(PG14+)判断事务状态,往往能快速定位是事务未提交还是快照过旧导致的可见性问题。
使用时还有几点要注意:pageinspect的函数会绕过MVCC直接读物理数据,输出的是最原始的存储状态,与SELECT看到的结果可能不一致,这是正常的;读取大表所有页会产生大量buffer访问,生产环境建议只抽样分析;这些函数需要对目标表有SELECT权限或使用pg_read_data_files角色(PG14+授予)。另外在分析前建议先执行pg_relation_size确认页数范围,避免传入越界的页号导致报错。
总体来说,pageinspect是理解PostgreSQL存储内部机制最好的入门工具之一,把它的输出和文档中PageHeaderData、HeapTupleHeader的结构定义对照着看,能让你对缓冲区管理、VACUUM机制和索引组织方式的理解上升一个台阶。
PostgreSQLpageinspect数据页修改时间:2026-09-04 05:14:40