MySQL 游标全解:声明游标、异常处理与条件处理程序实战
发布时间:2026/10/1 16:14:40来源:尧图网络
1. 为什么存储过程里的游标总在“最后一行”翻车先聊一个我见过太多次的场景你写了个存储过程想遍历一张订单表逐行做金额校验或者写日志。代码看着没问题LOOP里FETCH一行处理一行跑起来却发现——要么少处理了最后一条要么直接卡死循环出不来要么报个1329 No data - zero rows fetched。问题几乎都出在同一处游标走到结果集末尾时FETCH会抛出一个NOT FOUND条件而你没有正确接住它。MySQL 的游标不像某些语言的迭代器会返回一个“结束标志”它是靠异常机制告诉你“没数据了”。所以游标和异常处理是绑在一起的两件事拆开讲都不完整。这篇就按“声明游标 → 打开 → 逐行 FETCH → 关闭”的完整链路走一遍重点拆NOT FOUND和CONTINUE/EXIT条件处理程序怎么配合避免循环提前退出或者死循环。适合已经会写基础存储过程、但一用游标就心里没底的同学。全程用可复制的建表、样例数据和存储过程骨架你可以直接贴进客户端跑。核心检索词先摆出来MySQL 游标Cursor是存储过程里逐行处理结果集的对象它能做什么——把一条SELECT的结果一行行取出来单独加工适合谁——需要在数据库层做逐行计算、批量迁移、逐条写审计日志的开发者。下面所有示例基于 MySQL 8.05.7 也基本通用。2. 前置准备建表、样例数据与 TaoToken 接入环境动手之前先把“舞台”搭好。游标操作依赖一张有数据的表我建一张简单的员工薪资表字段少、数据直观方便你观察每一行被处理的顺序。-- 建库可选 CREATE DATABASE IF NOT EXISTS cursor_demo DEFAULT CHARSET utf8mb4; USE cursor_demo; -- 建表 DROP TABLE IF EXISTS employees; CREATE TABLE employees ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, dept VARCHAR(30) NOT NULL, salary DECIMAL(10,2) NOT NULL, bonus DECIMAL(10,2) DEFAULT 0.00 ) ENGINEInnoDB; -- 样例数据 INSERT INTO employees (name, dept, salary) VALUES (张伟, 研发, 18000.00), (李娜, 研发, 22000.00), (王强, 销售, 12000.00), (赵敏, 销售, 15000.00), (陈晨, 运营, 11000.00);跑完SELECT * FROM employees;应该看到 5 行。这 5 行就是后面游标要逐行“啃”的数据。如果你习惯在命令行或图形客户端里调试直接用本地 MySQL 就行。但如果你想让 AI 帮你生成、改写、解释这些存储过程或者边写边问“这个 HANDLER 为什么这么写”可以配一个稳定的模型调用入口。我平时用 TaoToken 做这类辅助它的 API 地址是https://taotoken.net/api官网在https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content。拿 Key 的入口在控制台的 API Keys 页面https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite。这里要强调一点TaoToken 是模型调用通道不是数据库工具也不替代你的 MySQL 客户端。它的作用是当你写游标逻辑卡住时把报错和代码贴给模型让它帮你定位。真正执行 SQL 的还是你自己的 MySQL。配好之后建议先做一次最小连通性验证确认 Key 和 Base URL 没问题再进入游标实战。验证方式在下一节给可复制的配置片段。3. 可复制配置游标骨架 条件处理程序完整写法这一节是全文的核心给你一份能直接跑的存储过程骨架把声明游标、打开、FETCH、关闭、NOT FOUND处理、CONTINUE与EXIT的区别全部串起来。先看最容易踩坑的写法很多人第一次写游标是这样的DELIMITER $$ CREATE PROCEDURE bad_cursor_demo() BEGIN DECLARE v_id INT; DECLARE v_name VARCHAR(50); DECLARE cur CURSOR FOR SELECT id, name FROM employees; OPEN cur; read_loop: LOOP FETCH cur INTO v_id, v_name; SELECT v_id, v_name; -- 处理数据 END LOOP; CLOSE cur; END$$ DELIMITER ;这段代码的问题LOOP没有退出条件FETCH到末尾后继续取MySQL 会抛NOT FOUND但你没接住循环不会自己停。结果要么报错中断要么在某些版本下行为诡异。游标循环必须有一个“结束标志”而这个标志只能靠条件处理程序来设置。正确骨架如下注意done变量和CONTINUE HANDLER的配合DELIMITER $$ DROP PROCEDURE IF EXISTS process_employees$$ CREATE PROCEDURE process_employees() BEGIN -- 1. 声明局部变量 DECLARE done INT DEFAULT FALSE; DECLARE v_id INT; DECLARE v_name VARCHAR(50); DECLARE v_salary DECIMAL(10,2); -- 2. 声明游标 DECLARE emp_cursor CURSOR FOR SELECT id, name, salary FROM employees ORDER BY id; -- 3. 声明条件处理程序捕获 NOT FOUND设置结束标志 DECLARE CONTINUE HANDLER FOR NOT FOUND SET done TRUE; -- 4. 打开游标 OPEN emp_cursor; -- 5. 循环逐行读取 read_loop: LOOP FETCH emp_cursor INTO v_id, v_name, v_salary; IF done THEN LEAVE read_loop; END IF; -- 这里写你的逐行业务逻辑 UPDATE employees SET bonus v_salary * 0.10 WHERE id v_id; SELECT CONCAT(已处理: , v_name, 薪资, v_salary) AS msg; END LOOP; -- 6. 关闭游标 CLOSE emp_cursor; END$$ DELIMITER ;调用并查看结果CALL process_employees(); SELECT * FROM employees;你应该看到 5 条已处理消息且bonus列被逐行更新为薪资的 10%。现在拆解关键点。DECLARE CONTINUE HANDLER FOR NOT FOUND SET done TRUE;这行的意思是当任何FETCH取不到数据触发NOT FOUND时不中断程序而是把done置为TRUE然后继续执行下一句。下一句正好是IF done THEN LEAVE read_loop;于是循环干净退出。这就是“避免死循环”的标准姿势。CONTINUE和EXIT的区别必须讲清楚这是异常处理的分水岭处理程序类型触发后行为典型用途CONTINUE HANDLER执行处理代码后继续执行触发语句的下一句游标NOT FOUND设标志、忽略可恢复警告EXIT HANDLER执行处理代码后终止当前BEGIN...END块严重错误回滚、参数非法直接退出如果你把上面的CONTINUE换成EXIT会怎样FETCH到末尾触发NOT FOUNDEXIT HANDLER直接终止整个存储过程块CLOSE emp_cursor都不会执行游标资源没释放而且你拿不到“正常处理完”的收尾逻辑。所以游标结束标志必须用CONTINUEEXIT留给真正的异常。再补一个EXIT HANDLER处理SQLEXCEPTION的写法用于兜底DELIMITER $$ CREATE PROCEDURE safe_process_employees() BEGIN DECLARE done INT DEFAULT FALSE; DECLARE v_id INT; DECLARE v_name VARCHAR(50); DECLARE v_salary DECIMAL(10,2); DECLARE emp_cursor CURSOR FOR SELECT id, name, salary FROM employees ORDER BY id; DECLARE CONTINUE HANDLER FOR NOT FOUND SET done TRUE; -- 兜底任何 SQL 异常都回滚并退出 DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; SELECT 发生异常已回滚 AS err_msg; END; START TRANSACTION; OPEN emp_cursor; read_loop: LOOP FETCH emp_cursor INTO v_id, v_name, v_salary; IF done THEN LEAVE read_loop; END IF; UPDATE employees SET bonus v_salary * 0.10 WHERE id v_id; END LOOP; CLOSE emp_cursor; COMMIT; END$$ DELIMITER ;注意EXIT HANDLER放在CONTINUE HANDLER之后、START TRANSACTION之前顺序不能乱——MySQL 要求变量和游标声明在前处理程序声明在后而处理程序之间也有优先级。如果你用 AI 辅助生成这类存储过程可以把 Base URL 配成https://taotoken.net/apiKey 用控制台生成的模型 ID 选一个擅长 SQL 的即可。三件套Base URL Key Model ID缺一不可少一个就会在调用时报鉴权或路由错误。模型对话入口在https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite接入文档在https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite。4. 验证请求逐行执行结果与成功标志配置写完必须验证它真的按预期逐行跑完而不是“看起来没报错”。我习惯用三步验证法。第一步看逐行输出。调用CALL process_employees();后客户端会返回 5 行msg顺序应该是按id升序张伟、李娜、王强、赵敏、陈晨。如果只出现 4 行说明最后一行被done提前拦掉了——这是把IF done判断放在FETCH之前的典型错误。正确顺序永远是先 FETCH再判断 done。第二步看数据落库。执行SELECT id, name, salary, bonus FROM employees ORDER BY id;bonus应该等于salary * 0.105 行全部更新。如果只有前 4 行有值回到上一步找原因。第三步看游标是否正常关闭。连续调用两次CALL process_employees();如果第二次报Cursor already open之类的错误说明第一次没走到CLOSE。正常情况下两次都能跑完因为每次调用都是独立的存储过程实例。再给一个更直观的验证把逐行处理改成往日志表插记录这样能精确看到每一行的处理时间戳。DROP TABLE IF EXISTS cursor_log; CREATE TABLE cursor_log ( log_id INT PRIMARY KEY AUTO_INCREMENT, emp_id INT, emp_name VARCHAR(50), log_time DATETIME DEFAULT CURRENT_TIMESTAMP ); DELIMITER $$ CREATE PROCEDURE log_employees() BEGIN DECLARE done INT DEFAULT FALSE; DECLARE v_id INT; DECLARE v_name VARCHAR(50); DECLARE cur CURSOR FOR SELECT id, name FROM employees ORDER BY id; DECLARE CONTINUE HANDLER FOR NOT FOUND SET done TRUE; OPEN cur; read_loop: LOOP FETCH cur INTO v_id, v_name; IF done THEN LEAVE read_loop; END IF; INSERT INTO cursor_log (emp_id, emp_name) VALUES (v_id, v_name); END LOOP; CLOSE cur; END$$ DELIMITER ; CALL log_employees(); SELECT * FROM cursor_log ORDER BY log_id;预期结果cursor_log里正好 5 条emp_id从 1 到 5 连续。这个验证比SELECT输出更硬因为它证明了每一行都真实进入了循环体。如果你在 AI 辅助下调试可以把cursor_log的查询结果贴给模型让它帮你确认“是否所有行都被处理”。模型对话入口https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite可以直接用。长期做这类数据库脚本开发、需要反复生成和校验存储过程的可以考虑 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite适合把 AI 嵌进日常编码流。5. 常见报错排查1329、401、local proxy failed 与 OAuth 类问题游标相关的报错其实就那么几个但每个都能让人卡半天。我按“数据库内报错”和“AI 辅助调用报错”分开列。数据库侧ERROR 1329 (02000): No data - zero rows fetched, selected, or processed。这是最经典的游标报错意思是FETCH时结果集已经空了。根因几乎都是没声明CONTINUE HANDLER FOR NOT FOUND或者声明了但done判断位置不对。检查两点HANDLER 是否在游标声明之后、IF done是否紧跟在FETCH之后。ERROR 1336: Duplicate handler declaration。同一个条件声明了两次处理程序。比如你既写了CONTINUE HANDLER FOR NOT FOUND又写了EXIT HANDLER FOR NOT FOUNDMySQL 不允许。一个条件只能有一个处理程序。ERROR 1337: Variable or condition declaration after cursor or handler declaration。声明顺序错了。MySQL 强制顺序是变量 → 游标 → 处理程序。把DECLARE done放到游标后面就会报这个。ERROR 1305: PROCEDURE does not exist。多半是DELIMITER没改回来或者CREATE PROCEDURE没执行成功。检查DELIMITER $$和结尾$$是否配对。AI 辅助调用侧401 Unauthorized。Key 错了、过期了或者请求头里没带对。检查Authorization: Bearer 你的KeyKey 从https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite重新生成一个再试。local proxy failed/ 连接超时。通常是 Base URL 写错或者本地网络到 API 端点不通。确认 Base URL 是https://taotoken.net/api不要多加路径后缀。如果公司网络有限制换网络环境再试。reading choices类解析错误。一般是模型返回格式和客户端预期不一致或者模型 ID 填错。确认 Model ID 是平台支持的名称别自己拼。OAuth相关报错。多见于用第三方客户端比如某些 IDE 插件接入时认证方式选成了 OAuth 而不是 API Key。改成 API Key 模式填 Base URL Key Model ID 三件套。排查顺序建议先确认数据库侧存储过程能单独跑通再确认 AI 调用侧连通两边分开定位别混在一起查。接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite里有各客户端的配置示例对着改比瞎试快。6. 把游标用对从能跑到跑得稳的几条经验游标这东西能跑通只是及格线跑得稳、不拖垮数据库才是目标。最后分享几条我实际踩出来的经验。第一能用集合操作就别用游标。游标是逐行的UPDATE ... JOIN或INSERT ... SELECT是集合的后者通常快一个数量级。只有当你需要“根据上一行的结果决定下一行怎么处理”这种行间依赖时游标才不可替代。上面bonus salary * 0.10的例子其实一条UPDATE就能搞定我用游标只是为了演示链路。第二done标志的初始值用FALSE而不是0。虽然等价但FALSE可读性更好团队协作时少一次“这是布尔还是数字”的犹豫。第三循环标签别省。read_loop: LOOP ... LEAVE read_loop;里的标签在嵌套循环时是救命的。如果你有两层游标嵌套没有标签根本没法精确LEAVE到外层。第四CLOSE放在LEAVE之后、END之前。别放在循环体内也别因为EXIT HANDLER触发就跳过。如果担心异常路径漏掉CLOSE可以在EXIT HANDLER里也补一句CLOSE但要注意游标未打开时CLOSE会报错得配合状态判断。第五大批量处理要分批。游标一次性把整个结果集拉进内存几百万行会直接把内存打满。加LIMIT分批或者用主键范围切片每次只处理几千行。第六调试期加SELECT输出上线前删掉。逐行SELECT在调试时很有用但生产环境每行一次网络往返性能灾难。用日志表替代或者干脆只在开发库开。把这几条和前面的骨架结合起来你的游标基本就不会再出现“少一行”“死循环”“资源不释放”这三类问题了。真正卡住的时候把报错原文和存储过程完整代码一起丢给模型比只贴一行报错高效得多——模型能看到声明顺序、HANDLER 类型、done判断位置这些上下文定位会准很多。
网站建设高端定制企业官网