应优先用update from或join批量更新替代游标:游标逐行更新10万行需10万次b-tree查找、加锁与日志刷盘,而集合操作一次哈希连接即可完成;mysql需改用update join;复杂逻辑可先物化#updates表;merge高并发易死锁,update from更稳。

UPDATE FROM 一次性更新替代游标逐行 UPDATE
游标里写 UPDATE t SET status = 'done' WHERE id = @id,本质是让数据库为每一行重复做主键查找、加锁、写日志。10万行就是10万次B-Tree定位 + 10万次事务日志刷盘。
直接改用集合操作:
UPDATE o SET status = 'processed' FROM orders o INNER JOIN #updates u ON o.id = u.order_id;
-
#updates必须提前物化好(SELECT id INTO #updates FROM ...),别在JOIN里嵌套子查询或函数调用 - MySQL 不支持
UPDATE ... FROM,得写成UPDATE t JOIN s ON ... SET t.col = s.val -
MERGE虽然语义清晰,但在高并发下容易死锁,UPDATE FROM更稳 - 如果更新逻辑依赖计算(比如
dbo.calc_score(@id)),先算好存进#updates,别让它在JOIN中反复执行
CTE + ROW_NUMBER() 分批处理替代顺序游标
适合必须按时间/ID顺序推进、又不能全量更新的场景(如分阶段状态流转、日志归档)。
WITH batch AS (
SELECT id, created_time,
ROW_NUMBER() OVER (ORDER BY created_time) AS rn
FROM orders
WHERE status = 'pending'
)
SELECT * FROM batch WHERE rn BETWEEN @start AND @end;
-
ORDER BY字段必须有索引,否则ROW_NUMBER()会强制排序,内存暴涨甚至溢出到tempdb - 别用
NEWID()排序——破坏索引利用,结果不可复现,批次不一致 - 分片大小建议 5000–10000 行:小于 5000 循环次数多;大于 10000 容易触发
LOG FULL或锁升级成表锁
WHILE + 临时表模拟可控逐行处理
仅当业务逻辑无法塞进单条SQL时才用:比如要调外部存储过程、写审计日志、控制每秒处理数、或依赖上一行结果。
SELECT id INTO #work_ids FROM orders WHERE status = 'pending'; SET XACT_ABORT ON; WHILE EXISTS (SELECT 1 FROM #work_ids) BEGIN DECLARE @id INT = (SELECT TOP 1 id FROM #work_ids); -- 处理单行逻辑 EXEC do_something @id; DELETE FROM #work_ids WHERE id = @id; END
- 第一步只查一次原表,把主键集提取进
#work_ids,避免每次循环都扫描原表 - 用
TOP 1或MIN(id)驱动,别用游标式FETCH NEXT - 每次处理完立刻
DELETE对应行,防止重跑或漏跑 -
SET XACT_ABORT ON是硬性要求,否则某次失败会导致后续循环卡住或数据残留
真正难的是判断“该不该逐行”
很多人不是不会写 WHILE,而是没想清楚:当前逻辑是否真的需要逐行?有没有隐藏的集合表达可能?比如状态机推进看似要顺序,但有时用 CASE WHEN + 窗口函数就能一次算出终态。
游标不是语法错误,是设计信号——它提示你:这段逻辑可能没被数据库引擎友好接纳。










