首页 > > > PostgreSQL的TOAST技术
本文参考:
TOAST是“The Oversized-Attribute Storage Technique”(超尺寸属性存储技术)的缩写,主要用于存储一个大字段的值。
要理解TOAST,我们要先理解页(BLOCK)的概念。在PG中,页是数据在文件存储中的基本单位,其大小是固定的且只能在编译期指定,之后无法修改,默认的大小为8KB。同时,PG不允许一行数据跨页存储。那么对于超长的行数据,PG就会启动TOAST,将大的字段压缩或切片成多个物理行存到另一张系统表中(TOAST表),这种存储方式叫行外存储。
只有特定的数据类型支持TOAST,因为那些整数、浮点数等不太长的数据类型是没有必要使用TOAST的。
另外,支持TOAST的数据类型必须是变长的。在变长类型中:

在 PG 中每个表字段有四种 TOAST 的策略:
首先创建一张 blog 表,查看它的各字段的TOAST策略:
postgres=# create table blog(id int, title text, content text);
CREATE TABLE
postgres=# \d+ blog;
Table "public.blog"
Column | Type | Modifiers | Storage | Stats target | Description
---------+---------+-----------+----------+--------------+-------------
id | integer | | plain | |
title | text | | extended | |
content | text | | extended | |可以看到,interger 默认 TOAST 策略为 PLAIN ,而 text 为 EXTENDED 。
另外可以修改某个字段系统默认分配的TOAST策略,假如要将上面blog表中的content字段的TOAST策略改成EXTERNAL,就可以这样:
ALTER TABLE blog ALTER content SET STORAGE EXTERNAL; 如果一个表中有任何一个字段是可以TOAST的,那么PostgreSQL会自动为该表建一个相关联的TOAST表,其OID存储在pg_class系统表的reltoastrelid记录里,行外的内容保存在TOAST表里。
查看blog表对应的TOAST表的OID:
postgres=# select relname,relfilenode,reltoastrelid from pg_class where relname='blog';
relname | relfilenode | reltoastrelid
---------+-------------+---------------
blog | 16441 | 16444
(1 row)通过上述语句,我们查到 blog 表的 oid 为16441,其对应 TOAST 表的 oid 为16444(关于 oid 和 pg_class 的概念,请参考PG官方文档),那么其对应 TOAST 表名则为: pg_toast.pg_toast_16441(注意这里是 blog 表的 oid )。
行外存储被切成了多个Chunk块,每个Chunk块大约是一个BLOCK的四分之一大小,如果块大小为8KB(默认就是8KB),则Chunk大约为2KB(比2KB略小一点),每个Chunk都作为独立的行存储在TOAST表中。
TOAST表有三个字段:
我们看下上面的TOAST表pg_toast.pg_toast_16441的定义:
postgres=# \d+ pg_toast.pg_toast_16441;
TOAST table "pg_toast.pg_toast_16441"
Column | Type | Storage
------------+---------+---------
chunk_id | oid | plain
chunk_seq | integer | plain
chunk_data | bytea | plain在chunk_id和chunk_seq上有一个唯一的索引,提供对数值的快速检索。
因此,一个表示行外存储的指针数据中包括了要查询的TOAST表的OID和特定数值的chunk_id(也是一个OID类型)。为了方便,指针数据还存储了逻辑数据的尺寸(原始的未压缩的数据长度)及实际存储的尺寸(如果使用了压缩,则两者不同)。加上头部的长度字,一个TOAST指针数据的总尺寸是20字节。
现在我们来实际验证下TOAST:
postgres=# insert into blog values(1, 'title', '0123456789');
INSERT 1
postgres=# select * from blog;
id | title | content
----+-------+------------
1 | title | 0123456789
(1 row)
postgres=# select * from pg_toast.pg_toast_16441;
chunk_id | chunk_seq | chunk_data
----------+-----------+------------
( rows)可以看到因为 content 只有10个字符,所以没有压缩,也没有行外存储。然后我们使用如下 SQL 语句增加 content 的长度,每次增长1倍,同时观察 content 的长度,看看会发生什么情况?
postgres=# update blog set content=content||content where id=1;
UPDATE 1
postgres=# select id,title,length(content) from blog;
id | title | length
----+-------+--------
1 | title | 20
(1 row)
postgres=# select * from pg_toast.pg_toast_16441;
chunk_id | chunk_seq | chunk_data
----------+-----------+------------
( rows)反复执行如上过程,直到 pg_toast_16441 表中有数据:
postgres=# select id,title,length(content) from blog;
id | title | length
----+-------+--------
1 | title | 327680
(1 row)
postgres=# select chunk_id,chunk_seq,length(chunk_data) from pg_toast.pg_toast_16441;
chunk_id | chunk_seq | length
----------+-----------+--------
16439 | | 1996
16439 | 1 | 1773
(2 rows)可以看到,直到 content 的长度为327680时(已远远超过页大小 8K),对应 TOAST 表中才有了2行 数据,且长度都是略小于2K,这是因为 extended 策略下,先启用了压缩,然后才使用行外存储。
下面我们将 content 的 TOAST 策略改为 EXTERNAL ,以禁止压缩。
postgres=# alter table blog alter content set storage external;
ALTER TABLE
postgres=# \d+ blog;
Table "public.blog"
Column | Type | Modifiers | Storage | Stats target | Description
---------+---------+-----------+----------+--------------+-------------
id | integer | | plain | |
title | text | | extended | |
content | text | | external | |然后我们再插入一条数据:
postgres=# insert into blog values(2, 'title', '0123456789');
INSERT 1
postgres=# select id,title,length(content) from blog;
id | title | length
----+-------+--------
1 | title | 327680
2 | title | 10
(2 rows)然后重复以上步骤,直到 pg_toast_16441 表中有数据:
postgres=# update blog set content=content||content where id=2;
UPDATE 1
postgres=# select id,title,length(content) from blog;
id | title | length
----+-------+--------
2 | title | 327680
1 | title | 327680
(2 rows)
postgres=# select chunk_id,chunk_seq,length(chunk_data) from pg_toast.pg_toast_16441;
chunk_id | chunk_seq | length
----------+-----------+--------
16447 | | 1996
16447 | 1 | 1773
16448 | | 1996
16448 | 1 | 1996
16448 | 2 | 1996
....(省略)
16448 | 164 | 1996
(167 rows)因为不允许压缩,所以新的操作在TOAST表中生成了更多Chunk块行记录。通过以上操作得出以下结论:
TOAST比那些更直接的方法(比如允许行值跨越多个页面)有更多优点。 假设查询通常是用相对比较短的键值进行匹配的,那么执行器的大多数工作都将使用主行项完成。TOAST过的属性的大值只是在把结果集发送给客户端的时候才被抽出来(如果它被选中)。 因此,主表要小得多,并且它的能放入到共享缓冲区中的行要比没有任何行外存储的方案更多。 排序集也缩小了,并且排序将更多地在内存里完成。一个小测试表明,一个典型的保存 HTML 页面以及它们的 URL 的表占用的存储(包括TOAST表在内)大约只有裸数据的一半,而主表只包含全部数据的 10%(URL和一些小的 HTML 页面)。与在一个非TOAST的对照表里面存储(把全部 HTML 页面裁剪成 7Kb 以匹配页面大小)同样的数据相比,运行时没有任何区别。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/31537575/viewspace-3134402/ ,如需转载,请注明出处,否则将追究法律责任。