☰
CURSOR游标实战:用DECLARE与FETCH逐行处理SQL结果集
2026/10/2 12:07:12 网站建设 项目流程

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_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 基址是 https://taotoken.net/api ,注意 API 地址后面不加 UTM 参数。模型对话入口在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite ,接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。

数据库这边,你至少需要一张有几十行数据的测试表。我用下面这张订单表做演示,字段包括订单号、客户、金额、状态:

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,非滚动型一般只支持 NEXT:

FETCH 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;

最后一定要 CLOSE:

CLOSE 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 和 1005:

SELECT * FROM order_audit;

预期结果类似:

audit_idorder_idnoteaudit_time
11003处理客户 王五 金额 89.902024-01-10 10:00:01
21005处理客户 孙七 金额 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_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,生成 Key 后直接复制。

还有一个容易忽略的点:游标里的 SELECT 如果涉及大表且没有索引,OPEN 的时候就会很慢,因为要先把结果集准备好。这时候先给 WHERE 字段加索引,再测游标。另外,CLOSE 之后如果还想再用,必须重新 OPEN,不能直接 FETCH。

排障时建议按这个顺序:先单独跑游标里的 SELECT 确认结果集,再检查 DECLARE 和 OPEN 是否配对,然后看 FETCH 循环的退出条件,最后确认 CLOSE 有没有执行。接入层的问题则先看 401 和地址,再看模型 ID。

6. 语义一致 CTA:把游标调试和模型辅助串起来

游标这套东西,语法不难,难在循环边界和资源释放。我的习惯是每写一个游标,先在测试表上跑通“取到几行、处理几行、关闭后还能不能重开”这三步,再去改业务逻辑。这样即使逻辑写错,也不会因为游标没关把连接池占满。

如果你在写存储过程时需要随时确认某个数据库的 FETCH 语法、或者想让人帮你看看循环为什么少跑了一行,可以用模型对话入口 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 直接问。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面有完整的参数说明。长期做数据库开发、需要反复调试 SQL 和存储过程的,可以看看 Coding Plan https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite ,把日常的语法查询和排错固定下来。

最后留一个实用技巧:游标循环里如果要更新当前行,尽量用主键定位,别用游标里的字段做全表 UPDATE,否则每循环一次就扫一次表,数据量一上来就慢得离谱。把 FETCH 出来的主键存进变量,循环里用 WHERE 主键 = 变量 来更新,这是逐行处理里最值得养成的习惯。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询