本节概述 TOAST(The Oversized-Attribute Storage Technique,超尺寸属性存储技术)。
PostgreSQL 使用固定的页面大小(通常 8 kB),并且不允许元组跨越多个页面。因此,无法直接存储非常大的字段 值。为了克服这一限制,大的字段值会被压缩和/或拆分成多个物理行。这一 过程对用户是透明的,对大多数后端代码只有很小的影响。这项技术被亲切地 称为 TOAST(或者 “the best thing since sliced bread”)。 TOAST 基础设施也被用于改进对内存中大数据值的处理。
只有某些数据类型支持 TOAST — 对那些不会产生大
字段值的数据类型,没有必要强加这种开销。要支持
TOAST,数据类型必须具有变长表示(
varlena),其中通常任何被存储值的头四个字节包含
该值的总长度(以字节计,包括其自身)。TOAST 不约束
该数据类型表示的其余部分。被统称为
TOASTed 值的特殊表示通过修改
或重新解释这个初始长度字来工作。因此,支持可
TOAST 数据类型的 C 层面函数在处理可能被
TOAST 的输入值时必须小心:输入值在被
解 TOAST(detoast)之前,可能并不是由一个四字节
长度字加内容构成的。(这通常通过在处理输入值之前调用
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 指针的内容来表明。
无论是行内还是行外压缩数据所使用的压缩技术,都可以通过在
CREATE
TABLE 或 ALTER TABLE 中设置
COMPRESSION 列选项来为每一列选择。没有显式设置的列的
默认行为是在数据插入时参考
default_toast_compression 参数。
如上所述,TOAST 指针 datum 有多种类型。最古老也最
常见的类型是指向存储在一个TOAST
表中的行外数据的指针,该 TOAST 表与包含
TOAST 指针 datum 本身的表相互独立但又彼此关联。
当要存储到磁盘上的元组大到无法按原样存储时,TOAST
管理代码(位于 access/common/toast_internals.c)
就会创建这些on-disk 指针 datum。
更多细节见 Section 66.2.1。
另外,TOAST 指针 datum 也可以包含一个指向出现在内存
中其他位置的行外数据的指针。这样的 datum 必然是短寿命的,也绝不会出现在
磁盘上,但它们对于避免大数据值的复制和冗余处理非常有用。
更多细节见 Section 66.2.2。
如果表的任何列是可 TOAST 的,该表就会有一个关联的
TOAST 表,其 OID 存储在表的
pg_class.reltoastrelid
条目中。磁盘上的 TOASTed 值保存在
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_TUPLE_THRESHOLD 字节(通常 2 kB)时,
TOAST 管理代码才会被触发。
TOAST 代码会压缩和/或把字段值移到行外,直到行值短于
TOAST_TUPLE_TARGET 字节(同样通常为 2 kB,可调整),
或者无法再获得更多收益。在 UPDATE 操作期间,未变化字段的值通常按原样
保留;因此,如果没有任何行外值发生变化,对包含行外值的行做 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 指针又被进一步划分为 读写和只读指针。 无论哪种方式,被指向的表示都是相同的,但接收读写指针的函数被允许就地 修改被引用的值,而接收只读指针的函数则不允许;如果它想创建该值的一个 修改版本,必须先创建一个副本。这一区别以及一些相关约定使得在查询执行 期间避免对展开式值的不必要复制成为可能。
对于所有类型的内存中 TOAST 指针, TOAST 管理代码确保这样的指针 datum 不会意外地被存储 到磁盘上。内存中的 TOAST 指针在存储之前会被自动 展开为普通的行内 varlena 值 — 然后如果包含它的元组因此太大, 可能会再被转换为磁盘上的 TOAST 指针。