本节概述超长属性存储技术(TOAST, The Oversized-Attribute Storage Technique)。
PostgreSQL 使用固定页面大小(通常为 8 kB),并且不允许 元组跨越多个页面。因此,无法直接存储非常大的字段值。为克服这一限制,大字段值会被 压缩并且/或者拆分成多个物理行。这个过程对用户是透明的,而且对大部分后端代码的影响 很小。这项技术被亲切地称为 TOAST(或者 “自切片面包问世以来最棒的东西”)。TOAST 基础设施也被用于改进内存中大型数据值的处理。
只有某些数据类型支持 TOAST,因为没有必要让那些不可能产生大字段值的 数据类型承担这部分额外开销。要支持 TOAST,数据类型必须具有变长 (varlena)表示形式,在通常情况下,任何已存储值的第一个四字节字 都包含该值以字节计的总长度(包括其自身)。TOAST 对该数据类型表示形式的 其余部分没有约束。统称为 TOAST 化值 的特殊表示形式, 是通过修改或重新解释这个初始长度字来工作的。因此,支持可 TOAST 数据类型的 C 级函数,必须小心处理可能已经 TOAST 化的输入值:在被 去 TOAST 化之前,一个输入值未必真正由四字节长度字和内容组成。 (通常是在对输入值做任何处理之前调用 PG_DETOAST_DATUM, 但在某些情况下也可以采用更高效的方法。详见 Section 36.13.1。)
TOAST 会占用 varlena 长度字中的两个位 (在大端机器上是最高位,在小端机器上是最低位),从而把任何可 TOAST 数据类型值的逻辑大小限制为 1 GB (230 - 1 字节)。当这两个位都为零时, 该值就是这种数据类型的普通、未经 TOAST 处理的值,长度字的其余位 给出该 datum 的总大小(包括长度字),以字节计。当最高位或最低位被置位时, 该值使用的不是通常的四字节头部,而是单字节头部;该字节的其余位给出 datum 的总大小(包括长度字节),以字节计。这样既能高效存储短于 127 字节的值, 又仍然允许数据类型在需要时增长到 1 GB。带单字节头部的值不按任何特定边界对齐, 而带四字节头部的值至少按四字节边界对齐;省去这部分对齐填充后,相对于短值而言, 可以显著节省空间。作为特殊情况,如果单字节头部的其余位全为零 (对于自包含长度而言这是不可能的),则该值是一个指向线外数据的指针, 它可能有下面将描述的几种形式。这样一种 TOAST 指针 的类型和大小, 由 datum 第二个字节中存储的编码决定。最后,当最高位或最低位为零、但相邻的一位被置位时, 该 datum 的内容已被压缩,使用前必须先解压。在这种情况下,四字节长度字的其余位给出的 是压缩后 datum 的总大小,而不是原始数据的大小。请注意,线外数据也可能被压缩, 但 varlena 头部不会告诉你是否发生了压缩,这一点要由 TOAST 指针的内容来说明。
无论是线内压缩数据还是线外压缩数据,其所用的压缩技术都可以通过设置 COMPRESSION 列选项来按列选择,该选项可在 CREATE TABLE 或 ALTER TABLE 中指定。 对未显式设置的列,会在插入数据时参考参数 default_toast_compression 的值。
如前所述,TOAST 指针 datum 有多种类型。最早、也最常见的一种, 是指向存储在 TOAST 表中的线外数据的指针, 该表与包含该 TOAST 指针 datum 本身的表相关联,但与之分离存储。 当一个要存储到磁盘上的元组过大而无法原样存储时, 这些磁盘上的指针 datum 会由 TOAST 管理代码(位于 access/common/toast_internals.c)创建。更多细节见 Section 65.2.1。另一种情况是, TOAST 指针 datum 可以包含指向内存中其他位置的线外数据的指针。 这类 datum 必然是短命的,永远不会出现在磁盘上,但它们对于避免大型数据值的复制和 冗余处理非常有用。更多细节见 Section 65.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 化值的指针 datum,需要存储要查找的 TOAST 表的 OID,以及该特定值的 OID(即其 chunk_id)。为方便起见,指针 datum 还会存储逻辑 datum 大小 (原始未压缩数据长度)、物理存储大小(如果应用了压缩,这两者会不同),以及所用的 压缩方法(如果有)。再加上 varlena 头部字节,磁盘上的 TOAST 指针 datum 的总大小因此恒为 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 页面)。与未进行 TOAST 处理的对照表相比,运行时没有差异;在那个对照表中, 所有 HTML 页面都被裁剪到 7 kB 以内以便放得下。
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 指针。
如果您发现文档中有不正确的内容、与您使用特定功能的经验不符或需要进一步说明,请使用此表单来报告文档问题。