一、存储架构全景视图
1.1 物理存储层级体系
Database Cluster (PGDATA目录)
├── 全局控制文件 (pg_control, pg_version)
├── 全局系统表目录 (global/)
├── 预写日志 (pg_wal/)
├── 提交日志 (pg_xact/)
├── 表空间目录
│ ├── pg_default → $PGDATA/base/
│ │ ├── 1/ (template1数据库)
│ │ ├── 12345/ (用户数据库OID)
│ │ │ ├── 16384 (表文件, relfilenode)
│ │ │ ├── 16384_fsm (空闲空间映射)
│ │ │ ├── 16384_vm (可见性映射)
│ │ │ ├── 16384_init (初始化分支)
│ │ │ └── 16384.1, 16384.2, ... (分段文件)
│ │ └── pg_filenode.map
│ └── pg_global → $PGDATA/global/
└── 配置与日志文件
1.2 文件命名与分段机制
-- 查看表的物理文件节点
SELECT oid, relname, relfilenode, reltablespace
FROM pg_class
WHERE relname = 'your_table';
-- 文件分段算法
-- 主文件:<relfilenode>
-- 第一段:<relfilenode>.0 (1GB-2GB)
-- 第二段:<relfilenode>.1 (2GB-3GB)
-- 分支文件:<relfilenode>_<fork> (fsm, vm, init)
文件扩展策略:
- 默认段大小:1GB (可在编译时修改,但强烈不推荐)
- PostgreSQL 16+支持可配置表空间级段大小
- 每个段文件独立分配空间,减少文件系统碎片
二、页(Page)结构深度解析
2.1 PageHeaderData详细结构
typedef struct PageHeaderData {
/* LSN: 最后修改此页的WAL位置 */
PageXLogRecPtr pd_lsn; /* 8字节: (high 4B, low 4B) */
/* 页面校验和 (pg_checksums启用时) */
uint16 pd_checksum; /* 2字节: CRC-32C校验和 */
/* 页面标志位 */
uint16 pd_flags; /* 2字节: 详见pd_flags枚举 */
/* 空闲空间边界 */
LocationIndex pd_lower; /* 2字节: 行指针数组结束位置 */
LocationIndex pd_upper; /* 2字节: 空闲空间开始位置 */
/* 特殊空间 (索引页使用) */
LocationIndex pd_special; /* 2字节: 特殊空间开始位置 */
/* 页面大小与版本 */
uint16 pd_pagesize_version; /* 2字节: (高4位版本, 低12位页大小/256) */
/* VACUUM优化提示 */
TransactionId pd_prune_xid; /* 4字节: 可修剪的最老XID */
/* 行指针数组 (变长部分开始) */
ItemIdData pd_linp[FLEXIBLE_ARRAY_MEMBER];
} PageHeaderData;
pd_flags标志位详解:
#define PD_HAS_FREE_LINES 0x0001 /* 有未使用的行指针 */
#define PD_PAGE_FULL 0x0002 /* 页面已满 (用于提示) */
#define PD_ALL_VISIBLE 0x0004 /* 所有元组对所有事务可见 */
#define PD_VALID_FLAG_BITS 0x0007 /* 所有有效标志的掩码 */
2.2 页内布局的运行时状态
页面布局动态示例 (8KB页,编号从0开始):
0x0000: +---------------------------+
| PageHeader (24字节) |
| - pd_lsn = 0x1234/0x5678 |
| - pd_checksum = 0x89AB |
| - pd_lower = 0x0060 | <- 行指针数组结束在96字节处
| - pd_upper = 0x1F80 | <- 空闲空间从8064字节处开始
| - pd_special = 0x2000 |
+---------------------------+
0x0018: | 行指针0: offset=0x1F80 |
| length=128 |
0x001C: | 行指针1: offset=0x1F00 |
| length=96 |
| ... |
0x005C: | 行指针15: offset=0x0000 |
| length=0 (UNUSED)|
+---------------------------+
| 空闲空间 |
| (8064 - 96 = 7968字节) |
+---------------------------+
0x1F00: | 元组1数据 (96字节) |
| - HeapTupleHeader |
| - 列数据 |
+---------------------------+
0x1F60: | 填充字节 (对齐用) |
+---------------------------+
0x1F80: | 元组0数据 (128字节) |
| - HeapTupleHeader |
| - 列数据 |
+---------------------------+
0x2000: | 特殊空间 (索引页特有) |
+---------------------------+
0x2000+ : 可能的额外元组数据
2.3 页面管理算法
空闲空间搜索算法:
def find_free_space(page, needed_size):
# 1. 检查行指针数组中的UNUSED项
for item_id in page.item_ids:
if item_id.status == UNUSED:
# UNUSED项可以复用,但需要空间存放元组
pass
# 2. 检查pd_upper - pd_lower是否足够
free_space = page.pd_upper - page.pd_lower
if free_space >= needed_size + sizeof(ItemIdData):
return page.pd_upper - needed_size
# 3. 如果不够,尝试紧凑存储(VACUUM)
if page.pd_flags & PD_HAS_FREE_LINES:
# 有删除的元组,可以回收空间
pass
# 4. 返回NULL表示页面已满
return NULL
三、元组(Tuple)结构全解析
3.1 HeapTupleHeaderData完整定义
typedef struct HeapTupleFields {
TransactionId xmin; /* 插入事务ID */
TransactionId xmax; /* 删除/锁定事务ID */
union {
CommandId cid; /* 插入/删除命令ID */
TransactionId xvac; /* VACUUM操作移动元组的事务ID */
} field3;
} HeapTupleFields;
typedef struct DatumTupleFields {
int32 datum_len_; /* 变长头部 */
int32 datum_typmod; /* 类型修饰符 */
Oid datum_typeid; /* 复合类型OID */
} DatumTupleFields;
typedef struct HeapTupleHeaderData {
union {
HeapTupleFields t_heap;
DatumTupleFields t_datum;
} t_choice;
ItemPointerData t_ctid; /* 当前TID或更新后的TID */
/* 以下字段必须在t_choice之后 */
uint16 t_infomask2; /* 属性数量 + 标志位 */
uint16 t_infomask; /* 各种标志位 */
uint8 t_hoff; /* 头部长度,包括NULL位图 */
/* ^--- 必须在此处结束 ---^ */
bits8 t_bits[FLEXIBLE_ARRAY_MEMBER]; /* NULL位图 */
/* 更多存储可以在此后 */
} HeapTupleHeaderData;
3.2 信息掩码详解
t_infomask2 (16位) :
位 0-10: 属性数量 (最多1024列,实际限制为1600)
位 11: HEAP_HOT_UPDATED - 此元组是HOT更新的源
位 12: HEAP_ONLY_TUPLE - 仅HOT元组 (没有索引指向它)
位 13: HEAP_NATTS_MASK - 未使用
位 14: HEAP_KEYS_UPDATED - 索引键被更新
位 15: HEAP_XACT_MASK - 未使用
t_infomask (16位) :
可见性相关标志:
HEAP_XMIN_COMMITTED (0x0100) - xmin已提交
HEAP_XMIN_INVALID (0x0200) - xmin无效/中止
HEAP_XMIN_FROZEN (0x0400) - xmin已冻结
HEAP_XMAX_COMMITTED (0x0800) - xmax已提交
HEAP_XMAX_INVALID (0x1000) - xmax无效/中止
HEAP_XMAX_IS_MULTI (0x2000) - xmax是多事务ID
HEAP_UPDATED (0x4000) - 此元组已被更新
HEAP_MOVED_OFF (0x8000) - 移动到其他位置(VACUUM FULL)
HEAP_MOVED_IN (0x4000) - 从其他位置移动来
锁相关标志:
HEAP_XMAX_EXCL_LOCK (0x0040) - xmax持有排他锁
HEAP_XMAX_KEYSHR_LOCK (0x0020) - xmax持有键共享锁
HEAP_XMAX_SHR_LOCK (0x0010) - xmax持有共享锁
组合标志:
HEAP_XLOCK_COMMITTED (0x0080) - 锁定事务已提交
HEAP_IS_LOCKED (HEAP_XMAX_EXCL_LOCK |
HEAP_XMAX_KEYSHR_LOCK |
HEAP_XMAX_SHR_LOCK)
HEAP_XMIN_FROZEN (HEAP_XMIN_COMMITTED|HEAP_XMIN_INVALID)
3.3 元组数据存储格式
定长数据类型:
+----------------+----------------+-----+----------------+
| 列1 (4字节整数) | 列2 (8字节浮点) | ... | 列N (定长类型) |
+----------------+----------------+-----+----------------+
变长数据类型:
变长头格式 (1-4字节):
位布局: 00 = 4字节长度字(无数据)
01 = 1字节头 + 1字节长度 (最大126字节)
10 = 1字节头 + 2字节长度 (最大32766字节)
11 = 1字节头 + 4字节长度 (最大1GB)
示例: VARCHAR(100)存储 "Hello":
+----+----+-------------------------+
| 0x85 | 5 | H | e | l | l | o | ... |
+----+----+-------------------------+
^ ^ ^- 实际数据
| +------ 长度=5 (0x85 & 0x7F = 5)
+--------- 头字节: 0x80(有数据) | 0x05(1字节长度头)
对齐要求:
/* PostgreSQL数据类型对齐要求 */
typedef struct AlignmentMap {
char type; // C语言类型
short alignment; // 对齐要求(字节)
} AlignmentMap;
AlignmentMap align_map[] = {
{'c', 1}, // char
{'s', 2}, // short
{'i', 4}, // int
{'l', 8}, // long (在64位系统)
{'f', 4}, // float
{'d', 8}, // double
{'p', 8}, // pointer
};
3.4 TOAST机制深度解析
TOAST存储决策树:
开始: 元组大小检查
↓
元组 <= TOAST_TUPLE_THRESHOLD (~2KB)?
├── 是 → 行内存储 (Inline)
└── 否 → 检查每列的TOAST存储策略
↓
对于每列:
└── 列存储策略是什么?
├── PLAIN → 强制行内 (可能失败)
├── MAIN → 尝试压缩
│ ├── 压缩后 <= 2KB? → 行内存储
│ └── 仍 > 2KB? → 线外存储
├── EXTENDED → 压缩 + 线外
└── EXTERNAL → 直接线外
TOAST指针格式:
/* 线外存储指针 (varatt_external) */
struct varatt_external {
int32 va_rawsize; /* 原始数据大小 (解压后) */
int32 va_extsize; /* 外部存储大小 */
Oid va_valueid; /* TOAST值的唯一ID */
Oid va_toastrelid; /* TOAST表的OID */
};
/* 压缩行内指针 (varattrib_1b_compressed) */
struct varattrib_1b_compressed {
uint8 va_header; /* 总是0x1B */
uint8 va_rawsize[3]; /* 原始大小, 24位 */
/* 压缩数据紧随其后 */
};
/* TOAST表结构 */
CREATE TABLE pg_toast.pg_toast_12345 (
chunk_id oid NOT NULL, /* 对应va_valueid */
chunk_seq integer NOT NULL, /* 块序号, 从0开始 */
chunk_data bytea, /* 实际数据块 */
PRIMARY KEY (chunk_id, chunk_seq)
);
TOAST分块算法:
def toast_insert_value(value, strategy):
max_chunk_size = TOAST_MAX_CHUNK_SIZE # 通常2000字节
if strategy in [EXTENDED, MAIN]:
compressed = try_compress(value)
if len(compressed) < len(value):
value = compressed
if len(value) <= max_chunk_size and strategy != EXTERNAL:
return store_inline(value)
# 线外存储
toast_id = next_toast_id()
chunks = []
for i in range(0, len(value), max_chunk_size):
chunk = value[i:i+max_chunk_size]
chunks.append((toast_id, i//max_chunk_size, chunk))
# 将chunks插入TOAST表
insert_to_toast_table(chunks)
# 返回TOAST指针
return create_toast_pointer(toast_id, len(value))
四、索引存储与访问
4.1 B-tree索引结构
B-tree页布局:
+-----------------------------------------+
| BTPageOpaqueData (特殊空间) |
| - btpo_prev (左兄弟页) |
| - btpo_next (右兄弟页) |
| - btpo.level (树高度, 0=叶子) |
| - btpo_flags (BTP_LEAF, BTP_ROOT等) |
+-----------------------------------------+
| 标准PageHeader |
+-----------------------------------------+
| 行指针数组 |
+-----------------------------------------+
| 元组数据 (BTree索引元组) |
| +-------------------+-----------------+ |
| | IndexTupleHeader | 键值数据 | |
| | - t_tid (指向堆) | | |
| | - t_info (标志位) | | |
| +-------------------+-----------------+ |
+-----------------------------------------+
B-tree索引元组格式:
typedef struct IndexTupleData {
ItemPointerData t_tid; /* 指向堆元组的TID或下层的索引TID */
/* ---------------
* t_info封装了大小和标志位信息。
* [15] 如果有null位图则置1
* [14] 如果键有变长属性则置1
* [13-0] 元组大小
* ---------------
*/
unsigned short t_info;
} IndexTupleData;
4.2 索引访问路径
-- PostgreSQL查询规划器选择的访问方法
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders WHERE customer_id = 12345;
-- 可能输出:
-- Index Scan using idx_orders_customer on orders
-- Index Cond: (customer_id = 12345)
-- Buffers: shared hit=4 read=2
-- 其中: hit=缓冲池命中, read=磁盘读取
索引扫描成本模型:
总成本 = 索引访问成本 + 堆访问成本
索引访问成本:
- 索引树高度: H
- 每个级别的页访问: H次随机I/O
- 叶子页扫描: L次顺序I/O
堆访问成本:
- 每个匹配的TID: 1次随机I/O (如果不在缓冲池)
- 使用位图堆扫描时,可以排序TID减少随机I/O
五、事务可见性与MVCC实现
5.1 事务状态管理
CLOG(提交日志)结构:
CLOG页布局 (每个事务2位):
位对含义:
00 - 事务进行中
01 - 事务已提交
10 - 事务已中止
11 - 子事务状态
CLOG缓存 (共享内存):
+---------+---------+-----+---------+
| 页0 | 页1 | ... | 页N |
+---------+---------+-----+---------+
每个CLOG页: 8KB = 32768个事务状态
事务快照结构:
typedef struct SnapshotData {
/* 这些字段用于MVCC可见性检查 */
TransactionId xmin; /* 所有XID < xmin可见 */
TransactionId xmax; /* 所有XID >= xmax不可见 */
TransactionId *xip; /* 进行中的XID数组 */
uint32 xcnt; /* 进行中的XID数量 */
/* 子事务信息 */
TransactionId *subxip;
int32 subxcnt;
bool suboverflowed;
/* 快照类型 */
SnapshotType snapshot_type;
CommandId curcid;
/* 其他元数据 */
uint32 speculativeToken;
bool copied;
} SnapshotData;
5.2 HOT(仅堆元组)更新优化
HOT更新条件:
- UPDATE不修改任何索引列
- 新元组能放入同一页
- 页有足够的空闲空间或可清理的死元组
HOT链维护:
原始状态:
页123, 行指针[0] → 元组A (xmin=100, ctid=(123,0))
执行UPDATE后:
页123, 行指针[0] → 元组A' (xmin=200, ctid=(123,1))
页123, 元组A (xmax=200, ctid=(123,1)) ← 指向新版本
索引仍然指向行指针[0],通过HOT链找到最新版本
HOT链断裂检测:
-- 检查表的HOT更新效率
SELECT schemaname, tablename,
n_tup_upd,
n_tup_hot_upd,
round(n_tup_hot_upd * 100.0 /
NULLIF(n_tup_upd, 0), 2) as hot_ratio
FROM pg_stat_user_tables
WHERE n_tup_upd > 0
ORDER BY hot_ratio DESC;
-- HOT更新失败的原因:
-- 1. 更新了索引列
-- 2. 页面空间不足
-- 3. 填充因子(fillfactor)设置过高
六、VACUUM机制深度解析
6.1 VACUUM阶段与算法
LAZY VACUUM(常规VACUUM) :
阶段1: 扫描堆页
↓
对于每个页:
1. 检查可见性映射(VM)
2. 如果页全部可见,跳过
3. 否则,扫描页内所有元组
↓
对于每个元组:
a. 检查xmin/xmax的事务状态
b. 标记死元组为LP_DEAD
c. 标记可冻结的元组
↓
4. 如果页有LP_DEAD项,尝试修剪
5. 更新VM位图
阶段2: 截断末端空页
VACUUM FULL(重写表) :
-- VACUUM FULL的内部过程
1. 创建新表文件 (relfilenode.new)
2. 逐行复制存活元组到新文件
3. 重建所有索引
4. 更新系统目录
5. 删除旧文件
6. 重命名新文件
-- 注意事项:
-- - 需要排它锁
-- - 需要额外存储空间
-- - 索引完全重建
6.2 可见性映射(VM)工作机制
VM位图格式:
每个页对应2位:
位0: 所有元组对所有事务可见
位1: 页内所有元组已被冻结
VM优化:
- 设置位0的页可以被Index-Only Scan使用
- 设置位1的页可以跳过冻结扫描
- VACUUM可以跳过两个位都设置的页
VM更新策略:
/* 何时设置all-visible位 */
if (page_has_no_dead_tuples &&
all_tuples_visible_to_everyone) {
visibility_map_set(relation, block, true);
}
/* 何时清除all-visible位 */
if (page_has_been_modified_by_insert_or_update) {
visibility_map_clear(relation, block);
}
七、高级存储特性
7.1 表分区存储
分区表物理结构:
-- 创建分区表示例
CREATE TABLE measurement (
city_id int not null,
logdate date not null,
peaktemp int,
unitsales int
) PARTITION BY RANGE (logdate);
-- 每个分区是独立的物理表
CREATE TABLE measurement_y2023m01
PARTITION OF measurement
FOR VALUES FROM ('2023-01-01') TO ('2023-02-01')
WITH (fillfactor=90);
-- 查看分区存储
SELECT relname, relkind, relfilenode,
pg_size_pretty(pg_total_relation_size(oid))
FROM pg_class
WHERE relname LIKE 'measurement%';
7.2 压缩存储(TOAST与LZ4)
列级压缩设置:
-- 使用LZ4压缩(PostgreSQL 14+)
CREATE TABLE compressed_data (
id serial PRIMARY KEY,
uncompressed_data text,
compressed_data text COMPRESSION lz4
);
-- 查看压缩效果
SELECT pg_column_compression(compressed_data) as compression_method,
pg_column_size(uncompressed_data) as uncompressed_size,
pg_column_size(compressed_data) as compressed_size
FROM compressed_data;
7.3 存储参数调优
关键存储参数:
-- 创建表时指定存储参数
CREATE TABLE optimized_table (
id bigint PRIMARY KEY,
data jsonb,
created_at timestamptz DEFAULT now()
) WITH (
fillfactor = 85, -- 页填充因子
autovacuum_enabled = true, -- 启用自动清理
toast_tuple_target = 128, -- TOAST触发阈值
parallel_workers = 4 -- 并行工作进程
);
-- 修改现有表
ALTER TABLE optimized_table SET (
fillfactor = 80,
autovacuum_vacuum_cost_delay = 10
);
八、监控与诊断
8.1 存储状态查询
综合存储分析查询:
WITH table_stats AS (
SELECT
schemaname,
tablename,
pg_relation_size(schemaname||'.'||tablename) as table_size,
pg_total_relation_size(schemaname||'.'||tablename) as total_size,
n_live_tup,
n_dead_tup,
n_tup_upd,
n_tup_hot_upd,
last_vacuum,
last_autovacuum,
last_analyze,
last_autoanalyze
FROM pg_stat_user_tables
WHERE schemaname NOT LIKE 'pg_%'
)
SELECT
schemaname,
tablename,
pg_size_pretty(table_size) as table_size,
pg_size_pretty(total_size - table_size) as index_size,
pg_size_pretty(total_size) as total_size,
n_live_tup,
n_dead_tup,
CASE
WHEN n_live_tup + n_dead_tup > 0
THEN round(n_dead_tup * 100.0 / (n_live_tup + n_dead_tup), 2)
ELSE 0
END as dead_tuple_percent,
CASE
WHEN n_tup_upd > 0
THEN round(n_tup_hot_upd * 100.0 / n_tup_upd, 2)
ELSE NULL
END as hot_update_ratio,
last_autovacuum
FROM table_stats
WHERE total_size > 100 * 1024 * 1024 -- 大于100MB的表
ORDER BY total_size DESC
LIMIT 20;
8.2 页面级诊断工具
使用pageinspect扩展:
-- 安装扩展
CREATE EXTENSION pageinspect;
-- 查看页头信息
SELECT * FROM page_header(get_raw_page('my_table', 0));
-- 查看页内元组
SELECT lp, lp_off, lp_len, lp_flags,
t_xmin, t_xmax, t_ctid,
t_infomask, t_infomask2
FROM heap_page_items(get_raw_page('my_table', 0));
-- 查看B-tree索引页
SELECT * FROM bt_metap('my_index');
SELECT * FROM bt_page_stats('my_index', 1);
SELECT * FROM bt_page_items('my_index', 1);
使用pgstattuple分析表膨胀:
CREATE EXTENSION pgstattuple;
-- 获取表级统计
SELECT * FROM pgstattuple('my_table');
-- 获取更详细的信息
SELECT * FROM pgstatindex('my_index');
-- 输出示例:
-- table_len: 表总大小
-- tuple_count: 存活元组数
-- tuple_len: 存活元组总大小
-- dead_tuple_count: 死元组数
-- dead_tuple_len: 死元组总大小
-- free_space: 空闲空间
九、性能优化实践
9.1 填充因子优化
-- 根据UPDATE模式设置填充因子
-- 1. 频繁UPDATE的表
ALTER TABLE frequently_updated_table SET (fillfactor = 70);
-- 2. 只读或INSERT-only的表
ALTER TABLE read_mostly_table SET (fillfactor = 100);
-- 3. 批量UPDATE后重建表
CLUSTER my_table USING my_index;
VACUUM FULL my_table; -- 谨慎使用
9.2 监控与自动维护
自动化VACUUM策略:
-- 基于表级别的自动VACUUM配置
ALTER TABLE large_table SET (
autovacuum_vacuum_scale_factor = 0.05, -- 死元组达到5%时触发
autovacuum_vacuum_threshold = 50000, -- 最少50000个死元组
autovacuum_analyze_scale_factor = 0.02,
autovacuum_analyze_threshold = 10000,
autovacuum_vacuum_cost_delay = 10, -- 降低对系统的影响
autovacuum_vacuum_cost_limit = 1000
);
-- 监控自动VACUUM进度
SELECT schemaname, tablename,
last_autovacuum,
last_autoanalyze,
autovacuum_count,
autoanalyze_count
FROM pg_stat_user_tables
WHERE n_dead_tup > n_live_tup * 0.1; -- 死元组超过10%的表
十、故障恢复与完整性
10.1 页面损坏检测与修复
-- 启用数据校验和
-- 需要在initdb时启用,或后续使用pg_checksums启用
-- 检查表是否启用了校验和
SELECT relname, relchecksum
FROM pg_class
WHERE relname = 'my_table';
-- 使用amcheck扩展检查B-tree完整性
CREATE EXTENSION amcheck;
-- 检查索引完整性
SELECT bt_index_check('my_index');
-- 深度检查(需要排他锁)
SELECT bt_index_parent_check('my_index');
10.2 防止事务ID回卷
-- 监控事务年龄
SELECT datname,
age(datfrozenxid) as database_age,
mxid_age(datminmxid) as multixact_age
FROM pg_database
ORDER BY age(datfrozenxid) DESC;
-- 监控表的事务年龄
SELECT relname,
age(relfrozenxid) as table_age,
pg_size_pretty(pg_total_relation_size(oid)) as size
FROM pg_class
WHERE relkind = 'r'
ORDER BY age(relfrozenxid) DESC
LIMIT 10;
-- 紧急情况下手动冻结
VACUUM FREEZE my_table;
总结
PostgreSQL的表存储设计是一个极其复杂但高度优化的系统,核心围绕MVCC、数据完整性和性能展开。关键要点:
- MVCC实现:通过xmin/xmax和CTID链实现多版本,避免读锁
- 存储优化:HOT更新减少索引膨胀,TOAST处理大对象
- 空间管理:FSM跟踪空闲空间,VM加速VACUUM
- 维护需求:定期VACUUM防止表膨胀,监控事务年龄防回卷
- 可观测性:丰富的系统视图和扩展工具提供深度洞察
理解这些底层机制是进行高性能PostgreSQL数据库设计、调优和故障排除的基础。