Help us learn about your current experience with the documentation. Take the survey.

PostgreSQL 中表列的排序

对于 GitLab,我们要求新表的列按照使用最少空间的方式进行排序。一个简单的方法是根据类型大小降序排列,将可变长度的类型(如 textvarchar、数组、jsonjsonb 等)放在最后。

类似于 C 结构,表的内存占用受列顺序的影响。这是因为列的大小会根据后续列的类型进行对齐。让我们看一个例子:

  • id (integer, 4 字节)
  • name (text, 可变长度)
  • user_id (integer, 4 字节)

第一列是一个 4 字节的整数。下一列是可变长度的 text。text 数据类型需要 1 字词对齐,在 64 位平台上,1 个词是 8 字节。为了满足对齐要求,需要在第一列后面添加 4 个零,因此 id 占用 4 字节,然后是 4 字节的对齐填充,接下来才存储 name。因此,在这种情况下,存储一个 4 字节的整数需要花费 8 字节。

行之间的空间也会受到对齐填充的影响。user_id 列只占用 4 字节,在 64 位平台上,会添加 4 个零作为对齐填充,以便允许下一行以"干净"的词开始。

结果,每列的实际大小将是(忽略可变长度数据和 24 字节的元组头):8 字节、可变、8 字节。这意味着每行至少需要 16 字节来存储两个 4 字节的整数。如果一个表只有几行,这不是问题。但是,一旦你开始存储数百万行,通过使用不同的顺序就可以节省空间。对于上面的例子,理想的列顺序如下:

  • id (integer, 4 字节)
  • user_id (integer, 4 字节)
  • name (text, 可变)

或者

  • name (text, 可变)
  • id (integer, 4 字节)
  • user_id (integer, 4 字节)

在这些例子中,iduser_id 列被打包在一起,这意味着我们只需要 8 字节来存储它们。这反过来意味着每行需要少占用 8 字节的空间。

从 Ruby on Rails 5.1 开始,ID 的默认数据类型是 bigint,它使用 8 字节。我们在示例中使用 integer 是为了展示一个更真实的重新排序场景。

类型大小

虽然 PostgreSQL 文档 包含了大量信息,但我们在这里列出常见类型的大小,以便更容易查找。这里的"词"指的是字长,在 32 位平台上是 4 字节,在 64 位平台上是 8 字节。

类型 大小 所需对齐
smallint 2 字节 1 词
integer 4 字节 1 词
bigint 8 字节 8 字节
real 4 字节 1 词
double precision 8 字节 8 字节
boolean 1 字节 不需要
text / string 可变,1 字节加数据 1 词
bytea 可变,1 或 4 字节加数据 1 词
timestamp 8 字节 8 字节
timestamptz 8 字节 8 字节
date 4 字节 1 词

“可变"大小意味着实际大小取决于存储的值。如果 PostgreSQL 确定可以直接嵌入到行中,它可能会这样做,但对于非常大的值,它会将数据存储在外部,并在列中存储一个指针(大小为 1 词)。因此,可变大小的列应该始终放在表的末尾。

实际例子

让我们以 events 表为例,它目前有以下布局:

类型 大小
id integer 4 字节
target_type character varying 可变
target_id integer 4 字节
title character varying 可变
data text 可变
project_id integer 4 字节
created_at timestamp without time zone 8 字节
updated_at timestamp without time zone 8 字节
action integer 4 字节
author_id integer 4 字节

在对齐列添加填充后,这会将列分成固定大小的块,如下所示:

块大小
8 字节 id
可变 target_type
8 字节 target_id
可变 title
可变 data
8 字节 project_id
8 字节 created_at
8 字节 updated_at
8 字节 action, author_id

这意味着忽略可变大小数据和元组头,我们每行至少需要 8 * 6 = 48 字节。

我们可以通过使用以下列顺序来优化:

类型 大小
created_at timestamp without time zone 8 字节
updated_at timestamp without time zone 8 字节
id integer 4 字节
target_id integer 4 字节
project_id integer 4 字节
action integer 4 字节
author_id integer 4 字节
target_type character varying 可变
title character varying 可变
data text 可变

这将产生以下块:

块大小
8 字节 created_at
8 字节 updated_at
8 字节 id, target_id
8 字节 project_id, action
8 字节 author_id
可变 target_type
可变 title
可变 data

在这里,忽略可变大小数据和 24 字节元组头,我们每行只需要 40 字节。节省 8 字节听起来可能不多,但对于像 events 表这样的大表来说,这确实开始变得重要。例如,当存储 80,000,000 行时,这至少可以节省 610 MB 的空间,而这仅仅是通过改变几列的顺序实现的。