54.2. TOAST #
本节概述TOAST(超长属性存储技术)。
PostgreSQL 使用固定页面大小(通常为 8 kB),并且不允许元组跨越多个页面。因此,无法直接存储非常大的字段值。为克服这一限制,大字段值会被压缩并且/或者拆分成多个物理行。这个过程对用户是透明的,而且对大部分后端代码的影响很小。这项技术被亲切地称为 TOAST(或者“自切片面包问世以来最棒的东西”)。
只有某些数据类型支持 TOAST,因为没有必要让那些不可能产生大字段值的数据类型承担这部分额外开销。要支持 TOAST,数据类型必须具有变长(varlena)表示形式,其中任何已存储值的第一个 32 位字都包含该值以字节计的总长度(包括其自身)。TOAST 不约束表示形式的其余部分。所有支持可 TOAST 数据类型的 C 级函数都必须小心处理已 TOAST 化的输入值。(通常是在对输入值做任何处理之前调用 PG_DETOAST_DATUM,但在某些情况下也可以采用更高效的方法。)
TOAST 会占用 varlena 长度字中的两个位(在大端机器上是最高位,在小端机器上是最低位),从而把任何可 TOAST 数据类型值的逻辑大小限制为 1 GB(230 - 1 字节)。当这两个位都为零时,该值就是这种数据类型的普通、未经 TOAST 处理的值,长度字的其余位给出该 datum 的总大小(包括长度字),以字节计。当最高位或最低位被置位时,该值使用的不是通常的四字节头部,而是单字节头部;该字节的其余位给出 datum 的总大小(包括长度字节),以字节计。作为特殊情况,如果其余位全为零(对于自包含长度而言这是不可能的),则该值是指向存储在单独 TOAST 表中的线外数据的指针。(TOAST 指针的大小由 datum 的第二个字节给出。)带单字节头部的值同样不按任何特定边界对齐。最后,当最高位或最低位为零、但相邻的一位被置位时,该 datum 的内容已被压缩,使用前必须先解压。在这种情况下,长度字的其余位给出的是压缩后 datum 的总大小,而不是原始数据的大小。请注意,线外数据也可能被压缩,但 varlena 头部不会告诉你是否发生了压缩,这一点要由 TOAST 指针的内容来说明。
如果一个表的任意列是可 TOAST 的,该表就会有一个相关联的
TOAST 表,其 OID 存储在该表的
pg_class.reltoastrelid 项中。线外 TOAST 化值保存在 TOAST 表中,下文会更详细地描述。
所用的压缩技术是 LZ 压缩技术族中一种相当简单且非常快速的方法。详情见 src/backend/utils/adt/pg_lzcompress.c。
线外值会被划分成最多 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既不允许压缩,也不允许线外存储;此外,它还会禁止 varlena 类型使用单字节头部。这是不可 TOAST 数据类型列唯一可能的策略。EXTENDED同时允许压缩和线外存储。这是大多数可 TOAST 数据类型的默认策略。系统会先尝试压缩,如果行仍然过大,再使用线外存储。EXTERNAL允许线外存储,但不允许压缩。使用EXTERNAL会让宽text和bytea列上的子串操作更快(代价是占用更多存储空间),因为在值未压缩时,这些操作经过优化,只需提取线外值中所需的部分。MAIN允许压缩,但不允许线外存储。(实际上,对于这类列,仍然可能进行线外存储,但只有在别无他法、必须这样做才能让行足够小以放入页面时,才会把它作为最后手段。)
每种可 TOAST 的数据类型都会为该数据类型的列指定默认策略,但可以通过以下命令更改某个表列的策略:ALTER TABLE SET STORAGE。
与允许行值跨页之类更直接的方法相比,这种方案有不少优点。假定查询通常是通过与相对较小的键值比较来限定的,执行器的大部分工作都会只使用主行项完成。TOAST 化属性的大值只有在结果集发送给客户端时才会被取出(前提是它们确实被选中了)。因此,主表会小得多,其更多的行能够装入共享缓冲区缓存,这是没有线外存储时做不到的。排序集也会缩小,因此排序更常能够完全在内存中完成。一个小测试表明,一个包含典型 HTML 页面及其 URL 的表,其总存储量(包括 TOAST 表)大约只有原始数据大小的一半,而主表只包含全部数据的大约 10%(URL 以及一些较小的 HTML 页面)。与未进行 TOAST 处理的对照表相比,运行时没有差异;在那个对照表中,所有 HTML 页面都被裁剪到 7 kB 以内以便放得下。