1. 为什么存储过程里需要游标:从一次批量对账说起
MySQL 的 SQL 是面向集合的语言,一条UPDATE或SELECT默认处理的是整张表、整个结果集。但真实业务里经常遇到「必须一行一行处理」的场景:比如给每个学生按年龄打不同标签、逐条把订单金额累加到汇总表、对每条记录调用不同的分支逻辑。这时候普通的SELECT拿不到「第一行、下一行、前十行」,你需要一个能定位到结果集某一行、并且能顺序往后走的工具——这就是游标(Cursor,也叫光标)。
游标本质上是一个标识,用来标记「数据取到哪儿了」。如果你写过 Java 或 Python,可以把它理解成数组遍历时的下标i:FETCH一次,下标往后挪一格,直到越界触发结束条件。MySQL 的游标有个重要限制:它只能用在存储过程或函数里,不能像某些数据库那样在普通 SQL 里直接开游标。所以本文的所有示例都围绕存储过程展开。
这篇文章面向三类人:刚学 MySQL 存储过程、被游标语法绕晕的初学者;需要写逐行处理逻辑、但不确定HANDLER怎么配的后端同学;以及正在用 TaoToken 统一 Key/API 通道调用大模型、想让 AI 帮忙生成和调试存储过程的开发者。我会把声明、打开、提取、关闭四步拆开讲,再给出 Loop、While、Repeat 三种循环的完整可复制代码,最后用 TaoToken 的 API 通道做一次「让模型帮我审游标代码」的验证。
先明确一个核心检索词:MySQL 存储过程游标逐行处理结果集,这是本篇要解决的主线问题。游标不是银弹,它逐行处理,性能天然不如集合操作,所以能用UPDATE ... WHERE解决的别用游标。但当你确实需要「读一行、判断、再写一行」时,它是存储过程里唯一的选择。
2. TaoToken 前置准备:统一 Key 与 API 通道怎么配
在写游标之前,先把调用环境搭好。TaoToken 的作用是提供一个统一的 Key 和 API 通道,让你在写存储过程、调试 SQL、让模型帮忙审查代码时,不用在多个模型供应商之间来回切换配置。官网入口是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 基址是 https://taotoken.net/api (这个地址不加 UTM 参数)。
你需要准备三件套:Base URL、API Key、Model ID。这三样在 TaoToken 控制台都能拿到。Base URL 填https://taotoken.net/api,API Key 在控制台的 API Keys 页面生成,Model ID 按你实际要用的模型填。这三件套是后面所有配置的基础,缺一不可。
如果你用的是 Claude Code 这类命令行工具,配置方式是在项目里建一个 settings 文件,把 Base URL 和 Key 写进去。如果你用的是 Cline 这类带 MCP 的编辑器插件,需要在 MCP 配置里填同样的三件套。Codex 用户则在auth.json里配置。不管哪种工具,核心都是那三样:Base URL、Key、Model ID。
我试过把这套配置用在「让模型审查游标代码」的场景:把存储过程贴给模型,让它检查HANDLER有没有漏、FETCH顺序对不对、变量字符集有没有设。模型能快速指出DECLARE CONTINUE HANDLER FOR NOT FOUND必须放在游标声明之后、变量声明之前这类顺序问题。这个顺序是 MySQL 的硬性要求,写反了直接报错。
配置完成后,你可以先用模型对话功能验证通道是否通:打开 https://taotoken.net/api 对应的对话入口,发一句「MySQL 游标 FETCH 后为什么要判断 done」,看是否正常返回。通了再往下写存储过程。如果你打算长期用模型辅助写 SQL 和存储过程,可以考虑 Coding Plan,把日常的代码审查、生成、排障都走同一条通道,省得每次重新配。
这里要提醒一句:TaoToken 是 API 通道,不是数据库工具,它不会替你执行 SQL。存储过程最终还是在你的 MySQL 客户端(命令行、Navicat、DBeaver 都行)里跑。TaoToken 负责的是「让 AI 帮你写和查」,MySQL 负责「真正执行」。
3. 可复制配置:游标四步 + 三种循环完整代码
这一节是全文的核心,所有代码都可以直接复制到 MySQL 客户端执行。先建测试表和数据,再写存储过程。
先建一张student表并插入数据:
CREATE TABLE student( stuId INT PRIMARY KEY AUTO_INCREMENT, stuName VARCHAR(20), stuSex VARCHAR(2), stuAge INT ) DEFAULT CHARSET=utf8; INSERT INTO student(stuName, stuSex, stuAge) VALUES ('小明','男',20), ('小花','女',19), ('大赤','男',20), ('可乐','男',19), ('莹莹','女',19);游标的使用固定是四步:DECLARE 声明 → OPEN 打开 → FETCH 提取 → CLOSE 关闭。声明时必须用DECLARE cursor_name CURSOR FOR select_statement;,而且游标声明要放在变量声明之后、HANDLER 声明之前。打开游标时它并不指向第一条记录,而是指向第一条记录的前边,第一次FETCH才拿到第一行。
下面是一个最基础的存储过程,只取一条记录:
DELIMITER // CREATE PROCEDURE p1() BEGIN DECLARE id INT; DECLARE name VARCHAR(100) CHARACTER SET utf8; DECLARE done INT DEFAULT 0; DECLARE mc CURSOR FOR SELECT stuId, stuName FROM student WHERE stuAge > 19; DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1; OPEN mc; FETCH mc INTO id, name; SELECT id, name; CLOSE mc; END // DELIMITER ;注意DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;这一行,它是游标循环的「刹车」。当FETCH越界(没有更多行了),MySQL 会抛一个NOT FOUND条件,这个 HANDLER 捕获它并把done置 1,循环靠done判断是否退出。没有它,循环会死。
接下来是三种循环写法的完整版,都用来统计年龄大于 19 的记录数。
Loop 循环:
DELIMITER // CREATE PROCEDURE p2() BEGIN DECLARE id, age, total INT; DECLARE name, sex VARCHAR(20) CHARACTER SET utf8; DECLARE done INT DEFAULT 0; DECLARE cur CURSOR FOR SELECT stuId, stuName, stuSex, stuAge FROM student WHERE stuAge > 19; DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1; OPEN cur; SET total = 0; xxx: LOOP FETCH cur INTO id, name, sex, age; IF done = 1 THEN LEAVE xxx; END IF; SET total = total + 1; END LOOP; CLOSE cur; SELECT total; END // DELIMITER ;While 循环:
DELIMITER // CREATE PROCEDURE p3() BEGIN DECLARE id, age, total INT; DECLARE name, sex VARCHAR(20) CHARACTER SET utf8; DECLARE done INT DEFAULT 0; DECLARE cur CURSOR FOR SELECT stuId, stuName, stuSex, stuAge FROM student WHERE stuAge > 19; DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1; OPEN cur; SET total = 0; WHILE done != 1 DO FETCH cur INTO id, name, sex, age; IF done != 1 THEN SET total = total + 1; END IF; END WHILE; CLOSE cur; SELECT total; END // DELIMITER ;Repeat 循环:
DELIMITER // CREATE PROCEDURE p4() BEGIN DECLARE id, age, total INT; DECLARE name, sex VARCHAR(20) CHARACTER SET utf8; DECLARE done INT DEFAULT 0; DECLARE cur CURSOR FOR SELECT stuId, stuName, stuSex, stuAge FROM student WHERE stuAge > 19; DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1; OPEN cur; SET total = 0; REPEAT FETCH cur INTO id, name, sex, age; IF done != 1 THEN SET total = total + 1; END IF; UNTIL done = 1 END REPEAT; CLOSE cur; SELECT total; END // DELIMITER ;三种写法结果一样,区别在循环结构:Loop 靠LEAVE跳出,While 先判断后执行,Repeat 先执行后判断。选哪个看个人习惯,我一般用 Loop,因为LEAVE xxx的退出点最直观。
如果你想让模型帮你生成这类代码,可以把上面任意一段贴进 TaoToken 的模型对话,让它改成「统计每个性别的人数」或「把结果写入汇总表」。模型会保留四步结构和 HANDLER,只改 SELECT 和循环体。这就是统一通道的价值:同一套 Key,随时调模型改代码。
4. 验证请求与成功结果:跑一遍看输出
代码写完必须验证。在 MySQL 客户端里依次执行建表、插入、建存储过程,然后调用:
CALL p2(); CALL p3(); CALL p4();三个存储过程都应该返回total = 3,因为年龄大于 19 的记录是「小明 20、大赤 20」,等等——这里要仔细看:stuAge > 19会匹配 20 岁的记录,小明和大赤是 20,小花、可乐、莹莹是 19,所以实际匹配 2 条。如果你插入的数据不同,结果会变。跑之前先SELECT * FROM student WHERE stuAge > 19;确认一下预期条数,再和存储过程输出对比。
验证时重点看三件事:第一,CALL有没有报错;第二,返回的total是否等于你手算的条数;第三,把SELECT换成SELECT COUNT(*)做交叉验证:
SELECT COUNT(*) FROM student WHERE stuAge > 19;如果存储过程返回 2,COUNT(*)也返回 2,说明游标逻辑正确。如果存储过程返回 0 或报错,往下看排障章节。
再验证一个「逐行处理」的真实场景:把每条记录的名字拼成一个字符串。这能证明游标确实在逐行走:
DELIMITER // CREATE PROCEDURE p5() BEGIN DECLARE name VARCHAR(20) CHARACTER SET utf8; DECLARE done INT DEFAULT 0; DECLARE all_names VARCHAR(200) DEFAULT ''; DECLARE cur CURSOR FOR SELECT stuName FROM student WHERE stuAge > 19; DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1; OPEN cur; read_loop: LOOP FETCH cur INTO name; IF done = 1 THEN LEAVE read_loop; END IF; SET all_names = CONCAT(all_names, name, ','); END LOOP; CLOSE cur; SELECT all_names; END // DELIMITER ;CALL p5();应该返回类似小明,大赤,的字符串。这个例子最能体现游标的意义:集合操作很难直接做「逐行拼接」,而游标天然适合。
如果你在验证过程中想让模型帮你解释某段输出,可以把CALL的结果和存储过程代码一起贴到 TaoToken 模型对话里,问「为什么 total 是 2 不是 3」。模型会帮你核对 WHERE 条件和数据。这一步不需要额外配置,用第 2 节配好的通道直接发就行。
5. 本篇常见错排查:401、HANDLER 顺序、字符集乱码
游标报错集中在几类,逐个对照。
第一类:DECLARE顺序错。MySQL 要求存储过程里声明顺序是:变量 → 游标 → HANDLER。如果你把DECLARE CONTINUE HANDLER写在游标前面,会报ERROR 1337 (42000): Variable or condition declaration after cursor or handler declaration。解决办法就是把 HANDLER 挪到游标声明之后。这个顺序我踩过坑,调了半天才发现是顺序问题。
第二类:FETCH后不判断 done,死循环。如果忘了IF done = 1 THEN LEAVE xxx; END IF;,循环会一直跑,直到客户端超时或内存爆掉。记住:FETCH越界不会自动停,必须靠 HANDLER 置位 + 循环内判断。
第三类:中文乱码。接收中文的变量必须指定字符集,写成DECLARE name VARCHAR(20) CHARACTER SET utf8;。不写的话,FETCH出来的中文可能是问号。表本身也要DEFAULT CHARSET=utf8。
第四类:调用 TaoToken 通道时的 401。如果你在让模型审查代码时遇到401 Unauthorized,先检查 API Key 有没有填对、有没有过期。Base URL 必须是https://taotoken.net/api,Key 从控制台 API Keys 页面复制,别多空格。如果报local proxy failed,说明本地网络到通道的连接有问题,检查网络设置,别用任何非正规的网络工具。如果报reading choices相关错误,通常是返回体解析问题,确认 Model ID 填的是通道支持的模型。
第五类:OAuth 或认证失败。如果你用 Claude Code 或 Codex 这类工具,认证走的是 OAuth 或auth.json,检查配置文件里的 Base URL、Key、Model ID 三件套是否齐全。缺任何一个都会认证失败。Cline 的 MCP 配置同理,三件套必须完整。
第六类:游标关闭后还用。CLOSE cur;之后再FETCH会报错。一个游标关闭后,要么重新OPEN,要么重新声明。不显式关闭的话,MySQL 在END时会自动关,但显式关闭是好习惯,能及时释放内存。
排查时如果拿不准,把完整报错贴给模型,让它对照 MySQL 错误码解释。TaoToken 通道在这里的作用是「随时有个懂 MySQL 的助手」,不用自己翻文档。排障相关的入口建议走 API Keys 和接入文档,先把通道配稳,再谈调模型。
6. 把游标用对:什么时候该用,什么时候别用
游标能解决逐行处理,但它有明确的适用边界。能用集合操作解决的,优先用集合操作。比如「把年龄大于 19 的记录数统计出来」,直接SELECT COUNT(*)比开游标快几个数量级。游标的价值在于「读一行、做判断、再写一行」这种集合 SQL 表达起来很别扭的逻辑,比如逐行调用不同的分支、逐行拼接字符串、逐行写入日志表。
性能上要有个预期:游标是逐行 FETCH,数据量大时明显慢于批量操作。如果结果集上万行,先想想能不能用JOIN或UPDATE ... WHERE替代。真要用游标,尽量缩小SELECT的范围,别SELECT *全表。
最后给一个实用技巧:写完游标存储过程,先用小数据集(比如 5 行)验证逻辑,确认total和COUNT(*)一致,再放到生产数据上跑。验证通过后,把存储过程代码和测试数据一起存进版本库,下次改的时候有对照。如果你用 TaoToken 的 Coding Plan 做长期开发,可以把这些存储过程纳入模型的上下文,让它在你改代码时提醒「这个游标忘了关」或「HANDLER 顺序反了」。通道配一次,后面写 SQL、审代码、排错都走同一条路,省去反复切换的麻烦。