1. 动态游标到底解决什么问题从固定表名到多表循环遍历先说结论Oracle 里的动态游标REF CURSOR本质上是把「查哪张表、按什么条件查」这件事从编译期推迟到运行期。普通游标在写 PL/SQL 的时候 SQL 就定死了表名、字段名、WHERE 条件都是硬编码而动态游标允许你用字符串拼出一条 SQL再OPEN 游标 FOR 字符串把它打开然后照常FETCH循环取值。这个能力在什么场景下特别值钱我举几个真实遇到的例子。第一类是分表场景比如日志表按月份拆成LOG_202401、LOG_202402你要写一个存储过程遍历最近 12 个月的表做统计不可能写 12 个游标。第二类是配置驱动的取数业务方在配置表里维护「要监控哪些表、取哪些字段」存储过程读配置再动态拼 SQL。第三类是多条件灵活查询前端传进来的筛选条件组合不确定用动态 SQL 拼 WHERE 子句比写一堆 IF 分支清爽得多。动态游标和动态 SQL 是两件事但经常一起用。动态 SQL 解决「SQL 文本运行时才确定」动态游标解决「结果集运行时才绑定」。EXECUTE IMMEDIATE适合执行不需要返回多行的 DDL 或单行 DML而OPEN ... FOR才是遍历多行结果集的正解。很多人一开始用EXECUTE IMMEDIATE ... INTO去接多行结果报ORA-01422: exact fetch returns more than requested number of rows就是因为没搞清这个分工。适合谁看这篇后端开发要写批量数据处理存储过程的、DBA 要做跨表巡检脚本的、数据迁移时要在多张结构相似表之间循环取数的。如果你只是查一张固定表普通游标甚至一条 SQL 就够了别为了动态而动态动态 SQL 的可读性和执行计划稳定性都不如静态 SQL。还有一个容易被忽略的点动态游标在存储过程里的声明位置有讲究。它必须放在IS或AS之后的第一行区域也就是声明区的最前面。我试过把它写在变量声明中间编译直接报错。这个细节后面配置章节会展开。核心检索词先明确Oracle 动态游标、REF CURSOR、动态 SQL 循环遍历、PL/SQL OPEN FOR。这几个词贯穿全文你搜的时候也基本是这几个组合。2. TaoToken 统一 Key 前置准备把调用凭证收口到一处在写 PL/SQL 之前先把凭证管理这件事理清楚。很多团队的做法是每个存储过程、每个脚本里各写一份连接串和密钥时间一长根本不知道哪个 Key 对应哪个环境轮换的时候要满仓库找。TaoToken 的思路是提供一个统一的 API 通道把模型调用、编码辅助这类外部能力的凭证收口到一个 Key 上PL/SQL 侧只需要引用这个统一入口即可。先明确几个地址后面配置会用到官网入口https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentAPI 基址https://taotoken.net/api模型对话页https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteCoding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite控制台https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteAPI Keys 管理https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteClaude Code 接入https://taotoken.net/ClaudeCodeAnthropic?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite为什么要在 Oracle 场景里提这个因为现在很多 DBA 和开发在写复杂 PL/SQL 时会借助模型辅助生成动态 SQL 拼接逻辑、排查执行计划问题。如果每个工具、每个脚本都单独配 Key管理成本很高。统一 Key 之后你在控制台生成一个 Key然后在各个客户端里复用轮换时只改一处。前置准备分三步走。第一步进控制台创建 API Key建议按用途命名比如oracle-plsql-dev、oracle-plsql-prod别用test1、key2这种。第二步确认你要用的模型 ID这个在模型对话页能看到当前可用的模型列表记下你要调用的那个 ID。第三步把 Base URL、Key、Model ID 这三件套记下来后面配置客户端时三个都要填。这里要强调一个安全边界TaoToken 是合规的 API 通道不要把它和任何网络访问工具混为一谈也不要在存储过程里硬编码 Key。生产环境的 Key 应该走环境变量或者 Oracle Wallet而不是明文写在v_sql拼接的字符串里。我见过有人在动态 SQL 里把密钥拼进去结果 SQL 文本被记进V$SQL等于泄露。如果你用的是 Cline、Codex 这类编码客户端来辅助写 PL/SQL配置时同样需要 Base URL、Key、Model ID 三件套。以 Codex 的auth.json为例结构大致是这样{ base_url: https://taotoken.net/api, api_key: sk-你的统一Key, model: 你选定的模型ID }Cline 的 MCP 配置里也是同样的三要素Base URL 填https://taotoken.net/apiKey 填控制台生成的Model ID 填模型对话页确认的那个。三件套缺一不可少填 Model ID 最常见的报错就是请求发出去了但返回模型不存在。3. 可复制配置REF CURSOR 声明、OPEN FOR 拼接与循环模板这一节是全文核心直接给可复制的模板。先看最基础的动态游标结构也就是 excerpt 里提到的那个骨架CREATE OR REPLACE PROCEDURE P_TEST_SQL IS TYPE ref_cursor_type IS REF CURSOR; -- 定义动态游标类型 tablename VARCHAR2(200) DEFAULT ess_client; v_sql VARCHAR2(1000); mobile VARCHAR2(15); usrs ref_cursor_type; -- 声明游标变量 BEGIN v_sql : select usrmsisdn from || tablename || where rownum 11; OPEN usrs FOR v_sql; -- 运行时绑定 SQL LOOP FETCH usrs INTO mobile; EXIT WHEN usrs%NOTFOUND; INSERT INTO tmp(usrmsisdn) VALUES(mobile); END LOOP; CLOSE usrs; COMMIT; END P_TEST_SQL;几个关键点必须说清楚。TYPE ref_cursor_type IS REF CURSOR定义的是游标类型usrs ref_cursor_type才是游标变量两者别混。OPEN usrs FOR v_sql里的v_sql是字符串变量不是静态 SQL这是动态游标和普通游标的分水岭。EXIT WHEN usrs%NOTFOUND放在FETCH之后顺序反了会多处理一行或者漏一行。再看一个带多字段、带业务逻辑的完整实例这是订单跟踪场景的改写版CREATE OR REPLACE PROCEDURE DDGZ1 ( P_flag VARCHAR2, -- 订单类型 P_operate VARCHAR2, -- 操作类型 P_BH VARCHAR2 -- 传入的编号ID ) IS TYPE ref_cursor_type IS REF CURSOR; v_cur_CKD ref_cursor_type; v_ERP VARCHAR2(50); v_DRP VARCHAR2(50); v_jxsmc VARCHAR2(100); v_jxs VARCHAR2(50); v_DWMC VARCHAR2(100); v_czr VARCHAR2(50); v_jzsj DATE; BEGIN OPEN v_cur_CKD FOR select v_ERP,v_DRP,v_jxsmc,v_jxs,v_DWMC,v_czr,v_jlsj from tmp_DDGZ1; FETCH v_cur_CKD INTO v_ERP,v_DRP,v_jxsmc,v_jxs,v_DWMC,v_czr,v_jzsj; WHILE v_cur_CKD%FOUND LOOP INSERT INTO DRPDDGZJL2 (JLID,DRP,ERP,JXS,JXSMC,JLSJ,GZJL,CZR,BHID) VALUES (SYS_GUID(),v_DRP,v_ERP,v_jxs,v_jxsmc,v_jzsj, v_DWMC||受托代销成品库接收订单组织发货,v_CZR,出库单||P_BH); FETCH v_cur_CKD INTO v_ERP,v_DRP,v_jxsmc,v_jxs,v_DWMC,v_czr,v_jzsj; END WHILE; CLOSE v_cur_CKD; END DDGZ1;注意这里用的是WHILE ... LOOP而不是LOOP ... EXIT WHEN两种写法等价但WHILE版本在进入循环前先FETCH一次循环体内再FETCH一次新手容易漏掉循环体里那次FETCH导致死循环。我个人更推荐LOOP ... EXIT WHEN的写法结构更对称。动态 SQL 拼接时表名和字段名不能用绑定变量只能用字符串拼接这是 Oracle 的硬限制。但 WHERE 条件里的值必须用USING绑定防止 SQL 注入v_sql : select usrmsisdn from || tablename || where status :1; OPEN usrs FOR v_sql USING v_status;如果你要遍历多张表可以外层再套一个循环从配置表里读出表名列表逐个OPEN FORFOR rec IN (SELECT table_name FROM cfg_table_list WHERE enabled Y) LOOP v_sql : select id from || rec.table_name || where rownum 101; OPEN usrs FOR v_sql; LOOP FETCH usrs INTO v_id; EXIT WHEN usrs%NOTFOUND; -- 处理逻辑 END LOOP; CLOSE usrs; END LOOP;这里有个坑OPEN和CLOSE必须成对如果循环中途抛异常没CLOSE游标会一直占着超过OPEN_CURSORS参数上限就报ORA-01000: maximum open cursors exceeded。稳妥做法是用BEGIN ... EXCEPTION ... END包住在异常分支里也CLOSE。4. 验证请求与成功结果执行计划与遍历结果核对写完存储过程别急着上生产先验证。验证分两层语法编译层和数据结果层。第一层编译检查。在 SQL*Plus 或 SQL Developer 里执行SHOW ERRORS PROCEDURE DDGZ1如果有编译错误会列出来。动态 SQL 的坑在于编译期不报错因为 SQL 文本是运行时才拼出来的所以编译通过不代表运行通过。第二层实际执行并核对结果。先造点测试数据INSERT INTO tmp_DDGZ1 (v_ERP,v_DRP,v_jxsmc,v_jxs,v_DWMC,v_czr,v_jlsj) VALUES (ERP001,DRP001,经销商A,JXS001,单位A,张三,SYSDATE); COMMIT;然后执行存储过程BEGIN DDGZ1(出库,新增,BH20240101); END; /执行完查结果表SELECT JLID,DRP,ERP,JXS,JXSMC,CZR,BHID FROM DRPDDGZJL2 WHERE BHID 出库单BH20240101;如果行数和tmp_DDGZ1里的源数据行数一致说明遍历完整。这里有个细节SYS_GUID()每次生成的主键不同所以核对时用业务字段比对别用主键。执行计划怎么看动态 SQL 的执行计划不会自动缓存到静态 SQL 区域你可以用DBMS_XPLAN配合V$SQL查SELECT sql_id, sql_text, executions FROM V$SQL WHERE sql_text LIKE %tmp_DDGZ1% ORDER BY last_active_time DESC;拿到sql_id后SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(你的sql_id, 0, ALLSTATS LAST));重点看TABLE ACCESS FULL还是INDEX RANGE SCAN。动态 SQL 因为文本每次可能不同硬解析多如果拼接出来的 SQL 没有走索引全表扫描在大表上会拖垮性能。我踩过的坑就是拼接时字段类型不匹配导致隐式转换索引失效执行计划从索引扫描变成全表扫描慢了几十倍。如果你用 TaoToken 的模型对话能力辅助分析执行计划可以把DBMS_XPLAN的输出贴进去让它帮你解读哪一步开销大。模型对话入口在 https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 把执行计划文本和表结构一起给它比单纯问「为什么慢」有效得多。验证遍历结果时还要注意%ROWCOUNT属性DBMS_OUTPUT.PUT_LINE(处理行数 || usrs%ROWCOUNT);这个值在CLOSE之后还能读是核对遍历数量的直接依据。如果%ROWCOUNT是 0说明OPEN FOR的 SQL 没查到数据回去检查拼接的 WHERE 条件。5. 本篇常见报错排查401、ORA-01000、ORA-00942 对照处理这一节按真实报错来每个都给现象、原因、处理。ORA-01000: maximum open cursors exceeded。现象是存储过程跑一会儿就报这个。原因是OPEN了没CLOSE或者循环里反复OPEN同一个游标变量没关。处理确保每个OPEN都有对应CLOSE异常分支也要关。查当前打开的游标SELECT COUNT(*) FROM V$OPEN_CURSOR WHERE user_name 你的用户名;如果数值接近OPEN_CURSORS参数值调大参数或者修代码SHOW PARAMETER open_cursors; ALTER SYSTEM SET open_cursors 1000 SCOPE BOTH;ORA-00942: table or view does not exist。动态 SQL 里拼的表名不存在或者当前用户没权限。因为动态 SQL 运行时才解析编译期不报。处理先单独执行拼出来的 SQL 字符串确认表名拼对了再检查权限。注意表名大小写Oracle 默认大写如果你拼的是小写又没加双引号会找不到。ORA-00904: invalid identifier。字段名拼错或者字段在当前表不存在。同样是运行时才报。处理把v_sql用DBMS_OUTPUT.PUT_LINE打出来肉眼核对字段名。ORA-01422: exact fetch returns more than requested number of rows。用EXECUTE IMMEDIATE ... INTO接多行结果。处理改用OPEN FORFETCH循环别用EXECUTE IMMEDIATE INTO。401 Unauthorized。这个不是 Oracle 的错是你在用编码客户端或 API 通道时 Key 不对。现象是请求被拒。处理检查三件套——Base URL 是不是https://taotoken.net/apiKey 是不是控制台最新生成的Model ID 是不是模型对话页确认过的。三者任一不对都可能 401。Key 轮换后旧 Key 失效也会 401去 API Keys 页面重新生成。local proxy failed。客户端配置了本地代理但代理没起来或者 Base URL 填成了本地地址。处理确认 Base URL 直接填https://taotoken.net/api不要填localhost或127.0.0.1。reading choices 相关报错。通常是返回结构解析失败多见于 Model ID 填错或者请求体格式不对。处理核对 Model ID确认请求体是标准的 chat completions 格式。OAuth 相关报错。如果你用的是 Claude Code 接入OAuth 流程没走完或者 token 过期。处理重新走一遍接入流程参考 https://taotoken.net/ClaudeCodeAnthropic?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 的步骤。排查顺序建议先看 Oracle 报错号ORA- 开头的是数据库层去查V$SQL和V$OPEN_CURSOR401、proxy、choices 这类是客户端层去查三件套配置。两层别混着查不然容易绕晕。6. 把动态游标用稳凭证收口与长期维护建议动态游标写出来不难难的是长期维护不失控。我给几个实操建议。第一动态 SQL 字符串一定要打日志。在OPEN FOR之前加一行DBMS_OUTPUT.PUT_LINE(v_sql)或者写进日志表。出问题时你能直接看到拼出来的 SQL 长什么样比猜快得多。生产环境可以把日志级别调低只在调试时打开。第二表名和字段名做白名单校验。因为表名不能用绑定变量只能拼接所以必须防注入。做法是维护一张允许的表名清单拼接前先查清单确认SELECT COUNT(*) INTO v_cnt FROM cfg_allowed_tables WHERE table_name tablename; IF v_cnt 0 THEN RAISE_APPLICATION_ERROR(-20001, 非法表名 || tablename); END IF;第三凭证统一管理。不管是数据库连接串还是外部 API 的 Key都别硬编码。Oracle 侧可以用 Wallet客户端侧用 TaoToken 的统一 Key控制台轮换一次所有引用处生效。Coding Plan 适合长期做编码辅助的团队把 Key 和额度统一管起来入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。第四动态游标的性能要定期看。因为硬解析多共享池压力大。可以开CURSOR_SHARING或者用绑定变量减少硬解析但绑定变量和动态表名有冲突需要权衡。我的经验是表名动态、条件值绑定这样既灵活又不至于每次都硬解析。第五异常处理要完整。动态游标最常见的生产事故就是异常没捕获导致游标没关。模板BEGIN OPEN usrs FOR v_sql; LOOP FETCH usrs INTO v_val; EXIT WHEN usrs%NOTFOUND; -- 处理 END LOOP; EXCEPTION WHEN OTHERS THEN IF usrs%ISOPEN THEN CLOSE usrs; END IF; RAISE; END;%ISOPEN判断很重要游标没开就CLOSE会报ORA-01001: invalid cursor。最后说个真实体会动态游标是把双刃剑灵活性的代价是可读性和执行计划稳定性。能用静态 SQL 解决的别用动态非用不可的时候把拼接逻辑封装成独立函数加白名单、加日志、加异常处理三样齐全再上生产。凭证这块统一到 TaoToken 一个 Key 上轮换和审计都省心比散落在各个脚本里强太多。
阅读完成 · 觉得有帮助?