本篇来自少辉,利用空闲时间深入研究pgstattuple,给年轻优秀的DBA点赞。
一、pgstattuple介绍
pgstattuple 是 PostgreSQL 数据库的一个官方扩展模块,核心作用是统计表、索引或指定表分区的详细存储信息与元组(行)状态,帮助数据库管理员(DBA)分析表膨胀、索引健康度、存储效率等问题,为性能优化(如 VACUUM、REINDEX)和存储规划提供数据支撑。
官方文档说明 [1]
F.33. pgstattuple — obtain tuple-level statistics
二、pgstattuple源码探究
1、整体架构

2、入口函数:pgstattuple()
这里可以看到存在两个入口,如果pgstattuple版本小于v1.5 将会走pgstattuple(),如果大于等于v1.5将会走pgstattuple_v1_5(),最大的区别就是:pgstattuple v1.5以下查询必须用Superuser用户,v1.5版本及其以上的不在需要Superuser用户执行。

pgstattuplebyid()同上所述

3、分发函数:pgstat_relation()
根据传入的类型执行不同的函数:

从这里我们也能看出pgstattuple支持的类型:
- RELKIND_RELATION - 普通表 - RELKIND_MATVIEW - 物化视图 - RELKIND_TOASTVALUE - TOAST表 - RELKIND_SEQUENCE - 序列 - BTREE_AM_OID - B树索引 - HASH_AM_OID - 哈希索引 - GIST_AM_OID - GiST索引 - GIN_AM_OID - GIN索引 - SPGIST_AM_OID - SP-GiST索引 - BRIN_AM_OID - BRIN索引 - RELKIND_VIEW - 视图 - RELKIND_FOREIGN_TABLE - 外部表 - RELKIND_PARTITIONED_TABLE - 分区表 - RELKIND_PARTITIONED_INDEX - 分区索引
4、堆表统计函数:pgstat_heap()
对于普通表、物化视图、TOAST表、序列等将会走pgstat_heap()函数进行计算。 pgstattuple只支持传统的堆表,不支持其他存储格式(如列存储)

启动扫描,初始化快照视图,获取块数。之后开始正向扫描,检查元组可见性,如果可见就累计到活跃元组统计,如果不可见,就累计到死元组统计。 同时在扫描过程中计算空闲空间,为什么是block<=tuplock呢,原因是heap_getnext可能跳过空页面,所以不能只统计有元组的页面。

最后扫描剩下的所有块的空闲空间,停止扫描并释放资源,计算表的总长度。

5、索引统计函数:pgstat_index()
动态遍历索引,扫描索引块大小,同时调用函数完成页面扫描,BTREE、HASH、GIST索引扫描函数下面有介绍,释放资源返回结果。

pgstat_btree_page()、pgstat_hash_page()、pgstat_gist_page() 这三个函数过程相似,内部都会调用pgstat_index_page() 去计算

(1)BTREE统计函数:pgstat_btree_page()
从缓冲区获取页面数据,如果是新页面,直接加页面的大小,然后获取B树页面元数据,如果页面是删除或者半死页面,则直接算作空闲页面累加free,如果是叶子节点就调用pgstat_index_page() 计算

(2)HASH统计函数:pgstat_hash_page()
先从缓冲区获取实际页面数据,然后检查页面的完整性,之后获取页面元数据,如果完全空闲就直接加页面大小的free,如果是哈希桶页面或者溢出页面,就调用pgstat_index_page() 计算,其他页面类型跳过。

(3)GIST统计函数:pgstat_gist_page()
先检查gis页面完整性,然后获取实际数据,调用就调用pgstat_index_page() 计算,根节点和内部节点不做统计。

(4)PAGE统计函数:pgstat_index_page()
根据偏移量遍历页面状态,如果是死亡状态,死项目数量加1,累加死项目的字节长度

6、输出函数:build_pgstattuple_type()
最终输出环节,计算各种指标,并输出结果

7、调试验证
调试样例构建:
CREATETABLE test_index ( idSERIAL, user_id INT, actionTEXT, action_time TIMESTAMPDEFAULTCURRENT_TIMESTAMP, details JSONB ); ALTERTABLE test_index SET (autovacuum_enabled = off); CREATEINDEX idx_test_index_user_id ON test_index(user_id); CREATEINDEX idx_test_index_action_time ON test_index(action_time); CREATEINDEX idx_test_index_details ON test_index USING GIN(details); INSERTINTO test_index (user_id, action, details) SELECT floor(random() * 10000)::int, (array['login', 'logout', 'purchase', 'view', 'click'])[floor(random() * 5 + 1)::int], jsonb_build_object( 'ip', inet_client_addr()::text, 'device', (array['mobile', 'desktop', 'tablet'])[floor(random() * 3 + 1)::int], 'duration', floor(random() * 3600) ) FROM generate_series(1, 1500000); UPDATE test_index SET user_id = floor(random() * 10000)::int, action_time = CURRENT_TIMESTAMP - (random() * 30)::int * '1 day'::interval WHEREid % 4 = 0;
(1)表调试

(2)索引调试

Reference
[1] https://www.postgresql.org/docs/current/pgstattuple.html