PG之TOAST技术

0    715    7

👉 本文共约7206个字,系统预计阅读时间或需28分钟。

TOAST简介

TOAST是The OverSized Attribute-Storage Technique(超尺寸字段存储技术)的缩写,顾名思义,是说超长字段在PG的一种存储方式,主要用于存储大字段的值;PG页面的大小是固定的(通常是8KB),并且一个元组不允许跨多个页面存储,所以并不能直接存储大字段数据;Toast就是为此应运而生,它会将大字段值压缩或者分散为多个物理行来存储;对于用户来说完全不用关注这一技术实现,完全是透明的。

在 PG 中,页是数据在文件存储中的基本单位,其大小是固定的且只能在编译期指定,之后无法修改,默认的大小为8 KB 。同时,PG 不允许一行数据跨页存储,那么对于超长的行数据,PG 就会启动 TOAST ,具体就是采用压缩和切片的方式。如果启用了切片,实际数据存储在另一张系统表的多个行中,这张表就叫 TOAST 表,这种存储方式叫行外存储。

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

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

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

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

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

行外磁盘上 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列的策略:

  • PLAIN避免压缩或者行外存储;而且它禁用变长类型的单字节头部。这是不可TOAST数据类型列的唯一可能的策略。只是对那些不能TOAST的数据类型才有可能。
  • EXTENDED允许压缩和行外存储。这是大多数可TOAST数据类型的默认策略。 首先将尝试进行压缩,如果行仍然太大,那么则进行行外存储。
  • EXTERNAL允许行外存储,但是不许压缩。使用EXTERNAL将令那些在宽textbytea列上的子串操作更快(代价是增加了存储空间), 因此这些操作被优化为只抓取未压缩线外数据中需要的部分。
  • MAIN允许压缩,但不允许行外存储(实际上,在这样的列上仍然会进行行外存储,但只是作为没有办法把行变得足以放入一页的情况下的最后手段)。

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

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

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

行外内存中 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指针(如果包含的元组不是太大)。

TOAST的存储方式

PG只有部分数据类型支持toast,因为有些字段类型是不会产生大字段数据的,完全没必要用到Toast技术(比如int,date,time,boolean等);

支持Toast的数据类型应当是变长的(variable-length);

当表中字段任何一个有Toast,那这个表都会有这一个相关联的Toast表,OID被存储在pg_class.reltoastrelid里面;

超出的的数值将会被分割成chunks,并最多toast_max_chunk_size 个byte(缺省是2Kb)

当存储的行数据超过toast_tuple_threshold值(通常是2kB),就会触发toast存储;

toast将会压缩或者移动字段值直到超出部分比toast_tuple_targer值小(这个值通常也是2KB)。

Toast的4种策略

策略说明
PLAIN避免压缩和行外存储。 只有那些不需要 TOAST 策略就能存放的数据类型允许选择(例如 int 类型),而对于 text 这类要求存储长度超过页大小的类型,是不允许采用此策略的。
EXTENDED允许压缩和行外存储。 一般会先压缩,如果还是太大,就会行外存储
EXTERNA允许行外存储,但不许压缩。 类似字符串这种会对数据的一部分进行操作的字段,采用此策略可能获得更高的性能,因为不需要读取出整行数据再解压。
MAIN允许压缩,但不许行外存储。 不过实际上,为了保证过大数据的存储,行外存储在其它方式(例如压缩)都无法满足需求的情况下,作为最后手段还是会被启动。因此理解为尽量不使用行外存储更贴切。

Toast的优缺点

Toast的优点

1.可以存储超长超大字段,避免之前不能直接存储的限制

2.物理上与普通表是分离的,检索查询时不检索到该字段会极大地加快速度

3.更新普通表时,该表的Toast数据没有被更新时,不用去更新Toast表

Toast的缺点

1.对大字段的索引创建是一个问题,有可能会失败,其实通常也不建议在大字段上创建,全文检索倒是一个解决方案。

2.大字段的更新会有点慢,其它DB也存在。

更改表的存储方式为TOAST

示例:

TOAST额外的三个字段

字段名属性
chunk_id标识TOAST表的OID字段
chunk_seqchunk的序列号,与chunk_id的组合唯一索引可以加速访问
chunk_data存储TOAST表的实际数据

通过以上语句,我们查到toast_t1表的 oid 为16468,其对应 TOAST 表的 oid 为16471(关于 oid 和 pg_class 的概念,请参考PG官方文档),那么其对应 TOAST 表名则为: pg_toast.pg_toast_16468(注意这里是 toast_t1表的 oid ),我们看下其定义:

TOAST表的计算

计算一个表的大小时要注意统计Toast的大小,因为对超长字段存储时,在基础表上可能只存了20%,另外的数据都存到了Toast里面去了,计算大小时要结合起来看,索引也是一样,对于表里有main或者extended类型的会创建Toast表,两者的关联是通过pg_class里的OID去关联的。

可以看到后插入的数据随着字段内容的增多,toast段一直在变大。基础表的大小没有变化。

这个和Oracle存储的大字段内容比较像,Oracle存储Blob类的数据时也是指定另外的segment来存储,而不是在原表中存储,当然可以设置enable storage in row来指定表中存储

示例

首先创建一张 blog 表:

可以看到,interger 默认 TOAST 策略为 plain ,而 text 为 extended 。PG 资料告诉我们,如果表中有字段需要 TOAST ,那么系统会自动创建一张 TOAST 表负责行外存储,那么这张表在哪里?

本人提供Oracle、MySQL、PG等数据库的培训和考证业务,私聊QQ646634621或微信db_bao,谢谢!

通过上诉语句,我们查到 blog 表的 oid 为16441,其对应 TOAST 表的 oid 为16444(关于 oid 和 pg_class 的概念,请参考PG官方文档),那么其对应 TOAST 表名则为: pg_toast.pg_toast_16441(注意这里是 blog 表的 oid ),我们看下其定义:

TOAST 表有3个字段:

  • chunk_id :用来表示特定 TOAST 值的 OID ,可以理解为具有同样 chunk_id 值的所有行组成原表(这里的 blog )的 TOAST 字段的一行数据
  • chunk_seq :用来表示该行数据在整个数据中的位置
  • chunk_data :实际存储的数据。

现在我们来实际验证下:

可以看到因为 content 只有10个字符,所以没有压缩,也没有行外存储。然后我们使用如下 SQL 语句增加 content 的长度,每次增长1倍,同时观察 content 的长度,看看会发生什么情况?

反复执行如上过程,直到 pg_toast_16441 表中有数据:

可以看到,直到 content 的长度为327680时(已远远超过页大小 8K),对应 TOAST 表中才有了2行数据,且长度都是略小于2K,这是因为 extended 策略下,先启用了压缩,然后才使用行外存储。

下面我们将 content 的 TOAST 策略改为 EXTERNA ,以禁止压缩。

然后我们再插入一条数据:

然后重复以上步骤,直到TOAST表中产生新的行:

这次我们看到当 content 长度达到2560(按照官方文档,应该是超过2KB左右), TOAST 表中产生了新的2条 chunk_id 为16448的行,且2行数据的 chunk_data 的长度之和正好等于2560。通过以上操作得出以下结论:

  • 如果策略允许压缩,则TOAST优先选择压缩。
  • 不管是否压缩,一旦数据超过2KB左右,就会启用行外存储。
  • 修改TOAST策略,不会影响现有数据的存储方式。

在pg11中新增了存储参数toast_tuple_target

请注意,在pg11中,TOAST_TUPLE_TARGET是存储参数,不是配置参数。也就是说支持在table级别动态设置

请注意:在pg10中,TOAST_TUPLE_TARGET不是存储参数,而是在源码中定义的符号常量

当tuple length长度超过toast_tuple_target时就会compress和/或者move字段值。

如下翻译来自https://www.postgresql.org/docs/11/storage-toast.html

当a row value宽于TOAST_TUPLE_THRESHOLD bytes (通常是2KB)时,TOAST management code 会被触发,

注意,TOAST_TUPLE_THRESHOLD这个不是存储参数,这是pg源码的符号常量。

TOAST Code将会compress 以及/或者 move 字段值 out-of-line直到row value短于TOAST_TUPLE_TARGET bytes(通常是2KB),

在update期间,没有变化的字段值会跟之前一样保持不变.

标签:

头像

小麦苗

学习或考证,均可联系麦老师,请加微信db_bao或QQ646634621

您可能还喜欢...

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

15 + 9 =

 

嘿,我是小麦,需要帮助随时找我哦
  • 18509239930
  • 个人微信

  • 麦老师QQ聊天
  • 个人邮箱
  • 点击加入QQ群
  • 个人微店

  • 回到顶部
返回顶部