9.3 9.4 9.5 9.6 10 11 12 13 14 15
阿里云PostgreSQL 问题报告 纠错本页面

63.2. TOAST

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

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

只有特定的数据类型支持TOAST — 我们没必要在那些不可能生成大域值的数据类型上强加这种负担。要支持TOAST, 数据类型必须有变长(varlena)的表现形式, 通常在存储的值中,头四个字节包含值的总字节长度(包括值本身)。TOAST 并不约束该数据类型的表达的剩余部分。这种特殊的表达被统称为TOAST 过的值,对它们的操作都必须通过修改或者重新解释这个初始长度字来进行。 因此,支持一种可TOAST数据类型的 C 级函数必须要小心它们怎样处理潜在被 TOAST过的输入值:一个输入值可能并不真正由一个四字节长度和内容构成, 直到它被detoasted(通常是在对一个输入值做任何事情之前,先调用 PG_DETOAST_DATUM;但是在某些情况下也存在更高效的方法, 详见第 35.11.1 节)。

TOAST占用使用变长类型的长度字的最高两个二进制位 (大端机器上的高位,小端机器上的低位),这样就把任何可TOAST 数据类型值的逻辑长度限制在1GB(230 - 1字节)。如果两个位都是零, 那么数值是该数据类型一个普通的未TOAST的值, 并且长度字的剩余位给出整个数据以字节计的大小(包括长度字) 当最高位或者最低位被设置时,该值只是有一个单字节头部而不是通常的四字节头部, 并且该字节的剩余位数给出了以字节计的总数据尺寸(包括长度字节)。 此替代方案支持空间有效的存储小于127个字节的值, 不过需要时仍然允许数据类型增长到 1GB。 带有单字节头部的值不会按照任何特别的边界对齐, 反之带有四字节头部的值会按照至少一个四字节边界对齐。 这种对齐填充的省略额外地节省了空间,这种节省比起短值来说更加显著。 作为一种特殊情况,如果一个单字节头部的剩余位全是零 (对于一个自包含的长度来说是不可能的),该值就是一个线外数据的指针, 这就可能有下文所述的几种可能的情况。这样一个TOAST 指针 的类型和尺寸由该数据的第二个字节中存储的一个代码决定。最后, 如果最高位或最低位被清除而相邻位被设置, 则表示该数据的内容被压缩过并且在使用前必须先解压。 在这种情况中四字节长度字的剩余位指出了压缩过的数据的大小, 而不是原始数据的大小。注意对于线外数据也可能存在压缩, 但是变长数据的头部不会告诉我们压缩是否发生 — TOAST 指针的内容将说明这个问题。

如前所述,有多种类型的TOAST指针数据。 最古老且最常见的类型是指向存储在一个TOAST 中的线外数据的指针,TOAST表与包含该TOAST 指针数据本身的表是相关的,但两者又是被分离存储的。 当一个要被存储在磁盘上的元组过大时,这些磁盘上的指针数据由 TOAST管理代码(在access/heap/tuptoaster.c中)所创建。 第 63.2.1 节中给出了更多的细节。或者,一个 TOAST指针数据能够包含一个指向出现在内存中某处的线外数据的指针。 这种数据必定是短命的并且将不会出现在磁盘上, 但是它们对于避免大型数据值的复制和冗余处理非常有用。 详见第 63.2.2 节

线内或者线外压缩数据所使用的压缩技术是 LZ 压缩技术家族中一种相对简单且非常快速的成员。详见 src/common/pg_lzcompress.c

63.2.1. 线外磁盘上 TOAST 存储

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

线外值被分裂成(如果压缩过,在压缩之后分裂)最大为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 以匹配页面大小)同样的数据相比,运行时没有任何区别。

63.2.2. 线外内存中 TOAST 存储

TOAST指针可以指向不在磁盘上但在当前服务器进程内存中的数据。 这样的指针显然不是长期存在的,但是它们是有用的。当前有两种子情况: 指向间接数据的指针以及指向扩展数据的指针.

间接TOAST指针指向存储在内存中某个地方的非间接 varlena 值。 这种情况起初仅仅作为一种概念验证而创建, 但是当前它被用来在逻辑解码期间避免创建超过 1GB 的物理元组 (把所有线外域值都拉入元组就会这样)。这种情况用处有限, 因为该指针数据的创建者需要完全负责确保只要指针存在,被引用数据就应该存在, 并且没有其他设施来帮助它。

扩展的TOAST指针对于复杂数据类型有用, 这些数据类型在盘上的表示形式不是特别适合计算性的目的。例如, 一个PostgreSQL数组的标准 varlena 表达包括了维度信息、 一个空值位图(如果有任何空值元素),然后是按顺序的所有元素的值。 当元素类型本身是变长时,找到第N 个元素的唯一方式是扫描所有在它前面的元素。这种表达适合于磁盘上的存储, 因为它的紧凑性。但是为了对该数组进行计算,则"扩展"或者"结构" 表达会更好,这些表达中所有元素的开始位置都会被标记出来。TOAST 指针机制支持这种需要,通过允许通过引用传递的数据指向一个标准 varlena 值 (磁盘上的表达)或者一个TOAST指针指向内存中某处的一个扩展表达。 这种扩展表达的细节取决于数据类型,不过它必须具有一个标准的头部并且符合 src/include/utils/expandeddatum.h中给定的其他 API 要求。 该数据类型的 C-级别函数可以选择处理任一表达。 不了解扩展表达但简单地在其输入上应用PG_DETOAST_DATUM 的函数将自动地接收到传统的 varlena 表达。 因此对于一种扩展表达的支持可以被增量式地引入,一次一个函数。

扩展值的TOAST指针会被进一步分解成read-writeread-only指针。两种方式指向的表达是相同的, 但是收到一个读写指针的函数被允许就地修改被引用值, 而接收到只读指针的函数则不能。如果后者想要做一个该值的被修改的版本, 它必须先创建一个副本。 这种区分和一些相关的惯例使得可以在查询执行期间避免不必要的扩展值副本。

对于所有内存中TOAST指针的类型,TOAST 管理代码会确保这类指针数据不会意外地被存储在磁盘上。 在存储之前内存中TOAST指针会被自动地扩展成通常的线内 varlena 值 —然后可能会被转换成磁盘上的TOAST指针(如果包含的元组不是太大)。

<
/BODY >