73.2. TOAST#
本节概述了TOAST(超大属性存储技术)。
PostgreSQL使用固定页面大小(通常为 8 kB),并且不允许元组跨多个页面。因此,无法直接存储非常大的字段值。为了克服此限制,大型字段值被压缩和/或分解为多个物理行。这对于用户来说是透明的,对大多数后端代码的影响很小。该技术亲切地称为TOAST(或“自切片面包以来最好的东西”)。TOAST基础设施还用于改善内存中大数据值的处理。
只有特定数据类型支持TOAST——无需对无法生成大字段值的数据类型施加开销。要支持TOAST,数据类型必须具有可变长度(varlena)表示形式,其中通常任何存储值的第一个四字节词包含值的总长度(以字节为单位,包括它本身)。TOAST不会限制数据类型表示形式的其余部分。统称为TOASTed 值的特殊表示形式通过修改或重新解释此初始长度词来工作。因此,支持TOAST数据类型的 C 级函数必须小心处理可能TOASTed 的输入值:输入可能实际上不包含四字节长度词和内容,直到它detoasted之后。(通常在对输入值执行任何操作之前通过调用PG_DETOAST_DATUM
来完成此操作,但在某些情况下,可以采用更有效的方法。有关更多详细信息,请参见第 38.13.1 节。)
TOAST篡夺了 varlena 长度词的两个位(在大端机器上为高位,在小端机器上为低位),从而将TOAST可用数据类型的逻辑大小限制为 1 GB(230- 1 字节)。当两个位都为零时,该值是数据类型的普通未TOAST化值,并且长度词的剩余位以字节为单位给出总数据大小(包括长度词)。当最高位或最低位被设置时,该值只有单字节头,而不是正常的四字节头,并且该字节的剩余位以字节为单位给出总数据大小(包括长度字节)。这种替代方式支持对小于 127 字节的值进行空间高效的存储,同时仍允许数据类型根据需要增长到 1 GB。具有单字节头的值未对齐在任何特定边界上,而具有四字节头的值至少对齐在四字节边界上;这种对齐填充的省略提供了与短值相比显着的额外空间节省。作为一个特例,如果单字节头的剩余位全部为零(对于自包含长度而言是不可能的),则该值是指向外部数据的指针,具有如下所述的几种可能的替代方式。此类TOAST 指针的类型和大小由存储在数据第二字节中的代码确定。最后,当最高位或最低位清除,但相邻位被设置时,数据的内容已被压缩,并且在使用之前必须解压缩。在这种情况下,四字节长度词的剩余位给出压缩数据的大小,而不是原始数据。请注意,对于外部数据也可以进行压缩,但 varlena 头不告诉是否发生了压缩——相反,TOAST指针的内容告诉了这一点。
可以通过在CREATE TABLE
或ALTER TABLE
中设置COMPRESSION
列选项,为每列选择用于行内或行外压缩数据的压缩技术。对于没有明确设置的列,默认行为是在插入数据时咨询default_toast_compression参数。
如上所述,有多种类型的TOAST指针数据。最古老且最常见的类型是存储在TOAST表中的离线数据指针,该表与包含TOAST指针数据本身的表是分开的,但与之相关联。当存储在磁盘上的元组太大而无法按原样存储时,TOAST管理代码(在access/common/toast_internals.c
中)会创建这些磁盘上指针数据。更多详细信息请参见第 73.2.1 节。或者,TOAST指针数据可以包含指向内存中其他位置的离线数据的指针。此类数据必然是短寿命的,并且永远不会出现在磁盘上,但它们对于避免复制和对大型数据值进行冗余处理非常有用。更多详细信息请参见第 73.2.2 节。
73.2.1. 离线、磁盘上 TOAST 存储#
如果表中的任何列都是TOAST兼容的,则该表将具有一个关联的TOAST表,其 OID 存储在表的pg_class
.reltoastrelid
条目中。磁盘上TOAST后的值保存在TOAST表中,如下文所述。
离线值被分成(如果使用的话,在压缩后)最多TOAST_MAX_CHUNK_SIZE
字节的块(默认情况下,选择此值以便四个块行可以放在一页上,使其约为 2000 字节)。每个块都作为TOAST表中属于拥有表的一个单独行存储。每个TOAST表都具有列chunk_id
(一个标识特定TOAST后的值的 OID)、chunk_seq
(其值中块的序列号)和chunk_data
(块的实际数据)。chunk_id
和chunk_seq
上的唯一索引提供了对值的快速检索。因此,表示磁盘上离线TOAST后的值的指针数据需要存储要查找的TOAST表的 OID 和特定值(其chunk_id
)的 OID。为了方便起见,指针数据还存储逻辑数据大小(原始未压缩数据长度)、物理存储大小(如果应用了压缩则不同)和使用的压缩方法(如果有)。考虑到 varlena 头字节,因此磁盘上TOAST指针数据的大小始终为 18 字节,而不管表示的值的实际大小如何。
只有当要存储在表中的行值比TOAST_TUPLE_THRESHOLD
字节(通常为 2 kB)宽时,TOAST管理代码才会被触发。TOAST代码会压缩和/或将字段值移出行,直到行值短于TOAST_TUPLE_TARGET
字节(通常也为 2 kB,可调整)或无法再获得更多收益。在 UPDATE 操作期间,未更改字段的值通常按原样保留;因此,如果没有任何超出范围的值发生变化,则对具有超出范围的值的行进行 UPDATE 不会产生任何TOAST成本。
TOAST管理代码识别出四种不同的策略,用于在磁盘上存储可TOAST的列
PLAIN
阻止压缩或超出范围的存储。这是非可 数据类型列的唯一可能策略。EXTENDED
允许压缩和超出范围的存储。这是大多数可 数据类型的默认值。如果行仍然太大,则会首先尝试压缩,然后尝试超出范围的存储。EXTERNAL
允许超出范围的存储,但不允许压缩。使用EXTERNAL
会使在宽text
和bytea
列上进行子字符串操作变得更快(以增加存储空间为代价),因为当超出范围的值未压缩时,这些操作经过优化,仅获取所需部分。MAIN
允许压缩,但不允许超出范围的存储。(实际上,对于此类列仍然会执行超出范围的存储,但仅在别无他法时作为最后手段,以使行足够小以适合一页。)
每个可TOAST数据类型都为该数据类型的列指定默认策略,但可以使用ALTER TABLE ... SET STORAGE
更改给定表列的策略。
可以使用ALTER TABLE ... SET (toast_tuple_target = N)
为每个表调整TOAST_TUPLE_TARGET
与更直接的方法(例如允许行值跨页)相比,此方案具有许多优势。假设查询通常通过与相对较小的键值进行比较来限定,那么执行程序的大部分工作将使用主行条目完成。只有在将结果集发送到客户端时,才会提取TOASTed 属性的大值(如果已选择)。因此,主表要小得多,并且与没有任何超出范围存储的情况相比,其更多行适合共享缓冲区缓存。排序集也会缩小,并且排序通常完全在内存中完成。一个小测试表明,包含典型 HTML 页面及其 URL 的表存储在包括TOAST表在内的原始数据大小的一半左右,并且主表仅包含约 10% 的整个数据(URL 和一些小型 HTML 页面)。与未TOAST的比较表相比,没有运行时间差异,其中所有 HTML 页面都被缩减到 7 kB 以适合。
73.2.2. 非行内、内存中 TOAST 存储#
TOAST指针可以指向不在磁盘上但位于当前服务器进程内存中的数据。此类指针显然不能长期存在,但它们仍然有用。目前有两种子情况:指向间接数据的指针和指向扩展数据的指针。
间接TOAST指针仅仅指向存储在内存中的非间接 varlena 值。此案例最初只是作为概念证明而创建,但目前在逻辑解码期间使用,以避免可能创建超过 1 GB 的物理元组(因为将所有非行内字段值拉入元组可能会这样做)。此案例的用途有限,因为指针数据创建者完全负责在指针存在期间引用数据,并且没有基础设施来帮助完成此操作。
扩展TOAST指针对于磁盘表示不特别适合计算目的的复杂数据类型很有用。例如,PostgreSQL数组的标准 varlena 表示包括维度信息、(如果有任何空元素)空位图,然后按顺序排列所有元素的值。当元素类型本身是可变长度时,查找*N
*'th 元素的唯一方法是扫描所有前面的元素。此表示由于其紧凑性而适合磁盘存储,但对于数组计算,最好有一个“扩展”或“解构”表示,其中已识别所有元素起始位置。TOAST指针机制通过允许按引用传递的数据指向标准 varlena 值(磁盘表示)或指向内存中某个位置的扩展表示的TOAST指针来支持此需求。此扩展表示的详细信息取决于数据类型,尽管它必须具有标准标头并满足src/include/utils/expandeddatum.h
中给出的其他 API 要求。使用数据类型的 C 级函数可以选择处理任何表示。不知道扩展表示但只是将PG_DETOAST_DATUM
应用于其输入的函数将自动接收传统 varlena 表示;因此,可以逐步引入对扩展表示的支持,一次一个函数。
指向扩展值的TOAST指针进一步细分为读写和只读指针。指向的表示无论哪种方式都是相同的,但接收读写指针的函数允许就地修改引用值,而接收只读指针的函数则不允许;如果它想创建修改版本的值,它必须首先创建一个副本。此区别和一些相关约定可以避免在查询执行期间对扩展值进行不必要的复制。
对于所有类型的内存中TOAST指针,TOAST管理代码确保此类指针数据不会意外存储在磁盘上。内存中TOAST指针在存储前自动扩展为正常的行内 varlena 值 — 然后可能转换为磁盘上TOAST指针,如果包含的元组太大。