markdown复制## 1. 空间监控的必要性与应用场景
在数据库运维工作中,定期监控数据库及表的空间占用情况是基础但至关重要的任务。以PostgreSQL为例,当数据库实例运行时间超过半年,往往会遇到这些典型场景:某个业务表突然增长到上百GB导致磁盘告警、索引膨胀使查询性能下降却找不到原因、归档策略失效导致历史数据堆积。我曾处理过一个案例,某电商平台的订单表在促销期间每天增长30GB,由于缺乏空间监控,直到磁盘写满才被发现,直接导致下单服务中断2小时。
PostgreSQL不像MySQL那样可以通过简单的`SHOW TABLE STATUS`获取空间信息,其存储机制更为复杂。了解空间占用不仅关乎容量规划,更直接影响查询性能——当表膨胀率超过30%时,即使有索引也可能出现全表扫描。通过本文介绍的方法,你可以快速定位空间异常对象,制定合理的清理或扩容策略。
## 2. 核心系统函数与统计视图解析
### 2.1 存储结构基础认知
PostgreSQL的物理存储包含几个关键概念:
- **OID(Object Identifier)**:每个数据库对象(表、索引等)的唯一数字ID
- **表空间(Tablespace)**:物理文件的存储位置,默认使用`pg_default`
- **TOAST(The Oversized-Attribute Storage Technique)**:用于存储超长字段的附属表
理解这些概念很重要,因为后续查询中会频繁涉及。例如当查询表大小时,实际需要统计主表、TOAST表、索引等的总和。
### 2.2 关键系统函数说明
这几个函数是空间分析的核心工具:
```sql
pg_database_size(oid) -- 数据库级别
pg_relation_size(oid) -- 单个关系(表/索引)基础大小
pg_total_relation_size(oid) -- 包含索引、TOAST等的总大小
pg_size_pretty(bigint) -- 将字节数转为易读格式(如1GB)
特别注意pg_relation_size的多种用法:
sql复制pg_relation_size(oid, 'main') -- 主数据文件
pg_relation_size(oid, 'fsm') -- 空闲空间映射
pg_relation_size(oid, 'vm') -- 可见性映射
pg_relation_size(oid, 'init') -- 初始化分支
2.3 实用统计视图组合
pg_class与pg_namespace视图配合使用效果最佳:
sql复制SELECT
nspname AS schema,
relname AS object,
relkind AS type,
pg_size_pretty(pg_total_relation_size(c.oid)) AS size
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE relkind IN ('r','i') -- r=普通表, i=索引
ORDER BY pg_total_relation_size(c.oid) DESC;
3. 实战查询方案与解读
3.1 数据库级别空间分析
获取所有数据库大小排名:
sql复制SELECT
datname,
pg_size_pretty(pg_database_size(oid)) AS size,
pg_database_size(oid) AS bytes
FROM pg_database
ORDER BY bytes DESC;
重要提示:pg_database_size的计算开销较大,在大型实例上避免频繁执行。我曾在一个包含200+数据库的实例上发现此查询消耗了500ms以上。
3.2 表级空间分析进阶版
这个增强版查询包含更多实用信息:
sql复制SELECT
n.nspname AS schema,
c.relname AS table,
pg_size_pretty(pg_total_relation_size(c.oid)) AS total_size,
pg_size_pretty(pg_relation_size(c.oid)) AS table_size,
pg_size_pretty(pg_indexes_size(c.oid)) AS indexes_size,
pg_size_pretty(pg_total_relation_size(c.oid) - pg_relation_size(c.oid) - pg_indexes_size(c.oid)) AS toast_size,
c.reltuples AS rows_estimate
FROM pg_class c
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind = 'r'
ORDER BY pg_total_relation_size(c.oid) DESC
LIMIT 20;
字段解读:
toast_size异常大可能意味着有大量JSONB或TEXT字段rows_estimate是估算值,VACUUM后会更准确- 索引占比超过表数据的50%时需要考虑索引优化
3.3 索引空间分析技巧
专用于索引分析的查询:
sql复制SELECT
n.nspname AS schema,
c.relname AS index,
pg_size_pretty(pg_relation_size(c.oid)) AS size,
pg_size_pretty(pg_relation_size(i.indrelid)) AS table_size,
i.indisvalid AS is_valid,
i.indisprimary AS is_primary
FROM pg_class c
JOIN pg_index i ON c.oid = i.indexrelid
JOIN pg_namespace n ON n.oid = c.relnamespace
ORDER BY pg_relation_size(c.oid) DESC
LIMIT 20;
经验之谈:删除无效索引(indisvalid=false)可立即回收空间,但需确认是否为暂时性无效。
4. 自动化监控与异常检测
4.1 定期监控脚本示例
将以下SQL保存为monitor.sql:
sql复制\x auto
SELECT
current_timestamp AS check_time,
datname,
pg_size_pretty(size) AS size,
size AS bytes
FROM (
SELECT
datname,
pg_database_size(oid) AS size
FROM pg_database
) t
ORDER BY bytes DESC;
通过crontab设置每日执行:
bash复制0 3 * * * psql -U monitor -d postgres -f /path/to/monitor.sql >> /var/log/pg_size.log
4.2 异常增长检测策略
在监控脚本中加入增长率计算:
sql复制WITH today AS (
SELECT datname, pg_database_size(oid) AS size
FROM pg_database
),
yesterday AS (
SELECT * FROM database_sizes
WHERE date = current_date - interval '1 day'
)
SELECT
t.datname,
pg_size_pretty(t.size) AS current_size,
pg_size_pretty(t.size - y.size) AS growth,
round((t.size - y.size) * 100.0 / y.size, 2) AS growth_pct
FROM today t
JOIN yesterday y ON t.datname = y.datname
WHERE t.size > y.size * 1.2 -- 增长超过20%
ORDER BY growth_pct DESC;
关键阈值建议:
- 日增长率>20%触发警告
- 周增长率>100%触发严重警告
- 单个表超过10GB需要特别关注
5. 空间回收与优化实践
5.1 常见空间回收方法
| 方法 | 适用场景 | 锁级别 | 空间回收效果 |
|---|---|---|---|
| VACUUM FULL | 表膨胀严重 | 排他锁 | 最佳 |
| REINDEX | 索引膨胀 | 表级锁 | 仅索引空间 |
| TRUNCATE | 清空全表 | 排他锁 | 100%回收 |
| DROP TABLE | 删除表 | 排他锁 | 100%回收 |
重要提示:VACUUM FULL会重写整个表,在9.0+版本建议使用
CLUSTER或pg_repack替代
5.2 使用pg_repack在线优化
安装扩展:
sql复制CREATE EXTENSION pg_repack;
执行重组(示例对orders表操作):
bash复制pg_repack -U postgres -d mydb -t orders
优势:
- 不需要长事务
- 几乎不阻塞读写
- 支持索引同步重建
5.3 分区表空间管理
对于按时间分区的表,可定期删除旧分区:
sql复制-- 查看分区结构
SELECT relname FROM pg_class
WHERE relispartition AND relkind = 'r'
ORDER BY relname;
-- 删除历史分区(需确认外键等依赖)
DROP TABLE sales_202301;
最佳实践:结合crontab设置保留策略,例如只保留最近12个月的分区。
6. 疑难问题排查实录
6.1 常见问题速查表
| 现象 | 可能原因 | 检查方法 |
|---|---|---|
| 表大小远大于数据量 | 膨胀严重 | SELECT pg_size_pretty(pg_relation_size('table')), pg_size_pretty(pg_table_size('table') - pg_relation_size('table')) |
| 索引比表还大 | 无效索引或多余索引 | 执行EXPLAIN ANALYZE确认索引使用情况 |
| TOAST异常大 | 大量大字段存储 | SELECT attname, avg(width) FROM pg_stats WHERE tablename = 'table' GROUP BY attname |
6.2 真实案例:幽灵空间占用
某次发现数据库显示占用500GB,但统计所有对象只有300GB。通过以下命令找到"丢失"的空间:
sql复制SELECT oid, spcname
FROM pg_tablespace
WHERE oid > 16384;
最终发现是已删除的临时表空间未清理,通过rm -rf手动删除文件后空间释放。
6.3 扩展推荐
这些工具可以提供更直观的空间分析:
在长期维护PG数据库的过程中,我总结出一个经验法则:当发现任何表的空间增长率连续3天超过日均15%,就必须要介入调查。曾经有一个文本日志表因为应用BUG导致重复写入,3天内从10GB暴涨到300GB,及时发现后通过修正应用逻辑和清理数据避免了存储危机。
code复制