当前位置: 代码网 > it编程>数据库>MsSqlserver > SQLServer插入数据后怎么获取自增主键

SQLServer插入数据后怎么获取自增主键

2026年09月15日 MsSqlserver 我要评论
sql server 插入数据后,究竟该怎么获取自增主键?在 sql server 开发中,一个非常常见的需求是:插入一条主表数据 ↓拿到数据库生成的 id ↓继续插入明细

sql server 插入数据后,究竟该怎么获取自增主键?

在 sql server 开发中,一个非常常见的需求是:

插入一条主表数据
        ↓
拿到数据库生成的 id
        ↓
继续插入明细数据

例如:

insert into bsyc_purchase_plan(name, plan_no, ...)
values ('2026采购计划', 'pp20260001', ...);

-- 怎么拿到刚刚生成的 id?

看起来只是一个简单问题,但 sql server 实际上提供了多种方式:

  • output inserted.id
  • scope_identity()
  • @@identity
  • ident_current()
  • select max(id)
  • output ... into

它们表面上都是“获取 id”,但作用域、并发安全性、触发器影响、批量插入能力完全不同

如果系统需要兼容 sql server 2008 及以上版本,理解这些区别非常重要。

先理解 sql server 的 identity

假设有这样一张表:

create table bsyc_purchase_plan
(
    id bigint identity(1,1) not null primary key,
    name varchar(100) not null,
    plan_no varchar(50) not null
);

其中:

id bigint identity(1,1)

表示数据库自动生成:

1
2
3
4
5
...

应用程序插入数据时一般不传 id

insert into bsyc_purchase_plan
(
    name,
    plan_no
)
values
(
    '2026采购计划',
    'pp20260001'
);

问题就变成:

数据库刚刚生成的那个 id,到底应该怎么拿?

方式一:output inserted.id

这是非常直接的一种方式。

insert into bsyc_purchase_plan
(
    name,
    plan_no
)
output inserted.id
values
(
    '2026采购计划',
    'pp20260001'
);

数据库直接返回:

id
----
101

output 可以返回 insertupdatedeletemerge 所影响的行;对于 insertinserted 表示插入后的数据,因此可以直接取得数据库生成的 identity、计算列等值。

这实际上非常符合函数式思维:

insert(data) -> inserted row

而不是:

insert(data)
然后再想办法查询 id

优点:

语义非常清楚:

insert ...
output inserted.id
values ...

意思就是:

插入数据,并把插入后的 id 返回给我。

而且它不仅能返回 id:

output
    inserted.id,
    inserted.plan_no,
    inserted.name

例如:

insert into bsyc_purchase_plan
(
    name,
    plan_no
)
output
    inserted.id,
    inserted.plan_no,
    inserted.name
values
(
    '2026采购计划',
    'pp20260001'
);

这对 api、脚本、数据处理程序非常方便。microsoft 也明确说明,output 可用于取得插入后产生的 identity 或计算列。

方式二:scope_identity()

这是 sql server 很经典的一种写法。

insert into bsyc_purchase_plan
(
    name,
    plan_no
)
values
(
    '2026采购计划',
    'pp20260001'
);

select scope_identity() as id;

scope_identity() 返回:

当前 session、当前 scope 中最后产生的 identity 值。

sql server 中所谓 scope,可以理解成当前的存储过程、触发器、函数或者 sql batch。

例如:

declare @id bigint;

insert into bsyc_purchase_plan
(
    name,
    plan_no
)
values
(
    '2026采购计划',
    'pp20260001'
);

set @id = cast(scope_identity() as bigint);

select @id as id;

这里为什么建议:

cast(scope_identity() as bigint)

因为 scope_identity() 返回的数据类型实际上是:

numeric(38,0)

而我们的主键是:

bigint

因此显式转换更加清晰。

什么时候 scope_identity() 比 output 更方便?

例如典型的:

创建采购计划
      ↓
拿到 plan_id
      ↓
插入采购计划维度
      ↓
插入采购计划明细

这种情况下:

declare @plan_id bigint;

insert into bsyc_purchase_plan
(
    name,
    plan_no
)
values
(
    '2026采购计划',
    'pp20260001'
);

set @plan_id = cast(scope_identity() as bigint);

insert into bsyc_purchase_plan_item
(
    plan_id,
    product_code
)
values
(
    @plan_id,
    '10001'
);

select @plan_id as id;

代码非常自然。

因此可以简单记:

只想返回 id
    ↓
output

后面的 sql 还需要使用 id
    ↓
scope_identity()

当然,这并不是绝对规则,因为 output into 同样能够解决后一类问题。

方式三:@@identity

还有一种历史悠久的写法:

insert into bsyc_purchase_plan
(
    name,
    plan_no
)
values
(
    '2026采购计划',
    'pp20260001'
);

select @@identity;

它与 scope_identity() 的最大区别是:

scope_identity()
当前 session
+
当前 scope

@@identity
当前 session
+
所有 scope

microsoft 文档明确指出,@@identityscope_identity() 都限制在当前 session,但 @@identity 不限制 scope。

这个区别在存在触发器时非常关键

为什么一般不推荐 @@identity?

假设:

purchase_plan
    ↓ insert
trigger
    ↓ insert
operation_log

两张表都有 identity:

purchase_plan.id = 101

operation_log.id = 9001

执行:

insert into purchase_plan(...)
values (...);

然后触发器执行:

insert into operation_log(...)
values (...);

此时:

select scope_identity();

可能得到:

101

而:

select @@identity;

可能得到:

9001

因为 @@identity 会跨 scope 获取当前 session 最后生成的 identity,所以触发器中的 identity 可能覆盖你真正想要的值。

microsoft 文档就使用了类似的触发器场景解释两者差异,并特别指出复制机制中的触发器也可能影响 @@identity

因此工程实践中:

scope_identity()  ✅

@@identity       ⚠️ 尽量不用

方式四:ident_current()

还有一个函数:

select ident_current('bsyc_purchase_plan');

例如返回:

101

很多刚接触 sql server 的开发者会觉得:

这个不是更好吗?我直接指定表名了。

实际上恰恰相反。

ident_current('表名') 返回的是:

指定表最后产生的 identity,不限制 session,也不限制 scope。

因此:

session a
insert -> id = 101

session b
insert -> id = 102

session a
ident_current(...)

session a 完全可能看到:

102

而不是自己的:

101

所以:

insert into bsyc_purchase_plan(...)
values (...);

select ident_current('bsyc_purchase_plan');

不能用于可靠地获取“我刚刚插入的 id”。

microsoft 也明确提醒,不能依赖 ident_current + ident_incr 去预测下一个 identity,因为其他 session 可以同时插入数据。

它更适合:

查看表当前 identity 状态
诊断
管理
监控

而不是业务代码获取新插入记录的主键。

方式五:select max(id)

还有一种非常常见但危险的代码:

insert into bsyc_purchase_plan(...)
values (...);

select max(id)
from bsyc_purchase_plan;

单用户测试的时候:

插入 101

max(id) = 101

看起来一点问题都没有。

但是生产环境:

线程 a
insert -> 101

线程 b
insert -> 102

线程 a
select max(id)

结果:

102

线程 a 拿到了线程 b 的 id。

问题的本质不是 max 性能,而是:

max(id)

表达的是:

当前整张表最大的 id。

而我们真正需要的是:

当前这个 insert 产生的 id。

它们根本不是同一个语义。

所以:

select max(id)

用于获取刚插入的主键,在并发系统中属于典型错误设计。

几个方法真正的区别是什么?

可以从两个维度理解:

session
scope

假设:

session a
    scope 1
        insert table_a

        trigger
            scope 2
                insert table_b

session b
    insert table_a

那么:

方法session 限制scope 限制指定表
scope_identity()
@@identity
ident_current()
output inserted.id当前 dml当前 dml当前 dml
max(id)

microsoft 对三个 identity 函数的定义可以总结为:scope_identity() 是当前 session + 当前 scope;@@identity 是当前 session + 任意 scope;ident_current() 则是指定表 + 任意 session + 任意 scope。

如果我们用集合关系表达:

scope_identity
        ↓
范围最小

@@identity
        ↓
扩大到整个 session

ident_current
        ↓
扩大到所有 session

所以业务程序最常使用的通常是范围最明确的方式。

其实 output 还有一个非常重要的能力:批量获取 id

假设一次插入三条数据:

insert into bsyc_purchase_plan
(
    name,
    plan_no
)
output
    inserted.id,
    inserted.plan_no
values
    ('采购计划a', 'pp001'),
    ('采购计划b', 'pp002'),
    ('采购计划c', 'pp003');

可能得到:

id      plan_no
----------------
101     pp001
102     pp002
103     pp003

这时候 scope_identity() 就做不到同样的事情。

因为:

select scope_identity();

只返回当前 scope 中最后产生的一个 identity,而不是整个批次的 id 集合。@@identity 在多行插入情况下同样只返回最后生成的 identity。

所以:

单条 insert
    output / scope_identity 都可以

批量 insert
    output 明显更合适

这也是 output 最大的价值之一。

output into:更强的玩法

很多人知道:

output inserted.id

但不知道还有:

output inserted.id into ...

例如:

declare @ids table
(
    id bigint,
    plan_no varchar(50)
);

insert into bsyc_purchase_plan
(
    name,
    plan_no
)
output
    inserted.id,
    inserted.plan_no
into @ids(id, plan_no)
values
    ('采购计划a', 'pp001'),
    ('采购计划b', 'pp002'),
    ('采购计划c', 'pp003');

select *
from @ids;

microsoft 的 output 语法明确支持把结果写入表或者表变量,而不是直接返回给客户端。

这样就可以:

insert
   ↓
output
   ↓
@ids
   ↓
继续参与后续 sql

例如:

insert into purchase_plan_log
(
    plan_id,
    plan_no
)
select
    id,
    plan_no
from @ids;

这种模式在:

批量创建订单
批量创建采购计划
批量创建任务
批量导入数据
主从表生成

里面非常好用。

output 和触发器还有一个容易踩坑的地方

很多人会认为:

output inserted.id

在任何表上都可以直接使用。

实际上并不是。

如果目标表针对对应的 dml 操作存在启用状态的 trigger,那么:

output ...

如果不带 into,会受到限制。

microsoft 文档明确说明:如果 output 没有搭配 into,那么对应 dml 的目标表不能存在该操作的启用 trigger。

比如:

insert into orders(...)
output inserted.id
values (...);

而:

orders
存在启用的 insert trigger

就可能无法直接这样使用。

这时可以考虑:

declare @result table
(
    id bigint
);

insert into orders(...)
output inserted.id into @result
values (...);

select id
from @result;

或者直接采用:

scope_identity()

因此在大量老 erp、drp、财务系统中,如果表上 trigger 很多,scope_identity() 往往更加省心。

output 返回的是“触发器之前”的数据

还有一个高级细节。

output inserted.xxx 返回的是:

dml 完成后的值
+
trigger 执行之前的值

microsoft 文档对此有明确说明。

所以假设:

insert ...

之后 trigger 又修改了某些字段,那么:

output inserted.xxx

得到的不一定是 trigger 最终修改完成后的最终数据库状态。

id 来说通常没有影响,但如果你:

output inserted.status,
       inserted.amount,
       inserted.xxx

就需要意识到这一点。

还有一个经常被误解的问题:identity 不保证连续

假设:

100
101
102

下一次 insert 失败了。

之后再 insert,完全可能出现:

104

而不是:

103

这是正常现象。

sql server 的 identity 值即使因为语句失败或者事务回滚没有最终提交,也可能已经消耗,因此 identity 序列可能产生空洞。

因此:

identity

应该理解成:

自动产生的唯一标识。

而不是:

永远连续的业务流水号。

如果业务需要:

cg2026000001
cg2026000002
cg2026000003

这种严格业务编号,应当设计独立的编号生成机制,而不是依赖 identity 连续性。

最终推荐

如果是普通业务系统,可以采用下面的原则。

场景一:插入一条,直接返回 id

首选:

insert into bsyc_purchase_plan
(
    name,
    plan_no
)
output inserted.id
values
(
    '采购计划',
    'pp001'
);

简单、直观。

场景二:插入之后,后续 sql 继续使用 id

推荐:

declare @id bigint;

insert into bsyc_purchase_plan
(
    name,
    plan_no
)
values
(
    '采购计划',
    'pp001'
);

set @id = cast(scope_identity() as bigint);

-- 后续业务
insert into ...
values (@id, ...);

select @id as id;

尤其是老系统中存在大量 trigger 时,这种方式非常实用。

场景三:批量插入

推荐:

output inserted.id

甚至:

output inserted.id into @ids

因为它天然处理的是:

一组输入
        ↓
一组 insert
        ↓
一组生成结果

场景四:不要再使用这些方式获取当前 insert 的 id

谨慎甚至避免:

@@identity

不要用于这个目的:

ident_current('table')

更不要:

select max(id)

一张表记住所有区别

方法单条插入批量插入trigger 安全性并发安全推荐度
output inserted.id⚠️ 直接 output 有限制⭐⭐⭐⭐⭐
scope_identity()❌ 只能拿最后一个✅ 不受 trigger scope 干扰⭐⭐⭐⭐⭐
output into更灵活⭐⭐⭐⭐⭐
@@identity❌ 可能被 trigger 影响当前 session 内⭐⭐
ident_current()
max(id)无关

从架构角度理解这件事情

其实这个问题本质上不是:

sql server 有哪几个获取 id 的函数?

而是一个非常典型的上下文边界问题

我们真正需要表达的是:

y = f(x)

其中:

x = 要插入的数据

f = insert

y = 数据库真正生成的数据

理想模型应该是:

insertedrow = insert(input)

所以从这个角度看:

insert ...
output inserted...

其实是最接近这个模型的 sql 设计:

input
   ↓
insert
   ↓
output

而:

insert ...

select max(id) ...

实际上已经变成:

input
   ↓
insert

database global state
   ↓
select max

第二个查询依赖的是数据库全局状态,而不是第一次 insert 的直接输出,因此并发问题自然就出现了。

这也是为什么现代系统设计越来越强调:

input → operation → output

而不是:

执行操作
   ↓
再从全局状态猜测刚才发生了什么

结论

sql server 获取新增主键并不复杂,真正需要理解的是 session、scope、trigger 和并发边界

可以最终记成三句话:

单条插入返回结果:
output inserted.id

后续 sql 需要继续使用:
scope_identity()

批量插入:
output / output into

而下面三种:

@@identity
ident_current()
max(id)

虽然某些场景“看起来也能拿到 id”,但它们表达的语义与“获取我刚刚插入的这条数据的 id”并不完全一致。

数据库编程中,一个非常值得坚持的原则就是:

不要从全局状态推断局部操作的结果;能直接取得操作结果,就直接取得结果。

这不仅适用于 sql server 的 identity,同样适用于事务、消息队列、分布式系统、api 设计以及整个软件架构。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com