游标CURSOR的基本用法:从PLSQL到REF CURSOR的实战解析
1. 从一段报错说起为什么你的 PL/SQL 游标跑不通刚接触 Oracle PL/SQL 的同学十有八九会在游标这块卡一下。我见过最常见的场景是这样的照着某篇博客把students%rowtype写进声明区结果 Oracle 直接甩一个PLS-00302: 必须声明 STU_REC 组件或者ORA-06550出来人当场就懵了。其实问题不在你而在于很多示例代码是从 PostgreSQL 的 PL/pgSQL 直接搬过来的Oracle 对记录类型的处理方式跟它不一样。游标 CURSOR 到底是什么你可以把它理解成数据库在内存里开的一个结果集传送带。SQL 查询出来的行不会一次性全塞给你而是放在这个传送带上你用FETCH一行一行地取。这个机制在 Oracle 里叫游标在 PL/SQL 里分两大类静态游标显式 隐式和动态游标REF CURSOR。显式游标是你自己声明、自己打开、自己取、自己关隐式游标是FOR ... IN ...那种写法系统帮你把打开和关闭都包了REF CURSOR 则是运行时才决定查什么的游标常用于存储过程向客户端返回结果集。这篇文章面向的是数据库开发初学者和从其他数据库转型过来的工程师。我会把显式游标、隐式游标、REF CURSOR 的完整流程拆开讲每个环节都给可复制的脚本并且告诉你哪些地方容易踩坑。目标很明确看完你就能在自己的 Oracle 环境里把游标跑起来遇到报错也知道往哪查。在开始写代码之前先提一句环境准备的事。如果你本地没有 Oracle 实例可以用 Docker 拉一个 Oracle XE或者连公司的测试库。另外PL/SQL 的输出默认是不显示的你得先执行SET SERVEROUTPUT ON不然DBMS_OUTPUT.PUT_LINE打了等于没打。这个坑我后面还会在排错章节再强调一次。2. 显式游标四步走声明、打开、取值、关闭显式游标是理解游标机制的最佳入口因为它的每一步都是你手动控制的逻辑最清晰。它的生命周期就四步DECLARE里声明、BEGIN里OPEN、LOOP里FETCH、最后CLOSE。下面这段代码是我在 Oracle 里实测能跑通的版本注意记录类型的写法跟 PostgreSQL 不一样。SET SERVEROUTPUT ON; DECLARE -- 第一步声明游标此时只是分配了名字SQL 还没执行 CURSOR stu_cur IS SELECT stu.stu_no, stu.name, stu.age, stu.sex, stu.course, stu.score FROM students stu ORDER BY stu.stu_no; -- Oracle 里推荐用 %ROWTYPE 或自定义 RECORD不要照搬 PG 的写法 TYPE type_stu_rec IS RECORD( stu_no students.stu_no%TYPE, name students.name%TYPE, age students.age%TYPE, sex students.sex%TYPE, course students.course%TYPE, score students.score%TYPE ); stu_rec type_stu_rec; BEGIN -- 第二步打开游标此时 SQL 才真正执行结果集进入缓冲区 OPEN stu_cur; -- 第三步循环取值 LOOP EXIT WHEN stu_cur%NOTFOUND; -- 取不到数据就退出 FETCH stu_cur INTO stu_rec; -- 取一行游标下移 DBMS_OUTPUT.PUT_LINE(stu_rec.stu_no || | || stu_rec.name || | || stu_rec.score); END LOOP; -- 第四步关闭游标释放资源 CLOSE stu_cur; END; /这里有几个细节值得展开。%NOTFOUND是游标的属性表示上一次FETCH有没有取到行。注意它的判断位置——必须在FETCH之后、处理数据之前否则会多输出一行空数据。另外%ROWCOUNT可以拿到当前已经取了多少行%ISOPEN判断游标是否打开%FOUND跟%NOTFOUND相反。这四个属性在调试的时候特别好用。关于记录类型Oracle 其实支持stu_rec students%ROWTYPE前提是你的 SELECT 列跟表结构完全对应。但如果你只查了部分列或者做了计算就必须用自定义RECORD。我建议初学者统一用自定义 RECORD虽然多写几行但不容易出类型不匹配的错。嵌套游标也是常被问到的。两层嵌套的逻辑跟单层一样外层OPEN之后LOOP内层再OPEN再LOOP注意内层游标要在内层循环里关闭别关错了。如果内层游标依赖外层的某个字段值那内层游标声明时就要带上参数这个用法在 REF CURSOR 那节会更自然。显式游标的优点是控制粒度细你可以在FETCH之间插入任意逻辑比如累加、条件判断、写日志。缺点就是代码量大一个简单的遍历要写十几行。所以实际工作中如果只是单纯遍历大家更倾向用隐式游标。3. 隐式游标 FOR 循环少写一半代码的写法隐式游标在 PL/SQL 里就是FOR rec IN cursor_name LOOP这种形式。你不需要OPEN、FETCH、CLOSE系统自动帮你完成。代码量直接砍半可读性也更好。下面这段跟上一节的显式游标功能完全等价。SET SERVEROUTPUT ON; DECLARE CURSOR stu_cur IS SELECT stu.stu_no, stu.name, stu.age, stu.sex, stu.course, stu.score FROM students stu ORDER BY stu.stu_no; BEGIN FOR stu_rec IN stu_cur LOOP DBMS_OUTPUT.PUT_LINE(stu_rec.stu_no || | || stu_rec.name || | || stu_rec.score); END LOOP; END; /注意这里stu_rec不需要你提前声明FOR 循环会自动把它定义成跟游标结果集匹配的记录类型。这也是隐式游标最舒服的地方——少写声明少出错。隐式游标还有另一种更简化的写法连游标声明都省了直接把 SQL 写在 FOR 里BEGIN FOR r IN (SELECT stu_no, name, score FROM students ORDER BY stu_no) LOOP DBMS_OUTPUT.PUT_LINE(r.stu_no || | || r.name || | || r.score); END LOOP; END; /这种写法在临时查询、脚本处理里用得最多。但要注意它每次执行都会重新解析 SQL如果放在大循环里反复调用性能不如显式游标。所以我的经验是一次性遍历用隐式需要复用或者带参数的用显式。隐式游标也有属性可以访问但访问方式跟显式不同。你不能写stu_cur%ROWCOUNT而是用SQL%ROWCOUNT、SQL%FOUND、SQL%NOTFOUND这些它们指的是最近一条 DML 或查询语句的影响行数。比如BEGIN UPDATE students SET score score 1 WHERE course Math; DBMS_OUTPUT.PUT_LINE(更新了 || SQL%ROWCOUNT || 行); IF SQL%NOTFOUND THEN DBMS_OUTPUT.PUT_LINE(没有匹配的记录); END IF; END; /这里SQL%ROWCOUNT拿到的是 UPDATE 影响的行数跟游标遍历是两回事别搞混了。很多初学者看到%NOTFOUND就以为是游标属性其实在隐式游标语境下它指的是 SQL 语句的执行结果。从实际使用频率来看隐式游标在日常开发中占绝大多数因为大部分场景就是查出来遍历一下。但面试的时候显式游标的四步流程、属性含义、跟隐式游标的区别几乎是必问的。所以两个都得会不能偏废。4. REF CURSOR 动态游标运行时才决定查什么前面两种游标有个共同限制SQL 在声明的时候就定死了。但有些场景你没法提前知道要查哪张表、哪个条件比如根据传入参数决定查students还是test或者要把结果集返回给 Java/Python 客户端。这时候就要用 REF CURSOR。REF CURSOR 的核心特点是打开时才绑定 SQL。它分两种强类型带返回类型和弱类型不带。弱类型更灵活先看一个基础示例SET SERVEROUTPUT ON; DECLARE -- 声明 REF 游标类型弱类型 TYPE type_ref_cur IS REF CURSOR; ref_cur type_ref_cur; v_score NUMBER; BEGIN -- 根据系统日期的奇偶动态打开不同的 SQL IF MOD(TO_NUMBER(TO_CHAR(SYSDATE, dd)), 2) 1 THEN OPEN ref_cur FOR SELECT T.SCORE FROM STUDENTS T; ELSE OPEN ref_cur FOR SELECT T.SCORE FROM TEST T; END IF; LOOP EXIT WHEN ref_cur%NOTFOUND; FETCH ref_cur INTO v_score; DBMS_OUTPUT.PUT_LINE(v_score); END LOOP; CLOSE ref_cur; END; /这里OPEN ref_cur FOR ...就是动态绑定的关键。同一个游标变量可以在不同分支里打开不同的 SQL这在静态游标里是做不到的。REF CURSOR 更常见的用法是在存储过程里作为OUT参数把结果集返回给调用方。下面这个例子定义一个过程根据传入的课程名返回学生列表CREATE OR REPLACE PROCEDURE get_students_by_course( p_course IN VARCHAR2, p_cursor OUT SYS_REFCURSOR ) AS BEGIN OPEN p_cursor FOR SELECT stu_no, name, score FROM students WHERE course p_course ORDER BY score DESC; END; /SYS_REFCURSOR是 Oracle 内置的弱类型 REF CURSOR不用自己定义类型直接用就行。调用的时候在匿名块里接收DECLARE v_cur SYS_REFCURSOR; v_no students.stu_no%TYPE; v_name students.name%TYPE; v_score students.score%TYPE; BEGIN get_students_by_course(Math, v_cur); LOOP FETCH v_cur INTO v_no, v_name, v_score; EXIT WHEN v_cur%NOTFOUND; DBMS_OUTPUT.PUT_LINE(v_no || | || v_name || | || v_score); END LOOP; CLOSE v_cur; END; /这种模式在 Java 里通过 JDBC 的CallableStatement调用时特别常见registerOutParameter注册OracleTypes.CURSOR然后getCursor拿结果集。Python 的cx_Oracle也类似cursor.var(cx_Oracle.CURSOR)接收。强类型 REF CURSOR 的写法是TYPE type_ref_cur IS REF CURSOR RETURN students%ROWTYPE;它要求打开的 SQL 返回结构必须匹配。强类型更安全但灵活性差实际项目里弱类型SYS_REFCURSOR用得更多。动态游标和静态游标的区别我整理成一张表方便对照对比项静态游标显式/隐式动态游标REF CURSORSQL 绑定时机声明时固定运行时 OPEN FOR 决定能否返回客户端不能可以常作 OUT 参数定义位置可全局也可局部必须在过程/函数/块内子程序间传递不支持支持执行效率静态 SQL 更高动态解析略低典型用途内部遍历处理向客户端返回结果集效率这块要说明一下静态游标的 SQL 在编译期就解析好了执行计划可以复用REF CURSOR 每次OPEN FOR都要重新解析所以纯内部遍历优先用静态游标只有需要动态拼接或返回结果集时才用 REF CURSOR。5. 常见报错排查清单从 ORA-06550 到游标未打开游标相关的报错其实就那么几类我把踩过的坑整理成清单你对着报错信息查就行。PLS-00302 / PLS-00225记录类型不匹配。最常见的就是从 PostgreSQL 搬代码写了stu_rec students%ROWTYPE但 SELECT 的列跟表结构对不上或者干脆 Oracle 不认这种写法。解决办法是改用自定义TYPE ... IS RECORD每个字段用表.列%TYPE声明。如果 SELECT 里有计算列比如score * 1.1那必须用 RECORD 并且给计算列起别名。ORA-01001无效的游标invalid cursor。这个通常是你FETCH或CLOSE了一个没打开的游标。检查OPEN语句是不是在异常分支里被跳过了或者游标名拼错了。还有一种情况是游标已经CLOSE了又去FETCH比如循环里不小心多关了一次。ORA-06511游标已打开cursor already open。同一个游标变量被OPEN了两次。REF CURSOR 尤其容易出这个问题因为它是变量可能在多个分支里都被打开。解决办法是打开前先判断IF ref_cur%ISOPEN THEN CLOSE ref_cur; END IF;或者确保逻辑上只走一个分支。ORA-06550 / PLS-00103语法错误。这类报错信息通常指向某一行但真正的问题可能在上一行。比如END LOOP后面漏了分号或者EXIT WHEN写成了EXIT WHEN;。还有一种隐蔽的DBMS_OUTPUT.PUT_LINE里用了||拼接但某个变量没声明编译器报的是必须声明标识符。看不到任何输出。这个不算报错但最让人抓狂。99% 的原因是没执行SET SERVEROUTPUT ON。在 SQL*Plus 里它是会话级设置在 SQL Developer 里要手动点DBMS Output面板的绿色加号启用。另外注意DBMS_OUTPUT有缓冲区大小限制默认 20000 字节输出太多会被截断可以SET SERVEROUTPUT ON SIZE UNLIMITED。ORA-01422实际返回行数超过请求行数。这个跟游标关系不大但常一起出现。当你用SELECT ... INTO而结果有多行时会报这个。解决办法是加WHERE ROWNUM 1或者改用游标遍历。REF CURSOR 返回给客户端时报游标已关闭。这通常是因为存储过程里OPEN了游标但过程结束时没有保持打开状态或者调用方在FETCH之前就CLOSE了。记住 REF CURSOR 作为 OUT 参数时过程内只OPEN不CLOSE关闭的责任在调用方。排查的时候有个通用技巧在OPEN前后各打一条日志在FETCH循环里打%ROWCOUNT在CLOSE后打一条。这样能快速定位是哪个环节出的问题。另外DBMS_OUTPUT的输出顺序跟实际执行顺序一致别被 IDE 的异步刷新误导了。6. 把游标用对从练习到生产的几个建议游标这东西语法不难难的是用对场景。我自己的经验是能用一条 SQL 搞定的就别开游标。游标是逐行处理行数一多性能就下来了。比如你要更新一批数据UPDATE ... WHERE比游标遍历再逐条UPDATE快得多。游标真正的用武之地是需要复杂逐行逻辑、需要调用其他存储过程、需要向客户端返回结果集。如果确实要用游标记住几个原则。第一显式游标用完必须CLOSE虽然会话结束会释放但在长连接里不关会累积。第二隐式游标 FOR 循环优先代码短且不容易漏关。第三REF CURSOR 只用于返回结果集或动态 SQL别拿它做内部遍历。第四循环里避免做 DML 操作如果非要做考虑用BULK COLLECT和FORALL批量处理性能能差出几十倍。最后留个练习给你建一张students表插几条数据然后把本文的显式游标、隐式游标、REF CURSOR 三段代码各跑一遍再故意制造一个PLS-00302错误看看报错信息长什么样。跑通一遍比看十篇文章都管用。