TOAST技术
什么是TOAST技术
TOAST是The Oversized-Attribute Storage Technique的缩写,即行外存储,主要用于存储大字段的值。由于PostgreSQL块的大小是固定的(通常是8KB),且不允许行跨越多个页面,因此不可能存储非常大的字段值。为了突破这个限制,大的字段值通常被压缩或者切片成多个物理行存储到另一张系统表中,即TOAST表。只有特定的数据类型支持TOAST技术,整数、浮点等不太长的数据类型没必要使用TOAST技术。另外,支持TOAST技术的数据类型必须是变长的。
长度字
- 在变长类型中前4个字节(32bit)称为长度字,长度字后面存储的是具体内容或指针。
- 长度字的高2bit是标志位,后面的30位是长度值。由此可见TOAST数据类型的逻辑长度最多是30bit,即1GB之内(2^30 - 1)。
- 前2bit一个表示是否压缩,一个表示是否行外存储,如果两个都是0则表示该字段值既未被压缩也没有行外存储。如果设置了第一位则表示该数值被压缩过,使用前必须解压。如果设置了另外一位,则表示使用的是行外存储。此时长度字后面的部分只是一个指针,指向存储实际数据的TOAST表的位置。如果两个都设置了,那么这个行外数据也被压缩过了。不管哪种情况,长度字里剩下的30bit的长度值都表示数据的实际尺寸,而不是压缩过的数据长度。
- 只有当数据长度超过2040字节(大约一个块的四分之一)时才会触发压缩。在PostgreSQL 11之后可以通过toast_tuple_target参数来改变这个值的大小
- alter table test01 set(toast_tuple_target=128);
- 如果没有使用行外存储则长度字后面的内容则是该字段具体的值
TOAST 的策略
- PLAIN :避免压缩和行外存储。只有那些不需要 TOAST 策略就能存放的数据类型允许选择(例如 int 类型),而对于 text 这类要求存储长度超过页大小的类型,是不允许采用此策略的
- EXTENDED :允许压缩和行外存储。一般会先压缩,如果还是太大,就会行外存储
- EXTERNA :允许行外存储,但不许压缩。类似字符串这种会对数据的一部分进行操作的字段,采用此策略可能获得更高的性能,因为不需要读取出整行数据再解压。
- MAIN :允许压缩,但不许行外存储。不过实际上,为了保证过大数据的存储,行外存储在其它方式(例如压缩)都无法满足需求的情况下,作为最后手段还是会被启动。因此理解为:尽量不使用行外存储更贴切。
例子
创建一张 blog 表
testdb=# create table blog(id int, title text, content text);
CREATE TABLE
testdb=# \d+ blog ;
Table "public.blog"
Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description
---------+---------+-----------+----------+---------+----------+-------------+--------------+-------------
id | integer | | | | plain | | |
title | text | | | | extended | | |
content | text | | | | extended | | |
Access method: heap
可以看到,interger 默认 TOAST 策略为 plain ,而 text 为 extended 。PG 资料告诉我们,如果表中有字段需要 TOAST ,
那么系统会自动创建一张 TOAST 表负责行外存储,那么这张表在哪里?
testdb=# select relname,relfilenode,reltoastrelid from pg_class where relname='blog';
relname | relfilenode | reltoastrelid
---------+-------------+---------------
blog | 19036 | 19039
(1 row)
通过上面语句,我们查到 blog 表的 oid 为19036,其对应 TOAST 表的 oid 为19039
那么其对应 TOAST 表名则为: pg_toast.pg_toast_19036(注意这里是 blog 表的 oid ),我们看下其定义:
testdb=# \d+ pg_toast.pg_toast_19036;
TOAST table "pg_toast.pg_toast_19036"
Column | Type | Storage
------------+---------+---------
chunk_id | oid | plain
chunk_seq | integer | plain
chunk_data | bytea | plain
Owning table: "public.blog"
Indexes:
"pg_toast_19036_index" PRIMARY KEY, btree (chunk_id, chunk_seq)
Access method: heap
TOAST 表有3个字段
- chunk_id :用来表示特定 TOAST 值的 OID ,可以理解为具有同样 chunk_id 值的所有行组成原表(这里的 blog )的 TOAST 字段的一行数据
- chunk_seq :用来表示该行数据在整个数据中的位置
- chunk_data :实际存储的数据。
插入数据
此时blog表中content的长度只有10个字符,所以既没有压缩也没有行外存储,此时toast表中没有数据
testdb=# select * from blog;
id | title | content
----+-------+------------
1 | title | 0123456789
(1 row)
testdb=# select * from pg_toast.pg_toast_19036;
chunk_id | chunk_seq | chunk_data
----------+-----------+------------
(0 rows)
使用以下语句增加content的长度,每次增加一倍,重复执行,直到toast表中有数据。直到content的长度为327680时(已远远超过页大小8K,8K可以表示英文字符的长度为8*1024),对应TOAST表中才有了2行数据,且长度都是略小于2K,这是因为extended策略下,先启用了压缩,然后才使用行外存储。
testdb=# update blog set content=content||content where id=1;
UPDATE 1
testdb=# select chunk_id,chunk_seq,length(chunk_data) from pg_toast.pg_toast_19036;
chunk_id | chunk_seq | length
----------+-----------+--------
19050 | 0 | 1996
19050 | 1 | 1773
(2 rows)
testdb=# select id,title,length(content) from blog;
id | title | length
----+-------+--------
1 | title | 327680
(1 row)
下面我们将content的TOAST策略改为EXTERNA,以禁止压缩
testdb=# alter table blog alter content set storage external;
ALTER TABLE
testdb=# \d+ blog;
Table "public.blog"
Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description
---------+---------+-----------+----------+---------+----------+-------------+--------------+-------------
id | integer | | | | plain | | |
title | text | | | | extended | | |
content | text | | | | external | | |
Access method: heap
重复执行,直到toast表中有数据
testdb=# insert into blog values(2, 'title', '0123456789');
INSERT 0 1
testdb=# update blog set content=content||content where id=2;
UPDATE 1
testdb=# select chunk_id,chunk_seq,length(chunk_data) from pg_toast.pg_toast_19036;
chunk_id | chunk_seq | length
----------+-----------+--------
19050 | 0 | 1996
19050 | 1 | 1773
19051 | 0 | 1996
19051 | 1 | 564
(4 rows)
此时数据大小为2560字节(按照官方文档应该是超过2KB左右),pg_toast.pg_toast_19036中产生了两条chunk_id为19051的行,且两个数据和刚好为2560
testdb=# select id,title,length(content) from blog;
id | title | length
----+-------+--------
2 | title | 2560
1 | title | 327680
(2 rows)
通过以上操作得出以下结论:
- 如果策略允许压缩,则TOAST优先选择压缩
- 不管是否压缩,一旦数据超过2KB左右,就会启用行外存储
- 修改TOAST策略,不会影响现有数据的存储方式
toast的优势
- 可以存储超长超大字段,避免之前不能直接存储的限制
- 物理上与普通表是分离的,检索查询时不检索到该字段会极大地加快速度
- 更新普通表时,该表的Toast数据没有被更新时,不用去更新Toast表
toast的劣势:
- 对大字段的索引创建是一个问题,有可能会失败,其实通常也不建议在大字段上创建,全文检索倒是一个解决方案。
- 大字段的更新会有点慢,其它DB也存在,通病