Oracle游标使用全解:从显式游标到游标变量,一次讲透TaoToken

📅 发布时间:2026/9/30 23:50:38
Oracle游标使用全解:从显式游标到游标变量,一次讲透TaoToken
1. Oracle 游标到底解决什么问题从一次批量加薪说起Oracle 游标Cursor是 PL/SQL 里处理「一行一行数据」的核心机制。你可以把它理解成一个指向查询结果集的指针查询语句执行后拿到一批行游标负责按顺序把每一行喂给你的程序逻辑。它最典型的用途就是——对结果集逐行做判断、计算、更新而不是一条 SQL 全表刷完。适合谁看写过select ... into但一遇到多行就报TOO_MANY_ROWS的人需要按部门、按工种批量改工资、改佣金的人以及想把存储过程里的循环逻辑写得更稳、更好排错的人。这篇我会用一张员工表把显式游标、隐式游标、参数化游标、FOR UPDATE更新游标、REF CURSOR游标变量全部串起来脚本可以直接跑。先说一个真实场景公司要给不同部门按不同比例加薪10 号部门加 5%20 号加 10%30 号加 15%40 号加 20%而且加薪前要打印旧工资、加薪后打印新工资。用一条update也能写但一旦规则变复杂、要记录日志、要跳过某些行纯 SQL 就会很别扭。这时候游标循环就是最直观的解法。游标分两大类。显式游标是你自己CURSOR ... IS ...声明、自己OPEN/FETCH/CLOSE的隐式游标是 Oracle 为每条 DML 和单行查询自动开的用SQL%FOUND、SQL%ROWCOUNT这些属性观察。还有一类是游标变量REF CURSOR它把「结果集」当成参数在程序之间传递是后面做 API 校验、动态查询的关键。我先把演示表建好后面所有例子都基于它。注意emp1是从emp复制出来的避免动到系统示例表-- 建一张和 emp 结构一致的测试表 create table emp1 as select * from emp; -- 确认数据 select empno, ename, job, sal, comm, deptno, hiredate from emp1 order by deptno, sal;如果你本地没有emp表用下面这段自建字段名保持一致即可create table emp1 ( empno number(4) primary key, ename varchar2(20), job varchar2(20), sal number(7,2), comm number(7,2), deptno number(2), hiredate date ); insert into emp1 values (7469,ALEARK,MANAGER, 2975, null, 20, date 1981-04-02); insert into emp1 values (7499,ALLEN,SALESMAN, 1600, 300, 30, date 1981-02-20); insert into emp1 values (7521,WARD,SALESMAN, 1250, 500, 30, date 1981-02-22); insert into emp1 values (7566,JONES,MANAGER, 2975, null, 20, date 1981-04-02); insert into emp1 values (7654,MARTIN,SALESMAN, 1250, 1400, 30, date 1981-09-28); insert into emp1 values (7698,BLAKE,MANAGER, 2850, null, 30, date 1981-05-01); insert into emp1 values (7782,CLARK,MANAGER, 2450, null, 10, date 1981-06-09); insert into emp1 values (7839,KING,PRESIDENT, 5000, null, 10, date 1981-11-17); insert into emp1 values (7844,TURNER,SALESMAN, 1500, 0, 30, date 1981-09-08); insert into emp1 values (7900,JAMES,CLERK, 950, null, 30, date 1981-12-03); insert into emp1 values (7902,FORD,ANALYST, 3000, null, 20, date 1981-12-03); insert into emp1 values (7934,MILLER,CLERK, 1300, null, 10, date 1982-01-23); commit;表有了接下来按「声明 → 打开 → 提取 → 关闭」这条主线把每种游标讲透。你会发现FOR循环游标之所以省事是因为 Oracle 帮你自动做了 open、fetch、close 和退出判断但理解底层四步排错时才不会懵。2. 显式游标四步法与 FOR 循环游标声明、打开、提取、关闭全流程显式游标的完整生命周期就四步CURSOR声明、OPEN打开、FETCH提取、CLOSE关闭。声明只是告诉编译器「我要查这些列」此时并不执行查询OPEN才真正执行并定位到第一行之前FETCH每次取一行放进变量CLOSE释放资源。漏掉CLOSE在长会话里会累积打开游标数超过open_cursors参数就报ORA-01000: maximum open cursors exceeded。先看最标准的FETCH写法用%NOTFOUND判断退出declare cursor c_job is select empno, ename, job, sal from emp1 where job MANAGER; c_row c_job%rowtype; begin open c_job; loop fetch c_job into c_row; exit when c_job%notfound; dbms_output.put_line(c_row.empno || - || c_row.ename || - || c_row.job || - || c_row.sal); end loop; close c_job; end; /这里有个新手常踩的坑exit when c_job%notfound必须放在fetch之后、处理数据之前。因为FETCH取不到行时c_row里的值是未定义的如果你先打印再判断最后会多输出一行脏数据。同样的逻辑用FOR循环游标写就短很多declare cursor c_job is select empno, ename, job, sal from emp1 where job MANAGER; begin for c_row in c_job loop dbms_output.put_line(c_row.empno || - || c_row.ename || - || c_row.job || - || c_row.sal); end loop; end; /FOR循环里c_row是隐式声明的记录变量类型自动匹配游标行类型你不用写%rowtype也不用 open/close。Oracle 在循环开始前自动 open每次迭代自动 fetch%NOTFOUND为真时自动退出并 close。日常业务里只要不需要在循环中途手动控制提取节奏优先用FOR循环代码少、出错少。那什么时候必须手写FETCH两种典型情况一是你想「只取前 N 行」比如只提升资格最老的 2 个人二是你想在循环外先取一行做特殊处理。看只取两条的例子declare cursor crs_top is select * from emp1 order by hiredate asc; top_two number : 2; r crs_top%rowtype; begin open crs_top; fetch crs_top into r; while top_two 0 loop dbms_output.put_line(员工 || r.ename || 入职 || r.hiredate); top_two : top_two - 1; fetch crs_top into r; end loop; close crs_top; end; /注意while top_two 0里也要再fetch一次否则会死循环打印同一行。这种「计数器 手动 fetch」的模式在分页导出、只处理头部数据时很常见。再看WHILE ... %FOUND的写法它和FETCH配合的顺序是「先取一行再判断」declare cursor csr_loc is select loc from dept; row_loc csr_loc%rowtype; begin open csr_loc; fetch csr_loc into row_loc; while csr_loc%found loop dbms_output.put_line(部门地点 || row_loc.loc); fetch csr_loc into row_loc; end loop; close csr_loc; end; /%FOUND和%NOTFOUND是反义属性%ROWCOUNT返回已成功提取的行数%ISOPEN判断游标是否打开。这四个属性在显式游标和隐式游标上都能用但隐式游标的%ISOPEN永远是 false因为 Oracle 执行完 DML 立刻自动关闭了。到这里显式游标的骨架就清楚了。下一节讲参数化游标和FOR UPDATE更新游标这才是批量改工资真正要用的东西。3. 参数化游标与 FOR UPDATE 更新游标批量加薪的可复制配置参数化游标让同一个游标定义能接收不同入参避免为每个部门写一个游标。语法是CURSOR 名字(参数名 IN 类型) IS ...调用时传值。下面这个例子接收部门编号打印该部门所有员工declare cursor c_dept(p_deptno number) is select * from emp1 where deptno p_deptno; begin for r in c_dept(20) loop dbms_output.put_line(员工号 || r.empno || 姓名 || r.ename || 工资 || r.sal); end loop; end; /参数默认是IN模式也可以写DEFAULT给默认值。传工种同理declare cursor c_job(p_job varchar2) is select * from emp1 where job p_job; begin for r in c_job(CLERK) loop dbms_output.put_line(员工号 || r.empno || 姓名 || r.ename); end loop; end; /真正做批量更新时要用FOR UPDATE OF 列名锁定行再用WHERE CURRENT OF 游标名更新当前行。这样写的好处是更新精确指向游标当前指向的那一行不用再拼主键条件也不会误伤别的行。先看按部门比例加薪的完整脚本declare cursor crs_case is select * from emp1 for update of sal; r crs_case%rowtype; sal_new emp1.sal%type; begin for r in crs_case loop case when r.deptno 10 then sal_new : r.sal * 1.05; when r.deptno 20 then sal_new : r.sal * 1.10; when r.deptno 30 then sal_new : r.sal * 1.15; when r.deptno 40 then sal_new : r.sal * 1.20; else sal_new : r.sal; end case; dbms_output.put_line(r.ename || 原工资 || r.sal || 新工资 || sal_new); update emp1 set sal sal_new where current of crs_case; end loop; commit; end; /FOR UPDATE OF sal只锁定 sal 列减少锁冲突。如果你不加OF sal默认锁整行。执行完记得commit否则别的会话读不到更新还可能一直等锁。再看一个「加薪但设上限」的场景所有员工按工资 20% 加薪但如果增加额超过 300 就取消加薪。这个逻辑用游标逐行判断最清晰declare cursor crs_up is select * from emp1 for update of sal; r crs_up%rowtype; add_amt emp1.sal%type; sal_new emp1.sal%type; begin for r in crs_up loop add_amt : r.sal * 0.2; if add_amt 300 then sal_new : r.sal; dbms_output.put_line(r.ename || 加薪失败维持 || r.sal); else sal_new : r.sal add_amt; dbms_output.put_line(r.ename || 加薪成功变为 || sal_new); end if; update emp1 set sal sal_new where current of crs_up; end loop; commit; end; /还有按姓名首字母加薪、给 SALESMAN 加佣金 500 这类需求套路完全一样只是WHERE条件和更新列不同-- 名字以 A 或 S 开头的员工加薪 10% declare cursor crs_a is select * from emp1 where ename like A% or ename like S% for update of sal; r crs_a%rowtype; begin for r in crs_a loop dbms_output.put_line(r.ename || 原工资 || r.sal); update emp1 set sal r.sal * 1.1 where current of crs_a; end loop; commit; end; / -- 给 SALESMAN 加佣金 500 declare cursor crs_comm(p_job varchar2) is select * from emp1 where job p_job for update of comm; r crs_comm%rowtype; begin for r in crs_comm(SALESMAN) loop update emp1 set comm nvl(r.comm, 0) 500 where current of crs_comm; end loop; commit; end; /这里nvl(r.comm, 0)很关键因为部分员工comm是 null直接null 500结果还是 null佣金就加不上。如果你要把这些逻辑封装成存储过程把游标声明放进过程体即可参数从过程入参传进来create or replace procedure raise_by_dept(p_deptno in number, p_rate in number) is cursor c is select * from emp1 where deptno p_deptno for update of sal; begin for r in c loop update emp1 set sal r.sal * (1 p_rate) where current of c; end loop; commit; end; /调用exec raise_by_dept(10, 0.05);就能给 10 号部门加 5%。这种「参数化游标 FOR UPDATE 存储过程」的组合是 Oracle 批量业务处理里最稳的写法。4. 隐式游标 SQL% 属性与 REF CURSOR 游标变量验证请求与成功结果隐式游标不需要你声明每条 DMLinsert/update/delete和单行select into执行时Oracle 自动开一个叫SQL的游标。你可以用SQL%FOUND、SQL%NOTFOUND、SQL%ROWCOUNT、SQL%ISOPEN观察执行结果。注意SQL%ISOPEN对隐式游标永远是 false因为语句一执行完就自动关了。看一个 update 后观察属性的例子begin update emp1 set ename ALEARK where empno 7469; if sql%isopen then dbms_output.put_line(Openging); else dbms_output.put_line(closing); end if; if sql%found then dbms_output.put_line(游标指向了有效行); else dbms_output.put_line(Sorry); end if; if sql%notfound then dbms_output.put_line(Also Sorry); else dbms_output.put_line(Haha); end if; dbms_output.put_line(影响行数 || sql%rowcount); exception when no_data_found then dbms_output.put_line(Sorry No data); when too_many_rows then dbms_output.put_line(Too Many rows); end; /SQL%ROWCOUNT返回刚执行的 DML 影响行数这个在「更新了 0 行要报警」的场景特别有用。单行select into也走隐式游标declare v_empno emp1.empno%type; v_ename emp1.ename%type; begin select empno, ename into v_empno, v_ename from emp1 where empno 7499; dbms_output.put_line(查到 || v_empno || || v_ename); dbms_output.put_line(行数 || sql%rowcount); exception when no_data_found then dbms_output.put_line(没查到数据); when too_many_rows then dbms_output.put_line(返回了多行); end; /select into如果返回多行就抛TOO_MANY_ROWS返回 0 行抛NO_DATA_FOUND这两个异常必须处理否则程序直接中断。接下来是REF CURSOR游标变量它和前面「声明时就绑定死查询」的静态游标不同可以在运行时动态指定查询还能作为参数在存储过程之间传递。先定义类型再打开declare type t_emp_cur is ref cursor; v_cur t_emp_cur; v_row emp1%rowtype; begin open v_cur for select * from emp1 where deptno 30; loop fetch v_cur into v_row; exit when v_cur%notfound; dbms_output.put_line(v_row.ename || || v_row.sal); end loop; close v_cur; end; /REF CURSOR最实用的地方是「一个过程返回结果集给调用方」。比如封装一个按部门查员工的存储过程create or replace procedure get_emp_by_dept( p_deptno in number, p_cur out sys_refcursor ) is begin open p_cur for select empno, ename, job, sal from emp1 where deptno p_deptno; end; /调用方拿到sys_refcursor后自己 fetchdeclare v_cur sys_refcursor; v_empno emp1.empno%type; v_ename emp1.ename%type; v_job emp1.job%type; v_sal emp1.sal%type; begin get_emp_by_dept(20, v_cur); loop fetch v_cur into v_empno, v_ename, v_job, v_sal; exit when v_cur%notfound; dbms_output.put_line(v_empno || || v_ename || || v_job || || v_sal); end loop; close v_cur; end; /sys_refcursor是 Oracle 内置的弱类型游标变量不用自己定义类型最省事。强类型REF CURSOR要指定返回行类型比如type t is ref cursor return emp1%rowtype;好处是编译期就能检查列是否匹配。到这里游标本身的用法就齐了。但实际项目里光在数据库里跑还不够你往往需要把查询结果拿出来做校验、比对、生成报告。下面讲怎么用 TaoToken 统一 Key 调 API 来校验 SQL 结果。5. 用 TaoToken 统一 Key 校验游标执行结果配置示例与验证步骤场景是这样的你在 PL/SQL 里跑完批量加薪想确认「10 号部门平均工资涨了 5%」这个结论对不对或者想把游标输出的结果交给一个模型做异常检测。手动比对太累可以用 TaoToken 的统一 Key 调 API把 SQL 结果作为输入做校验。TaoToken 是一个统一的大模型 API 接入层官网是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 端点是 https://taotoken.net/api 。它的价值在于你只用一个 Key、一个 Base URL就能调用不同模型不用为每个模型单独配一套鉴权和地址。对于「SQL 结果校验」这种轻量任务非常合适。第一步去控制台创建 API Key。打开 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 在 API Keys 页面生成一个 Key复制保存。注意 Key 只显示一次丢了只能重建。第二步准备校验脚本。假设你已经把游标执行前后的工资导出成两段文本用 Python 调 API 做比对。先安装依赖pip install openai然后写配置。TaoToken 兼容 OpenAI SDK 的调用方式只需要改base_url和api_keyfrom openai import OpenAI client OpenAI( base_urlhttps://taotoken.net/api, api_key你的_TaoToken_Key ) before 10 号部门CLARK 2450, KING 5000, MILLER 1300 after 10 号部门CLARK 2572.5, KING 5250, MILLER 1365 prompt f下面是 Oracle 游标批量加薪前后的工资数据。 加薪规则10 号部门统一加 5%。 请校验加薪后数值是否正确逐行给出结论最后给一句总结。 加薪前 {before} 加薪后 {after} resp client.chat.completions.create( modelgpt-4o-mini, messages[{role: user, content: prompt}] ) print(resp.choices[0].message.content)如果你更习惯用 curl 直接验证连通性可以这样curl https://taotoken.net/api/chat/completions \ -H Authorization: Bearer 你的_TaoToken_Key \ -H Content-Type: application/json \ -d { model: gpt-4o-mini, messages: [{role: user, content: 校验 2450*1.05 是否等于 2572.5}] }第三步把游标结果自动喂进去。你可以在 PL/SQL 里用dbms_output输出结果重定向到文件再用 Python 读取也可以直接在应用层查询后拼接。核心是让「SQL 执行」和「结果校验」解耦游标负责取数API 负责判断。第四步验证成功结果。正常返回会是一段 JSONchoices[0].message.content里是模型给出的逐行校验结论。如果返回 401说明 Key 不对或没带Bearer如果返回local proxy failed之类网络错误检查你的出口网络和 Base URL 是否写成了https://taotoken.net/api注意结尾不要多加/v1SDK 会自己拼。模型选择上校验这种结构化任务用轻量模型就够成本低、响应快。如果你要长期跑批量校验、甚至做 Agent 自动化可以看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 它更适合持续性的编码和自动化任务。只想先手动试一次模型对话用 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 就行。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有各语言 SDK 的完整示例。API Key 管理页再贴一次https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。6. 游标常见报错排查ORA-01000、TOO_MANY_ROWS 与 OAuth 类鉴权失败游标用起来不难但报错信息往往让人摸不着头脑。这一节把最常见的几类错误和排查路径列清楚对照着改就行。第一类ORA-01000: maximum open cursors exceeded。原因几乎都是显式游标OPEN后没CLOSE或者循环里反复 open 不关。排查方法查当前会话打开的游标数select a.value, s.username, s.sid, s.serial# from v$sesstat a, v$statname b, v$session s where a.statistic# b.statistic# and s.sid a.sid and b.name opened cursors current;修复原则能用FOR循环就别手写 open/close必须手写时把CLOSE放进异常处理的finally逻辑里确保出错也能关。临时调大参数可以alter system set open_cursors 1000 scopeboth;但治标不治本。第二类ORA-01422: exact fetch returns more than requested number of rows也就是TOO_MANY_ROWS。这是select into返回多行导致的。排查把select into改成游标或加rownum 1或者确认where条件是否唯一。比如where empno 7499是主键不会多行但where job CLERK就会多行。第三类ORA-01403: no data found。select into没查到数据。要么加exception when no_data_found要么改用游标循环游标查不到行时%NOTFOUND为真不会抛异常。第四类ORA-06550/PLS-00382这类编译错误多半是变量类型和游标列类型不匹配。用%rowtype或%type声明变量让类型自动跟随表结构能避免大部分此类问题。第五类更新游标报ORA-00054: resource busy或一直卡住。说明行被别的会话锁了。查锁select s.sid, s.serial#, s.username, o.object_name from v$locked_object l, dba_objects o, v$session s where l.object_id o.object_id and l.session_id s.sid;确认后可以alter system kill session sid,serial#;解锁但生产环境要谨慎。第六类调 TaoToken API 时的鉴权错误。返回 401 通常是 Key 写错、Key 被删、或者请求头没带Authorization: Bearer xxx。返回 403 可能是 Key 权限不足。返回local proxy failed或连接超时检查 Base URL 是否为https://taotoken.net/api以及本机网络是否能正常访问。返回reading choices相关错误一般是响应体解析失败确认模型名拼写正确、请求 JSON 格式合法。如果你用的是 Claude Code 这类工具接入配置里要写全三件套Base URL 填https://taotoken.net/apiKey 填你的 TaoToken KeyModel ID 填你要用的模型名。三者缺一不可只填 Key 不填 Base URL 会默认打到官方地址导致鉴权失败。Claude Code 的接入说明在 https://taotoken.net/claudecode?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecodeutm_campaignrewrite Anthropic 兼容端点在 https://taotoken.net/anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentanthropicutm_campaignrewrite 。最后提醒一个游标性能点FOR UPDATE会加行锁批量更新时尽量缩小锁定范围OF 具体列并在循环结束后尽快commit。如果数据量很大考虑用BULK COLLECT批量提取替代逐行 fetch能显著减少上下文切换。但那是另一个话题了先把逐行游标这套基础打牢排错时你才知道每一步在干什么。