ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Oracle Cursor 游标基本用法详解:从显式游标到游标 FOR 循环的完整实践

Oracle Cursor 游标基本用法详解:从显式游标到游标 FOR 循环的完整实践 1. 为什么你写的 PL/SQL 一查多行就报错刚接触 Oracle 存储过程和匿名块的同学大概率都踩过这个坑写了个SELECT ... INTO ...测试时只查一条数据没问题一上生产就抛ORA-01422: exact fetch returns more than requested number of rows。原因很简单PL/SQL 里的SELECT INTO天生只认一行返回多行它就直接罢工。这时候就轮到游标Cursor出场了。游标本质上就是指向查询结果集的一个指针你可以把它想象成一根手指按顺序一行一行划过结果集每划一行就处理一行。Oracle 把游标分成两类隐式游标由数据库自动管理你执行一条 DML 它就悄悄开一个用完立刻关显式游标则需要你自己声明、打开、取数、关闭适合处理返回多行的查询。这篇文章就围绕显式游标展开从最原始的OPEN/FETCH/CLOSE骨架到省心省力的游标 FOR 循环再到带参数的游标全部给出可直接复制运行的代码并说明在 SQL*Plus 和 SQL Developer 里怎么验证结果集。适合刚写存储过程、对游标概念还模糊的开发者。2. 先把环境和一个可用的测试表准备好在动手写游标之前得先有个能跑代码的地方。你可以用 SQL*Plus 命令行也可以用 SQL Developer 图形界面两者执行 PL/SQL 块的方式略有不同下面分别说。SQL*Plus 里执行匿名块需要在最后单独加一行斜杠/来触发执行并且要先打开输出开关否则DBMS_OUTPUT.PUT_LINE的内容你看不到SET SERVEROUTPUT ON SIZE UNLIMITED;SQL Developer 里就简单多了把 PL/SQL 块贴进工作表直接点运行按钮输出会显示在下面的「Dbms Output」面板里。记得先点一下那个面板的绿色加号启用输出。为了后面所有例子都能跑先建一张测试表并塞几条数据。这里用经典的员工表结构字段少一点方便看CREATE TABLE emp_test ( empno NUMBER(4) PRIMARY KEY, ename VARCHAR2(20), job VARCHAR2(20), deptno NUMBER(2), sal NUMBER(8,2) ); INSERT INTO emp_test VALUES (7369,SMITH,CLERK,20,800); INSERT INTO emp_test VALUES (7499,ALLEN,SALESMAN,30,1600); INSERT INTO emp_test VALUES (7521,WARD,SALESMAN,30,1250); INSERT INTO emp_test VALUES (7566,JONES,MANAGER,20,2975); INSERT INTO emp_test VALUES (7698,BLAKE,MANAGER,30,2850); INSERT INTO emp_test VALUES (7782,CLARK,MANAGER,10,2450); INSERT INTO emp_test VALUES (7839,KING,PRESIDENT,10,5000); COMMIT;数据准备好后先记住一个判断标准如果你的查询确定只返回一行用SELECT INTO最省事只要可能返回多行就老老实实用显式游标。3. 显式游标的标准四步骨架显式游标的使用流程固定为四步声明、打开、取数、关闭。声明放在DECLARE部分打开和取数放在BEGIN里关闭也放在BEGIN里。先看最原始、最啰嗦但最能说明原理的写法SET SERVEROUTPUT ON; DECLARE v_ename emp_test.ename%TYPE; v_sal emp_test.sal%TYPE; CURSOR c_emp IS SELECT ename, sal FROM emp_test WHERE deptno 30 ORDER BY ename; BEGIN OPEN c_emp; LOOP FETCH c_emp INTO v_ename, v_sal; EXIT WHEN c_emp%NOTFOUND; DBMS_OUTPUT.PUT_LINE(v_ename || 的工资是 || v_sal); END LOOP; CLOSE c_emp; END; /这里有几个关键点必须理解。%TYPE让变量继承列的数据类型列改了类型你不用改代码。FETCH每执行一次游标就往下走一行把当前行的值塞进INTO后面的变量里。c_emp%NOTFOUND是游标属性当FETCH取不到数据时它为真所以EXIT WHEN要放在FETCH之后、处理逻辑之前否则最后一行会被漏掉或者多处理一次空值。游标有四个常用属性用表格对照一下更清楚属性含义典型用途%FOUND最近一次 FETCH 是否取到行判断是否还有数据%NOTFOUND最近一次 FETCH 是否没取到行循环退出条件%ROWCOUNT到目前为止取了多少行统计处理条数%ISOPEN游标是否处于打开状态关闭前判断避免重复关闭报错如果你要处理的是整行数据一列一列声明变量太累用%ROWTYPE记录变量更省事DECLARE CURSOR c_emp IS SELECT * FROM emp_test WHERE deptno 20; 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.empno || - || r_emp.ename || - || r_emp.sal); END LOOP; CLOSE c_emp; END; /c_emp%ROWTYPE表示这个记录变量的结构和游标查询出来的列一一对应取数时直接FETCH ... INTO r_emp用的时候r_emp.ename这样点出来就行列多的时候特别香。4. 游标 FOR 循环把四步骨架收进一行上面那种OPEN/LOOP/FETCH/EXIT/CLOSE的写法写多了你会发现全是模板代码。Oracle 提供了游标 FOR 循环把打开、取数、判断结束、关闭全部自动完成你只需要关心循环体里干什么。同样的需求用 FOR 循环重写BEGIN FOR r_emp IN (SELECT ename, sal FROM emp_test WHERE deptno 30 ORDER BY ename) LOOP DBMS_OUTPUT.PUT_LINE(r_emp.ename || 的工资是 || r_emp.sal); END LOOP; END; /注意这里连游标声明都省了直接在FOR ... IN (...)里写查询Oracle 自动帮你定义一个隐式的记录变量r_emp。循环每转一圈r_emp就自动指向下一行取完自动退出退出后自动关闭。你完全不用碰OPEN、FETCH、CLOSE也不会忘记写EXIT WHEN。如果你想把游标声明和循环分开也可以先声明再循环效果一样DECLARE CURSOR c_emp IS SELECT ename, sal FROM emp_test WHERE deptno 20 ORDER BY ename; BEGIN FOR r_emp IN c_emp LOOP DBMS_OUTPUT.PUT_LINE(r_emp.ename || 的工资是 || r_emp.sal); END LOOP; END; /实测下来日常开发里九成的多行处理场景用游标 FOR 循环就够了。只有当你需要在循环中途根据条件提前退出、或者需要手动控制取数节奏时才回到显式四步写法。5. 带参数的游标和嵌套循环实战游标可以像存储过程一样接收参数这在「外层循环拿到一个值内层用它去查明细」的场景里特别有用。语法是在游标名后面加括号声明参数打开时传值进去。下面这个例子按部门分组外层游标遍历部门内层游标用部门号做参数查该部门员工并累加工资DECLARE CURSOR c_dept IS SELECT DISTINCT deptno FROM emp_test ORDER BY deptno; CURSOR c_emp(p_deptno NUMBER) IS SELECT ename, sal FROM emp_test WHERE deptno p_deptno ORDER BY ename; v_total NUMBER : 0; BEGIN FOR r_dept IN c_dept LOOP DBMS_OUTPUT.PUT_LINE(部门 || r_dept.deptno || 的员工); v_total : 0; FOR r_emp IN c_emp(r_dept.deptno) LOOP DBMS_OUTPUT.PUT_LINE( || r_emp.ename || 工资 || r_emp.sal); v_total : v_total r_emp.sal; END LOOP; DBMS_OUTPUT.PUT_LINE( 部门工资合计 || v_total); END LOOP; END; /参数游标也可以给默认值写法是p_deptno NUMBER DEFAULT 20打开时不传参就用默认值。要注意游标参数只能传入不能传出而且只定义数据类型不定义长度写VARCHAR2就行别写VARCHAR2(20)。6. 用 FOR UPDATE 游标边遍历边更新有时候你需要在遍历结果集的同时更新当前行比如给某个部门的员工统一调薪。这时候要用FOR UPDATE子句锁定行再用WHERE CURRENT OF 游标名精准定位当前行避免用主键再查一次。DECLARE CURSOR c_emp IS SELECT empno, ename, sal FROM emp_test WHERE deptno 30 FOR UPDATE OF sal; BEGIN FOR r_emp IN c_emp LOOP UPDATE emp_test SET sal r_emp.sal * 1.1 WHERE CURRENT OF c_emp; DBMS_OUTPUT.PUT_LINE(r_emp.ename || 原工资 || r_emp.sal || 已上调 10%); END LOOP; COMMIT; END; /FOR UPDATE OF sal表示只锁定 sal 列所在的行WHERE CURRENT OF c_emp表示更新游标当前指向的那一行。这里有个坑FOR UPDATE会持有行级锁直到事务提交或回滚如果循环里处理很慢别的会话想改这些行就得排队。所以循环体里尽量别做耗时操作处理完尽快COMMIT。7. 跑完怎么验证结果对不对代码执行完光看DBMS_OUTPUT的输出还不够最好回到 SQL 层面核对数据。比如上面调薪的例子执行前后各查一次SELECT empno, ename, sal FROM emp_test WHERE deptno 30 ORDER BY ename;对比调薪前后的 sal 值确认每行都按 1.1 倍更新了。如果是纯查询类游标验证方法就是把你游标里的SELECT语句单独拎出来在 SQL Developer 里跑一遍看返回的行数和内容再和DBMS_OUTPUT打印的逐行对比行数一致、内容一致就说明游标逻辑没问题。还有一个实用技巧在循环里用c_emp%ROWCOUNT打印当前处理到第几行跑完再打印一次总数和SELECT COUNT(*)的结果对一下能快速发现是不是漏行或多行。8. 游标使用中最容易踩的几个坑第一个坑是EXIT WHEN的位置。如果你写成先处理再判断最后一行会被处理两次第二次用的是上一轮的残留值。正确顺序永远是FETCH→EXIT WHEN %NOTFOUND→ 处理。第二个坑是忘记CLOSE。显式游标打开后不关闭会一直占用数据库资源循环次数多了可能触发ORA-01000: maximum open cursors exceeded。用游标 FOR 循环就没这个问题它自动关。第三个坑是%NOTFOUND在没FETCH之前的值是 NULL不是 TRUE 也不是 FALSE。所以别在OPEN之后、第一次FETCH之前就用它做判断逻辑会乱。第四个坑是FOR UPDATE锁表。如果你只是查询不想锁行千万别加FOR UPDATE。加了之后其他会话对这些行的修改都会被阻塞生产环境上很容易引发连锁等待。第五个坑是游标里用SELECT *配合%ROWTYPE时如果表结构变了加列删列记录变量的结构也跟着变代码里如果硬编码了列顺序就可能出错。稳妥做法是显式列出需要的列。9. 把游标逻辑交给更顺手的工具链游标写多了你会发现真正花时间的往往不是语法本身而是反复调试、查文档、验证结果集。我现在习惯把 PL/SQL 片段和对应的 SQL 验证语句放在一起管理改一版跑一版效率高很多。如果你也在做 Oracle 相关的开发需要快速验证某段游标逻辑或者对比不同写法的执行结果可以借助 TaoToken 的模型对话能力来辅助梳理思路把游标声明、循环体、异常处理拆开逐个确认。接入方式很简单在 API Keys 页面生成密钥后参考接入文档配置即可地址是 https://taotoken.net/api-keys 和 https://taotoken.net/doc 。对于需要长期写存储过程、做数据库迁移的同学Coding Plan 能帮你把重复的游标模板沉淀下来减少手写样板代码的时间详情看 https://taotoken.net/coding-plan 。工具是辅助游标的核心还是那四步骨架和 FOR 循环的取舍把这两样吃透剩下的就是熟练度问题。
返回列表