记录一次排查PostgreSQL违反唯一性限制的过程

- Bug的现象:
当时,主要的目标是通过Docker
搭建容器pg-conf
,该容器的CMD
主要运行了一个python脚本pg-conf-start.py
,同时设置了若容器启动失败则自动重启的选项,该python
脚本的主要功能是创建数据库db1
,然后通过执行提前写好的table_create.sql
脚本创建表sys_user
,最后,在上述基础上执行table_insert.sql
脚本向表sys_user
插入几行预定义的数据,pg-conf-start.py
、table_create.sql
和table_insert.sql
脚本的片段,如下所示。
1 | # just part of code, can't execute directly. |
1 | -- table_create.sql |
1 | -- table_insert.sql |
Bug
的现象是容器pg-conf
能够启动,但是使用docker logs
查看日志会发现日志中存在报错,且容器重启了几次,报错记录如下所示。报错提示违反了唯一性原则,出现了重复的key,但是每次重启重复的key都是不一样的。
1 | 2025-09-05 14:15:55,792 - INFO - 数据库 wag 已存在,跳过创建 |
由于换到了新的产线,我刚接触过PostgreSQL
数据库和Docker
不久,也没有使用过psycopg2
,python
和MySQL
的知识基本也忘的一干二净了。当时遇到这个问题,主要有3个疑问:首先,为什么会报错?其次,为什么每次报错的Id
都不相同?最后,为什么pg-conf
容器最后又能够正常运行了,数据库的内容也正确?
遇事不决问DeepSeek,我就把sql
脚本和报错的现象丢给了DeepSeek,它给的回答大概是“可能之前插入了数据或者遗留重复的数据,需要使用select * from sys_user;
查看是否存在数据…,然后给我一些不会报错的解决方案”等等,具体情况记不清了。但是,我就是想整明白上面的问题。
我通过使用select * from sys_user;
查看在执行table_insert.sql
前确实是没有数据的,但是就是不明白我什么会报错,当时认为是由于执行id=1
的时候报的错,因此就非常矛盾。
在此基础上,我当时想的是如果在插入前能够清空表的内容并重置主键的值是不是就不会报这个问题了,然后问了DeepSeek什么命令能够实现上面的任务,得到如下答案:
1 | -- 清空表并重置主键的值 |
同时,我又问了DeepSeek如何查看当前主键的值是什么,然后它给了我一堆答案,提出出的有效信息是SELECT nextval('sys_user_id_seq');
可以获取下一个主键的值,但是会消耗一个主键的值;SELECT last_value FROM sys_user_id_seq;
可以获取当前主键的值。后来才明白,创建sys_usr表的sql脚本中CONSTRAINT sys_user_pkey PRIMARY KEY (id)
会值自动创建一个名为sys_user_pkey
的表记录当前的主键的。我通过执行上述命令,发现清空表并重置自增主键的值后,当前主键的值为1,此时为认为这个值的含义为下一次插入记录是使用的主键的值。然后我执行如下sql
命令后,在此查询当前主键的值,发现还是为1。此时就很奇怪,为什么还是1呢?应该变为2才对。
1 | INSERT INTO public.sys_user (username, "password", roles, description, login_type, "type", status) |
后来询问DeepSeek明白了:SELECT last_value FROM sys_user_id_seq;
打印的是上次使用的值,但是初始化是就是1,同时还有一个标识位is_called表示是否调用了nextval
,当自增主键被重置时,is_called=f
,此时插入新数据会nextval
仍然会返回1,但此时会将is_called
置为t
,下次调用就会返回2。查看当前自增主键的值和is_called
的值,可以通过如下命令:
1 | SELECT last_value, is_called FROM sys_user_id_seq; |
得到上述信息之后,我便开始重新测试,将table_insert.sql
进一步缩小,如下所示,看看执行完成后主键的值的变化,同时观察是否会报错。
1 | -- table_insert.sql |
此时,我发现执行过程并没有报错,同时主键的值也并没有增加,同时is_called
仍然为f
,因此,可以手动指定主键的值,但是自增主键的值并不会变化。此时把id
为1,2,3的三条都加上也没有报错。直到执行下面的sql语句时报错了。
1 | INSERT INTO public.sys_user (username, "password", roles, description, login_type, "type", status, last_change_pwd_time,pwd_exp) |
此时,基本已经明白了,由于该语句没有指定id
的值,因此会使用nextval
获取下一个自增主键的值,此时获取的值为1,但是id
为1的记录已经包含了,因此就会报如下错误:
1 | psql:data.1.sql:6: ERROR: duplicate key value violates unique constraint "sys_role_pkey" |
为什么报错的原因找到了?但是为什么Docker容器自动重启了几次python脚本每次报错的id
均不同,并且最后可以正常运行了呢?
通过对python脚本分析发现,在使用psycopg2扩展库的时候,可以指定是否自动提交,由脚本代码看,当不指定自动提交时,只有手动commit
才能提交事务,一旦cur.execute(sql)
出错,那么该事务就会被回滚,因此当执行插入username
为__internal
的记录时出错,之前插入的id
为1~3
的记录都会被回滚,但是询问DeepSeek可知,自增主键每次执行nextval
是不会回滚的,主要是为了提高执行效率、避免死锁等原因。因此,每次Docker容器重新执行python脚本时,自增主键就会加1,所以才会造成每次报错的id
是不同的,当自增主键增加至3时,再次执行python脚本,就会执行成功,因此最后该python脚本的执行是成功的!
挺神奇的!哈哈,最初看到这个报错的时候,感觉挺匪夷所思的,但是,通过不断地排查Bug
,最终这个报错发现确实非常符合逻辑。
- Title: 记录一次排查PostgreSQL违反唯一性限制的过程
- Author: DaiLu
- Created at : 2025-09-06 21:28:13
- Updated at : 2025-09-10 21:59:51
- Link: https://dailu-blogspot.com/2025/09/06/记录一次排查PostgreSQL违反唯一性限制的过程/
- License: This work is licensed under CC BY-NC-SA 4.0.