PostgreSQL 教程: 让数据页面容纳更多元组

八月 16, 2024

摘要:在本教程中,您将学习如何让数据页面容纳更多元组。

目录

介绍

当数据自然对齐时,CPU 可以高效地执行对内存的读取和写入。因此,PostgreSQL 中的每种数据类型都有特定的对齐要求。当多个属性连续存储在元组中时,会在属性之前填充内容,以便它从所需的对齐边界开始。更好地了解这些对齐要求,可能有助于最大程度地减少在磁盘上存储元组时所需的填充量,从而节省磁盘空间。

Postgres 中的数据类型分为以下几类:

  • 按值传递,固定长度:按值传递到 Postgres 内部例程,并具有固定长度的数据类型,属于此类别。长度可以是 1、2 或 4(在 64 位系统上还可以为 8)字节。
  • 按引用传递,固定长度:对于这些数据类型,内存中堆页面中的地址引用,将发送到内部 Postgres 例程。它们也有固定的长度。
  • 按引用传递,可变长度:对于可变长度数据类型,Postgres 会在实际数据之前预置一个 varlena 结构的头部。它存储了一些关于数据如何实际存储在磁盘上的信息(未压缩,已压缩或 TOAST),以及数据的实际长度。对于 TOAST 属性,实际数据存储在一个单独的关系中。在这些情况下,varlena 头部后面会有关于数据在其相应 TOAST 关系中的实际位置的一些信息。

数据类型的对齐

通常,varlena 头部的磁盘大小为 1 个字节。但是,如果无法对数据进行 TOAST,并且未压缩数据的大小超过 126 个字节,则它会使用 4 字节的头部。例如:

CREATE TABLE t1 (
  a varchar
);

insert into t1 values(repeat('a',126));

insert into t1 values(repeat('a',127));

select pg_column_size(a) from t1;
    pg_column_size
---------------------
          127
          131

此外,具有 4 字节 varlena 头部的属性,需要与 4 字节对齐的内存位置对齐。它可能会浪费多达 3 个字节的额外填充空间。因此,对这样的列进行一些谨慎的长度限制,可能会节省空间。

  • 按引用传递,可变长度(cstring,unknown):最后,有两种数据类型,即 ctringunknown,它们是字符串文字。它们可以从任何 1 字节对齐的边界进行存储。此外,它们不需要任何 varlena 头部。

您可以使用下面的查询,检查每种类型的对齐要求,其中 typname 是数据类型的名称;如果数据类型是按值传递来访问的,则 typbyval 为 true,否则为 false;typlen 是数据类型的实际长度,但是对于可变长度的数据类型,它的值为 < 0(对于 cstring 和 unknown,为 -2,否则为 -1);typalign 是数据类型所需的对齐。

select typname,typbyval,typlen,typalign from pg_type;

减少列的填充空间

在检查每种数据类型的对齐要求后,您可以通过以有利于对齐的方式放置它们,来减少填充空间。例如,下面的表结构浪费了大量磁盘空间进行填充:

CREATE TABLE 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 对齐列,short 对齐列和 char 对齐列在后,则每个元组可以节省 (1+3+7)=11 个字节的空间。

CREATE TABLE t1 (
  f int8,
  d int4,
  b int2,
  a char,
  c char,
  e char
 );

在元组属性之前,会存储一个固定大小的 23 字节元组头部,后跟一个可选的空值位图和一个可选的对象 ID。属性始终从 MAXALIGN 长的边界开始 - 在 64 位操作系统上通常为 8 个字节(在 32 位操作系统上为 4 个字节)。因此,最小元组头部的有效大小为 24 个字节(23 个字节的头部 + 1 个字节的填充)。当空值位图存在时,它会占用足够的字节,使每个数据列有一个位。在空值位的列表中,1 位表示非空,0 位表示为空。当位图不存在时,假定所有列都为非空。例如:

SELECT pg_column_size(row(NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL));
 pg_column_size
----------------
            24

这包括 23 字节固定大小的元组头部和由 8 位组成的空值位图。现在,如果我们将列数增加到 9 个,则它包括 23 字节固定大小的元组头、一个由 16 位和 7 个字节填充组成的空值位图。

SELECT pg_column_size(row(NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL));
 pg_column_size
----------------
            32

在由数百万行组成的关系中,为每个元组节省几个字节,可能会节省出大量的存储空间。此外,如果我们可以在数据页中容纳更多的元组,则只需要较少的 I/O 活动,从而提高访问性能。

了解更多

PostgreSQL 优化