3. 数据库对象级容量盘点
库级容量只能告诉我们“哪个库在涨”,却无法回答“到底是哪张表、哪个索引在占空间”。对象级容量盘点的目标,就是把 pg_database_size() 给出的总量,逐层拆解到表、索引、TOAST 等具体对象上,形成一张可排序、可追踪的“空间账单”。PostgreSQL 为此提供了一组内置函数,理解它们各自的统计口径,是避免误判的第一步。
三个核心函数的统计口径
最常用的三个函数是 pg_relation_size()、pg_total_relation_size() 和 pg_database_size(),它们覆盖了从对象到库的三个层级:
pg_relation_size(oid):只统计单个关系(表、索引、TOAST 表、物化视图)主文件(main fork)的字节数,不含 TOAST、不含空闲空间映射(FSM)和可见性映射(VM)。pg_total_relation_size(oid):统计该表及其所有附属对象,包括主表、TOAST 表、TOAST 索引以及全部索引,是“一张表真正占了多少磁盘”的口径。pg_database_size(oid):统计整个数据库目录下所有文件的总和,包含系统目录、WAL 之外的全部数据文件,是库级总量。
三者关系可以概括为:pg_database_size ≈ 所有对象 pg_total_relation_size 之和 + 系统目录与未归属空间。因此做对象级盘点时,应以 pg_total_relation_size 作为“表占用”的主口径,用 pg_relation_size 区分主表与索引,用 pg_database_size 做总量校验。
VACUUM FULL 或 REINDEX,文件不会自动缩小,盘点结果会持续偏高。定位大表:按总占用排序
最直接的盘点方式,是查询 pg_class 与 pg_namespace,按 pg_total_relation_size 降序排列。下面这段 SQL 会列出当前库中占用最大的 20 张表,并同时给出主表、索引、TOAST 的拆分:
SELECT
n.nspname AS schema,
c.relname AS table_name,
pg_size_pretty(pg_total_relation_size(c.oid)) AS total_size,
pg_size_pretty(pg_relation_size(c.oid)) AS main_size,
pg_size_pretty(pg_total_relation_size(c.oid)
- pg_relation_size(c.oid)) AS index_toast_size,
pg_size_pretty(pg_indexes_size(c.oid)) AS index_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;
结果中若 index_toast_size 远大于 main_size,说明瓶颈在索引或 TOAST,而不是表数据本身。例如某订单表主表 8 GB、索引合计 22 GB,那么优化重点应放在索引而非表。反之,若主表远大于索引,则要考虑是否可以通过分区、归档或压缩降低主表体积。
定位大索引:逐索引拆解
表级排序只能看到索引总量,要进一步定位到具体索引,需要查询 pg_index 与 pg_class 的关联:
SELECT
tn.nspname AS table_schema,
tc.relname AS table_name,
ic.relname AS index_name,
pg_size_pretty(pg_relation_size(ic.oid)) AS index_size,
i.indisunique AS is_unique,
i.indisprimary AS is_primary
FROM pg_index i
JOIN pg_class ic ON ic.oid = i.indexrelid
JOIN pg_class tc ON tc.oid = i.indrelid
JOIN pg_namespace tn ON tn.oid = tc.relnamespace
WHERE tc.relkind = 'r'
AND tn.nspname NOT IN ('pg_catalog','information_schema')
ORDER BY pg_relation_size(ic.oid) DESC
LIMIT 20;
这张结果表能直接回答“哪个索引最贵”。常见的大索引来源包括:对高基数列建了重复索引、对宽文本列建了 B-tree 索引、以及长期未重建导致膨胀的索引。判断索引是否值得保留,可以结合 pg_stat_user_indexes 中的 idx_scan:若某索引体积很大但 idx_scan 长期为 0,就是典型的“占空间不干活”。
库级校验与对象级汇总
对象级盘点完成后,应与库级总量做一次对账,确认没有遗漏。可以先用 pg_database_size 取当前库总量,再用聚合查询汇总所有用户对象:
SELECT pg_size_pretty(pg_database_size(current_database())) AS db_total;
SELECT
pg_size_pretty(SUM(pg_total_relation_size(c.oid))) AS user_objects_total
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind IN ('r','m','i')
AND n.nspname NOT IN ('pg_catalog','information_schema');
两者之差即为系统目录、TOAST 元数据、空闲空间等未单独列出的部分。若差值异常大,通常意味着存在大量已删除但未回收的空间,或存在临时文件残留,需要进一步排查。
把盘点结果变成可执行的判断
拿到排序结果后,不要只看“谁最大”,而要看“谁大得不合理”。一个实用的判断顺序是:先看索引总量是否超过主表,若是,优先检查索引;再看单索引是否远大于同表其他索引,若是,检查是否重复或低效;最后看 TOAST 占比,若 TOAST 异常大,说明存在宽列或大文本,可考虑拆分冷热列或改用外部存储。
下面是一份可直接套用的对象级盘点记录表,建议每次盘点时填写并留存,便于对比增长趋势:
| 对象类型 | 对象名 | 总大小 | 主表大小 | 索引大小 | TOAST 大小 | 近 30 天增长 | 处置建议 |
|---|---|---|---|---|---|---|---|
| 表 | public.orders | 30 GB | 8 GB | 22 GB | 0.2 GB | +4 GB | 检查冗余索引 |
| 索引 | idx_orders_remark | 9 GB | — | 9 GB | — | +1.2 GB | idx_scan=0,建议删除 |
| 表 | public.logs | 18 GB | 6 GB | 3 GB | 9 GB | +6 GB | 按天分区 + 归档 |
pg_total_relation_size 统计的是文件分配量,若表刚经历大批量删除,结果会明显偏大。此时可结合 pg_stat_user_tables.n_dead_tup 判断膨胀程度,再决定是否执行 VACUUM FULL 或 REINDEX。对象级盘点的价值不在于一次性列出所有大对象,而在于建立“库 → 表 → 索引/TOAST”的逐层下钻能力。当库级容量告警时,你能在几分钟内回答:是 orders 表在涨,还是它的某个索引在涨,还是 logs 表的 TOAST 在涨。这个判断链条一旦建立,后续的清理、分区、归档和索引治理才有明确的靶点。