上周半夜两点,监控告警把我从床上叫醒:磁盘使用率92%。爬起来第一件事不是去看日志,而是登录数据库,想搞清楚到底是哪个库、哪张表把空间吃没了。这个场景做运维的应该都经历过,而PostgreSQL在这一点上确实非常友好——系统内置了一整套大小函数,pg_database_size、pg_total_relation_size、pg_size_pretty这些随时可以调用。再配合DeepSeek做查询整理,几分钟就能把所有库、所有表按大小排个名,定位问题比翻日志快多了。
这篇文章就把这块内容从头到尾盘一遍:先解释为什么查询数据库大小是个刚需,再把这些大小函数的原理和区别讲清楚,给出可以直接抄作业的SQL,最后聊聊我用DeepSeek辅助写这些查询的实际做法,以及几个容易踩的坑。新手看完能直接用,老手也可以对照查漏。
1. 到底什么时候需要“查询数据库大小”?
很多人觉得查大小就是看一眼硬盘满没满,其实这个需求比想象中频繁得多。我自己的经验里,至少有这么几类场景必须拿到准确的大小数据。
磁盘告警和容量规划是最直接的。生产环境磁盘告警是深夜被叫醒的主力原因,这时候你不是想知道大概有多满,而是需要立刻定位到具体哪个库、哪张表、甚至哪个索引在暴涨。PostgreSQL的大小函数做这件事非常顺手,因为它能一层层拆开:数据库大小、表大小、索引大小、TOAST大小,都能单独拿出来看。相比之下,MySQL早期版本查表大小要翻information_schema,差点意思。
备份和恢复窗口的估算也离不开它。备份耗时和数据库物理大小直接相关,恢复更是如此。我见过不止一次,因为没提前关注大小增长,备份窗口被越挤越短,最后凌晨的备份任务和业务高峰撞在一起,把IO拖垮。定期记录各库大小,配合历史趋势,可以提前预判“这个月备份会多慢”“下个月磁盘要不要加”,把问题消灭在发生之前。
巡检和慢查询定位同样需要大小数据。一张表查得慢,原因可能是索引没建好,也可能是表膨胀得太厉害——逻辑大小和物理页数都上去了,扫描代价自然暴涨。把表和索引的大小排在同一个查询里看,很多问题的方向马上清晰。比如某张表数据只有300MB,索引却2GB,那大概率是索引设计不合理,而不是数据本身的问题。
分库分表、数据归档这类改造,更需要精确度量。哪些库占了80%空间?哪些表是历史数据可以归档?决定动刀之前,没有准确的大小清单,方案基本靠拍脑袋。把全库的大小榜拉出来,按“表主体+索引+TOAST”三个维度看,归档优先级立刻就有了。
一句话:查询数据库大小不是“看一眼容量”这种一次性动作,而是运维巡检、容量规划、性能排查路上的基础设施。PostgreSQL把这套能力做成系统函数直接暴露给用户,就是让这类问题永远不需要去翻文件系统。
2. 认识这几个核心大小函数
PostgreSQL关于“大小”的函数都定义在pg_catalog里,不需要安装扩展,连超级用户权限都不用——只要有对应对象的访问权限就能查。它们之间的关系有时候容易绕晕,先给一张对照表,后面逐个拆。
| 函数 | 返回内容 | 典型用途 |
|---|---|---|
pg_database_size(datname) | 整个数据库全部对象占用的总字节数 | 看一个库的整体体量 |
pg_total_relation_size(regclass) | 表主体 + TOAST表 + TOAST索引 + 附属索引 | 看一张表“连同索引”的真实成本 |
pg_table_size(regclass) | 表主体 + TOAST表 + TOAST索引 | 看表数据本身,不含普通索引 |
pg_relation_size(regclass) | 表的主体数据文件(main fork) | 不含TOAST、不含索引,最纯粹的“表数据” |
pg_indexes_size(regclass) | 该表附属索引的总大小 | 评估索引开销 |
pg_size_pretty(bigint) | 把字节数格式化成MB、GB等易读单位 | 让人眼能直接看懂 |
pg_size_bytes(text) | 反向解析字符串为字节数 | 配置项换算时有用 |
注意一下:pg_relation_size也能传索引的OID,这时返回的就是索引文件大小。同一个函数,传表名返回表主体大小,传索引名返回索引大小,靠参数类型来判断。
很多人会混淆pg_table_size和pg_total_relation_size。简单记忆:total=table+ 普通索引。pg_table_size本身已经包含了TOAST的部分,所以不要把TOAST再单独加一遍。
TOAST是什么,必须多说一句。PostgreSQL的页大小默认8KB,一行数据如果超过了大约2KB(实际上是一个页能容纳的行数阈值),变长字段就会被压缩,甚至拆到额外的TOAST表里。TOAST表有自己的文件,独立于主表存储。所以pg_relation_size返回的只是主表文件的大小,没算TOAST。一些大字段特别多的表,TOAST可能比主表大好几倍,只看pg_relation_size会严重低估真实存储成本,这也是后面要聊的常见坑之一。
这些函数返回的都是字节数,是bigint。直接看一长串数字让人头大,所以正经用法永远是套一层pg_size_pretty:
SELECT pg_size_pretty(pg_database_size('mydb'));需要强调一个特点:这些大小函数都是实时计算的,不是读统计信息快照。每次调用都要实际去扫描目录、统计页面数量。对于小库来说是毫秒级,但碰上几TB的库,pg_database_size也可能慢到让你怀疑是不是卡住了——它确实要遍历这个库相关的所有文件。
3. 实操:从全库总览到单表定位
函数本身很简单,真正的价值在于组合起来解决实际问题。这一节给几个我日常最常用的查询,全部验证过,可以直接复制替换库名或表名。
3.1 查看当前数据库大小
SELECT pg_size_pretty(pg_database_size(current_database())) AS db_size;current_database()返回当前连接的库名,省去手写库名的麻烦。这条命令在确认“某台服务器上的库里谁最大”时不是首选,因为只能看当前库。
3.2 列出实例上所有数据库,按大小排序
SELECT datname, pg_size_pretty(pg_database_size(datname)) AS size FROM pg_database ORDER BY pg_database_size(datname) DESC;这里有个细节:排序必须用未格式化的pg_database_size(datname),不能按size排。因为pg_size_pretty返回的是文本,6.9 GB和80 MB按字符串排序的话,结果是不可预测的。我见过很多新手在这上面栽跟头,排出来最大最小完全混乱,其实只要排序字段用原始字节数就没问题。
执行结果的示例大概长这样:
| datname | size |
|---|---|
| warehouse | 856 GB |
| orders | 312 GB |
| analytics | 45 GB |
| postgres | 8 MB |
如果只想看库里有没有“异常大”的,可以加HAVING条件过滤,比如只显示超过10GB的,省得被一堆几十MB的开发库干扰视线。
3.3 查看某个库内所有表的大小排行
这是使用频率最高的一条:
SELECT n.nspname AS schema_name, c.relname AS table_name, 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 n.nspname NOT IN ('pg_catalog', 'information_schema') AND c.relkind = 'r' ORDER BY pg_total_relation_size(c.oid) DESC LIMIT 20;拆开解释一下。pg_class是系统表,存了所有表和索引的元信息;pg_namespace是命名空间,也就是schema。过滤掉pg_catalog和information_schema是为了不把系统对象算进来。relkind = 'r'表示只要普通表,不要索引、视图、序列这些。
如果不想和系统表打交道,也可以用pg_stat_user_tables视图:
SELECT relname, pg_size_pretty(pg_total_relation_size(relid)) AS total_size FROM pg_stat_user_tables ORDER BY pg_total_relation_size(relid) DESC LIMIT 20;这个写法更简洁,但注意pg_stat_user_tables里没有直接的schema信息,多个schema重名时会分不清。生产环境我习惯用pg_class那一版,信息完整,多加两个join也不麻烦。
实际看结果的时候,不要只盯着第一行。我通常会把前20条整体扫一遍:如果最大表和第二大表之间有数量级的断层,说明单体大表问题;如果前20名全部很大,说明这个库整体设计偏“重”,可能要考虑分区或归档了。
3.4 只看表的索引大小,揪出索引膨胀
表大小是表+索引的合计,但在排查索引膨胀时,需要单独看索引:
SELECT i.relname AS index_name, t.relname AS table_name, pg_size_pretty(pg_relation_size(i.oid)) AS index_size FROM pg_index idx JOIN pg_class i ON i.oid = idx.indexrelid JOIN pg_class t ON t.oid = idx.indrelid ORDER BY pg_relation_size(i.oid) DESC LIMIT 20;或者用现成的视图pg_stat_user_indexes,更简单:
SELECT indexrelname, pg_size_pretty(pg_relation_size(indexrelid)) AS index_size FROM pg_stat_user_indexes ORDER BY pg_relation_size(indexrelid) DESC LIMIT 20;索引膨胀的经典情况是:某张表的索引占了表本身大小的好几倍。这是因为频繁更新时,索引页的旧版本不会立刻被复用,膨胀比例可以非常夸张。看到这种场景,基本就可以安排pg_repack或者重建索引了。
3.5 用分项拆解函数精确定位空间去向
有时候我们不只是想知道谁大,而是想知道大在哪里。一条查询把表拆开:
SELECT c.relname AS table_name, pg_size_pretty(pg_relation_size(c.oid)) AS table_data, pg_size_pretty(pg_indexes_size(c.oid)) AS index_data, 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 n.nspname NOT IN ('pg_catalog', 'information_schema') AND c.relkind = 'r' ORDER BY pg_total_relation_size(c.oid) DESC LIMIT 10;这种拆解的价值在于“对症下药”。如果total_size大是因为index_data占大头,那重建索引或删掉冗余索引比任何添加磁盘都更有效;如果table_data占大头,就要考虑数据归档、分区裁剪;如果总大小不算大但查询还慢,那就是别的问题了,跟容量无关。数据是不会骗人的,把大小拆开看,方向基本不会错。
3.6 反向解析:用pg_size_bytes处理自动任务
pg_size_bytes比较冷门,但在写脚本时很实用。比如某个自动化脚本需要“找出大小超过10GB的库”,如果配置项是文本10 GB,可以这么用:
SELECT datname, pg_size_pretty(pg_database_size(datname)) AS size FROM pg_database WHERE pg_database_size(datname) > pg_size_bytes('10 GB') ORDER BY pg_database_size(datname) DESC;这函数就是把'10 GB'这种人类友好的写法解析成字节数,省得自己在脚本里做单位换算,也避免写错数量级。
4. 用DeepSeek辅助写大小查询的实战做法
说回标题里那个组合:DeepSeek和PostgreSQL大小函数。其实逻辑很简单——PostgreSQL提供了准确、完整的大小函数体系,而DeepSeek的价值在于帮你把这些函数快速组织成能解决问题的SQL,尤其是当你记不清函数名、或者需要临时写一个多表关联的排行查询时。
4.1 自然语言直接生成SQL
我在日常运维中经常临时起意查一个东西,以前要翻文档回忆函数签名,现在直接把需求扔给DeepSeek。举个例子,我当时的输入是:
帮我写一条PostgreSQL SQL,列出当前实例所有非系统数据库的名称和大小,按大小降序,用易读单位显示,结果限制20行。
它给出的答案跟我在3.2节写的基本一致:
SELECT datname, pg_size_pretty(pg_database_size(datname)) AS size FROM pg_database WHERE datistemplate = false AND datallowconn = true ORDER BY pg_database_size(datname) DESC LIMIT 20;注意它还主动加了datistemplate = false过滤掉模板库,这个细节说明它理解“非系统数据库”这个需求。模板库确实不该混进容量排行里。
再比如我想找“哪些表的索引占比超过50%”,这种需求如果自己拼SQL要写子查询,用自然语言就很直接:
查出当前库里所有用户表,显示表名、表数据大小、索引大小和索引占比,索引占比超过50%的优先排前面。
DeepSeek会组合pg_total_relation_size、pg_relation_size、pg_indexes_size,用一条带子查询的语句完成,还能保证排序逻辑正确。这比我徒手写要快得多。
4.2 报错信息丢进去,几秒钟得到排查方向
执行SQL报错是家常便饭。最典型的是权限问题:
ERROR: permission denied for database mydb把完整的报错贴给DeepSeek,它通常会给出三条主要检查路径:当前用户是否拥有该库的CONNECT权限、是否被授予了pg_read_all_stats角色、表级大小查询需要相应表的权限。我第一次遇到这个报错时,用DeepSeek定位到是某个只读账号缺少pg_read_all_stats,加上就好。
它的价值不在于替代DBA的判断,而是把“遇到报错→查文档→确认原因”这个过程压缩到几秒钟。大小函数相关的报错就那么几类,语料充分,回答一般都比较准确。
4.3 生成巡检脚本和持续监控方案
查一次大小不难,难的是持续监控。用DeepSeek生成一个每天定时执行、把Top20表大小写入日志的脚本,是目前我觉得最好用的场景之一。下面是一个典型的Prompt:
写一个Linux shell脚本,用psql查询PostgreSQL中每个库的大小和每个库内最大的5张表,把结果按日期追加写入 /var/log/pg_size_daily.log,失败时返回非零退出码。
它会给出可行的脚本骨架:psql -c执行SQL,date +%F拼文件名,最后用$?检查退出码。这些逻辑本身不复杂,但让DeepSeek先出一版,再按你的环境改改,比从零写要快不少。
4.4 用DeepSeek前必须掌握的三个防错要点
AI生成的SQL不是免检产品,用得多了,我总结出三个必须自己把关的地方。
第一,系统对象过滤条件不能丢。如果Prompt里只说“列出所有表”,生成的SQL可能把pg_catalog里的系统表也带进来,统计出来的大小完全失真。看到不含nspname NOT IN ('pg_catalog', 'information_schema')的版本,要能自己补上。
第二,排序必须基于数值而不是格式化文本。DeepSeek偶尔会生成ORDER BY pg_size_pretty(...)这种写法,结果看起来是按照字符串排的,容易出问题。判断标准很简单:凡是用于排序的字段,必须传原始字节数。
第三,TOAST和索引的包含关系要确认。有时候你问“查一下这张表多大”,它会用pg_relation_size,返回的只是表主体,不含索引。如果业务上想看的“多大”包括了索引和TOAST,就要明确说用pg_total_relation_size。需求描述越准确,返回结果越不用改。
4.5 从“会查”到“会问”的小技巧
我使用DeepSeek配合数据库运维的经验是:把问题描述得越接近SQL逻辑,结果越能用。与其说“看看哪个库很大让我清理一下”,不如说“找出占用空间最大的10个数据库,显示库名和大小,按大小降序”。心里先有一个大概的SQL结构——要查哪个视图、用哪个函数、怎么过滤、怎么排序——再让AI补全,这种方法在任何数据库运维场景都通用。
5. 常见问题与排查技巧实录
大小函数看着简单,实际用起来有一堆细节坑。下面这些都是我在现网环境踩过的,整理出来供参考。
5.1 查出来的大小和du看到的不一致
有段时间我发现pg_database_size算出来某个库是500GB,但du -sh那个库对应的目录不到400GB。最初以为函数算错了,后来才捋清楚:数据库的物理目录不止包含表和索引文件,还有一堆别的东西。
排查的思路是:先看整个数据目录的构成。base/目录放着用户数据库,pg_wal/是WAL日志,postgresql.conf和pg_control这些是配置文件。WAL可能占到几十GB甚至更多,特别是没用异步提交或归档策略密集写入时,pg_wal目录经常成为“隐藏容量黑洞”。pg_database_size统计的是该库的表和索引文件,不会把WAL算进去,所以两者对不上是正常现象。
真要核对某个库的文件,需要去base/<数据库OID>/目录下看。数据库OID可以从pg_database表查。但表文件可能分布在多个目录,而且TOAST表是另一个文件,手动核对非常繁琐,一般不建议做。知道“为什么对不上”就够了。
5.2 删了大量数据,大小却一点没变小
这是最常见的认知误区。PostgreSQL里执行DELETE只是把行标记为不可见,空间并不会归还给操作系统。表文件里的那些页还在,只是变成“可复用”状态。
要让它变小,常见做法是VACUUM FULL或者pg_repack。但注意VACUUM FULL会持锁,生产环境搞不好会阻塞写入,必须安排在维护窗口或者用pg_repack在线处理。还有一些DBA会犯另一个错误:删完数据什么都不做,以为下次VACUUM会自动回收。普通VACUUM只清理死元组腾出页内空间,它不会主动把文件末尾的空页截断。想从文件系统层面看到释放,必须执行带FULL的重写操作。
大小函数返回的字节数会如实反映这种状态:删完数据不重建,查出来还是旧的那么大。这也是为什么巡检时看到表逻辑数据量明显小于物理大小,第一反应应该是“表膨胀了”。
5.3 权限不足导致查询失败
有只读账号登录后执行pg_database_size报permission denied,这个问题不止一次被问到。原因通常是当前角色没有对应数据库的CONNECT权限,或者不是pg_read_all_stats的成员。对于表级的大小函数,还需要对该表有权限。
快速验证方法是:用超级用户执行GRANT CONNECT ON DATABASE xxx TO xxx,或者把账号加入pg_read_all_stats角色。如果只是想看大小、不需要数据,给pg_read_all_stats更合适,权限范围也更克制。
5.4 TOAST导致表大小严重失实
前面提到过TOAST,这里给一个真实案例。有一张表存用户行为日志,有个字段是JSONB,数据膨胀得非常厉害。用pg_relation_size查出来只有60GB,总觉得“还可以”,但磁盘告警却一直没停。后来用分项拆解一查,pg_total_relation_size显示250GB,差值几乎全是TOAST和索引。
从那以后,我判断一张表是否“大”,只会用pg_total_relation_size,不再看pg_relation_size。TOAST是PostgreSQL自动管理的内部表,用户很容易忽略它,但它占的空间真实存在,而且大字段场景下往往是主要成本。
5.5 超大库执行pg_database_size特别慢
前面说过这些函数是实时计算的,每次调用都要遍历文件。一个2TB的大型库执行pg_database_size可能要几十秒甚至几分钟。有同事一度以为数据库卡死了,直接重启,结果问题更大。
应对手段有三个:一是把这类查询放到业务低谷;二是如果只是想看趋势,建议定时把结果存到一张统计表里,巡检时直接查历史表而不是现场计算;三是配合监控系统,从操作系统层面对比趋势,避免在高峰期反复执行大范围统计。
这里提供一个简单的记忆表,方便日后排查:
| 现象 | 可能原因 | 优先排查方向 |
|---|---|---|
| 库大小远小于磁盘占用 | WAL、归档日志、临时文件 | pg_wal目录、pg_stat_archiver |
| 删除数据后大小不变 | 表膨胀 | VACUUM FULL或pg_repack |
| 查询报权限错误 | 缺少CONNECT或pg_read_all_stats | 检查角色权限 |
| 表大小合计远超直觉 | TOAST文件占空间 | 用pg_total_relation_size复核 |
| 大库查询卡顿 | 实时扫描文件导致 | 低谷执行或定时落库 |
6. 我的日常巡检习惯
最后分享一个我自己坚持了很久的做法:写一个简单的巡检脚本,每天固定时间记录全库Top20表和最大索引。脚本不复杂,就是把前面几条SQL交给cron执行,把输出追加到按日期命名的文件里。不需要任何可视化平台,连续跑一个月,哪个库在涨、哪张表突然加速,翻翻日志就一目了然。
有一次我就是靠这个日志发现一张日志表每周五都会出现一次明显的容量“台阶”,排查后定位到是周五的批处理任务没有做周期清理,数据只增不减。这种问题,没有历史大小记录,靠现场排查很难复现,因为查的时候可能已经回落了。
我个人对大小函数的使用体会是:PostgreSQL这套函数设计得非常顺手,它把DBA最常用的容量查询抽象成了几个明确的小函数,组合起来就能应对绝大多数场景。而DeepSeek在这里扮演的角色更像一个随叫随到的“查询助手”——你不需要背函数签名,不需要翻系统表结构,只要把需求说清楚,它就能给出可用的SQL。前提是你自己得懂函数之间的包含关系、知道怎么排序、会检查系统对象过滤条件,AI输出的是草稿,最后的校验还得靠自己的基本功。这两者结合起来,日常数据库容量管理这件事,确实可以变得非常轻松。