
1. MySQL 游标是什么逐行处理结果集的存储过程实战场景MySQL 游标CURSOR是存储过程里用来逐行读取结果集的机制。你可以把它理解成 Java 里的迭代器一条 SELECT 可能返回 N 行游标就是在这 N 行上“游动”的句柄每次 FETCH 取一行取完为止。它适合的场景很明确——需要对每一行做独立逻辑处理比如按行计算、逐行写日志表、逐行调用其他存储过程、逐行做条件分支更新。不适合的场景同样明确能用一条 UPDATE ... JOIN 或 INSERT ... SELECT 搞定的批量操作就别用游标因为游标是逐行处理性能远低于集合操作。我见过不少后端同学第一次写游标存储过程卡在三个地方一是变量声明顺序报错二是循环结束条件写错导致多读一行或死循环三是 NOT FOUND 处理器用 CONTINUE 还是 EXIT 分不清。这篇就按“声明 → 打开 → 逐行读取 → 关闭”的完整链路把可复制的存储过程示例、执行验证步骤、以及常见报错排查一次讲透。你跟着敲一遍基本就能在自己的业务库里落地。先明确游标的四个动作语法骨架如下DECLARE 游标名 CURSOR FOR SELECT语句; OPEN 游标名; FETCH 游标名 INTO 变量1, 变量2, ...; CLOSE 游标名;这里有个硬性规则所有 DECLARE 必须写在 BEGIN ... END 块的最前面顺序是「变量声明 → 游标声明 → 处理器声明」。顺序错了直接报ERROR 1337 (42000): Variable or condition declaration after cursor or handler declaration。这个坑后面排障章节会详细说。下面用一张商品表 goods 贯穿全文字段是 gid、gname、num。建表和造数据CREATE TABLE goods ( gid INT PRIMARY KEY, gname VARCHAR(100), num INT ); INSERT INTO goods VALUES (1, ipad, 600), (2, 笔记本电脑, 500), (3, 牙膏, 200), (4, 六神花露水, 800);目标写一个存储过程逐行读出每条商品记录并打印。这就是最基础的游标实战也是理解后续循环写法的地基。2. TaoToken 前置准备给游标调试配一个稳定的模型辅助环境写存储过程时报错信息往往只有一行比如ERROR 1329 (02000): No data - zero rows fetched光看这行很难判断是游标越界还是 SELECT 本身没数据。这时候有个能对话的模型帮你分析报错、生成对照 SQL效率会高很多。我用 TaoToken 的模型对话来做这件事它把多个主流模型聚在一个入口调试 SQL 时切换模型对比解释挺方便。TaoToken 的定位是模型调用聚合平台官网在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。它能做什么统一 API 入口调用不同模型适合需要对比模型输出、或者做 coding 辅助的场景。适合谁后端开发者、需要写 SQL/存储过程时想要即时解释和排错的人。前置准备分两步。第一步拿到 API Key。进入控制台创建密钥地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 在 API Keys 页面生成密钥形如sk-开头的一串字符复制保存好后面配置要用。第二步确认你要用的模型 ID比如做代码解释常用的模型在模型列表里能看到对应的 Model ID。如果你是用 Claude Code 这类命令行工具做开发辅助TaoToken 也提供了对应的接入方式文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。配置的核心三件套永远是Base URL、API Key、Model ID。Base URL 用 https://taotoken.net/api 注意这个地址不带 UTM 参数是纯 API 端点。Key 就是刚才控制台生成的Model ID 按你选的模型填。这里要提醒一句TaoToken 是帮你调用模型的工具不是数据库客户端它不替代 MySQL 本身。游标逻辑还是得在 MySQL 里跑TaoToken 负责在你卡住时帮你读报错、给思路。两者配合调试效率会高不少。3. 可复制配置游标存储过程完整写法与循环模板这一节给可直接复制的代码。先看最朴素的单次 FETCH 版本理解游标怎么打开和取值DELIMITER $ CREATE PROCEDURE p14() BEGIN DECLARE temp_gid INT; DECLARE temp_gname VARCHAR(100); DECLARE temp_num INT; DECLARE getGoods CURSOR FOR SELECT gid, gname, num FROM goods; OPEN getGoods; FETCH getGoods INTO temp_gid, temp_gname, temp_num; SELECT temp_gid, temp_gname, temp_num; CLOSE getGoods; END$ DELIMITER ; CALL p14();执行后只输出第一行1 | ipad | 600。这说明游标打开后停在第 0 行之前第一次 FETCH 才移动到第 1 行。想取第二行就再 FETCH 一次但硬编码 FETCH 次数显然不现实所以需要循环。用 WHILE 循环的版本先查总数再按次数循环DELIMITER $ CREATE PROCEDURE p16() BEGIN DECLARE temp_gid INT; DECLARE temp_gname VARCHAR(100); DECLARE temp_num INT; DECLARE rows INT DEFAULT 0; DECLARE myIndex INT DEFAULT 0; DECLARE getGoods CURSOR FOR SELECT gid, gname, num FROM goods; SELECT COUNT(*) INTO rows FROM goods; OPEN getGoods; WHILE myIndex rows DO SET myIndex : myIndex 1; FETCH getGoods INTO temp_gid, temp_gname, temp_num; SELECT temp_gid, temp_gname, temp_num; END WHILE; CLOSE getGoods; END$ DELIMITER ; CALL p16();这个版本能跑通但有个隐患COUNT(*) 和游标 SELECT 之间如果数据被改动行数会对不上。更稳的做法是用 NOT FOUND 处理器让游标自己告诉你“取完了”。用 CONTINUE HANDLER 的版本DELIMITER $ CREATE PROCEDURE p17() BEGIN DECLARE temp_gid INT DEFAULT 666; DECLARE temp_gname VARCHAR(100) DEFAULT 空空如也; DECLARE temp_num INT DEFAULT 888; DECLARE flag INT DEFAULT 1; DECLARE getGoods CURSOR FOR SELECT gid, gname, num FROM goods; DECLARE CONTINUE HANDLER FOR NOT FOUND SET flag : 0; OPEN getGoods; REPEAT FETCH getGoods INTO temp_gid, temp_gname, temp_num; SELECT temp_gid, temp_gname, temp_num; UNTIL flag 0 END REPEAT; CLOSE getGoods; END$ DELIMITER ; CALL p17();跑一下你会发现多输出了一行666 | 空空如也 | 888。原因是 CONTINUE 触发后后面的 SELECT 还是执行了把默认值打了出来。解决办法有两个把 CONTINUE 改成 EXIT或者调整 FETCH 和 SELECT 的顺序。EXIT 版本DELIMITER $ CREATE PROCEDURE p18() BEGIN DECLARE temp_gid INT DEFAULT 666; DECLARE temp_gname VARCHAR(100) DEFAULT 空空如也; DECLARE temp_num INT DEFAULT 888; DECLARE flag INT DEFAULT 1; DECLARE getGoods CURSOR FOR SELECT gid, gname, num FROM goods; DECLARE EXIT HANDLER FOR NOT FOUND SET flag : 0; OPEN getGoods; REPEAT FETCH getGoods INTO temp_gid, temp_gname, temp_num; SELECT temp_gid, temp_gname, temp_num; UNTIL flag 0 END REPEAT; CLOSE getGoods; END$ DELIMITER ; CALL p18();EXIT 触发后后面的语句不再执行所以不会多打那一行。这是最推荐的写法。如果你坚持用 CONTINUE那就把 FETCH 提到循环体开头SELECT 放后面先取再判断DELIMITER $ CREATE PROCEDURE p19() BEGIN DECLARE temp_gid INT DEFAULT 666; DECLARE temp_gname VARCHAR(100) DEFAULT 空空如也; DECLARE temp_num INT DEFAULT 888; DECLARE flag INT DEFAULT 1; DECLARE getGoods CURSOR FOR SELECT gid, gname, num FROM goods; DECLARE CONTINUE HANDLER FOR NOT FOUND SET flag : 0; OPEN getGoods; FETCH getGoods INTO temp_gid, temp_gname, temp_num; REPEAT SELECT temp_gid, temp_gname, temp_num; FETCH getGoods INTO temp_gid, temp_gname, temp_num; UNTIL flag 0 END REPEAT; CLOSE getGoods; END$ DELIMITER ; CALL p19();这个版本先 FETCH 一次循环里先 SELECT 再 FETCHNOT FOUND 触发时 flag 变 0循环退出不会多打。WHILE 版本同理DELIMITER $ CREATE PROCEDURE p20() BEGIN DECLARE temp_gid INT DEFAULT 666; DECLARE temp_gname VARCHAR(100) DEFAULT 空空如也; DECLARE temp_num INT DEFAULT 888; DECLARE flag INT DEFAULT 1; DECLARE getGoods CURSOR FOR SELECT gid, gname, num FROM goods; DECLARE CONTINUE HANDLER FOR NOT FOUND SET flag : 0; OPEN getGoods; FETCH getGoods INTO temp_gid, temp_gname, temp_num; WHILE flag 1 DO SELECT temp_gid, temp_gname, temp_num; FETCH getGoods INTO temp_gid, temp_gname, temp_num; END WHILE; CLOSE getGoods; END$ DELIMITER ; CALL p20();到这里四种循环写法WHILECOUNT、REPEATCONTINUE、REPEATEXIT、WHILECONTINUE你都拿到了。实际项目里我优先用 EXIT 版本代码短、不易错。4. 验证请求与成功结果执行存储过程并核对输出代码写完必须验证。按顺序执行下面几步确认游标行为符合预期。第一步确认表里有数据SELECT * FROM goods;应该返回 4 行。如果返回空先执行第 1 节的 INSERT。第二步调用 p18EXIT 版本CALL p18();预期输出 4 个结果集分别是1 | ipad | 600 2 | 笔记本电脑 | 500 3 | 牙膏 | 200 4 | 六神花露水 | 800没有多余的第 5 行说明 EXIT 处理器正确终止了循环。第三步测试空表边界。把数据清掉再调用TRUNCATE goods; CALL p18();预期不输出任何行也不报错。因为第一次 FETCH 就触发 NOT FOUNDflag 变 0循环体一次都没进。这一步很关键很多线上事故就是空表时游标处理不当导致的。第四步恢复数据再验证一次INSERT INTO goods VALUES (1, ipad, 600), (2, 笔记本电脑, 500), (3, 牙膏, 200), (4, 六神花露水, 800); CALL p18();输出应恢复为 4 行。第五步验证 SELECT 本身无结果的场景。把游标里的 WHERE 改成恒假条件DELIMITER $ CREATE PROCEDURE p22() BEGIN DECLARE temp_gid INT DEFAULT 666; DECLARE temp_gname VARCHAR(100) DEFAULT 空空如也; DECLARE temp_num INT DEFAULT 888; DECLARE flag INT DEFAULT 1; DECLARE getGoods CURSOR FOR SELECT gid, gname, num FROM goods WHERE 28 89; DECLARE CONTINUE HANDLER FOR NOT FOUND SET flag : 0; OPEN getGoods; FETCH getGoods INTO temp_gid, temp_gname, temp_num; WHILE flag 1 DO SELECT temp_gid, temp_gname, temp_num; FETCH getGoods INTO temp_gid, temp_gname, temp_num; END WHILE; CLOSE getGoods; END$ DELIMITER ; CALL p22();预期无输出。因为 SELECT 返回空集第一次 FETCH 就 NOT FOUND。如果你在调试过程中想让模型帮你解释某个报错可以把报错原文贴到模型对话里地址是 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 让它逐行分析存储过程逻辑。实测下来对于No data - zero rows fetched这类报错模型能快速指出是游标越界还是 SELECT 空集。5. 本篇常见错排查游标声明顺序、NOT FOUND 与多读一行这一节对照真实报错逐个拆。报错一ERROR 1337 (42000): Variable or condition declaration after cursor or handler declaration原因DECLARE 顺序错了。MySQL 要求变量声明在最前游标声明居中处理器声明最后。下面这种写法必报错-- 错误示范 DECLARE getGoods CURSOR FOR SELECT gid FROM goods; DECLARE temp_gid INT; -- 变量在游标后面报错正确顺序DECLARE temp_gid INT; DECLARE getGoods CURSOR FOR SELECT gid FROM goods; DECLARE CONTINUE HANDLER FOR NOT FOUND SET flag : 0;报错二ERROR 1329 (02000): No data - zero rows fetched, selected, or processed这个报错通常出现在没有声明 NOT FOUND 处理器、却 FETCH 到越界的时候。游标取完最后一行后再 FETCH就会抛这个。解决办法就是加DECLARE CONTINUE HANDLER FOR NOT FOUND或EXIT HANDLER。注意这个报错在存储过程里如果被处理器捕获就不会往外抛没捕获就会中断过程。报错三多输出一行默认值前面 p17 演示过CONTINUE 处理器触发后 SELECT 仍执行把666 | 空空如也 | 888打了出来。两种修法改 EXIT或调整 FETCH/SELECT 顺序。我建议直接改 EXIT最省心。报错四ERROR 1064 (42000): You have an error in your SQL syntax多半是 DELIMITER 没设对。存储过程体内有分号必须先用DELIMITER $把语句结束符改掉写完END$再改回DELIMITER ;。忘了改回来后续普通 SQL 会连着报错。报错五游标 SELECT 的列数和 FETCH INTO 变量数不一致比如 SELECT 三列FETCH INTO 只写了两个变量报ERROR 1222 (21000): The used SELECT statements have a different number of columns。数清楚列数一一对应。关于 CONTINUE / EXIT / UNDO 的区别这里说清楚CONTINUE 触发后后面的语句继续执行EXIT 触发后后面的语句不再执行UNDO 触发后撤销前面的语句但 MySQL 目前不支持 UNDO写了也没用。所以实际只有 CONTINUE 和 EXIT 两种选择。如果你在配置模型辅助环境时遇到 401 或 local proxy failed 这类报错先检查 API Key 是否复制完整、Base URL 是否写成 https://taotoken.net/api 。Key 管理入口在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 可以重新生成一个对比测试。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有完整的 Base URL Key Model ID 三件套说明。6. 游标与普通查询的边界以及长期编码的辅助方案游标不是万能药。判断标准很简单如果你的逻辑能用一条集合 SQL 表达就别用游标。比如“把所有 num 小于 300 的商品价格上调 10%”一条UPDATE goods SET num num * 1.1 WHERE num 300就完了用游标逐行 UPDATE 性能差一个数量级。游标真正该上的场景是逐行调用另一个存储过程、逐行写审计日志、逐行做复杂的条件分支且无法用 CASE WHEN 表达、或者需要按行维护某种状态机。还有一个容易忽略的点游标是只读向前的不能回退也不能跳过。取过的行就过去了。如果你需要反复遍历要么重新 OPEN要么换临时表方案。对于需要长期写存储过程、做数据库层开发的同学如果想让模型持续辅助你读代码、生成 SQL、解释执行计划可以考虑 Coding Plan入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。它适合把模型能力接进日常编码流程而不是每次临时开对话。配置时同样认准三件套Base URL 用 https://taotoken.net/api Key 从控制台拿Model ID 按需选。最后留一个实用技巧调试游标时在循环体里加一句SELECT CONCAT(当前行:, temp_gid)作为进度标记比只看最终结果更容易定位卡在哪一行。等逻辑跑通再删掉。这个习惯帮我省过不少排查时间。