本节提供 TOAST 的一个概述(超尺寸字段存储技巧-The Oversized-Attribute Storage Technique)。
因为 PostgreSQL 的页面大小是固定的(通常是8Kb), 并且不允许元组跨越多个页面,因此不可能直接存储非常大的字段值。 在 PostgreSQL 7.1 之前,代码里有一个硬限制, 限制了一个表中一个数据行存储的数据的总大小为刚好略小于一个页面。 从版本 7.1 以及以后的版本开始,这个限制被克服了,方法是允许大的字段值被压缩和/或打碎成多个物理行。 这些事情对用户都是透明的,只是在后端代码上有一些小的影响。 这个技术的爱称是TOAST(或者"切片面包之后的最好的东西")。
只有一部分数据类型支持 TOAST — 我们没必要在那些不可能生成大的字段值的数据类型强制这种额外开销。
要支持 TOAST,数据类型必须有变长(varlena)的表现形式,
这个时候,任何存储的数值的头 32 位都是存储着以字节记的数值的总长度(包括长度本身)。
TOAST 并不约束剩下的表现形式。所有支持可以 TOAST 的数据类型之 C 级别的函数都必须仔细处理
TOAST 的输入值。(通常是在对一个输入值做任何事情之前,调用PG_DETOAST_DATUM
;
但是在某些情况下,更高效的方法也是存在的。)
TOAST 使用变长的长度字的最高两个二进制位, 这样就把任何可以TOAST的数据类型的逻辑长度限制在1Gb(230 - 1 字节)。 如果两个位都是零,那么数值是该数据类型一个普通的未TOAST的数值。 如果其中一个位置了一,那么表示该数值被压缩过,使用前必须先解压缩。如果设置了另外一个位, 则表示该数值是在线外存储的。这个时候,该值剩下的部分只是一个指针, 而正确的数值必须在其他地方查找。如果两个位都设置上了, 那么这个线外数据也被压缩过了。不管哪种情况,长度字里剩下的低位都表示数据的实际尺寸, 而不是解压缩或者从线外数据抓过来之后的逻辑尺寸。
如果一个表中有任何一个字段是可以TOAST的, 那么该表将有一个关联的TOAST表,其 OID 存储在表的pg_class.reltoastrelid 记录里,线外TOAST过的数值保存在TOAST表里,下面有更详细的描述。
这里使用的压缩技术是非常简单并且非常快速的 LZ 族压缩技巧。 参阅 src/backend/utils/adt/pg_lzcompress.c 获取细节。
线外数据被分裂成(如果压缩过,在压缩之后)最多TOAST_MAX_CHUNK_SIZE (这个数值略小于BLCKSZ/4,或者缺省 2000 字节)字节的块, 每个块都作为独立的行在TOAST表里为所属表存储。 每个TOAST表都有字段chunk_id(一个表示特定的TOAST过之数据的 OID), chunk_seq(一个序列号,存储该块在数值中的位置),和一个chunk_data (该块实际的数据)。在chunk_id和chunk_seq上有一个唯一索引, 提供对数值的快速检索。因此,一个表示线外TOAST过的数值的指针数据需要存储要查阅的TOAST的OID 和特定数值的OID(它的chunk_id)。为了方便, 指针数据还存储逻辑数据的尺寸(原始的未压缩的数据长度)以及实际存储的尺寸(如果使用了压缩,则两者不同)。 加上头部的长度字,一个TOAST指针数据的总尺寸是20字节,不管它代表的数值的实际长度是多大。
TOAST 代码只有在准备向某表中存储超过BLCKSZ/4字节(通常是2Kb)的行的时候才会触发。 TOAST 代码将压缩和/或线外存储字段值,直到数值比BLCKSZ/4字节短,或者无法得到更好的结果的时候才停止。 在一个 UPDATE 操作过程中,未改变的字段的数值通常原样保存; 所以,如果 UPDATE 一个带有线外数据的行时,假如线外数据值没有变化,那么将不会有TOAST开销存在。
TOAST代码识别四种不同的存储可TOAST字段的策略:
PLAIN避免压缩或者线外存储。 这只是对那些不能TOAST的数据类型才有可能。
EXTENDED允许压缩和线外存储。 这是大多数可以TOAST的数据类型的缺省。 首先将企图进行压缩,如果行仍然太大,那么则进行线外存储。
EXTERNAL 允许线外存储,但是不许压缩。 使用 EXTERNAL 将令那些在 text 和 bytea 字段上的子字串操作更快(代价是增加了存储空间), 因此这些操作是经过优化的:如果线外数据没有压缩,那么它们只会去抓取需要的部分。
MAIN 允许压缩,但不允许线外存储。 (实际上,在这样的字段上仍然会进行线外存储,但只是作为没有办法把数据行变得更小的情况下的最后的手段。)
每个可以 TOAST 的数据类型都为该数据类型的字段声明一个缺省策略, 但是特定表的字段的存储策略可以用 ALTER TABLE SET STORAGE 修改。
这个方法比那些更直接的方法,比如允许行数值直接跨越多个页面,有更多优点。 假设查询通常是用相对比较短的键值进行匹配的,那么大多数执行器的工作都将使用主行记录完成。 TOAST 过的属性的大的数值只是在把结果集发送给客户端的时候才抽出来(如果选择了它的话)。 因此,主表要小得多,并且它的大部分行都存储在共享缓冲区里,因此就可以不需要任何线外存储。 排序集也缩小了,并且排序将更多地在内存里完成。一个小测试表明,一个用于保存 HTML 页面以及它们的 URL 的表,在存储数据的时候将在包括 TOAST 表在内存储将近一半大小的裸数据, 而主表只包含全部数据的 10%(URL和一些小的 HTML 页面)。 与在一个非 TOAST 的伴侣表里面存储(把全部 HTML 页面裁剪成 7Kb 以匹配页面大小),没有任何运行时的区别。