PostgreSQL --- 自增主键【序列】的避坑指南

🌟什么是自增主键

自增主键是数据库表设计中非常经典且常用的一种主键策略。简单来说,它就是一个能自动递增的字段,用来作为表中每一行数据的唯一身份标识(主键)

当在表中插入一条新数据时,不需要手动去指定这个主键的值,数据库系统会自动生成一个唯一的、且比上一条记录更大的数值(比如 1, 2, 3...)。

🌟 为什么要使用自增主键?(核心优势)

  1. 保证数据的唯一性

    自增主键能确保每条记录都有一个独一无二的"身份证号",彻底避免了因为手动分配主键可能导致的重复或冲突问题,保证了数据的完整性。

  2. 极高的写入与查询性能

    这是自增主键最大的优势。因为 ID 是按顺序递增的,新插入的数据总是排在现有数据的末尾。数据库在存储时(尤其是使用 B+ 树索引时),不需要频繁地移动或重排已有的数据页,极大地减少了磁盘碎片,从而让数据的插入和查询效率都非常高。

  3. 简化开发逻辑

    对于程序员来说,使用自增主键非常省心。在写代码插入数据时,完全不需要去操心"这个 ID 该填多少",直接交给数据库自动生成即可,大大减少了业务代码的复杂度。

🛠️ 不同数据库是如何实现的?

PostgreSQL、MySQL 和 SQL Server 在实现自增 ID 的底层原理上有着显著的区别。简单来说,它们分别代表了**"独立对象模式"** 、"表属性计数器模式" 和**"元数据强绑定模式"**。

以下是这三种数据库自增 ID 底层原理的深度解析:

🐘 1.PostgreSQL:独立的序列(Sequence)对象

在 PostgreSQL 中,自增 ID 并不是直接依附于表的,而是依赖于一个独立的数据库对象------序列(Sequence)

  • 底层实现 :当创建一个 SERIALIDENTITY 类型的自增列时,PostgreSQL 会在后台自动创建一个名为 表名_字段名_seq 的独立序列对象。该字段的默认值被设置为 nextval('序列名')
  • 核心特性
    • 独立性:序列独立于表存在,甚至可以跨表共享同一个序列。
    • 事务非原子性(不回滚):序列值的分配不跟随事务回滚。如果一个事务获取了 ID=100 但随后回滚了,这个 100 就会被"浪费"掉,下一个事务会直接获取 101。这种设计是为了在高并发环境下,避免为了等待事务提交而加锁,从而极大地提升了并发插入的性能。
🐬2. MySQL:表级别的自增计数器

在 MySQL(以主流的 InnoDB 存储引擎为例)中,自增 ID 是作为表的一个属性来维护的。

  • 底层实现 :InnoDB 在内存中为每个带有自增主键的表维护一个自增计数器(Auto-increment Counter) 。当需要插入新数据时,MySQL 会根据配置的锁模式(innodb_autoinc_lock_mode)获取锁,读取当前计数器的值作为新 ID,然后将计数器加 。
  • 核心特性
    • 依附于表:自增逻辑完全绑定在具体的表上,无法像 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

sql 复制代码
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 字段生成一个唯一的、自增的序列值,通常用来作为表的主键。

可以把它拆解为三个部分来详细理解:

1. nextval 函数

nextval(即 "next value" 的缩写)是数据库(如 PostgreSQL、openGauss 等)中用于操作"序列(Sequence)"的核心函数。

  • 作用:每次调用它时,它都会让指定的序列对象递增(比如从 1 变成 2),并返回这个新的数值。
  • 特性:它能保证在多用户并发访问时,每个进程都能安全地获取到一个不重复的唯一值。

2. 'users1_id_seq' 序列名

这是 nextval 函数要操作的具体目标,即一个名为 users1_id_seq序列对象1。

  • 序列是一个独立的数据库对象,专门用来按既定规则(如每次加1)生成数字。
  • 当在 id 字段设置这个默认值后,每次向 t_user_01 表插入一条新数据且没有手动指定 id 时,数据库就会自动从 users1_id_seq 这个序列里"拿"下一个数字填进去。

3. ::regclass 类型转换

这是 PostgreSQL 等数据库特有的语法,表示将前面的字符串 'users1_id_seq' 强制转换为 regclass 数据类型

  • 为什么要转换? nextval 函数的参数要求是 regclass 类型。regclass 是数据库内部用来存储对象(如表、序列)OID(对象标识符)的一种特殊数据类型。
  • 转换的好处 :通过 ::regclass 转换,数据库会在编译或准备阶段就锁定这个序列的真实身份(即"早期绑定")。这意味着,即使以后给这个序列改了名或者移动了模式(schema),只要 OID 没变,这个默认值表达式依然能准确找到它,避免了运行时查找可能出现的错误。

💡 补充说明:

这种写法(int4 DEFAULT nextval('...'::regclass))是早期 PostgreSQL 版本中定义自增主键的常见方式。

在现代的 PostgreSQL 开发中,为了书写简便,通常会直接使用 SERIALBIGSERIAL 类型,或者使用标准的 GENERATED BY DEFAULT AS IDENTITY 语法,它们在底层其实也是自动创建了序列并绑定了类似的 nextval 逻辑。

方法2

sql 复制代码
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(4字节整数)。
  • GENERATED BY DEFAULT AS IDENTITY:这是整段语句的精髓,也是 PostgreSQL 10 及以上版本推荐的自增主键写法(符合 SQL 标准)。
    • GENERATED ... AS IDENTITY:表示这个字段的值由数据库自动生成,底层会自动创建一个独立的序列(Sequence)对象来管理数字的递增2。
    • BY DEFAULT :这是它的行为模式。意思是"默认情况下自动生成,但也允许手动指定 "。
      • 如果插入数据时不写 id,数据库会自动从序列中拿下一个数字填进去。
      • 如果在插入数据时强行指定了 id(例如 INSERT INTO t_user_023 (id, user_name) VALUES (100, '张三')),数据库也会接受,而不会报错。
  • PRIMARY KEY:将 id 字段设为主键。这意味着 id 的值必须是唯一 的,且不能为空。数据库会自动为该字段创建一个唯一索引,以加快查询速度。

💡 深度对比:BY DEFAULTALWAYS

在定义自增列时,除了 BY DEFAULT,还有一个常用的模式是 GENERATED ALWAYS AS IDENTITY。两者的区别决定了平时插数据时的"自由度":

表格

模式 语法 行为特点 适用场景
BY DEFAULT GENERATED BY DEFAULT AS IDENTITY 较灵活。平时自动生成,但也允许手动插入指定的 ID3。 适合需要偶尔手动迁移历史数据、或指定特定 ID 的业务场景。
ALWAYS GENERATED ALWAYS AS IDENTITY 较严格 。永远由数据库自动生成。如果尝试手动插入 ID,数据库会直接报错拒绝(除非使用特殊的 OVERRIDING SYSTEM VALUE 语法)1。 适合绝大多数常规业务,能最大程度保证主键的纯净,防止人为插错数据。

总结来说 ,提供的这段建表语句,创建了一个带有**"灵活自增主键"**的用户表。它既享受了数据库自动分配唯一 ID 的便利,又保留了在特殊情况下(如数据迁移)手动控制 ID 的权利。

🐘 PostgreSQL 怎操作序列值?

在 PostgreSQL 中,查看序列的"当前值"其实有两种不同的需求:一种是查看下一个待生成的值 ,另一种是查看目前表中实际已分配的最大值

这里有 4 种常用的方法,可以根据实际场景选择:

1. 查看下一个待生成的值(最常用)

如果想知道下一次调用 nextval() 会产生什么 ID,可以直接查询序列对象:

sql 复制代码
-- 直接查看序列的当前状态,last_value 字段即为当前已分配的值
SELECT * FROM users1_id_seq;

注:last_value 加上步长(increment_by)通常就是下一个生成的值1。

2. 查看当前会话最后一次获取的值

如果在当前会话(Session)中刚刚调用过 nextval('users1_id_seq'),可以用这个函数查看:

sql 复制代码
SELECT currval('users1_id_seq');

⚠️ 注意:currval 有严格的限制。它只能查看当前会话最近一次获取的值。如果刚连上数据库还没用过这个序列,执行这条会直接报错。

3. 查看表中实际已分配的最大 ID(排查冲突最稳妥)

最靠谱查看方式是直接去表里查最大的 ID 是多少。这能判断序列是否落后于实际数据:

sql 复制代码
SELECT MAX(id) FROM t_user_01;

如果这个值比序列的 last_value 还要大,就说明序列确实滞后了,需要执行之前提到的 setval 语句来同步。

4. 动态获取序列的最新已分配值(生产级通用写法)

如果不确定序列的确切名字,或者想要一个更健壮、能直接反映"最新已分配值"的查询,可以使用 PostgreSQL 的内置函数组合:

sql 复制代码
-- 自动根据表和字段名找到关联的序列,并获取其最新已分配值

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:基于数据库日志解析,能非常准确地返回序列的真实进度3。

💡 总结建议:

  • 日常开发想看下一个 ID 是多少,用 方法 1 (SELECT * FROM 序列名)。
  • 排查主键冲突或做数据迁移校验时,用 方法 3 (SELECT MAX(id)) 结合 方法 4 最为准确可

5.修改自增主键的起始值

在 PostgreSQL 中,修改自增主键的起始值,本质上就是修改它背后绑定的序列。这里有三种最常用且实用的方法:

方法一:直接指定一个固定的起始值

如果下一个插入 ID 从一个特定的数字(例如 1000)开始,可以使用 ALTER SEQUENCE 命令

首先,需要知道 id 字段绑定的序列名(通常默认为 表名_字段名_seqt_user_03_id_seq)。然后执行:

sql 复制代码
-- 将序列的下一个值重置为 1000
ALTER SEQUENCE t_user_03_id_seq RESTART WITH 1000;

执行后,下一次插入数据时,id 就会从 1000 开始1。

方法二:同步到当前表的最大 ID(最推荐)

如果因为手动插入了一些数据,或者删除了部分数据,想要让自增 ID 接上当前表里实际最大的 ID,避免主键冲突,这是最稳妥的方法。可以结合 setval 函数和子查询,直接把序列的值拨到当前表的最大 ID 处:

sql 复制代码
-- 将序列的当前值设置为表中最大的 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 开始,下次自增就是 18。
  • setval 设置后,序列的下一个值会自动变成 最大值 + 12。
方法三:全自动获取序列名并修改(最安全)

如果不确定序列的具体名称,可以使用 PostgreSQL 自带的 pg_get_serial_sequence 函数来自动获取。这在生产环境中非常实用,能有效避免因序列名写错导致的报错。这条语句完全不需要输入序列名,直接复制粘贴表名即可使用。

sql 复制代码
-- 自动获取 t_user_02 表 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)**对象来控制的。

根据新建表 还是修改现有表,有以下几种设置步进的方法:

1. 新建表时设置步进

如果正在创建一张新表,可以通过以下两种方式自定义步进:

  • 方式一:使用标准的 IDENTITY 语法(推荐)

    在定义字段时,直接在**GENERATED ... AS IDENTITY**的括号内指定 INCREMENT BY :这样,id 字段每次自增的步长就是 10。

    sql 复制代码
    CREATE TABLE orders (
        id INT GENERATED BY DEFAULT AS IDENTITY (INCREMENT BY 10) PRIMARY KEY,
        order_name VARCHAR(100)
    );
  • 方式二:手动创建序列并绑定

    先创建一个自定义步进的序列,再将其绑定到表字段上:

    sql 复制代码
    -- 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)
    );

2. 修改现有表的步进

如果表已经存在,需要先找到它绑定的序列,然后通过 ALTER SEQUENCE 命令来修改步进。

  • 步骤一:查找序列名

    如果不记得序列的名字,可以使用系统函数自动获取:

    sql 复制代码
    -- 将 '表名', '字段名' 替换为的实际名称
    SELECT pg_get_serial_sequence('表名', 'id');
  • 步骤二:修改步进

    获取到序列名(例如 表名_id_seq)后,执行以下命令修改步进(例如改为每次增加 10):

    sql 复制代码
    ALTER SEQUENCE 表名_id_seq INCREMENT BY 10;

    注意:修改后,新的步进规则会从下一次调用 nextval(即下一次插入数据)时开始生效。

3. 进阶:设置负数步进(递减序列)

PostgreSQL 的序列同样支持负数步进,实现主键的自动递减:

sql 复制代码
-- 创建一个从 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 这个序列的当前值。

**序列(Sequence)**对此一无所知,依然按自己的节奏往下走。当它走到 100 并准备生成下一个 ID 时,就会撞上刚刚手动插入的 ID 100,从而抛出"主键冲突(duplicate key value violates unique constraint)"的错误。

🛠️ 如何解决?(重置序列)

解决这个问题的核心思路是:手动把序列的当前值,同步到表中最大 ID 的下一个位置

在数据库中执行以下 SQL 语句来修复:

sql 复制代码
-- 将序列 users1_id_seq 的下一个值重置为表中当前最大 id + 1
SELECT setval('users1_id_seq', 
                (SELECT COALESCE(MAX(id), 0) FROM t_user_01) + 1);

语句原理解析:

  1. (SELECT MAX(id) FROM t_user_01):先查出表里目前最大的那个 ID (假设是 100)。
  2. 序列+1:让序列从 101 开始,这样下次插入就不会冲突了。
  3. COALESCE(..., 0):这是一个防御性写法。万一你的表是空的(MAX(id) 为 NULL),它会默认返回 0,加 1 后序列从 1 开始,避免报错。
  4. setval('序列名', 值):把序列的当前值强行设置为这个计算出来的新值。

执行完这条语句后,再尝试正常插入数据(不指定 ID),nextval() 就会生成一个全新的、不冲突的 ID 了。

🛡️ 以后如何避免?

  1. 数据迁移/批量导入后必做 :如果使用 COPY 命令或者第三方工具批量导入了带 ID 的历史数据,导入完成后,务必 顺手执行一次上面的 setval 语句来同步序列。
  2. 尽量少手动指定 ID :在正常的业务代码中,插入数据时不要给 id 字段传值,完全交给数据库自动生成。
  3. 建立检查机制 :如果是生产环境,可以定期写个简单的脚本,检查核心表的**MAX(id)** 和序列的**last_value** 是否一致,及时发现并修复。

🛠️ 怎样让postgresql自动更新自增id

PostgreSQL 的自增 ID(序列)默认情况下不会 自动同步表中手动插入的数据,这正是之前遇到主键冲突的原因。不过,可以通过以"自动化"的方式来避免手动执行 setval 命令:

💡. 使用触发器(Trigger)实现真正的"自动更新"

如果希望每次插入数据时,数据库都能自动检查并修正 ID,可以创建一个触发器。这个触发器会在每次插入前,自动把序列的值更新为当前表中最大 ID 的下一个值。

sql 复制代码
-- 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) 查询,在数据量极大的高并发场景下可能会轻微影响写入性能。

相关推荐
段ヤシ.1 小时前
【Java框架】知识点汇总Day7:Spring Boot +Vue(持续更新)
vue.js·spring boot·后端·框架
土狗TuGou1 小时前
SQL进阶笔记 · 第1篇:存储引擎
java·数据库·笔记·后端·sql·mysql
科技互联.1 小时前
2026轻量化图形引擎白皮书:PG官网发布渠道与分布式PG数据库架构解析
数据库·分布式·数据库架构
码语智行1 小时前
Spring Security自定义AuthenticationManager实现手机号/密码双认证
java·后端·spring
爱喝水的鱼丶1 小时前
SAP-ABAP:SAP 简单报表输出开发系列(共6篇)第二篇:SAP 报表数据筛选优化:选择屏幕自定义与查询效率提升
开发语言·数据库·学习·性能优化·sap·abap
武子康1 小时前
Build-Your-Own-X 从零构建轻量级事件驱动微框架:嵌入式与物联网场景下的极简实践
人工智能·后端·物联网·ai·c#·大模型·嵌入式
肖爱Kun1 小时前
GB28181启动传参的设计
linux·服务器·数据库
空圆小生1 小时前
Vue3 + Spring Boot 全栈实战:从零搭建在线彩票模拟系统
java·spring boot·后端
iNeuOS工业互联网1 小时前
iNeuOS_AiInsight·数智灵鉴(Text2SQL/NL2SQL自然语言大模型智能问数),免费下载试用
大数据·数据库·人工智能·智能制造·工业互联网·ineuos