文章目录
环境
系统平台:Linux x86-64 Red Hat Enterprise Linux 7
版本:4.5.10
文档用途
本文介绍瀚高数据库中序列使用,值不连续、重复写入等现象及处理办法。
详细信息
1、序列(Sequence)介绍
序列主要用来生成单调递增的整数值,依赖pg_sequence系统表和WAL日志,保证即使在数据库崩溃恢复后依然不会产生重复值。
创建序列
sql
-- 创建序列
CREATE SEQUENCE seq_1
INCREMENT BY 1 -- 步长
MINVALUE 1 -- 最小值
MAXVALUE 999999999 -- 最大值(INT2、INT4、INT8)
START WITH 1 -- 起始值
CACHE 1 -- 缓存大小,默认 1
NO CYCLE; -- 达到最大值后报错,不回绕
序列函数:
- nextval(seq): 序列的下一个值
- currval(seq): 序列当前值(最后一次nextval的值),若当前会话未对该序列执行过nextval,则报错
- setval(seq, n): 设置序列的值
- lastval(): 不区分序列,当前会话最后一次nextval的值,如果当前会话未执行过nextval,则报错
特性:
序列值不会随事务回滚而回滚,一旦调用nextval(),值就分配且写入WAL,即使事务回滚也不会将值"还回去",因此这也是导致值不连续的一种情况。
序列与表自增列
数据库可以使用序列实现自增列,自增列可以作为自增主键使用。
sql
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT
);
SERIAL伪类型是把"创建序列+绑定DEFAULT nextval"两步合并了,不使用SERIAL创建自增列如下操作:
sql
CREATE SEQUENCE users_id_seq;
CREATE TABLE users (
id INT NOT NULL DEFAULT nextval('users_id_seq'),
name TEXT
);
| 子类型 | 底层类型 | 对应序列 |
|---|---|---|
| SMALLSERIAL | SMALLINT (2B) | 上限 32767 |
| SERIAL | INTEGER (4B) | 上限 2147483647 |
| BIGSERIAL | BIGINT (8B) | 上限 9223372036854775807 |
SERIAL伪类型只能在CREATE TABLE时指定,已有字段改为自增时,必须使用DEFAULT nextval指定。
清空表时重置序列
sql
TRUNCATE tablename RESTART IDENTITY;
TRUNCATE时添加RESTART IDENTITY会将序列值重置。
2、值不连续现象
现象:
id 分布: 1, 2, 5, 6, 7, 10 → 缺少 3, 4, 8, 9
数据库序列保证顺序但不保证连续,原因:
1、事务回滚,事务中调用了nextval()后,执行了ROLLBACK
2、某行被删除,该行的ID不会复用
3、数据库崩溃或重启,CACHE中未使用的值丢失
4、ON CONFLICT DO NOTHING,nextval()调用后由于冲突未插入
建议:
业务层面需容忍或规避这种不连续现象(例如不依赖ID连续性做业务逻辑),而非依赖数据库序列保证连续。
3、序列值重复
序列回绕导致重复
sql
CREATE SEQUENCE bad_seq
MAXVALUE 5
CYCLE; -- 达到 5 后回绕到 1
SELECT nextval('bad_seq'); -- 1
SELECT nextval('bad_seq'); -- 2
...
SELECT nextval('bad_seq'); -- 5
SELECT nextval('bad_seq'); -- 1 ← 重复!
如果序列显式设置了CYCLE,当达到MAXVALUE后便会回绕到MINVALUE,从而导致值重复。若序列对应的数据类型达到上限且序列为NO CYCLE,则会直接报错,而非回绕。
解决:
增大序列的MAXVALUE值,并同步修改数据类型,SMALLINT改为INTEGER,INTEGER改为BIGINT,如果达到BIGINT最大值,需从业务层面解决。
手动插入序列值
手动插入一个大于当前序列值的ID后,序列本身并不会自动更新,后续如果继续使用DEFAULT nextval('seq') 或SERIAL自动生成ID,序列仍会从其上次缓存的值开始递增,很可能产生一个已存在的手动插入ID,从而导致序列值重复。
sql
-- 手动插入ID
INSERT INTO users (id, name) VALUES (10, 'legacy');
-- 序列当前值仍为 5,下次自动插入
INSERT INTO users (name) VALUES ('new'); -- id=6,然后 id=10 时冲突
-- 手动插入后,应该同步序列值
SELECT setval('users_id_seq', (SELECT max(id) FROM users));
数据迁移
分步迁移时,使用pg_dump单独导出表结构和数据,然后再单独导入,这种场景下,最容易遗漏序列同步。序列的当前值存储在pg_sequence系统表中,并不随表数据一同迁移,如果只迁移了表结构和数据,而未重置序列的起始值,序列会从其默认初始值重新开始递增,但此时表中已存在从源库迁移过来的数据(例如ID已经到了10),序列生成的新值很快就会与已有值重复。迁移完成后,必须将序列的当前值设置为表中已有数据的最大值(或更大),确保后续 nextval() 不会生成已存在的值。
sql
-- pg_dump 默认包含此命令,分步操作时遗漏
SELECT pg_catalog.setval('users_id_seq', 1000, true);
pg_dump默认会在导出文件末尾包含SELECT pg_catalog.setval(...) 语句来同步序列值,完整导入时不会出问题。但如果采用分步操作先导结构,再导数据,或只迁移部分表,就容易遗漏这些setval语句。建议迁移后统一执行一次序列校验。
4、已有数据的表,增加自增列
表已经存在且已有数据时,无法通过ALTER TABLE ... ADD COLUMN直接添加自增列,需要创建序列,回填字段数据,将列默认值绑定到nextval()等步骤实现自增列添加。
方法一:ALTER TABLE + 序列
适用场景:表结构简单、只需在表末尾追加一个自增列、希望尽量少改动现有表结构时使用。
1.创建序列,添加列
sql
CREATE SEQUENCE users_id_seq;
ALTER TABLE users ADD COLUMN id INT;
此时,id列数据为NULL。
2.为已有数据回填序列值
sql
UPDATE users SET id = nextval('users_id_seq');
UPDATE需要全表扫描并持有行锁,数据量大时建议分批执行,避免长时间锁表。
3.列默认值绑定到nextval()
sql
-- 设置NOT NULL约束
ALTER TABLE users ALTER COLUMN id SET NOT NULL;
-- 将列默认值绑定为序列,新插入的数据自动取值
ALTER TABLE users ALTER COLUMN id SET DEFAULT nextval('users_id_seq');
-- 添加主键
ALTER TABLE users ADD PRIMARY KEY (id);
必须先完成回填再设置NOT NULL,否则会报错:ERROR: column "id" contains null values;设置DEFAULT后,新插入的行才会自动从序列取值;添加主键会自动创建唯一索引,若表中存在重复数据,需先清理重复数据。
4.同步序列值,避免后续nextval生成已回填过的值
sql
SELECT setval('users_id_seq', (SELECT max(id) FROM users));
方法二:重建表
适用场景:自增列必须位于首列,需要调整字段顺序,需要同时修改字段类型/默认值/约束,或表结构较复杂(存在多个索引、外键、CHECK 约束)时,重建表比ALTER TABLE更可控。
1.创建新表
sql
-- 自增列放在首列,其余字段与旧表保持一致
CREATE TABLE users_new (
id SERIAL PRIMARY KEY,
name TEXT
);
需要调整的字段类型、默认值、约束在这一步一并设计好,避免二次重建;索引、外键可在数据导入后再创建,避免导入期间维护索引的开销。
2.拷贝数据
只拷贝业务字段,id 由新表序列按导入顺序生成,无需手工回填。如需 id 按某字段顺序分配(例如按name排序),可在SELECT中加ORDER BY。
sql
INSERT INTO users_new (name) SELECT name FROM users;
3.同步序列到已有数据的最大值
连续导入时序列通常已自动走到最后一行,但若导入过程中存在删除或回滚,仍需执行一次同步,保证下次 nextval() 从 max(id)+1 开始。
sql
SELECT setval('users_new_id_seq', (SELECT max(id) FROM users_new));
4.数据对比并替换表名
sql
-- 校验数据完整性(行数对比,应相等)
SELECT (SELECT count(*) FROM users) AS old_cnt,
(SELECT count(*) FROM users_new) AS new_cnt;
-- 表名替换
ALTER TABLE users RENAME TO users_bak;
ALTER TABLE users_new RENAME TO users;
替换前先对比行数,确认数据完整后再重命名。第2步拷贝数据期间若有业务持续写入旧表,新表会缺失增量数据,需在应用维护窗口执行,或替换前对增量数据做合并处理。
注意
大表执行UPDATE回填或重建表会锁表,建议在业务低峰期执行,并提前评估表数据量。
方法二替换表名后,需确认新表运行正常再删除备份表:DROP TABLE users_bak;,原表上的索引、外键约束需在新表上重新创建。