SQL Server 中 NEXT VALUE FOR 生成的序列号不可回滚,因其分配动作独立于事务、不参与 undo 日志,每次调用即永久消耗值;常见于触发器中误用导致“跳号无记录”,需通过预占范围、应用层控制或禁用缓存等方式规避。

SQL Server 中的序列号一旦由 NEXT VALUE FOR 分配,就不可回滚 —— 无论事务是否提交或回滚,该序列值都已消耗。 这不是 bug,是设计使然。理解这一点,才能避免在订单号、单据号等场景中出现“跳号却查不到记录”的困惑。
为什么 NEXT VALUE FOR 不受事务控制
序列对象独立于表和事务生命周期存在。每次调用 NEXT VALUE FOR 都会立即推进序列内部计数器,并从缓存(如有)取值或向磁盘写入新起始点。这个动作本身不参与事务日志的 undo 链路。
- 即使后续执行
ROLLBACK TRANSACTION,序列已分配的值不会返还、不重用、不回退 - 与标识列(
IDENTITY)类似,但更彻底:标识列至少还受限于表插入上下文;序列则完全无绑定 - 底层依赖
sys.sequences的current_value和cache_size,而这两个字段更新不记入事务 undo 日志
NEXT VALUE FOR 在触发器里调用的风险
把 NEXT VALUE FOR 放进 INSTEAD OF 或 AFTER 触发器里,极易造成“隐形消耗”。尤其当触发器因业务逻辑失败而 THROW 或 RAISERROR 后未显式 ROLLBACK,序列号照常递增,但主表可能没插入任何数据。
- 常见错误现象:
INSERT INTO orders返回成功,查表为空,但下一次插入发现 order_no 跳了 3 位 - 根本原因:触发器内调用了三次
NEXT VALUE FOR seq_order(比如用于主单+子单+日志),然后某处IF @valid = 0 THROW ...,但没配SET XACT_ABORT ON或ROLLBACK - 验证方法:在触发器开头加
INSERT INTO debug_log VALUES (GETDATE(), 'seq_used', NEXT VALUE FOR seq_order),再对比日志与实际写入行数
如何安全地生成“可回滚”的业务编号
如果业务要求编号必须与数据强一致(即没插入成功就不能有编号),就不能依赖序列的即时分配行为。得把编号生成时机后移到真正要提交前,或改用其他机制。
- 方案一:用
sp_sequence_get_range预占一批号,仅在确认提交前才从缓存中取用 —— 缓存未用完的部分可丢弃,不触发序列推进 - 方案二:将编号逻辑移出触发器,改在应用层用
BEGIN TRY / BEGIN CATCH包裹整个流程,确保NEXT VALUE FOR只在最终COMMIT前调用一次 - 方案三:对高一致性要求场景,放弃序列,改用带
UPDLOCK, HOLDLOCK的SELECT MAX(no) + 1模式(注意性能瓶颈和死锁风险)
缓存设置对序列“浪费”程度的影响
序列默认启用 CACHE 50(具体值取决于版本和创建语句),这意味着哪怕只调用一次 NEXT VALUE FOR,SQL Server 也可能预分配并缓存 50 个值到内存。如果实例意外重启,这 50 个中的剩余未用值就永久丢失。
- 查看当前缓存状态:
SELECT cache_size, current_value FROM sys.sequences WHERE name = 'seq_order' - 低频业务可设
CACHE 1减少浪费,但会增加 latch 争用;高频并发则需保留合理缓存以避免序列争用成为瓶颈 - 禁用缓存(
NO CACHE)看似“精确”,实则让每次调用都触发元数据更新,反而放大性能问题
序列号的不可回滚性,在多数 OLTP 场景下是合理取舍;但一旦它被嵌进触发器、又混进复杂事务流,就很容易变成一个静默的坑——你查不到数据,也找不到编号去哪了,因为那串数字早就被序列自己悄悄记在了系统表里。

















