支持的版本:当前 (17) / 16 / 15 / 14 / 13
开发版本:devel
不支持的版本:12 / 11 / 10 / 9.6 / 9.5 / 9.4 / 9.3 / 9.2 / 9.1 / 9.0 / 8.4 / 8.3 / 8.2 / 8.1 / 8.0

65.2. TOAST #

本节概述了TOAST(超大属性存储技术)。

PostgreSQL 使用固定的页面大小(通常为 8 KB),并且不允许元组跨越多个页面。因此,不可能直接存储非常大的字段值。为了克服这个限制,大的字段值会被压缩和/或分成多个物理行。这对用户是透明的,对大多数后端代码只有很小的影响。这项技术被亲切地称为TOAST(或者 自切片面包以来最好的东西)。TOAST基础设施也用于改进内存中大型数据值的处理。

只有某些数据类型支持TOAST— 没有必要对不能产生大型字段值的数据类型施加开销。为了支持TOAST,数据类型必须具有可变长度 (varlena) 表示形式,其中,通常,任何存储值的第一个四字节字包含该值的总字节长度(包括它自己)。TOAST不限制数据类型表示形式的其余部分。统称为 TOASTed 值 的特殊表示形式通过修改或重新解释此初始长度字来工作。因此,支持TOAST的数据类型的 C 级函数必须小心处理可能TOASTed 的输入值:输入可能实际上不包含四字节长度字和内容,直到它被 detoasted。(这通常是通过在对输入值执行任何操作之前调用 PG_DETOAST_DATUM 来完成的,但在某些情况下,可以采用更有效的方法。有关更多详细信息,请参阅 第 36.13.1 节。)

TOAST会占用 varlena 长度字的两个位(大端机器上的高位,小端机器上的低位),从而将TOASTed 数据类型的任何值的逻辑大小限制为 1 GB (230 - 1 个字节)。当两个位都为零时,该值是数据类型的普通未TOASTed 值,并且长度字的剩余位给出总数据大小(包括长度字),以字节为单位。当设置最高位或最低位时,该值只有一个单字节标头,而不是普通的四字节标头,并且该字节的剩余位给出总数据大小(包括长度字节),以字节为单位。这种替代方案支持对小于 127 个字节的值进行空间高效的存储,同时仍然允许数据类型在需要时增长到 1 GB。具有单字节标头的值不会对齐到任何特定边界,而具有四字节标头的值至少对齐到四字节边界;与短值相比,这种省略对齐填充提供了额外的空间节省,这是非常显著的。作为特殊情况,如果单字节标头的剩余位都为零(对于自包含长度而言这是不可能的),则该值是指向离线数据的指针,如下所述,有几种可能的替代方案。这种 TOAST 指针的类型和大小由存储在数据第二字节中的代码确定。最后,当最高位或最低位清除但相邻位被设置时,数据的内容已被压缩,并且必须在使用前解压缩。在这种情况下,四字节长度字的剩余位给出压缩数据的总大小,而不是原始数据。请注意,压缩也可能用于离线数据,但 varlena 标头不会指示是否发生了压缩 —TOAST指针的内容会指示这一点。

可以使用 CREATE TABLEALTER TABLE 中设置 COMPRESSION 列选项来为每个列选择用于在线或离线压缩数据的压缩技术。对于没有显式设置的列,默认值是在插入数据时查询 default_toast_compression 参数。

如前所述,有多种类型的TOAST指针数据。最古老和最常见的类型是指向存储在 TOAST 中的离线数据的指针,该表与包含TOAST指针数据本身的表是分开的,但与之关联。当要存储在磁盘上的元组太大而无法按原样存储时,这些 磁盘上 指针数据由TOAST管理代码(在 access/common/toast_internals.c 中)创建。更多详细信息请参阅 第 65.2.1 节。或者,TOAST指针数据可以包含指向内存中其他位置出现的离线数据的指针。此类数据必然是短暂的,永远不会出现在磁盘上,但它们对于避免复制和大型数据值的冗余处理非常有用。更多详细信息请参阅 第 65.2.2 节

65.2.1. 离线磁盘 TOAST 存储 #

如果表的任何列是TOASTable,则该表将有一个关联的TOAST表,其 OID 存储在表的 pg_class.reltoastrelid 条目中。磁盘上的TOASTed 值保存在TOAST表中,如下面更详细的描述。

离线值被分成最多 TOAST_MAX_CHUNK_SIZE 字节的块(如果使用压缩,则在压缩后)(默认情况下,选择此值以使四个块行适合一个页面,使其约为 2000 字节)。每个块都作为单独的行存储在TOAST属于拥有表的表中。每个TOAST表都具有列 chunk_id(一个 OID,用于标识特定的TOASTed 值)、chunk_seq(其值内块的序列号)和 chunk_data(块的实际数据)。chunk_idchunk_seq 上的唯一索引提供了对值的快速检索。因此,表示磁盘上离线TOASTed 值的指针数据需要存储要在其中查找的TOAST表的 OID 和特定值的 OID(其 chunk_id)。为了方便起见,指针数据还存储逻辑数据大小(原始未压缩数据长度)、物理存储大小(如果应用了压缩则不同)以及使用的压缩方法(如果有)。考虑到 varlena 标头字节,磁盘上TOAST指针数据的总大小为 18 个字节,而不管表示的值的实际大小如何。

仅当要存储在表中的行值大于 TOAST_TUPLE_THRESHOLD 字节(通常为 2 KB)时,才会触发TOAST管理代码。当TOAST代码将压缩和/或将字段值移出在线存储,直到行值短于 TOAST_TUPLE_TARGET 字节(通常也为 2 KB,可调整)或无法获得更多收益为止。在 UPDATE 操作期间,未更改字段的值通常保持原样;因此,如果离线值都没有更改,则对具有离线值的行进行 UPDATE 操作不会产生任何TOAST开销。

仅当要存储在表中的行值大于 TOAST_TUPLE_THRESHOLD 字节(通常为 2 KB)时,才会触发TOAST管理代码识别用于在磁盘上存储TOASTable 列的四种不同策略。

  • PLAIN 阻止压缩或离线存储。这是非TOASTable 数据类型列的唯一可能策略。

  • EXTENDED 允许压缩和离线存储。这是大多数TOAST类型的可变数据类型。将首先尝试压缩,如果行仍然太大,则进行行外存储。

  • EXTERNAL 允许行外存储,但不允许压缩。使用 EXTERNAL 会使对宽 textbytea 列的子字符串操作更快(代价是增加了存储空间),因为这些操作经过优化,仅在未压缩时才获取行外值的所需部分。

  • MAIN 允许压缩,但不允许行外存储。(实际上,对于此类列仍然会执行行外存储,但这仅作为最后手段,当没有其他方法使行足够小以适合页面时才会这样做。)

每个TOAST类型的可变数据类型指定该数据类型列的默认策略,但是可以使用 ALTER TABLE ... SET STORAGE 来更改给定表列的策略。

可以使用 ALTER TABLE ... SET (toast_tuple_target = N) 为每个表调整 TOAST_TUPLE_TARGET

与允许行值跨页的更直接的方法相比,此方案具有许多优势。假设查询通常通过与相对较小的键值进行比较来限定,则执行器的大部分工作将使用主行条目完成。的较大值TOAST属性仅在结果集发送到客户端时才会被拉出(如果被选中)。因此,主表要小得多,并且与没有任何行外存储的情况相比,更多行适合在共享缓冲区缓存中。排序集也缩小了,并且排序将更频繁地完全在内存中完成。一项小测试表明,包含典型 HTML 页面及其 URL 的表存储的原始数据大小约为一半,包括TOAST表,并且主表仅包含大约 10% 的全部数据(URL 和一些小型 HTML 页面)。与未执行TOAST比较表相比,运行时没有差异,在该表中,所有 HTML 页面都被缩减到 7 kB 以适合。

65.2.2. 行外、内存 TOAST 存储 #

TOAST指针可以指向不在磁盘上的数据,而是当前服务器进程内存中的其他位置。显然,此类指针不能长期存在,但它们仍然有用。目前有两种子情况:指向间接数据的指针和指向扩展数据的指针。

间接TOAST指针只是指向内存中某处存储的非间接 varlena 值。这种情况最初只是作为概念验证而创建的,但目前在逻辑解码期间使用,以避免可能必须创建超过 1 GB 的物理元组(因为将所有行外字段值拉入元组可能会这样做)。这种情况的用途有限,因为指针数据的创建者完全负责确保引用的数据在指针可能存在的时间内存在,并且没有基础设施可以帮助实现这一点。

扩展TOAST指针对于磁盘表示不特别适合计算目的的复杂数据类型很有用。例如,PostgreSQL 数组的标准 varlena 表示形式包括维度信息、如果有任何空元素,则包括空位图,然后按顺序排列所有元素的值。当元素类型本身是可变长度时,查找第 N 个元素的唯一方法是扫描所有前面的元素。此表示形式适合磁盘存储,因为它紧凑,但是对于数组的计算,最好使用展开解构表示形式,其中所有元素的起始位置都已确定。该TOAST指针机制通过允许按引用传递的 Datum 指向标准 varlena 值(磁盘表示形式)或TOAST指向内存中某处扩展表示形式的指针来支持此需求。此扩展表示形式的详细信息取决于数据类型,尽管它必须具有标准标头并满足 src/include/utils/expandeddatum.h 中给出的其他 API 要求。使用数据类型的 C 级函数可以选择处理任一表示形式。不了解扩展表示形式,而只是对其输入应用 PG_DETOAST_DATUM 的函数将自动接收传统的 varlena 表示形式;因此,可以逐步引入对扩展表示形式的支持,一次一个函数。

TOAST指向扩展值的指针进一步细分为读写指针和只读指针。无论哪种方式,指向的表示形式都是相同的,但是接收读写指针的函数可以就地修改引用的值,而接收只读指针的函数则不得这样做;如果它想创建修改后的值版本,则必须首先创建副本。这种区别和一些相关的约定使得在查询执行期间可以避免不必要地复制扩展值。

对于所有类型的内存TOAST指针,TOAST管理代码确保此类指针数据不会意外存储在磁盘上。内存中的TOAST指针在存储之前会自动扩展为正常的内联 varlena 值,如果包含的元组太大,则可能会转换为磁盘上的TOAST指针。

提交更正

如果您发现文档中的任何内容不正确,与您使用特定功能的经验不符或需要进一步澄清,请使用此表格报告文档问题。