12.pg_toast 详解
pg_toast 详解
一、TOAST 是什么
PostgreSQL 的页固定为 8 KB,且不允许一条元组跨页。TOAST(The Oversized-Attribute Storage Technique)就是为此设计的“大字段幕后仓库”:当行内变长字段(TEXT、BYTEA、JSONB、数组等)超过约 2 KB 时,系统先把数据压缩,再把压缩后仍大的部分拆成若干 chunk,转存到一张独立的“TOAST 表”里,原记录只保留一个 20 B 左右的指针,整个过程对 SQL 完全透明 。
二、物理结构
- 1.主表:
- pg_class.reltoastrelid 登记其附属 TOAST 表的 OID。
- 2.TOAST 表:命名规则 pg_toast.pg_toast_<主表 OID>,系统自动创建,包含 3 个固定列:
- chunk_id 标识同一大字段
- chunk_seq 分片序号
- chunk_data 实际分片数据(默认最大 2 KB,受 toast_max_chunk_size 限制)
另带一个唯一索引 (chunk_id, chunk_seq),保证按序读取 。
- 3.行内指针(TOAST pointer):
- 保存压缩算法、原始长度、首 chunk 的 OID/序号,主表行通过它定位外部数据
三、触发阈值
- TOAST_TUPLE_THRESHOLD ≈ 2 KB(1/4 页)—— 行内任一可 TOAST 列达到此大小即启动机制。
- TOAST_TUPLE_TARGET 也≈2 KB,系统尽量把处理后元组压到该大小以下,以便一个页至少存 4 行 。
四、4 种列级存储策略(ALTER TABLE … SET STORAGE)
| 策略 | 行外存储 | 压缩 | 适用场景 |
|---|---|---|---|
| PLAIN | × | × | 小字段或固定长度类型 |
| EXTENDED | √ | √ | 默认,大文本/JSONB 等 |
| EXTERNAL | √ | × | 已压缩文件、需子串访问 |
| MAIN | 仅最后手段 | √ | 想优先留在行内,但允许极端情况行外 |
五、内部流程(插入/更新)
- 判断是否超过阈值 → 2. 尝试压缩(LZ 系列)→ 3. 仍超大则切片 → 4. 写入 TOAST 表 → 5. 主表列值换成指针 。
- 查询时,执行器按需“反 TOAST”(de-TOAST):先读指针,再按 chunk_id 把分片拉回、解压、拼回完整值;若只是取 substring 或 byte 范围,可直接定位相关 chunk,避免全值拉回
六、空间与维护
- TOAST 数据同样受 MVCC 管理,更新主表行会写新 chunk,旧 chunk 延迟回收;
- VACUUM(或 autovacuum)负责清理 TOAST 表中被删除的 chunk,并回收空间 ;
- 可用 pgstattuple 扩展查看 TOAST 表膨胀:
SELECT * FROM pgstattuple('pg_toast.pg_toast_12345'); - 监控磁盘时别忘了把 TOAST 表和它的索引一起算进去
七、常见误区
- “TOAST 表需要手动建”——不需要,系统自动维护;
- “TOAST 只存 TEXT”——所有变长(varlena)类型都可能进 TOAST;
- “行外就一定慢”——EXTENDED 策略下,压缩率好反而减少 I/O;只有在频繁随机读取小段数据时 EXTERNAL 才更优。
总结:TOAST 通过“压缩 + 行外分片”让 PostgreSQL 在 8 KB 页面限制下仍能存储 1.6 TB 级行,开发者几乎零感知,只需根据访问模式选好存储策略并定期 VACUUM 即可
PostgreSQL TOAST 技术理解
TOAST 是“ The Oversized-Attribute Storage Technique ”的缩写,主要用于存储一个大字段的值。要理解 TOAST ,我们要先理解页( BLOCK )的概念。在 PG 中,页是数据在文件存储中的基本单位,其大小是固定的且只能在编译期指定,之后无法修改,默认的大小为8 KB 。同时,PG 不允许一行数据跨页存储,那么对于超长的行数据,PG 就会启动 TOAST ,具体就是采用压缩和切片的方式。如果启用了切片,实际数据存储在另一张系统表的多个行中,这张表就叫 TOAST 表,这种存储方式叫行外存储。
在深入细节之前,我们要先了解,在 PG 中每个表字段有四种 TOAST 的策略:
- PLAIN :避免压缩和行外存储。只有那些不需要 TOAST 策略就能存放的数据类型允许选择(例如 int 类型),而对于 text 这类要求存储长度超过页大小的类型,是不允许采用此策略的
- EXTENDED :允许压缩和行外存储。一般会先压缩,如果还是太大,就会行外存储
- EXTERNA :允许行外存储,但不许压缩。类似字符串这种会对数据的一部分进行操作的字段,采用此策略可能获得更高的性能,因为不需要读取出整行数据再解压。
- MAIN :允许压缩,但不许行外存储。不过实际上,为了保证过大数据的存储,行外存储在其它方式(例如压缩)都无法满足需求的情况下,作为最后手段还是会被启动。因此理解为:尽量不使用行外存储更贴切。
现在我们通过实际操作来研究 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 。PG 资料告诉我们,如果表中有字段需要 TOAST ,那么系统会自动创建一张 TOAST 表负责行外存储,那么这张表在哪里?
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 ),我们看下其定义:
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
TOAST 表有3个字段:
- chunk_id :用来表示特定 TOAST 值的 OID ,可以理解为具有同样 chunk_id 值的所有行组成原表(这里的 blog )的 TOAST 字段的一行数据
- chunk_seq :用来表示该行数据在整个数据中的位置
- chunk_data :实际存储的数据。
现在我们来实际验证下:
postgres=# insert into blog values(1, 'title', '0123456789');
INSERT 0 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
----------+-----------+------------
(0 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
----------+-----------+------------
(0 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 | 0 | 1996
16439 | 1 | 1773
(2 rows)
可以看到,直到 content 的长度为327680时(已远远超过页大小 8K),对应 TOAST 表中才有了2行数据,且长度都是略小于2K,这是因为 extended 策略下,先启用了压缩,然后才使用行外存储。
下面我们将 content 的 TOAST 策略改为 EXTERNA ,以禁止压缩。
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 0 1
postgres=# select id,title,length(content) from blog;
id | title | length
----+-------+--------
1 | title | 327680
2 | title | 10
(2 rows)
然后重复以上步骤,直到TOAST表中产生新的行:
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 | 2560
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 | 0 | 1996
16447 | 1 | 1773
16448 | 0 | 1996
16448 | 1 | 564
(4 rows)
这次我们看到当 content 长度达到2560(按照官方文档,应该是超过2KB左右), TOAST 表中产生了新的2条 chunk_id 为16448的行,且2行数据的 chunk_data 的长度之和正好等于2560。通过以上操作得出以下结论:
- 如果策略允许压缩,则TOAST优先选择压缩。
- 不管是否压缩,一旦数据超过2KB左右,就会启用行外存储。
- 修改TOAST策略,不会影响现有数据的存储方式。

浙公网安备 33010602011771号