news 2026/10/10 3:45:29

PostgreSQL空间占用排查:库级、表级与索引膨胀定位指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PostgreSQL空间占用排查:库级、表级与索引膨胀定位指南

如果你负责的 PostgreSQL 实例最近这几天频繁收到磁盘告警,登录服务器一看数据目录几十个 G,却说不清到底哪个库、哪张表在“吃”空间,那这篇文章就是写给你的。PostgreSQL 查看数据库及表中数据占用空间大小虽然是运维入门操作,但很多刚接触 PG 的开发者会栽在细节上:pg_relation_size和pg_total_relation_size差了一个索引体积;删了几亿行数据磁盘占用纹丝不动;分区表统计出来只有几千字节……我也是一路踩过来的,这次索性把库级、表级、膨胀估算的查询脚本和排查思路全都整理出来,方便你直接“抄作业”。

1. 为什么 PostgreSQL 空间统计比你想的更重要

1.1 从一次磁盘告警说起

有次帮某公司的生产库做巡检,业务那边反馈“存储快满了,但又不敢删数据”。我连上实例后先跑了一条最基础的 SQL:

SELECT datname, pg_size_pretty(pg_database_size(datname)) AS size FROM pg_database ORDER BY pg_database_size(datname) DESC;

结果一眼就看到某个业务库占了 700GB,而其它库加起来才几十 GB。再钻进去定位表,发现一张保存用户行为日志的“大宽表”就有 460GB,其中索引占了接近 180GB。后来清理了历史分区并重建索引,磁盘占用又回到 300GB 以内。整个过程不到一小时,靠的全是 PG 内置的空间统计函数。

这件事给我的教训是:空间排查不应该等磁盘 100% 才动手,而应该成为巡检脚本里的常规项。PG 和 MySQL 在空间管理上差异很大,MySQL 的 InnoDB 对删除数据的页可以内部复用,而 PostgreSQL 的 MVCC 机制会让旧版本数据长期留在页里,如果不理解膨胀原理,你很可能看到“表空间占用越来越大”却无从下手。

1.2 PostgreSQL 的空间统计体系概览

PostgreSQL 把空间占用拆分得很细,这类“视图级”到“层级”都有一套对应的工具函数,核心包括:

  • 数据库级:当个库整体用了多少,看pg_database_size。
  • 表级:表本身数据文件多大、索引多大、TOAST 多大,分别有pg_relation_size、pg_indexes_size、pg_total_relation_size。
  • 对象级:索引、物化视图、序列、分区表,都能用 relkind 区分后统计。
  • 统计视图:pg_stat_user_tables提供了活的元组数、死元组数和扫描次数,用于估算膨胀程度。

这套体系设计的初衷,就是让 DBA 能“从大到小”逐层定位空间瓶颈。你可以先看哪个库大,再进库看哪张表大,最终定位到表里的哪个索引拖后腿。理解了这个递进关系,后面所有脚本都只是顺着这条链路去补 SQL 而已。

2. 核心函数与视图:查空间必须吃透的六个 API

2.1 五个常驻函数:大小统计的基石

PG 里最常用的空间统计函数就这么几个,建议背下来:

函数入参返回值实际含义
pg_database_size(name/oid)数据库名或 OID字节数整个数据库占用大小,含表、索引、TOAST、系统表
pg_relation_size(relation)表名或 regclass字节数表主数据 fork 的大小,不含 TOAST、不含索引
pg_indexes_size(relation)表名或 regclass字节数该表上所有索引的总大小
pg_total_relation_size(relation)表名或 regclass字节数表数据 + 索引 + TOAST + 其它附属 fork 的全量大小
pg_size_pretty(bytea/numeric)字节数可读文本把字节转成 B、kB、MB、GB 等易读格式

这里最容易被绕晕的是pg_relation_size和pg_total_relation_size的差异。简单来说,前者只算“表本身的数据文件”,后者把索引和 TOAST 都算进去了。如果你只查询前者,很容易低估一张表的真实空间——因为一个索引比表大是常有的事。

补充一点:pg_relation_size还支持第三个参数 fork,比如pg_relation_size(t.oid, 'toast')能单独看 TOAST 文件大小。不过日常排查用pg_total_relation_size就够了,少数需要深挖 TOAST 膨胀时才需要单独拆。

2.2 配套系统表与视图

函数负责算字节,系统表和视图负责提供“有哪些对象”的清单。最常用的组合是pg_class(表/索引的元数据)和pg_namespace(schema 信息):

  • pg_class.relkind:r表示普通表,i表示索引,S表示序列,m表示物化视图,p表示分区表。
  • pg_namespace.nspname:schema 名称,通常用来过滤public或排除pg_catalog、information_schema这类系统 schema。
  • pg_stat_user_tables:提供每个用户表的扫描次数、n_live_tup(有效元组数)、n_dead_tup(死元组数),用于膨胀分析。

实际查询时,我会把pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace当作基础表,再去关联pg_indexes或直接调用函数。

2.3 函数选择中最容易踩的三个误区

第一个误区是拿字符串拼接直接传给pg_total_relation_size。比如你写了:

SELECT pg_total_relation_size(schemaname || '.' || tablename) FROM pg_tables;

表名如果含大写或特殊字符,经常报错或返回空。正确姿势是通过pg_class.oid传参,或者用to_regclass('schema.table')做安全转换,后者遇到不存在的表返回 NULL 而不是报错。

第二个误区是统计父分区表时只查出个位数大小。PG 10+ 的分区表本身作为父对象没有独立数据文件,真正的数据都在子分区里,必须循环统计子分区。

第三个误区是拿pg_database_size当作整个实例的数据目录大小。它只算数据库内部的逻辑空间,WAL 日志、归档日志、统计信息文件都不在其中。你数据目录里有个 20GB 的pg_wal,靠数据库统计是看不出来的,得用pg_walfile_name()或文件系统命令单独查。

3. 实操脚本:从库级到表级一键摸清空间占用

3.1 库级空间查看

最基础的库级脚本就是这样,按大小倒序排:

SELECT datname AS database_name, pg_size_pretty(pg_database_size(datname)) AS total_size FROM pg_database ORDER BY pg_database_size(datname) DESC;

这个脚本会包含template0、template1、postgres这些默认库,通常不用特别过滤,因为它们一般都很小。如果你只想看当前连接的库:

SELECT current_database(), pg_size_pretty(pg_database_size(current_database()));

3.2 表级空间查看

进入目标库后,用下面的 SQL 查所有用户表大小:

SELECT n.nspname AS schema_name, 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 heap_size, pg_size_pretty(pg_indexes_size(c.oid)) AS index_size, c.reltuples::bigint AS approx_rows FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind IN ('r', 'm') AND n.nspname NOT IN ('pg_catalog', 'information_schema') ORDER BY pg_total_relation_size(c.oid) DESC;

这里我特意开了三列:total_size、heap_size、index_size。大多数情况下你会看到total_size明显大于heap_size,差出来的那一块基本就是索引,还有一小部分是 TOAST。reltuples是规划器用的估算行数,不是精确值,但对判断表规模已经够用了。

3.3 一次性输出所有关键维度

如果需要更深度的巡检,可以把 TOAST 也单独拆出来。TOAST 是 PG 存储大字段(如超长文本、JSONB 等)的“外置仓库”,很多大表空间异常其实都藏在 TOAST 里:

SELECT n.nspname AS schema_name, c.relname AS table_name, pg_size_pretty(pg_total_relation_size(c.oid)) AS total_with_index, pg_size_pretty(pg_relation_size(c.oid)) AS heap, pg_size_pretty(pg_indexes_size(c.oid)) AS indexes, pg_size_pretty(pg_relation_size(c.oid, 'toast')) AS toast_size, pg_size_pretty(pg_total_relation_size(c.oid) - pg_relation_size(c.oid) - pg_indexes_size(c.oid) - pg_relation_size(c.oid, 'toast')) AS other_size FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind IN ('r', 'm') AND n.nspname NOT IN ('pg_catalog', 'information_schema') ORDER BY pg_total_relation_size(c.oid) DESC LIMIT 50;

other_size一般是表自身的空闲空间(free space map、visibility map)或一些小 fork,正常不会特别大。如果某张表的toast_size占到总大小的一半以上,说明大字段对象很多,可以考虑拆表存储或压缩。注意:执行这种排序查询时,大库可能需要一点时间,因为pg_total_relation_size会对每个对象做 stat 调用,建议先LIMIT 50减少开销。

3.4 psql 快捷命令

如果不方便写 SQL,psql 自带的元命令能很快给出初筛结果:

  • \l+列出所有数据库及大小。
  • \dt+列出当前 schema 的所有表,并显示估算行数和表大小。
  • \di+列出索引大小。

这些命令底层调用的正是上面那几个函数,胜在快、零成本。但实际排查时仍然建议用自定义 SQL,因为\dt+只显示表的主数据大小,不含索引。我之前就被\dt+骗过一次,以为某张表只占 5GB,结果让业务导数据导到一半才发现索引比表还大,从那以后查表空间我只信pg_total_relation_size。

4. 深入原理:为什么空间“只增不减”与膨胀估算

4.1 MVCC 与死元组的空间代价

PostgreSQL 的每个事务都在版本链上工作,更新一行并不会覆盖旧值,而是插入一个新版本,旧版本标记为“死亡”。删除操作也只是在元组头部打上删除标记。这些死元组不会立刻清空,而是保留在页面上直到下一次 VACUUM 处理。

这样一来,业务高峰期频繁 UPDATE/DELETE 的表会迅速膨胀。同一行数据,MySQL 可能原地覆盖,PG 则需要把新旧版本都留在文件里。数据库统计里看到的“表大小”,其实包含了大量对查询不可见的死元组。你可能明明只插入了 100 万行,但由于反复更新,表空间已经涨到了 500 万行的规模。

这也是为什么我们做空间排查时,不能只看表大小,还要看死元组比例。pg_stat_user_tables里的n_dead_tup就是关键指标:

SELECT schemaname, relname, n_live_tup, n_dead_tup, CASE WHEN n_live_tup > 0 THEN round(n_dead_tup::numeric / n_live_tup * 100, 2) ELSE 0 END AS dead_ratio_percent, pg_size_pretty(pg_total_relation_size(relid)) AS total_size FROM pg_stat_user_tables WHERE n_dead_tup > 0 ORDER BY n_dead_tup DESC LIMIT 30;

当dead_ratio_percent超过 20%,就该检查 autovacuum 是否正常工作了。

4.2 空间回收:VACUUM 与 VACUUM FULL 的区别

很多新手以为删完数据跑一次VACUUM就能让磁盘占用降下来,这是误解。普通VACUUM只是把死元组占用的空间标记为可复用,空间还是留在表文件里,操作系统看到的文件大小不变。要让空间真正还给文件系统,需要VACUUM FULL或pg_repack。

我用一句话总结:VACUUM 是“内循环”,VACUUM FULL 是“搬家”。VACUUM FULL会重写整张表,执行期间会锁表,业务只能只读。生产环境如果表太大,更稳妥的方案是使用pg_repack在线重建,它通过触发器同步变更,能把对业务的影响降到最低:

pg_repack -d yourdb -t public.big_table -T postgres -k

注意:VACUUM FULL之后统计信息里的relpages会刷新,磁盘占用显著下降;但操作前务必确认磁盘剩余空间至少能容纳该表的完整副本,否则会“搬一半卡死”。大表收缩前也建议先约业务低峰窗口,至少先打个快照。

4.3 膨胀的定量估算

如果想知道一张表到底“虚胖”了多少,可以用pgstattuple扩展,它需要扫描整个表,生产环境要挑低峰执行:

CREATE EXTENSION IF NOT EXISTS pgstattuple; SELECT * FROM pgstattuple('public.big_table');

关键字段:

  • table_len:表总长度。
  • tuple_count/tuple_len:有效元组数和总字节。
  • dead_tuple_count/dead_tuple_len:死元组数量和字节。
  • dead_tuple_percent:死元组占比,越高表示越需要 VACUUM。
  • free_percent:页面空闲空间占比。

我习惯把dead_tuple_percent > 10%或free_percent > 20%作为是否需要重建表的参考线。不过这个扩展扫描成本比较高,几百 GB 的大表可能会跑很久,使用前一定要评估窗口。日常巡检还是用pg_stat_user_tables的n_dead_tup做粗筛,发现问题再上pgstattuple精查。

5. 实战排查案例:一条 SQL 揪出十亿日志表

5.1 现场排查步骤

某公司一个线上实例磁盘使用率达到 92%,团队先怀疑是归档日志过多,但检查pg_wal后发现只有 2GB。于是我用三分钟做了三层定位:

第一层:查库大小。

SELECT datname, pg_size_pretty(pg_database_size(datname)) FROM pg_database ORDER BY pg_database_size(datname) DESC;

结果:app_log库 600GB,占整个实例将近八成。

第二层:进库查表大小。

\c app_log SELECT n.nspname, c.relname, pg_size_pretty(pg_total_relation_size(c.oid)) AS total, pg_size_pretty(pg_relation_size(c.oid)) AS heap, pg_size_pretty(pg_indexes_size(c.oid)) AS idx FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind = 'r' ORDER BY pg_total_relation_size(c.oid) DESC LIMIT 10;

结果:access_log_2024子分区 210GB,access_log_2025子分区 180GB,历史分区加起来还有 170GB。业务说“每天都会自动清理老分区”,但某个月份清理任务失败一直没人发现。

第三层:查索引膨胀和死元组。

SELECT schemaname, relname, n_live_tup, n_dead_tup, round(n_dead_tup::numeric / NULLIF(n_live_tup, 0), 4) AS dead_ratio FROM pg_stat_user_tables WHERE schemaname = 'public' AND relname LIKE 'access_log%' ORDER BY n_dead_tup DESC;

到这里已经很清楚:没人清理的分区,加上频繁 UPDATE 导致许多死元组,空间被长期“锁住”。

5.2 处理结果

处理动作分三步:

  1. 先删除过期分区:DROP TABLE public.access_log_2024;
  2. 对剩下的生产分区执行VACUUM FULL或pg_repack,回收被死元组占据的文件空间。
  3. 把分区清理任务挂到定时调度中,并增加巡检告警:一旦pg_database_size超过阈值或分区表数量异常,自动通知值班人员。

最终磁盘占用从 92% 回落到 60% 左右。这次排查过程其实没有高深技术,就是一条条组合 SQL 按顺序执行。只要掌握了库级到表级的统计方法,再大的实例也能快速定位空间黑洞。

6. 常见问题速查与避坑指南

现象原因解决方案
删了大量数据,磁盘占用没下降死元组仍留在表文件中,普通 VACUUM 只做内部复用执行VACUUM FULL或用pg_repack重建表
pg_relation_size显示很小,但目录里文件很大索引、TOAST 未计入改用pg_total_relation_size看整体
查询父分区表返回 0 字节PG 分区表父对象无数据文件遍历所有子分区,递归汇总
pg_database_size远小于数据目录实际占用WAL、归档、备份文件不在统计范围用文件系统命令检查pg_wal、archive目录
报错权限不足当前用户无法访问指定对象用超管账号,或授予对象访问权限
pg_stat_user_tables死元组长期偏高autovacuum 参数太保守或未触发调整autovacuum_vacuum_scale_factor,或手动 VACUUM
\dt+大小和实际磁盘空间对不上元命令可能只显示主 fork以pg_total_relation_size为准

一个容易忽略的点是autovacuum 的参数配置。PG 的默认参数在大多数场景够用,但如果你的实例是频繁 UPDATE 的类 OLTP 业务,建议把单表的 autovacuum 阈值调小。例如:

ALTER TABLE public.big_table SET ( autovacuum_vacuum_scale_factor = 0.05, autovacuum_vacuum_threshold = 1000 );

scale_factor越小,越早触发 VACUUM,死元组越不容易堆积。缺点是 VACUUM 会更频繁。生产环境到底怎么配,取决于你对延迟和空间回填的取舍,不是越大越好。

还有个小坑:查询pg_database_size(current_database())时会消耗一定 IO,尤其是超大库。不要在业务高峰期频繁跑全库扫描,尽量落到低频巡检任务中。

7. 写在最后:一点个人建议

我见过太多团队把数据库“空间告警”当成一次性的救火动作,磁盘满了临时删一删。实际上,PostgreSQL 的空间统计函数应该变成你巡检脚本的基础设施。我自己的习惯是:

  • 每天凌晨跑一次库级统计,输出 top 5 库的大小。
  • 每周跑一次表级统计,观察周环比变化,超过 20% 涨幅立刻分析原因。
  • 每月对 top 20 的大表做一次膨胀评估,结合pg_stat_user_tables的死元组数据判断是否需要重建。

排查空间问题最怕的不是不够深入,而是“只查表面不查原理”。就算你今天用VACUUM FULL把磁盘腾出来了,如果不理解死元组机制和索引占用,过不了多久还会重演。希望这篇文章能帮你把 PostgreSQL 的空间统计体系真正吃透,下次再遇到磁盘告警,你不是更慌,而是更快。

最后再分享一个非常实用的小技巧:把下面这段 SQL 存成常用查询,它能把库级、表级、索引级一次性列出来:

SELECT current_database() AS db, n.nspname AS schema, c.relname AS table_name, pg_size_pretty(pg_total_relation_size(c.oid)) AS total, pg_size_pretty(pg_relation_size(c.oid)) AS heap, pg_size_pretty(pg_indexes_size(c.oid)) AS indexes, pg_size_pretty(pg_relation_size(c.oid, 'toast')) AS toast, c.reltuples::bigint AS approx_rows FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind = 'r' AND n.nspname NOT LIKE 'pg_temp%' AND n.nspname NOT IN ('pg_catalog', 'information_schema') ORDER BY pg_total_relation_size(c.oid) DESC;

你只需把它存成space_inventory.sql,需要的时候执行psql -d yourdb -f space_inventory.sql,一分钟内就能获得一份完整的空间体检报告。排查空间这件事,说到底就是“多看几次、多跑几条 SQL、形成肌肉记忆”。祝你的数据库永远不再需要救火。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/10 3:45:29

微信小程序预约挂号系统:SSM后台与数据库设计全解析

简介:一套面向高校毕业设计/课程设计的微信小程序预约挂号系统完整项目包,覆盖管理员、医生、用户三类角色,并附有本地运行辅助配置。后台基于 Java 的 SSM 框架开发,结合 MySQL 数据库实现数据管理,小程序端通过微信开…

作者头像 李华
网站建设 2026/10/10 3:45:17

VNX Unified存储实验指南:从双控初始化到ALUA路径验证

简介:本资源是EMC中国教育服务官方发布的《VNX统一存储实施实验室指南》中文版PDF文档,面向企业存储工程师、系统集成人员及数据中心运维技术人员,聚焦VNX统一存储平台的现场部署、配置管理与故障处理实战能力提升。文档涵盖VNX系统架构解析、…

作者头像 李华
网站建设 2026/10/10 3:45:06

C++项目用ADO接数据库:从AdoDB.rar到regtlibv12注册与避坑指南

简介:这是一份面向C程序员的ADODB数据库操作源码包,适合在Windows环境中通过COM/OLE DB访问SQL Server、Oracle、MySQL等数据库的开发者,用于解决C项目中的数据库读写、查询与事务管理等核心需求。压缩包共79个文件,大小约38.43MB…

作者头像 李华
网站建设 2026/10/10 3:44:52

Windows权限提升实战指南:从令牌到服务的提权命令详解

搞Windows安全这几年,我有一个特别深的感受:大部分“拿到一台机器却拿不到管理员权限”的窘境,不是卡在漏洞利用上,而是卡在基础权限模型没吃透。很多朋友上来就想用提权工具一把梭,结果不是被杀毒软件拦掉&#xff0c…

作者头像 李华
网站建设 2026/10/10 3:43:51

DBeaver连接MySQL建库建表实操指南

很多学MySQL的同学,第一关不是SQL语法,而是不知道用什么工具干活。命令行当然能建库建表,但那条路对新手太劝退。我用DBeaver连接本地MySQL、创建数据库表这套流程,已经重复过上百次,也带过不少零基础的人上手&#xf…

作者头像 李华
网站建设 2026/10/10 3:42:40

2003-2023地级市工业三废面板数据整理与清洗指南

做了几年环境数据分析,手里的城市面板数据少说也整理过几十套。要说哪类数据最让人又爱又恨,工业三废绝对排得上号——想研究污染排放和经济增长的关系,它是核心变量;想评估环境规制的影响,它是因变量;可真…

作者头像 李华