PostgreSQL pageinspect 插件深度解析
1. 简介
pageinspect 是 PostgreSQL 官方提供的一个强大的底层调试与诊断扩展。它允许数据库管理员(DBA)和开发者直接检查数据库页面(Page/Block)的内部二进制结构。
核心价值
- 底层透视:绕过 SQL 层,直接查看数据在磁盘上的物理存储格式。
- 故障诊断:用于检测数据损坏、页面分裂异常、索引结构错误等深层问题。
- 性能优化:分析页面填充率(Fill Factor)、死元组分布、索引碎片化程度。
- 学习研究:深入理解 PostgreSQL 的 MVCC 机制、堆表结构及 B-Tree 索引原理。
注意 :由于该插件涉及底层数据读取,所有
pageinspect提供的函数**仅限超级用户(Superuser)**执行。
2. 安装与启用
在大多数标准 PostgreSQL 发行版中,pageinspect 随数据库一起安装。只需在目标数据库中执行以下 SQL 即可启用:
sql
-- 以超级用户身份连接数据库
CREATE EXTENSION IF NOT EXISTS pageinspect;
index_page
sql
DROP FUNCTION index_page(text, integer);
CREATE FUNCTION index_page(relname text, pageno integer)
RETURNS TABLE(itemoffset smallint, htid tid, dead boolean)
AS $$
SELECT itemoffset,
htid,
dead -- starting from v.13
FROM bt_page_items(relname,pageno);
$$ LANGUAGE sql;
heap_page
sql
DROP FUNCTION heap_page(text,integer);
CREATE FUNCTION heap_page(relname text, pageno integer)
RETURNS TABLE(
ctid tid, state text,
xmin text, xmax text,
hhu text, hot text, t_ctid tid
) AS $$
SELECT (pageno,lp)::text::tid AS ctid,
CASE lp_flags
WHEN 0 THEN 'unused'
WHEN 1 THEN 'normal'
WHEN 2 THEN 'redirect to '||lp_off
WHEN 3 THEN 'dead'
END AS state,
t_xmin || CASE
WHEN (t_infomask & 256) > 0 THEN ' c'
WHEN (t_infomask & 512) > 0 THEN ' a'
ELSE ''
END AS xmin,
t_xmax || CASE
WHEN (t_infomask & 1024) > 0 THEN ' c'
WHEN (t_infomask & 2048) > 0 THEN ' a'
ELSE ''
END AS xmax,
CASE WHEN (t_infomask2 & 16384) > 0 THEN 't' END AS hhu,
CASE WHEN (t_infomask2 & 32768) > 0 THEN 't' END AS hot,
t_ctid
FROM heap_page_items(get_raw_page(relname,pageno))
ORDER BY lp;
$$ LANGUAGE sql;
核心函数分类
1. 通用页函数
get_raw_page(relname, blkno)
读取指定表的第 blkno 块(从 0 开始),返回原始字节数据(bytea)。
sql
SELECT get_raw_page('mytable', 0);
也可指定 fork 类型(main/fsm/vm/init):
sql
SELECT get_raw_page('mytable', 'main', 0);
page_header(page bytea)
解析页头信息(PageHeaderData):
sql
SELECT * FROM page_header(get_raw_page('mytable', 0));
输出字段:
| 字段 | 说明 |
|---|---|
| lsn | 页面最后修改的 WAL LSN |
| checksum | 页校验和 |
| flags | 页标志位 |
| lower | 空闲空间起始偏移 |
| upper | 空闲空间结束偏移 |
| special | special space 起始偏移 |
| pagesize | 页大小(通常 8192) |
| version | 页版本号 |
| prune_xid | 页剪枝候选的最老 xmax |
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2. 堆表(Heap)函数
heap_page_items(page bytea)
列出页内所有 tuple 的元数据(包括死元组):
sql
SELECT * FROM heap_page_items(get_raw_page('mytable', 0));
关键输出字段:
| 字段 | 说明 |
|---|---|
| lp | ItemId 编号(行指针序号) |
| lp_off | tuple 在页内的偏移量 |
| lp_flags | ItemId 状态:1=正常, 2=redirect, 3=dead |
| lp_len | tuple 长度 |
| t_xmin | 插入该版本的事务 ID |
| t_xmax | 删除/更新该版本的事务 ID(0 表示未删除) |
| t_ctid | 当前或新版本的物理位置 (page, lp) |
| t_infomask | tuple 信息标志位 |
| t_infomask2 | 包含列数和 HOT 标志 |
| t_bits | NULL 位图 |
| t_data | tuple 原始数据(bytea) |
观察 HOT 链示例
sql
-- 更新一行后观察 HOT 链
UPDATE mytable SET col = 'new' WHERE id = 1;
SELECT lp, lp_flags, t_xmin, t_xmax, t_ctid,
(t_infomask2 & 16384) > 0 AS heap_only_tuple,
(t_infomask2 & 8192) > 0 AS hot_updated
FROM heap_page_items(get_raw_page('mytable', 0));
t_infomask2 关键位:
- 0x4000(16384)= HEAP_ONLY_TUPLE(HOT 新版本)
- 0x2000(8192)= HEAP_HOT_UPDATED(HOT 旧版本)
heap_page_item_attrs(page, relid)
与 heap_page_items 类似,但额外解码每列的实际值:
sql
SELECT * FROM heap_page_item_attrs(
get_raw_page('mytable', 0),
'mytable'::regclass
);
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3. B-Tree 索引函数
bt_metap(relname)
查看 B-Tree 索引元页:
sql
SELECT * FROM bt_metap('mytable_pkey');
| 字段 | 说明 |
|---|---|
| magic | 固定魔数 340322 |
| version | B-Tree 版本 |
| root | 根页块号 |
| level | 树高度 |
| fastroot | 快速根页块号 |
| oldest_xact | 最老活跃事务(用于 VACUUM) |
| last_cleanup_num_tuples | 上次清理时的元组数 |
bt_page_stats(relname, blkno)
查看某个 B-Tree 页的统计信息:
sql
SELECT * FROM bt_page_stats('mytable_pkey', 1);
| 字段 | 说明 |
|---|---|
| blkno | 块号 |
| type | 页类型:r=root, l=leaf, i=internal |
| live_items | 活跃索引条目数 |
| dead_items | 死亡索引条目数 |
| avg_item_size | 平均条目大小 |
| page_size | 页大小 |
| free_size | 剩余空闲空间 |
| btpo_prev/next | 同层前后页链接 |
| btpo_level | 当前页层级(0=叶子) |
bt_page_items(relname, blkno)
查看 B-Tree 页内所有索引条目:
sql
SELECT * FROM bt_page_items('mytable_pkey', 1);
| 字段 | 说明 |
|---|---|
| itemoffset | 条目偏移序号 |
| ctid | 指向堆表的物理位置 |
| itemlen | 条目长度 |
| nulls | 是否含 NULL |
| vars | 是否含变长字段 |
| data | 键值原始字节 |
| dead | 是否为死亡条目(LP_DEAD) |
| htid | 堆 tuple 的真实 ctid(去重后) |
| tids | 去重模式下所有 ctid 列表 |
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4. 其他索引函数
Hash 索引
sql
SELECT * FROM hash_metap('myhash_idx');
SELECT * FROM hash_page_stats(get_raw_page('myhash_idx', 0));
SELECT * FROM hash_page_items(get_raw_page('myhash_idx', 2));
GIN 索引
sql
SELECT * FROM gin_metapage_info(get_raw_page('mygin_idx', 0));
SELECT * FROM gin_page_opaque_info(get_raw_page('mygin_idx', 1));
SELECT * FROM gin_leafpage_items(get_raw_page('mygin_idx', 2));
BRIN 索引
sql
SELECT * FROM brin_metapage_info(get_raw_page('mybrin_idx', 0));
SELECT * FROM brin_revmap_data(get_raw_page('mybrin_idx', 1));
SELECT * FROM brin_page_items(get_raw_page('mybrin_idx', 2), 'mybrin_idx');
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
实用诊断示例
查看表的空闲空间
sql
-- 结合 page_header 计算空闲空间
SELECT
blkno,
upper - lower AS free_space
FROM
generate_series(0, pg_relation_size('mytable') / 8192 - 1) AS blkno,
page_header(get_raw_page('mytable', blkno::int));
统计死元组分布
sql
SELECT
blkno,
count(*) FILTER (WHERE t_xmax <> 0) AS dead_tuples,
count(*) AS total_tuples
FROM
generate_series(0, pg_relation_size('mytable') / 8192 - 1) AS blkno,
heap_page_items(get_raw_page('mytable', blkno::int))
GROUP BY blkno
ORDER BY dead_tuples DESC;
验证 HOT 链完整性
sql
SELECT
lp,
lp_flags,
t_ctid,
(t_infomask2 & 16384) > 0 AS is_hot_tuple,
(t_infomask2 & 8192) > 0 AS is_hot_updated,
t_xmin,
t_xmax
FROM heap_page_items(get_raw_page('mytable', 0))
ORDER BY lp;
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
权限说明
- PostgreSQL 14 之前:需要 superuser
- PostgreSQL 14+:可授权给普通用户
sql
-- PG14+ 授权
GRANT EXECUTE ON FUNCTION get_raw_page(text, int) TO myuser;
GRANT EXECUTE ON FUNCTION heap_page_items(bytea) TO myuser;
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
注意事项
- get_raw_page 读取的是共享缓冲区中的页,不是磁盘文件,结果反映当前内存状态
- 对大表全量扫描所有页会产生大量 I/O,生产环境谨慎使用
- t_data 是原始字节,需结合表结构手动解析列值,建议用 heap_page_item_attrs 代替
- 块号从 0 开始,最大块号 = pg_relation_size(rel) / 8192 - 1