1. 为什么需要 toast 技术
PostgreSQL 表数据的存储以页作为基本单位,默认情况下,一个页面 8kb 大小,pg 不允许一条记录跨多个页面,那么当一条记录的大小超过 8kb 时怎么处理?这就需要引入 toast 技术,英文全称为 The OverSized Attribute Storage Technique(超尺寸字段存储技术)。主要思路是使用额外的 toast 表来存储大字段数据,在原来的字段存储区内存储对应的 toast 表数据的 id。
2. 特定的字段类型才能使用 toast 机制
\d+ table_name
可以查看表字段的 Storage,分为如下几种类型:
- plain:避免压缩或行外存储
- main:允许压缩,不允许行外存储
- external:允许行外存储,不允许压缩
- extended:允许压缩和行外存储
对于可变长度的字段,其 Storage 通常为 extended 或者 external,可变长度的字段可通过 toast 机制存储大字段类型。
3. 普通表与 toast 表的关联
查询 pg_class 表,其 reltoastrelid 就是 toast 表的 oid,toast 表的表名后缀为普通表的 oid,举个例子:
普通表 t,oid 为 10022077,reltoastrelid 为 10022080
toast 表为:pg_toast_10022077,oid 为 10022080。
普通表与 toast 表在 pg_class 中通过字段 relkind 与可以区别,普通表为 'r',toast 表为 't'
4. 普通表的大字段 与 toast 表记录关联
- external 的字段数据的第 1 个字节必须为 0x01
- extended 的字段数据的第 1 个字节的低 2 位不能全部为 0,低 1 位表示是否 short 存储,低 2 位表示是否可压缩
源码参考宏定义:
- VARATT_IS_EXTERNAL
- VARATT_IS_EXTENDED
对于使用 toast 存储的字段,通常使用宏 VARATT_IS_EXTERNAL_ONDISK 来对字段数据进行判断,即第 1 个字节为 0x01,第 2 个字节为 0x12,后面是一个 varatt_external 结构,如下:
typedef struct varatt_external
{
int32 va_rawsize; /* Original data size (includes header) */
int32 va_extsize; /* External saved size (doesn't) */
Oid va_valueid; /* Unique ID of value within TOAST table */
Oid va_toastrelid; /* RelID of TOAST table containing it */
} varatt_external;
以上结构可以看出,一个 toast 字段,其存储的内容包括原始数据大小,外部数据大小,toast 表中数据的 chunk_id,toast 表的 oid。通过 oid,chunk_id,就能很容易的定位到字段数据。
toast 表中的数据通常是压缩的,判断是否压缩的方法是对比 va_rawsize 和 va_extsize 的大小,如下:
#define VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer) \
((toast_pointer).va_extsize < (toast_pointer).va_rawsize - VARHDRSZ)
对于 toast 表,其单条记录也是有大小限制的,对于一个大字段,其在 toast 表中会对应多条记录,这些记录的 chunk_id 相同,但是 chunk_seq 不同。
看一个 toast 例子:
下面是一个普通表大字段存储的值:
\x0112191800002d0d0000c3ec9800c0ec9800
- 0x0112 表示外部存储,即宏 VARATT_IS_EXTERNAL_ONDISK 对应的判断。
- 0x19180000 表示 4 字节原始存储大小
- 0x2d0d0000 表示 4 字节外部存储大小
- 0xc3ec9800 表示 toast 表记录的 chunk_id
- 0xc0ec9800 表示 toast 表的 oid
5. 涉及 toast 表的 DML 操作
对一个涉及大字段的普通表执行 DML 操作,普通表与 toast 表的 wal 写入顺序如下:
- insert 操作,先 insert toast 表记录,再对普通表进行 insert 操作
- update 操作,先 insert toast 表记录,再删除 toast 表记录,最后更新普通表。
- delete 操作,先删除普通表记录,再删除 toast 表记录
以上可以看出,insert 与 update 都是先对 toast 表进行操作,然后对普通表操作。而 delete 则相反。
内核版本:pg12