Redrock Postgres 搜索 英文
版本: 9.3 / 9.4 / 9.5 / 9.6 / 10 / 11 / 12 / 13 / 14 / 15 / 16

73.2. TOAST #

73.2.1. 离线、磁盘上 TOAST 存储
73.2.2. 离线、内存中 TOAST 存储

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

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

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

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

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

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

73.2.1. 离线、磁盘上 TOAST 存储 #

如果表中的任何列都是 TOAST 兼容的,则该表将具有一个关联的 TOAST 表,其 OID 存储在表的 pg_class.reltoastrelid 条目中。磁盘上 TOAST 后的值保存在 TOAST 表中,如下文所述。

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

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

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

  • PLAIN 阻止压缩或超出范围的存储。这是非可 TOAST 数据类型列的唯一可能策略。

  • EXTENDED 允许压缩和超出范围的存储。这是大多数可 TOAST 数据类型的默认值。如果行仍然太大,则会首先尝试压缩,然后尝试超出范围的存储。

  • EXTERNAL 允许超出范围的存储,但不允许压缩。使用 EXTERNAL 会使在宽 textbytea 列上进行子字符串操作变得更快(以增加存储空间为代价),因为当超出范围的值未压缩时,这些操作经过优化,仅获取所需部分。

  • MAIN 允许压缩,但不允许超出范围的存储。(实际上,对于此类列仍然会执行超出范围的存储,但仅在别无他法时作为最后手段,以使行足够小以适合一页。)

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

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

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

73.2.2. 非行内、内存中 TOAST 存储 #

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

间接TOAST指针仅仅指向存储在内存中的非间接 varlena 值。此案例最初只是作为概念证明而创建,但目前在逻辑解码期间使用,以避免可能创建超过 1 GB 的物理元组(因为将所有非行内字段值拉入元组可能会这样做)。此案例的用途有限,因为指针数据创建者完全负责在指针存在期间引用数据,并且没有基础设施来帮助完成此操作。

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

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

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