游标慢是执行模型决定的:将集合操作拆为N次单行事务,每次FETCH都需重复解析、权限检查等,10万行即10万次上下文切换,且默认持锁易致死锁。

游标为什么一用就慢
不是写法问题,是执行模型决定的:游标把集合操作硬拆成 N 次单行事务。每次 FETCH 都要重新走解析、权限检查、锁粒度评估、内存寻址,10 万行 = 至少 10 万次上下文切换。更麻烦的是,它默认持有行锁或键范围锁直到 CLOSE,并发时极易触发死锁,错误信息常是 Deadlock encountered。
常见错误现象包括:
-
@@FETCH_STATUS是全局变量,嵌套游标时容易被覆盖,导致循环提前退出或死循环 -
DECLARE CURSOR FOR SELECT *没加WHERE或索引字段,一开就全表扫描 - 日志里只看到存储过程调用入口耗时,查不到游标内部哪一行卡住——监控完全盲区
WHILE 循环替代游标的正确写法
WHILE 本身不比游标快,关键在“数据提取与处理分离”:先一次性查出主键集到临时表,再按行号控制处理节奏。这样避免反复扫描原表,也方便分批提交。
必须做到以下三点,否则只是换个方式慢:
- 用
SELECT ... INTO #temp把待处理 ID 和必要字段一次性落盘,别在 WHILE 里反复查原表 - 临时表必须带自增
RowID(用IDENTITY(INT,1,1)),不能依赖原表顺序 - 每批处理完立刻
UPDATE #temp SET DealFlg = 1标记,再用MIN(RowID)查下一批,避免重复处理
错误示范:SELECT @id = MIN(id) FROM orders WHERE processed = 0 然后 UPDATE,再重复查——这会让优化器每次重估执行计划,且原表被高频扫描。
什么时候该放弃 WHILE,直接上集合操作
95% 的游标逻辑其实根本不需要逐行处理。只要业务语义是“对满足条件的记录统一更新/插入”,就该用一条 SQL 解决。
典型可替换场景:
- 根据另一张表更新当前表 → 用
UPDATE ... JOIN或UPDATE ... WHERE id IN (SELECT ...) - 按规则批量赋值(如 status = CASE WHEN ...)→ 直接
UPDATE ... SET col = CASE ... - 需要分页处理大结果集 → 用
ROW_NUMBER() OVER ()+BETWEEN @offset AND @offset+@batch_size
比如原游标逻辑是“遍历订单找用户,再更新用户积分”,直接写成:UPDATE users u JOIN orders o ON u.id = o.user_id SET u.points = u.points + o.amount WHERE o.status = 'paid',性能差距往往是 20 倍起。
MySQL 和 SQL Server 的 WHILE 差异要点
MySQL 存储过程中 WHILE 语法和 SQL Server 类似,但有几处关键区别必须注意:
- MySQL 不支持
@@FETCH_STATUS这类状态变量,所以不能靠它判断循环退出,得自己维护计数器或用SELECT COUNT(*)判空 - SQL Server 的
IDENTITY在临时表中可用,MySQL 要用ALTER TABLE #temp ADD COLUMN rn INT AUTO_INCREMENT PRIMARY KEY FIRST或改用变量模拟行号 - MySQL 8.0+ 支持递归 CTE,复杂逻辑优先考虑
WITH RECURSIVE,比 WHILE 更安全可控
真正难的从来不是怎么写循环,而是识别出“这里其实根本不用循环”。一旦开始写 DECLARE cur CURSOR,先停三秒,问自己一句:这个需求,能不能用一条 UPDATE 或 INSERT ... SELECT 搞定?











