☰
ORACLE存储过程中游标的使用:从显式游标到游标FOR循环的完整实践
2026/10/3 22:07:02 网站建设 项目流程

1. 从一次批量更新卡住说起:ORACLE 存储过程游标到底解决什么问题

如果你写过 ORACLE 存储过程,大概率遇到过这种场景:需要按部门逐条处理员工数据,或者对满足条件的记录做批量更新,用一条UPDATE ... WHERE又搞不定复杂逻辑,只能把查询结果拿出来一条条判断。这时候 ORACLE 存储过程中游标的使用就成了绕不开的基本功。

游标本质上是一个指向查询结果集的指针。你可以把它想象成排队叫号:SELECT语句把符合条件的数据排成一队,游标就是那个叫号器,每次FETCH叫一个号,你拿到这一行的数据去处理,处理完再叫下一个。相比一次性把所有数据塞进变量,游标让你能逐行控制、逐行判断,特别适合批量数据处理里那些“每行逻辑不一样”的需求。

这篇内容面向的是已经在写 PL/SQL、但游标用得不够顺手的开发者。我会从显式游标的声明、OPEN/FETCH/CLOSE 完整流程讲起,再到游标 FOR 循环的简化写法、参数化游标、FOR UPDATE做更新删除、BULK COLLECT批量取值,最后给出可直接在 PL/SQL Developer 里运行的建表和存储过程脚本,以及逐条验证输出的操作步骤。核心检索词就是 ORACLE 存储过程游标,适合谁?适合需要做批量数据清洗、逐行校验、条件更新的后端和数据库开发同学。

我试过在几个数据迁移项目里用游标处理几十万行数据,踩过的坑主要集中在%NOTFOUND的位置、FOR UPDATE忘记加导致更新报错、以及BULK COLLECT内存占用这几处。下面按可跟做的顺序展开,每一步都给完整脚本。

2. 前置准备:建一张能跑的测试表,把游标环境搭起来

在讲游标语法之前,先把测试数据准备好。很多教程直接拿emp表举例,但emp是 ORACLE 自带示例表,不同环境不一定有,而且字段固定,不方便演示参数化游标和更新删除。我们自己建一张emp_demo表,字段和emp对齐,数据量控制在十几行,方便逐条看输出。

先执行建表语句:

-- 建表:员工演示表 CREATE TABLE emp_demo ( empno NUMBER(4) PRIMARY KEY, ename VARCHAR2(20), sal NUMBER(7,2), deptno NUMBER(2) ); -- 插入测试数据,覆盖 10、20、30 三个部门 INSERT INTO emp_demo VALUES (1001, 'SMITH', 800, 20); INSERT INTO emp_demo VALUES (1002, 'ALLEN', 1600, 30); INSERT INTO emp_demo VALUES (1003, 'WARD', 1250, 30); INSERT INTO emp_demo VALUES (1004, 'JONES', 2975, 20); INSERT INTO emp_demo VALUES (1005, 'MARTIN',1250, 30); INSERT INTO emp_demo VALUES (1006, 'BLAKE', 2850, 30); INSERT INTO emp_demo VALUES (1007, 'CLARK', 2450, 10); INSERT INTO emp_demo VALUES (1008, 'SCOTT', 3000, 20); INSERT INTO emp_demo VALUES (1009, 'KING', 5000, 10); INSERT INTO emp_demo VALUES (1010, 'TURNER',1500, 30); INSERT INTO emp_demo VALUES (1011, 'ADAMS', 1100, 20); INSERT INTO emp_demo VALUES (1012, 'JAMES', 950, 30); INSERT INTO emp_demo VALUES (1013, 'FORD', 3000, 20); INSERT INTO emp_demo VALUES (1014, 'MILLER',1300, 10); COMMIT;

建完之后确认一下数据:

SELECT deptno, COUNT(*) AS cnt, SUM(sal) AS total_sal FROM emp_demo GROUP BY deptno ORDER BY deptno;

正常应该看到 10 部门 3 人、20 部门 5 人、30 部门 6 人。这一步很关键,因为后面所有游标案例都基于这张表,数据量小、部门分布清晰,输出结果一眼能对得上。

接下来要确保DBMS_OUTPUT能显示。在 PL/SQL Developer 里,需要手动开启输出窗口:菜单栏Tools→DBMS Output,点绿色加号连接当前会话,或者直接执行:

SET SERVEROUTPUT ON SIZE UNLIMITED;

在 SQL*Plus 或 SQLcl 里,SET SERVEROUTPUT ON是必须的,否则DBMS_OUTPUT.PUT_LINE的内容不会打印。很多人第一次写游标发现“没输出”,八成是这里没开。PL/SQL Developer 的 SQL 窗口默认不显示,要单独开 DBMS Output 面板。

环境搭好后,建议先跑一个最简单的匿名块验证输出通道:

BEGIN DBMS_OUTPUT.PUT_LINE('DBMS_OUTPUT 已开启'); END; /

看到输出就说明环境没问题,可以进入游标正题了。

3. 显式游标完整流程与可复制配置:声明、OPEN、FETCH、CLOSE

显式游标是理解一切游标写法的基础。它的生命周期固定四步:声明(DECLARE)、打开(OPEN)、取值(FETCH)、关闭(CLOSE)。把这四步吃透,后面的 FOR 循环、参数化游标都是在这上面的语法糖。

先看一个标准写法,查询 10 部门员工姓名和薪水:

CREATE OR REPLACE PROCEDURE p_cursor_basic IS -- 1. 声明游标 CURSOR c_emp IS SELECT ename, sal FROM emp_demo WHERE deptno = 10; -- 声明接收变量,类型跟随表字段 v_ename emp_demo.ename%TYPE; v_sal emp_demo.sal%TYPE; BEGIN -- 2. 打开游标,此时查询才真正执行 OPEN c_emp; LOOP -- 3. 取值,把当前行赋给变量 FETCH c_emp INTO v_ename, v_sal; -- 取完判断是否还有数据,没有就退出 EXIT WHEN c_emp%NOTFOUND; DBMS_OUTPUT.PUT_LINE('ename:' || v_ename || ' sal:' || v_sal); END LOOP; -- 4. 关闭游标,释放资源 CLOSE c_emp; END; /

这里有几个细节值得单独说。第一,%TYPE让变量类型自动跟随表字段,表字段改了类型,变量不用改,这是 PL/SQL 里非常实用的写法。第二,EXIT WHEN c_emp%NOTFOUND的位置很关键。FETCH之后如果没取到数据,%NOTFOUND为真,此时v_ename、v_sal是上一次的值或者空,所以必须在PUT_LINE之前退出,否则最后一行会重复打印。这个坑我在多个项目里见过,输出结果总是多一行,原因就是EXIT写在了打印后面。

游标的四个属性要记牢:%FOUND(最近一次 FETCH 是否取到行)、%NOTFOUND(是否没取到)、%ROWCOUNT(到目前为止取了多少行)、%ISOPEN(游标是否打开)。%ROWCOUNT在统计处理条数时特别好用。

如果查询字段多,一个个声明变量太麻烦,可以用%ROWTYPE直接声明一个记录变量:

CREATE OR REPLACE PROCEDURE p_cursor_rowtype IS CURSOR c_emp IS SELECT ename, sal, deptno FROM emp_demo WHERE deptno = 20; v_rec c_emp%ROWTYPE; -- 记录变量,字段与游标查询列一致 BEGIN OPEN c_emp; LOOP FETCH c_emp INTO v_rec; EXIT WHEN c_emp%NOTFOUND; DBMS_OUTPUT.PUT_LINE('第' || c_emp%ROWCOUNT || '行: ' || v_rec.ename || ' 薪水:' || v_rec.sal || ' 部门:' || v_rec.deptno); END LOOP; CLOSE c_emp; END; /

c_emp%ROWTYPE的好处是字段名直接对应,v_rec.ename、v_rec.sal这样访问,不用再单独声明每个变量。当查询列很多时,这个写法能省不少代码。

关于配置,如果你用 PL/SQL Developer,存储过程编译后可以在左侧对象树里找到,右键Test直接运行,输出在 DBMS Output 面板。如果用命令行,编译和调用分开:

-- 编译 @ p_cursor_basic.sql -- 调用 BEGIN p_cursor_basic; END; /

这里给一个可复制的会话级配置片段,放在脚本开头,保证输出和格式正常:

SET SERVEROUTPUT ON SIZE UNLIMITED FORMAT WRAPPED; SET LINESIZE 200; SET PAGESIZE 100; ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS';

SET SERVEROUTPUT ON SIZE UNLIMITED解决输出被截断的问题,FORMAT WRAPPED让长行自动换行。LINESIZE和PAGESIZE影响查询结果的显示宽度和分页,调试时调大一点看着舒服。

4. 游标 FOR 循环、参数化游标与 BULK COLLECT 的验证请求与成功结果

显式游标四步走虽然清晰,但每次都要 OPEN、FETCH、CLOSE,代码啰嗦。游标 FOR 循环把这三步自动化了:循环开始时隐式打开游标,每次迭代隐式 FETCH,循环结束隐式关闭。你只需要写循环体。

CREATE OR REPLACE PROCEDURE p_cursor_for IS CURSOR c_emp IS SELECT ename, sal FROM emp_demo WHERE deptno = 30; BEGIN FOR r IN c_emp LOOP DBMS_OUTPUT.PUT_LINE('第' || c_emp%ROWCOUNT || '雇员: ' || r.ename || ' 薪水:' || r.sal); END LOOP; END; /

注意r是隐式声明的记录变量,作用域只在循环内,字段直接用r.ename访问。c_emp%ROWCOUNT在 FOR 循环里依然可用,能拿到当前处理到第几行。这种写法最省心,日常开发里用得最多。

更进一步,连游标声明都可以省掉,直接在 FOR 里写子查询:

CREATE OR REPLACE PROCEDURE p_cursor_inline IS BEGIN FOR r IN (SELECT empno, ename, sal, deptno FROM emp_demo ORDER BY empno) LOOP DBMS_OUTPUT.PUT_LINE('员工编号:' || r.empno || ', 姓名:' || r.ename || ', 薪水:' || r.sal || ', 部门:' || r.deptno); END LOOP; END; /

这种“隐式游标 FOR 循环”适合一次性查询,不需要复用游标定义的场景。代码短,可读性好。

参数化游标解决的是同一个查询逻辑、不同参数重复使用的问题。游标声明时带参数,OPEN 时传入:

CREATE OR REPLACE PROCEDURE p_cursor_param(p_deptno NUMBER) IS CURSOR c_emp(p_no NUMBER) IS SELECT ename, sal FROM emp_demo WHERE deptno = p_no; BEGIN FOR r IN c_emp(p_deptno) LOOP DBMS_OUTPUT.PUT_LINE('部门' || p_deptno || ' 第' || c_emp%ROWCOUNT || '人: ' || r.ename || ' 薪水:' || r.sal); END LOOP; END; /

调用时传不同部门号,就能复用同一段逻辑:

BEGIN p_cursor_param(10); DBMS_OUTPUT.PUT_LINE('----'); p_cursor_param(20); END; /

预期输出是 10 部门 3 行、分隔线、20 部门 5 行。参数化游标在报表类存储过程里特别常见,一个过程处理多个部门,参数一传就行。

当数据量大时,逐行 FETCH 效率低,BULK COLLECT可以一次性把结果集批量加载到集合里,减少上下文切换:

CREATE OR REPLACE PROCEDURE p_cursor_bulk IS CURSOR c_emp IS SELECT ename FROM emp_demo WHERE deptno = 30; TYPE t_ename IS TABLE OF emp_demo.ename%TYPE; v_names t_ename; BEGIN OPEN c_emp; FETCH c_emp BULK COLLECT INTO v_names; CLOSE c_emp; DBMS_OUTPUT.PUT_LINE('共取到 ' || v_names.COUNT || ' 条'); FOR i IN 1 .. v_names.COUNT LOOP DBMS_OUTPUT.PUT_LINE('第' || i || '个: ' || v_names(i)); END LOOP; END; /

BULK COLLECT把 30 部门 6 条记录一次性装进v_names集合,v_names.COUNT是元素个数,用FOR i IN 1 .. v_names.COUNT遍历。注意集合下标从 1 开始,不是 0。数据量特别大时,可以配合LIMIT分批取,避免一次性占用过多内存:

FETCH c_emp BULK COLLECT INTO v_names LIMIT 100;

这样每次最多取 100 行,循环处理,兼顾效率和内存。

5. 本篇常见错排查:从 ORA 报错到输出异常逐条对照

游标用起来不难,但报错信息有时候不够直观。下面按真实遇到的报错逐条对照。

ORA-01001: invalid cursor(无效的游标)。常见原因是游标没 OPEN 就 FETCH,或者已经 CLOSE 了还 FETCH。显式游标必须严格按 OPEN → FETCH → CLOSE 顺序。如果你在 FOR 循环里又手动 OPEN,会报这个错,因为 FOR 循环已经隐式打开了。检查代码里有没有重复 OPEN。

ORA-01002: fetch out of sequence(提取顺序错误)。典型场景是用了FOR UPDATE游标,但在 FETCH 之前就做了 COMMIT,或者循环里 COMMIT 之后继续 FETCH。FOR UPDATE游标在 COMMIT 后游标状态失效,再 FETCH 就报这个。解决办法是把 COMMIT 放到循环外,或者用FOR UPDATE NOWAIT并重新组织事务边界。

ORA-06550 / PLS-00201: identifier must be declared(标识符未声明)。多半是游标名或变量名拼错,或者游标声明在了 BEGIN 之后。游标必须在IS/AS和BEGIN之间的声明区声明,不能写在 BEGIN 里面。另外%ROWTYPE引用的游标必须在当前作用域可见。

输出结果最后一行重复。这是EXIT WHEN c_emp%NOTFOUND位置错误导致的。正确顺序是 FETCH → EXIT WHEN %NOTFOUND → 处理数据。如果写成 FETCH → 处理数据 → EXIT WHEN %NOTFOUND,最后一行会被处理两次。这个错误不报异常,只是结果多一行,最容易被忽略。

DBMS_OUTPUT 没有输出。先确认SET SERVEROUTPUT ON,PL/SQL Developer 还要确认 DBMS Output 面板已连接。另外PUT_LINE有缓冲区大小限制,默认 20000 字节,输出太多会被截断,用SET SERVEROUTPUT ON SIZE UNLIMITED解决。

ORA-01410: invalid ROWID或WHERE CURRENT OF报错。用WHERE CURRENT OF cursor_name做更新删除时,游标定义必须带FOR UPDATE,否则报错。而且WHERE CURRENT OF只能用于显式游标,不能用于 FOR 循环的隐式游标。看下面这个正确写法:

CREATE OR REPLACE PROCEDURE p_cursor_update IS CURSOR c_emp IS SELECT empno, ename, sal FROM emp_demo WHERE deptno = 30 FOR UPDATE; -- 必须加 FOR UPDATE BEGIN FOR r IN c_emp LOOP IF r.sal < 1500 THEN UPDATE emp_demo SET sal = sal + 200 WHERE CURRENT OF c_emp; DBMS_OUTPUT.PUT_LINE('加薪: ' || r.ename || ' ' || r.sal || ' -> ' || (r.sal + 200)); END IF; END LOOP; COMMIT; -- 循环外提交 END; /

注意FOR UPDATE会锁定查询到的行,循环里不要 COMMIT,否则游标失效。COMMIT 放在循环结束后。

BULK COLLECT 内存溢出。一次性把百万行加载到集合里,PGA 内存扛不住,报 ORA-04030 或直接会话挂掉。解决办法是加LIMIT分批取,配合外层循环:

LOOP FETCH c_emp BULK COLLECT INTO v_names LIMIT 500; EXIT WHEN v_names.COUNT = 0; FOR i IN 1 .. v_names.COUNT LOOP -- 处理 NULL; END LOOP; END LOOP;

游标变量(REF CURSOR)报 ORA-01000: maximum open cursors exceeded。REF CURSOR 是动态游标,打开后忘记关闭会累积,超过open_cursors参数上限就报错。检查每个 OPEN 是否都有对应 CLOSE,异常处理里也要 CLOSE。可以查当前打开数:

SELECT COUNT(*) FROM v$open_cursor WHERE sid = SYS_CONTEXT('USERENV','SID');

如果确实需要更多,让 DBA 调大open_cursors,但根本办法还是及时关闭。

6. 把游标用进真实批量处理:从脚本到验证的收尾建议

游标真正的价值在批量数据处理。前面每个案例都是独立存储过程,实际项目里往往要把查询、判断、更新、日志串起来。这里给一个综合案例:遍历所有部门,统计每个部门人数和总薪水,把结果写入一张统计表,同时打印处理日志。

先建统计表:

CREATE TABLE dept_stat ( deptno NUMBER(2), emp_cnt NUMBER(4), total_sal NUMBER(10,2), stat_time DATE );

存储过程用参数化游标加 FOR 循环:

CREATE OR REPLACE PROCEDURE p_dept_stat IS CURSOR c_dept IS SELECT DISTINCT deptno FROM emp_demo ORDER BY deptno; v_cnt NUMBER; v_total NUMBER; BEGIN FOR d IN c_dept LOOP SELECT COUNT(*), NVL(SUM(sal), 0) INTO v_cnt, v_total FROM emp_demo WHERE deptno = d.deptno; INSERT INTO dept_stat(deptno, emp_cnt, total_sal, stat_time) VALUES (d.deptno, v_cnt, v_total, SYSDATE); DBMS_OUTPUT.PUT_LINE('部门' || d.deptno || ' 人数:' || v_cnt || ' 总薪水:' || v_total); END LOOP; COMMIT; END; /

执行并验证:

BEGIN p_dept_stat; END; / SELECT deptno, emp_cnt, total_sal, TO_CHAR(stat_time, 'YYYY-MM-DD HH24:MI:SS') AS stat_time FROM dept_stat ORDER BY deptno;

预期输出三个部门:10 部门 3 人、20 部门 5 人、30 部门 6 人,总薪水分别是 8750、11425、9350。如果数字对不上,回头检查emp_demo数据是否插全。

几个收尾建议。第一,游标命名统一加前缀c_,变量加v_,参数加p_,团队协作时一眼能分清。第二,循环里尽量不做 DDL 和 COMMIT,事务边界放在循环外。第三,处理大数据量优先考虑BULK COLLECT加LIMIT,逐行 FETCH 只在数据量小或逻辑复杂时用。第四,异常处理里记得关闭游标,避免资源泄漏:

EXCEPTION WHEN OTHERS THEN IF c_emp%ISOPEN THEN CLOSE c_emp; END IF; RAISE;

如果你在写更复杂的 Agent 或自动化脚本,需要调用大模型来生成或审查 PL/SQL 代码,可以配合 TaoToken 的模型对话能力做代码辅助,接入文档在 https://taotoken.net/api 有说明,API Key 在 https://taotoken.net/api-keys 获取。长期做数据库开发自动化的,Coding Plan 在 https://taotoken.net/coding-plan 有更完整的方案。游标这块把上面十几个案例跑一遍,基本就能覆盖日常存储过程开发里 90% 的场景了。

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

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

立即咨询