PostgreSQL SET DEFAULT 后加 NOT NULL,仍报 contains null values
给已有字段设置默认值不会填充旧NULL。检查历史空值和应用输入,评估业务补值、分批回填与USING重写的影响。
给已有整数字段设置默认值 0,再加非空约束,迁移却报:
ERROR: column "c" contains null values
失败语句可能把两项改动写在一起:
ALTER TABLE public.t
ALTER COLUMN c SET DEFAULT 0,
ALTER COLUMN c SET NOT NULL;
SET DEFAULT 改的是之后请求默认值时使用的值,不会填充表中已有的 NULL。普通 SET NOT NULL 仍要检查已有数据,所以合并成一条 ALTER 也不会替你修复旧行。
以下按 PostgreSQL 18 的普通表和现有 integer 列说明;示例表、列名应替换为实际对象。新增列时带默认值的行为与这里修改已有列不同,不能直接套用。
先确认旧空值和现有默认值
在报错所用的数据库检查列定义:
SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 't'
AND column_name = 'c';
SELECT count(*) AS existing_nulls
FROM public.t
WHERE c IS NULL;
计数查询只读取数据,但大表仍可能产生扫描负载,应按数据库运行情况安排。列定义查询没有结果时,核对连接、schema、大小写和查看权限;不要改成操作另一个同名表。
如果前面的组合 ALTER 失败,默认值设置也可能随该语句回滚,不能只看迁移文件里写了 SET DEFAULT 就认定数据库已保存它。以实际列定义为准,并先结束失败的迁移事务,再由原迁移流程决定后续步骤。
默认值也不会替换新请求显式传入的 NULL
修复旧行前,需要检查应用是否仍在把这一列绑定成空值。省略列、使用 SQL DEFAULT 与明确写 NULL 是不同请求:默认值不会自动替换第三种情况。
如果旧空值清理后又出现新空值,先排查普通接口、导入和后台任务的最终 SQL。只改字段默认值,无法让所有显式空值输入变成 0;业务要求不能为空时,应修正输入,或在经过规划的迁移阶段让约束保护新写入。
涉及自动编号列的输入问题,可以另看 identity 列显式 NULL 不触发默认编号。不要为了普通业务字段的迁移,去改自增列定义。
给旧行补什么值,需要先确定业务含义
0 只有在业务认可时才能作为旧 NULL 的替代值。缺失数量、未知状态和真实零值可能有不同含义,不能因为默认值写成 0,就把所有历史空值都解释为零。
确认补值规则后,可以评估分批更新旧行,再添加非空约束。方案需覆盖相关新写入,在每批事务中核对受影响行,保留旧值和可恢复备份。单次查不到 NULL 也不能证明之后不会再有并发请求写入空值。
若确实需要一条 DDL 完成补值,且已经确认现有列是 integer、0 是批准的业务替代值,可以在隔离副本评估以下写法:
ALTER TABLE public.t
ALTER COLUMN c TYPE integer USING COALESCE(c, 0),
ALTER COLUMN c SET DEFAULT 0,
ALTER COLUMN c SET NOT NULL;
类型仍是 integer,但 USING 对每行计算结果,把 NULL 变为 0。这实际改了业务数据,并不是只改 schema。已有默认值、索引、约束和依赖对象仍需检查;其他数据类型不能直接照抄。
这种会改变列内容的转换会重写表,涉及锁、磁盘空间和索引处理。对于持续运行的大表,应评估分批回填和约束验证的迁移方式,而不是根据语句数量判断快慢。只有 DDL 权限也不意味着已经获得修改这些业务值的授权。
验收旧数据和新增入口
迁移成功后,重新核对列默认值、非空属性和空值数量,并抽查实际补值的记录。应用使用省略该列或 DEFAULT 的路径,应得到预期默认值;显式空值应按非空要求被拒绝,不能被应用吞掉异常后返回成功。
回退前明确是否需要撤销补值。删除非空约束、恢复旧默认值不会把已经写成 0 的记录变回 NULL;恢复原始含义需要保留的主键和旧值。事务提交后,也不能靠再执行一次 ROLLBACK 恢复这次迁移。


