SQL Server存储过程应避免游标,因其将集合运算降级为单行循环,每次FETCH触发锁检查、内存寻址和执行计划重评估,导致性能断崖式下跌;95%场景可用UPDATE FROM、CTE+ROW_NUMBER或临时表分批等集合操作替代。

SQL Server 存储过程中频繁使用游标,本质是把数据库最擅长的「集合运算」强行降级为「单行循环」,性能断崖式下跌不是配置问题,而是执行模型冲突——每次 FETCH 都是一次独立查询上下文启动,10 万行就是 10 万次开销叠加。
每次 FETCH 都触发一次完整执行路径
游标不是“取一行数据”那么简单。每次 FETCH NEXT 实际会:重新检查锁状态(可能升级为页锁或表锁)、重新做内存寻址、重新评估执行计划缓存是否可用(常因参数嗅探失效而编译新计划)。这些动作本该由优化器在集合操作中一次性完成。
- 现象:
sys.dm_exec_requests中看到大量status = running但wait_type为LATCH_EX或PAGEIOLATCH_SH - 对比:等效的
UPDATE ... FROM语句通常只生成一个执行计划,走索引扫描或并行哈希连接,CPU 占用稳定在 20% 以下 - 陷阱:即使加了
FAST_FORWARD READ_ONLY,也无法绕过每次FETCH的上下文切换成本
游标结果集长期驻留,持续占用 tempdb 和锁资源
静态游标(STATIC)会把整个结果集写入 tempdb;键集游标(KEYSET)则维护键值索引结构+意向锁;动态游标(DYNAMIC)更是实时扫描源表——无论哪种,只要没执行 DEALLOCATE,资源就不释放。
- 现象:
tempdb.sys.database_files文件增长迅猛;sys.dm_tran_locks显示大量KEY或OBJECT级别锁长时间持有 - 后果:其他会话更新同一张表时被阻塞,
sp_who2出现大量blkby值 - 关键点:
CLOSE不等于释放——必须跟DEALLOCATE,漏掉一步,连接就卡在sleeping状态且open_cursor > 0
@@FETCH_STATUS 是全局变量,嵌套时极易失控
这个变量不是局部作用域的,任何位置执行一次 FETCH(包括子存储过程、触发器、甚至错误处理块里的游标),都会覆盖当前所有游标的上一次状态。
- 现象:外层游标循环突然退出,日志只处理了前几十行;或陷入死循环,反复处理同一行
- 安全做法:每次
FETCH后立刻存入局部变量,例如DECLARE @fetch_status INT; FETCH NEXT FROM cur INTO @id; SET @fetch_status = @@FETCH_STATUS; - 更糟的是:如果游标声明在
IF分支里,而FETCH在外面,@@FETCH_STATUS可能根本未初始化,直接导致逻辑错乱
替代方案不是“选哪个游标类型”,而是“能不能不用”
95% 的游标场景都能用集合操作无损替换。真正不可替代的只有三类:逐行调外部 API、强依赖上一行计算结果(窗口函数无法覆盖)、必须严格 FIFO 加锁冲正。其余情况硬上游标,等于主动放弃 SQL Server 的优化能力。
- 简单更新:用
UPDATE t1 SET x = t2.y FROM t1 INNER JOIN t2 ON ... WHERE ... - 分批可控处理:用
CTE + ROW_NUMBER()打序号,再用WHERE rn BETWEEN @start AND @end驱动批次 - 复杂中间逻辑:先
SELECT INTO #work_ids提取主键和必要字段,再用WHILE循环删TOP (@batch),每批显式COMMIT
最容易被忽略的一点:游标慢,往往不是因为数据量大,而是因为 DECLARE CURSOR FOR SELECT * 这种写法触发了隐式排序和临时表膨胀——哪怕你只打算取前 5 行,SQL Server 也可能先把全表扫一遍再排序。


















