☰
Oracle cursor(游标)总结:从显式游标到REF游标的配置与验证
2026/9/26 14:16:50 网站建设 项目流程

1. 为什么你写的游标总在%NOTFOUND上翻车

Oracle 的 cursor(游标)本质上是一个指向查询结果集的指针,你可以把它想成「数据库帮你把 SELECT 结果先缓存成一个可逐行读取的容器」。它真正解决的问题是:当结果集有几千上万行、又需要在 PL/SQL 里逐行做业务判断时,一次性SELECT INTO会直接抛TOO_MANY_ROWS,而游标能让你一行一行地取、一行一行地处理。

游标分三类:隐式游标(DML 自动带的 SQL 游标)、显式游标(静态,编译期就绑定 SQL)、REF 游标(动态,运行时才绑定 SQL)。前两者属于静态游标,REF 游标属于动态游标,这个区别决定了你能不能把「查什么表」当成参数传进存储过程。

这篇面向正在写存储过程、批处理脚本的数据库开发者,交付的是可以直接复制进 SQL*Plus 或 SQL Developer 跑通的游标骨架,包括声明、打开、取值、关闭全流程,以及%FOUND、%NOTFOUND、%ROWCOUNT三个属性的验证动作。如果你之前遇到过「循环多输出一行」「exit when位置写错导致死循环」「REF 游标在包里声明报错」,下面的排障部分基本能对上号。

2. 前置准备:环境与 TaoToken 接入

在动手写游标之前,先把执行环境理清楚。游标代码本身不依赖任何外部服务,但如果你想让 AI 辅助生成或审查游标逻辑,可以走 TaoToken 的模型对话入口,把 PL/SQL 片段贴进去让它帮你找%NOTFOUND位置问题。

TaoToken 官网地址是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 入口是 https://taotoken.net/api 。如果你打算在脚本里批量调用模型来审查游标代码,需要先去控制台创建密钥:

  • 控制台:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite
  • API Keys 管理:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite
  • 接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite

数据库侧的准备很简单:一个能连上的 Oracle 实例(11g 及以上都行),一张有数据的测试表。下面统一用经典的emp表举例,字段包括empno、ename、sal。执行前记得打开输出:

SET SERVEROUTPUT ON;

注意:DBMS_OUTPUT.PUT_LINE只有在SERVEROUTPUT打开时才会显示,很多人写完游标看不到输出,第一反应是代码错了,其实是这个开关没开。

3. 可复制配置:三类游标的完整骨架

3.1 隐式游标:DML 自动管理,属性挂在 SQL 上

隐式游标不需要你声明,任何UPDATE、DELETE、INSERT执行时 Oracle 自动创建,属性通过SQL%属性访问。它的%ISOPEN永远是FALSE,因为 Oracle 在执行完 DML 后立刻关闭了它。

DECLARE v_empno emp.empno%TYPE := 7000; BEGIN UPDATE emp SET ename = 'fxe' WHERE empno = v_empno; IF SQL%FOUND THEN DBMS_OUTPUT.PUT_LINE(SQL%ROWCOUNT || ' 行被更新'); END IF; IF SQL%NOTFOUND THEN DBMS_OUTPUT.PUT_LINE('雇员编号 ' || v_empno || ' 不存在'); END IF; END; /

这里的关键点是:SQL%ROWCOUNT必须在 DML 之后、下一条 DML 之前读取,否则会被覆盖。我见过有人在IF里先调了一次SQL%ROWCOUNT,再在ELSE分支里又调一次,结果第二次拿到的是 0,因为中间夹了别的语句。

3.2 显式游标:声明、打开、取值、关闭四步走

显式游标是静态的,声明时就绑定了 SQL。标准四步:

DECLARE CURSOR emp_cur IS SELECT * FROM emp; empRecord emp%ROWTYPE; BEGIN OPEN emp_cur; LOOP FETCH emp_cur INTO empRecord; EXIT WHEN emp_cur%NOTFOUND; DBMS_OUTPUT.PUT_LINE(empRecord.ename); END LOOP; CLOSE emp_cur; END; /

EXIT WHEN必须放在FETCH之后、业务逻辑之前。原因:FETCH取不到行时%NOTFOUND才变TRUE,如果你把EXIT写在FETCH前面,第一次循环时%NOTFOUND还是初始的FALSE,会多处理一行空数据。

带参数的显式游标把过滤条件参数化:

DECLARE CURSOR emp_cur(dest VARCHAR2) IS SELECT * FROM emp WHERE empno = dest; empRecord emp%ROWTYPE; BEGIN OPEN emp_cur(7369); LOOP FETCH emp_cur INTO empRecord; EXIT WHEN emp_cur%NOTFOUND; DBMS_OUTPUT.PUT_LINE(empRecord.ename); END LOOP; CLOSE emp_cur; END; /

3.3 游标更新:FOR UPDATE配合WHERE CURRENT OF

当你要在遍历过程中更新「当前行」,声明游标时必须加FOR UPDATE,更新时用WHERE CURRENT OF 游标名,这样 Oracle 会锁定活动集里的行,避免并发修改。

DECLARE old_sal NUMBER(4); emp_name VARCHAR2(20); CURSOR emp_cur IS SELECT ename, sal FROM emp WHERE sal < 1000 FOR UPDATE OF sal; BEGIN OPEN emp_cur; LOOP FETCH emp_cur INTO emp_name, old_sal; EXIT WHEN emp_cur%NOTFOUND; UPDATE emp SET sal = 1.1 * old_sal WHERE CURRENT OF emp_cur; DBMS_OUTPUT.PUT_LINE(emp_name || ' 更新成功'); END LOOP; CLOSE emp_cur; END; /

3.4 循环游标:省掉 OPEN/FETCH/CLOSE 的简化写法

如果你只是要遍历全部记录、不需要手动控制打开关闭,用FOR ... IN循环游标最省事,Oracle 自动完成打开、取值、关闭:

DECLARE CURSOR emp_cur IS SELECT empno, ename, sal FROM emp; BEGIN FOR empRecord IN emp_cur LOOP DBMS_OUTPUT.PUT_LINE(empRecord.empno || ' ' || empRecord.ename || ' ' || empRecord.sal); END LOOP; END; /

注意empRecord是隐式声明的记录变量,不需要你提前定义,也不能在循环外引用。

3.5 REF 游标:运行时绑定 SQL 的动态游标

REF 游标分两步:先声明类型,再声明变量。强类型带RETURN,弱类型不带。

DECLARE TYPE emp_cur IS REF CURSOR RETURN emp%ROWTYPE; -- 强类型 empObj emp_cur; empRecord emp%ROWTYPE; BEGIN OPEN empObj FOR SELECT * FROM emp; LOOP FETCH empObj INTO empRecord; EXIT WHEN empObj%NOTFOUND; DBMS_OUTPUT.PUT_LINE(empRecord.ename); END LOOP; CLOSE empObj; END; /

弱类型就是把RETURN emp%ROWTYPE去掉,这样同一个变量可以OPEN FOR不同的 SELECT。REF 游标最大的价值是可以作为存储过程的OUT参数,把结果集返回给调用方,这是静态游标做不到的。

4. 验证请求:跑一遍看属性对不对

把下面这段放进 SQL Developer 执行,验证%ROWCOUNT和%NOTFOUND的行为:

DECLARE CURSOR emp_cur IS SELECT ename FROM emp WHERE sal > 5000; v_name emp.ename%TYPE; v_count NUMBER := 0; BEGIN OPEN emp_cur; LOOP FETCH emp_cur INTO v_name; EXIT WHEN emp_cur%NOTFOUND; v_count := v_count + 1; DBMS_OUTPUT.PUT_LINE('第 ' || v_count || ' 行: ' || v_name); END LOOP; DBMS_OUTPUT.PUT_LINE('游标属性 %ROWCOUNT = ' || emp_cur%ROWCOUNT); CLOSE emp_cur; END; /

预期结果:每行输出带序号,最后一行打印%ROWCOUNT等于实际取到的行数。如果%ROWCOUNT比实际行数多 1,说明你的EXIT WHEN位置有问题——FETCH失败那次也会让%ROWCOUNT加 1,但%NOTFOUND为TRUE时你已经退出了,所以正常情况不会多。

再验证 REF 游标作为过程参数:

CREATE OR REPLACE PROCEDURE get_emp_by_dept( p_deptno IN NUMBER, p_cursor OUT SYS_REFCURSOR ) AS BEGIN OPEN p_cursor FOR SELECT empno, ename FROM emp WHERE deptno = p_deptno; END; /

调用时用SYS_REFCURSOR接收,这是 Oracle 预定义的弱类型 REF 游标,省去自己声明类型。

5. 本篇常见错排查

报错ORA-01001: invalid cursor:通常是OPEN之前就FETCH,或者CLOSE之后又FETCH。检查你的OPEN/CLOSE是否成对,循环里有没有提前CLOSE。

报错ORA-06550: PLS-00201: identifier 'SYS_REFCURSOR' must be declared:客户端版本太老,或者你在匿名块里用了但没权限。换成自己声明的TYPE ... IS REF CURSOR即可。

循环多输出一行空值:EXIT WHEN写在了FETCH前面。记住顺序永远是FETCH→EXIT WHEN %NOTFOUND→ 业务逻辑。

%ROWCOUNT拿到 0:在 DML 之后插了别的语句才读属性。隐式游标的属性必须紧跟 DML 读取。

REF 游标在包(PACKAGE)里声明报错:这是 Oracle 的限制,游标变量不能在包规范里声明,只能在过程或匿名块里声明。如果你需要跨过程传递结果集,用SYS_REFCURSOR作为参数类型。

FOR UPDATE和 REF 游标一起用报错:FOR UPDATE子句不能与游标变量一起使用,这是 REF 游标的硬限制。需要锁行的话改用静态显式游标。

WHERE CURRENT OF报ORA-01410: invalid ROWID:游标声明时没加FOR UPDATE,或者活动集被其他会话改了。确认声明语句里有FOR UPDATE OF 列名。

6. 接下来怎么用

如果你只是偶尔写几个游标,上面这些骨架复制改改就够了。但如果你在维护几十个存储过程、需要批量审查游标逻辑(比如统一检查EXIT WHEN位置、%ROWCOUNT读取时机),手动看效率很低。这种场景可以走 TaoToken 的 Coding Plan,把 PL/SQL 文件批量丢进去做静态审查:

https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite

如果你更习惯在对话里逐段调试,直接用模型对话入口贴代码问:

https://taotoken.net/chat?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

最后留一个我踩过的坑:写循环游标时不要在里面做COMMIT,FOR ... IN循环游标底层用的是隐式打开的快照,中途COMMIT可能导致ORA-01555: snapshot too old,尤其是大表遍历时。要提交就等循环结束后统一提交。

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

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

立即咨询