游标让存储过程性能急剧下降,因其将批量操作拆为万次独立执行;95%场景可用集合操作替代,性能提升数量级;仅三类不可替代:调用外部系统、滚动计算依赖上行、严格fifo加锁更新。

游标在绝大多数存储过程场景中不是“慢一点”,而是执行模型与数据库优化器根本冲突——它把本可一次完成的批量操作,硬拆成成千上万次独立执行路径。95% 以上的游标逻辑都能被集合操作无损替代,且性能提升通常是数量级的。
游标为什么让存储过程变慢到不可接受
每次 FETCH NEXT 都不是简单取一行:它触发一次上下文切换、一次锁检查、一次内存拷贝、一次执行状态更新。10 万行 = 至少 10 万次这些开销。而一条 UPDATE ... FROM 或 INSERT ... SELECT 只需一次解析、一次计划生成、一次批量 I/O。
- SQL Server 中默认游标类型(
KEYSET/DYNAMIC)会维护额外键集或临时表,初始化就卡住 - MySQL 游标不支持动态 SQL,且结果集结构每次打开都要重新解析,无法复用计划
-
@@FETCH_STATUS是全局变量,嵌套游标时极易被覆盖,导致循环提前退出或死循环 - 游标打开后持续持有行锁或键范围锁,一个 5 分钟的游标可能阻塞其他所有写操作
用 UPDATE FROM 或 JOIN 替代逐行 UPDATE
这是最常见也最值得优先替换的场景。游标里写 UPDATE t SET col = @val WHERE id = @id,本质是让数据库为每一行重复做主键查找、加锁、写日志;而集合操作一次哈希连接就能完成。
- SQL Server 写法:
UPDATE o SET status = 'processed' FROM orders o INNER JOIN #batch b ON o.id = b.order_id - MySQL 写法:
UPDATE orders o JOIN #batch b ON o.id = b.order_id SET o.status = 'processed' - 别在
JOIN里嵌套子查询或函数调用——先算好存进临时表,再关联 - 如果更新逻辑依赖计算(比如
dbo.calc_score(id)),必须提前物化结果,否则每行都重算
用 CTE + ROW_NUMBER() 分批处理替代顺序游标
当业务要求“按时间顺序分批处理”(如日志归档、状态流转),又不能全量更新时,CTE + ROW_NUMBER() 是比游标更可控、更高效的选择。
-
ORDER BY字段必须有索引,否则ROW_NUMBER()会强制排序,内存暴涨甚至溢出到tempdb - 分片大小建议 5000–10000 行:太小则循环次数多;太大易触发日志满或锁升级
- 别用
NEWID()排序——破坏索引利用,结果不可复现,批次不一致 - 示例:
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 @start AND @end
只有三类情况真需要游标,其余都该砍掉
不是“能不能写游标”,而是“有没有更稳更快的方式”。真正不可替代的场景极少:
- 逐行调用外部系统(如每条订单发 HTTP 请求到风控服务)——数据库没法发网络请求
- 强依赖上一行结果的滚动计算(如余额累加:当前行 = 上一行余额 + 本次变动),且窗口函数无法覆盖
- 必须严格 FIFO 顺序逐条加锁更新(如银行流水冲正),而
UPDATE不保证物理执行顺序
即使必须用游标,也要显式声明 READ_ONLY FAST_FORWARD,每次 FETCH 后立刻把 @@FETCH_STATUS 存入局部变量,并确保 CLOSE 和 DEALLOCATE 成对出现——漏掉 DEALLOCATE 会导致连接池资源长期卡死。











