KingbaseES toast技术原理及实现

前言

1、TOAST的作用
TOAST全称是The Oversized-Attribute Storage Technique, 超大属性存储技术,就是超长字段在数据库中的存储方式。主要用来应对物理数据行超大的场景。

在KingbaseES中,页是数据在文件存储中的基本单位,它的大小是固定的,并且只能在编译期指定,默认的大小为8kb。同时不允许一行数据跨页存储。因此页大小就是行大小的硬上限,因此,引入了TOAST技术,TOAST技术是采用压缩和切片的方式,使得行的大小变小。需要说明这一技术对用户来说是完全透明的。

2、关于toast压缩
我们创建的每个表都有自己关联且唯一的TOAST表。当数据超过TOAST_TUPLE_THRESHOLD(默认2KB)时,数据库将压缩数据。
如果对大列数据的压缩后没有得到更小的块(<2KB),那么它将被拆分为更小的块,并存储在相关TOAST表中多个物理行(行外存储)。
每个原始表字段值都被一个指针替换,实际上根据这个指针可以找到对应toast表中行外存储的数据。

3、数据库中表字段的TOAST策略
PLAIN: 避免压缩和行外存储。
EXTENDED:允许压缩和行外存储。
EXTERNAL: 允许行外存储,但不允许压缩。
MAIN: 允许压缩,但不允许行外存储。

4、查看表字段的TOAST策略

[复制代码](javascript:void(0)😉

EST=# \d+ t1
                               Table "PUBLIC.T1"
 Column |       Type        | Modifiers | Storage  | Stats target | Description
--------+-------------------+-----------+----------+--------------+-------------
 ID     | INTEGER           |           | plain    |              |
 NAME   | CHARACTER VARYING |           | extended |              |

[复制代码](javascript:void(0)😉

其中的Storage对应字段使用的TOAST策略。

5、修改表字段的TOAST策略
alter table tablename alter column column_name set storage EXTENDED;

toast技术的实现方式:

[复制代码](javascript:void(0)😉

创建一张 tosttest 表

test=# create table tosttest(id int, tad text, name text);
CREATE TABLE
TEST=# \d+ tosttest;
                                  Table "public.tosttest"
 Column |  Type   | Collation | Nullable | Default | Storage  | Stats target | Description
--------+---------+-----------+----------+---------+----------+--------------+-------------
 id     | integer |           |          |         | plain    |              |
 tad    | text    |           |          |         | extended |              |
 name   | text    |           |          |         | extended |              |
Access method: heap

如上,interger类型默认TOAST策略为plain,而text类型为extended,所以对于非变长数据类型默认是不需要toast存储的。如果表中字段需要用到TOAST,那么数据库会自动创建一张TOAST表负责行外存储。

test=# select relname,relfilenode,reltoastrelid from sys_class where relname='tosttest';
 relname  | relfilenode | reltoastrelid
----------+-------------+---------------
 tosttest |       92932 |         92935
(1 row)

如上,我们查到 tosttest 表的 oid为92932,其对应 TOAST 表的 oid为92935,其对应TOAST表名则为:pg_toast.pg_toast_92932,我们可以看下定义:

test=# \d+ pg_toast.pg_toast_92932;
TOAST table "pg_toast.pg_toast_92932"
   Column   |  Type   | Storage 
------------+---------+---------
 chunk_id   | oid     | plain
 chunk_seq  | integer | plain
 chunk_data | bytea   | plain


验证toast表:
test=# insert into tosttest values(1, 'tad', '0123456789');
INSERT 0 1
test=# select * from tosttest;
 id | tad |  name   
----+-------+------------
  1 | tad | 0123456789
(1 row)

test=# select * from pg_toast.pg_toast_92932;
 chunk_id | chunk_seq | chunk_data 
----------+-----------+------------
(0 rows)
可以看到因为name只有10个字符,所以没有压缩,也没有行外存储。下面增加 name 的长度,看看会发生什么

test=# update tosttest set name=name||name where id=1;
UPDATE 1
test=# select id,tad,length(name) from tosttest;
 id | tad | length 
----+-------+--------
  1 | tad |     20
(1 row)
test=# select * from pg_toast.pg_toast_92932;
 chunk_id | chunk_seq | chunk_data 
----------+-----------+------------
(0 rows)
反复执行如上过程,直到 pg_toast_92932 表中有数据:

test=# select id,tad,length(name) from tosttest;
 id | tad | length 
----+-------+--------
  1 | tad | 327680
(1 row)

TEST=#  select chunk_id,chunk_seq,length(chunk_data) from pg_toast.pg_toast_92932;
 chunk_id | chunk_seq | length
----------+-----------+--------
    92938 |         0 |   1988
    92938 |         1 |   1781
(2 rows)

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

下面我们将 name字段的 TOAST 策略改为 EXTERNA ,禁止压缩。

test=# alter table tosttest alter name set storage external;
ALTER TABLE
test=# \d+ tosttest;
                          Table "public.tosttest"
 Column  |  Type   | Modifiers | Storage  | Stats target | Description 
---------+---------+-----------+----------+--------------+-------------
 id      | integer |           | plain    |              | 
 tad     | text    |           | extended |              | 
 name    | text    |           | external |              |

然后我们重新插入一条数据:
test=# insert into tosttest values(2, 'tad', '0123456789');
INSERT 0 1
test=# select id,tad,length(name) from tosttest;
 id | tad | length 
----+-------+--------
  1 | tad | 327680
  2 | tad |     10
(2 rows)
再次重复以上步骤,直到TOAST表中产生新的行:

test=# update tosttest set name=name||name where id=2;
UPDATE 1
TEST=#  select id,tad,length(name) from tosttest;
 id | tad | length
----+-----+--------
  1 | tad | 327680
  2 | tad |   5120
(2 rows)


TEST=# select chunk_id,chunk_seq,length(chunk_data) from pg_toast.pg_toast_92932;
 chunk_id | chunk_seq | length
----------+-----------+--------
    92938 |         0 |   1988
    92938 |         1 |   1781
    92940 |         0 |   1988
    92940 |         1 |   1988
    92940 |         2 |   1144
(5 rows)我们看到当, TOAST 表中产生了新的3条 chunk_id 为92940的行,且3行数据的 chunk_data 的长度之和正好等于name字段长度5120,所以这里并没有压缩。

[复制代码](javascript:void(0)😉

总结:

如果存储策略允许压缩,则TOAST优先选择压缩。

不管是否压缩,一旦数据超过2KB,就会启用行外存储,数据存储到toast表中。

修改TOAST策略,不会影响现有数据的存储方式。

posted @ 2023-02-28 17:43  KINGBASE研究院  阅读(79)  评论(0编辑  收藏  举报