根本原因是DECLARE CONTINUE HANDLER FOR NOT FOUND作用域全局,任一FETCH遇空即设done=TRUE,致外层误判退出;须为内外层游标分别声明独立done变量、专属handler,并在内层OPEN前重置其done为FALSE。

MySQL游标嵌套时,为什么外层循环总被内层FETCH提前终止?
根本原因是 DECLARE CONTINUE HANDLER FOR NOT FOUND 是作用域全局的——它不区分内外层游标,只要任意一个 FETCH 遇到结果集末尾,就会触发并设置 done = TRUE,导致外层循环也误判退出。
实操建议:
- 每个游标必须配独立的状态变量(如
done_outer、done_inner),不能共用同一个done - 每个游标声明后,必须紧跟专属的
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done_xxx = TRUE - 内层游标
OPEN前,记得重置其对应done_inner = FALSE;否则上一次循环残留的TRUE会让本次FETCH直接跳过 - 避免在内层循环中
LEAVE外层标签(如LEAVE outer_loop)时遗漏CLOSE,否则外层游标未关闭会泄漏资源
LEAVE 标签命名必须显式声明,不能靠缩进或注释推断
MySQL 不识别代码缩进或中文注释里的“外层”“内层”,LEAVE 后面跟的必须是真实存在的标签名,且该标签需用 label_name: LOOP 或 label_name: BEGIN 显式定义。
常见错误写法:
outer_loop: LOOP FETCH cur_outer INTO @id; IF done_outer THEN LEAVE outer_loop; END IF; <p>inner_loop: LOOP FETCH cur_inner INTO @item; IF done_inner THEN LEAVE outer_loop; END IF; -- ❌ 错!outer_loop 在 inner_loop 作用域外不可见 END LOOP; END LOOP; </p>
正确做法:
- 用
LEAVE inner_loop跳出内层,再用ITERATE outer_loop或额外标志控制外层逻辑 - 若真需跨层跳出,把外层标签定义在更外层作用域(如整个存储过程体顶部)
- 标签名区分大小写,且不能与变量/游标同名,否则报错
ERROR 1305 (42000): PROCEDURE does not exist
嵌套游标里重复 OPEN 会直接报错:Cursor is not open
这不是警告,是硬性报错:ERROR 1326 (HY000): Cursor is not open。原因常被误读——实际触发点不是“没 OPEN”,而是“已 CLOSE 后又 OPEN”。MySQL 游标不支持 reopen。
典型踩坑场景:
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
- 内层循环结束时执行了
CLOSE cur_inner,但外层循环下一次迭代又直接OPEN cur_inner→ 报错 - 把
OPEN写在了外层循环开头,却忘了内层每次都要重新OPEN(因为上轮已CLOSE) - 错误地认为“游标像文件句柄一样可反复打开”,其实它是单次生命周期对象
安全写法:内层循环体开始前,固定三步 —— SET done_inner = FALSE; → OPEN cur_inner; → FETCH cur_inner INTO ...;
别用 COUNT(*) 预判游标行数,否则并发下逻辑就乱了
有人想绕开 NOT FOUND 句柄,改用 SELECT COUNT(*) INTO @cnt FROM ... 然后 WHILE @i 。这在单线程测试里看似可行,但生产环境必出问题。
原因很实在:
-
COUNT(*)和游标SELECT是两次独立查询,中间可能有其他事务 INSERT/DELETE 行,导致计数和实际游标结果不一致 - 游标基于快照读(取决于隔离级别),而
COUNT(*)是当前一致性读,二者看到的数据版本可能不同 - 多一次全表扫描,性能反而更差,尤其大表
真正可靠的终止方式只有一种:严格依赖 NOT FOUND 句柄 + 独立 done 标志。这是 MySQL 游标设计的硬约束,不是优化建议。
最易被忽略的一点:所有 FETCH 必须紧接在对应 OPEN 之后执行第一次,否则 done 永远不会被置为 TRUE,循环变成死循环——这个细节没有日志、不报错,只能靠人工核对流程。










