自增主键别只会 auto_increment,先把值从哪来讲清楚

MySQL 里建自增主键,常见写法就是 bigint primary key auto_increment。建表时声明好,插入时不传 id,数据库自己给下一个值。很多业务表一开始都是这么写的。

到 KingbaseES 之后,第一反应通常还是先试 auto_increment。这里没有绕开它,直接在当前环境里跑了一遍。结果是能建表,也能自动生成 1、2。不过这只能说明当前版本能识别这个兼容写法,不能把所有自增场景都压到 auto_increment 上。

先看当前连接。库是 app_db,用户是 app_user,版本是 KingbaseES V009R001C010。刚连进去时,current_schema() 返回 publicsearch_path"$user", public。后面的对象统一放到 app_schema,所以会话里先执行:

vbnet 复制代码
set search_path to app_schema, public;

自增实验会创建表,也会创建序列。先把 schema 固定住,后面看 \d 输出时更清楚,不会一会儿在 public,一会儿在 app_schema

auto_increment 在当前环境里能跑

先试最熟悉的 MySQL 写法:

sql 复制代码
create table app_schema.t_mysql_auto_increment(
  id bigint primary key auto_increment,
  name varchar(50)
);

建表成功,插入时不写 id

sql 复制代码
insert into app_schema.t_mysql_auto_increment(name)
values ('mysql-style-a'), ('mysql-style-b')
returning id, name;

返回结果是 12

这个结果先放在前面:当前 V009R001C010 环境下,auto_increment 可以完成自增效果。简单表从 MySQL 迁过来时,这种写法不一定马上报错。

但如果到这里就停,后面还是会卡。表能建,只是第一步。后面还要看表结构里到底多了什么,默认值怎么写,序列对象是否能单独操作。否则换成迁移工具生成的 DDL、换成历史数据导入、换成手动修复主键时,还是不知道该查哪里。

所以 auto_increment 这里先当作兼容写法记下来:能用,但还要继续看 KingbaseES 自己常见的写法。

这里也不建议马上把所有表都改成 auto_increment。如果项目已经有一大批 MySQL DDL,先验证兼容写法可以省时间;如果是新建 KingbaseES 表,还是要看看后面几种写法。尤其是脚本需要多人维护时,字段类型、默认值、序列名这些信息越明确,后面排查越省事。

迁移场景里还有一个实际问题:DDL 不一定全靠人手写。ORM、数据库迁移工具、旧系统导出的建表语句,都可能生成不一样的自增写法。只验证 create table 能不能执行还不够,最好再插入几条数据,看返回的主键值,再用 \d 看列默认值。这样后面发现主键值不对时,至少知道该从表结构还是序列对象查起。

serial 是最短的自增写法

接着用 serial 建一张表:

sql 复制代码
create table app_schema.t_serial_demo(
  id serial primary key,
  name varchar(50) not null
);

插入三行数据,不写 id

sql 复制代码
insert into app_schema.t_serial_demo(name)
values ('serial-a'), ('serial-b'), ('serial-c')
returning id, name;

结果返回 1、2、3

从插入结果看,serial 和前面的 auto_increment 没什么区别,都是不传 id 也能返回递增值。差别在表结构里。继续看 \d 输出,id 的类型是 integer,默认值是:

php 复制代码
nextval('t_serial_demo_id_seq'::regclass)

这条默认值比插入结果更重要。插入结果只能说明数字变大了,默认值能说明数字从哪里来。t_serial_demo_id_seq 是数据库创建出来的序列对象,id 列每次需要默认值时都会调用它。

这个细节在导入数据时会用上。比如先导入一批带 id 的旧数据,表里最大 id 已经到了 5000,但序列还停在很小的值,下一次插入就可能撞主键。处理这类问题时,方向不是改 id 列类型,而是把对应序列调到正确位置。

所以看 serial 时,不要只记住"它能自增"。更该记住的是它生成了一个 integer 字段,并且把字段默认值指到了一个序列。这个判断来自表结构,不是猜的。

这里已经不是单纯的"列属性"了。serial 建表后,列类型落成了 integer,默认值落成了 nextval(...)。插入数据时没有传 id,数据库就执行这个默认值,从对应的序列里取下一个数。

以后遇到自增值不对,不能只盯着表字段看。表结构里的 nextval('t_serial_demo_id_seq'::regclass) 已经把线索给出来了,序列名就在这里。

serial 对应 integer。如果原来的 MySQL 表用的是 bigint auto_increment,下一步就该看 bigserial

bigserial 更接近 bigint auto_increment

bigserial 的写法和 serial 一样短:

sql 复制代码
create table app_schema.t_bigserial_demo(
  id bigserial primary key,
  name varchar(50) not null
);

插入两行后,返回 1、2。继续看结构,idbigint,默认值是:

php 复制代码
nextval('t_bigserial_demo_id_seq'::regclass)

bigserialserial 的差别主要在类型:一个是 bigint,一个是 integer。两者的默认值都走 nextval(...)。如果原表是 bigint auto_incrementbigserial 更贴近原来的字段范围。

它的好处是短,缺点也是短。序列名由数据库按规则生成,起始值、步长、缓存这些设置都没有在建表语句里展开。实验里生成的序列名是 t_bigserial_demo_id_seq,小表够用;建表脚本如果要统一命名和参数,就得把 sequence 单独写出来。

这里先记住一条就够:bigserial 不是把自增逻辑藏没了,只是替你把 sequence 创建好了。

实际写表时,bigserial 的可读性还算不错,至少一眼能看出这是一个 bigint 自增主键。问题出在后续管理:序列对象什么时候创建、名字叫什么、缓存是多少,建表语句本身没有全部展开。小项目不一定在意这些,迁移脚本和初始化脚本会更在意。

如果只是日常开发建表,bigserial 已经够直白。需要换成显式 sequence 的,通常是那些要进版本库、要反复执行、要在多套环境里跑的脚本。脚本一旦进了发布流程,少写几行 SQL 的收益就没那么大了,能看懂、能回滚、能定位对象更重要。

显式 sequence 把自增拆开了

显式写 sequence 会比 bigserial 多几行,但可读性更强。先创建序列:

sql 复制代码
create sequence app_schema.t_seq_demo_id_seq
  start with 100
  increment by 1
  minvalue 100
  no maxvalue
  cache 1;

再建表,让 id 默认调用这个序列:

sql 复制代码
create table app_schema.t_seq_demo(
  id bigint primary key default nextval('app_schema.t_seq_demo_id_seq'),
  name varchar(50) not null
);

插入三行数据,返回 100、101、102currval 查到的当前序列值也是 102,继续插入一行,得到 103

这时自增主键被拆成两段:

sql 复制代码
sequence:负责产生数字
table column default:负责在插入时调用 sequence

序列叫什么、从多少开始、每次加多少,都在 DDL 里。比 bigserial 啰嗦,但排查时少猜一步。

这种写法在导入历史数据时更好处理。比如原表已经有数据,下一次插入不能再从 1 开始,就需要把序列调到当前最大主键之后。显式 sequence 至少把操作对象摆在明面上。

示例里 nextval 写了完整的 app_schema.t_seq_demo_id_seq。多 schema 环境里,完整名字比裸序列名更省心。

显式 sequence 还有一个好处:看 SQL 时能知道主键从哪里开始。示例里故意从 100 开始,就是为了让结果和前面的 1、2、3 区分开。插入后返回 100、101、102,说明起始值确实生效;再插一条得到 103,说明表字段默认值和序列已经接上。

nextval 会推进序列

sequence 不是只有表插入时才会动。手动调用 nextval,它也会往前走:

csharp 复制代码
select nextval('app_schema.t_seq_demo_id_seq') as next_id;

前面已经插到了 103,手动调用后得到 104。此时再查 currval,当前值也是 104。继续插入一条表数据,返回 105

这个输出能说明一个常见现象:自增主键不保证连续。序列被取过一次,下一个值就往后走一次。手动取号会这样,回滚、缓存、批量导入也可能让数字中间空出来。

因此,自增 id 适合当内部主键,不适合拿来当必须连续的业务编号。订单号、流水号、票据号要单独设计。

看到 103 后面变成 105,不用先怀疑数据丢了。先查有没有调用过 nextval,再看事务和导入过程。

这也是自增主键和业务编号要分开的原因。内部主键只要唯一、稳定、方便关联就够了;业务编号经常还要带日期、地区、渠道、流水位数,甚至还要满足人工对账习惯。把这些要求压到自增 id 上,后面会很别扭。

在 MySQL 里也会遇到类似情况,只是平时不一定去看底层生成器。到了 KingbaseES,sequence 是明面上的对象,正好把这件事讲清楚:数字能增长,不代表它必须连续。

identity 是另一套更标准的写法

除了 sequence 和 serial/bigserial,KingbaseES 还支持 identity 列。先看 generated by default as identity

sql 复制代码
create table app_schema.t_identity_default_demo(
  id bigint generated by default as identity primary key,
  name varchar(50) not null
);

不写 id 插入两行,返回 1、2。再手动插入 id = 1000,也能成功。表结构里,id 的默认规则显示为 generated by default as identity

by default 的表现比较宽松:不传 id 时数据库生成,传了 id 也接受。导入历史数据时,这种行为会方便一些。

这类写法看起来不像 nextval(...) 那么直白,但表结构已经把规则写出来了:generated by default as identity。如果只关心建表语句紧凑,identity 比显式 sequence 清爽;如果要单独管理生成器,显式 sequence 更直接。

再看 generated always as identity

sql 复制代码
create table app_schema.t_identity_always_demo(
  id bigint generated always as identity primary key,
  name varchar(50) not null
);

不写 id 插入两行,同样返回 1、2。但手动插入 id = 2000 时会报错:

sql 复制代码
ERROR: cannot insert into column "id"
DETAIL: Column "id" is an identity column defined as GENERATED ALWAYS.
HINT: Use OVERRIDING SYSTEM VALUE to override.

两种 identity 的差别可以直接按插入行为记:

csharp 复制代码
generated by default:默认自动生成,允许手动给值
generated always:默认自动生成,手动给值会被拦住

always 更硬,手动插入要额外写覆盖语法;by default 更松,迁移数据时少一道限制。选哪个,要看是否允许外部 SQL 显式写主键。

identity 把自增规则写在列定义里,显式 sequence 把生成器单独拿出来。两种写法都能用,差别主要在管理方式。

从这组实验看,identity 两种模式都能跑。by default 适合需要保留历史主键的导入场景,always 适合不希望普通插入语句随便指定主键的表。哪种更合适,不看名字,看插入规则能不能接受。

如果表会经常做数据修复、临时补数、历史导入,by default 少一些限制;如果表的主键必须完全交给数据库生成,always 更符合这种约束。报错里的 OVERRIDING SYSTEM VALUE 也说明了,严格模式下不是完全不能覆盖,而是要显式声明覆盖行为。

更像业务表的写法

如果只是写一个练习表,bigserial 很省事:

bash 复制代码
id bigserial primary key

建表脚本需要长期维护时,可以直接写 sequence。下面这个表只放几个常见字段:自增主键、业务编号、金额、创建时间。

sql 复制代码
create sequence app_schema.t_order_demo_id_seq
  start with 1
  increment by 1
  cache 20;

create table app_schema.t_order_demo(
  id bigint primary key default nextval('app_schema.t_order_demo_id_seq'),
  order_no varchar(64) not null,
  amount numeric(12, 2) not null,
  created_at timestamp not null default current_timestamp
);

插入两条数据后,id1、2order_no 是业务编号,created_at 自动取当前时间。结构里也能看到 id 的默认值来自 t_order_demo_id_seq

这里的 idt_order_demo_id_seq 生成,order_no 仍然单独保存业务编号。两者不要混用。cache 20 也写在序列定义里,后面想改缓存时知道该改哪个对象。

临时表、验证表,用 bigserial 少写几行很正常。迁移 MySQL 旧表时,如果 auto_increment 在目标环境里能跑,也没必要为了改写而改写。到了需要长期留存的建表脚本,显式 sequence 或 identity 至少能让生成规则留在 DDL 里。

created_at 这里也给了默认值。插入订单数据时只传 order_noamount,返回结果里同时有 idcreated_at。主键来自 t_order_demo_id_seq,创建时间来自 current_timestamp,两个默认值都在这次插入里生效了。

写表结构时怎么选

把上面的实验压成几条:

sql 复制代码
auto_increment              当前版本能跑,迁移 MySQL 表时先验证
serial                      integer + sequence,适合小实验
bigserial                   bigint + sequence,更接近 bigint auto_increment
sequence + default nextval  名字、起始值、缓存都能自己控制
identity                    写在列定义里的自增规则,分 by default 和 always

如果只是从 MySQL 迁一批简单表,auto_increment 能跑当然省事;如果开始写新的 KingbaseES 建表脚本,bigserial 和显式 sequence 都应该会用。bigserial 快,显式 sequence 清楚,identity 则适合想把生成规则放进列定义的场景。

这几个写法不用急着分出高低。当前这套实验里,auto_incrementserialbigserial、显式 sequence、identity 都能完成"不传 id 自动生成值"。区别不在第一条插入语句,而在表结构、导入历史数据、手动覆盖主键、后续排查时暴露出来。

后面真遇到主键问题,先看两处:表结构里的 default,以及它引用的 sequence 或 identity 规则。能查到这一步,自增主键就不再只是一个建表关键字了。

相关推荐
lixora1 小时前
Oracle 11g Active Data Guard Go 自动化部署工具 v1.0
数据库·oracle
叶小鸡1 小时前
Java 篇-项目实战-AI 天机学堂(从 0 到 1)-day5
数据库·redis·缓存
Slice_cy1 小时前
基于node实现服务端内核引擎
前端·后端
mN9B2uk171 小时前
大数据量高并发的数据库优化
服务器·数据库·oracle
神奇小汤圆1 小时前
什么是面向切面编程AOP?
后端
Database_Cool_1 小时前
PolarSearch AutoETL:让数据库内置搜索不再需要搬运工
数据库
倾颜1 小时前
从手写 Runner 到 LangGraph:受控 Agent 接入 LangGraph
前端·后端·langchain
谁在黄金彼岸2 小时前
Lance模型解读
后端
神奇小汤圆2 小时前
深入理解MySQL事务隔离级别:MVCC机制与Next-Key Lock如何解决幻读问题?
后端