
1. 为什么你写的 PL/SQL 游标总是取不到数据如果你正在写 Oracle 存储过程需要把一张表或多表关联的结果集返回给上层应用或者需要在循环里逐行处理数据那游标Cursor和游标变量REF CURSOR几乎是绕不开的东西。我见过太多人卡在同一个地方游标声明了、OPEN了、FETCH了结果DBMS_OUTPUT一行都不打印或者存储过程编译通过但调用时报ORA-01001: invalid cursor、ORA-06550这类错误。问题往往不在 SQL 本身而在游标的生命周期没管对——什么时候打开、什么时候取值、什么时候关闭以及%FOUND、%NOTFOUND、%ROWCOUNT这几个属性到底在哪个时刻是有效的。这篇内容聚焦三件事显式游标的完整四步流程、隐式游标和游标 FOR 循环的省事写法、以及 REF CURSOR 游标变量在存储过程返回结果集场景下的配置骨架。每一段都给出可以直接复制到 SQL*Plus 或 SQL Developer 里跑的代码并附上执行后的预期输出和常见报错的处理动作。适合已经会写基本 SELECT、但一碰到游标就心里没底的开发同学。下面所有示例基于 Oracle 的EMP、DEPT经典表结构你换成自己的表名和列名即可。2. 显式游标声明、打开、取值、关闭四步拆解显式游标是你自己用CURSOR关键字声明的游标它和一条固定的 SELECT 语句绑定。使用它必须走完四个动作DECLARE声明、OPEN打开、FETCH取值、CLOSE关闭。少一步都不行尤其是CLOSE忘了关会消耗数据库的游标资源。2.1 带参数的显式游标声明带参数的游标让同一段逻辑可以复用到不同的过滤条件上。声明时把参数写在游标名后面的括号里类型只写数据类型不写长度DECLARE CURSOR c_emp(p_deptno NUMBER) IS SELECT empno, ename, sal FROM emp WHERE deptno p_deptno ORDER BY empno; v_empno emp.empno%TYPE; v_ename emp.ename%TYPE; v_sal emp.sal%TYPE; BEGIN OPEN c_emp(20); LOOP FETCH c_emp INTO v_empno, v_ename, v_sal; EXIT WHEN c_emp%NOTFOUND; DBMS_OUTPUT.PUT_LINE(v_empno || - || v_ename || - || v_sal); END LOOP; CLOSE c_emp; END; /执行前记得先SET SERVEROUTPUT ON;否则DBMS_OUTPUT.PUT_LINE的内容不会显示。这段代码里EXIT WHEN c_emp%NOTFOUND的位置很关键它必须放在FETCH之后、处理数据之前。因为%NOTFOUND反映的是上一次 FETCH 是否没取到行如果放在 FETCH 之前判断第一次循环时属性还没被赋值逻辑就乱了。2.2 用 %ROWTYPE 简化变量声明当 SELECT 的列很多时一个个声明变量很烦。用游标名%ROWTYPE定义一个记录变量字段名就是 SELECT 的列名或列别名DECLARE CURSOR c_emp IS SELECT empno, ename, sal, deptno FROM emp WHERE sal 1000; r_emp c_emp%ROWTYPE; BEGIN OPEN c_emp; LOOP FETCH c_emp INTO r_emp; EXIT WHEN c_emp%NOTFOUND; DBMS_OUTPUT.PUT_LINE(r_emp.ename || 工资 || r_emp.sal || 部门 || r_emp.deptno); END LOOP; CLOSE c_emp; END; /r_emp.ename、r_emp.sal这些字段名直接来自 SELECT 列表改 SQL 时只要列名不变记录变量的访问代码就不用动维护成本低很多。2.3 四个游标属性分别在什么时候有效%ISOPEN、%FOUND、%NOTFOUND、%ROWCOUNT是显式游标自带的四个属性但它们的有效时机不一样用错时机是取不到数据的常见原因。属性含义有效时机%ISOPEN游标是否已打开OPEN 之后、CLOSE 之前%FOUND上一次 FETCH 是否取到行FETCH 之后%NOTFOUND上一次 FETCH 是否没取到行FETCH 之后%ROWCOUNT到当前位置已取出的行数OPEN 之后、CLOSE 之前%ROWCOUNT在 OPEN 之后是 0每成功 FETCH 一行加 1。如果你想只处理前 N 行可以在循环里判断IF c_emp%ROWCOUNT N THEN EXIT; END IF;。注意%ROWCOUNT统计的是已取出的行数不是结果集总行数别拿它当总数用。3. 隐式游标与游标 FOR 循环少写代码的两种方式不是所有场景都需要手写四步。Oracle 提供了两种更省事的写法理解它们的边界能帮你少踩坑。3.1 隐式游标SELECT INTO 背后的自动游标当你在 PL/SQL 里写SELECT ... INTO ...时Oracle 会自动创建一个隐式游标来执行这条查询。你不需要声明、打开、关闭它但必须保证查询只返回一行否则会抛NO_DATA_FOUND或TOO_MANY_ROWSDECLARE v_ename emp.ename%TYPE; v_sal emp.sal%TYPE; BEGIN SELECT ename, sal INTO v_ename, v_sal FROM emp WHERE empno 7369; DBMS_OUTPUT.PUT_LINE(v_ename || 的工资是 || v_sal); EXCEPTION WHEN NO_DATA_FOUND THEN DBMS_OUTPUT.PUT_LINE(没有找到该员工); WHEN TOO_MANY_ROWS THEN DBMS_OUTPUT.PUT_LINE(查询返回了多行请检查条件); END; /隐式游标也有属性通过SQL%FOUND、SQL%ROWCOUNT访问常用于判断 UPDATE、DELETE 影响了几行BEGIN UPDATE emp SET sal sal * 1.1 WHERE deptno 20; DBMS_OUTPUT.PUT_LINE(更新了 || SQL%ROWCOUNT || 行); IF SQL%NOTFOUND THEN DBMS_OUTPUT.PUT_LINE(没有匹配的行); END IF; END; /3.2 游标 FOR 循环自动打开、取值、关闭游标 FOR 循环把 OPEN、FETCH、CLOSE 全包了循环变量自动声明为记录类型退出循环时游标自动关闭。这是日常写得最多的一种形式BEGIN FOR r IN (SELECT deptno, dname FROM dept ORDER BY deptno) LOOP DBMS_OUTPUT.PUT_LINE(部门 || r.deptno || : || r.dname); END LOOP; END; /也可以先声明游标再在 FOR 里用DECLARE CURSOR c_emp(p_deptno NUMBER) IS SELECT ename, sal FROM emp WHERE deptno p_deptno; BEGIN FOR r IN c_emp(30) LOOP DBMS_OUTPUT.PUT_LINE(r.ename || - || r.sal); END LOOP; END; /注意在游标 FOR 循环内部不要再写OPEN、FETCH、CLOSE否则会报PLS-00307之类的错误。循环变量r是只读的不能给它赋值。4. REF CURSOR 游标变量存储过程返回结果集的标准做法显式游标是静态的声明时就和一条 SQL 绑死了。如果你需要根据运行时条件动态决定查哪张表或者要把结果集返回给调用方比如 Java 的CallableStatement、Python 的cursor就得用游标变量 REF CURSOR。4.1 强类型与弱类型的区别REF CURSOR 分两种。强类型用RETURN 表名%ROWTYPE限定返回结构弱类型不限定可以打开任意结果集DECLARE -- 弱类型不限定返回结构 TYPE t_weak_cur IS REF CURSOR; v_cur t_weak_cur; v_id NUMBER; v_name VARCHAR2(50); BEGIN OPEN v_cur FOR SELECT deptno, dname FROM dept WHERE deptno :1 USING 10; LOOP FETCH v_cur INTO v_id, v_name; EXIT WHEN v_cur%NOTFOUND; DBMS_OUTPUT.PUT_LINE(v_id || - || v_name); END LOOP; CLOSE v_cur; END; /弱类型配合动态 SQL 字符串可以在运行时拼不同的查询。强类型则在编译期就能检查字段是否匹配更安全但灵活性差一些。4.2 存储过程返回结果集的完整骨架这是实际项目里最常用的模式在包PACKAGE里定义强类型 REF CURSOR存储过程通过 OUT 参数把游标变量传出去。先建包规范CREATE OR REPLACE PACKAGE pkg_emp_query AS TYPE t_emp_cur IS REF CURSOR RETURN emp%ROWTYPE; PROCEDURE get_emp_by_dept( p_deptno IN NUMBER, p_cur OUT t_emp_cur ); END pkg_emp_query; /再建包体CREATE OR REPLACE PACKAGE BODY pkg_emp_query AS PROCEDURE get_emp_by_dept( p_deptno IN NUMBER, p_cur OUT t_emp_cur ) IS BEGIN OPEN p_cur FOR SELECT * FROM emp WHERE deptno p_deptno ORDER BY empno; END get_emp_by_dept; END pkg_emp_query; /调用方拿到的是游标变量可以继续 FETCH也可以交给客户端驱动去遍历。这种写法的好处是结果集不在数据库端缓存客户端取一批处理一批内存占用可控。4.3 动态 SQL 打开游标变量的写法当查询条件或表名需要运行时决定时用字符串拼 SQL 再OPEN ... FORCREATE OR REPLACE PROCEDURE get_dynamic( p_table IN VARCHAR2, p_cur OUT SYS_REFCURSOR ) IS v_sql VARCHAR2(500); BEGIN v_sql : SELECT * FROM || DBMS_ASSERT.SIMPLE_SQL_NAME(p_table) || WHERE ROWNUM 10; OPEN p_cur FOR v_sql; END; /SYS_REFCURSOR是 Oracle 预定义的弱类型游标变量不用自己声明 TYPE直接用就行。DBMS_ASSERT.SIMPLE_SQL_NAME用来校验表名合法性防止拼接注入这个习惯建议保留。5. 验证请求跑通并确认结果集正确返回写完游标代码怎么确认它真的按预期返回了数据分两步验证。第一步在 SQL*Plus 或 SQL Developer 里直接执行匿名块用DBMS_OUTPUT打印结果SET SERVEROUTPUT ON SIZE UNLIMITED; DECLARE v_cur SYS_REFCURSOR; v_empno emp.empno%TYPE; v_ename emp.ename%TYPE; BEGIN OPEN v_cur FOR SELECT empno, ename FROM emp WHERE deptno 20; LOOP FETCH v_cur INTO v_empno, v_ename; EXIT WHEN v_cur%NOTFOUND; DBMS_OUTPUT.PUT_LINE(员工: || v_empno || / || v_ename); END LOOP; DBMS_OUTPUT.PUT_LINE(共取出 || v_cur%ROWCOUNT || 行); CLOSE v_cur; END; /预期输出是部门 20 的若干员工记录最后一行显示总行数。如果一行都不打印先检查SET SERVEROUTPUT ON是否执行了再检查 WHERE 条件是否真的匹配到数据。第二步验证存储过程返回的游标变量。在 SQL Developer 里可以直接用测试窗口或者写一段匿名块调用DECLARE v_cur pkg_emp_query.t_emp_cur; r_emp emp%ROWTYPE; BEGIN pkg_emp_query.get_emp_by_dept(30, v_cur); LOOP FETCH v_cur INTO r_emp; EXIT WHEN v_cur%NOTFOUND; DBMS_OUTPUT.PUT_LINE(r_emp.empno || || r_emp.ename); END LOOP; CLOSE v_cur; END; /如果调用时报PLS-00306: wrong number or types of arguments多半是 OUT 参数的类型和包规范里定义的不一致检查t_emp_cur是否用对了。6. 本篇常见报错排查ORA-01001: invalid cursor游标没打开就 FETCH或者已经 CLOSE 了还在 FETCH。检查 OPEN 和 CLOSE 是否配对循环里有没有提前 CLOSE。ORA-01000: maximum open cursors exceeded游标打开后没关闭累积超过open_cursors参数上限。用SHOW PARAMETER open_cursors查看当前值同时排查代码里所有 OPEN 是否都有对应的 CLOSE。游标 FOR 循环不会出这个问题因为它自动关闭。ORA-06550 / PLS-00307: too many declarations of ...游标 FOR 循环里又写了 OPEN 或 FETCH。记住 FOR 循环内部不能手动操作游标。ORA-01422: exact fetch returns more than requested number of rows隐式游标SELECT INTO返回了多行。给查询加上AND ROWNUM 1或者补全 WHERE 条件或者改用显式游标遍历。ORA-01403: no data foundSELECT INTO没查到数据。加EXCEPTION WHEN NO_DATA_FOUND处理或者先判断存在性再查。PLS-00201: identifier SYS_REFCURSOR must be declared数据库版本太老低于 9i不支持SYS_REFCURSOR需要自己TYPE ... IS REF CURSOR声明。存储过程编译通过但调用方取不到数据检查 OUT 参数是否真的被 OPEN 了。如果过程里走了 IF 分支但没进 OPEN 的那条路游标变量就是未打开状态调用方 FETCH 会报错。7. 把游标用对从能跑通到能维护游标本身不复杂难的是把生命周期管清楚。显式游标记住四步走隐式游标记住只返回一行游标 FOR 循环记住别手动干预REF CURSOR 记住强类型更安全、弱类型更灵活。实际项目里返回结果集给客户端优先用 REF CURSOR库内批量处理优先用游标 FOR 循环单行查询用隐式游标就够了。如果你在配置存储过程、调试游标报错时需要快速验证某段 SQL 的执行结果可以借助模型对话能力把报错信息和代码片段一起丢进去做推理比翻文档快。日常写 PL/SQL 和存储过程这类需要反复调试的活儿用 Coding Plan 把常用游标模板沉淀下来下次直接改表名就能用省掉重复搭骨架的时间。接入前先在 API Keys 页面生成密钥再对照接入文档把连接串配好就能在本地环境里跑通上面这些验证步骤了。