66.2. TOAST

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

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

仅某些数据类型支持 TOAST-无需将开销强加于不能产生较大字段值的数据类型。为了支持 TOAST,数据类型必须具有可变长度(* varlena )表示形式,在该表示形式中,通常任何存储值的前四个字节字都包含值的总长度(以字节为单位)(包括其自身)。 TOAST 不会约束其余数据类型的表示。统称为 TOASTed values 的特殊表示形式是通过修改或重新解释此初始长度单词来工作的。因此,支持可 TOAST 数据类型的 C 级函数必须谨慎对待它们如何处理可能的 TOASTedImporting 值:Importing 可能在被 detoasted 之后才 true 由四字节长的字和内容组成。 (这通常是通过在对 Importing 值进行任何操作之前调用PG_DETOAST_DATUM来完成的,但是在某些情况下,可以使用更有效的方法.有关更多详细信息,请参见Section 37.11.1。)

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

如前所述,有多种类型的 TOAST 指针基准。最古老和最常见的类型是指向存储在* TOAST 表中的脱机数据的指针,该表与包含 TOAST 指针数据本身的表独立但相关联。当要存储在磁盘上的 Tuples 太大而不能按原样存储时,这些 on-disk *指针基准由 TOASTManagement 代码(在access/heap/tuptoaster.c中)创建。更多详细信息显示在Section 66.2.1中。或者,TOAST 指针数据可以包含指向内存中其他位置出现的离线数据的指针。这样的数据必然是短暂的,并且永远不会出现在磁盘上,但是对于避免复制和冗余处理大数据值非常有用。更多详细信息显示在Section 66.2.2中。

用于行内或行外压缩数据的压缩技术是 LZ 系列压缩技术中相当简单且非常快速的成员。有关详情,请参见src/common/pg_lzcompress.c

66 .2.1. 离线磁盘 TOAST 存储

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

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

仅当要存储在表中的行值大于TOAST_TUPLE_THRESHOLD字节(通常为 2 kB)时,才会触发 TOASTManagement 代码。 TOAST 代码将脱机压缩和/或移动字段值,直到行值小于TOAST_TUPLE_TARGET字节(通常也为 2 kB)或无法获得更多收益为止。在 UPDATE 操作期间,未更改字段的值通常按原样保留;因此,如果行值没有变化,则使用行值更新的行不会产生 TOAST 成本。

TOASTManagement 代码识别出四种在磁盘上存储 TOAST-able 列的策略:

  • PLAIN防止压缩或离线存储;此外,它还禁止对 varlena 类型使用单字节 Headers。对于不可 TOAST 数据类型的列,这是唯一可行的策略。

  • EXTENDED允许压缩和离线存储。这是大多数适用于 TOAST 的数据类型的默认设置。如果行仍然太大,将首先尝试压缩,然后进行离线存储。

  • EXTERNAL允许离线存储,但不能压缩。 EXTERNAL的使用将使textbytea宽列上的子字符串操作更快(以增加存储空间为代价),因为优化了这些操作以仅在未压缩时获取离线值的必需部分。

  • MAIN允许压缩,但不能离线存储。 (实际上,仍将对此类列执行离线存储,但只有在没有其他方法使行足够小以适合页面时,才作为最后的选择.)

每种 TOAST-able 数据类型都为该数据类型的列指定了默认策略,但是可以使用更改表...设置存储更改给定表列的策略。

与更直接的方法(例如允许行值跨越页面)相比,此方案具有许多优点。假设查询通常通过与相对较小的键值进行比较来限定,执行程序的大部分工作将使用主行条目来完成。仅在将结果集发送到 Client 端时,才会抽出 TOASTed 属性的大值(如果完全选中)。因此,与没有任何离线存储的情况相比,主表要小得多,并且更多行适合共享缓冲区高速缓存。排序集也会缩小,排序通常会完全在内存中完成。一项小测试显示,包含典型 HTML 页面及其 URL 的表以包括 TOAST 表在内的原始数据大小的大约一半存储,并且主表仅包含全部数据(URL 和一些小的 HTML)的 10%页)。与未修改的比较表相比,运行时间没有差异,在未修改的比较表中,所有 HTML 页面均缩减至 7 kB 以适合。

66 .2.2. 离线,内存中的 TOAST 存储

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

间接 TOAST 指针仅指向存储在内存中某个位置的非间接 varlena 值。这种情况最初只是作为概念证明而创建的,但是目前在逻辑解码期间使用它是为了避免可能必须创建超过 1 GB 的物理 Tuples(因为将所有离线字段值拉入 Tuples 可能会这样做)。这种情况的使用是有限的,因为指针数据的创建者完全负责只要指针可以存在,被引用的数据就可以生存,并且没有基础结构可以帮助它。

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

扩展值的 TOAST 指针进一步细分为* read-write read-only *指针。两种方法的指向都相同,但是接收读写指针的函数被允许就地修改引用的值,而接收只读指针的函数则不能。如果要对该值进行修改,则必须首先创建一个副本。这种区别和一些相关的约定可以避免在查询执行期间不必要地复制扩展值。

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