mysql 5.7游标性能极差,因其强制逐行执行、绕过索引、禁用查询缓存;90%场景应改用insert...select、临时表join或变量聚合等集合操作替代。

MySQL 5.7 中游标不是“慢一点”,而是默认就走最差路径——它强制逐行执行、绕过所有索引优化、无法被查询缓存覆盖。真要提速,得先承认:90% 的游标场景本不该用游标。
为什么 MySQL 5.7 的游标特别伤性能
游标在 5.7 里本质是客户端模拟的“伪指针”,每次 FETCH 都触发一次独立的行级取值流程,不复用执行计划,也不走预编译缓存(即使你用了 ps_cache,游标内部的 SELECT 仍每次重解析)。更关键的是:CURSOR FOR SELECT ... 声明时若没加 ORDER BY,结果集顺序不可控,容易引发隐式排序或临时表,而 5.7 对临时表的磁盘落盘控制又弱,极易撑爆 tmp_table_size。
- 游标打开后,
SELECT语句不会走query cache(哪怕你开了) - 每 fetch 一行,都相当于一次独立的
SELECT ... LIMIT 1 OFFSET N逻辑,但底层没做任何优化 - 5.7 不支持游标声明中的参数化(如
CURSOR FOR SELECT * FROM t WHERE id > ?),只能靠拼接或变量,极易触发隐式类型转换
替代游标的三种可行方案
别硬扛游标,优先用集合操作拉平逻辑。以下方案均在 5.7 实测有效:
- 用
INSERT INTO ... SELECT替代“遍历+插入”:比如原游标逐条查用户再插日志,改成INSERT INTO user_log (user_id, event) SELECT id, 'login' FROM users WHERE last_login > DATE_SUB(NOW(), INTERVAL 7 DAY) - 用临时表 +
JOIN替代“遍历+查关联表”:把待处理 ID 先写入CREATE TEMPORARY TABLE tmp_targets (id BIGINT PRIMARY KEY),再UPDATE orders o JOIN tmp_targets t ON o.user_id = t.id SET status = 'processed' - 用变量累计替代“循环内条件累加”:比如统计支付/未支付订单数,直接
SELECT COUNT(IF(status='paid',1,NULL)) paid_cnt, COUNT(IF(status='pending',1,NULL)) pend_cnt FROM orders WHERE ...,别在游标里用IF ... THEN SET @paid = @paid + 1
非用游标不可时的硬核收敛策略
真有外部 API 调用、复杂状态机或逐行校验等刚性需求,必须用游标时,只保留最低限度的开销:
- 声明游标前,显式加
ORDER BY且确保排序字段有索引,避免 filesort;例如DECLARE cur CURSOR FOR SELECT id, amount FROM orders WHERE created_at >= '2026-09-01' ORDER BY id - 用
DECLARE CONTINUE HANDLER FOR NOT FOUND,**不要**用SQLSTATE '02000'—— 5.7 对后者的兼容性差,易漏触发 - FETCH 后立刻做
IF done THEN LEAVE,别在循环体里再嵌套判断;循环体内只做纯计算或单次 UPDATE/INSERT,禁止再查数据库 - 游标变量类型必须和字段完全一致:比如
id是BIGINT UNSIGNED,变量就得声明为DECLARE v_id BIGINT UNSIGNED,否则隐式转换会让后续 WHERE 条件失效
调试游标性能的三个必查点
线上跑得慢?别猜,直接查这三项:
- 执行
SELECT VERSION(),确认不是 5.7.20 或更早版本——5.7.21 才开始支持存储过程级执行计划缓存,低版本游标根本无缓存可言 - 在
performance_schema.events_statements_history_long里搜你的存储过程名,看多次调用时SQL_TEXT是否一致;若每次SQL_TEXT都带不同字面值(如WHERE id = 123/WHERE id = 456),说明你在游标外拼 SQL,已彻底放弃预编译优势 - 开
profiling(SET profiling = 1),跑完后SHOW PROFILES,重点看 “Sending data” 和 “Copying to tmp table” 占比——这两项高,基本就是游标内没索引或临时表没主键
游标不是语法糖,是性能红灯。5.7 下写游标,等于主动放弃 MySQL 最擅长的集合运算能力。真正难的不是让游标变快,而是说服自己:这一段逻辑,真的不能用一条 SQL 拉平吗?











