游标性能暴跌的根本原因是执行模型冲突,每次FETCH触发状态切换、内存拷贝、锁等待,1000行即1000次开销,远不如UPDATE JOIN等集合操作单次批量执行高效。

游标不是慢,是它根本就不是为“频繁处理数据”设计的。它的执行模型和数据库优化器天然冲突,只要进了循环,性能就大概率断崖式下跌。
每次 FETCH 都是一次微型事务开销
你看到的是“取一行”,数据库实际在做:重定位结果集位置 + 拷贝当前行到变量 + 更新游标内部状态 + 触发隐式锁检查 + 可能触发元数据锁等待。1000 行 = 至少 1000 次上下文切换和内存赋值。
-
DECLARE CURSOR FOR SELECT ...一旦执行,多数引擎(如 SQL Server 的 STATIC、MySQL 的默认游标)会把整个结果集预加载进内存或临时表 - 如果结果集撑爆
tmp_table_size或sort_buffer_size,就会落地磁盘临时表,Created_tmp_disk_tables会突增 - 即使只读游标,
FETCH仍可能阻塞 DDL(尤其在触发器里),报错常见:Waiting for table metadata lock
游标内嵌 SQL 是性能核弹
最常踩的坑:在 FETCH 循环里再查一次表。这不是 O(N),是 O(N×M) —— 每行都触发一次独立查询,全表扫描反复执行。
- 错误写法:
FETCH cur INTO @user_id; SELECT count(*) FROM user_actions WHERE user_id = @user_id; - 正确思路:提前用
LEFT JOIN或子查询把所有需要字段一次性拉出,游标只负责取值 - 监控信号:执行时
SHOW GLOBAL STATUS LIKE 'Handler_read%'中Handler_read_rnd_next暴涨,说明大量随机回表
执行计划无法复用,越跑越慢
很多开发者以为“SELECT 很快,套进游标应该也快”,但 EXPLAIN 会揭示真相:游标体内的 SELECT 在每次 FETCH 时都被当成新查询重编译,执行计划不缓存。
- SQL Server 中受
Parameter Sniffing影响更明显:首次参数生成的计划,后续不同参数下严重劣化 - MySQL 8.0+ 虽支持游标缓存,但仅限于
READ ONLY+FAST_FORWARD类型,且无法覆盖嵌套查询 - 真正省事的做法:把游标逻辑反推成集合操作,比如用
UPDATE ... FROM替代逐行UPDATE,用INSERT INTO ... SELECT替代循环INSERT
最难的从来不是怎么写游标,而是哪一行数据本就不该被“一行一行地拿”。90% 的游标场景,其实一条带 JOIN 或窗口函数的 SQL 就能闭环 —— 但人容易被“逻辑要一步步走”的直觉带偏。


















