网易首页 > 网易号 > 正文 申请入驻

11GB数据占了156GB:PostgreSQL膨胀14倍

0
分享至




一、一张附件表,两个对不上的数字

一个 165 GB 的 PostgreSQL 数据库,最大的一张表吃掉 156 GB。运维的第一反应通常是:这张表里堆了 156 GB 的附件。

作者 Amit Kumar 起初也是这么想的,然后他去量了一下——活数据只有 11 GB。5703 条记录,156 GB 的磁盘占用,中间空着 145 GB。

更值得琢磨的是这张表的构成:表本身的堆只有 31 MB,索引 13 MB。两个加起来 44 MB,占 156 GB 的万分之三不到。也就是说,这 156 GB 里几乎一个字节都不在看得见的地方,全在一张平时根本不会去翻的表里——TOAST。

这篇文章把原文的排查过程完整复现了一遍,跑在本机的 PostgreSQL 18.6 上。原结论站得住,但有三处它没说透的地方,实测下来比结论本身更有用。

二、六步排查,每一步到底在量什么

先把原文的路径走一遍,同时说清每一步的数字是什么口径。

第一步,量整个库。pg_database_size() 给出 165 GB。

第二步,找最大的表。从 pg_statio_user_tables 里按 pg_total_relation_size() 倒序取前 20,附件表毫无悬念地排第一。

第三步,把这张表拆开。原文在这里用了三个体积函数:

SELECT pg_size_pretty(pg_total_relation_size('schema.table_name')) AS total_size,       pg_size_pretty(pg_relation_size('schema.table_name'))       AS heap_size,       pg_size_pretty(pg_indexes_size('schema.table_name'))        AS index_size;

结果是 total 156 GB、heap 31 MB、index 13 MB。

这里补一句官方函数定义,很多人卡在这一步:pg_total_relation_size 算的是"包含全部索引和 TOAST 数据"的磁盘占用,pg_relation_size 只算主数据分支。所以这两个数字之间的差额就是 TOAST 表——156 GB 减去 44 MB,缺口全在 TOAST 里。

原文是到第六步用 reltoastrelid 关联 pg_class 才坐实这一点的。其实第三步就能看出来,写法还可以更直接:

SELECT c.relname,       pg_size_pretty(pg_relation_size(c.oid))           AS heap,       pg_size_pretty(pg_relation_size(c.reltoastrelid)) AS toast,       pg_size_pretty(pg_indexes_size(c.oid))            AS indexFROM pg_class c WHERE c.relname = 'your_table';

一行拆成三块,不用猜。

第四步,量活数据。原文把所有行的大小加了起来:

SELECT count(*) AS rows, pg_size_pretty(sum(pg_column_size(t.*))::bigint) AS live_sizeFROM schema.table_name t;

得到 5703 行、11 GB。156 除以 11,约等于 14 倍。

这一步的 pg_column_size 是最容易看错的地方。官方对它的定义是"存储单个数据值所用的字节数",后面还跟了一句关键的话:直接作用在表的列上时,这个结果会反映所做的压缩。

也就是说,它给的是落盘后的大小,不是这段数据的原始大小。为了确认这一点,本机跑了一组对照:三类附件、每类 40 行,原始体积在 40 MB 到 115 MB 之间。

载荷类型

逻辑体积

pg_column_size 合计

TOAST 实际落盘

随机二进制 PDF/JPG/ZIP

40 MB

40 MB

41 MB

随机十六进制文本 base64/hash 类

40 MB

40 MB

41 MB

重复中文正文 XML/JSON 类

115 MB

1349 kB

1440 kB

前两类压不动,三个数字几乎一样。第三类逻辑上 115 MB,落到盘上只剩 1349 kB,压掉了 98.8%。

这组数字有两个用处。其一,原文那个 11 GB 是已经压过的落盘数字,所以 156 除以 11 这个比值成立——它比的是"盘上占了多少"和"这些数据实际需要多少盘",口径是对得上的。其二,反过来提醒:如果拿 octet_length() 求出原始字节数再算倍数,会得到一个虚高得多、却毫无操作意义的数字。

再看第三行:同样叫"附件",文本型内容和二进制内容落盘能差几十倍。所以"这张表有 N GB 附件"这句话,本身不告诉你任何事。

第五步,算可回收空间。156 减 11 等于 145 GB。

第六步,定位到 TOAST。用 reltoastrelid 关联 pg_class,确认绝大部分占用都在 TOAST 表里。

根因链条原文写得清楚:附件被归档或删除,行被标记为已删除,autovacuum 清理掉死元组,空间变成可复用,但物理文件仍然留在那里。

按原文的每行 1.93 MB 折算,145 GB 大约相当于 7.5 万条已经被删掉的附件。

三、三件原文没说透的事 第一件:普通 VACUUM 有时候自己就把文件缩了

原文直接跳到了 VACUUM FULL。但官方文档里有一句常被跳过的话:标准 VACUUM 不会把空间还给操作系统,"除非表中有一个或多个位于末尾的页完全空闲,并且能顺利获得排他表锁"。

这句话的意思是,文件会不会缩,取决于被删掉的数据落在文件的哪个位置。本机做了个 A/B。

场景 A,先灌 300 行、每行 1 MiB 不可压缩数据,TOAST 表 308 MB,然后删掉最后 260 行,先不 VACUUM 量一次:

对象

文件大小

活元组

死元组

死元组字节

主表

24 kB

40

260

15,541

TOAST 表

308 MB

21,040

136,760

277,553,120

然后只跑一次普通 VACUUM,耗时 210 毫秒:主表从 24 kB 降到 8192 字节,TOAST 表从 308 MB 降到 41 MB。文件缩掉了 267 MB,没有锁表,没有停机。

场景 B,同样 300 行,删掉前 260 行,再插入 260 行新数据把文件尾部占住,然后 VACUUM:

阶段

主表

TOAST

合计

穿插删改后 VACUUM

40 kB

575 MB

582 MB

死元组清零了,文件一个字节没缩。按原文那套算法算:582 除以 300,1.94 倍。

两个场景唯一的区别,就是被删掉的数据在不在文件末尾。所以排查顺序应该是:先跑一次普通 VACUUM,再量一次文件大小,不行才考虑重写全表。前者成本是几百毫秒,后者要停业务。

第二件:官方其实不建议随便上 VACUUM FULL

原文把 VACUUM FULL 列为首选方案,并提醒要安排维护窗口、准备足够空闲空间、注意排他锁。这些提醒都对,但官方文档的措辞比"提醒"重得多:

• 管理员应当尽量使用标准 VACUUM,避免 VACUUM FULL。

• 如果这张表将来还会重新长大,做这件事没什么意义。

• 它要写一份新副本,旧副本在新副本完成前不能释放,所以临时需要大约等于表大小的额外磁盘空间。

第二条尤其值得抄下来贴在案头。空间收回来之后,只要附件继续往里灌,那些空间迟早会再被吃掉——你付出的是一次停机加一份额外磁盘,换来的是一段时间的账面好看。

实测代价。拿场景 B 那张 582 MB 的表(其中 267 MB 是空闲空间)跑一次 VACUUM FULL:

项目

实测结果

耗时

5.88 秒

表体积

582 MB 降到 312 MB

期间读这张表

2.5 秒超时被取消

期间读同库另一张表

正常返回

数据目录峰值增量

约 325 MB

"读这张表被取消"不是形容:VACUUM FULL 拿的是 ACCESS EXCLUSIVE 锁,连 SELECT 都要排队。它不是"有点慢",是这张表在整个过程中对外不可用。同一时刻同库另一张表照常可读,被挡住的只有这一张。

第三件:监控里根本看不到这些死空间

这一条是复现时意外发现的,也是最值得补进原文的一处。

原文的排查路径是"先看表大,再看活数据小"。但运维天天会看的那个视图,给的是完全不同的画面。造一张 60 行、每行 1 MiB 的表,删掉 48 行,然后在同一时刻看四个地方:

数据来源

看到的东西

pg_stat_user_tables 主表

死元组 48 条

pg_stat_all_tables TOAST 表

死元组 25,248 条

pgstattuple 主表

死元组字节 2,832

pgstattuple TOAST 表

死元组字节 51,240,576

主表 48 条死元组、2,832 字节,放在任何一份监控大盘上都是"健康"。而同一时刻,TOAST 表里躺着 25,248 条死块、48.9 MB——这张表总共才占 62 MB,死空间占了 79%。

原因很直白:一条带附件的记录被删掉,主表只少一行;而 1 MiB 的附件在 TOAST 表里是 526 个分块,每个分块都是一行独立记录。删 48 条业务记录,等于在 TOAST 表里干掉两万五千多行。按这个比例,原文那些 1.93 MB 的附件,一块就是一千多个分块,7.5 万条折过去是七千多万行死块。

所以,所有基于主表 n_dead_tup 的膨胀监控,对附件表天然是失明的。

真正能一眼看穿的,是 pgstattuple 扩展,它可以作用在 TOAST 表上:

SELECT table_len, tuple_count, dead_tuple_count, dead_tuple_len, free_spaceFROM pgstattuple((SELECT reltoastrelid FROM pg_class WHERE relname='your_table'));
还有一个前提问题:这 145 GB 是浪费还是预留

官方对这件事的表述是"稳态使用":每张表占的空间等于它的最小尺寸,加上两次 vacuum 之间用掉的部分。VACUUM 标为空闲的空间可以被复用,不是永久损失。

实测验证一下:场景 B 里 TOAST 表有 267 MB 空闲空间,再往里灌 200 行新附件,TOAST 表占用从 575 MB 到 575 MB——一个字节都没涨,新数据把这 267 MB 吃回去了。

所以"145 GB 可回收"这句话要分两种情况看。这张表还会继续长,那这些空间本来就会被用回去,回收的价值主要在磁盘账面;这张表已经不再增长,那 145 GB 才真的是一笔可以从采购清单上划掉的钱。

四、下次遇到,按这个顺序走

1. 先拆体积。用 pg_class 一行拆成主表、TOAST、索引三块,别猜。

2. 用 pgstattuple 同时量主表和 TOAST 表的死元组与空闲空间。这一步直接告诉你"有多少能收",比全表扫一遍求和轻得多。

3. 先跑一次普通 VACUUM,再量文件。文件尾部的空闲页会被自动截断,有时候问题自己就没了,成本是几百毫秒。

4. 普通 VACUUM 无效,才考虑重写全表。要准备三样东西:排他锁带来的停写停读、大约等于表大小的额外磁盘、以及"这张表以后还会不会长"这个问题的答案。

5. 如果这张表是按周期整体清空的,直接用 TRUNCATE,它立即释放空间,不需要后续 VACUUM。这是官方专门给出的提示。

6. 长期看要问的是另一个问题:为什么每个 1.93 MB 的附件,要住在一张关系表里。

PostgreSQL 本身开源免费,采用 PostgreSQL License,当前版本 18.6,支持到 2030 年 11 月,主仓库 github.com/postgres/postgres 有 22,282 个 star。

五、互动

一句话总结:表大不等于数据多,数据多不等于盘上占得多,盘上占得多也不等于这笔空间丢了。

你手上的库有没有那种"常年几百 GB、业务量却没怎么变"的表?说说表名后面跟着的数字,下次挑几个典型的拆一拆。

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
广东南澳大桥一私家车越线撞上公交车,两车车头相撞,公交车上有乘客,当地交警:目前还在清理现场,将尽快恢复道路通行,事故正在调查

广东南澳大桥一私家车越线撞上公交车,两车车头相撞,公交车上有乘客,当地交警:目前还在清理现场,将尽快恢复道路通行,事故正在调查

潇湘晨报
2026-10-05 18:14:54
2026年生理学或医学奖诺奖得主:研究奠定光遗传学基础,论文曾因“看不出实际用途”被拒

2026年生理学或医学奖诺奖得主:研究奠定光遗传学基础,论文曾因“看不出实际用途”被拒

红星新闻
2026-10-05 21:43:32
又曝出轨大瓜,男子自曝妻子出轨班长5个月,班长只给他妻子买过情趣内衣,还曝光俩人大尺度聊天内容

又曝出轨大瓜,男子自曝妻子出轨班长5个月,班长只给他妻子买过情趣内衣,还曝光俩人大尺度聊天内容

汉史趣闻
2026-09-10 18:28:47
大家提前做好准备,不出意外的话,10月开始中国或将出现4大转变

大家提前做好准备,不出意外的话,10月开始中国或将出现4大转变

梦回千年aa
2026-10-04 09:38:11
空气炸锅的第一批受害者,已经出现了

空气炸锅的第一批受害者,已经出现了

牛锅巴小钒
2026-10-05 02:47:58
美联航闹乌龙!华人乘客拿着“别人登机牌”一路过安检、海关,坐上飞机……

美联航闹乌龙!华人乘客拿着“别人登机牌”一路过安检、海关,坐上飞机……

华人生活网
2026-10-06 04:13:24
40年养老口号大变!从“政府来养老”到“自己备养老”,看懂普通人晚年真相

40年养老口号大变!从“政府来养老”到“自己备养老”,看懂普通人晚年真相

小虎新车推荐员
2026-10-06 01:45:48
多个中亚国家加强与俄罗斯接壤边境检疫,应对疑似肺鼠疫风险

多个中亚国家加强与俄罗斯接壤边境检疫,应对疑似肺鼠疫风险

桂系007
2026-10-05 22:19:08
足坛重磅官宣!U23国脚向余望与名宿李明之女李衍慧官宣恋情

足坛重磅官宣!U23国脚向余望与名宿李明之女李衍慧官宣恋情

陈意小可爱
2026-10-05 11:23:40
韩国亚运男足天塌了?夺冠免兵役政策或被废除 队长言论惹怒多名议员

韩国亚运男足天塌了?夺冠免兵役政策或被废除 队长言论惹怒多名议员

风过乡
2026-10-05 10:51:24
伊朗军方宣布:决定升级导弹射程,原因是从与美国和以色列的战事中汲取了“教训”

伊朗军方宣布:决定升级导弹射程,原因是从与美国和以色列的战事中汲取了“教训”

政知新媒体
2026-10-05 08:47:52
打哭孙心然,却因首盘争议球重打被外网骂疯,高芙怎么了?

打哭孙心然,却因首盘争议球重打被外网骂疯,高芙怎么了?

网球之家
2026-10-06 09:27:45
还剩下2000人,一个也不放过——以色列国防军击毙了10月7日入侵以色列的恐怖分子的三分之二

还剩下2000人,一个也不放过——以色列国防军击毙了10月7日入侵以色列的恐怖分子的三分之二

老王说正义
2026-10-06 00:03:11
一度被球迷干扰!郑钦文赛后道歉:没控制住吼了几声 我会努力改变

一度被球迷干扰!郑钦文赛后道歉:没控制住吼了几声 我会努力改变

醉卧浮生
2026-10-05 17:45:34
艾滋病新增130万!多人无辜中招!公众场合千万坚持“6不碰”原则

艾滋病新增130万!多人无辜中招!公众场合千万坚持“6不碰”原则

医学科普汇
2026-10-05 18:49:25
别等失去了才后悔。 厦门小伙暗恋高一班主任多年,大学毕业后果断追娶回家,祝有情人终成眷属

别等失去了才后悔。 厦门小伙暗恋高一班主任多年,大学毕业后果断追娶回家,祝有情人终成眷属

捣蛋窝
2026-10-03 09:50:51
8天前曾3-0国足!新西兰1-2日本 全场仅3射 日本男足热身赛4战全胜

8天前曾3-0国足!新西兰1-2日本 全场仅3射 日本男足热身赛4战全胜

我爱英超
2026-10-05 20:32:37
俄军单日伤亡再破2千!为一周以来的第二次

俄军单日伤亡再破2千!为一周以来的第二次

项鹏飞
2026-10-05 20:01:20
Shams:唐斯愿为续约降薪,但尼克斯报价仅为4年2亿美元

Shams:唐斯愿为续约降薪,但尼克斯报价仅为4年2亿美元

懂球帝
2026-10-06 07:43:12
郭华萍:表面是人民公仆,实际是电诈头目,竟折磨多名中国人致死

郭华萍:表面是人民公仆,实际是电诈头目,竟折磨多名中国人致死

空樽对月花独瘦
2026-10-06 05:45:37
2026-10-06 10:04:49
侃故事的阿庆
侃故事的阿庆
几分钟看完一部影视剧,诙谐幽默的娓娓道来
793文章数 9593关注度
往期回顾 全部

科技要闻

光遗传学是什么,为什么这项发现如此重要

头条要闻

牛弹琴:中国人还在快乐过节 世界上至少发生三件大事

头条要闻

牛弹琴:中国人还在快乐过节 世界上至少发生三件大事

体育要闻

30天30队·热:扬尼斯、阿德巴约与克雷

娱乐要闻

蔡康永回应漏洞百出,太平轮旧事被扒

财经要闻

美印贸易谈判陷入僵局

汽车要闻

方程豹9月热销破4万 首款皮卡鲨鱼将于四季度上市

态度原创

旅游
家居
健康
教育
游戏

旅游要闻

传播琅琊风华!临沂文旅推荐官景区权益清单发布

家居要闻

2026建博会(广州) 公装联探展交流活动

刷酸祛痘,为什么有人翻车?

教育要闻

这里就是海南最牛的贵族学校。

国产抗日《抵抗者》玩法不止大战场 有些事不能忘

无障碍浏览 进入关怀版