PostgreSQL的多版本并发控制机制依靠保留旧行版本来实现一致性读,这意味着一次UPDATE或DELETE并不会立即释放磁盘空间,而是留下被称为死元组的旧版本。常规的系统视图如pg_stat_user_tables能提供估算的行数和上次清理时间,却无法精确回答某张表里究竟有多少死元组、浪费了多少空间。pgstattuple扩展正是为解决这个问题而生,它直接扫描堆文件或索引文件的物理页面,返回页面级别的精确统计数据,帮助数据库管理员判断表膨胀程度、评估VACUUM效果以及决定是否执行重组操作。

一、pgstattuple扩展的安装与启用
pgstattuple是PostgreSQL官方提供的contrib扩展之一,默认编译安装后即可使用,但需要在目标数据库中显式启用。启用命令很简单:
CREATE EXTENSION pgstattuple;
执行前可以查看扩展是否可用:
SELECT name, default_version, installed_version FROM pg_available_extensions WHERE name = 'pgstattuple';
值得注意的是,pgstattuple依赖存储管理器接口读取物理页面,因此只有具备超级用户权限或者被授予相应执行权限的角色才能调用。生产环境中通常不会把超级用户权限直接交给普通账号,更合适的做法是创建一个专用的监控角色,然后通过SECURITY DEFINER包装函数来暴露必要的统计能力。启用扩展本身不会带来持续的资源消耗,真正的开销发生在调用统计函数时,所以安装动作可以放心执行。
二、pgstattuple核心函数的输出字段
pgstattuple函数既可以接收表名,也可以接收表OID,返回一行包含该表物理存储状况的记录。常用调用方式如下:
SELECT * FROM pgstattuple('public.orders');
返回结果中最重要的字段包括table_len、tuple_count、tuple_len、tuple_percent、dead_tuple_count、dead_tuple_len、dead_tuple_percent、free_space和free_percent。其中table_len表示整个表占用的物理字节数,tuple_count是可见元组的总数,tuple_len是所有可见元组占据的字节数,tuple_percent则是可见元组空间占表总大小的百分比。dead_tuple_count和dead_tuple_len分别统计已删除或已更新后残留的旧版本数量和尺寸,dead_tuple_percent衡量死元组空间占比。最后的free_space与free_percent表示页面中可复用的空闲空间大小和比例。
如果只想针对大表快速获取近似值,可以使用pgstattuple_approx。这个函数不会逐页扫描全部数据块,而是基于统计信息和物理采样进行计算,因此速度快得多。它返回的列包括table_len、scanned_percent、approx_tuple_count、approx_tuple_len、approx_tuple_percent、dead_tuple_count、dead_tuple_len、dead_tuple_percent以及approx_free_space、approx_free_percent。在监控场景中,先使用近似函数发现可疑对象,再对个别表执行精确扫描,是更高效的工作方式。
三、用死元组比例定位表膨胀
表膨胀最直接的信号就是dead_tuple_percent和free_percent过高。一个健康的表在正常写入负载下也可能产生少量死元组,但如果死元组比例长期超过20%到30%,说明旧版本没有得到及时清理,或者autovacuum的频率和力度不够。此时不仅会浪费磁盘空间,还会拖慢顺序扫描和索引扫描,因为执行器必须跳过大量不再可见的元组。
使用以下查询可以从函数返回中提取关键指标:
SELECT
(t).table_len,
(t).tuple_count,
(t).dead_tuple_count,
(t).dead_tuple_percent,
(t).free_space,
(t).free_percent
FROM pgstattuple('public.orders') AS t;
可以先记录一组基线数据,再执行一次VACUUM (VERBOSE) orders;,然后重新运行统计函数。普通VACUUM会清理死元组并标记空闲空间供后续写入复用,但不会把空间归还给操作系统,因此table_len通常不会显著下降。相反,dead_tuple_count会明显减少,free_space可能上升,因为被清理出来的空间进入可复用池。如果执行的是VACUUM FULL或CLUSTER,表文件会被重写,table_len才会明显缩小。通过前后对比可以准确判断维护操作是否达到预期效果,也可以验证autovacuum配置调整后死元组是否得到更及时的回收。
四、索引页面统计:pgstatindex与碎片分析
除堆表外,pgstattuple扩展还提供针对索引的统计函数。最常用的是pgstatindex,它专门用于B-tree索引,返回索引大小、树高度、叶子页数、空闲页数以及叶子页密度等信息。典型调用如下:
SELECT * FROM pgstatindex('idx_orders_customer_id');
返回值中的avg_leaf_density表示叶子页的平均填充率,leaf_fragmentation表示叶子页的逻辑顺序与物理顺序不一致的程度。如果平均叶子密度持续低于50%,或者碎片率明显升高,通常说明索引经历了大量随机插入、删除或更新,页内空间利用不充分,扫描时需要读取更多页面。此时可以考虑执行REINDEX INDEX idx_orders_customer_id;来重建索引,以恢复紧凑的页面布局并提升范围扫描性能。
除了B-tree,扩展还提供pgstatginindex和pgstathashindex分别用于GIN和Hash索引。它们返回的字段与B-tree版本不同,主要反映pending list、页面统计等信息。无论哪种索引,执行统计函数都要扫描索引文件,索引越大耗时越长,因此在生产环境同样建议在低峰期运行。
五、性能影响与监控建议
需要明确的是,pgstattuple的精确统计通过扫描整个数据文件实现,表越大执行时间越长。对于上百GB的表,一次统计可能需要几分钟甚至更久,并产生持续的物理读I/O。不过它只获取AccessShareLock,不会阻塞写入和其他查询,因此并发方面相对安全。问题主要在于资源消耗,建议在从库或业务低峰时段执行,同时避免在同一个连接中连续扫描多张大表。
对于日常监控,更推荐使用pgstattuple_approx快速获取死元组和空闲空间比例的近似值,并结合阈值告警。下面的查询遍历public模式下的普通表,按死元组比例降序排列:
SELECT c.oid::regclass AS table_name, approx.dead_tuple_count, approx.dead_tuple_percent, approx.approx_free_percent FROM pg_class c CROSS JOIN LATERAL pgstattuple_approx(c.oid) AS approx WHERE c.relkind = 'r' AND c.relnamespace = 'public'::regnamespace ORDER BY approx.dead_tuple_percent DESC;
当发现某些表反复进入高死元组状态时,不应只依赖手动VACUUM,而应检查autovacuum_vacuum_threshold、autovacuum_vacuum_scale_factor等参数,或者查看pg_stat_user_tables中last_autovacuum和autovacuum_count,判断自动清理是否按预期运行。对于更新极其频繁的小表,可以适当降低阈值或改用autovacuum_vacuum_insert_threshold进行干预。把pgstattuple的统计结果纳入巡检流程,能够把隐性的空间浪费转变成可量化、可跟踪的维护指标。
pgstattuplePostgreSQL死元组修改时间:2026-08-20 00:30:09