2. PostgreSQL存储架构与容量计量
做容量治理,第一步不是加磁盘,而是搞清楚“空间到底被谁吃掉了”。PostgreSQL 的一个数据库实例,物理上并不只是几张表那么简单:堆表、各类索引、TOAST 溢出数据、预写日志(WAL)、临时文件、运行日志,各自落在不同的目录、遵循不同的生命周期、受不同的参数控制。如果把它们混在一起看,容量规划必然失真。本节先把这套物理存储结构拆开,再给出可落地的容量统计方法。
一、实例的物理目录骨架
PostgreSQL 的数据目录由 PGDATA 指向,核心子目录包括:base/ 存放各数据库的堆表与索引文件,global/ 存放共享系统目录,pg_wal/ 存放 WAL 段文件,pg_log/(或由 log_directory 指定)存放运行日志,base/pgsql_tmp/ 与 pg_stat_tmp/ 则涉及临时与统计文件。理解这个骨架,是后续按目录做容量归因的前提。
需要特别注意的是,PostgreSQL 中一个“表”在磁盘上通常不是一个文件。当堆表或索引超过 1GB 时,会被切分为多个段文件,命名形如 relfilenode、relfilenode.1、relfilenode.2……因此统计单表大小时,不能只看一个文件名,而要按 relfilenode 前缀聚合。
二、六类存储对象的容量特征
堆表(Heap):存放行数据,受 fillfactor、更新与删除产生的死元组影响。频繁 UPDATE/DELETE 的表,物理大小会明显大于逻辑数据量,直到 VACUUM 回收。
索引:B-tree、GIN、GiST、BRIN 等各有不同的空间放大特性。B-tree 通常约为被索引列数据量的 1~2 倍;GIN 在全文检索、数组、JSONB 场景下可能数倍于原表。索引膨胀同样需要 REINDEX 或 VACUUM 处理。
TOAST:当单行中变长字段(text、bytea、jsonb 等)压缩后仍超过约 2KB 时,会溢出到独立的 TOAST 表。TOAST 数据物理上属于该表,但不在主堆文件中,容易被“表大小”统计遗漏。
WAL:位于 pg_wal/,默认每个段 16MB。WAL 的生成速率取决于写入量、wal_level、全页写(full_page_writes)以及检查点间隔。它既是崩溃恢复的保障,也是复制与 PITR 的基础,容量上必须预留。
临时文件:排序、哈希连接、哈希聚合在 work_mem 不足时会落盘,生成临时文件。它们随查询结束而释放,但峰值可能瞬间撑爆磁盘,是容量事故的高发区。
日志:包括运行日志、慢查询日志、审计日志。单条不大,但长期累积、未做轮转时,可能占用数十 GB。
三、容量统计的三种口径
统计口径不统一,是容量治理中最常见的坑。至少要区分三层:
- 逻辑大小:
pg_relation_size()返回单个关系主文件大小,不含 TOAST 与索引;pg_total_relation_size()返回表 + 索引 + TOAST 合计;pg_database_size()返回单库总占用。 - 物理大小:用
du -sh $PGDATA/base/<oid>或对整个PGDATA做目录级统计,反映真实磁盘占用,包含膨胀、空闲空间与段文件碎片。 - 增长速率:单点大小没有意义,必须按天/周采样,计算增量。例如每天记录一次
pg_database_size(),连续 7 天即可估算周增长与剩余可用天数。
下面是一段可直接使用的统计脚本,按库、按表输出逻辑大小与 TOAST 占比,便于定位“谁在长”:
-- 1. 各库总大小
SELECT datname,
pg_size_pretty(pg_database_size(datname)) AS db_size
FROM pg_database
ORDER BY pg_database_size(datname) DESC;
-- 2. 单库内 Top 20 表:主表 / TOAST / 索引 分列
SELECT n.nspname AS schema,
c.relname AS table_name,
pg_size_pretty(pg_relation_size(c.oid)) AS heap_size,
pg_size_pretty(pg_total_relation_size(c.oid)
- pg_relation_size(c.oid)) AS idx_toast_size,
pg_size_pretty(pg_total_relation_size(c.oid)) AS total_size
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind = 'r'
AND n.nspname NOT IN ('pg_catalog','information_schema')
ORDER BY pg_total_relation_size(c.oid) DESC
LIMIT 20;
-- 3. WAL 目录占用
SELECT count(*) AS wal_segments,
pg_size_pretty(sum(size)) AS wal_total
FROM pg_ls_waldir();
-- 4. 临时文件累计(自统计重置以来)
SELECT datname,
temp_files,
pg_size_pretty(temp_bytes) AS temp_total
FROM pg_stat_database
ORDER BY temp_bytes DESC;
四、从统计结果到容量判断
拿到数据后,关键是把“大小”翻译成“风险”。几个实用的判断经验:
- 若某表
total_size远大于heap_size,优先怀疑索引膨胀或 TOAST 占比过高,可进一步查pg_stat_user_tables.n_dead_tup与pgstattuple。 - 若
temp_bytes持续增长,说明work_mem偏小或存在大排序/大哈希查询,应结合pg_stat_statements定位。 - 若
pg_wal段数长期高于max_wal_size对应值,检查是否有复制槽未消费或归档失败,避免 WAL 无限堆积。 - 日志目录建议单独挂载或设置
log_rotation_size与保留策略,防止与数据盘争抢空间。
capacity_snapshot(snap_date, db_name, object_name, size_bytes)),这样增长趋势、异常突增、回收效果都能用 SQL 直接对比,而不是靠人肉记忆。总结来说,PostgreSQL 的容量不是单一数字,而是堆表、索引、TOAST、WAL、临时文件、日志六类对象在不同目录、不同生命周期下的叠加。只有先建立“目录—对象—统计函数”三者的对应关系,再用统一口径持续采样,容量治理才有可靠的数据基础。