Skip to content

底层存储模型与 TOAST 机制

堆表文件组织、8KB 数据页物理二进制结构、元组双向对冲分配与 TOAST 超长属性存储。

Updated View as Markdown
For humans

底层存储模型与 TOAST 机制

校招考察频度:☆ 中频(底层机制·大厂底层推导) 一句话速记:物理三件套(主数据、FSM与VM),8K 页内双向对冲长;槽位向下数据向上走,超两千字节 TOAST 压缩切片藏。

PostgreSQL 的物理存储架构以“堆表(Heap)+ 8KB 页结构 + TOAST 扩展”为基石。在校招面试中,如果能清晰画出 8KB 数据页的内存布局,并讲透为什么元组不能跨页以及 TOAST 的四种策略,能给面试官留下极深的科班功底印象。


一、堆表的物理文件三件套

在 PostgreSQL 的数据目录(base/<database_oid>/)下,每张数据表在物理磁盘上并不是单一的大文件,而是由至少三个文件协同组成的(以关系对象 OID 命名,如 16384):

文件后缀文件全称核心职责访问与维护机制
无后缀主文件Relation Data File存放实际的行数据元组(Tuple)。当单文件超过 1GB 时,自动切分为 16384.1、16384.2 等分段。读写通过 shared_buffers 缓存,按 8KB 块进行 I/O。
_fsmFree Space Map(空闲空间映射)以多叉树形式记录每个 8KB 数据页中剩余可用空闲空间的大小。当执行 INSERT 时,引擎直接查 FSM 快速找到哪个已有页装得下,避免每次都追加新页或顺序翻找。由 VACUUM 维护。
_vmVisibility Map(可见性映射)每个数据页用 2 个 bit 记录状态:
① 该页是否全部元组可见(All-Visible);
② 该页是否全部元组已冻结(All-Frozen)。
两大神效:① 支撑 Index-Only Scan(标记为 All-Visible 的页读索引免回表);② 让 VACUUM 跳过全可见页,大幅缩短垃圾回收耗时。

二、8KB 数据页(Page)的底层二进制布局

PostgreSQL 规定:数据页是磁盘与内存之间 I/O 交换的最小原子物理单位,默认固定为 8192 字节(8KB)。

一个标准数据页在内存与磁盘上的物理布局采用了极具工程智慧的**“双向对冲生长”**结构:

8KB 数据页物理内存布局(双向对冲生长)

1. 核心组成部分拆解

  1. PageHeaderData(页头,固定 24 字节):
    • pd_lsn(8 字节):最近一次修改该数据页的 WAL 日志序列号(LSN)。在崩溃恢复(Crash Recovery)时,引擎拿数据页的 pd_lsn 与 WAL 记录的 LSN 对比,只重放比当前页更年轻的修改;
    • pd_checksum(2 字节):页面校验和,防止磁盘静默损坏;
    • pd_lower(2 字节):指向行指针数组(ItemId)的末尾偏移量(未分配空间的起点);
    • pd_upper(2 字节):指向最新写入元组的起始偏移量(未分配空间的终点)。
  2. ItemIdData(行指针数组 / 槽位):
    • 每个槽位占用 4 字节;
    • 记录元组在当前页内的起始偏移量(Offset)与长度(Length);
    • CTID 的第 2 个数字指的就是这个槽位下标!例如 ctid = (10, 3) 表示第 10 号数据块中的第 3 个 ItemId 指针。
  3. 元组真实数据区(Tuple Data):
    • 包含元组头(xmin, xmax, t_cid, t_ctid, t_infomask, NULL 位图)及用户真实列数据;
    • 逆向生长特性:新写入的元组从页的最底部(8192 字节处)开始向上填充,而 ItemId 指针从顶部向下增长;两者在中间的 Free Space 相向而行。当 pd_lower == pd_upper 时,说明当前页已被填满。

三、TOAST 机制(超限属性存储技术)

1. 为什么必须有 TOAST?

  • 物理铁律:PostgreSQL 严禁一条元组跨越两个数据页存储!一条元组的大小必须严格限制在单个 8KB 数据页内部。
  • 痛点:若业务表中包含大文本(TEXT)、JSON(JSONB)、长二进制数据(BYTEA),单个字段动辄几十 KB 甚至几 MB,单页根本装不下。
  • 解法:TOAST(The Oversized-Attribute Storage Technique)!

2. 触发阈值与工作流程

  • 触发阈值:当一行元组的总大小超过约 2KB(即 TOAST_TUPLE_THRESHOLD,通常为页大小的 1/4)时,TOAST 机制被强制唤醒:
TOAST 执行四部曲:
1. 评估超长字段:检查哪些字段属于可压缩或变长类型 (varlena);
2. 就地压缩:使用 LZ4 或 pglz 算法对字段数据进行高效无损压缩;
3. 外存切片:若压缩后仍然超标,系统将超长大字段切分成多个约 2KB 的数据块 (Chunks);
4. 写入独立 TOAST 表:切片被存入专用的附属 TOAST 物理表 (如 pg_toast_16384) 中;
5. 主表留指针:主表元组中该字段的原位置只留下一个 18 字节的 toast_pointer。

TOAST 外存切片机制

3. TOAST 的四大存储策略(必背)

开发者可以通过 ALTER TABLE 表名 ALTER COLUMN 列名 SET STORAGE 策略; 手动配置:

策略模式允许压缩?允许移入外部 TOAST 表?典型适用场景
EXTENDED(系统默认)✅ 优先压缩✅ 仍超标则外存大多数 TEXT、VARCHAR、JSONB 默认首选。
MAIN✅ 极力压缩⚠️ 仅当全行仍超标时外存适合频繁读取的中等大小文本,尽量保留在主表加速查询。
EXTERNAL❌ 禁止压缩✅ 直接移入外部表性能优化点:原本就已被压缩的多媒体数据(如 JPG 图片、MP3、GZIP 压缩包)。禁止压缩可以避免浪费大量 CPU 算力进行无用功压缩尝试。
PLAIN❌ 禁止压缩❌ 严禁移入外部表仅用于基础定长类型(如 INT, BIGINT, DATE)。若行超标直接报错。

4. TOAST 对查询性能的影响(面试加分深度)

  • 显著正向收益:当执行 SELECT id, name FROM users; 时,引擎只扫描紧凑的主表数据页,完全跳过体积庞大的 TOAST 表。这使得单个主表数据页能容纳更多普通元组,大幅提升内存命中率与全表扫描吞吐。
  • 潜在性能陷阱:若业务代码随意写 SELECT *,把 TOAST 的超长字段也读出来,引擎将被迫通过索引去附属 TOAST 表把所有 2KB 的切片拼装回来并解压缩,产生严重的磁盘随机 I/O 与 CPU 开销。因此高并发下严禁无脑 SELECT *!
Navigation

Type to search…

↑↓ navigate↵ selectEsc close