首先需要明确一个基本判断:在绝大多数业务场景中,游标并非最优选择,但某些情况下依然无法绕开。例如,当你需要逐行处理结果集,根据某一字段的值动态计算并更新另一字段,而这类逻辑无法通过一条 UPDATE ... JOIN 或窗口函数实现时,游标才真正派上用场。
在 SQL Server 中,声明显式游标的标准语法如下:DECLARE 游标名 CURSOR [LOCAL | GLOBAL] [FORWARD_ONLY | SCROLL] [STATIC | KEYSET | DYNAMIC | FAST_FORWARD] FOR SELECT语句。声明完成后,还需要依次配合 OPEN、FETCH NEXT INTO、WHILE @@FETCH_STATUS=0 循环、CLOSE 和 DEALLOCATE 等步骤才能完整执行。

SQL Server 中 DECLARE CURSOR 的基本语法与常见陷阱
实际开发中有几个容易踩的坑:首先,不要遗漏 DEALLOCATE——一旦漏掉,下次执行时会报错“The cursor is already declared”。其次,如果在循环内忘记用 FETCH NEXT 更新对应的 @variable,程序将陷入无限循环。
以下几条实操建议可以帮助你避免这些问题:
- 声明游标时,始终加上
LOCAL FAST_FORWARD——表示只读、前向、轻量,性能更优:
DECLARE cur_update CURSOR LOCAL FAST_FORWARD FOR SELECT id, amount FROM orders WHERE status = 'pending'
- 变量的数据类型必须与查询列严格匹配。例如,如果
id是INT类型,就不要声明为@id VARCHAR(10)。 - 每次
FETCH之后立即检查@@FETCH_STATUS = 0,否则可能会处理到空行或上一轮残留的数据。
循环体内 UPDATE 必须通过 WHERE 主键精确定位
这里有一个容易混淆的细节:游标本身并不提供直接更新数据的能力,你需要自己在循环内编写 UPDATE 语句。关键在于——这条 UPDATE 必须使用主键或唯一约束来精确锁定当前记录。如果写成 UPDATE orders SET ... WHERE status = 'pending',那么每次循环都会扫描整个表中所有符合条件的数据行,这并非更新当前行,而是在反复更新整个结果集。
一个典型的应用场景是:对每个待处理的订单,按实时汇率换算出 amount_usd 字段。
操作要点如下:
SELECT子句中必须包含用于定位的主键(例如id),并在UPDATE的WHERE条件中引用它:
UPDATE orders SET amount_usd = @amount * @exchange_rate WHERE id = @id
- 避免在循环内部再次查询表(比如再去
SELECT用户信息),应提前将需要的关联字段一并SELECT出来。 - 如果业务允许,优先考虑使用 CTE 配合
UPDATE ... FROM来替代游标,这种方式的性能通常高出整整一个量级。
MySQL 存储过程中游标没有 FETCH STATUS,需改用 NOT FOUND 处理
MySQL 与 SQL Server 在游标处理上有一个关键区别:MySQL 不支持 @@FETCH_STATUS。你必须使用 DECLARE CONTINUE HANDLER FOR NOT FOUND 来捕获游标数据读取完毕的信号。如果遗漏了 handler,循环只执行一次就会直接退出——最终仅处理了第一行记录。
遇到该问题时,典型表现是:存储过程执行完毕后只更新了第一条记录,而控制台没有任何错误提示。
建议的做法如下:
- handler 必须在
OPEN之前声明,同时在 handler 内部为标记变量(如@done)赋值:
DECLARE done INT DEFAULT FALSE;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE; - 在
FETCH之后立即检查done的状态,不要等到下一轮循环开始再判断:
FETCH cur INTO @id, @amount;
IF done THEN LEA VE read_loop; END IF; - MySQL 的游标不可滚动、不可更新、只能读取,所以
UPDATE必须单独编写语句。
游标性能差:哪些场景其实根本不需要它
根据实际经验,约90%声称“必须使用游标”的需求,本质上是因为没有意识到数据操作可以用集合方式解决。举个例子,如果要“给每个用户发送通知”——正确的做法并非通过游标逐个查询用户并调用存储过程发送邮件,而是生成一张通知记录表,由后台任务异步消费。
以下几个容易被忽略的思路可以帮你减少游标的使用:
UPDATE ... FROM(SQL Server)或UPDATE ... JOIN(MySQL)能完成大部分“查询—计算—更新”的链路。- 临时表配合
WHILE循环(SQL Server)比游标更快,省去了游标自身的开销,而且可以添加索引。 - 触发器或应用层的分页处理,有时比数据库层面的游标更可控、更易于监控。
- 如果游标循环内部还嵌套了远程查询、文件 I/O 或 HTTP 调用——坦白说,这已经超出了数据库的职责范围,应考虑将其移出存储过程。
