发现一个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 槽位仍然保留所以:
natts。VACUUM FULL 能回收 dropped column 旧值占用的数据空间,但不会压缩 attnum,不会删除 pg_attribute 中的 dropped column 目录记录,也不会把这些列槽位还给后续 ADD COLUMN 使用。如果目标是“彻底消除 dropped column 的目录槽位和 1600 列计数影响”,需要重建逻辑表结构,例如新建一张只包含有效列的表、导入数据、重建索引/约束/权限/依赖后切换,或者通过 dump/restore 类方式重建表。单独 VACUUM FULL 不够。
源码 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 复用。
官方文档 doc/src/sgml/limits.sgml:132 到 doc/src/sgml/limits.sgml:136 明确写到:
最大表列数常量在源码中定义为:
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 的属性数计算。
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 行。
doc/src/sgml/ref/alter_table.sgml:1758 到 doc/src/sgml/ref/alter_table.sgml:1764 说明:
DROP COLUMN 不会物理移除列,只是让 SQL 操作不可见;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 验证:
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 = true,atttypid = 0;relnatts 仍包含 dropped column;ADD COLUMN 会继续使用新的 attnum,不会复用 dropped column 的 attnum。如果一张表经过大量 ADD COLUMN / DROP COLUMN 后接近 1600 列上限,VACUUM FULL 不能解决列槽位耗尽问题。应规划逻辑重建表:
CREATE TABLE t_new AS
SELECT live_col1, live_col2, live_col3
FROM t_old;
实际生产环境还需要补齐主键、唯一约束、外键、索引、默认值、生成列、权限、注释、触发器、分区、依赖对象、统计信息和应用切换步骤。