当前位置: 首页 > news >正文

PostgreSQL 12.2 源码探秘:手把手带你拆解Heap表文件,看懂数据在磁盘上的真实模样

PostgreSQL 12.2 存储引擎深度解析:从二进制文件到内存结构的完整映射

在数据库系统的核心层,存储引擎扮演着数据持久化和高效访问的关键角色。PostgreSQL作为一款开源关系型数据库,其存储引擎的设计哲学体现了对可靠性和扩展性的极致追求。本文将带领读者深入PostgreSQL 12.2的存储引擎内部,通过直接分析磁盘文件结构和内存映射机制,揭示数据从存储介质到查询结果的完整生命周期。

1. PostgreSQL存储引擎架构概览

PostgreSQL的存储引擎采用经典的堆表(Heap Table)结构,这种设计源于传统关系型数据库的存储范式。堆表的核心特点是数据以非聚集方式存储,行数据可以存放在表的任何位置,通过额外的索引结构实现快速访问。

存储引擎的主要组件包括:

  • 表空间管理:负责数据库文件的物理存储布局
  • 页面管理:将数据文件划分为固定大小的页面(默认为8KB)
  • 元组存储:处理行数据的物理存储格式
  • 事务可见性:通过多版本并发控制(MVCC)实现事务隔离
  • 空闲空间管理:跟踪和重用被删除数据占用的空间

在磁盘上,一个PostgreSQL数据库表现为$PGDATA/base目录下的一系列文件。每个数据库对应一个子目录,目录名是该数据库的OID。表数据存储在名为relfilenode的文件中,通常与表的OID相同。

$ ls -l $PGDATA/base/16384 total 1024 -rw------- 1 postgres postgres 8192 Jan 10 10:00 16430 -rw------- 1 postgres postgres 8192 Jan 10 10:00 16430_fsm -rw------- 1 postgres postgres 8192 Jan 10 10:00 16430_vm

2. 堆表文件的物理结构解析

2.1 页面布局剖析

PostgreSQL将每个表文件划分为固定大小的页面(默认为8KB),每个页面包含以下关键部分:

区域偏移量大小描述
PageHeader024字节页面元数据信息
LinePointer数组24变长指向元组的指针数组
空闲空间变长变长可用于存储新元组的空间
特殊空间页面末尾变长用于特殊用途的空间

页面头部(PageHeaderData)的结构定义如下:

typedef struct PageHeaderData { PageXLogRecPtr pd_lsn; /* 最后修改的LSN */ uint16 pd_checksum; /* 页面校验和 */ uint16 pd_flags; /* 标志位 */ LocationIndex pd_lower; /* 空闲空间起始位置 */ LocationIndex pd_upper; /* 空闲空间结束位置 */ LocationIndex pd_special; /* 特殊空间起始位置 */ uint16 pd_pagesize_version; /* 页面大小和版本 */ TransactionId pd_prune_xid; /* 可回收的最老XID */ ItemIdData pd_linp[FLEXIBLE_ARRAY_MEMBER]; /* 行指针数组 */ } PageHeaderData;

2.2 元组存储格式

每个元组(行数据)在页面中的存储分为三个部分:

  1. HeapTupleHeader:元组的元数据信息
  2. NULL位图:标识哪些列值为NULL
  3. 用户数据:实际的列值数据

HeapTupleHeader的关键字段包括:

typedef struct HeapTupleFields { TransactionId t_xmin; /* 插入事务XID */ TransactionId t_xmax; /* 删除/更新事务XID */ union { CommandId t_cid; /* 插入/删除命令ID */ TransactionId t_xvac; /* VACUUM操作XID */ } t_field3; } HeapTupleFields; typedef struct HeapTupleHeaderData { union { HeapTupleFields t_heap; DatumTupleFields t_datum; } t_choice; ItemPointerData t_ctid; /* 当前或新元组的位置 */ uint16 t_infomask2; /* 属性数量+标志位 */ uint16 t_infomask; /* 各种标志位 */ uint8 t_hoff; /* 头长度+NULL位图 */ bits8 t_bits[FLEXIBLE_ARRAY_MEMBER]; /* NULL位图 */ } HeapTupleHeaderData;

3. 实战:使用pageinspect扩展分析存储结构

PostgreSQL提供了pageinspect扩展,允许直接查看页面和元组的内部结构。以下是使用示例:

-- 创建扩展 CREATE EXTENSION pageinspect; -- 创建测试表并插入数据 CREATE TABLE test_heaptuple (id int, name text, value float); INSERT INTO test_heaptuple VALUES (1, 'item1', 1.11); INSERT INTO test_heaptuple VALUES (2, 'item2', 2.22); -- 查看页面头部信息 SELECT * FROM page_header(get_raw_page('test_heaptuple', 0)); -- 查看页面中的元组信息 SELECT lp, lp_off, t_xmin, t_xmax, t_ctid, t_infomask, t_infomask2, t_hoff, t_bits, t_data FROM heap_page_items(get_raw_page('test_heaptuple', 0)); -- 查看元组属性数据 SELECT lp, t_attrs FROM heap_page_item_attrs( get_raw_page('test_heaptuple', 0), 'test_heaptuple' );

通过pageinspect工具,我们可以观察到:

  • 新插入的元组t_xmin被设置为当前事务ID
  • t_xmax初始为0,表示该元组未被删除或更新
  • t_ctid指向自身,格式为(页面号,行指针号)
  • t_infomask和t_infomask2包含元组的各种状态标志

4. 堆表读写操作的内核实现

4.1 写入路径深度解析

PostgreSQL的写入操作遵循"WAL先行"原则,所有数据修改必须先写入WAL日志,然后才能修改内存中的数据页面。堆表插入操作的主要步骤如下:

  1. 元组头初始化:在heap_prepare_insert函数中设置元组头部的各个字段
  2. 查找可用页面:通过RelationGetBufferForTuple函数查找有足够空间的页面
  3. 冲突检测:检查事务隔离级别要求的各种约束条件
  4. 页面修改:将元组写入页面并更新页面头部信息
  5. WAL日志记录:生成并写入描述此修改的WAL记录
  6. 标记缓冲区脏:标记包含修改页面的缓冲区为脏,将由后台写入器稍后写入磁盘

关键代码路径:

exec_simple_query -> PortalRun -> ProcessQuery -> standard_ExecutorRun -> ExecModifyTable -> ExecInsert -> table_tuple_insert -> heapam_tuple_insert -> heap_insert

4.2 读取路径优化策略

PostgreSQL的读取操作需要处理MVCC可见性判断,确保事务只能看到符合其隔离级别的数据版本。堆表扫描的主要步骤包括:

  1. 获取缓冲区:将包含目标元组的页面读入共享缓冲区
  2. 可见性检查:根据元组的xmin/xmax和事务快照判断是否可见
  3. 元组构造:将磁盘上的元组数据转换为内存中的HeapTuple结构
  4. 投影处理:根据查询需求提取所需的列值

读取操作的关键优化点:

  • 预取:顺序扫描时预读后续页面
  • 并行查询:多个worker进程协同扫描大表
  • 仅索引扫描:当查询只需索引列时避免访问堆表

5. 高级存储特性与性能优化

5.1 TOAST机制处理大字段

PostgreSQL使用TOAST(The Oversized-Attribute Storage Technique)技术存储超过页面大小限制的字段值(默认阈值2KB)。TOAST将大字段值压缩或拆分为多个"烤面包片"存储:

TOAST策略描述适用场景
PLAIN禁止压缩和行外存储小字段
EXTENDED允许压缩和行外存储默认策略
EXTERNAL允许行外存储但不压缩文本等已压缩数据
MAIN尽量行内存储,必要时行外平衡策略

查看表的TOAST策略:

SELECT attname, attstorage FROM pg_attribute WHERE attrelid = 'test_heaptuple'::regclass;

5.2 空闲空间管理

PostgreSQL使用两个辅助结构管理堆表的空闲空间:

  1. 空闲空间映射(FSM):记录每个页面的空闲空间概况
  2. 可见性映射(VM):标记只包含全部可见元组的页面

这些结构显著提升了以下操作的效率:

  • 新元组插入时的空间定位
  • VACUUM操作的目标选择
  • 仅追加工作负载的性能

5.3 存储参数调优

PostgreSQL提供了多个表级存储参数用于性能优化:

CREATE TABLE perf_table ( id serial PRIMARY KEY, data text ) WITH ( fillfactor = 90, -- 页面填充因子(百分比) autovacuum_enabled = true, -- 启用自动清理 toast_tuple_target = 2000, -- TOAST行外存储阈值 parallel_workers = 4 -- 并行扫描工作进程数 );

关键优化建议:

  • 对频繁更新的表设置较低的fillfactor(70-90)
  • 对只读表设置fillfactor=100以最大化空间利用率
  • 根据工作负载特性调整autovacuum参数

6. 存储引擎与PostgreSQL生态的协同

PostgreSQL存储引擎的设计与数据库其他子系统紧密集成:

  1. 事务系统:通过xmin/xmax实现MVCC
  2. WAL日志:保证写入操作的持久性
  3. 缓冲区管理:缓存热数据页面减少IO
  4. 索引访问:通过TID(页面号+偏移量)定位元组
  5. 查询执行:提供高效的扫描和获取元组接口

这种深度集成使得存储引擎能够:

  • 支持复杂的查询计划和执行策略
  • 实现各种事务隔离级别
  • 提供灵活的数据类型支持
  • 保证系统崩溃后的数据一致性

在多年的PostgreSQL性能优化实践中,我们发现存储引擎的调优往往能带来最显著的性能提升。特别是在处理高并发写入负载时,合理配置存储参数、监控空间利用率和优化VACUUM策略可以避免许多性能问题。

http://www.jsqmd.com/news/681595/

相关文章:

  • 哪些独立站外链策略最有效? 每天多拿50个询盘的绝招
  • 【开源项目】tinyprintf:为资源受限MCU定制的极简格式化输出库
  • 把MinIO变成Windows系统服务:用WinSW实现开机自启与后台运行
  • TNAHosting测评:AMD Ryzen 5900X/1GB内存/NVMe硬盘/1Gbps带宽芝加哥VPS(Ubuntu 22.04.5 LTS)
  • RK3588驱动编译踩坑记:手把手教你解决‘-Werror’导致的‘all warnings being treated as errors’
  • nmcli 无法配置loopback口地址
  • 2026年全国镀锌钢板水箱厂家优选 从技术参数到工程应用的全面考量 - 深度智识库
  • 除了‘机械音’,开源TTS工具Ekho还能怎么玩?试试给它换个‘声音’
  • WeChatPad:Android应用多设备登录的技术实现与架构解析
  • K210串口通信保姆级教程:从MaixPy配置到与STM32单片机数据互传实战
  • FPGA设计中的AXI4 vs AXI4-Stream:选哪个?用Xilinx Zynq-7000的DMA传输案例说清楚
  • 5分钟快速上手:Nexus Mods App模组管理神器完全指南
  • 别再凭感觉调CAN采样点了!手把手教你用VH6501精准测量(附500Kbps实测波形)
  • 如何3分钟搞定WPS文献引用:科研写作效率提升终极指南
  • 告别龟速处理!用Python+ArcPy多线程批量处理MOD13A3 NDVI数据(附完整代码)
  • Davinci Configurator实战:利用Supplier Notification机制为你的UDS诊断服务加一把“安全锁”
  • Parse12306:零代码获取全国高速列车数据全攻略 [特殊字符]
  • 5分钟告别单调:用HackBGRT打造专属Windows开机画面的终极指南
  • #2026最新融合高中学校推荐!东北优质学校权威榜单发布,实力出众辽宁沈阳等地学校值得信赖 - 十大品牌榜
  • 保姆级教程:SSD202开发板从零到刷入OpenWrt的完整流程(含ISP、TFTP烧录避坑指南)
  • 非标与标准之争:国产拉力试验机品牌梯队分析(基于公开数据) - 品牌推荐大师1
  • SAP采购申请BAPI深度解析:从BAPI_PR_CREATE到BAPI_PR_CHANGE的完整生命周期管理
  • 别再只用MSE了!NeurIPS 2021新思路:用‘不确定性’给图像超分网络加个‘注意力’,效果立竿见影
  • 从零开始理解LoongArch指令集:给嵌入式开发者的快速入门指南(附指令格式速查表)
  • 手把手教你:用移动硬盘给Intel Mac降级Big Sur(保姆级避坑指南)
  • 用51单片机+DAC0832做个简易信号发生器:手把手教你生成方波、三角波和锯齿波(附完整汇编代码)
  • 告别慢吞吞!用DMA刷新STM32的ST7789V2 TFT屏,速度提升实测与避坑指南
  • 保姆级教程:在RK3588 Android 12上配置硬件看门狗(从DTS到watchdogd)
  • 用Python和TensorFlow搞定PINN:从Burgers方程到Navier-Stokes的保姆级代码实战
  • 打破语言壁垒:Translumo如何用智能实时翻译技术重塑跨语言体验