sql server的identity值在insert执行瞬间分配且不可回收,即使事务回滚也会跳号;根本原因是identity_cache预取机制(int默认1000)及设计上id分配不参与事务控制。

事务回滚时IDENTITY值已分配,无法回收
SQL Server 的 IDENTITY 列在 INSERT 语句执行的**瞬间**就完成值分配,不等事务提交。哪怕你马上 ROLLBACK,那个 ID 也永久作废了——它不会归还、不会重用、也不会触发任何警告。
常见错误现象:事务里插了 3 行,第 3 行因约束失败报错,整个事务回滚;但下一次成功插入时,ID 直接跳到 +3 后的位置,比如从 100 → 103。
- 根本原因不是“回滚没生效”,而是 SQL Server 设计上把 ID 分配视为不可逆的轻量操作
- 即使只写
INSERT INTO t (col) VALUES ('x');没指定 ID,只要表有IDENTITY列,就触发分配 -
SET IDENTITY_INSERT ON手动插入不会影响缓存计数,但它本身也不参与跳号逻辑
IDENTITY_CACHE 是默认开启的幕后推手
SQL Server 2012+ 默认启用 IDENTITY_CACHE,它让每次分配不是加 1,而是预取一段(INT 类型默认 1000 个),存在内存里批量用。一旦进程崩溃、实例重启或 AlwaysOn 故障转移,这部分未落地的 ID 就彻底丢失。
你看到的“跳号差值 ≈ 1000”不是偶然——那是缓存块大小,且这个值不可配置,只由数据类型绑定(BIGINT 是 10000)。
- 查当前状态:
SELECT name, value FROM sys.database_scoped_configurations WHERE name = 'IDENTITY_CACHE';,返回value = 1即开启 - 老版本(2016 及以前)无此视图,得靠
DBCC TRACESTATUS(272)查跟踪标志 - 禁用它(设为 0)能消除缓存导致的大跳号,但高并发插入性能会明显下降,闩锁争用上升
存储过程中无法绕过这个机制
你在存储过程里加 BEGIN TRAN / COMMIT / ROLLBACK,对 IDENTITY 分配行为毫无影响。它不看事务边界,只看 INSERT 是否被执行。
典型误判场景:
- 用
TRY...CATCH捕获错误后ROLLBACK,以为能“挽回”ID —— 实际不能 - 在循环中逐条
INSERT并检查@@ERROR,每失败一次就丢一个 ID - 用
IF NOT EXISTS做存在性校验再插入,结果校验和插入之间被并发写入抢占,导致唯一冲突 + 回滚 + ID 浪费
真正可控的替代方案只有业务层介入
如果你的业务真要求“显示序号连续”(比如订单号、单据流水号),别碰 IDENTITY。它天生不是干这个的。
务实选择:
- 用
SEQUENCE对象(SQL Server 2012+):NEXT VALUE FOR seq_order_no显式取号,再插入到普通字段;比IDENTITY更可控,但仍有小概率间隙(如取号后应用崩溃) - 号段表 + 行锁:
UPDATE number_pool SET current_no = current_no + 1 OUTPUT INSERTED.current_no WHERE type = 'order';,确保原子性 - 纯展示用序号?直接
ROW_NUMBER() OVER (ORDER BY create_time),查的时候算,不存、不依赖、不跳
跳号本身不是 bug,是性能与一致性的权衡结果。关键在于分清:哪部分 ID 是给数据库当主键用的,哪部分是给人看的——混在一起,早晚踩坑。











