MySQL游标处理百万级记录注定慢、锁久、易卡死,必须走索引、禁用SELECT*、避免循环内子查询和HTTP调用,每100行显式COMMIT,并优先用批量UPDATE替代。

MySQL存储过程里用游标处理百万级记录,本质上是在对抗设计缺陷——它注定慢、锁得久、容易卡死。能不用就别用;必须用,就得接受“逐行”带来的所有代价,并主动设限止损。
DECLARE CURSOR FOR 的 SELECT 必须走索引
游标打开时不会报错,但第一次 FETCH 就可能卡住几秒甚至超时,原因几乎全是这里没走索引。
- 用
EXPLAIN检查游标声明里的SELECT语句,type至少要是ref或range,绝不能是ALL - 禁止
SELECT *,只选真正要用的字段,减少内存拷贝和网络模拟开销 - WHERE 条件字段、ORDER BY 字段必须联合建索引,尤其避免在条件中对字段用函数,比如
DATE_FORMAT(created_at, '%Y-%m')会让索引失效 - 如果原始查询带
LIMIT,注意 MySQL 游标不支持在声明时直接加LIMIT,得靠 WHERE + 排序字段分页模拟
FETCH 循环里不能有子查询或嵌套存储过程
每次 FETCH 后立刻执行另一个 SELECT 或 CALL,等于把单行处理放大成 N×N 次查询,百万行就变成亿级操作。
- 所有依赖数据需提前查好,存进临时表或变量,例如用户状态、配置项、映射关系等
- 禁止在循环体内调用外部 HTTP 接口(哪怕封装成 UDF),这类操作应移出数据库,由应用层异步处理
- 不要用
SLEEP()控制节奏——它会延长事务持有时间,增加锁冲突概率 - 若逻辑真需要“前一行结果”,改用变量累加:
SET @sum := @sum + value,在单条SELECT中完成
必须显式 COMMIT + 分批控制行数
MySQL 游标默认在事务内隐式维持锁,不 COMMIT,整个循环期间可能一直锁着主表,其他业务全被堵住。
- 每处理 100 行后执行一次
COMMIT,并用SELECT ROW_COUNT()校验上一批是否全部生效 - 用计数器控制批次,而不是依赖
NOT FOUND:它只捕获“无数据”,不捕获锁等待、主从延迟、磁盘 IO 阻塞等真实失败 -
DECLARE CONTINUE HANDLER FOR NOT FOUND只能放在游标声明之后、OPEN之前,放错位置会导致 handler 不生效 - 循环结束必须
CLOSE游标,否则连接资源泄漏,长期运行会触发max_prepared_stmt_count超限
为什么 LIMIT 1000 的 UPDATE 比游标快一个数量级?
因为它是集合操作:引擎一次定位、批量加锁、单次日志刷写;而游标是把这过程拆成百万次独立执行路径。
- 95% 的所谓“逐行逻辑”,其实可以改写为带条件的
UPDATE ... WHERE ... LIMIT 1000,配合外部脚本循环调用 - 需要拆字段(如逗号分隔值)或多行展开,优先用
CREATE TEMPORARY TABLE+JOIN或INSERT ... SELECT替代 - 真要调用外部服务或写审计日志,把这些动作抽到应用层,数据库只负责提供 ID 列表和上下文数据
- 游标不是“高级功能”,是“兜底方案”——它的存在意义,是让你知道“这条路虽然难走,但至少能通”,而不是鼓励你选它


















