PG这个设计极不合理: 经常加删列, 可导致无法再添加列
发现一个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 descriptor 的 natts 计算,而 dropped column 仍在 natts 里。
结论
对执行过 ALTER TABLE ... DROP COLUMN 的普通表再执行 VACUUM FULL 后,pg_attribute 里对应的 dropped column 记录仍然存在,通常表现为:
attisdropped = trueatttypid = 0attname被改成类似........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.c 的 RemoveAttributeById() 注释直接说明它是 ALTER TABLE DROP COLUMN 的核心:在 pg_attribute 中把属性标记为删除。
关键实现:
src/backend/catalog/heap.c:1675注释说明这是ALTER TABLE DROP COLUMN的核心逻辑。src/backend/catalog/heap.c:1712到src/backend/catalog/heap.c:1723把attisdropped置为true,并把atttypid置为InvalidOid。src/backend/catalog/heap.c:1731到src/backend/catalog/heap.c:1736把列名改成内部 dropped 名字。src/backend/catalog/heap.c:1756到src/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:7456到src/backend/commands/tablecmds.c:7462:newattnum = 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:132 到 doc/src/sgml/limits.sgml:136 明确写到:
dropped column 仍贡献到最大列数限制; 新建 tuple 中 dropped column 的值会在 tuple null bitmap 中标记为 NULL; null bitmap 本身也占空间。
最大表列数常量在源码中定义为:
src/include/access/htup_details.h:37到src/include/access/htup_details.h:48:MaxHeapAttributeNumber是 1600。
tuple null bitmap 的长度计算在:
src/include/access/htup_details.h:581到src/include/access/htup_details.h:588:BITMAPLEN(NATTS) = (NATTS + 7) / 8。
而 heap_form_tuple() 使用 tuple descriptor 的 natts 来构造 tuple:
src/backend/access/common/heaptuple.c:1018到src/backend/access/common/heaptuple.c:1035:输入数组长度由tupleDescriptor->natts决定。src/backend/access/common/heaptuple.c:1061到src/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:86到doc/src/sgml/ref/vacuum.sgml:89。
源码路径也对应这一点:
src/backend/commands/vacuum.c:2297到src/backend/commands/vacuum.c:2299:VACUUM FULL调用cluster_rel(REPACK_COMMAND_VACUUMFULL, ...)。src/backend/commands/repack.c:1033到src/backend/commands/repack.c:1041:创建 transient new heap,然后复制旧表数据。
重写 tuple 时,heap AM 的代码明确说要处理 dropped columns:
src/backend/access/heap/heapam_handler.c:2336到src/backend/access/heap/heapam_handler.c:2343:重构 tuple 的原因之一是挤掉 dropped columns 的旧值,节省空间并避免 corner case。src/backend/access/heap/heapam_handler.c:2397到src/backend/access/heap/heapam_handler.c:2404:reform_tuple()会把 dropped columns 设置为 NULL;注释还说明这里假设新旧 relation 有相同的 tuple descriptor。src/backend/access/heap/heapam_handler.c:2415到src/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:1758 到 doc/src/sgml/ref/alter_table.sgml:1764 说明:
DROP COLUMN不会物理移除列,只是让 SQL 操作不可见;后续 insert/update 会为该列存 NULL; 已有空间不会立即回收,而会随着行更新逐步回收。
doc/src/sgml/ref/alter_table.sgml:1767 到 doc/src/sgml/ref/alter_table.sgml:1771 进一步说明,执行会重写整表的操作可以立即回收 dropped column 占用的空间,其机制是重构每一行,把 dropped column 替换为 NULL。
这里说的是回收旧值占用的行内/TOAST 数据空间,不是删除 pg_attribute 中的 dropped column 元数据。
实践建议
可以用下面的 SQL 验证:
CREATETABLE 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 = true,atttypid = 0;relnatts仍包含 dropped column;后续 ADD COLUMN会继续使用新的attnum,不会复用 dropped column 的attnum。
如果一张表经过大量 ADD COLUMN / DROP COLUMN 后接近 1600 列上限,VACUUM FULL 不能解决列槽位耗尽问题。应规划逻辑重建表:
CREATETABLE t_new AS
SELECT live_col1, live_col2, live_col3
FROM t_old;
实际生产环境还需要补齐主键、唯一约束、外键、索引、默认值、生成列、权限、注释、触发器、分区、依赖对象、统计信息和应用切换步骤。