1. 从一次订单对账说起变量、流程控制与游标到底解决什么问题先抛一个真实场景。你手上有一张订单表运营要求把每笔订单按金额分档打标签金额小于 100 记「小额」100 到 1000 记「中额」超过 1000 记「大额」同时统计各档订单数量最后把结果写进一张汇总表。用一条 UPDATE 加 CASE 也能做但如果规则里还要「逐行判断、遇到异常跳过、累计到一定金额就停止」纯 SQL 集合操作就会写得很别扭。这时候 MySQL 存储过程里的三样东西就派上用场了变量负责存中间结果流程控制负责决定「走哪条路、循环几次、什么时候跳出」游标负责把结果集一行一行取出来处理。三者组合等于把面向过程的开发能力搬进了数据库。这篇是 MySQL 第 15 章实战聚焦存储过程开发场景。我会给出可直接复制的建表脚本、存储过程脚本、执行验证步骤以及一份常见报错排查清单。适合已经会写基础 SELECT/INSERT、想进一步掌握存储过程逐行处理链路的同学。全文用一张orders表贯穿所有代码都能在本地 MySQL 8.0 直接跑。核心检索词先明确MySQL 存储过程中变量、流程控制与游标的组合应用本质是「声明变量 → 打开游标 → 循环取数 → 条件分支 → 累计/写回 → 关闭游标」这条完整链路。下面按这条链路拆开讲。2. 前置准备变量分类与游标声明顺序别一上来就踩坑在写存储过程之前必须先把「变量」这件事理清楚否则后面游标 FETCH 到变量时会各种报错。MySQL 里的变量大致分三层作用域和写法完全不同。第一层是系统变量由服务器定义用两个 开头。global.max_connections是全局的session.character_set_client是会话的直接写变量名会先找会话再找全局。查看用SHOW GLOBAL VARIABLES LIKE %标识符%修改用SET GLOBAL 变量名值或SET SESSION 变量名值。这一层在存储过程里一般只读不改改全局变量影响面太大。第二层是会话用户变量一个 开头比如total。它在当前连接的任何地方都能用不需要声明类型赋值用SET total 0或SELECT 表达式 INTO total。适合在存储过程外部传参、调试时看中间值。第三层是局部变量用DECLARE声明只在BEGIN ... END块内有效而且必须放在块的第一句。它需要指定类型可以带默认值DECLARE v_amount DECIMAL(10,2) DEFAULT 0.00; DECLARE v_level VARCHAR(10);这里有个高频坑局部变量声明顺序。MySQL 要求变量和条件的声明必须在游标声明之前游标声明必须在处理程序HANDLER声明之前。如果你把DECLARE cursor写在DECLARE v_amount前面直接报语法错误。记住这个顺序变量 → 条件 → 游标 → 处理程序。游标本身的使用分四步DECLARE cursor_name CURSOR FOR select_statement声明OPEN cursor_name打开FETCH cursor_name INTO var1, var2取一行CLOSE cursor_name关闭。FETCH 的变量个数必须和 SELECT 的字段数一致否则报Incorrect number of FETCH variables。还有一个必须提前知道的点游标遍历到最后一行后再 FETCH 会触发NOT FOUND条件SQLSTATE 02000。标准做法是声明一个CONTINUE HANDLER FOR NOT FOUND来设置一个结束标志变量循环里判断这个标志决定是否 LEAVE。没有这个处理程序游标循环要么死循环要么报错退出。如果你在本地或云端调试这些脚本想快速验证 SQL 逻辑、对比不同写法可以用 TaoToken 的模型对话功能让模型帮你逐行解释存储过程地址是 https://taotoken.net/api 配合接入文档 https://taotoken.net/api-keys 拿 Key 即可调试阶段挺省事。3. 可复制配置建表脚本 完整存储过程含 JSON 风格参数说明先把环境搭好。下面这段建表脚本直接复制执行创建订单表和汇总表DROP TABLE IF EXISTS orders; CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, amount DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 1 COMMENT 1待处理 2已处理, level_tag VARCHAR(10) DEFAULT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; INSERT INTO orders (order_no, amount, status) VALUES (A001, 50.00, 1), (A002, 300.00, 1), (A003, 1500.00, 1), (A004, 80.00, 1), (A005, 2000.00, 1); DROP TABLE IF EXISTS order_summary; CREATE TABLE order_summary ( level_tag VARCHAR(10) PRIMARY KEY, cnt INT DEFAULT 0, total_amount DECIMAL(12,2) DEFAULT 0.00 );接下来是核心存储过程。它用游标逐行读订单用 IF 分档用 LOOP LEAVE 控制循环用 CONTINUE HANDLER 处理游标结束最后把统计结果写入汇总表DELIMITER // CREATE PROCEDURE proc_tag_orders() BEGIN DECLARE v_amount DECIMAL(10,2); DECLARE v_tag VARCHAR(10); DECLARE v_done INT DEFAULT 0; DECLARE v_small_cnt INT DEFAULT 0; DECLARE v_mid_cnt INT DEFAULT 0; DECLARE v_big_cnt INT DEFAULT 0; DECLARE v_small_sum DECIMAL(12,2) DEFAULT 0.00; DECLARE v_mid_sum DECIMAL(12,2) DEFAULT 0.00; DECLARE v_big_sum DECIMAL(12,2) DEFAULT 0.00; DECLARE cur_orders CURSOR FOR SELECT amount FROM orders WHERE status 1 ORDER BY id; DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done 1; OPEN cur_orders; read_loop: LOOP FETCH cur_orders INTO v_amount; IF v_done 1 THEN LEAVE read_loop; END IF; IF v_amount 100 THEN SET v_tag 小额; SET v_small_cnt v_small_cnt 1; SET v_small_sum v_small_sum v_amount; ELSEIF v_amount 1000 THEN SET v_tag 中额; SET v_mid_cnt v_mid_cnt 1; SET v_mid_sum v_mid_sum v_amount; ELSE SET v_tag 大额; SET v_big_cnt v_big_cnt 1; SET v_big_sum v_big_sum v_amount; END IF; UPDATE orders SET level_tag v_tag WHERE order_no ( SELECT order_no FROM (SELECT order_no FROM orders WHERE status 1 ORDER BY id LIMIT 1) t ); END LOOP; CLOSE cur_orders; INSERT INTO order_summary (level_tag, cnt, total_amount) VALUES (小额, v_small_cnt, v_small_sum), (中额, v_mid_cnt, v_mid_sum), (大额, v_big_cnt, v_big_sum) ON DUPLICATE KEY UPDATE cnt VALUES(cnt), total_amount VALUES(total_amount); END// DELIMITER ;上面这段里read_loop是循环标签LEAVE read_loop跳出循环CONTINUE HANDLER FOR NOT FOUND在游标取完后把v_done置 1。注意 UPDATE 那段为了演示逐行更新实际生产建议用游标同时 FETCH 出 id 和 amount避免子查询。下面给一个更规范的版本把 id 一起取出来DECLARE cur_orders CURSOR FOR SELECT id, amount FROM orders WHERE status 1 ORDER BY id; DECLARE v_id INT; -- FETCH 时 FETCH cur_orders INTO v_id, v_amount; -- 更新时 UPDATE orders SET level_tag v_tag WHERE id v_id;如果你用 Cline、Codex 这类工具辅助写存储过程配置里需要写全三件套。以 Cline 的 MCP 配置为例Base URL 填https://taotoken.net/apiKey 从 https://taotoken.net/api-keys 获取Model ID 按你选的模型填。Codex 的auth.json里同样要保证 Base URL、Key、Model ID 三项齐全缺一项就会报 401 或 OAuth 相关错误。长期做数据库脚本开发的话Coding Plan 更适合持续调用入口在 https://taotoken.net/coding-plan 。4. 执行验证调用存储过程并检查结果脚本建好后调用和验证分三步。第一步调用存储过程CALL proc_tag_orders();第二步查看订单表打标结果SELECT id, order_no, amount, level_tag FROM orders ORDER BY id;预期输出五行A001 和 A004 是「小额」A002 是「中额」A003 和 A005 是「大额」。第三步查看汇总表SELECT * FROM order_summary;预期结果level_tagcnttotal_amount小额2130.00中额1300.00大额23500.00如果结果对得上说明「变量 流程控制 游标」这条链路跑通了。想再验证 WHILE 和 REPEAT 的写法可以把 LOOP 换成 WHILEWHILE v_done 0 DO FETCH cur_orders INTO v_id, v_amount; IF v_done 0 THEN -- 处理逻辑 END IF; END WHILE;REPEAT 则是先执行一次再判断REPEAT FETCH cur_orders INTO v_id, v_amount; IF v_done 0 THEN -- 处理逻辑 END IF; UNTIL v_done 1 END REPEAT;三种循环的差别LOOP 必须配 LEAVEWHILE 先判断后执行REPEAT 先执行后判断。游标场景下推荐 LOOP LEAVE逻辑最直观。验证过程中如果想快速确认某个表达式的值可以用SELECT 变量名看会话用户变量或者在存储过程里临时SELECT v_amount输出。调试完记得删掉临时输出语句。5. 常见报错排查清单401、local proxy failed、reading choices、OAuth 对照存储过程调试阶段报错基本集中在几类。下面按真实报错对照排查。报错一Incorrect number of FETCH variables。原因FETCH INTO 的变量个数和游标 SELECT 的字段数不一致。比如 SELECT 了 id 和 amount 两个字段FETCH 只写了一个变量。解决数清楚字段数一一对应。报错二Cursor is already open。原因同一个游标被 OPEN 两次或者上一次没 CLOSE 就再次 OPEN。解决确保 OPEN 和 CLOSE 成对出现循环结束后立即 CLOSE。报错三No data - zero rows fetched。原因游标已经取完还在继续 FETCH且没有 NOT FOUND 处理程序。解决加上DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done 1循环里判断 v_done。报错四Unknown column v_amount in field list。原因局部变量声明位置不对或者变量名拼写错误。解决确认 DECLARE 在 BEGIN 块第一句且变量名前后一致。报错五Cant update table orders in stored function/trigger。原因在游标遍历同一张表时又去 UPDATE 这张表某些场景会锁冲突。解决先把结果集取到临时表或者用 id 定位更新避免全表扫描。报错六401 Unauthorized/local proxy failed/reading choices/OAuth。这类不是 SQL 报错而是你在用 AI 工具辅助写存储过程时接口配置出了问题。401 通常是 Key 无效或没带local proxy failed是本地代理配置不通reading choices是返回体解析失败多半是 Base URL 写错OAuth 相关则是认证方式没选对。排查顺序先确认 Base URL 是https://taotoken.net/api再确认 Key 从 https://taotoken.net/api-keys 获取且没过期最后确认 Model ID 填对。三件套齐全后用模型对话 https://taotoken.net/api 发一条测试请求验证连通性。报错七Deadlock found when trying to get lock。原因游标逐行 UPDATE 时和别的会话争锁。解决缩小事务范围或者改用批量 UPDATE。报错八DECLARE is not allowed here。原因DECLARE 没放在 BEGIN 块开头。解决把所有 DECLARE 集中到块的第一部分顺序是变量 → 条件 → 游标 → 处理程序。排查时建议开一个会话专门跑SHOW WARNINGS和SHOW ERRORS存储过程里的错误信息会在这里显示。另外SHOW CREATE PROCEDURE proc_tag_orders可以回看过程定义确认脚本没被截断。6. 把这条链路用起来从对账到批量处理的通用套路变量、流程控制、游标这三样组合起来能覆盖很多「逐行处理 条件判断 累计统计」的场景。除了订单打标还有几个典型用法。批量数据清洗游标遍历原始表用 IF 判断字段是否为空或格式错误符合条件就 UPDATE 修正不符合就跳过。配合 CONTINUE HANDLER 还能记录错误行到日志表。分级审批流游标读待审批记录用 CASE 按金额或类型分流到不同审批人累计到某个阈值就 LEAVE 提前结束。CASE 比 IF 更适合多分支等值判断。分页统计游标配合 WHILE 循环每次取一批累计到目标数量就停。注意游标本身不支持 LIMIT 动态分页需要自己在循环里计数。性能上要提醒一句游标是逐行处理数据量大时比集合操作慢很多。能用一条 UPDATE ... CASE 解决的就别上游标。游标适合逻辑复杂、必须逐行判断的场景。用完之后一定 CLOSE否则游标占用的内存和锁会一直保持到存储过程结束。如果你想把这类存储过程脚本纳入版本管理或者让 AI 帮你生成测试数据、审查 SQL 逻辑长期编码场景用 Coding Plan 更划算入口 https://taotoken.net/coding-plan 。需要快速验证某段游标逻辑时模型对话 https://taotoken.net/api 直接贴代码问就行。接入文档在 https://taotoken.net/doc API Key 管理在 https://taotoken.net/api-keys 控制台在 https://taotoken.net/console 。最后留一个实用技巧写游标存储过程时先在外部用普通 SELECT 把结果集跑一遍确认字段和行数再套进游标。这样能避免大半的 FETCH 变量不匹配问题。存储过程建好后用CALL跑一次再SELECT验证结果比反复改脚本高效得多。