游标在SQL Server中应彻底避免,因其逐行处理导致性能剧降、死锁风险高;推荐用ROW_NUMBER()分批更新或临时表+WHILE替代;仅动态DDL等极少数场景才谨慎使用FAST_FORWARD READ_ONLY游标。

游标在 SQL Server 存储过程中几乎总是性能毒药——10 万行数据用游标处理,大概率比集合操作慢 50 倍以上。真正该做的不是“怎么写得快一点”,而是“彻底不用它”。
为什么 DECLARE CURSOR 一出现就该警觉
每次 FETCH NEXT 都触发一次独立执行路径:上下文切换、锁检查、内存寻址、查询计划重评估。80 万行 = 至少 80 万次重复开销;而等价的 UPDATE ... FROM 或 INSERT ... SELECT 是单次计划、索引扫描、可并行。
-
@@FETCH_STATUS是全局变量,嵌套游标时极易被覆盖,导致循环提前退出或死循环 - 默认
KEYSET或STATIC游标会把整个结果集缓存进 tempdb,内存和日志压力陡增 - 即使加了
FAST_FORWARD和READ_ONLY,锁持有时间仍由游标生命周期决定,无法规避阻塞 - 错误现象示例:
Deadlock encountered, ROLLBACK requested by user—— 多个事务同时打开相同 WHERE 条件的游标,极易因锁顺序不一致触发死锁
用 ROW_NUMBER() + 分批 UPDATE 替代单行游标
适用于需按顺序处理、但无强事务依赖的场景(如日志归档、状态批量更新)。核心是把“逐行”变成“分片集合”,避免反复扫描原表。
- 必须在
ORDER BY字段建索引,否则ROW_NUMBER() OVER (ORDER BY created_time)会强制 SORT,内存爆涨 - 别用
NEWID()排序——不可控,且无法复用索引 - 分片大小建议设为
5000~10000行:太小则循环次数多;太大则单次事务日志暴涨,易触发LOG FULL - 示例逻辑:
WITH numbered AS ( SELECT *, ROW_NUMBER() OVER (ORDER BY created_time) AS rn FROM orders WHERE status = 'pending' ) UPDATE o SET status = 'processed' FROM orders o INNER JOIN numbered n ON o.id = n.id WHERE n.rn BETWEEN @offset + 1 AND @offset + @batch_size;
用临时表 + WHILE 循环替代复杂游标逻辑
当业务逻辑无法全写进一个 UPDATE 或 INSERT ... SELECT(比如要调用函数、做条件分支、记录中间日志),就用临时表暂存主键+必要字段,再用 WHILE 控制批次——它比游标轻量得多,且每批可独立提交。
- 临时表名用
#work_ids这类语义明确的命名,避免与已有#temp冲突 - 用
DELETE TOP (@batch_size) FROM #work_ids OUTPUT DELETED.*,比先SELECT TOP再DELETE更安全,防止漏删或重删 - 所有依赖字段(非仅主键)必须在插入
#work_ids时一并SELECT INTO进来,禁止在循环里反复查原表 - 务必加
SET XACT_ABORT ON,否则某批失败可能卡住后续批次
哪些场景真没法绕开游标?极少数,且要严控
只有动态生成 DDL、逐行发邮件、跨异构系统逐条调用外部 API 等无法预知结果集结构或必须强顺序的场景,才考虑游标。即便如此,也只用 FAST_FORWARD READ_ONLY,且必须显式 CLOSE + DEALLOCATE。
最容易被忽略的一点:游标本身不慢,慢的是它强迫数据库放弃集合优化能力——而 SQL Server 的查询优化器,对纯集合操作的优化已经非常成熟。你写的那句 UPDATE ... FROM,很可能比你花半小时调试的游标快 100 倍,且更稳定、更易维护。











