PG中的数据对齐(译)
翻译自:https://www.enterprisedb.com/postgres-tutorials/data-alignment-postgresql
当数据自然对齐时,CPU可以有效地对内存执行读写操作。因此,PostgreSQL中的每种数据类型都有特定的对齐要求。当在一个元组中连续存储多个属性时,在属性之前插入填充,使其从所需的对齐边界开始。更好地理解这些对齐要求可能有助于减少在磁盘上存储元组时所需的填充量,从而节省磁盘空间。
Postgres中的数据类型分为以下几类:
- Pass-by-value, fixed length:通过值传递给Postgres内部例程并具有固定长度的数据类型属于这种类型。长度可以是1、2或4个字节(64位系统上为8个字节)。
- Pass-by-reference, fixed length: 对于这些数据类型,内存堆页中的地址引用被发送到内部Postgres例程。它们也有固定的长度。
- Pass-by_reference, variable length: 对于可变长度的数据类型,Postgres在实际数据之前添加一个varlena头。它存储了一些关于数据如何实际存储在磁盘上的信息(未压缩、压缩或TOASTed)以及数据的实际长度。对于 TOAST 属性,实际数据存储在一个单独的关系中。在这些情况下,varlena标头后面跟着一些关于数据在相应TOAST关系中的实际位置的信息。
通常,varlena头的磁盘上大小是1字节。但是,如果不能 TOAST 数据,并且未压缩数据的大小超过126个字节,则使用4个字节的头。例如,
CREATETABLE t1 (
, a varchar
);
insertinto t1 values(repeat('a',126));
insertinto t1 values(repeat('a',127));
select pg_column_size(a)from t1;
pg_column_size
---------------------
127
131
此外,具有4字节varlena头的属性需要与4字节对齐的内存位置对齐。它可能会浪费多达3字节的额外填充空间。因此,对这些列进行一些谨慎的长度限制可能会节省空间。
- Pass-by_reference,可变长度(cstring, unknown):最后,有两种数据类型,即ctring和unknown,它们都是字符串字面量。它们可以从任何1字节对齐的边界存储。而且,它们不需要任何varlena头文件。
可以使用以下查询检查每种类型的对齐要求,
selecttypname,typbyval,typlen,typalignfrompg_type;
// 输出
---------------------------------------------------------------------
typname |typbyval|typlen|typalign
---------------------------------------+----------+--------+----------
bool |t | 1|c
bytea |f | -1|i
char |t | 1|c
name |f | 64|c
int8 |t | 8|d
int2 |t | 2|s
int2vector |f | -1|i
int4 |t | 4|i
其中typname是数据类型的名称;
typbyval如果数据类型是通过值传递访问的,则为true,否则为false;
typlen是数据类型的实际长度,但是对于可变长度的数据类型,它的值< 0 (cstring和unknown为-2,否则为-1);
typalign是数据类型所需的对齐方式。其含义如下所示:
/home/gtwang/postgresql-13.2/src/include/catalog/pg_type.h
*'c'=CHARalignment, ienoalignmentneeded.
*'s'=SHORTalignment (2bytesonmostmachines).
*'i'=INTalignment (4bytesonmostmachines).
*'d'=DOUBLEalignment (8bytesonmanymachines, butbynomeansall).
* (UsetheTYPALIGNmacrosbelowforthese.)
在检查了每种数据类型的对齐要求之后,您可以通过以有利于对齐的方式定位它们来减少填充空间。例如,下面的表模式浪费了大量的磁盘空间来填充:
CREATETABLE t1 (
, a char
, b int2 -- 1 byte of padding after a
, c char
, d int4 -- 3 bytes of padding after c
, e char
, f int8 -- 7 bytes of padding after e
);
如果将列重新按照对齐要求排序:首先double 齐列,然后int对齐,最后短对齐和char对齐列,则可以为每个元组节省(1+3+7)=11字节的空间。
CREATETABLE t1 (
, f int8
, d int4
, b int2
, a char
, c char
, e char
);
在元组属性之前,存储一个固定大小的23字节的元组头,后跟一个可选的空位图和一个可选的对象ID。属性总是从maxaligned边界开始—在64位操作系统上通常为8字节(或在32位操作系统上为4字节)。因此,最小元组报头的有效大小是24字节(23字节报头+ 1字节填充)。当存在空位图时,它会占用足够的字节,使每个数据列都有一个位。在这个位列表中,1位表示非空,0位表示空。当位图不存在时,所有列都假定为非空。例如[3],
SELECT pg_column_size(row(NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL));
pg_column_size
----------------
24
上面这里例子包含了23个字节的固定大小的元组头和一个由8位组成的空位图。现在,如果我们将列数增加到9,它将包括23个字节的固定大小的元组头,一个由16位和7字节填充组成的空位图。
在由数百万行组成的关系中,为每个元组节省几个字节可能会节省一些重要的存储空间。此外,如果我们可以在一个数据页中放入更多的元组,性能就可以由于较少的I/O活动而得到提高。