
1. 为什么还要学 CURSOR 游标逐行处理 SQL 结果集的真实场景很多人第一次接触 CURSOR 游标都是在存储过程里被要求“按行处理”。比如订单表里有一批状态异常的记录需要逐条读取订单号再拿订单号去明细表里查金额、去日志表里查最近一次操作时间最后把结果写回一张对账表。这种“读一行、算一行、写一行”的逻辑用一条 UPDATE ... JOIN 很难表达清楚因为中间夹着业务判断和多次查询。CURSOR 游标就是干这个的它把 SELECT 的结果集变成一个可以一行一行往前取的“队列”你用 DECLARE 声明、OPEN 打开、FETCH 取一行、CLOSE 关闭。它适合的场景很明确——需要按行做复杂逻辑、需要把当前行的字段作为参数去查别的表、需要在循环里做条件分支。不适合的场景同样明确——能用一条集合 SQL 搞定的就别用游标因为逐行处理在数据量大时性能差距非常明显。这篇面向需要按行处理查询结果的数据库开发场景给出 DECLARE CURSOR、OPEN、FETCH、CLOSE 的完整可复制示例并在真实表上演示逐行读取与循环处理的验证步骤帮你掌握游标替代一次性结果集处理的适用边界。文中示例以常见的关系型数据库语法为主不同数据库在细节关键字上略有差异我会在关键处标注。如果你在本地或远程环境里调试这些 SQL需要一个稳定的模型对话入口来随时问语法细节可以先把工具链准备好后面配置章节会给出具体地址和参数。2. 前置准备TaoToken 接入与游标调试环境搭建在写游标之前先把两件事准备好一个能跑 SQL 的数据库环境以及一个能随时查语法、排报错的模型对话入口。我平时调试存储过程时习惯把模型对话放在旁边遇到 FETCH 报错或者游标不关闭的问题直接贴报错问。TaoToken 的接入地址如下官网入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 基址是 https://taotoken.net/api 注意 API 地址后面不加 UTM 参数。模型对话入口在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。数据库这边你至少需要一张有几十行数据的测试表。我用下面这张订单表做演示字段包括订单号、客户、金额、状态CREATE TABLE orders ( order_id INT PRIMARY KEY, customer VARCHAR(50), amount DECIMAL(10,2), status VARCHAR(20), created_at DATE ); INSERT INTO orders VALUES (1001, 张三, 299.00, PAID, 2024-01-05), (1002, 李四, 1580.50, PAID, 2024-01-06), (1003, 王五, 89.90, UNPAID, 2024-01-07), (1004, 赵六, 420.00, PAID, 2024-01-08), (1005, 孙七, 76.00, UNPAID, 2024-01-09);游标调试最容易踩的坑是“忘记关闭”和“循环不退出”。前者会占用连接资源后者会让存储过程卡死。所以准备阶段建议你单独开一个测试库别在生产库上练手。模型对话那边可以帮你快速确认某个数据库的 FETCH 语法是否支持 PREVIOUS、是否支持 SCROLL这些细节各库差异不小。另外提醒一句游标里的 SELECT 语句如果带 FOR UPDATE就变成锁表游标事务没结束前相关行会被锁住。测试阶段尽量别用 FOR UPDATE等逻辑跑通了再按需加。3. 可复制配置DECLARE / OPEN / FETCH / CLOSE 完整示例这一节给出可以直接复制运行的游标模板。不同数据库的存储过程语法不同下面以通用伪代码加具体 SQL 的形式呈现关键差异我会标注。核心四步永远是DECLARE 声明、OPEN 打开、FETCH 取数、CLOSE 关闭。先看声明部分。游标声明有三种常见写法直接用 SQL 语句、用 PREPARED ID、用字符串表达式。第一种最直观DECLARE cur_orders CURSOR FOR SELECT order_id, customer, amount, status FROM orders WHERE status UNPAID;第二种是先 PREPARE 再声明适合条件需要动态拼接的场景LET l_sql SELECT order_id, customer, amount FROM orders WHERE status ?; PREPARE prep_unpaid FROM l_sql; DECLARE cur_unpaid CURSOR FOR prep_unpaid;第三种是直接用字符串表达式声明LET l_sql SELECT order_id, amount FROM orders WHERE amount 100; DECLARE cur_big CURSOR FROM l_sql;声明之后要 OPEN如果 SQL 里有问号占位符OPEN 时用 USING 传入变量问号有几个就要传几个OPEN cur_unpaid USING UNPAID;取数用 FETCH。滚动型游标支持 NEXT、PREVIOUS、FIRST、LAST、RELATIVE、ABSOLUTE非滚动型一般只支持 NEXTFETCH NEXT cur_unpaid INTO v_order_id, v_customer, v_amount;循环处理时通常配合一个“是否还有数据”的判断。以常见的 WHILE 循环为例WHILE (SQLCODE 0) DO -- 在这里处理 v_order_id / v_customer / v_amount FETCH NEXT cur_unpaid INTO v_order_id, v_customer, v_amount; END WHILE;最后一定要 CLOSECLOSE cur_unpaid;如果你用的是支持 FOREACH 的数据库循环可以写得更简洁FOREACH 会自动按 SQL 抓取的顺序逐行循环抓多少笔就循环多少次FOREACH cur_unpaid USING UNPAID INTO v_order_id, v_customer, v_amount -- 逐行处理逻辑 END FOREACH;关于 WITH HOLD不加这个声明时事务一关闭游标就跟着关了加了 WITH HOLD事务提交后游标还能继续用。大多数逐行处理场景不需要 WITH HOLD因为处理完就 CLOSE 了。下面给一个完整的、可复制到存储过程里的模板把四步串起来CREATE PROCEDURE process_unpaid_orders() BEGIN DECLARE v_order_id INT; DECLARE v_customer VARCHAR(50); DECLARE v_amount DECIMAL(10,2); DECLARE done INT DEFAULT 0; DECLARE cur_unpaid CURSOR FOR SELECT order_id, customer, amount FROM orders WHERE status UNPAID; DECLARE CONTINUE HANDLER FOR NOT FOUND SET done 1; OPEN cur_unpaid; read_loop: LOOP FETCH cur_unpaid INTO v_order_id, v_customer, v_amount; IF done 1 THEN LEAVE read_loop; END IF; -- 逐行处理这里可以插入日志、更新状态、调用其他逻辑 INSERT INTO order_audit(order_id, note) VALUES (v_order_id, CONCAT(处理客户 , v_customer, 金额 , v_amount)); END LOOP; CLOSE cur_unpaid; END;这段模板里CONTINUE HANDLER 负责在 FETCH 取不到数据时把 done 置 1循环里检测到 done 就 LEAVE 退出。这是最经典的游标循环写法几乎所有关系型数据库都能找到对应实现。4. 验证请求与成功结果在真实表上逐行读取配置写好了接下来验证它真的按行跑。我用上面那张 orders 表先建一张审计表记录处理过程CREATE TABLE order_audit ( audit_id INT AUTO_INCREMENT PRIMARY KEY, order_id INT, note VARCHAR(200), audit_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP );然后调用存储过程CALL process_unpaid_orders();执行后查审计表应该看到两条记录对应 status UNPAID 的 1003 和 1005SELECT * FROM order_audit;预期结果类似audit_idorder_idnoteaudit_time11003处理客户 王五 金额 89.902024-01-10 10:00:0121005处理客户 孙七 金额 76.002024-01-10 10:00:01如果只看到一条或者一条都没有说明 FETCH 循环的退出条件写错了或者 WHERE 条件没匹配上。这时候可以先把游标里的 SELECT 单独拿出来跑一遍确认结果集行数SELECT order_id, customer, amount FROM orders WHERE status UNPAID;确认是 2 行之后再检查循环里的 done 判断。常见错误是把 done 的判断放在 FETCH 之前导致第一行还没处理就退出了。再验证一个带参数的游标。声明时用问号占位OPEN 时传值DECLARE cur_by_status CURSOR FOR SELECT order_id, amount FROM orders WHERE status ?; OPEN cur_by_status USING PAID;然后逐行 FETCH应该能取到 1001、1002、1004 三条。这个验证能帮你确认 USING 传参的数量和顺序是否正确——问号有几个USING 后面就要跟几个变量顺序一一对应。滚动型游标的验证稍微不同它支持来回取。比如先 FETCH LAST 取最后一行再 FETCH PREVIOUS 取倒数第二行FETCH LAST cur_scroll INTO v_order_id, v_amount; FETCH PREVIOUS cur_scroll INTO v_order_id, v_amount;如果你的数据库不支持 SCROLL 关键字这两条会直接报错那就说明只能用 NEXT 顺序取。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth游标本身是数据库层的语法但调试过程中如果你用模型对话辅助排查可能会遇到接入层的报错。这一节把两类问题放一起对照方便你快速定位。第一类接入层报错。401 通常表示 API Key 无效或没带上检查请求头里的 Authorization 字段是否拼写正确Key 是否复制完整。local proxy failed 一般是本地代理配置有问题检查你的请求地址是否指向了正确的 API 基址 https://taotoken.net/api 注意这个地址后面不要加多余路径。reading choices 报错通常出现在返回体解析阶段说明请求发出去了但响应结构不符合预期先确认模型 ID 是否写对。OAuth 相关报错多见于需要授权登录的场景检查 token 是否过期。第二类游标本身的报错。最常见的是“游标已存在”或“游标未打开”。前者是因为同名游标重复 DECLARE后者是没 OPEN 就 FETCH。还有“FETCH 超出结果集”的报错这在没有 HANDLER 的循环里会直接中断存储过程所以务必加 NOT FOUND 处理。如果你用的是 Claude Code 这类编码工具来辅助写存储过程配置时要写全三件套Base URL 填 https://taotoken.net/api Key 填你在控制台生成的密钥Model ID 填你实际使用的模型标识。三者缺一请求就会失败。控制台入口在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 生成 Key 后直接复制。还有一个容易忽略的点游标里的 SELECT 如果涉及大表且没有索引OPEN 的时候就会很慢因为要先把结果集准备好。这时候先给 WHERE 字段加索引再测游标。另外CLOSE 之后如果还想再用必须重新 OPEN不能直接 FETCH。排障时建议按这个顺序先单独跑游标里的 SELECT 确认结果集再检查 DECLARE 和 OPEN 是否配对然后看 FETCH 循环的退出条件最后确认 CLOSE 有没有执行。接入层的问题则先看 401 和地址再看模型 ID。6. 语义一致 CTA把游标调试和模型辅助串起来游标这套东西语法不难难在循环边界和资源释放。我的习惯是每写一个游标先在测试表上跑通“取到几行、处理几行、关闭后还能不能重开”这三步再去改业务逻辑。这样即使逻辑写错也不会因为游标没关把连接池占满。如果你在写存储过程时需要随时确认某个数据库的 FETCH 语法、或者想让人帮你看看循环为什么少跑了一行可以用模型对话入口 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 直接问。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有完整的参数说明。长期做数据库开发、需要反复调试 SQL 和存储过程的可以看看 Coding Plan https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 把日常的语法查询和排错固定下来。最后留一个实用技巧游标循环里如果要更新当前行尽量用主键定位别用游标里的字段做全表 UPDATE否则每循环一次就扫一次表数据量一上来就慢得离谱。把 FETCH 出来的主键存进变量循环里用 WHERE 主键 变量 来更新这是逐行处理里最值得养成的习惯。