2024年9月26日: PostgreSQL 17 发布!
支持的版本:当前 (17) / 16 / 15 / 14 / 13 / 12
开发版本:devel
不支持的版本:11 / 10 / 9.6 / 9.5 / 9.4 / 9.3 / 9.2 / 9.1 / 9.0 / 8.4 / 8.3 / 8.2 / 8.1 / 8.0

65.2. TOAST #

本节概述了TOAST(超大属性存储技术)。

PostgreSQL 使用固定页面大小(通常为 8 kB),并且不允许元组跨越多个页面。因此,无法直接存储非常大的字段值。为了克服此限制,大型字段值会被压缩和/或分解成多个物理行。这对用户来说是透明的,并且对大多数后端代码的影响很小。这种技术被亲切地称为TOAST(或 自从切片面包以来最好的东西)。该TOAST基础设施还用于改进对内存中大型数据值的处理。

只有某些数据类型支持TOAST—— 无需对无法生成大型字段值的数据类型施加开销。为了支持TOAST,数据类型必须具有可变长度(varlena)表示形式,在这种表示形式中,通常任何存储值的第一个四字节字都包含该值的总字节长度(包括自身)。TOAST不会限制数据类型其余表示形式。称为TOASTed 值的特殊表示形式通过修改或重新解释此初始长度字来工作。因此,支持TOAST的数据类型的 C 级函数必须小心处理潜在的TOASTed 输入值:在被detoasted之前,输入可能实际上并不包含四字节长度字和内容。(这通常是在对输入值进行任何操作之前调用PG_DETOAST_DATUM 来完成的,但在某些情况下,可以使用更有效的方法。有关更多详细信息,请参见第 36.13.1 节。)

TOAST占用 varlena 长度字的两位(在大端机上是高位,在小端机上是低位),从而将TOAST的数据类型的任何值的逻辑大小限制为 1 GB(230 - 1 字节)。当这两位都为零时,该值是数据类型的普通未TOASTed 值,并且长度字的其余位以字节为单位给出总数据大小(包括长度字)。当最高位或最低位被设置时,该值只有一个单字节标头而不是正常的四字节标头,并且该字节的其余位以字节为单位给出总数据大小(包括长度字节)。此替代方案支持对小于 127 字节的值进行空间高效的存储,同时仍允许数据类型根据需要增长到 1 GB。具有单字节标头的值不会对齐到任何特定边界,而具有四字节标头的值至少对齐到四字节边界;这种省略对齐填充提供了额外的空间节省,与短值相比,这种节省是显著的。作为特殊情况,如果单字节标头的其余位都为零(对于自包含长度来说这是不可能的),则该值是指向行外数据的指针,如下所述,有几种可能的替代方案。此类TOAST 指针的类型和大小由数据中第二个字节中存储的代码确定。最后,当最高位或最低位为清除但相邻位被设置时,数据的原始内容已被压缩,并且必须在使用前解压缩。在这种情况下,四字节长度字的其余位给出压缩数据的总大小,而不是原始数据。请注意,对于行外数据也可以进行压缩,但 varlena 标头不会说明是否发生了压缩——该TOAST指针会告知这一点。

用于行内或行外压缩数据的压缩技术可以通过在CREATE TABLEALTER TABLE 中设置COMPRESSION 列选项为每列选择。对于没有显式设置的列,默认情况下会在插入数据时查询default_toast_compression 参数。

如前所述,有多种类型的TOAST指针数据。最古老且最常见的类型是指向存储在TOAST中的行外数据,该表与包含TOAST指针数据本身的表分离但相关联。这些磁盘上指针数据由TOAST管理代码(在access/common/toast_internals.c 中)创建,当要存储在磁盘上的元组太大而无法按原样存储时。更多详细信息请参见第 65.2.1 节。或者,TOAST指针数据可以包含指向内存中其他位置的行外数据的指针。此类数据必然是短暂的,永远不会出现在磁盘上,但它们对于避免复制和冗余处理大型数据值非常有用。更多详细信息请参见第 65.2.2 节

65.2.1. 行外磁盘TOAST存储 #

如果表的任何列都是TOAST,则该表将具有一个关联的TOAST表,其 OID 存储在表的pg_class.reltoastrelid 条目中。磁盘上的TOASTed 值保存在TOAST表中,如下所述。

行外值被分成(如果使用则在压缩后)最多TOAST_MAX_CHUNK_SIZE 字节的块(默认情况下,此值的选择是为了使四个块行适合一个页面,使其约为 2000 字节)。每个块都作为chunk_id(标识特定TOAST表所属的拥有表的TOAST表。TOASTed 值的 OID)、chunk_seq(其值内块的序列号)和chunk_data(块的实际数据)。chunk_idchunk_seq 上的唯一索引提供了对值的快速检索。表示磁盘上行外TOASTed 值的指针数据因此需要存储要查找的TOAST表的 OID 和特定值的 OID(其chunk_id)。为方便起见,指针数据还存储逻辑数据大小(原始未压缩数据长度)、物理存储大小(如果应用了压缩则不同)以及使用的压缩方法(如果有)。考虑到 varlena 标头字节,磁盘上TOAST指针数据的总大小因此为 18 字节,而不管表示值的实际大小如何。

TOAST管理代码仅在要存储在表中的行值宽度超过TOAST_TUPLE_THRESHOLD 字节(通常为 2 kB)时触发。该TOAST代码将压缩和/或将字段值移出行外,直到行值短于TOAST_TUPLE_TARGET 字节(通常也为 2 kB,可调整)或无法获得更多收益为止。在 UPDATE 操作期间,未更改字段的值通常会按原样保留;因此,如果没有任何行外值更改,则更新包含行外值的行的操作不会产生TOAST成本。

TOAST管理代码识别四种不同的策略,用于在磁盘上存储TOAST

  • PLAIN 防止压缩或行外存储。对于非TOAST数据类型的列,这​​是唯一可能的策略。

  • EXTENDED 允许压缩和行外存储。这是大多数TOAST数据类型的默认值。将首先尝试压缩,如果行仍然太大,则尝试行外存储。

  • EXTERNAL 允许行外存储但不允许压缩。使用EXTERNAL 将使宽textbytea 列上的子字符串操作更快(以增加存储空间为代价),因为这些操作经过优化,仅在未压缩时才获取行外值的所需部分。

  • MAIN 允许压缩但不允许行外存储。(实际上,对于此类列仍将执行行外存储,但仅在别无他法使行足够小以适合页面时才作为最后手段。)

每个TOAST数据类型都为该数据类型的列指定默认策略,但可以使用ALTER TABLE ... SET STORAGE 更改给定表列的策略。

TOAST_TUPLE_TARGET 可以使用ALTER TABLE ... SET (toast_tuple_target = N) 为每个表调整。

与更直接的方法(例如允许行值跨越页面)相比,此方案具有许多优势。假设查询通常由针对相对较小的键值的比较限定,则执行程序的大部分工作将使用主行条目完成。的较大值TOAST扩展属性(如果被选中)只会当结果集发送到客户端时才会被提取。因此,主表会变得更小,并且其更多行可以放入共享缓冲区缓存中,而如果没有任何行外存储,则不会出现这种情况。排序集也会缩小,并且排序会更频繁地在内存中完成。一个小测试表明,包含典型 HTML 页面及其 URL 的表存储在约一半的原始数据大小中,包括TOAST表,并且主表仅包含大约 10% 的全部数据(URL 和一些小的 HTML 页面)。与未扩展的比较表相比,运行时没有差异,其中所有 HTML 页面都被裁剪到 7 kB 以适合。TOAST

65.2.2. 行外、内存中 TOAST 存储 #

TOAST指针可以指向不在磁盘上,而是在当前服务器进程内存中的其他位置的数据。此类指针显然不能长期存在,但它们仍然有用。目前有两种子情况:指向间接数据的指针和指向扩展数据的指针。

间接TOAST指针只是指向存储在内存某处的非间接 varlena 值。最初创建此情况仅仅是为了证明概念,但目前它在逻辑解码期间用于避免可能创建超过 1 GB 的物理元组(因为将所有行外字段值拉入元组可能会这样做)。这种情况的使用有限,因为指针数据的创建者完全负责引用的数据在指针可能存在的时间内存活,并且没有基础设施来帮助解决这个问题。

扩展TOAST指针对于复杂数据类型很有用,这些数据类型的磁盘表示不特别适合计算目的。例如,PostgreSQL 数组的标准 varlena 表示形式包括维度信息,如果有任何空元素,则为一个空位图,然后按顺序排列所有元素的值。当元素类型本身是可变长度时,找到第N个元素的唯一方法是扫描所有前面的元素。这种表示形式适用于磁盘存储,因为它具有紧凑性,但对于数组的计算,最好有一个扩展分解表示形式,其中所有元素起始位置都已识别。该TOAST指针机制通过允许按引用传递的 Datum 指向标准 varlena 值(磁盘表示)或指向内存中某个位置的扩展表示的TOAST指针来支持此需求。此扩展表示的详细信息取决于数据类型,尽管它必须具有标准标头并满足src/include/utils/expandeddatum.h中给出的其他 API 要求。处理数据类型的 C 级函数可以选择处理任一表示形式。不知道扩展表示形式但只是将其输入应用于PG_DETOAST_DATUM的函数将自动接收传统的 varlena 表示形式;因此,可以增量地、一次一个函数地引入对扩展表示形式的支持。

TOAST指向扩展值的指针进一步细分为读写指针和只读指针。指向的表示形式无论哪种方式都相同,但是接收读写指针的函数允许就地修改引用的值,而接收只读指针的函数则不允许;如果要创建修改后的值版本,它必须首先创建一个副本。这种区别和一些相关约定使得在查询执行期间避免不必要地复制扩展值成为可能。

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

提交更正

如果您在文档中看到任何不正确的内容,与您对特定功能的体验不符,或者需要进一步澄清,请使用此表单报告文档问题。