1. 三类游标到底差在哪:从一段真实报错说起
先抛一个我见过很多次的场景。你在 PL/SQL 里写了个存储过程,想把一张表的结果集返回给上层应用,于是声明了一个cursor,结果编译直接报PLS-00382: expression is of wrong type,或者更隐蔽一点,过程能建,但调用时ORA-06550一路飘红。问题往往不在 SQL 本身,而在于你把「静态游标」和「引用游标」的语义搞混了。
Oracle 里的游标,本质是「指向查询结果集的一个句柄」。按声明方式和生命周期,可以分成三类:显式 cursor、隐式 cursor、以及 REF CURSOR(其中SYS_REFCURSOR是系统预定义的弱类型引用游标)。它们最核心的区别在于:结果集能不能跨程序边界传递。显式和隐式游标是 PL/SQL 内部的私有变量,出了这个块就没了;而 REF CURSOR 是一个「指针的指针」,可以把结果集的读取权交给客户端或另一个子程序。
这篇文章适合谁?适合已经会写基本 PL/SQL、但在「什么时候用 cursor、什么时候必须用 sys_refcursor」上反复踩坑的开发者。我会给出可复制的建表脚本、三类游标的完整示例、逐条对比查询,并说明怎么用 TaoToken 统一 Key 在 AI 工具里生成和校验这些脚本,最后用真实执行输出验证行为差异。核心检索词就是:Oracle cursor、refcursor、sys_refcursor 的区别与适用场景。
先说结论,方便你带着判断往下读:能用隐式游标就别写显式,能用静态 SQL 就别上 REF CURSOR,只有当结果集必须返回给客户端、或在多个子程序间共享时,才动用 REF CURSOR / SYS_REFCURSOR。这个优先级背后是效率和维护成本的权衡,后面会用代码逐条印证。
2. 用 TaoToken 统一 Key 准备 AI 校验环境
写这类游标脚本,最容易出错的地方是语法细节:open ... for后面能不能跟变量、强类型 REF CURSOR 的return子句要不要和记录类型严格对齐、%ROWCOUNT在隐式游标里到底统计的是哪条语句。这些细节靠记忆很容易翻车,我习惯让 AI 工具帮我生成初稿再逐条核对。但多个 AI 工具各自要配 Key、切模型,管理起来很烦,所以我用 TaoToken 把 Key 和 API 通道统一起来。
TaoToken 在这里扮演的角色是「统一的模型接入层」:你拿到一个 Key,就能在支持自定义 Base URL 的 AI 工具里调用不同模型,不用为每个工具单独申请和轮换密钥。对写 Oracle 脚本这种需要反复生成、校验、对比的场景,省下的就是切换成本。
你需要准备三样东西,我把它称为「三件套」,缺一不可:
- Base URL:
https://taotoken.net/api - API Key:在控制台创建,地址是
https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite - Model ID:按你用的工具填对应模型标识,比如做代码生成和校验时选一个擅长 SQL 的模型
如果你用的是 Claude Code 这类编码工具,接入时同样填这三件套,Base URL 用上面的 API 地址,Key 用控制台生成的,Model ID 按工具要求填。想先验证模型能不能正常对话,可以去模型对话页试一句:https://taotoken.net/model-chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite。长期做编码和 Agent 任务的话,Coding Plan 更划算:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite。
这里给一个通用的配置片段,很多工具都认这种 JSON 结构,路径按你实际工具的配置文件放:
{ "base_url": "https://taotoken.net/api", "api_key": "sk-你的TaoToken密钥", "model": "你的模型ID" }注意:base_url只写到/api,不要自己拼/v1/chat/completions之类的后缀,工具通常会自己补全。Key 不要提交到 Git,放环境变量或本地配置文件里。配好之后,你就能让 AI 帮你生成下面的游标脚本,再拿回 SQL*Plus 或 SQL Developer 里跑,形成「生成—执行—纠错」的闭环。
3. 可复制配置:建表脚本与三类游标完整示例
这一节是全文的技术核心,所有脚本都可以直接复制执行。先建两张表,一张模拟号段资源,一张做隐式游标的更新测试。
-- 建表:号段资源表 create table gsm_resource ( gsmno varchar2(11), status varchar2(1), price number(8,2), store_id varchar2(32) ); insert into gsm_resource values('13905310001','0',200.00,'SD.JN.01'); insert into gsm_resource values('13905312002','0',800.00,'SD.JN.02'); insert into gsm_resource values('13905315005','1',500.00,'SD.JN.01'); insert into gsm_resource values('13905316006','0',900.00,'SD.JN.03'); commit; -- 建表:隐式游标测试表 create table zrp (str varchar2(10)); insert into zrp values ('ABCDEFG'); insert into zrp values ('ABCXEFG'); insert into zrp values ('ABCYEFG'); insert into zrp values ('ABCDEFG'); insert into zrp values ('ABCZEFG'); commit;3.1 显式 cursor:声明、打开、提取、关闭四步走
显式游标有明确的cursor ... is select ...声明,生命周期是 declare → open → fetch → close。它的作用域是当前 PL/SQL 块,静态 SQL,效率高,但不能返回给客户端。
declare cursor get_gsmno_cur (p_nettype in varchar2) is select gsmno from gsm_resource where gsmno like p_nettype || '%' and status = '0'; v_gsmno gsm_resource.gsmno%type; begin open get_gsmno_cur('139'); loop fetch get_gsmno_cur into v_gsmno; exit when get_gsmno_cur%notfound; dbms_output.put_line('显式游标输出: ' || v_gsmno); end loop; close get_gsmno_cur; end; /注意这里我用了like p_nettype || '%'而不是原文的nettype = p_nettype,因为gsmno是完整号码,用前缀匹配更贴近「按号段选号」的语义。执行后你会看到13905310001、13905312002、13905316006三条 status='0' 的记录被打印出来。%notfound在 fetch 之后判断,这是显式游标循环的标准写法,别用NO_DATA_FOUND。
3.2 隐式 cursor:DML 背后的 SQL 游标
隐式游标没有声明,Oracle 把每条 DML 都解析成一个名为SQL的隐式游标。SQL%ROWCOUNT、SQL%FOUND、SQL%NOTFOUND就是它的属性。FOR 循环遍历查询结果也是隐式游标。
begin update zrp set str = 'updateD' where str like '%D%'; if sql%rowcount = 0 then insert into zrp values ('1111111'); end if; dbms_output.put_line('第一次更新影响行数: ' || sql%rowcount); end; / begin update zrp set str = 'updateD' where str like '%S%'; if sql%rowcount = 0 then insert into zrp values ('0000000'); end if; dbms_output.put_line('第二次更新影响行数: ' || sql%rowcount); end; /第一次更新匹配到两条含 D 的记录,sql%rowcount = 2,不插入;第二次匹配含 S 的记录为 0 条,sql%rowcount = 0,于是插入0000000。这就是隐式游标最实用的地方:用SQL%ROWCOUNT做「更新不到就插入」的 upsert 逻辑,不用额外声明任何游标。
FOR 循环版本更简洁,连变量都不用声明:
begin for rec in (select gsmno, status from gsm_resource) loop dbms_output.put_line(rec.gsmno || '--' || rec.status); end loop; end; /3.3 REF CURSOR 与 SYS_REFCURSOR:把结果集交出去
REF CURSOR 是动态游标,运行时才绑定查询。它最大的价值是能返回给客户端,这是存储过程返回结果集的唯一方式。SYS_REFCURSOR是 Oracle 9i 之后系统预定义的弱类型 REF CURSOR,省去了自己type ... is ref cursor的声明。
先看强类型 REF CURSOR,它用return子句约束了结果集的列结构:
declare type gsm_rec is record( gsmno varchar2(11), status varchar2(1), price number(8,2)); type app_ref_cur_type is ref cursor return gsm_rec; my_cur app_ref_cur_type; my_rec gsm_rec; begin open my_cur for select gsmno, status, price from gsm_resource where store_id = 'SD.JN.01'; fetch my_cur into my_rec; while my_cur%found loop dbms_output.put_line(my_rec.gsmno || '#' || my_rec.status || '#' || my_rec.price); fetch my_cur into my_rec; end loop; close my_cur; end; /强类型的好处是编译期就能校验列匹配,坏处是灵活性差。实际项目里更常用SYS_REFCURSOR做存储过程出参:
create or replace procedure getEmpByDept( in_deptNo in number, out_curEmp out sys_refcursor ) as begin open out_curEmp for select gsmno, status, price from gsm_resource where status = '0'; exception when others then raise_application_error(-20101, 'Error in getEmpByDept: ' || sqlcode); end getEmpByDept; /调用时用绑定变量接收:
var rset refcursor; exec getEmpByDept(10, :rset); print rset;print rset会把结果集直接打印出来,这就是 REF CURSOR 能「跨边界」的证据——客户端拿到了结果集的读取权。对比一下:显式游标无论怎么 open,客户端都看不到它的数据。
4. 验证请求:执行输出与三类游标行为对比
脚本跑完,我们逐条看输出,验证语义差异。
显式游标那段,输出三行139开头的可用号码,证明它按参数化查询筛选、循环提取、正常关闭。隐式游标两段,第一段sql%rowcount = 2,第二段sql%rowcount = 0并插入0000000,证明 DML 隐式游标属性可用。强类型 REF CURSOR 输出13905310001#0#200和13905315005#1#500,正好是SD.JN.01门店的两条记录。SYS_REFCURSOR过程调用后print rset返回 status='0' 的三条记录。
把差异整理成一张对照表,方便你按场景选型:
| 维度 | 显式 cursor | 隐式 cursor | REF CURSOR / SYS_REFCURSOR |
|---|---|---|---|
| 声明方式 | cursor ... is select | 无声明,DML 自动生成 | type ... is ref cursor或sys_refcursor |
| 绑定时机 | 编译期静态 SQL | 编译期 | 运行时动态绑定 |
| 能否返回客户端 | 不能 | 不能 | 能,存储过程出参标准做法 |
| 作用域 | 当前 PL/SQL 块 | 当前语句 | 可跨子程序传递 |
| 效率 | 高 | 高 | 相对低,仅在必要时用 |
| 典型场景 | 参数化遍历、批量处理 | upsert、FOR 循环 | 返回结果集、子程序共享 |
再补一组游标属性的区别,这是排错时最容易混的:
%FOUND:有行返回为 TRUE%NOTFOUND:无行返回为 TRUE%ISOPEN:游标仍打开为 TRUE%ROWCOUNT:最近一条 SQL 影响的行数
关键提醒:SELECT ... INTO无数据触发NO_DATA_FOUND;显式游标 where 未命中触发%NOTFOUND;UPDATE/DELETE未命中触发SQL%NOTFOUND。在 fetch 循环里判断退出条件,用%NOTFOUND或%FOUND,不要用NO_DATA_FOUND,否则循环行为会不符合预期。
如果你想让 AI 帮你核对某个脚本的游标类型选得对不对,可以把脚本贴进模型对话页,让它逐行指出「这里该用隐式还是 REF CURSOR」。用 TaoToken 统一 Key 的好处是,你换模型对比结论时不用重新配环境。
5. 本篇常见错排查:从真实报错定位游标问题
这一节按真实报错来,每条都给出原因和修法。
PLS-00382: expression is of wrong type。最常见于把静态 cursor 当出参返回。存储过程出参必须是 REF CURSOR 类型,不能是cursor ... is select声明的静态游标。修法:把出参改成out sys_refcursor,过程内用open out_cur for select ...。
ORA-01001: invalid cursor。通常是游标没 open 就 fetch,或者 close 之后又 fetch。显式游标必须严格 open → fetch → close;REF CURSOR 在open ... for之前不能 fetch。检查你的%ISOPEN判断。
ORA-06550 / PLS-00201: identifier 'SYS_REFCURSOR' must be declared。多见于老版本客户端或权限问题。SYS_REFCURSOR是 9i 之后系统预定义的,确认数据库版本,并检查当前 schema 是否有权限。实在不行就自己声明type rc is ref cursor;替代。
local proxy failed / 401。如果你在 AI 工具里生成脚本时报这类错,多半是 Base URL 或 Key 配错了。检查三件套:Base URL 是否为https://taotoken.net/api、Key 是否从控制台正确复制、Model ID 是否填对。401 一般是 Key 无效或过期,去https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite重新生成。local proxy failed通常是工具的网络配置问题,确认 Base URL 没有多余后缀。
reading choices 报错 / 返回结构解析失败。这通常是模型返回格式和工具预期不一致,换一个 Model ID 或检查工具版本。接入文档里有各工具的配置说明:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite。
OAuth 相关报错。如果你用的是 Claude Code 这类带 OAuth 流程的工具,接入自定义 Base URL 时可能提示 OAuth 失败。这类工具通常支持 API Key 模式,切到 Key 模式填三件套即可,参考 Claude Code 接入说明:https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claude_code&utm_campaign=rewrite。
强类型 REF CURSOR 列不匹配。type ... is ref cursor return gsm_rec要求open ... for的 select 列顺序、类型和gsm_rec完全一致,多一列少一列都编译不过。修法:要么严格对齐,要么改用sys_refcursor弱类型。
%ROWCOUNT 取值不符合预期。记住SQL%ROWCOUNT统计的是最近一条 DML 影响的行数,不是整个块。在 fetch 循环里cursor%ROWCOUNT是已提取的行数,两者别混。
6. 把游标脚本接进你的 AI 工作流
三类游标的边界其实很清晰:显式游标管块内参数化遍历,隐式游标管 DML 和 FOR 循环,REF CURSOR / SYS_REFCURSOR 管跨边界返回结果集。选型优先级就是「隐式 > 显式 > REF CURSOR」,只有结果集必须交给客户端或在子程序间共享时才升级到 REF CURSOR。
实际写脚本时,我建议你把建表脚本和游标示例一起丢给 AI,让它按「声明—打开—提取—关闭」逐段检查,再拿回数据库执行验证。用 TaoToken 统一 Key 后,生成、校验、换模型对比都在一个通道里完成,不用反复配环境。需要长期做这类编码和 Agent 任务的,Coding Plan 比按次调用更省心:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite。先把上面三段脚本跑通,再对照报错清单排查,你对这三类游标的判断就会从「背概念」变成「看场景」。