PostgreSQL自增主键序列避坑指南
自增主键是常见主键策略,保证唯一性与高性能。PostgreSQL通过独立序列对象实现,支持跨表共享,事务不回滚导致空洞。MySQL和SQLServer各有特点。PostgreSQL中可使用SERIAL或IDENTITY语法创建自增主键,并通过nextval等函数操作序列值。
在数据库表设计中,自增主键可以说是最经典、最常用的主键策略之一。简单来说,它就是一个能自动递增的字段,专门用来给表中的每一行数据分配一个唯一的“身份证”。

当你往表里插入一条新数据时,完全不需要手动指定这个主键的值,数据库系统会自动生成一个唯一的、且比上一条记录更大的数值(比如 1, 2, 3...)。
? 为什么要使用自增主键?
1. 保证数据的唯一性
自增主键确保每条记录都有一个独一无二的数字标识,彻底避免了手动分配主键时可能出现的重复或冲突问题,数据的完整性也因此得到了保障。
2. 极高的写入与查询性能
这一点算是自增主键最大的亮点。因为 ID 是按顺序递增的,新插入的数据总是排在现有数据的最末尾。在数据库进行存储时(尤其是使用 B+ 树索引的场景),不需要频繁地移动或重排已有的数据页,磁盘碎片大大减少,数据的插入和查询效率自然就上去了。
3. 简化开发逻辑
对于开发者来说,自增主键是件很省心的事。写代码插入数据时,完全不用纠结“这个 ID 该填多少”,直接交给数据库自动生成就行,业务代码的复杂度也就随之下降了。
?️ 不同数据库是怎么实现的?
有意思的是,PostgreSQL、MySQL 和 SQL Server 在实现自增 ID 的底层原理上,差别还挺大的。简单来说,它们分别代表了 “独立对象模式”、“表属性计数器模式” 和 “元数据强绑定模式”。
下面我们来深入看看这三者的底层逻辑。
? 1.PostgreSQL:独立的序列(Sequence)对象
在 PostgreSQL 中,自增 ID 并不是直接依附于表的,它依赖于一个独立的数据库对象——序列(Sequence)。
底层实现:
当你创建一个 SERIAL 或 IDENTITY 类型的自增列时,PostgreSQL 会在后台自动创建一个名为 表名_字段名_seq 的独立序列对象。这个字段的默认值被设置为 nextval('序列名')。
核心特性:
- 独立性:序列独立于表存在,甚至可以跨表共享同一个序列。
- 事务非原子性(不回滚):序列值的分配跟事务回滚没关系。如果一个事务获取了 ID=100 但随后回滚了,这个 100 就会被“浪费”掉,下一个事务会直接获取 101。这种设计是为了在高并发环境下,避免等待事务提交而加锁,从而极大地提升并发插入的性能。
?2. MySQL:表级别的自增计数器
在 MySQL(以主流的 InnoDB 存储引擎为例)中,自增 ID 是作为表的一个属性来维护的。
底层实现:
InnoDB 在内存中为每个带有自增主键的表维护一个自增计数器(Auto-increment Counter)。当需要插入新数据时,MySQL 会根据配置的锁模式(innodb_autoinc_lock_mode)获取锁,读取当前计数器的值作为新 ID,然后将计数器加 1。
核心特性:
- 依附于表:自增逻辑完全绑定在具体的表上,无法像 PostgreSQL 的序列那样跨表共享。
- 预分配与空洞:为了保证高并发插入的性能,MySQL 在批量插入(如 INSERT ... SELECT)时会预分配一段 ID 区间。如果批量插入中途失败或事务回滚,这些预分配的 ID 就会被直接丢弃,导致 ID 出现较大的跳跃。
- 重启恢复机制的进化:MySQL 5.7 及更早版本重启后需要扫描全表最大值来恢复计数器;而 MySQL 8.0 及更高版本将计数器的变更记录在 Redo Log 中,重启时直接从日志恢复,不再需要扫描全表。
?️ 3.SQL Server:基于元数据的 IDENTITY 属性
SQL Server 的 IDENTITY 属性在底层设计上有着自己独特的“强绑定”哲学。
底层实现:
IDENTITY 属于列定义的元数据(metadata-level attribute)。自增值由存储引擎在插入时原子生成并写入页结构,其序列状态(如 last_value 等)持久化在系统目录 sys.identity_columns 中。从 SQL Server 2012 开始,其后台实际上也使用了 sequence object(序列对象)进行封装,但它与列定义的绑定非常紧密。
核心特性:
- 物理存储耦合性:由于与表的物理存储和元数据强绑定,SQL Server 不支持通过简单的 ALTER COLUMN 语句直接为一个已有的普通列添加 IDENTITY 属性。如果需要这样做,通常必须重建表(创建新表带 IDENTITY -> 迁移数据 -> 删除旧表)。
- 事务日志不可逆性:若允许随意修改 IDENTITY,需要回滚已插入但未提交的 identity 值,而当前的日志格式不记录 identity 生成上下文,导致无法安全回退。
- 同样存在空洞:与 MySQL 和 PG 一样,由于删除记录、事务回滚或服务器重启等原因,IDENTITY 值也会出现不连续的情况。
⚔️ 4.三大数据库自增 ID 核心差异总结
| 特性维度 | PostgreSQL (序列对象) | MySQL (InnoDB 计数器) | SQL Server (IDENTITY) |
|---|---|---|---|
| 底层载体 | 独立的数据库对象 (Sequence) | 表属性 (内存计数器 + 磁盘元数据) | 列元数据 + 系统目录 (后台封装序列) |
| 跨表共享 | 支持 (多个表可共用一个序列) | 不支持 (完全依赖表) | 不支持 (与列强绑定) |
| 事务回滚 | ID 不回滚,会产生空洞 | ID 不回滚,预分配也会导致空洞 | ID 不回滚,会产生空洞 |
| 修改灵活性 | 极高 (可独立操作序列对象) | 较高 (可通过 ALTER TABLE 修改) | 极低 (不支持直接 ALTER 添加 IDENTITY) |
| 重启恢复 | 序列值持久化,重启后继续递增 | 8.0+ 从日志恢复;5.7 需扫描表最大值 | 从系统元数据中恢复 |
总结一下:
PostgreSQL 的自增 ID 最灵活且独立;MySQL 的自增 ID 与表深度绑定,通过预分配机制换取高并发性能;而 SQL Server 的 IDENTITY 则最为严格和封闭,将其作为表物理结构的一部分,保证了极高的数据一致性,但牺牲了修改的灵活性。
最后补充一个值得注意的小特点:ID 不连续(空洞)。自增主键并不保证绝对连续。如果删除了某条数据,或者插入数据的事务发生了回滚,已经分配出去的 ID 通常不会被回收,从而产生“空洞”。它非常适合绝大多数业务场景,但在某些特殊需求下(例如需要跨数据库合并数据,或者主键本身需要有特定的业务含义时),可能就需要考虑 UUID 或其他形式的主键了。
? PostgreSQL 建表时如何创建自增主键
方法1
CREATE TABLE public.t_user_01 (
id int4 DEFAULT nextval('users1_id_seq'::regclass) NOT NULL,
user_name varchar(100) NOT NULL,
-- 其他字段...
CONSTRAINT users1_pkey PRIMARY KEY (id)
);
在这段建表语句中,nextval('users1_id_seq'::regclass) 是一个数据库函数调用,它的核心作用是为 id 字段生成一个唯一的、自增的序列值,通常用来作为表的主键。
我们可以把它拆解成三个部分来理解:
- nextval 函数:nextval(即 "next value" 的缩写)是操作“序列(Sequence)”的核心函数。每次调用它时,它都会让指定的序列对象递增(比如从 1 变成 2),并返回这个新的数值。它能保证在多用户并发访问时,每个进程都能安全地获取到一个不重复的唯一值。
- 'users1_id_seq' 序列名:这是 nextval 函数要操作的具体目标,即一个名为 users1_id_seq 的序列对象。序列是一个独立的数据库对象,专门用来按既定规则(如每次加1)生成数字。当在 id 字段设置这个默认值后,每次向 t_user_01 表插入一条新数据且没有手动指定 id 时,数据库就会自动从 users1_id_seq 这个序列里“拿”下一个数字填进去。
- ::regclass 类型转换:这是 PostgreSQL 等数据库特有的语法,表示将前面的字符串 'users1_id_seq' 强制转换为 regclass 数据类型。为什么要转换?因为 nextval 函数的参数要求是 regclass 类型。通过 ::regclass 转换,数据库会在编译或准备阶段就锁定这个序列的真实身份(即“早期绑定”)。这意味着,即使以后给这个序列改了名或者移动了模式(schema),只要 OID 没变,这个默认值表达式依然能准确找到它,避免了运行时查找可能出现的错误。
补充一句:这种写法是早期 PostgreSQL 版本中定义自增主键的常见方式。在现代开发中,通常直接使用 SERIAL 或 BIGSERIAL 类型,或者使用标准的 GENERATED BY DEFAULT AS IDENTITY 语法,它们在底层其实也是自动创建了序列并绑定了类似的 nextval 逻辑。
方法2
CREATE TABLE t_user_023 (
id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
user_name varchar(100) NOT NULL
-- 其他字段...
);
-- 或者
CREATE TABLE public.t_user_03 (
id int4 GENERATED BY DEFAULT AS IDENTITY( INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START 1 CACHE 1 NO CYCLE) NOT NULL,
user_name varchar(100) NOT NULL,
-- 其他字段...
CONSTRAINT t_user_023_pkey PRIMARY KEY (id)
);
核心字段定义:
id INT:定义了一个名为 id 的字段,数据类型为 INT。GENERATED BY DEFAULT AS IDENTITY:这是整段语句的精髓,也是 PostgreSQL 10 及以上版本推荐的自增主键写法(符合 SQL 标准)。底层会自动创建一个独立的序列(Sequence)对象来管理数字的递增。- BY DEFAULT:这是它的行为模式,意思是“默认情况下自动生成,但也允许手动指定”。如果插入数据时不写 id,数据库会自动从序列中拿下一个数字填进去;如果在插入数据时强行指定了 id(例如
INSERT INTO t_user_023 (id, user_name) VALUES (100, '张三')),数据库也会接受,而不会报错。
- BY DEFAULT:这是它的行为模式,意思是“默认情况下自动生成,但也允许手动指定”。如果插入数据时不写 id,数据库会自动从序列中拿下一个数字填进去;如果在插入数据时强行指定了 id(例如
PRIMARY KEY:将 id 字段设为主键,意味着 id 的值必须是唯一的,且不能为空。数据库会自动为该字段创建一个唯一索引。
深度对比:BY DEFAULT 与 ALWAYS
在定义自增列时,除了 BY DEFAULT,还有一个常用的模式是 GENERATED ALWAYS AS IDENTITY。两者的区别决定了平时插数据时的“自由度”:
| 模式 | 语法 | 行为特点 | 适用场景 |
|---|---|---|---|
| BY DEFAULT | GENERATED BY DEFAULT AS IDENTITY | 较灵活。平时自动生成,但也允许手动插入指定的 ID。 | 适合需要偶尔手动迁移历史数据、或指定特定 ID |
| ALWAYS | GENERATED ALWAYS AS IDENTITY | 较严格。永远由数据库自动生成。如果尝试手动插入 ID,数据库会直接报错拒绝(除非使用特殊的 OVERRIDING SYSTEM VALUE 语法)。 | 适合绝大多数常规业务,能最大程度保证主键的纯净,防止人为插错数据。 |
总的来说,这段建表语句创建了一个带有 “灵活自增主键” 的用户表。它既享受了数据库自动分配唯一 ID 的便利,又保留了在特殊情况下(如数据迁移)手动控制 ID 的权利。
? PostgreSQL 怎么操作序列值?
在 PostgreSQL 中,查看序列的“当前值”其实有两种不同的需求:一种是查看下一个待生成的值,另一种是查看目前表中实际已分配的最大值。
这里有 4 种常用的方法,可以根据实际场景选择:
- 查看下一个待生成的值(最常用)
如果想知道下一次调用 nextval() 会产生什么 ID,可以直接查询序列对象:
-- 将序列的下一个值重置为 1000 ALTER SEQUENCE t_user_03_id_seq RESTART WITH 1000;
注:last_value 加上步长(increment_by)通常就是下一个生成的值。
- 查看当前会话最后一次获取的值
如果在当前会话(Session)中刚刚调用过nextval('users1_id_seq'),可以用这个函数查看:
SELECT currval('users1_id_seq');
⚠️ 注意:currval 有严格的限制。它只能查看当前会话中最近一次获取的值。如果刚连上数据库还没用过这个序列,执行这条会直接报错。
- 查看表中实际已分配的最大 ID(排查冲突最稳妥)
最靠谱的方式是直接去表里查最大的 ID 是多少,这能判断序列是否落后于实际数据:
SELECT MAX(id) FROM t_user_01;
如果这个值比序列的 last_value 还要大,就说明序列确实滞后了,需要执行之前提到的 setval 语句来同步。
- 动态获取序列的最新已分配值(生产级通用写法)
如果不确定序列的确切名字,或者想要一个更健壮、能直接反映“最新已分配值”的查询,可以使用 PostgreSQL 的内置函数组合:
-- 自动根据表和字段名找到关联的序列,并获取其最新已分配值
SELECT pg_sequence_last_value(pg_get_serial_sequence('t_user_01', 'id')::regclass);
pg_get_serial_sequence('表名', '字段名'):能自动查出 t_user_01 表的 id 字段绑定的是哪个序列(防止序列名记错)。pg_sequence_last_value:基于数据库日志解析,能非常准确地返回序列的真实进度。
总结建议:
- 日常开发想看下一个 ID 是多少,用方法 1 (
SELECT * FROM 序列名)。 - 排查主键冲突或做数据迁移校验时,用方法 3 (
SELECT MAX(id)) 结合方法 4 最准确可靠。
5. 修改自增主键的起始值
在 PostgreSQL 中,修改自增主键的起始值,本质上就是修改它背后绑定的序列。这里有三种最常用且实用的方法:
方法一:直接指定一个固定的起始值
如果下一个插入 ID 从一个特定的数字(例如 1000)开始,可以使用 ALTER SEQUENCE 命令。首先,需要知道 id 字段绑定的序列名(通常默认为 表名_字段名_seq,即 t_user_03_id_seq)。然后执行:
-- 将序列的下一个值重置为 1000 ALTER SEQUENCE t_user_03_id_seq RESTART WITH 1000;
执行后,下一次插入数据时,id 就会从 1000 开始。
方法二:同步到当前表的最大 ID(最推荐)
如果因为手动插入了一些数据,或者删除了部分数据,想要让自增 ID 接上当前表里实际最大的 ID,避免主键冲突,这是最稳妥的方法。可以结合 setval 函数和子查询,直接把序列的值拨到当前表的最大 ID 处:
-- 将序列的当前值设置为表中最大的 id
SELECT setval('t_user_03_id_seq', (SELECT COALESCE(MAX(id), 0) FROM t_user_023));
MAX(id)会查出当前表里最大的 ID。COALESCE(..., 0)是为了防止表是空的(MAX 结果为 NULL)导致报错,空表时默认从 0 开始,下次自增就是 1。setval设置后,序列的下一个值会自动变成最大值 + 1。
方法三:全自动获取序列名并修改(最安全)
如果不确定序列的具体名称,可以使用 PostgreSQL 自带的 pg_get_serial_sequence 函数来自动获取。这在生产环境中非常实用,能有效避免因序列名写错导致的报错。这条语句完全不需要输入序列名,直接复制粘贴表名即可使用。
-- 自动获取 t_user_03 表 id 字段绑定的序列,并将其重置为当前最大 ID
SELECT setval(
pg_get_serial_sequence('t_user_03', 'id'),
(SELECT COALESCE(MAX(id), 0) FROM t_user_03)
);
? PostgreSQL 自增主键的步进设置
在 PostgreSQL 中,自增主键的步进(即每次增加的数值,默认为 1)是通过底层的序列(Sequence)对象来控制的。根据是新建表还是修改现有表,有以下几种设置步进的方法:
- 新建表时设置步进
如果正在创建一张新表,可以通过以下两种方式自定义步进:
方式一:使用标准的 IDENTITY 语法(推荐)
在定义字段时,直接在 GENERATED ... AS IDENTITY 的括号内指定 INCREMENT BY。比如下面这个例子,id 字段每次自增的步长就是 10:
CREATE TABLE orders (
id INT GENERATED BY DEFAULT AS IDENTITY (INCREMENT BY 10) PRIMARY KEY,
order_name VARCHAR(100)
);
方式二:手动创建序列并绑定
先创建一个自定义步进的序列,再将其绑定到表字段上:
-- 1. 创建一个步进为 5 的序列
CREATE SEQUENCE custom_step_seq INCREMENT BY 5;
-- 2. 建表并绑定该序列
CREATE TABLE products (
id INT PRIMARY KEY DEFAULT nextval('custom_step_seq'),
product_name VARCHAR(100)
);
- 修改现有表的步进
如果表已经存在,需要先找到它绑定的序列,然后通过 ALTER SEQUENCE 命令来修改步进。
步骤一:查找序列名
如果不记得序列的名字,可以使用系统函数自动获取:
-- 将 '表名', '字段名' 替换为实际的名称
SELECT pg_get_serial_sequence('表名', 'id');
步骤二:修改步进
获取到序列名(例如 表名_id_seq)后,执行以下命令修改步进(例如改为每次增加 10):
ALTER SEQUENCE 表名_id_seq INCREMENT BY 10;
注意:修改后,新的步进规则会从下一次调用 nextval(即下一次插入数据)时开始生效。
- 进阶:设置负数步进(递减序列)
PostgreSQL 的序列同样支持负数步进,实现主键的自动递减:
-- 创建一个从 1000 开始,每次减少 5 的序列 CREATE SEQUENCE countdown_seq START WITH 1000 INCREMENT BY -5 MINVALUE 1; -- 建议设置最小值,防止无限递减
性能优化与注意事项
- 避免索引热点:在高并发写入的场景下,如果步进为 1,新插入的数据会集中在 B+ 树索引的同一页(页尾),容易引发“索引热点”竞争。将步进设置为较大的数值(如 10、100),可以让新生成的 ID 在索引中分布得更分散,从而减少页竞争,提升写入性能。
- 业务逻辑风险:设置非 1 的步进(尤其是大跨度步进)会导致主键 ID 出现大量“空洞”(不连续)。务必确保业务逻辑没有依赖 ID 连续性的判断(例如“偶数 ID 代表测试数据”等),以免造成业务漏洞。
? PostgreSQL 中一个非常经典的“坑”
问题现象:使用自增ID插入一条数据时,与已经存在的使用指定ID插入的数据,出现了主键冲突。
? 为什么会出现冲突?
根本原因在于:PostgreSQL 的序列(Sequence)和表里的实际数据是相互独立的。当手动插入一条指定 ID 的数据(例如 INSERT INTO t_user_01 (id, ...) VALUES (100, ...))时,数据库只会乖乖地把这条数据存入表中,但它不会自动去更新 users1_id_seq 这个序列的当前值。序列对此一无所知,依然按自己的节奏往下走。当它走到 100 并准备生成下一个 ID 时,就会撞上刚刚手动插入的 ID 100,从而抛出“主键冲突”的错误。
?️ 如何解决?(重置序列)
解决这个问题的核心思路是:手动把序列的当前值,同步到表中最大 ID 的下一个位置。可以执行以下 SQL 语句来修复:
-- 将序列 users1_id_seq 的下一个值重置为表中当前最大 id + 1
SELECT setval('users1_id_seq',
(SELECT COALESCE(MAX(id), 0) FROM t_user_01) + 1
);
语句原理解析:
(SELECT MAX(id) FROM t_user_01):先查出表里目前最大的 ID(假设是 100)。+1:让序列从 101 开始,这样下次插入就不会冲突了。COALESCE(..., 0):这是一个防御性写法。万一表是空的(MAX(id) 为 NULL),它会默认返回 0,加 1 后序列从 1 开始,避免报错。setval('序列名', 值):把序列的当前值强行设置为这个计算出来的新值。
执行完这条语句后,再尝试正常插入数据(不指定 ID),nextval() 就会生成一个全新的、不冲突的 ID 了。
?️ 以后如何避免?
- 数据迁移/批量导入后必做:如果使用 COPY 命令或者第三方工具批量导入了带 ID 的历史数据,导入完成后务必顺手执行一次上面的 setval 语句来同步序列。
- 尽量少手动指定 ID:在正常的业务代码中,插入数据时不要给 id 字段传值,完全交给数据库自动生成。
- 建立检查机制:如果是生产环境,可以定期写个简单的脚本,检查核心表的
MAX(id)和序列的last_value是否一致,及时发现并修复。
?️ 怎样让 PostgreSQL 自动更新自增 ID
PostgreSQL 的自增 ID(序列)默认情况下不会自动同步表中手动插入的数据,这正是之前遇到主键冲突的原因。不过,可以通过“自动化”的方式来避免手动执行 setval 命令:
使用触发器(Trigger)实现真正的“自动更新”
如果希望每次插入数据时,数据库都能自动检查并修正 ID,可以创建一个触发器。这个触发器会在每次插入前,自动把序列的值更新为当前表中最大 ID 的下一个值。
-- 1. 创建一个自动同步序列的函数
CREATE OR REPLACE FUNCTION sync_id_sequence()
RETURNS TRIGGER AS $$
BEGIN
-- 将序列 users1_id_seq 的下一个值设为当前表最大ID + 1
PERFORM setval('users1_id_seq', (SELECT COALESCE(MAX(id), 0) FROM t_user_01) + 1);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- 2. 为 t_user_01 表创建触发器,在每次插入前执行该函数
CREATE TRIGGER before_insert_sync_seq
BEFORE INSERT ON t_user_01
FOR EACH ROW
EXECUTE FUNCTION sync_id_sequence();
提示:这种方法虽然实现了全自动,但因为每次插入都要额外执行一次 MAX(id) 查询,在数据量极大的高并发场景下可能会轻微影响写入性能。
游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。
同类文章
Redis是什么:核心特性、架构与应用场景解析
Redis是一款基于内存的键值型NoSQL数据库,以超高读写速度和丰富的数据结构著称。本文系统梳理Redis的核心特性、架构组成、性能优势及典型应用场景,并通过与Memcached、MySQL、MongoDB的对比,帮助开发者快速判断Redis是否适合当前业务需求。
Windows 安装 MongoDB 完整图文教程
本文详细介绍在 Windows 系统上安装 MongoDB 的完整流程。从官网下载 MSI 安装包开始,逐步演示自定义安装路径、配置 Windows 服务、跳过 MongoDB Compass 等关键选项,并提供通过系统服务列表验证安装是否成功的方法,帮助开发者快速搭建本地 MongoDB 环境。
Linux 安装 MongoDB 完整指南:依赖配置、环境变量与服务启动
本文详解在 Linux 系统下安装 MongoDB 的完整流程,涵盖依赖包安装、二进制包下载解压、环境变量配置、数据与日志目录创建及服务启动验证。通过标准化命令与路径说明,帮助开发者快速完成部署并确认服务状态。
MacOS安装MongoDB完整教程
本文介绍在MacOS系统下安装MongoDB的完整流程,涵盖下载、解压、目录配置、环境变量设置及服务启动。通过明确的命令与参数说明,帮助开发者快速完成环境搭建并验证安装结果。
Ubuntu系统安装与配置Redis完整指南
本文详解在Ubuntu系统中安装Redis的两种主流方式:apt在线安装与源码编译安装。涵盖版本选择逻辑、服务启停与状态检查、连接验证方法,以及在线练习工具与桌面GUI客户端的对比与使用建议,帮助开发者快速搭建并验证Redis运行环境。
- 热门数据榜
1
2
3
4
5
6
7
8
9
10
相关攻略
2026-09-01 06:20
2026-09-01 06:20
2026-09-01 06:20
2026-09-01 06:19
2026-09-01 06:19
2026-09-01 06:19
2026-09-01 06:18
2026-09-01 06:18
热门教程
- 游戏攻略
- 安卓教程
- 苹果教程
- 电脑教程

