PostgreSQL 用久了,你迟早会撞上一个让人挠头的现象:一张表里存了 100MB 的文本,翻遍主表的数据文件却发现它只占了几百 KB,真正的大头全躲在一个叫pg_toast的角落里;又或者反过来,明明只是插入一条不大的记录,磁盘写入却慢得离谱。这些反直觉的表现,几乎都和 PostgreSQL 里一个低调但极其关键的技术有关——TOAST。TOAST 全称是 The Oversized-Attribute Storage Technique,直译过来就是"超大属性存储技术"。它是 PostgreSQL 处理大字段(text、bytea、jsonb、数组等变长类型)的底层机制,决定了这些字段能不能被压缩、要不要被搬出主表、以及读写时到底付出了多少代价。不管你是在做日志系统、内容平台、地理数据的存储,还是整天和 jsonb 打交道,只要碰大字段,就一定会和 TOAST 打交道。这篇就把 TOAST 从触发条件、存储策略、压缩分块到落盘结构完整拆一遍,再配上我实际动手跑出来的观测数据,让你看完就能自己诊断生产环境里的 TOAST 问题。
1. 先讲清楚 TOAST 到底在解决什么矛盾
理解 TOAST 之前,得先理解 PostgreSQL 存储层的一个硬约束。很多人对"数据库能存大字段"这件事想当然,觉得既然字段类型叫 text、bytea,那往里塞多大都行。能塞确实是能塞,但塞进去之后怎么放、放哪里、取的时候怎么拼回来,背后全是设计取舍。TOAST 就是被这个硬约束"逼"出来的方案。
1.1 8KB 的页,和"一行必须装进一页"
PostgreSQL 的磁盘存储单位是页(page,也叫 block),默认大小 8KB(编译时的BLCKSZ,通常就是 8192 字节)。堆表(heap table)里所有的行,都是按页组织的,每一行必须完整地落在某一个页里,不允许跨页。为什么不允许跨页?因为堆表给每一行只维护一个行指针(ItemId),里面记录的是页内偏移;一旦允许一行跨页,指针就没法用一个偏移量表达,顺序扫描和 MVCC 的页内可见性判断都会变得复杂。所以 PostgreSQL 干脆规定:**一行数据的所有内容,除