news 2026/10/21 7:54:24

PostgreSQL 物理层体检:pageinspect 实战解析数据页与索引页

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PostgreSQL 物理层体检:pageinspect 实战解析数据页与索引页

接手过疑难杂症的 DBA 基本都绕不开一个痛点:SQL 层查言行云流水,但数据到底在磁盘页里怎么存、索引页里键和指针长什么样,常规手段完全看不见。我曾经遇到一次线上大表查询突然从索引扫描变成全表扫描,统计信息也刷了、强制走索引反而更慢,折腾半天最后用 pageinspect 直接看到索引页内部满了空洞和重复键,才真正把病根挖出来。从那之后,pageinspect 就成了我固定的“物理层体检工具”,每次怀疑存储结构有问题,第一反应就是把它拉出来看两眼。

这篇东西我会把自己实际用 pageinspect 的经验、踩过的坑、以及常见的故障排查思路完整讲一遍。适合手里已经有 PostgreSQL 生产环境、想深入了解存储结构的 DBA,也适合正在学数据库内核、想直观理解数据页和索引页机制的开发者。内容不会止步于“怎么安装”“有哪些函数”,而是会落到真实场景:怎么看一个表的页面头部、怎么顺着元组追 MVCC 链、怎么检查 B-tree 索引页、怎么用最小代价定位膨胀和损坏问题。

1. pageinspect 是什么,为什么说它是数据库的“物理层体检工具”

普通的 SQL 查询面对的是逻辑表,PostgreSQL 的优化器帮你把 SQL 翻译成对物理文件的访问路径,但文件内部怎么组织,SQL 层是不透明的。pageinspect 是 PostgreSQL 官方 contrib 自带的扩展,它提供了一组底层函数,可以直接读取数据库文件里的页面(page),并按照页面格式把头部信息、空闲空间、行指针、行数据、索引元数据、键值指针等结构一点一点解析出来。

打个比方:常规运维工具帮你在图书馆目录系统里查某本书有没有被借走,而 pageinspect 是让你直接进书库,翻开某一本书的某一页,看清楚这一页上有多少字、哪些字被人画过线、书页之间哪里夹了纸条。这种粒度对日常监控来说是多余的,但一旦遇到“目录显示正常、书实际有问题”的状况,它就不可替代。

它能解决的核心问题大致有三类:

  • 物理结构异常:索引页损坏、页面校验和不一致、空闲空间失衡导致的异常膨胀。
  • MVCC 与行版本问题:HOT 链断裂、死元组清理不掉、长事务留下的旧版本累积。
  • 学习与调优验证:看 fillfactor 设置真正起了多少作用,看删除数据后空间是否真能被复用,看索引键分布是否配合查询计划。

权限方面需要注意,pageinspect 涉及读取物理存储内容,所以在默认配置下需要有超级用户或者被显式授予权限才能调用。只读函数很多,它本身不修改页面数据,但如果拿它在大表上扫全页,依然会产生真实 IO,不能当成普通视图随便刷。

2. 拆解 pageinspect 核心函数:一张表看懂整个功能面

pageinspect 的函数量不算少,名字也直白,基本上“你怀疑什么结构,就调什么函数”。我按大类拆开讲,方便你按图索骥。

2.1 堆表(Heap)相关函数

get_raw_page(relname text, fork text, block_number int)

所有堆表分析的基础函数,作用是把指定关系、指定 fork、指定块编号的原始页面以 bytea 二进制形式读出来。fork 可以填main、fsm、vm,分别对应主数据文件、空闲空间映射文件、可见性映射文件。后面所有函数基本都是拿这个函数的输出做二次解析。

page_header(page bytea)

解析 get_raw_page 拿到的页面头部,返回 lsn、checksum、flags、lower、upper、special、pagesize、version、prune_xid 等字段。看这个页面有没有损坏、有没有发生过 pruning、空闲空间还有多少,全靠它。

heap_page_items(page bytea)与heap_page_item_attrs(page bytea, relname text, [fields text[]])

heap_page_items 把页面里的行指针(ItemId)和元组(HeapTupleHeaderData)展开成一行一行的记录,能看到lp_off、lp_flags、lp_len,以及元组里的t_xmin、t_xmax、t_field3、t_ctid、t_infomask等字段。这个函数是最常用的“解剖”工具。它不会解析每个字段的值,要连带看到列的实际内容,就需要heap_page_item_attrs额外传入表名,让内核按表结构把属性值转出来。

2.2 索引相关函数

B-tree 系

  • bt_metap(relname text):查看 B-tree 索引的元页,返回 root 块、level 层数、快速root块、旧root块等。
  • bt_page_stats(relname text, blkno int):查看索引页统计,返回页类型(root/internal/leaf)、当前页上的活元组数、死元组数、平均项大小、空闲空间等。
  • bt_page_items(relname text, blkno int):把指定索引页里的条目逐条列出,返回 ctid、itemoffset、key 的二进制内容,以及指向下一层的 downlink 页号。

这三个函数基本能让你把一颗 B-tree 从 root 到 leaf 完完整整走一遍,也是定位索引膨胀、索引损坏最直接的工具。

Hash 系

hash_page_type(page bytea)、hash_page_stats(page bytea)、hash_page_items(page bytea),用于哈希索引的桶页、溢出页结构检查。哈希索引在日常业务里不如 B-tree 常见,但一旦业务用到了,排查逻辑损坏时这几个函数就是救命稻草。

GIN 系

gin_metapage_info(page bytea)、gin_page_opaque_info(page bytea)、gin_leafpage_items(page bytea)。GIN 索引在全文检索等场景很常用,这几个函数能查 pending list、leaf 页内条目,对确认 GIN 索引是否健康很有帮助。

GiST 系

gist_page_opaque_info(page bytea)、gist_page_items(page bytea, index_oid regclass)。GiST 索引用于几何、范围类型等,遇到诡异查询结果时,可以用它们确认页面层级和数据指针是否正常。

BRIN 系

brin_page_type(page bytea)、brin_metapage_info(page bytea)、brin_revmap_data(page bytea)、brin_page_items(page bytea, index_oid regclass)。BRIN 索引体积小但结构特殊,检查的时候主要看 range map 和 summary tuple。

2.3 辅助函数

  • fsm_page_contents(page bytea):解析 FSM 页里记录的每个堆页空闲空间的上限值,很好用于研究空闲空间管理机制。
  • page_checksum(page bytea, blkno int)与page_checksum_set(page bytea, blkno int):计算或重置页面校验和,适合做页面损坏的模拟验证。
  • page_leaf_headers(page bytea):8.4 之后加的,主要用来快速判断页面是否发生过叶分裂。

这些函数按 PostgreSQL 版本分批加入的,我建议你在自己的版本上直接执行\dx+ pageinspect看支持列表,不同大版本之间有些函数名和入参会有差异,例如早期版本bt_page_items接收的是page bytea,新版本同时支持通过索引名和块号直接抓页。

3. 实操演练:从一张测试表开始拆解数据页

理论知识再多,不如动手看一眼。下面我用一个可复现的步骤,带你从建表到看懂一个完整的数据页。

3.1 准备测试环境并定位目标块

先建一个最朴素的测试表,插入一批数据,再删掉一部分,目的是让页面产生空闲空间和死元组:

CREATE TABLE test_page ( id int primary key, name text, created_at timestamp default now() ); INSERT INTO test_page (id, name) SELECT g, 'name_' || g FROM generate_series(1, 500) AS g; DELETE FROM test_page WHERE id BETWEEN 100 AND 200;

此时这张表至少有两类页面:被删过数据的页和完全干净的页。我们想找到具体哪个 block 上有空闲空间,最笨的办法是一页一页试,但更聪明的做法是借助 FSM:

CREATE EXTENSION IF NOT EXISTS pageinspect; SELECT * FROM fsm_page_contents(get_raw_page('test_page', 'fsm', 0));

你会看到 FSM 第一页里记录了一堆数值,每个值代表对应堆页的空闲空间等级。等级数字越大表示该页剩余空间越多。等查到某个块号对应较高等级后,直接去读 main fork:

SELECT * FROM page_header(get_raw_page('test_page', 'main', 0));

这里block_number就是从 0 开始的物理块号,和 FSM 结果里的 block number 一一对应。

3.2 解读页面头部结构

拿到的page_header返回字段比较多,我挑重点说:

  • lsn:本页最后被修改的日志序列号,用于崩溃恢复对齐。
  • checksum:如果开启了 data checksum,这里是非零值;为 0 时表示没有校验和或者该页从未被写入。
  • flags:常见的位组合是 1(已分配)、4(该页是 BTREE_ROOT 的某一层等,堆页里主要是 1),如果看到异常组合就要警惕。
  • lower与upper:这是空闲空间的边界。lower表示行指针数组结束的位置,upper表示元组数据区开始的位置。两者差值就是当前可用空闲空间。
  • special:堆表中指向特殊区域起始位置,一般为页面末尾;索引页里用来存放页级附加信息。
  • prune_xid:页面上最早的可能被剪枝的事务 ID,用于判断 HOT 清理进度。

举个常见判断:如果flags显示页面已被分配,但lower和upper之间几乎没有空间,说明这个页基本写满了;如果upper远小于页面大小且lower很小,两者差值大,则说明页内有大量可复用空间,这就是膨胀的“物质基础”。

提示:page_header里看到的free space并不是直接给的,需要自己算。页面大小通常是current_setting('block_size')返回的数值,PostgreSQL 默认 8192 字节,所以空余空间 =upper - lower。

3.3 解析行数据与 MVCC 元信息

重点来了,一条 SQL 把页面内所有元组的物理头信息展开:

SELECT lp, lp_off, lp_flags, lp_len, t_xmin, t_xmax, t_field3, t_ctid, t_infomask FROM heap_page_items(get_raw_page('test_page', 'main', 0));

解释几个核心字段:

  • lp:行指针编号,从 1 开始。行指针数组是页面最前面的连续区域。
  • lp_flags:1 表示该指针未使用,2 表示正常指向元组,3 表示是 HOT 重定向指针。这里如果大量出现未使用指针,说明页面有碎片。
  • lp_off:该元组在页面内的偏移量,配合lp_len可以快速判断元组是否连续分布。
  • t_xmin/t_xmax:插入和删除该版本时的事务 ID。t_xmax=0通常表示当前版本尚未被删除或更新。
  • t_ctid:指向当前版本自身或下一个新版本的(block_number, offset)。如果指向另一个偏移量,说明发生过更新。
  • t_infomask:一组位标志,内核用它判断元组的可见性、是否发生多事务、是否已提交等。直接读数值很枯燥,可以查 PostgreSQL 头文件htup_details.h里的宏定义按位解析。

实操里我最常用的是用下面的查询把行状态粗分出来:

SELECT lp, lp_off, t_xmin, t_xmax, t_ctid, (t_infomask & 2) > 0 AS has_xmax, (t_infomask & 256) > 0 AS xmin_committed, (t_infomask & 512) > 0 AS xmin_invalid FROM heap_page_items(get_raw_page('test_page', 'main', 0));

把t_infomask拆成人类可读的标志位后,哪些行已经是死元组(xmin_invalid 或 has_xmax 且 xmax 已提交),哪些是活元组,一眼就能看出来。这也是我遇到“vacuum 一直清不掉脏数据”时的第一步排查动作:先确认页面上死元组的物理形态,再看是 long-running 事务阻止了清理,还是 HOT 链太长导致回收困难。

3.4 看清楚 HOT 链的真实形态

页面上做一次 UPDATE,让 PostgreSQL 在同一个页面里形成 HOT 链,然后重新解剖:

UPDATE test_page SET name = name || '_updated' WHERE id = 1;

再次查询同一页的heap_page_items,注意t_ctid的变化。如果新版本仍然在同一页,你会看到旧元组的t_ctid指向新元组的(0, new_lp),同时lp_flags里可能有一个是 HOT 重定向(3)。这意味着更新没有跨页,且索引不需要额外维护,这也是 PostgreSQL 在重复更新场景下保持性能的关键。

我在生产环境检查 HOT 链,还会顺手统计一下单页内链长,这个方法配合heap_page_items可以验证表上的填充率是否过高导致 HOT 频繁溢出到新页面:

WITH items AS ( SELECT lp, t_ctid FROM heap_page_items(get_raw_page('test_page', 'main', 0)) ) SELECT count(*) AS total_tuples, count(*) FILTER (WHERE t_ctid::text != format('(0,%s)', lp)) AS moved_tuples FROM items;

moved_tuples数量越大,说明发生过越多的版本迁移,hot 命中率越低,页面写入放大越明显。调优时把 fillfactor 适当调小一些,往往就能压住这个数字。

4. 实操演练:索引页结构诊断与膨胀溯源

表数据页看明白了,索引页才是很多诡异问题的根源。B-tree 索引是 PostgreSQL 最常用的索引类型,所以我拿它做完整演示。

4.1 查看 B-tree 元页与每层统计

CREATE INDEX test_page_id_idx ON test_page(id); SELECT * FROM bt_metap('test_page_id_idx');

返回的root字段指向 root 块号,level表示索引高度。感受一下:如果单列主键索引都出现了 3 层以上,要么数据量极大,要么索引页结构异常。拿到 root 块号以后,接着查每页统计:

SELECT * FROM bt_page_stats('test_page_id_idx', 1);

这个函数返回的关键字段包括:

  • type:索引页类型,值为r(root)、i(internal)、l(leaf)等。
  • live_items与dead_items:页面上有效条目与待清理条目的数量。
  • avg_item_size:条目平均大小。
  • page_size与free_size:页面大小与剩余空间。

如果某个叶子页dead_items长期居高不下,同时vacuum又没能回收,大概率是某些长事务导致索引清理无法推进。更严重的是,如果live_items异常少于同层其他页,那可能出现了物理损坏或删改异常。

4.2 直接查看索引页内的键与指针

再往下钻,直接读 leaf 页上的每条索引项:

SELECT itemoffset, ctid, data FROM bt_page_items('test_page_id_idx', 1);

对于叶子页,ctid指向堆表里的实际行;data是键值的内部二进制表示。对于内部页,ctid字段实际上是向下子页的块号,data是该子页中的最大键值。

这套信息在定位“索引歧义”问题时特别好用。比如查询计划普遍选择全表扫描,你很怀疑索引页结构异常导致扫描成本虚高,就可以对比 root 到 leaf 的完整链路,检查内部页下探指针是否连续、leaf 页键值是否单调递增。索引键值本应该按序排列,一旦你在bt_page_items里看到同页键值乱序,那基本可以断定索引逻辑一致性出问题了,不能只靠REINDEX解决,得先确认是否有硬件层面的坏块。

4.3 结合 amcheck 扩展做一次完整一致性验证

pageinspect 负责把索引页“翻给人看”,但真正用于逻辑一致性验证的可靠工具是另一个官方扩展amcheck。它的核心函数bt_index_check('test_page_id_idx')会从元页开始递归检查整个 B-tree:键值顺序、上下层指针、以及每个键与堆表元组的对应关系。如果函数正常返回,说明索引逻辑结构没问题;如果抛出"block ... is not the leftmost block"这类错误,就说明某个页违反了 B-tree 左右兄弟链规则。

我建议的索引健康检查标准动作是:

-- 先看 pageinspect 找物理异常 SELECT bt_metap('test_page_id_idx'); SELECT bt_page_stats('test_page_id_idx', 1); -- 再用 amcheck 做逻辑一致性验证 CREATE EXTENSION IF NOT EXISTS amcheck; SELECT bt_index_check('test_page_id_idx');

这一组合拳打下来,物理层和逻辑层都覆盖了。pageinspect 查出的异常信息帮你在业务不可见时快速锁定可疑块,amcheck 则负责最终判定索引是否需要重建。生产环境里我见过不少“REINDEX 之后查询计划恢复正常”的案例,表面上是索引坏了,背后原因却常常是此前某次非正常宕机或磁盘控制器把页面写错位,amcheck 输出的错误信息比应用层报错更能暴露根因。

4.4 顺带看其他索引类型的检查方法

哈希索引常用hash_page_stats和hash_page_items,GIN 索引用gin_leafpage_items检查 leaf 页上的条目,GiST 用gist_page_items查看页面内 pointers。这些函数用法和 B-tree 类似,只要先用get_raw_page拿到对应 fork 的页,再调用对应解析函数即可。索引出问题不用太慌张,优先确认是哪一层哪个块出错,再决定局部重建还是整体重建。pageinspect 就是帮你完成“定位到块”这一关键步骤的。

5. 实际排障场景:让 pageinspect 帮你破案

工具光会装没用,关键要会用。我复盘了几个真实经历过的场景,都是 pageinspect 起决定性作用的案例,这里做了脱敏处理,但排查思路完全是原汁原味。

5.1 场景一:索引坏了但 VACUUM 没修好

现象是某核心业务表有一段时间随机出现慢查询,每次慢查询的 SQL 都不一样,唯一共同点是都涉及同一个复合索引。优化器统计信息正常,pg_stat_user_indexes里 idx_scan 数量骤减。手动SET enable_indexscan = off后反而变快,最终锁定索引本身。

当时我做了两步:

SELECT bt_metap('idx_order_created_composite'); SELECT bt_page_stats('idx_order_created_composite', blkno);

通过 root 一层层下钻,在某个 internal 页发现live_items和dead_items数值极端不成比例,且同一页内data二进制值出现明显的键序颠倒。这已经不是“索引碎片多”能解释的问题,更像是页面逻辑内容损坏。随后我用 amcheck 跑bt_index_check,直接报了左右兄弟链不一致的错误。

处理方式也很朴素:备份后重建索引。但关键收获在于,我借 pageinspect 找到了具体损坏块,再反查该块对应时间点的系统日志,最终定位到是磁盘控制器缓存策略导致掉电后部分页面写串位。没有 pageinspect,我无法解释为什么一个看起来正常的复合索引会在内部页留下键序颠倒的反常数据,这个问题可能要让业务无端背很久的锅。

注意:一旦发现索引逻辑一致性错误,不要尝试用VACUUM FULL去修复,也不要原地REINDEX有风险,最好先备份相关表数据或整个数据库,再用REINDEX INDEX或DROP INDEX / CREATE INDEX重建,因为损坏索引本身可能已经指向错误的堆元组位置。

5.2 场景二:表膨胀与 fillfactor 选择的验证

某报表表的更新频率极高,每次更新业务会改 30 多个字段。DBA 团队讨论要不要把 fillfactor 从默认 100 降到 80,以减少 HOT 被迫溢出到新页的概率。讨论之前我们用 pageinspect 收集了两个关键数据点。

先看每个页面的lower与upper分布:

SELECT blk, lower, upper, upper - lower AS free_space FROM ( SELECT blk, (page_header(get_raw_page('report_table', 'main', blk))).lower, (page_header(get_raw_page('report_table', 'main', blk))).upper FROM generate_series(0, 100) AS blk ) s ORDER BY free_space;

再把每个页面的 HOT 链数量统计出来。两批数据放一起看非常直观:大量页面剩余空间已经接近于零,HOT 链却仍有 3-5 跳,说明更新经常跨页产生新版本,索引维护成本随之上升。降 fillfactor 到 85 之后,页面留出余量,同样的更新模式可以在页内完成新版本写入,t_ctid跨页指针比例大幅下降,主库写放大和 vacuum 压力都明显缓解。

这种验证如果只靠宏观指标(如表大小、vacuum 次数)很难有说服力,但 pageinspect 把页面层面的行为量化为每个块的空闲空间、每条链的长度,决策就变成了数据驱动。

5.3 场景三:崩溃恢复后的页面完整性检查

有一回模拟环境里做了异常断电测试,重启后 PostgreSQL 通过 WAL 恢复了大部分数据,但有几个表报告“could not read block ... in file ...”。这种错误往往意味着页面本身已经损坏,与 WAL 恢复流程无关。当时我通过 pageinspect 直接扫了一遍可疑表的全部页面,对比 checksum 字段,快速区分出哪些页是单纯的 CRC 错误(可通过备份恢复页面),哪些页虽然 checksum 通过但逻辑内容异常(行指针引用越界、元组长度超过页面边界)。后者即便用zero_damaged_pages也可能丢掉部分数据,必须走备份恢复。

在这个场景里,page_checksum和page_header是主力工具,先确认物理层是否可信,再讨论逻辑层一致性。顺序不能反,否则容易在错误的方向上浪费时间。

6. 实际操作中容易踩的坑与问题速查

工具好用,但坑也不少。我把自己和身边同事踩过的坑按主题整理出来,避免你再走一遍。

6.1 权限与安装的坑

第一个坑是权限。正常执行CREATE EXTENSION pageinspect;后,普通业务账号往往无法调用函数,因为不少核心函数默认只运行超级用户调用。PostgreSQL 16 开始对pg_read_all_data角色放开了部分权限,但旧版本里你需要手动GRANT EXECUTE ON FUNCTION ... TO ...一把一把授权。我建议直接给需要排查问题的账号授予pg_read_all_data(如果版本支持),或者建一个专门的监控只读账号,授予必要的函数执行权限。

-- 在支持的版本上,给监控账号只读权限 GRANT pg_read_all_data TO monitor_user; -- 旧版本需要手动授权 GRANT EXECUTE ON FUNCTION get_raw_page(text, text, int) TO monitor_user; GRANT EXECUTE ON FUNCTION heap_page_items(bytea) TO monitor_user;

第二个坑是误把get_raw_page当成能跨库使用。get_raw_page的 relname 是文本格式,且会在当前数据库里解析。如果你在数据库 A 里想查看数据库 B 的表,必须先连到 B 库或用current_database()保证关系名解析正确,否则只会收到 “relation does not exist”。

6.2 调用方式与版本兼容的坑

早期版本bt_page_items的入参是两个:一个是bytea页面内容,一个是int块号,后来在 14 左右版本陆续增加了直接传 relname 和 blkno 的写法。如果你在生产库上执行报函数签名不存在,先确认页面版本再调整调用方式。另外heap_page_item_attrs里传入表名做属性解析时,如果目标表有 dropped column,解析结果可能出现意外的空值或类型转换失败,这时不要慌,直接改看原始t_infomask,不回退到列值解析层面。

我建议把核心排查查询包装成函数或视图,这样以后遇到问题直接调用,不需要每次手写大段 SQL。例如:

CREATE OR REPLACE VIEW v_heap_page_info AS SELECT current_database() AS datname, t.relname, blk AS block_number, (ph).lower, (ph).upper, (ph).upper - (ph).lower AS free_space, s.ctid_count FROM ( SELECT t.relname, blk, page_header(get_raw_page(t.relname, 'main', blk)) AS ph FROM pg_class t, generate_series(0, 100) AS blk WHERE t.relname = 'test_page' ) s, LATERAL ( SELECT count(*) AS ctid_count FROM heap_page_items(get_raw_page('test_page', 'main', s.blk)) ) s;

这只是一个最简单的模板,实际使用中我还会把 blk 范围改成动态子查询,避免全表扫描式的页级遍历。

6.3 数据解读的坑

解读bt_page_items输出时,最容易误读的是内部页里的ctid字段。内部页的 ctid 不是堆表位置,而是指向子页的块号和页内偏移,这个语义差异很关键。同样,leaf 页里data列在 PostgreSQL 默认bytea_output = hex时显示成十六进制,看着像乱码,其实是正常的内部键表示。真要读键值,最好在会话级别先设置:

SET bytea_output = escape;

恢复成可读转义字符后再对照键值类型判断,否则很容易被十六进制串干扰判断。另一个常见误区是把lp_flags = 1(未使用的行指针)直接等同于“页面碎片”。未使用行指针其实很正常,表经过大量删除后自然会产生散落的空闲行指针,真正需要关注的是未使用指针与已使用指针的比值,以及lp_off的连续性。如果 1000 个 lp 里 800 个未用,同时upper却已经快到lower,那才是明显碎片化。

6.4 常见问题速查

现象优先排查点常用 pageinspect 函数
表膨胀但 vacuum 频繁页内空闲空间分布,HOT 链长度page_header, heap_page_items
索引扫描突然消失索引页内部键序,dead_items 堆积bt_metap, bt_page_stats, bt_page_items
页面损坏错误checksum 与头部结构page_header, page_checksum
vacuum 清不掉死元组事务状态标志位,长事务影响heap_page_items, t_infomask 解析
崩溃恢复后数据未知物理页完整性page_checksum_set, page_header
FSM 与堆页不一致空闲空间映射内容fsm_page_contents, page_header

这张表基本覆盖了我遇到 80% 的物理层问题。加粗建议是:把 pageinspect 当“第二层验证工具”,不要替代pg_stat_user_tables、pg_stat_all_indexes这类统计视图,而是在统计视图给出可疑信号后,再深入到页面级验证。

7. 我习惯把 pageinspect 用在哪些关键时刻

最后分享一点个人操作习惯。我并不是每天都用 pageinspect,日常巡检还是以统计信息、等待事件、慢查询为主。但一旦出现以下三种信号,我会第一时间打开 pageinspect:

  • 查询计划诡异变化,且统计信息刷新后不恢复。
  • VACUUM 持续执行但表/索引大小不减。
  • 数据库报出“could not read/write block”或校验和错误。

前面几类问题处理得多了,你会形成一套肌肉记忆:先看表页头部算空闲空间,再用 heap_page_items 看行版本状态,接着用 bt_page_stats 看索引页结构,最后用 amcheck 做逻辑校验。这套流程在多个环境里都帮我快速收敛了问题范围,比在一堆统计视图里猜原因要快得多。

一个小工具上的经验:如果你要批量检查多张表,不要对每张表逐页调用get_raw_page,那样会产生大量随机 IO。我通常是先通过pg_class.relpages估算总块数,然后把扫描限定在怀疑范围内,或者直接用采样函数抽查若干块,而非全量遍历。排查损坏不是审计,抽样加上定向定位的效率最高。

pageinspect 是个底层的、看起来有点“危险”的扩展,但它实际是只读的,只要权限控制到位,完全可以在生产环境放心启用。我这个项目做完之后最大的体会是:数据库排障的终极武器往往不是那些好看的 dashboard,而是能让你直接看见磁盘页面的那条 SQL。如果下次你也被一个查不出根因的性能问题折磨,不妨先建上扩展,从第 0 页开始看起。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 7:54:06

Bootstrap 5实战:从源码构建到深色模式一键切换

简介:本资源是一套面向前端初学者与进阶开发者的Bootstrap 4.0系统化学习材料,聚焦响应式网页开发核心能力培养,解决从零掌握栅格布局、组件集成、JS插件调用到真实项目落地的全链路学习痛点。压缩包共1389个文件,涵盖250个HTML实…

作者头像 李华
网站建设 2026/10/21 7:54:04

Text-to-Prototype与D2C实战:从一句话到可交互原型的工程化工作流

一句话丢给 AI,它真能吐出一套能点、能跳转、能填表单的 App 原型?这个问题在过去一年里被问了无数遍,我自己也拿它当正经课题折腾了小半年。Text-to-Prototype,也就是文本生成原型,再到 D2C(Design to Cod…

作者头像 李华
网站建设 2026/10/21 7:54:04

MATLAB单应性矩阵原理与实战:从DLT到RANSAC的底层解析

简介:本资源是一套基于MATLAB实现图像单应性矩阵计算的完整实践方案,面向计算机视觉初学者、图像处理课程学习者及需要完成相关实验作业的本科生。资源聚焦于两张平面图像间点对应关系建模,通过交互式选点与矩阵求解,帮助用户深入…

作者头像 李华
网站建设 2026/10/21 7:53:49

.NET源码生成器实战:用partial范式终结样板代码

有没有算过你日常项目的样板代码到底占了多少行?我以前统计过一个不大不小的业务项目,DTO、枚举转换、通知属性这些“复制粘贴型”代码能占到四成。改一个字段名要动三四个地方,漏一处就在运行时报奇怪的错。后来我用.NET源码生成器把这些活全…

作者头像 李华