1. 变长数据类型
对于任意类型的关系型数据库,比如MySQL
、PostgreSQL
、DB2
或Oracle
等,其内部均支持可变长的数据类型,它们用于存储文本字符串数据,比如PostgreSQL中的CHARACTER VARYING(n)
,简写为VARCHAR(n)
,变长类型,其中n
指明该变长类型的长度限制,也可不填写n
;CHARACTER(n)
,简写为CHAR(n)
,没有参数n
,则等于CHAR(1)
,以及TEXT
、BYTEA
、BLOB
、CBLOB
等。我们知道,关系型数据库是将用户插入的每一条数据按照行(元组、记录)的方式组织到数据表文件中。

在干货 | PostgreSQL数据表文件底层结构布局分析 一文中,详细地分析了数据库中表文件的底层结构布局方式。它通过将表文件划分为若干个大小为8KB
的页面来存储用户插入的每一行数据。但是用户创建的数据表中,极有可能存在大量的文本字段,比如TEXT
、VARCHAR
等。如果用户插入的某条数据其文本数据类型对应的数据超过了页(8KB
)大小,PostgreSQL会如何处理?是选择将该数据存储在表底层的两个页(page
)中,还是采取其他的方案来存储数据?
1.1 元组不能跨行
PostgreSQL官方文档中曾提到:
PostgreSQL数据库不允许元组(行,记录)跨越多个页面(page)存储,所以,它不能直接存储非常大的字段值。对于大字段值,它将被压缩且(或)分解为多个物理行,该技术称为“TOAST”。
所以,如果某条记录中字段值的大小不能存储于一个页中,那么它并不会继续使用其他1
个或n
个页来存储这条数据,而是采用了其他的技术,比如TOAST
或大对象数据存储机制。

注意,TOAST
技术仅适用于那些可变长的数据类型,比如VARCHAR
、TEXT
、JSON
和BYTEA
等,而对于BLOB
、CBLOG
等数据类型,它们将使用另外一种方式(规则)来存储,即上面提到的“大对象存储机制”。在PostgreSQL数据库中,总共有两种方式用于存储大(文本)数据对象,分别是:TOAST
机制和大对象机制。本文主要讲解TOAST
机制,而对于“大对象存储机制”,后面将单独用一篇博文进行讲解。
2. TOAST技术
所谓TOAST(The Oversized-Attribute Storage Techniques
),即超大属性存储技术。它是PostgreSQL的一种机制,用于处理大块数据以适应页面缓冲区。当待插入的数据(元组)超过TOAST_TUPLE_THRESHOLD
(默认2KB
)时候,PostgreSQL将压缩数据,以适应2KB
的缓冲区大小。如果对大列(变长数据类型)项数据压缩没有产生更小的块(即小于2KB
),那么该数据将会被分割成更小的块(chunks
),然后创建一个TOAST表来存储和管理该元组。
/* * If the new tuple is too big for storage or contains already toasted * out-of-line attributes from some other relation, invoke the toaster. */if (relation->rd_rel->relkind != RELKIND_RELATION && relation->rd_rel->relkind != RELKIND_MATVIEW){ /* toast table entries should never be recursively toasted */ Assert(!HeapTupleHasExternal(tup)); return tup;}else if (HeapTupleHasExternal(tup) || tup->t_len > TOAST_TUPLE_THRESHOLD) //元组大小超过TOAST_TUPLE_THRESHOLD(2KB), 则创建TOAST表 return heap_toast_insert_or_update(relation, tup, NULL, options);
2.1 TOAST触发
TOAST机制是自动触发的,当它检测到元组大小超过TOAST_TUPLE_THRESHOLD
时候,就会走TOAST逻辑处理段分支,然后根据用户的选择进行对应的插入或更新操作。当然,如上面所描述,只有一些变长数据类型(如:TEXT
、JSON
、BYTEA
、VARCHAR
等)才会触发TOAST机制,因为将一个不能产生较大字段值的数据类型(比如VARCHAR(1)
、INT
等)关联TOAST机制将得不偿失,其收益将远低于TOAST所带来的开销。TOAST机制背后将伴随着大量的逻辑业务判断处理、数据压缩以及TOAST表系列操作。
2.2 TOAST表存储策略
在PostgreSQL中,所有的数据库列(字段)都有与它们相关联的(默认)存储策略,这里共有四种不同的存储数据策略,它们分别是:PLAIN
、EXTENDED
、EXTERNAL
和MAIN
。各存储策略的解释如下:
(1) PLAIN
不允许压缩和线外存储。对于那些不能TOAST机制的数据类型,默认都是该策略,比如整数类型(INT
,SMALLINT
,BIGINT
)、字符类型(CHAR
)、布尔类型(BOOLEAN
)等等。
(2) EXTENDED
允许线外存储和压缩。这是大多数可以使用TOAST机制的数据类型默认存储策略。它首先尝试压缩,如果这仍不能将数据放入页中,则使用线外存储。
(3) EXTERNAL
允许线外存储,但不允许压缩。该存储策略将使TEXT
、BYTEA
数据类型中的子串操作更快,其代价是牺牲存储空间。
(4) MAIN
允许压缩,但不能线外存储。然而在无法使列足够小以存入页时,它将会执行线外存储。所以它仍然是可以线外存储的,和EXTERNAL
具有相似性。
存储策略PLAIN
、EXTENDED
、EXTERNAL
和MAIN
在PostgreSQL源码中分别被简写为p
、e
、x
和m
。通过storage_name
函数,分别获取个存储策略所对应的名字。
#define TYPSTORAGE_PLAIN 'p' /* type not prepared for toasting */#define TYPSTORAGE_EXTERNAL 'e' /* toastable, don't try to compress */#define TYPSTORAGE_EXTENDED 'x' /* fully toastable */#define TYPSTORAGE_MAIN 'm' /* like 'x' but try to store inline */static const char * storage_name(char c){ switch (c) { case TYPSTORAGE_PLAIN: return "PLAIN"; case TYPSTORAGE_EXTERNAL: return "EXTERNAL"; case TYPSTORAGE_EXTENDED: return "EXTENDED"; case TYPSTORAGE_MAIN: return "MAIN"; default: return "???"; }}
在Linux
终端,使用psql
工具登录postgres
,然后创建数据表student
,该表共有3个字段,分别是:id
(主键, SERIAL
类型)、name
(姓名,VARCHAR
类型)和age
(年龄, INT
类型)。
test=#test=# CREATE TABLE student(id SERIAL PRIMARY KEY, name VARCHAR, age INT);CREATE TABLE
在创建该表的时候,并没有显示地指定各字段的存储策略类型。当该表成功创建后,使用:\d+ student
查看该表信息时,发现PostgreSQL将student
表中的各字段补充了默认对应的存储策略类型。

该表中,字段id
和age
都是数字类型,所以默认是PLAIN
;而字段name
是VARCHAR
,可变长数据类型,所以它支持TOAST策略,则对应的存储策略是EXTENDED
,允许线外存储和压缩。
2.2.1 修改存储策略
可以使用ALTER TABLE table_name ALTER column_name SET STORAGE storage_name;
语句来修改表中各字段的存储策略规则。比如表student
中,字段name
的存储规则是EXTENDED
,现使用ALTER TABLE
语句来更改其存储规则为EXTERNAL
;
test=# ALTER TABLE student ALTER name SET STORAGE EXTERNAL ;ALTER TABLE
更改成功后,再次查看数据表student
中的name
字段存储策略时,已经成功地由EXTENDED
改为了EXTERNAL
。

不过值得注意的是,你无法将一个不支持TOAST机制的数据类型(列)的存储策略修改为其他。比如字段age
是数据类型INT
,其默认存储策略是PLAIN
,若尝试将其修改为MAIN
,则会报错提示。

3. TOAST表结构
如果一张表中存在可变长数据类型,那么该表将有一个与之关联的TOAST表。TOAST是一张单独的表,专用于存储大块数据的列。比如表student
,其中字段name
是可变长数据类型,则该字段存在一个与之对应的TOAST
表,示意图如下:
我们知道,系统表pg_class
中包括当前数据库里的所有数据表,其中每个数据表都在在pg_class
中表示为一个元组,且每个数据表被分配一个OID
作为该表的唯一标识。很显然,我们的TOAST表也在pg_class
中,但是因为不知道TOAST表的名字,所以我们需要借助于表student
,来间接获取。
SELECT relname FROM pg_class WHERE oid = (SELECT reltoastrelid FROM pg_class WHERE relname = 'student');
系统表pg_class
中, 若该元组(某张表)中有变长数据类型,则字段reltoastrelid
将关联该表所对应的TOAST表。因此通过reltoastrelid
字段得到TOAST表的唯一OID
即可获取到TOAST表名。

现在得到了TOAST表的表名是pg_toast_16436
,我们通过TOAST表名反向查询其oid
,得到其oid
是16440
,和表student
中的字段reltoastrelid
的值是相互对应的。
test=# SELECT oid FROM pg_class WHERE relname = 'pg_toast_16436'; oid------- 16440(1 row)
对于TOAST表的命名,其规则是:pg_toast_$(oid)
。其中oid
是该TOAST表所属表的oid
值,比如数据表student
在系统表pg_class
中的oid
是16436
,则与之关联的TOAST的表名字是pg_toast_16436
。
由于toast
表位于pg_toast
模式中,所以当要查询一个toast
表时候,需要使用如下方式:
SELECT * FROM pg_toast.pg_toast_16436;
如下图所示,由于当前表student
为空,所以与之关联的toast
表也为空(0行记录)。

我们使用\d+
命令来查看该toast
( pg_toast.pg_toast_16436
)表中的所有可见字段列表,得到该表共有3个字段,即chunk_id
、chunk_seq
和chunk_data
。
字段chunk_id
,线外存储时候,为该toast
表所分配的oid
;字段chunk_seq
,块序列号,表明该块数据在toast表中的序列位置;字段chunk_data
,存储的实际块数据。
之所以特意强调“可见字段”,那是因为数据表除了用户创建的字段外,还有许多字段是默认隐藏了(没有显示在终端)。比如xmin、cmin、xmax、cmax和ctid等等,关于这些隐藏字段的具体含义,在后面的MVCC(多版本并发控制)博文中进行详细介绍。
在将超过2KB
大小的可变长数据存储到toast
表中的时候,它会被分成最多包含TOAST_MAX_CHUNK_SIZE
数据字节的块(chunk
),每个块(chunk
)都作为一个元组插入到toast
表中。
3.1 小试牛刀
现在我们向数据表student
中插入一条数据,其中name
字段对应的值大小是39KB
,这将远大于2KB
,因此,它将触发TOAST机制,数据使用LZ
压缩算法,并将压缩后的数据分成若干个大小不超过TOAST_MAX_CHUNK_SIZE
的块,然后存储于toast
表中。
这里的可变长数据是一个JSON报文(使用JSON工具压缩成一行)的字符串。仅列出部分信息,其余部分省略掉。
{"ipAddress":"10.244.0.133","protocolType":"HTTP", "dateTime":"2021-08-31T02:52:13Z", ......//省略}
现在向表student
插入该数据,然后再查看pg_toast_16436
。
test=# INSERT INTO student (id, name, age) VALUES (2, '{"ipAddress":"10.244.0.133", ......//省略}', 10);test=# INSERT 0 1
可以看到,当这个超过2KB
大小的元组尝试插入表student
时,它触发了TOAST超大属性字段存储技术机制,并使用LZ压缩算法对源数据进行压缩和切分。现在该toast
表中有6
条记录,其中字段chunk_seq
的值从0
开始递增,一直到5
。

字段chunk_data
中的数据已经被压缩过了,如下图所示,其中chunk_id
分配为16445
。

3.2 TOAST数据压缩
PostgreSQL中TOAST机制的实现,内部采用了LZ压缩算法,对将要线外存储的数据进行压缩。对于LZ压缩算法的历史版本演变,以及LZ压缩算法内部原理,在LZ77压缩算法原理剖析一文中有非常详细的讲解。
4. TOAST优点
它可以高效地节省查询时所占用的内存空间。因为TOAST表中的数据在查询时,并不需要去检查其具体的数据值,仅当需要查询出来并展示给用户时候,才会被取出。
PostgreSQL中文社区欢迎广大技术人员投稿
投稿邮箱:press@postgres.cn
1. 变长数据类型
对于任意类型的关系型数据库,比如MySQL
、PostgreSQL
、DB2
或Oracle
等,其内部均支持可变长的数据类型,它们用于存储文本字符串数据,比如PostgreSQL中的CHARACTER VARYING(n)
,简写为VARCHAR(n)
,变长类型,其中n
指明该变长类型的长度限制,也可不填写n
;CHARACTER(n)
,简写为CHAR(n)
,没有参数n
,则等于CHAR(1)
,以及TEXT
、BYTEA
、BLOB
、CBLOB
等。我们知道,关系型数据库是将用户插入的每一条数据按照行(元组、记录)的方式组织到数据表文件中。

在干货 | PostgreSQL数据表文件底层结构布局分析 一文中,详细地分析了数据库中表文件的底层结构布局方式。它通过将表文件划分为若干个大小为8KB
的页面来存储用户插入的每一行数据。但是用户创建的数据表中,极有可能存在大量的文本字段,比如TEXT
、VARCHAR
等。如果用户插入的某条数据其文本数据类型对应的数据超过了页(8KB
)大小,PostgreSQL会如何处理?是选择将该数据存储在表底层的两个页(page
)中,还是采取其他的方案来存储数据?
1.1 元组不能跨行
PostgreSQL官方文档中曾提到:
PostgreSQL数据库不允许元组(行,记录)跨越多个页面(page)存储,所以,它不能直接存储非常大的字段值。对于大字段值,它将被压缩且(或)分解为多个物理行,该技术称为“TOAST”。
所以,如果某条记录中字段值的大小不能存储于一个页中,那么它并不会继续使用其他1
个或n
个页来存储这条数据,而是采用了其他的技术,比如TOAST
或大对象数据存储机制。

注意,TOAST
技术仅适用于那些可变长的数据类型,比如VARCHAR
、TEXT
、JSON
和BYTEA
等,而对于BLOB
、CBLOG
等数据类型,它们将使用另外一种方式(规则)来存储,即上面提到的“大对象存储机制”。在PostgreSQL数据库中,总共有两种方式用于存储大(文本)数据对象,分别是:TOAST
机制和大对象机制。本文主要讲解TOAST
机制,而对于“大对象存储机制”,后面将单独用一篇博文进行讲解。
2. TOAST技术
所谓TOAST(The Oversized-Attribute Storage Techniques
),即超大属性存储技术。它是PostgreSQL的一种机制,用于处理大块数据以适应页面缓冲区。当待插入的数据(元组)超过TOAST_TUPLE_THRESHOLD
(默认2KB
)时候,PostgreSQL将压缩数据,以适应2KB
的缓冲区大小。如果对大列(变长数据类型)项数据压缩没有产生更小的块(即小于2KB
),那么该数据将会被分割成更小的块(chunks
),然后创建一个TOAST表来存储和管理该元组。
/* * If the new tuple is too big for storage or contains already toasted * out-of-line attributes from some other relation, invoke the toaster. */if (relation->rd_rel->relkind != RELKIND_RELATION && relation->rd_rel->relkind != RELKIND_MATVIEW){ /* toast table entries should never be recursively toasted */ Assert(!HeapTupleHasExternal(tup)); return tup;}else if (HeapTupleHasExternal(tup) || tup->t_len > TOAST_TUPLE_THRESHOLD) //元组大小超过TOAST_TUPLE_THRESHOLD(2KB), 则创建TOAST表 return heap_toast_insert_or_update(relation, tup, NULL, options);
2.1 TOAST触发
TOAST机制是自动触发的,当它检测到元组大小超过TOAST_TUPLE_THRESHOLD
时候,就会走TOAST逻辑处理段分支,然后根据用户的选择进行对应的插入或更新操作。当然,如上面所描述,只有一些变长数据类型(如:TEXT
、JSON
、BYTEA
、VARCHAR
等)才会触发TOAST机制,因为将一个不能产生较大字段值的数据类型(比如VARCHAR(1)
、INT
等)关联TOAST机制将得不偿失,其收益将远低于TOAST所带来的开销。TOAST机制背后将伴随着大量的逻辑业务判断处理、数据压缩以及TOAST表系列操作。
2.2 TOAST表存储策略
在PostgreSQL中,所有的数据库列(字段)都有与它们相关联的(默认)存储策略,这里共有四种不同的存储数据策略,它们分别是:PLAIN
、EXTENDED
、EXTERNAL
和MAIN
。各存储策略的解释如下:
(1) PLAIN
不允许压缩和线外存储。对于那些不能TOAST机制的数据类型,默认都是该策略,比如整数类型(INT
,SMALLINT
,BIGINT
)、字符类型(CHAR
)、布尔类型(BOOLEAN
)等等。
(2) EXTENDED
允许线外存储和压缩。这是大多数可以使用TOAST机制的数据类型默认存储策略。它首先尝试压缩,如果这仍不能将数据放入页中,则使用线外存储。
(3) EXTERNAL
允许线外存储,但不允许压缩。该存储策略将使TEXT
、BYTEA
数据类型中的子串操作更快,其代价是牺牲存储空间。
(4) MAIN
允许压缩,但不能线外存储。然而在无法使列足够小以存入页时,它将会执行线外存储。所以它仍然是可以线外存储的,和EXTERNAL
具有相似性。
存储策略PLAIN
、EXTENDED
、EXTERNAL
和MAIN
在PostgreSQL源码中分别被简写为p
、e
、x
和m
。通过storage_name
函数,分别获取个存储策略所对应的名字。
#define TYPSTORAGE_PLAIN 'p' /* type not prepared for toasting */#define TYPSTORAGE_EXTERNAL 'e' /* toastable, don't try to compress */#define TYPSTORAGE_EXTENDED 'x' /* fully toastable */#define TYPSTORAGE_MAIN 'm' /* like 'x' but try to store inline */static const char * storage_name(char c){ switch (c) { case TYPSTORAGE_PLAIN: return "PLAIN"; case TYPSTORAGE_EXTERNAL: return "EXTERNAL"; case TYPSTORAGE_EXTENDED: return "EXTENDED"; case TYPSTORAGE_MAIN: return "MAIN"; default: return "???"; }}
在Linux
终端,使用psql
工具登录postgres
,然后创建数据表student
,该表共有3个字段,分别是:id
(主键, SERIAL
类型)、name
(姓名,VARCHAR
类型)和age
(年龄, INT
类型)。
test=#test=# CREATE TABLE student(id SERIAL PRIMARY KEY, name VARCHAR, age INT);CREATE TABLE
在创建该表的时候,并没有显示地指定各字段的存储策略类型。当该表成功创建后,使用:\d+ student
查看该表信息时,发现PostgreSQL将student
表中的各字段补充了默认对应的存储策略类型。

该表中,字段id
和age
都是数字类型,所以默认是PLAIN
;而字段name
是VARCHAR
,可变长数据类型,所以它支持TOAST策略,则对应的存储策略是EXTENDED
,允许线外存储和压缩。
2.2.1 修改存储策略
可以使用ALTER TABLE table_name ALTER column_name SET STORAGE storage_name;
语句来修改表中各字段的存储策略规则。比如表student
中,字段name
的存储规则是EXTENDED
,现使用ALTER TABLE
语句来更改其存储规则为EXTERNAL
;
test=# ALTER TABLE student ALTER name SET STORAGE EXTERNAL ;ALTER TABLE
更改成功后,再次查看数据表student
中的name
字段存储策略时,已经成功地由EXTENDED
改为了EXTERNAL
。

不过值得注意的是,你无法将一个不支持TOAST机制的数据类型(列)的存储策略修改为其他。比如字段age
是数据类型INT
,其默认存储策略是PLAIN
,若尝试将其修改为MAIN
,则会报错提示。

3. TOAST表结构
如果一张表中存在可变长数据类型,那么该表将有一个与之关联的TOAST表。TOAST是一张单独的表,专用于存储大块数据的列。比如表student
,其中字段name
是可变长数据类型,则该字段存在一个与之对应的TOAST
表,示意图如下:
我们知道,系统表pg_class
中包括当前数据库里的所有数据表,其中每个数据表都在在pg_class
中表示为一个元组,且每个数据表被分配一个OID
作为该表的唯一标识。很显然,我们的TOAST表也在pg_class
中,但是因为不知道TOAST表的名字,所以我们需要借助于表student
,来间接获取。
SELECT relname FROM pg_class WHERE oid = (SELECT reltoastrelid FROM pg_class WHERE relname = 'student');
系统表pg_class
中, 若该元组(某张表)中有变长数据类型,则字段reltoastrelid
将关联该表所对应的TOAST表。因此通过reltoastrelid
字段得到TOAST表的唯一OID
即可获取到TOAST表名。

现在得到了TOAST表的表名是pg_toast_16436
,我们通过TOAST表名反向查询其oid
,得到其oid
是16440
,和表student
中的字段reltoastrelid
的值是相互对应的。
test=# SELECT oid FROM pg_class WHERE relname = 'pg_toast_16436'; oid------- 16440(1 row)
对于TOAST表的命名,其规则是:pg_toast_$(oid)
。其中oid
是该TOAST表所属表的oid
值,比如数据表student
在系统表pg_class
中的oid
是16436
,则与之关联的TOAST的表名字是pg_toast_16436
。
由于toast
表位于pg_toast
模式中,所以当要查询一个toast
表时候,需要使用如下方式:
SELECT * FROM pg_toast.pg_toast_16436;
如下图所示,由于当前表student
为空,所以与之关联的toast
表也为空(0行记录)。

我们使用\d+
命令来查看该toast
( pg_toast.pg_toast_16436
)表中的所有可见字段列表,得到该表共有3个字段,即chunk_id
、chunk_seq
和chunk_data
。
字段chunk_id
,线外存储时候,为该toast
表所分配的oid
;字段chunk_seq
,块序列号,表明该块数据在toast表中的序列位置;字段chunk_data
,存储的实际块数据。
之所以特意强调“可见字段”,那是因为数据表除了用户创建的字段外,还有许多字段是默认隐藏了(没有显示在终端)。比如xmin、cmin、xmax、cmax和ctid等等,关于这些隐藏字段的具体含义,在后面的MVCC(多版本并发控制)博文中进行详细介绍。
在将超过2KB
大小的可变长数据存储到toast
表中的时候,它会被分成最多包含TOAST_MAX_CHUNK_SIZE
数据字节的块(chunk
),每个块(chunk
)都作为一个元组插入到toast
表中。
3.1 小试牛刀
现在我们向数据表student
中插入一条数据,其中name
字段对应的值大小是39KB
,这将远大于2KB
,因此,它将触发TOAST机制,数据使用LZ
压缩算法,并将压缩后的数据分成若干个大小不超过TOAST_MAX_CHUNK_SIZE
的块,然后存储于toast
表中。
这里的可变长数据是一个JSON报文(使用JSON工具压缩成一行)的字符串。仅列出部分信息,其余部分省略掉。
{"ipAddress":"10.244.0.133","protocolType":"HTTP", "dateTime":"2021-08-31T02:52:13Z", ......//省略}
现在向表student
插入该数据,然后再查看pg_toast_16436
。
test=# INSERT INTO student (id, name, age) VALUES (2, '{"ipAddress":"10.244.0.133", ......//省略}', 10);test=# INSERT 0 1
可以看到,当这个超过2KB
大小的元组尝试插入表student
时,它触发了TOAST超大属性字段存储技术机制,并使用LZ压缩算法对源数据进行压缩和切分。现在该toast
表中有6
条记录,其中字段chunk_seq
的值从0
开始递增,一直到5
。

字段chunk_data
中的数据已经被压缩过了,如下图所示,其中chunk_id
分配为16445
。

3.2 TOAST数据压缩
PostgreSQL中TOAST机制的实现,内部采用了LZ压缩算法,对将要线外存储的数据进行压缩。对于LZ压缩算法的历史版本演变,以及LZ压缩算法内部原理,在LZ77压缩算法原理剖析一文中有非常详细的讲解。
4. TOAST优点
它可以高效地节省查询时所占用的内存空间。因为TOAST表中的数据在查询时,并不需要去检查其具体的数据值,仅当需要查询出来并展示给用户时候,才会被取出。
PostgreSQL中文社区欢迎广大技术人员投稿
投稿邮箱:press@postgres.cn




