MySql存储过程—游标使用(Cursor)遍历实战:从声明到循环的完整拆解
1. 为什么批量处理结果集时MySQL 存储过程游标遍历(Cursor)总踩坑先说清楚游标是什么、能做什么、适合谁。MySQL 存储过程里的游标Cursor就是一块指向结果集的“只读指针”它让你把SELECT出来的多行记录一行一行地取到变量里再对每一行做判断、计算、写日志、更新别的表。它适合的场景很具体批量对账、逐行校验、按行触发副作用比如给每个低库存商品写一条告警这些用一条UPDATE ... WHERE不好表达或者需要行级分支逻辑时游标就派上用场。但很多人第一次写游标就翻车报错集中在几个地方1329 - No data - zero rows fetched、DECLARE顺序报语法错、循环停不下来、FETCH之后变量还是旧值。根子在于游标的使用有严格的四步顺序——声明、打开、逐行取、关闭而且必须配一个NOT FOUND的CONTINUE HANDLER否则取到末尾时直接抛异常中断。我试过把游标当成 Java 的Iterator来理解思路就顺了DECLARE相当于拿到迭代器对象OPEN是初始化FETCH是next()CLOSE是释放。区别是 SQL 里没有hasNext()你得靠HANDLER捕获“没有下一行”这个条件来自己判断结束。这篇就按“建表 → 造数据 → 写过程 → 调用 → 验证 → 排错”的完整链路走一遍所有 SQL 都能直接复制执行。核心检索词就是 MySQL 存储过程游标遍历下面每一步我都会把可复制的代码贴全包括DELIMITER这种新手最容易漏的细节。先明确游标的三个硬约束这决定了你写代码时的边界游标是只读的不能通过它更新数据游标不能滚动只能单向向前不能回退或跳行不要在已经打开游标的表上做更新否则结果集行为不可预期。记住这三点能避开一大半逻辑坑。2. 前置准备建表、造测试数据与 TaoToken 环境说明在写游标之前得先把舞台搭好。我们建一张products表模拟商品库存再插几条数据其中故意留几条库存小于 100 的方便后面验证游标筛选逻辑。这段和业务无关纯粹是为了让游标有数据可遍历。CREATE DATABASE IF NOT EXISTS demo_cursor DEFAULT CHARACTER SET utf8mb4; USE demo_cursor; DROP TABLE IF EXISTS products; CREATE TABLE products ( id INT PRIMARY KEY AUTO_INCREMENT, code VARCHAR(32) NOT NULL, name VARCHAR(64) NOT NULL, quantity INT NOT NULL DEFAULT 0, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); INSERT INTO products (code, name, quantity) VALUES (P001, 机械键盘, 45), (P002, 无线鼠标, 180), (P003, 显示器支架, 12), (P004, USB集线器, 99), (P005, 笔记本支架, 260), (P006, 降噪耳机, 8);执行完SELECT * FROM products;应该看到 6 行其中 P001、P003、P004、P006 的库存小于 100这就是我们游标要挑出来的目标行。如果你平时是在客户端工具里连数据库调试或者用 AI 辅助生成/审查这些存储过程 SQL可以顺手把模型对话入口开着对照语法https://taotoken.net/api 对应的模型对话页在 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。写游标时最容易记混的就是DECLARE的先后顺序和HANDLER的位置让模型帮你逐行核对能省不少时间。需要长期跑批量任务、把这类过程挂到定时调度里的可以看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。环境上你只需要一个能执行存储过程的 MySQL 5.7 或 8.0 实例本地 Docker 起一个也行。注意客户端要支持DELIMITER改分隔符Navicat、DataGrip、命令行mysql客户端都支持。如果你用的是某些 Web 版 SQL 编辑器不支持DELIMITER那就把整段过程体拆成单条语句提交或者改用支持的工具。3. 可复制配置DECLARE CURSOR HANDLER LOOP 完整存储过程这一节是核心直接给你一份能跑的完整存储过程。它遍历products全表把库存小于 100 的商品 code 写进一张临时日志表最后把日志查出来。代码里我把四个关键步骤和HANDLER都标了注释你照着改表名、字段名就能用到自己项目里。USE demo_cursor; DROP PROCEDURE IF EXISTS CursorProc; DELIMITER $$ CREATE PROCEDURE CursorProc() BEGIN -- 变量声明区所有 DECLARE 必须放在最前面 DECLARE no_more_products INT DEFAULT 0; -- 结束标志 DECLARE prd_code VARCHAR(32); DECLARE quantity_in_stock INT DEFAULT 0; -- 第一步声明游标绑定结果集 DECLARE cur_product CURSOR FOR SELECT code FROM products; -- 第二步声明 NOT FOUND 处理器必须紧跟游标之后 DECLARE CONTINUE HANDLER FOR NOT FOUND SET no_more_products 1; -- 建临时表记录结果 CREATE TEMPORARY TABLE IF NOT EXISTS infologs ( id INT NOT NULL AUTO_INCREMENT, msg VARCHAR(255) NOT NULL, PRIMARY KEY (id) ); TRUNCATE TABLE infologs; -- 第三步打开游标 OPEN cur_product; -- 先取第一行避免空表时循环体先执行 FETCH cur_product INTO prd_code; -- 第四步循环遍历 read_loop: LOOP IF no_more_products 1 THEN LEAVE read_loop; END IF; SELECT quantity INTO quantity_in_stock FROM products WHERE code prd_code; IF quantity_in_stock 100 THEN INSERT INTO infologs (msg) VALUES (prd_code); END IF; FETCH cur_product INTO prd_code; END LOOP; -- 第五步关闭游标释放资源 CLOSE cur_product; SELECT * FROM infologs; DROP TEMPORARY TABLE IF EXISTS infologs; END$$ DELIMITER ;几个必须讲透的点。第一DECLARE有严格顺序变量 → 游标 → 处理器顺序错了直接报1064语法错误这是新手最高频的坑。第二HANDLER用CONTINUE而不是EXIT因为取到末尾时我们只想设置标志位然后继续走完循环收尾而不是整个过程退出。第三FETCH在循环前先执行一次循环体末尾再FETCH一次这样能正确处理“结果集为空”和“正常遍历”两种情况。如果你更习惯REPEAT ... UNTIL写法把LOOP那段换成下面这样效果完全一样REPEAT IF no_more_products 0 THEN SELECT quantity INTO quantity_in_stock FROM products WHERE code prd_code; IF quantity_in_stock 100 THEN INSERT INTO infologs (msg) VALUES (prd_code); END IF; FETCH cur_product INTO prd_code; END IF; UNTIL no_more_products 1 END REPEAT;两种写法我都实测过LOOP LEAVE可读性更好REPEAT更紧凑。选一种你团队统一的风格就行别混用。4. 验证请求与成功结果调用过程并核对输出代码写完调用和验证是必须做的动作不然你不知道游标到底遍历对没有。CALL CursorProc();预期输出是一张两列的表id从 1 开始自增msg列是四个商品编码idmsg1P0012P0033P0044P006看到这四行说明游标完整遍历了 6 行数据并对每行做了库存判断只把小于 100 的写进了日志。P002180和 P005260被正确跳过。再验证一次边界情况把products清空后调用过程应该返回空结果集而不是报错。DELETE FROM products; CALL CursorProc(); -- 应返回 Empty set无 1329 报错这一步专门验证HANDLER是否生效。如果没写CONTINUE HANDLER空表时FETCH会直接抛1329 No data过程中断。写了之后no_more_products被置 1循环第一次判断就LEAVE干净退出。验证完记得把数据插回去方便后续调试INSERT INTO products (code, name, quantity) VALUES (P001,机械键盘,45),(P002,无线鼠标,180), (P003,显示器支架,12),(P004,USB集线器,99), (P005,笔记本支架,260),(P006,降噪耳机,8);如果你在排查过程中想让 AI 帮你分析某段报错或优化循环逻辑把报错原文贴到模型对话里问比翻文档快https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。涉及 API 调用的批量任务Key 在控制台生成https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 接入细节看文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。5. 本篇常见错排查1329、DECLARE 顺序、死循环与 OAuth 类报错对照游标报错就那么几类我把真实遇到过的对照着列出来你按报错信息对号入座。报错一1329 - No data - zero rows fetched, selected, or processed这是最经典的。原因是没有声明NOT FOUND处理器或者处理器写成了EXIT导致提前退出。解决确认DECLARE CONTINUE HANDLER FOR NOT FOUND SET no_more_products 1;存在且位置在游标声明之后、OPEN之前。报错二1064 - You have an error in your SQL syntax指向 DECLARE 行九成是DECLARE顺序错了。变量、游标、处理器必须按这个顺序且都在BEGIN之后最前面。把HANDLER挪到游标后面即可。报错三循环停不下来 / 过程卡死通常是FETCH只写了一次或者LEAVE条件判断写反。检查循环体末尾有没有再FETCH一次以及IF no_more_products 1 THEN LEAVE是否在循环体开头。报错四local proxy failed/401 Unauthorized/OAuth相关这类不是游标本身的问题而是你在用外部工具或 API 调数据库/模型时鉴权失败。401一般是 Key 没带或过期去控制台重新生成local proxy failed多是本地代理配置和实际网络环境不匹配检查工具里的 Base URL 是否写成了https://taotoken.net/apiOAuth报错则常见于 Claude Code 这类需要授权的客户端重新走一遍授权流程即可。这三类都跟 SQL 语法无关别往游标代码里找。报错五reading choices解析失败出现在调用模型返回结果时通常是返回体被截断或格式非预期。检查请求参数里的model字段是否拼对以及max_tokens是否设得太小导致 JSON 不完整。排查顺序建议先看报错码13xx往游标逻辑找10xx往语法顺序找4xx往鉴权配置找。把报错原文完整贴出来比只看最后一行有用得多。6. 语义一致收尾把游标用对的几个实战习惯最后说几个我踩过坑之后养成的习惯都是能直接落地的。第一游标只读这个特性要刻在脑子里。想更新数据别在游标循环里对同一张表做UPDATE正确做法是把要改的主键先收集到临时表循环结束后统一UPDATE ... JOIN。第二结果集尽量小。游标是逐行处理几万行以上性能会明显下降能用集合操作UPDATE ... WHERE、INSERT ... SELECT就别用游标。第三临时表用完就DROP避免同名残留影响下次调用。第四HANDLER里除了设标志位别塞复杂逻辑它会在每次FETCH失败时触发写重了容易出意外。把这篇的CursorProc存下来当模板改表名和判断条件就能复用到对账、批量告警、逐行校验这些场景。真正写的时候先跑通空表不报错再跑通有数据结果正确最后才考虑性能优化。顺序对了游标就没那么难。