应优先用 update from 或 merge 替代游标,因其是单次集合操作,可生成并行执行计划;而游标将10万次更新拆为10万次独立语句,引发高上下文切换、长锁持有及无法并行等问题。

用 UPDATE FROM 或 MERGE 替代逐行 UPDATE
游标慢的主因是把 10 万次更新拆成 10 万次独立语句执行,而 UPDATE ... FROM 或 MERGE 是单次集合操作,由优化器生成并行计划。常见错误是直接把游标里 UPDATE t SET col = @val WHERE id = @id 原样搬出来,却没改写成关联更新。
- 必须确保目标表与源数据能通过 JOIN 明确关联,避免笛卡尔积(
UPDATE t1 SET x = t2.y FROM t1, t2没 ON 条件就是典型错误) - 如果源是计算结果,先用 CTE 或临时表固化,别在
FROM子句里写复杂子查询——SQL Server 可能重复执行 -
MERGE更适合“存在则更新、不存在则插入”场景,但要注意ON条件必须唯一,否则报错The MERGE statement attempted to UPDATE or DELETE the same row more than once
用 CTE + ROW_NUMBER() 实现可控分批处理
当业务逻辑太重,无法塞进单条 UPDATE,又不能接受游标长事务锁表时,ROW_NUMBER() 分批是最稳的折中方案。它不依赖游标生命周期管理,也不要求结果集可排序,只要主键或唯一标识存在即可。
- 先建临时表
#batch_ids,只存 ID 和必要字段(别 SELECT *,避免宽表膨胀 tempdb) - 用
ROW_NUMBER() OVER (ORDER BY id)打序号,再用WHERE rn BETWEEN @start AND @end切片 - 每批处理后显式
COMMIT,并在循环外加SET XACT_ABORT ON防止部分失败卡住事务 - 切忌在 WHILE 循环里反复查原表——所有字段必须在插入
#batch_ids时一次性SELECT INTO进来
游标真不可替代时,必须设对类型和选项
不是所有游标都能被绕过。比如要逐条调用 sp_OACreate 发 HTTP 请求,或银行流水严格 FIFO 冲正,这时游标是唯一选择。但错选类型会让性能雪上加霜。
- 默认用
STATIC READ_ONLY FORWARD_ONLY(SQL Server),禁用动态感知和更新能力,减少锁和版本控制开销 - 绝对不要写
DECLARE cur CURSOR FOR SELECT *——隐式排序会强制走 tempdb,加明确WHERE和ORDER BY(且字段必须有索引) -
@@FETCH_STATUS是全局变量,多层嵌套必出问题:每次FETCH后立刻存入局部变量,如SET @fs = @@FETCH_STATUS -
CLOSE不释放内存,DEALLOCATE才真正清理;异常路径也要覆盖,务必包在BEGIN TRY / BEGIN CATCH里
索引没对,游标再轻量也白搭
游标底层仍是 SELECT 查询,如果这个查询本身走全表扫描,加再多游标选项也没用。关键不是“游标快不快”,而是“游标查的那一句快不快”。
- 开启
SET STATISTICS XML ON,捕获实际执行计划,重点看游标对应SELECT节点是否出现“Key Lookup”或“Sort”警告 - 覆盖索引必须包含:WHERE 条件列、ORDER BY 列、以及
FETCH INTO接收的所有字段(哪怕只是@id和@name) - 别信“加了索引就行”——用
DBCC SHOW_STATISTICS确认统计信息是否过期,尤其在大批量导入后 - 测试时对比逻辑读取数:
SET STATISTICS IO ON下,优化后游标查询的logical reads应比优化前下降 50% 以上
SELECT 语句,本质上就是一条独立 SQL,该走索引、该加提示、该看执行计划——和任何其他查询毫无区别。











