我有一张超过100M行的Postgres表,其结构为{整数, 整数, 整数, 不带时区的时间戳}。我预期每行的大小应为3*整数 + 1*时间戳 = 3*4 + 1*8 = 20字节。
实际上,每行大小为pg_relation_size(tbl) / count(*)
= 52字节。为什么?
(没有对表进行删除操作:pg_relation_size(tbl, 'fsm')
~= 0)
我有一张超过100M行的Postgres表,其结构为{整数, 整数, 整数, 不带时区的时间戳}。我预期每行的大小应为3*整数 + 1*时间戳 = 3*4 + 1*8 = 20字节。
实际上,每行大小为pg_relation_size(tbl) / count(*)
= 52字节。为什么?
(没有对表进行删除操作:pg_relation_size(tbl, 'fsm')
~= 0)
行大小的计算比这复杂得多。
存储通常分为8kB的数据页。每个页面有一个小的固定开销、可能剩余的大小不足以放下另一个元组,更重要的是死亡行或最初使用FILLFACTOR
设置保留的百分比。
每一行(元组)也有更多的开销:页面开始处的4字节项标识符、23字节的HeapTupleHeader
和对齐填充。元组头开始处以及元组数据开始处都以MAXALIGN
的倍数对齐,典型的64位机器上为8字节。某些数据类型需要按2、4或8字节的下一个倍数对齐。
typalign
是存储此类型值时所需的对齐方式。它适用于磁盘上的存储以及PostgreSQL内部的大多数表示。当多个值连续存储时,例如在磁盘上完整行的表示中,在此类型的数据之前插入填充,以便它从指定边界开始。对齐参考是序列中第一个数据的开始处。可能的值包括:
c
=char
对齐,即不需要对齐。
s
=short
对齐(大多数机器上为2字节)。
i
=int
对齐(大多数机器上为4字节)。
d
=double
对齐(许多机器上为8字节,但绝非全部)。
在这里阅读手册中的基础知识。
这将导致你的三个integer
列后面有4个字节的填充,因为timestamp
列需要double
对齐,并且需要从下一个8字节的倍数开始。
因此,一行占据了:
23 -- heaptupleheader
+ 1 -- padding or NULL bitmap
+ 12 -- 3 * integer (no alignment padding here)
+ 4 -- padding after 3rd integer
+ 8 -- timestamp
+ 0 -- no padding since tuple ends at multiple of MAXALIGN
如A.H.在评论中指出,每个元组在页面头部都有一个加号的标识符:
+ 4 -- item identifier in page header
------
= 52 bytes
因此,我们得到了观察到的52个字节。
计算pg_relation_size(tbl) / count(*)
是一种悲观的估计。 pg_relation_size(tbl)
包括膨胀(死行)和由fillfactor
保留的空间,以及每个数据页和每个表的开销。(我们甚至没有提到在TOAST表中压缩长varlena
数据,因为这里不适用。)
您可以安装附加模块pgstattuple并调用SELECT * FROM pgstattuple('tbl_name');
获取有关表和元组大小的更多信息。
相关内容:
3 * 4 + 1 * 8 == your data
24 bytes == row overhead
total size per row: 23 + 20
大约是53个字节。我实际上写了postgresql-varint来帮助解决这个问题,正好符合这个使用场景。您可能需要查看类似的帖子以获取有关元组开销的其他详细信息。
ItemData
指针(4字节)? - A.H.ItemData
指针是按照每个元组在页面头中分配的,这应该解释了我计算的48字节和观察到的52字节磁盘空间之间的差异。我在我的答案中添加了一条注释。 - Erwin BrandstetterTOAST
的更多信息。因此,非常小的元组存在相当大的开销。尽管如此,表上的操作通常非常快,因此不要陷入过早去规范化您的表的诱惑中。如果有疑问,请运行性能测试。 - Erwin Brandstetter