pg_table_size计算的值大于pg_relation_size各分支的累计值,认知哪儿错了?
在前段时间验证toast相关功能的过程中,遇到一个让我疑惑的问题,就是pg_table_size计算的值大于pg_relation_size各分支的累计值并且计算的分支也包括了toast表的分支。如果你对Postgresql中计算表或对象大小函数有一定了解的话,我相信你看到到一些资料,可能会有类似描述“pg_relation_size各分支大小合计等于pg_table_size的值”,那么在我的测试验证过程中,这个差值到底是哪部分分支未计算到呢,下面我们来一起深入研究一下。
函数功能介绍
在测试前,我们先来了解一下上面的两个函数在官网文档中的介绍,具体的定义如下:
pg_table_size
pg_table_size ( regclass ) → bigint
计算指定表所使用的磁盘空间,不包括索引(但包括它的TOAST表,如果有的话,空闲空间映射,以及可见性映射)。
在这个函数中,我们需要注意的是pg_table_size的大小是不包括索引,但是包括它的TOAST表,因此在下面的验证计算的过程中,也分别计算了TOAST表的大小。
pg_relation_size
pg_relation_size ( relation regclass [, fork text ] ) → bigint
计算指定关系的一个“fork”所使用的磁盘空间。 (注意在大多数情况下,使用更高级的函数 pg_total_relation_size或pg_table_size更方便,它们将所有分叉(forks)的大小相加。) 使用一个参数,这将返回关系的主数据分叉的大小。第二个参数可以用来指定要检查哪个分叉:
main返回关系的主数据分叉的大小。
fsm 返回与该关系关联的空闲空间映射(参见第 73.3 节)的大小。
vm 返回与该关系相关联的可见性映射(参见第 73.4 节)的大小。
init 返回初始化分叉的大小,如果有的话,与关系相关。
在这个函数中,需要注意的pg_relation_size为某一个分支的大小,默认计算main分支的大小。
准备测试用例
创建测试用例表
create table tbl_blog (id serial primary key,author varchar(50),title varchar(200),content text);
执行命令插入数据
insert into tbl_blog(author,title,content) values('墨竹','推荐Postgresql中一些好用的psql命令','psql客户端工具应该是dba非常频繁使用的的工具。');
计算表大小并查看结果
在下面的SQL中,利用pg_relation_size函数分别计算了各个分支的大小。另外也需要注意,对于toast表的大小也是由各个分支的组成的,因此在验证的过程,也分别计算了各个分支。
select main,vm,fsm,init,main+vm+fsm+init as table_sum_size,table_size,
toast_main_size,toast_fsm_size,toast_vm_size,toast_init_size,
toast_main_size+toast_fsm_size+toast_vm_size+toast_init_size as toast_sum_size,toast_size
from (
select pg_relation_size(oid::regclass,'main') as main,
pg_relation_size(oid::regclass,'vm') as vm,
pg_relation_size(oid::regclass,'fsm') as fsm,
pg_relation_size(oid::regclass,'init') as init,
pg_table_size(oid::regclass) as table_size,
pg_relation_size(reltoastrelid,'main') as toast_main_size,
pg_relation_size(reltoastrelid,'fsm') as toast_fsm_size,
pg_relation_size(reltoastrelid,'vm') as toast_vm_size,
pg_relation_size(reltoastrelid,'init') as toast_init_size,
pg_table_size(reltoastrelid) as toast_size
from pg_class where relname='tbl_blog');
上面的SQL执行的结果如下:
-[ RECORD 1 ]---+------
main | 8192
vm | 0
fsm | 0
init | 0
table_sum_size | 8192
table_size | 16384
toast_main_size | 0
toast_fsm_size | 0
toast_vm_size | 0
toast_init_size | 0
toast_sum_size | 0
toast_size | 0
Time: 1.615 ms
从执行结果,可以看到pg_table_size计算的值为16384,表的各分支计算的结果8192,toast表的各分支累计值为0,在这里我们就可以看到pg_table_size比pg_relation_size计算的值大8192字节,这也就是我们在文章开头中提到的问题。
分析过程
在发现上面的问题之后,其实我已经反复的核对这两个函数分别计算的内容,也确认所有的已知的对象都已经计算了,对于目前存在的差异,也很明显是有部分对象未计算。在通过查找网上的资料未果后,也只能去看PG的源码中pg_table_size函数的定义,看看是否包含一些未知内容。在dbsize.c文件中,找到pg_table_size函数的定义,如下图:

在上图pg_table_size函数定义中,可以看到在函数中又调用calculate_table_size计算的大小,下面继续看看这个函数的说明。

在这里看到calculate_table_size需要计算两个分支大小,一个是表的大小(heap size, including FSM and VM),一个是toast 的大小(Size of toast relation)。我继续看了这个函数的说明。
* Calculate total on-disk size of a given table,
* including FSM and VM, plus TOAST table if any.
* Indexes other than the TOAST table's index are not included.
*
* Note that this also behaves sanely if applied to an index or toast table;
* those won't have attached toast tables, but they can have multiple forks.
*/
简单的翻译后,描述的是”计算给定表(包括FSM和VM)的总磁盘大小,如果有的话,再加上TOAST表。不包括TOAST表索引以外的索引“,在这里可以看到两个关键信息,**一个是TOAST表索引,说实话这个还是第一接触这个概念;另外一个是TOAST表索引的值是被包含在calculate_table_size函数计算的值内,即pg_table_size也包含该索引。**既然看到可能有问题的地方,那就接着往下看看TOAST大小计算相关的代码中还有什么有用的信息不。

在这里,就可以清晰看到清晰toast表的大小包括两部分内容,一个是toast堆表,另外一个是toast索引大小,也包括fsm和vm分支大小。看到这里,大概已经知道之前没有计算的部分,可能是toast索引大小。下面就尝试找找这个toast索引。
下面的SQL是后整理的,查询的测试表tbl_blog对应toast表及其toast索引信息,这样可以方便大家直接查看最终的汇总的信息。
--indrelid为toast表对应的oid
select pc.relname as table_name,pc1.relname as toast_name,
pc2.relname as toast_idx_name,pc2.oid idx_oid,pc2.relpages from pg_class pc
join pg_class pc1 on pc.reltoastrelid= pc1.oid
join pg_index pi on pi.indrelid=pc.reltoastrelid
join pg_class pc2 on pc2.oid=pi.indexrelid
where pc.relname='tbl_blog';
table_name | toast_name | toast_idx_name | idx_oid | relpages
------------+----------------+----------------------+---------+----------
tbl_blog | pg_toast_33048 | pg_toast_33048_index | 33053 | 1
(1 row)
Time: 0.619 ms
在上面的结果信息可以知道,这个toast表对应的索引名称为pg_toast_33048_index,其OID为33053。另外也看到relpages为1,大小应该就是8192正好是刚才的差值。
计算toast表索引大小
select
pg_relation_size(33053,'main') as toast_idx_main_size,
pg_relation_size(33053,'fsm') as toast_idx_fsm_size,
pg_relation_size(33053,'vm') as toast_idx_vm_size,
pg_relation_size(33053,'init') as toast_idx_init_size,
pg_table_size(33053) as toast_size;
toast_idx_main_size | toast_idx_fsm_size | toast_idx_vm_size | toast_idx_init_size | toast_size
---------------------+--------------------+-------------------+---------------------+------------
8192 | 0 | 0 | 0 | 8192
(1 row)
Time: 0.317 ms
从计算结果,可以知道toast表索引大小为8192,就是刚才的差值。其实这个我们也可以直接服务器上数据文件33053的的大小,如图为8192字节。

总结
虽然这个问题很简单,但是这个知识点隐藏的很深,如果不看代码,大概率是找不到原因,是由于toast表上默认就有索引存在并占用存储空间,即使toast表是没有值的情况下,而我们之前在验证的时候就未计算这部分toast表索引分支,因此导致两个比值存在差异。最后我就直接借用下面的这个图片,重新对几个函数的关系梳理一下。

1、pg_relation_size用于计算OID代表的表或者索引所使用的磁盘空间,当然这里也可以用于计算toast表及其索引大小,需要注意默认是计算的main分支的大小。
2、pg_indexes_size用于计算表的索引使用的总磁盘空间。
3、pg_table_size用于计算表使用的磁盘空间,不计索引(但是包含TOAST表与TOAST表索引,)
4、pg_total_relation_size用于表使用的总磁盘空间,包括所有的索引和TOAST数据
pg_table_size=pg_relation_size(表的分支)+pg_relation_size(toast表的分支)+pg_relation_size(toast表索引的分支)
pg_total_relation_size=pg_table_size+pg_indexes_size
最后,为了避免在日常工作对于表大小计算出现差错,建议大家尽可能的使用pg_table_size或pg_total_relation_size函数来计算表的大小。
参考
https://dba.stackexchange.com/questions/23879/measure-the-size-of-a-postgresql-table-row/23933#23933
https://www.modb.pro/db/1813145401399193600
– / END / –
可以通过下面的方式联系我
如果这篇文章为你带来了灵感或启发,就请帮忙点赞、收藏、转发;如果文章中不严谨或者错漏之处,请及时评论指正。非常感谢!




