2025年9月25日: PostgreSQL 18 发布!
支持的版本:当前 (18) / 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

66.2. TOAST #

本节概述了TOAST(超大属性存储技术,The Oversized-Attribute Storage Technique)。

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

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

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

用于行内或行外压缩数据的压缩技术可以通过在 CREATE TABLEALTER TABLE 中设置 COMPRESSION 列选项来为每个列选择。对于没有明确设置的列,默认是在插入数据时查阅 default_toast_compression 参数。

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

66.2.1. 行外、磁盘上的 TOAST 存储 #

如果表的任何列是 TOASTTOAST-able,则该表将具有一个关联的 TOASTTOAST表,其 OID 存储在表的 pg_class.reltoastrelid 条目中。磁盘上的 TOASTTOASTed 值保存在 TOASTTOAST表中,如下所述。

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

TOASTTOAST 管理代码仅在要存储在表中的行值宽度超过 TOAST_TUPLE_THRESHOLD 字节(通常为 2 kB)时才触发。TOASTTOAST代码将压缩和/或将字段值移到行外,直到行值短于 TOAST_TUPLE_TARGET 字节(通常也为 2 kB,可调整)或无法再获得更多收益。在 UPDATE 操作期间,未更改字段的值通常按原样保留;因此,如果行中没有行外值发生更改,则对具有行外值的行的 UPDATE 不会产生 TOASTTOAST成本。

TOASTTOAST 管理代码识别出四种不同的策略,用于在磁盘上存储 TOASTTOAST-able 列:

  • PLAIN 阻止压缩和行外存储。这是非 TOASTTOAST-able 数据类型列的唯一可能策略。

  • EXTENDED 允许压缩和行外存储。这是大多数 TOASTTOAST-able 数据类型的默认值。将首先尝试压缩,如果行仍然太大,则进行行外存储。

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

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

传递给 -c 的每个TOAST每个 TOAST-able 数据类型都为其数据类型的列指定了默认策略,但给定表列的策略可以通过 ALTER TABLE ... SET STORAGE 进行更改。

TOAST_TUPLE_TARGET 可以通过 ALTER TABLE ... SET (toast_tuple_target = N) 为每个表进行调整。

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

66.2.2. 行外、内存中的 TOAST 存储 #

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

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

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

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

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

提交更正

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