↑↓ 选择 ↵ 打开 ⌫ 改范围 完整检索页

pgsql.cc 提供对 postgresql.org 官网内容的中文翻译,由 Pigsty 团队维护。

百科 / 存储结构 / 物理结构

TOAST

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

当前查看 PostgreSQL 18.6。

说明

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

定义范围
同版本核心物理存储文档

手册定义

66.2. TOAST

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

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

只有部分数据类型支持 TOAST;无需让不会产生大字段值的类型承担额外开销。要支持 TOAST,类型必须采用变长(varlena)表示,通常其存储值的第一个四字节字记录总字节长度,包括该长度字本身。TOAST 不限制类型表示的其余部分。统称为 TOAST 值的特殊表示通过修改或重新解释这个首部长度字工作。因此,支持 TOAST 类型的 C 层函数必须谨慎处理可能经过 TOAST 处理的输入:在解除 TOAST 表示前,输入可能并不由四字节长度字加实际内容组成。通常应在任何处理之前调用 PG_DETOAST_DATUM,但某些情形可采用更高效的方法,详情参见第 36.13.1 节。

TOAST 占用 varlena 长度字的两个位:大端机器使用高位,小端机器使用低位,因此支持 TOAST 的类型中,任何值的逻辑大小都限于 1 GB(2^30 - 1 字节)。两位均为零时,该值是普通的未经过 TOAST 处理的值,长度字的其余位表示包括长度字本身在内的总字节数。最高位或最低位置位时,该值使用单字节首部而非通常的四字节首部,该字节的其余位表示包括长度字节在内的总大小。这使小于 127 字节的值能够紧凑存储,同时仍允许类型在需要时增长到 1 GB。单字节首部的值无需按特定边界对齐;四字节首部的值则至少按四字节边界对齐。省略对齐填充还能节省空间,对于短值尤其明显。一个特殊情况是:如果单字节首部的其余位全为零——这不可能是包含自身的长度——该值就是指向行外数据的指针,具体形式见下文。指针的类型和大小由数据第二个字节中的代码决定。最后,如果最高位或最低位清零,而相邻位置位,数据内容就已压缩,使用前必须解压。此时四字节长度字的其余位表示压缩后数据的总大小,而非原始数据大小。行外数据也可能压缩,但 varlena 首部不表明这一点,需查看 TOAST 指针的内容。

无论是行内压缩数据还是行外压缩数据,其所用的压缩技术都可以通过设置 COMPRESSION 列选项来按列选择,该选项可在 CREATE TABLE 或 ALTER TABLE 中指定。对未显式设置的列,会在插入数据时参考参数default_toast_compression的值。

如前所述,TOAST 指针 datum 有多种类型。最早、也最常见的一种,是指向存储在 TOAST 表中的行外数据的指针,该表与包含该 TOAST 指针 datum 本身的表相关联,但与之分离存储。当一个要存储到磁盘上的元组过大而无法原样存储时,这些磁盘上的指针 datum 会由 TOAST 管理代码(位于 access/common/toast_internals.c)创建。更多细节见第 66.2.1 节。另一种情况是,TOAST 指针 datum 可以包含指向内存中其他位置的行外数据的指针。这类 datum 必然是短命的,永远不会出现在磁盘上,但它们对于避免大型数据值的复制和冗余处理非常有用。更多细节见第 66.2.2 节。

如果表中有列支持 TOAST,表就会关联一个 TOAST 表,其 OID 存储在 pg_class.reltoastrelid 中。磁盘上的 TOAST 值存放在该 TOAST 表中,详情见下文。

行外值在压缩后(如果使用压缩)拆成最多 TOAST_MAX_CHUNK_SIZE 字节的块。默认选择此值,使一个页面能装下四个块行,因此约为 2000 字节。每块作为独立的一行存入所属表的 TOAST 表。每个 TOAST 表都有 chunk_id(标识特定 TOAST 值的 OID)、chunk_seq(该块在值中的序号)和 chunk_data(实际块数据)三列。chunk_id 与 chunk_seq 上的唯一索引用于快速检索值。因此,表示磁盘行外 TOAST 值的指针需要存储 TOAST 表的 OID,以及特定值的 OID,即 chunk_id。为方便使用,指针还存储逻辑数据大小(原始未压缩长度)和物理存储大小(使用压缩时与原始大小不同),以及所使用的压缩方法(如果有)。算上 varlena 首部字节,磁盘 TOAST 指针的总大小为 18 字节,与其表示的值的实际大小无关。

TOAST 管理代码只有在要存入表中的行值宽于 TOAST_TUPLE_THRESHOLD 字节(通常为 2 kB)时才会被触发。TOAST 代码会压缩并且/或者把字段值移到行外,直到该行值短于 TOAST_TUPLE_TARGET 字节(通常也为 2 kB,但可调整),或者已经无法再获得更多收益。在 UPDATE 操作中,未更改字段的值通常会原样保留;因此,如果更新一行时其行外值都没有变化,便不会产生 TOAST 成本。

TOAST 管理代码支持四种将可 TOAST 列存储到磁盘的策略:

  • PLAIN 禁止压缩和行外存储。对于不支持 TOAST 的数据类型,这是唯一可用的列存储策略。
  • EXTENDED 同时允许压缩和行外存储,是大多数支持 TOAST 的数据类型的默认策略。先尝试压缩;如果行仍然过大,再使用行外存储。
  • EXTERNAL 允许行外存储,但不允许压缩。使用 EXTERNAL 会让宽 text 和 bytea 列上的子串操作更快(代价是占用更多存储空间),因为在值未压缩时,这些操作经过优化,只需提取行外值中所需的部分。

  • MAIN 允许压缩,但不允许行外存储。(实际上,对于这类列,仍然可能进行行外存储,但只有在别无他法、必须这样做才能让行足够小以放入页面时,才会把它作为最后手段。)

每种支持 TOAST 的数据类型都为其列指定默认策略,但具体表列的策略可通过 ALTER TABLE ... SET STORAGE 修改。

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

与让行值跨页等更直接的做法相比,此方案有多项优势。假设查询通常按较小键值进行比较筛选,执行器的大部分工作就可只使用主行数据。TOAST 属性的大值仅在将结果发送给客户端时才读取,而且前提是查询确实选择了这些值。因而主表显著缩小,共享缓冲区可容纳更多行;排序数据集也会缩小,更容易完全在内存中排序。原文中的一个小测试表明:包含典型 HTML 页面及 URL 的表连同 TOAST 表,占用约为原始数据的一半;主表只占总数据约 10%,包括 URL 和少量小 HTML 页面。与将所有 HTML 页面截短至 7 kB 以便装入行内、未使用 TOAST 的对照表相比,运行时间没有差异。

TOAST 指针可以指向不在磁盘上、而位于当前服务器进程内存中其他位置的数据。这类指针显然不可能长期存在,但仍然很有用。目前有两个子情况:指向间接数据的指针,以及指向展开数据的指针。

间接 TOAST 指针只是简单地指向某处内存中存放的一个非间接 varlena 值。这一情况最初只是作为概念验证而创建,但目前在逻辑解码期间会用到它,以避免可能不得不创建超过 1 GB 的物理元组(把所有行外字段值都拉入元组就可能导致这种情况)。这种机制的用途有限,因为创建该指针 datum 的一方必须完全负责确保被引用数据在指针可能存在的整个期间都保持有效,而且系统并没有任何基础设施来帮助做到这一点。

展开的 TOAST 指针对那些磁盘表示形式并不特别适合计算用途的复杂数据类型很有用。以标准的 PostgreSQL 数组 varlena 表示为例,它包含维度信息、一个空值位图(如果存在空值元素),然后依次保存所有元素的值。当元素类型本身也是变长时,定位第 N 个元素的唯一办法就是扫描它前面的所有元素。这种表示形式因其紧凑性而适合磁盘存储,但对于数组计算而言,一种“展开”或“解构”的表示会更好,因为其中所有元素的起始位置都已识别出来。TOAST 指针机制通过允许按引用传递的 Datum 指向标准 varlena 值(即磁盘表示)或者指向内存中某处展开表示的 TOAST 指针,来满足这一需要。这种展开表示的具体细节由数据类型自行决定,不过它必须具有标准头部,并满足 src/include/utils/expandeddatum.h 中给出的其他 API 要求。处理该数据类型的 C 级函数可以选择支持任一种表示。不知道展开表示、但只是对输入应用 PG_DETOAST_DATUM 的函数,会自动获得传统的 varlena 表示;因此,对展开表示的支持可以逐步引入,一次增加一个函数即可。

指向展开值的 TOAST 指针还可进一步分为可读写(read-write)和只读(read-only)指针。无论哪种方式,被指向的表示是相同的;但收到可读写指针的函数可以原地修改所引用的值,而收到只读指针的函数则不可以,如果它想生成该值的修改版本,必须先创建一个副本。这种区分以及一些相关约定,使得在查询执行期间可以避免不必要的展开值复制。

对于所有类型的内存中 TOAST 指针,TOAST 管理代码都会确保这类指针 datum 不会意外地被存储到磁盘上。内存中的 TOAST 指针在存储前会自动展开为普通的行内 varlena 值,如果这样会使承载它的元组过大,还可能进一步转换为磁盘上的 TOAST 指针。

相关条目

文档与源码

来源构建
版本
18.6
构建
https://ftp.postgresql.org/pub/source/v18.6/postgresql-18.6.tar.bz2
来源指纹
555610c24d53e4316da5b7d3fc25c279d96856d5e0e23ee308c328c5fa881d9f

版本比较

PostgreSQL 17 → 18: 无变化。

比较已记录的接口与属性,排除来源指纹和构建元数据。某个样本中没有记录,不能据此判断实际引入或移除的版本。

相关条目

导出 JSON · 返回存储结构 · 收录范围为 PostgreSQL 10 至 20;最早采样版本不一定是实际引入版本。