ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Oracle游标学习笔记:从显式游标到游标FOR循环的实战拆解

Oracle游标学习笔记:从显式游标到游标FOR循环的实战拆解 1. 从一次“数据明明在表里游标却读不到”说起刚接触 PL/SQL 的人几乎都会在游标上卡一次。表里明明有 14 行员工数据SELECT COUNT(*)也数得出来可一写显式游标FETCH完%ROWCOUNT却是 0或者循环只跑了一遍就退出。问题往往不在 SQL 本身而在游标的“打开—取数—关闭”这条生命周期没走对。游标Cursor你可以先把它理解成指向查询结果集的一根“手指”。SELECT语句执行后Oracle 会在内存里划出一块私有工作区Private SQL Area存放结果集游标就是这块区域的句柄。你用手指一行一行往下点点一行处理一行这就是FETCH。点完了要记得把手收回来也就是CLOSE。它主要分两类显式游标由你亲手DECLARE、OPEN、FETCH、CLOSE控制粒度最细隐式游标是 Oracle 为每条 DML 和单行SELECT INTO自动开的你摸不到句柄但能通过SQL%FOUND、SQL%ROWCOUNT这些属性读到它的状态。再往上还有游标 FOR 循环把打开、取数、关闭三件事全包了写起来最省心。这篇笔记面向 Oracle 初学者和日常写 SQL 的开发者目标很具体给你一套能在 SQL*Plus 或 SQL Developer 里直接跑通的脚本从建表插数开始把显式游标、隐式游标属性、游标 FOR 循环、参数化游标、FOR UPDATE与WHERE CURRENT OF逐个拆开每段都附上执行输出让你亲眼看到%FOUND、%ROWCOUNT到底取什么值。下面所有例子都基于经典的emp表结构我会先建一张自己的测试表避免动到系统示例数据。2. 显式游标四步走OPEN / FETCH / CLOSE 与 %FOUND、%ROWCOUNT 实测先准备环境。在 SQL*Plus 或 SQL Developer 里执行下面这段建一张emp_demo表并插入 6 行数据部门号分 10、20、30 三组方便后面演示参数化游标。-- 建表 CREATE TABLE emp_demo ( empno NUMBER(4) PRIMARY KEY, ename VARCHAR2(20), job VARCHAR2(20), deptno NUMBER(2), sal NUMBER(8,2) ); -- 插入样例数据 INSERT INTO emp_demo VALUES (7369,SMITH,CLERK,20,800); INSERT INTO emp_demo VALUES (7499,ALLEN,SALESMAN,30,1600); INSERT INTO emp_demo VALUES (7521,WARD,SALESMAN,30,1250); INSERT INTO emp_demo VALUES (7566,JONES,MANAGER,20,2975); INSERT INTO emp_demo VALUES (7782,CLARK,MANAGER,10,2450); INSERT INTO emp_demo VALUES (7839,KING,PRESIDENT,10,5000); COMMIT;确认数据进去了SELECT empno, ename, deptno, sal FROM emp_demo ORDER BY empno;输出应该是 6 行从 7369 到 7839。接下来写第一个显式游标。显式游标的完整生命周期是四步DECLARE声明、OPEN打开、FETCH取数、CLOSE关闭。取数通常配一个LOOP用%NOTFOUND判断什么时候停。SET SERVEROUTPUT ON; DECLARE -- 1. 声明游标 CURSOR c_emp IS SELECT empno, ename, sal FROM emp_demo ORDER BY empno; -- 声明接收变量类型跟列对齐 v_empno emp_demo.empno%TYPE; v_ename emp_demo.ename%TYPE; v_sal emp_demo.sal%TYPE; BEGIN -- 2. 打开游标此时查询才真正执行 OPEN c_emp; DBMS_OUTPUT.PUT_LINE(打开后 %ISOPEN || CASE WHEN c_emp%ISOPEN THEN TRUE ELSE FALSE END); LOOP -- 3. 取一行 FETCH c_emp INTO v_empno, v_ename, v_sal; -- 取不到就退出 EXIT WHEN c_emp%NOTFOUND; DBMS_OUTPUT.PUT_LINE( 第 || c_emp%ROWCOUNT || 行: || v_empno || / || v_ename || / || v_sal ); END LOOP; DBMS_OUTPUT.PUT_LINE(循环结束 %ROWCOUNT || c_emp%ROWCOUNT); -- 4. 关闭游标 CLOSE c_emp; DBMS_OUTPUT.PUT_LINE(关闭后 %ISOPEN || CASE WHEN c_emp%ISOPEN THEN TRUE ELSE FALSE END); END; /执行输出打开后 %ISOPEN TRUE 第 1 行: 7369 / SMITH / 800 第 2 行: 7499 / ALLEN / 1600 第 3 行: 7521 / WARD / 1250 第 4 行: 7566 / JONES / 2975 第 5 行: 7782 / CLARK / 2450 第 6 行: 7839 / KING / 5000 循环结束 %ROWCOUNT 6 关闭后 %ISOPEN FALSE这里有几个点值得盯住。%ROWCOUNT在FETCH成功之后才自增所以第一次循环里它已经是 1不是 0。%NOTFOUND是在FETCH之后判断的——当第 7 次FETCH取不到数据时它才变TRUE此时%ROWCOUNT停在 6 不再涨。%ISOPEN在OPEN前和CLOSE后都是FALSE只有中间那段是TRUE。四个属性的取值规律可以整理成一张表方便你对照记忆属性含义典型取值时机%FOUND最近一次 FETCH 是否取到行取到为 TRUE取空为 FALSE%NOTFOUND最近一次 FETCH 是否没取到行与 %FOUND 相反%ROWCOUNT到目前为止成功取出的行数每 FETCH 成功一次加 1%ISOPEN游标是否处于打开状态OPEN 后 TRUECLOSE 后 FALSE一个常见坑EXIT WHEN c_emp%NOTFOUND必须写在FETCH之后。如果你写成先判断再FETCH第一次循环时%NOTFOUND还是初始的FALSE逻辑就乱了。另一个坑是忘记CLOSE游标一直占着私有工作区循环多了会报ORA-01000: maximum open cursors exceeded。显式游标用完就关这是纪律。3. 隐式游标属性与游标 FOR 循环少写一半代码还不出错隐式游标没有名字但 Oracle 给了它一个统一代号SQL。你执行一条UPDATE、DELETE或单行SELECT INTO之后马上就能用SQL%ROWCOUNT看影响了几行用SQL%FOUND看有没有命中。BEGIN UPDATE emp_demo SET sal sal * 1.1 WHERE deptno 30; DBMS_OUTPUT.PUT_LINE(更新了 || SQL%ROWCOUNT || 行); IF SQL%FOUND THEN DBMS_OUTPUT.PUT_LINE(确实有记录被更新); END IF; ROLLBACK; -- 演示用回滚掉 END; /输出更新了 2 行 确实有记录被更新注意SQL%ROWCOUNT只反映最近一条SQL 的影响行数中间插了别的语句它就被覆盖了。还有一点隐式游标的%ISOPEN永远是FALSE因为 Oracle 自动开自动关你抓不到它打开的那一刻。真正让代码变短的是游标 FOR 循环。它把OPEN、FETCH、CLOSE全隐式做掉你只需要写循环体。记录变量不用提前声明Oracle 自动按游标返回的列构造。BEGIN FOR r IN (SELECT empno, ename, sal FROM emp_demo ORDER BY empno) LOOP DBMS_OUTPUT.PUT_LINE(r.empno || - || r.ename || - || r.sal); END LOOP; END; /输出和前面显式游标一模一样6 行数据。区别在于这里没有OPEN、没有FETCH、没有CLOSE也没有%NOTFOUND判断。循环自动在结果集耗尽时退出退出时自动关闭游标。你甚至可以在FOR里直接写子查询连DECLARE游标都省了。如果这个查询要复用就把它声明成命名游标FOR里直接引用游标名DECLARE CURSOR c_emp IS SELECT empno, ename, sal FROM emp_demo ORDER BY empno; BEGIN FOR r IN c_emp LOOP DBMS_OUTPUT.PUT_LINE(r.empno || - || r.ename || - || r.sal); END LOOP; END; /两种写法输出一致。什么时候用显式游标、什么时候用 FOR 循环我的经验是只遍历、不中途干预一律用 FOR 循环需要手动控制取数节奏、或者要在循环中间根据%ROWCOUNT做特殊处理才用显式游标。FOR 循环还有个隐藏好处——它不会忘记CLOSE从根上避免了游标泄漏。这里补一个容易混的点游标 FOR 循环里的记录变量r是只读的你不能给它赋值也不能直接拿它去更新表。要改数据得走下一节的FOR UPDATE。4. 参数化游标 FOR UPDATE / WHERE CURRENT OF按部门筛选并原地改数据参数化游标让同一个游标定义能接受不同入参避免为每个部门写一遍 SQL。语法是在游标名后加括号声明形参形参类型用表.列%TYPE最稳。DECLARE CURSOR c_emp(p_deptno emp_demo.deptno%TYPE : 10) IS SELECT empno, ename, sal FROM emp_demo WHERE deptno p_deptno ORDER BY empno; BEGIN DBMS_OUTPUT.PUT_LINE( 部门 10 ); FOR r IN c_emp(10) LOOP DBMS_OUTPUT.PUT_LINE(r.empno || - || r.ename || - || r.sal); END LOOP; DBMS_OUTPUT.PUT_LINE( 部门 20 ); FOR r IN c_emp(20) LOOP DBMS_OUTPUT.PUT_LINE(r.empno || - || r.ename || - || r.sal); END LOOP; DBMS_OUTPUT.PUT_LINE( 部门 30 ); FOR r IN c_emp(30) LOOP DBMS_OUTPUT.PUT_LINE(r.empno || - || r.ename || - || r.sal); END LOOP; END; /输出 部门 10 7782 - CLARK - 2450 7839 - KING - 5000 部门 20 7369 - SMITH - 800 7566 - JONES - 2975 部门 30 7499 - ALLEN - 1600 7521 - WARD - 1250形参默认值: 10意味着c_emp不传参时按部门 10 处理。传参时用c_emp(20)这种位置传参即可。注意形参只在OPEN或 FOR 循环启动那一刻绑定循环中途改实参不会影响已经打开的结果集。接下来是游标更新。要在遍历的同时修改当前行游标声明必须带FOR UPDATE更新时用WHERE CURRENT OF 游标名定位当前行。这套组合能避免“先查主键再 update”的二次定位。DECLARE CURSOR c_emp_upd IS SELECT empno, ename, sal FROM emp_demo WHERE deptno 20 FOR UPDATE OF sal NOWAIT; BEGIN FOR r IN c_emp_upd LOOP DBMS_OUTPUT.PUT_LINE(调整前: || r.ename || 工资 || r.sal); UPDATE emp_demo SET sal sal 100 WHERE CURRENT OF c_emp_upd; END LOOP; COMMIT; END; / -- 验证结果 SELECT empno, ename, sal FROM emp_demo WHERE deptno 20 ORDER BY empno;输出调整前: SMITH 工资 800 调整前: JONES 工资 2975 EMPNO ENAME SAL ----- ----- ----- 7369 SMITH 900 7566 JONES 3075FOR UPDATE OF sal表示只锁定sal列NOWAIT表示如果行已被别的会话锁住就立刻报错而不是等待。WHERE CURRENT OF c_emp_upd里的游标名必须是已经用 FOR UPDATE 声明过的那个否则 Oracle 会报ORA-01410: invalid ROWID之类的错。还有一点FOR UPDATE会加行级锁事务没提交前别的会话改不了这些行所以循环里处理完要及时COMMIT别把锁攥太久。5. 常见报错排查ORA-01000、ORA-06550、%ROWCOUNT 为 0 怎么破游标相关的报错翻来覆去就那么几个对照着看能省不少时间。ORA-01000: maximum open cursors exceeded。这是最典型的游标泄漏。原因通常是显式游标OPEN了没CLOSE或者循环里反复OPEN同一个游标。排查办法是查当前会话打开的游标数SELECT COUNT(*) FROM v$open_cursor WHERE sid SYS_CONTEXT(USERENV,SID);如果数字一直涨基本就是没关。修复方式很简单能用 FOR 循环就用 FOR 循环它自动关必须用显式游标的把CLOSE放进EXCEPTION块或者用BEGIN...END包住保证异常路径也能关掉。ORA-06550 / PLS-00201: identifier must be declared。多半是游标名拼错或者DECLARE段里声明了但BEGIN段引用了另一个名字。还有一种情况是%TYPE引用的表或列不存在比如表名写成了emp但实际建的是emp_demo。逐字核对声明段和引用段的名字即可。%ROWCOUNT 一直是 0。三种可能一是FETCH之前就读了%ROWCOUNT它还没自增二是OPEN没执行游标根本没打开FETCH直接报ORA-01001: invalid cursor三是EXIT WHEN写在了FETCH前面第一次循环就退出了。把FETCH放前面、EXIT WHEN c%NOTFOUND放后面顺序别乱。ORA-01410: invalid ROWID。用WHERE CURRENT OF时游标没带FOR UPDATE或者游标名对不上。检查声明段有没有FOR UPDATE OF 列名以及WHERE CURRENT OF后面跟的名字和声明的是否完全一致。ORA-00054: resource busy and acquire with NOWAIT specified。FOR UPDATE NOWAIT撞上了别的会话持有的锁。要么去掉NOWAIT让它等要么先查v$lock找到阻塞会话处理掉。生产环境里NOWAIT要慎用容易把并发问题直接暴露成报错。隐式游标属性读到旧值。SQL%ROWCOUNT只认最近一条 SQL如果你在UPDATE和读属性之间插了DBMS_OUTPUT.PUT_LINE之外的任何 SQL值就被覆盖了。把属性读取紧跟在目标 SQL 后面。排查时有个通用套路先在BEGIN第一行加DBMS_OUTPUT.PUT_LINE(start)在OPEN后加一行打印%ISOPEN在FETCH后加一行打印%ROWCOUNT和%NOTFOUND在CLOSE后加一行。跑一遍看输出停在哪一步问题基本就定位了。6. 把游标接进日常开发流从 SQL Developer 到 API 调试游标脚本在 SQL Developer 里跑通只是第一步。真实项目里PL/SQL 往往要跟外部服务、模型接口、数据管道打交道这时候一个稳定的 API 接入层就很重要。我平时会把 TaoToken 作为统一的模型调用入口它的 Base URL 是https://taotoken.net/api配合 API Key 就能在脚本或工具里发起请求省去每个服务单独配一套鉴权的麻烦。如果你也在写 PL/SQL 之外要调模型做数据清洗、字段映射或者 SQL 生成可以先把 Key 建好到 TaoToken API Keys 生成密钥再对照 接入文档 把 Base URL 和 Model ID 填进你的客户端。想先验证模型通不通用 模型对话 发一条消息看返回即可。长期跑编码和 Agent 任务的话Coding Plan 更适合按量使用。控制台在 Console用量和调用记录都在里面。回到游标本身最后给你一个把参数化游标和异常处理合起来的完整模板可以直接改成你自己的业务逻辑DECLARE CURSOR c_dept(p_deptno emp_demo.deptno%TYPE) IS SELECT empno, ename, sal FROM emp_demo WHERE deptno p_deptno FOR UPDATE OF sal; v_total NUMBER : 0; BEGIN FOR r IN c_dept(20) LOOP v_total : v_total r.sal; UPDATE emp_demo SET sal sal WHERE CURRENT OF c_dept; END LOOP; DBMS_OUTPUT.PUT_LINE(部门 20 工资合计: || v_total); COMMIT; EXCEPTION WHEN OTHERS THEN ROLLBACK; DBMS_OUTPUT.PUT_LINE(出错回滚: || SQLERRM); RAISE; END; /这段模板把FOR UPDATE、WHERE CURRENT OF、聚合累加、异常回滚串在了一起。跑通它你对游标的掌握就从“会写”进到了“敢用在事务里”。剩下的就是多改参数、多换表把%FOUND、%ROWCOUNT的取值时机练成肌肉记忆。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进