SQLServer插入数据后怎么获取自增主键
SQL Server 插入数据后,究竟该怎么获取自增主键?
在 SQL Server 开发中,一个非常常见的需求是:
插入一条主表数据
↓
拿到数据库生成的 ID
↓
继续插入明细数据
例如:
insert into bsyc_purchase_plan(name, plan_no, ...)
values ('2026采购计划', 'PP20260001', ...);
-- 怎么拿到刚刚生成的 id?
看起来只是一个简单问题,但 SQL Server 实际上提供了多种方式:
OUTPUT INSERTED.idSCOPE_IDENTITY()@@IDENTITYIDENT_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 可以返回 INSERT、UPDATE、DELETE、MERGE 所影响的行;对于 INSERT,INSERTED 表示插入后的数据,因此可以直接取得数据库生成的 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 文档明确指出,@@IDENTITY 与 SCOPE_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 设计以及整个软件架构。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。
相关文章
详解DB2 sqlstate 57016 SQLCODE=-668 原因码 "7"错误的快速解决办法
db2 sqlstate 57016,db2 57016 原因码7错误怎么解决呢?下面小编给大家带来了DB2 sqlstate 57016 SQLCODE=-668 原因码 "7"错误的快速解决办法,一起看下吧2016-08-08
深入SQL SERVER合并相关操作Union,Except,Intersect的详解
本篇文章是对SQL SERVER合并相关操作Union,Except,Intersect进行了详细的分析介绍,需要的朋友参考下2013-06-06
SQLServer EVENTDATA()函数来获取DDL 触发器信息
SQL Server 2005/2008中可以使用EVENTDATA函数来获取DDL触发器的上下文,从而在ROLLBACK之前截获DDL信息。EVENTDATA返回XML字段,下面的例子显示如何截获Drop Table的DDL信息。2009-07-07


最新评论