首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >PG这个设计极不合理: 经常加删列, 可导致无法再添加列

PG这个设计极不合理: 经常加删列, 可导致无法再添加列

作者头像
用户4035096
发布2026-07-10 12:15:24
发布2026-07-10 12:15:24
50
举报

发现一个PG极不合理的设计, DROP 的 COLUMN 依旧占用元数据, 而且即使 VACUUM FULL 也不会移除 pg_attribute 中的 dropped column.

如果你的表经常加列删列, 可能很快就会达到PG单表1600列的上限, 导致无法再加列.

太长不看

执行过drop column的表, 在对其执行vacuum full后, pg_attribute里面的dropped column还在不在? 会不会继续占用 null bitmap? 会不会继续占用单表最大1600列计数?

VACUUM FULL 后,pg_attribute 里的 dropped column 还在。它仍然 attisdropped = true,仍占用 attnum 槽位,仍计入单表最大 1600 列限制;VACUUM FULL 只会重写数据文件,把 dropped column 的旧值重构成 NULL,从而回收旧值占用的数据空间,但不会删除 catalog 里的 dropped column 元数据,也不会重排/复用列号。

null bitmap 方面:是的,dropped column 对新 tuple/重写 tuple 会作为 NULL 参与 null bitmap;bitmap 大小按 tuple descriptornatts 计算,而 dropped column 仍在 natts 里。

结论

对执行过 ALTER TABLE ... DROP COLUMN 的普通表再执行 VACUUM FULL 后,pg_attribute 里对应的 dropped column 记录仍然存在,通常表现为:

  • attisdropped = true
  • atttypid = 0
  • attname 被改成类似 ........pg.dropped.<attnum>........ 的内部名字
  • attnum 槽位仍然保留

所以:

  • 它仍然计入 PostgreSQL 单表最大 1600 个用户列的限制。
  • 它仍然影响 heap tuple 的属性数 natts
  • 新写入或重写后的 tuple 会把 dropped column 标记为 NULL,因此只要存在 dropped column,tuple 通常需要 null bitmap。
  • VACUUM FULL 能回收 dropped column 旧值占用的数据空间,但不会压缩 attnum,不会删除 pg_attribute 中的 dropped column 目录记录,也不会把这些列槽位还给后续 ADD COLUMN 使用。

如果目标是“彻底消除 dropped column 的目录槽位和 1600 列计数影响”,需要重建逻辑表结构,例如新建一张只包含有效列的表、导入数据、重建索引/约束/权限/依赖后切换,或者通过 dump/restore 类方式重建表。单独 VACUUM FULL 不够。

原理与证据

1. DROP COLUMN 本身只是把 pg_attribute 行标记为 dropped

源码 src/backend/catalog/heap.cRemoveAttributeById() 注释直接说明它是 ALTER TABLE DROP COLUMN 的核心:在 pg_attribute 中把属性标记为删除。

关键实现:

  • src/backend/catalog/heap.c:1675 注释说明这是 ALTER TABLE DROP COLUMN 的核心逻辑。
  • src/backend/catalog/heap.c:1712src/backend/catalog/heap.c:1723attisdropped 置为 true,并把 atttypid 置为 InvalidOid
  • src/backend/catalog/heap.c:1731src/backend/catalog/heap.c:1736 把列名改成内部 dropped 名字。
  • src/backend/catalog/heap.c:1756src/backend/catalog/heap.c:1759 是更新 pg_attribute 记录,而不是删除该记录。
  • src/backend/catalog/heap.c:1769 只移除该列的统计信息。

同一段函数没有更新 pg_class.relnatts。相反,新增列时 ALTER TABLE ADD COLUMN 使用 pg_class.relnatts + 1 分配新列号:

  • src/backend/commands/tablecmds.c:7456src/backend/commands/tablecmds.c:7462newattnum = relform->relnatts + 1,超过 MaxHeapAttributeNumber 就报错。
  • src/backend/commands/tablecmds.c:7486:新增列后才把 relform->relnatts 更新成新的列号。

因此 dropped column 的 attnum 不会被后续 ADD COLUMN 复用。

2. 文档明确说 dropped column 仍计入最大列数,并占用 null bitmap

官方文档 doc/src/sgml/limits.sgml:132doc/src/sgml/limits.sgml:136 明确写到:

  • dropped column 仍贡献到最大列数限制;
  • 新建 tuple 中 dropped column 的值会在 tuple null bitmap 中标记为 NULL;
  • null bitmap 本身也占空间。

最大表列数常量在源码中定义为:

  • src/include/access/htup_details.h:37src/include/access/htup_details.h:48MaxHeapAttributeNumber 是 1600。

tuple null bitmap 的长度计算在:

  • src/include/access/htup_details.h:581src/include/access/htup_details.h:588BITMAPLEN(NATTS) = (NATTS + 7) / 8

heap_form_tuple() 使用 tuple descriptor 的 natts 来构造 tuple:

  • src/backend/access/common/heaptuple.c:1018src/backend/access/common/heaptuple.c:1035:输入数组长度由 tupleDescriptor->natts 决定。
  • src/backend/access/common/heaptuple.c:1061src/backend/access/common/heaptuple.c:1063:只要存在 NULL,就按 numberOfAttributes 分配 null bitmap。
  • src/backend/access/common/heaptuple.c:1092:把 tuple header 的属性数设为 numberOfAttributes

由于 dropped column 仍在 tuple descriptor 的 natts 中,null bitmap 的位数也按包含 dropped column 的属性数计算。

3. VACUUM FULL 重写数据文件,但使用同一 tuple descriptor,把 dropped column 改成 NULL

VACUUM FULL 的文档说明它会把整张表重写到新的磁盘文件:

  • doc/src/sgml/ref/vacuum.sgml:86doc/src/sgml/ref/vacuum.sgml:89

源码路径也对应这一点:

  • src/backend/commands/vacuum.c:2297src/backend/commands/vacuum.c:2299VACUUM FULL 调用 cluster_rel(REPACK_COMMAND_VACUUMFULL, ...)
  • src/backend/commands/repack.c:1033src/backend/commands/repack.c:1041:创建 transient new heap,然后复制旧表数据。

重写 tuple 时,heap AM 的代码明确说要处理 dropped columns:

  • src/backend/access/heap/heapam_handler.c:2336src/backend/access/heap/heapam_handler.c:2343:重构 tuple 的原因之一是挤掉 dropped columns 的旧值,节省空间并避免 corner case。
  • src/backend/access/heap/heapam_handler.c:2397src/backend/access/heap/heapam_handler.c:2404reform_tuple() 会把 dropped columns 设置为 NULL;注释还说明这里假设新旧 relation 有相同的 tuple descriptor。
  • src/backend/access/heap/heapam_handler.c:2415src/backend/access/heap/heapam_handler.c:2432:检测 dropped column 是否非 NULL,如果需要重构,就把 dropped column 对应的 isnull[i] 设为 true,再 heap_form_tuple()

这说明 VACUUM FULL 的作用边界是:把旧 tuple 中 dropped column 的实际数据值清掉,重写成 NULL;不是修改表的逻辑列描述,也不是从 pg_attribute 删除 dropped column 行。

4. ALTER TABLE 文档也区分了“回收旧值空间”和“列仍然是 dropped”

doc/src/sgml/ref/alter_table.sgml:1758doc/src/sgml/ref/alter_table.sgml:1764 说明:

  • DROP COLUMN 不会物理移除列,只是让 SQL 操作不可见;
  • 后续 insert/update 会为该列存 NULL;
  • 已有空间不会立即回收,而会随着行更新逐步回收。

doc/src/sgml/ref/alter_table.sgml:1767doc/src/sgml/ref/alter_table.sgml:1771 进一步说明,执行会重写整表的操作可以立即回收 dropped column 占用的空间,其机制是重构每一行,把 dropped column 替换为 NULL。

这里说的是回收旧值占用的行内/TOAST 数据空间,不是删除 pg_attribute 中的 dropped column 元数据。

实践建议

可以用下面的 SQL 验证:

代码语言:javascript
复制
CREATE TABLE t_drop_vf (a int, b text, c int);
INSERTINTO t_drop_vf VALUES (1, repeat('x', 1000), 3);

ALTERTABLE t_drop_vf DROPCOLUMN b;
VACUUM FULL t_drop_vf;

SELECT attnum, attname, attisdropped, atttypid
FROM pg_attribute
WHERE attrelid = 't_drop_vf'::regclass
AND attnum > 0
ORDERBY attnum;

SELECT relnatts
FROM pg_class
WHEREoid = 't_drop_vf'::regclass;

预期现象:

  • 原来的 b 仍有一行 pg_attribute 记录;
  • 该行 attisdropped = trueatttypid = 0
  • relnatts 仍包含 dropped column;
  • 后续 ADD COLUMN 会继续使用新的 attnum,不会复用 dropped column 的 attnum

如果一张表经过大量 ADD COLUMN / DROP COLUMN 后接近 1600 列上限,VACUUM FULL 不能解决列槽位耗尽问题。应规划逻辑重建表:

代码语言:javascript
复制
CREATE TABLE t_new AS
SELECT live_col1, live_col2, live_col3
FROM t_old;

实际生产环境还需要补齐主键、唯一约束、外键、索引、默认值、生成列、权限、注释、触发器、分区、依赖对象、统计信息和应用切换步骤。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-05-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 digoal德哥 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 太长不看
  • 结论
  • 原理与证据
    • 1. DROP COLUMN 本身只是把 pg_attribute 行标记为 dropped
    • 2. 文档明确说 dropped column 仍计入最大列数,并占用 null bitmap
    • 3. VACUUM FULL 重写数据文件,但使用同一 tuple descriptor,把 dropped column 改成 NULL
    • 4. ALTER TABLE 文档也区分了“回收旧值空间”和“列仍然是 dropped”
  • 实践建议
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档