59.2. TOAST

本节提供TOAST的概述(超尺寸属性存储技术-The Oversized-Attribute Storage Technique)。

PostgreSQL使用固定的页面尺寸(通常是8kB),并且不允许元组跨越多个额页面。因此不可能直接存储非常大的域值。为了克服这个限制,大的域值会被压缩并/或分解成多个物理行。这些处理对用户都是透明的,只是在大部分的后端代码上有一些小的影响。这个技术的昵称是TOAST(或者"切片面包之后的最好的东西")。

只有特定的数据类型支持TOAST — 我们没必要在那些不可能生成大域值的数据类型上强加这种负担。要支持TOAST,数据类型必须有变长(varlena)的表现形式, 在其中任何存储值的头32位字都包含以字节记的值的总长度(包括长度本身)。TOAST并不约束剩下的表现形式。所有支持可TOAST数据类型的C级别的函数都必须仔细处理TOAST过的输入值(通常是在对一个输入值做任何事情之前,先调用PG_DETOAST_DATUM; 但是在某些情况下也存在更高效的方法)。

TOAST占用使用变长类型的长度字的最高两个二进制位(大端法机器上的高位,小端法机器上的低位), 这样就把任何可TOAST值的逻辑长度限制在1GB(230 - 1字节)。如果两个位都是零,那么数值是该数据类型一个普通的未TOAST的值,并且长度字的剩余位给出整个数据以字节计的大小(包括长度字)。作为一种特殊情况,如果剩余的字节也都是0(它不可能时一个自包含的长度),该值是一个指向存放在独立TOAST表中的线外数据的指针(TOAST指针的大小由数据的第二个字节给定)。只有单字节头部的值也不会按照任何边界对齐。最后,如果最高位或最低位被清除而另一位被设置,则表示该数据的内容被压缩过并且在使用前必须先解压。在这种情况中长度字的剩余位指出了压缩过的数据的大小,而不是原始数据的大小。注意对于线外数据也可能存在压缩,但是变长数据的头部不会告诉我们压缩是否发生 — TOAST指针的内容将说明这个问题。

如果一个表中有任何一个列是可以TOAST的, 那么该表将有一个与之关联的TOAST表,其 OID 存储在表的pg_class.reltoastrelid项中。线外的被TOAST过的值保存在TOAST表里,下文有更详细的描述。

这里使用的压缩技术是相当简单并且快速的 LZ 族压缩技术。更多细节请参考src/backend/utils/adt/pg_lzcompress.c

线外值被分裂成(如果压缩过,在压缩之后分裂)最大为TOAST_MAX_CHUNK_SIZE(默认情况下该值应选为使得四个块(chunk)行能放在一个页面中,这个数值大约为2000 字节)字节的块。每个块都作为独立的行存储在所属表的TOAST表中。每个TOAST表都有列chunk_id(一个表示特定的被TOAST过的数据的OID)、chunk_seq(一个序列号,存储该块在值中的位置)和一个chunk_data(该块的实际数据)。在chunk_idchunk_seq上有一个唯一索引, 提供对值的快速检索。因此,一个表示线外TOAST过的值的指针数据应存储要查看的TOAST表的OID以及 指定值的OID(它的chunk_id)。为了方便, 指针数据还存储逻辑数据的尺寸(原始的未压缩的数据长度)以及实际存储的尺寸(如果应用了压缩,则两者不同)。 加上变长数据头部的字节,一个TOAST指针数据的总尺寸是18字节,不管它代表的值的实际长度是多大。

TOAST代码只有在准备向一个表中存储超过TOAST_TUPLE_THRESHOLD字节(通常是2kB)的行值的时候才会触发。TOAST代码将压缩和/或线外存储域值,直到行值比TOAST_TUPLE_TARGET字节(通常也是2kB)短,或者无法得到更好的结果的时候才停止。在一个 UPDATE 操作过程中,未改变的域的值通常原样保存; 所以,如果 UPDATE 一个带有线外值的行时,假如线外值没有变化,那么将不会产生TOAST开销。

TOAST代码识别四种不同的存储可TOAST列的策略:

每个可TOAST的数据类型都为该数据类型的列指定了一个缺省策略, 但是一个给定表的列的存储策略可以用ALTER TABLE SET STORAGE修改。

这个方法比那些更直接的方法(比如允许行值跨越多个页面)有更多优点。 假设查询通常是用相对比较短的键值进行匹配的,那么执行器的大多数工作都将使用主行项完成。TOAST过的属性的大值只是在把结果集发送给客户端的时候才被抽出来(如果它被选中)。 因此,主表要小得多,并且它的能放入到共享缓冲区中的行要比没有任何线外存储的方案更多。 排序集也缩小了,并且排序将更多地在内存里完成。一个小测试表明,一个典型的保存 HTML 页面以及它们的 URL 的表占用的存储(包括TOAST表在内)大约只有裸数据的一半,而主表只包含全部数据的 10%(URL和一些小的 HTML 页面)。与在一个非TOAST的对照表里面存储(把全部 HTML 页面裁剪成 7Kb 以匹配页面大小)同样的数据相比,运行时没有任何区别。