PostgreSQL 导入显式 ID 后,自动编号插入报 duplicate key
PostgreSQL 导入显式 ID 后,序列可能仍在发出表内已有编号。核对实际取号路径、max(id) 与序列状态,在满足停写和缓存条件时推进序列,并避免用事务回滚或重置编号造成再次冲突。
导入了一批带有 id 的记录后,应用新增数据时明明没有填写主键,却遇到这样的错误:
ERROR: duplicate key value violates unique constraint "orders_pkey"
DETAIL: Key (id)=(6) already exists.
如果主键默认值通过序列取号,显式导入的 ID 不会自动把序列推进到表内最大 ID。表里已经有编号 6,序列仍可能再次发出 6,最后由主键约束拦住这次插入。
这条报错本身不能证明序列落后。应用重复提交了显式 ID、默认值指向另一条序列、触发器改写了主键,也会造成冲突。先核对失败语句、表定义和序列状态,再决定是否调整序列。
下面的处理限定为一张非空表使用自己的递增序列:步长为 1、不循环、缓存为 1,且没有其他业务共用序列或预先领取编号。示例对象为 public.orders、主键列 id 和 public.orders_id_seq,执行前换成实际名称。所用行为按 PostgreSQL 18 核对;其他版本应核对对应版本文档。
找到真正生成主键的序列
先查看失败的 INSERT 及其绑定参数,确认 id 被省略或写成 DEFAULT。如果应用传入的就是 6,调整序列不会改变这次请求的值,应修正应用或导入流程。
在目标数据库查询列关联的序列:
SELECT pg_get_serial_sequence('public.orders', 'id');
这个函数既能找到 serial、bigserial 列关联的序列,也能找到 identity 列的内部序列。使用它返回的名称,不要根据表名猜测。
返回 NULL 时先停在这里,检查表定义、默认表达式和触发器。手工配置的 nextval(...) 默认值可能没有建立列与序列的所有权关联,不能把 NULL 理解成“完全没有取号逻辑”。即使返回了名称,也要确认失败语句实际走的就是这条取号路径。
比较最大 ID 与下一次可能发出的值
先通过现有维护流程暂停所有相关写入和取号任务,等正在执行的事务结束。仅暂停一个接口不够:后台任务、导入进程和直接调用序列的程序也要覆盖。随后由有权限的维护账号保存表定义、序列参数与以下查询结果,并确认有可用的变更前备份。
SELECT max(id) AS max_id
FROM public.orders;
SELECT schemaname, sequencename, increment_by,
min_value, max_value, cycle, cache_size
FROM pg_sequences
WHERE schemaname = 'public'
AND sequencename = 'orders_id_seq';
SELECT last_value, is_called
FROM public.orders_id_seq;
这些查询不调用 nextval,不会消耗新编号。不要用 count(*) 代替 max(id):200 行数据的最大 ID 可以是 10000,删除记录后两者更容易不同。
在步长为 1、缓存为 1、没有并发取号且未到序列上限的前提下:
| 序列状态 | 下一次取号 |
|---|---|
is_called = true |
last_value + 1 |
is_called = false |
last_value 本身 |
例如最大 ID 为 200,last_value 为 6 且 is_called 为真,下一次会尝试 7。如果 7 已存在,就会继续冲突。序列不知道哪些编号已经通过显式导入进入表中。
下一次取号已经大于表内最大 ID 时,不要把序列往回调。应继续检查实际失败参数、默认表达式、触发器,以及是否连错数据库或 schema。序列值比最大 ID 大很多也可能正常:失败或回滚的插入会留下空号。
如果 cache_size 大于 1,其他会话可能还握着预分配的旧编号,setval 不会立即使这些缓存失效。此时不要直接套用下文命令,应另行安排所有取号连接与缓存的处理;只把配置改成缓存 1,不能作为旧编号已经清空的证明。应用自己预分配 ID、多个表共用序列、负向步长或循环序列,也不在这里的修复范围内。
在停写窗口内把序列推进到最大 ID
确认最大 ID 非空,位于序列允许范围内,且“最大 ID 加 1”仍能被序列和主键列的数据类型容纳。空表的 max(id) 返回 NULL,不要机械改成 0;不同序列的最小值和起点不一定允许 0,空表初始化应按它自己的定义处理。
只有前面的检查确认序列落后、所有相关写入和取号仍然暂停,且没有未使用的预领编号时,才由具有该序列 UPDATE 权限的维护账号执行:
SELECT setval(
'public.orders_id_seq'::regclass,
(SELECT max(id) FROM public.orders),
true
);
第三个参数 true 表示这个值已经使用过。最大 ID 为 200、步长为 1 时,后续取号会从 201 开始。如果这里改成 false,下一次会再次返回 200,反而与表内已有记录冲突。
不要同时写成“最大 ID 加 1”又传 true,否则还会额外跳过一个编号。空号通常不影响唯一性。也不要为了完成维护,把序列调整权限长期授予普通应用账号。
停写是这段命令的前提:查询最大 ID 和设置序列之间若有其他会话取号,就可能把序列调回已经发出的编号。只锁业务表也未必覆盖直接调用序列的程序,因此不能把一次表锁当作所有取号者都已停止的证明。
验证后再恢复写入
保持停写,再读取一次状态:
SELECT max(id) AS max_id
FROM public.orders;
SELECT last_value, is_called
FROM public.orders_id_seq;
按上述方案处理后,last_value 应等于当时保存的最大 ID,is_called 应为真,序列参数应保持不变。随后先在允许写入的测试环境验证原插入流程;生产恢复时观察正常业务新增记录的返回 ID 与错误日志,确认主键仍由默认取号路径生成,重复主键错误不再出现。
不要把 SELECT nextval(...) 当成只读验收,它会实际取走一个编号。也不要要求恢复后的所有 ID 严格连续;事务回滚、插入失败等情况都可能造成空缺。如果错误仍在,记录完整错误和实际参数,重新核对取号路径,不要靠重复提交把序列“撞”到空闲位置。
setval 不能靠事务回滚撤销
序列状态变更会立即对其他事务可见,setval 不会因 ROLLBACK 撤销。不要用“先执行,错了就回滚”的方式试调,也不要把它包进事务后便认为有了回退保障。
如果操作后发现对象选错或数值不合适,继续保持相关取号者停止,结合变更前记录和操作期间实际发出的编号重新评估。已经恢复写入并发出新编号后,直接恢复旧的 last_value 可能再次制造重复值。通常需要确认新的安全上界再向前调整;涉及误操作的其他序列,也要独立核对其使用者。不要通过删掉冲突业务行、删除主键约束或将序列重置为 1 来掩盖问题。
后续导入应明确是否必须保留原 ID。不需要保留时,让默认值生成编号;必须保留时,把导入完成后的序列核对纳入停写流程。identity 也不能一概防止显式编号:GENERATED BY DEFAULT 允许显式值优先,GENERATED ALWAYS 通常会拒绝显式值,但可通过 OVERRIDING SYSTEM VALUE 明确覆盖。无论采用哪种定义,都需要保留主键或唯一约束来检查唯一性。


