最常用方法是用 OUTPUT INSERTED.id 直接返回新插入主键值,需紧接 INSERT 后、显式指定列名、注意客户端映射;批量插入可用表变量捕获全部新 ID 和计算列;跨表插入需通过 FROM 关联源字段;避免直接 INTO 审计表以防触发器或约束失败。

INSERT 后直接返回新插入的主键值
最常见需求是插入一行后立刻拿到自增 ID,不用再查一遍。用 OUTPUT INSERTED.id 就行,它和 INSERT 语句绑在一次原子操作里,不会被并发干扰。
容易踩的坑:
-
OUTPUT必须紧跟在INSERT之后,不能换行或加其他语句隔开 - 如果表有多个标识列,
INSERTED.*会报错,必须显式列出要返回的列 - 返回结果不带列名别名时,客户端可能无法正确映射字段(比如 EF Core 会丢掉值)
示例:
INSERT INTO Users (Name, Email) OUTPUT INSERTED.Id, INSERTED.Email VALUES ('Alice', 'alice@example.com');
批量 INSERT 时捕获全部新生成的 ID 和业务字段
一次插多行,又想拿到每行的 Id、CreatedTime 等计算列或默认值,就不能靠 @@IDENTITY 或 SCOPE_IDENTITY()——它们只返回最后一个 ID。
实操要点:
- 用表变量接收输出,结构必须和
OUTPUT列表严格匹配(顺序、类型、是否允许 NULL) -
GETDATE()在OUTPUT中只执行一次,不是每行都调用,这点比触发器更可控 - 如果目标表有
DEFAULT约束或COMPUTED列,只要在INSERTED中显式引用,就能取到实际写入值
示例:
DECLARE @Inserted TABLE (Id INT, Name NVARCHAR(50), CreatedAt DATETIME2);<br>INSERT INTO Users (Name) OUTPUT INSERTED.Id, INSERTED.Name, INSERTED.CreatedAt INTO @Inserted VALUES ('Bob'), ('Charlie');<br>SELECT * FROM @Inserted;
INSERT INTO SELECT 场景下用 OUTPUT 捕获源数据与新 ID 的映射
从一张表查出数据插入另一张表,同时需要知道“源记录 A 对应了目标表里的哪个新 ID”,这是迁移或同步常见需求。
关键限制:
-
INSERTED只包含目标表字段,SELECT子句里的源字段必须通过FROM显式关联才能出现在OUTPUT中 - 不能在
OUTPUT里直接写source_table.id,得用FROM+ 别名方式带进来 - 目标表若含计算列或
ROWVERSION,这些值在INSERTED中可直接读取
示例:
INSERT INTO Orders (CustomerId, Amount) OUTPUT INSERTED.Id, inserted.CustomerId, s.OrderDate<br>SELECT c.Id, s.Amount<br>FROM StagingOrders s<br>JOIN Customers c ON s.CustomerEmail = c.Email<br>OUTPUT INSERTED.Id, INSERTED.CustomerId, s.OrderDate;
为什么不要把 OUTPUT 直接 INTO 主业务表做日志
有人想省事,直接 OUTPUT ... INTO AuditLog,但高并发下这很危险。
真实风险点:
-
INTO目标表如果启用了触发器、CHECK 约束或外键,OUTPUT会直接失败(SQL Server 明确禁止) - 审计表若没建好索引,
INSERT INTO AuditLog会成为性能瓶颈,拖慢原INSERT事务 - 更稳妥的做法是先进表变量,再异步批量写审计表,或者用 CDC 替代
真正该加的其实是非聚集索引:CREATE NONCLUSTERED INDEX IX_AuditLog_OpTime ON AuditLog(op_time)——否则按时间范围查日志会全表扫。

















