PostgreSQL pageinspect 插件深度解析

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;

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

注意事项

  1. get_raw_page 读取的是共享缓冲区中的页,不是磁盘文件,结果反映当前内存状态
  2. 对大表全量扫描所有页会产生大量 I/O,生产环境谨慎使用
  3. t_data 是原始字节,需结合表结构手动解析列值,建议用 heap_page_item_attrs 代替
  4. 块号从 0 开始,最大块号 = pg_relation_size(rel) / 8192 - 1
相关推荐
云边有个稻草人2 小时前
【MySQL】第十四节—事务:从基础概念到隔离性理论与实践 | 详解
数据库·mysql·事务·隔离级别·事务的隔离性·事务提交方式
干啥啥不行,秃头第一名2 小时前
Python深度学习入门:TensorFlow 2.0/Keras实战
jvm·数据库·python
FL4m3Y4n2 小时前
redis的主从同步与对象模型
数据库·redis·缓存
Mr.45672 小时前
JDK17+Druid+SpringBoot3+ShardingSphere5 多表分库分表完整实践(MySQL+PostgreSQL)【生产优化版】
数据库·spring boot·后端
FL4m3Y4n2 小时前
redis存储原理与数据模型
数据库·redis·缓存
蓝黑20202 小时前
把数据库表里两列的值互换
数据库·sql·mysql
梦想的旅途22 小时前
企业微信引用消息的实现与配置
数据库
是桃萌萌鸭~2 小时前
oracle中的 CDB 和 PDB 详解
数据库·oracle