游标在sql server中性能差是因其本质逐行处理,应优先用集合操作替代;update from、分批cte、while+临时表是三大优化方案,核心是避免单行i/o与锁开销。

游标在SQL Server里慢,不是因为写法不对,而是它天生就干不了批量活。能不用就别用,优先走集合操作;非得逐行处理时,WHILE + 临时表比游标更可控、更轻量。
UPDATE FROM 或 MERGE 一次性更新替代游标逐行UPDATE
这是最常见也最容易改的场景:原游标里循环做 UPDATE t SET status = 'done' WHERE id = @id,实际完全可以用一次集合操作代替。
- 游标问题:每行触发一次主键查找、一次日志写入、一次锁申请,10万行就是10万次B-Tree定位
- 推荐写法:
UPDATE o SET status = 'processed' FROM orders o INNER JOIN #updates u ON o.id = u.order_id,哈希连接一次完成全部匹配 - 注意
MERGE在高并发下可能死锁,UPDATE FROM更稳;MySQL要换成UPDATE t JOIN s ON ... SET t.col = s.val - 如果更新逻辑依赖函数结果(如
dbo.calc_score(@id)),先物化到#updates里,别让函数在JOIN里反复执行
CTE + ROW_NUMBER() 分批处理替代单行游标
适用于需要按顺序处理、但又不能一次性全量更新的场景,比如日志归档、状态分阶段推进。
- 核心是把“逐行”变成“分片”,用
ROW_NUMBER() OVER (ORDER BY created_time)打序号,再用WHERE rn BETWEEN @start AND @end驱动批次 - 别用
NEWID()排序——它破坏索引利用,且结果不可复现 - 分片大小建议5000~10000行:太小导致循环次数多;太大易触发事务日志满(
LOG FULL) -
ORDER BY字段必须有索引,否则ROW_NUMBER()会强制SORT,内存暴涨
WHILE + 临时表模拟可控逐行处理
当业务逻辑复杂到无法塞进一条UPDATE或INSERT SELECT(比如要调外部存储过程、写审计日志、控制节奏),才考虑这个方案。
- 先用
SELECT order_id, customer_id INTO #work_ids FROM orders WHERE status = 'pending'提取主键集,只查一次原表 - 用
MIN(order_id)或自增row_number()作驱动键,每次循环后必须推进:SET @current_id = (SELECT MIN(order_id) FROM #work_ids WHERE order_id > @current_id) - 循环体里用
WHERE order_id = @current_id精准定位,处理完立即DELETE FROM #work_ids WHERE order_id = @current_id防重 - 务必加
SET XACT_ABORT ON,否则某批失败可能导致后续批次卡住或数据不一致
为什么FAST_FORWARD游标也不该轻易选
很多人以为加了FAST_FORWARD就能解决性能问题,其实只是掩耳盗铃。
-
FAST_FORWARD本质仍是逐行FETCH,只是省了部分元数据开销,I/O和锁压力一点没少 - 它默认是
READ_ONLY,没法在循环中回写或修改当前行上下文 - 一旦业务逻辑稍有变化(比如要根据上一行结果决定下一行行为),就得退回到
KEYSET或STATIC,内存开销翻倍 - 真正该问的是:“这逻辑能不能抽象成集合关系?”——90%的情况,答案都是能
最容易被忽略的一点:游标慢,往往不是慢在FETCH,而是慢在它让你心安理得地把本该集合处理的逻辑,拆成了N次单行操作。改之前先看执行计划,如果看到Compute Scalar节点下挂着标量函数调用,或者Clustered Index Seek重复出现几十万次,那基本就是游标在拖后腿。











