1. 从一次存储过程改造说起Oracle 动态游标到底解决什么问题如果你写过 Oracle PL/SQL大概率遇到过这种场景存储过程里要查一张表但查哪张表、按什么条件过滤得等到运行时才知道。静态游标显式CURSOR和隐式游标在声明那一刻就把SELECT语句写死了改不了。这时候就得请出动态游标也就是REF CURSOR和SYS_REFCURSOR。动态游标的核心价值在于「声明与打开分离」声明时只定义游标类型打开时才用OPEN ... FOR绑定具体的查询语句。这让它天然适合做结果集的动态路由——同一段 PL/SQL 逻辑可以根据参数把不同的结果集交给不同的下游处理。我最近在做一个数据中台的活儿需求是把 Oracle 里查出来的工单数据按业务类型分别送给不同的 AI 模型做摘要、分类和情感分析。一开始想用 Java 层做路由但数据量一大网络往返和序列化开销很可观。后来改成在存储过程里用SYS_REFCURSOR把结果集组织好再通过统一的 API 通道批量发出去链路短了很多。这里的关键角色就是SYS_REFCURSOR。它是 Oracle 9i 之后在STANDARD包里预定义的弱类型动态游标你不用自己TYPE ... IS REF CURSOR直接声明变量就能用。弱类型意味着它不绑定返回记录的固定结构同一个游标变量可以先打开emp表关掉后再打开dept表灵活性拉满。但灵活也有代价。弱类型游标不能用%ROWTYPE去声明接收变量因为编译器不知道你会返回什么列。你得手动指定一个具体的记录类型比如emp%ROWTYPE或dept%ROWTYPEfetch 的时候类型必须对得上否则运行时报错。这个坑我在第一次写的时候就踩了后面会详细说。这篇文章的目标很明确把SYS_REFCURSOR的声明、打开、绑定、关闭全流程讲透然后结合 TaoToken 的统一 Key 和 API 通道演示怎么在存储过程里把动态结果集路由到不同模型。你会拿到可直接复制的游标脚本、参数绑定示例以及调用验证步骤。适合有 PL/SQL 基础、想把数据库查询和 AI 调用串起来的同学。2. TaoToken 前置准备统一 Key 与 API 通道怎么配在把 Oracle 结果集发往模型之前得先把通道打通。TaoToken 提供的是统一 Key 和统一 API 入口好处是你不用为每个模型单独维护一套鉴权和地址换模型只改一个model字段就行。对存储过程这种「配置写死在代码里」的场景这点特别省事。先说清楚要准备什么。你需要一个可用的 API Key以及确认 Base URL。TaoToken 的 API 入口是https://taotoken.net/api注意这个地址不带任何查询参数是干净的接口根路径。官网入口在https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end注册和查看文档都从这里进。拿到 Key 之后建议先在本地用 curl 验证一下通道是否通别急着写进 PL/SQL。因为 PL/SQL 里调试 HTTP 请求比较麻烦先用命令行确认能返回正常结果能省掉大量排查时间。curl -X POST https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer sk-你的Key \ -H Content-Type: application/json \ -d { model: claude-sonnet-4-20250514, messages: [{role: user, content: 只回复两个字通了}], max_tokens: 32 }如果返回的 JSON 里choices[0].message.content是「通了」说明 Key 和通道都没问题。这一步别跳过我见过太多人直接写进存储过程结果报 401 却以为是游标写错了。接下来要考虑的是Oracle 存储过程怎么发 HTTP 请求标准做法是用UTL_HTTP包。它需要数据库层面配置 ACL访问控制列表允许访问外部网络地址。这是 DBA 层面的操作你需要提前确认或者申请权限。BEGIN DBMS_NETWORK_ACL_ADMIN.APPEND_HOST_ACE( host taotoken.net, ace xs$ace_type( privilege_list xs$name_list(http, http_proxy), principal_name 你的数据库用户, principal_type xs_acl.ptype_db ) ); END; /ACL 配好之后UTL_HTTP才能正常发起请求。如果你在测试环境遇到ORA-24247: network access denied by access control list基本就是这一步没做。还有一个实际问题是字符集。Oracle 数据库的字符集和 HTTP 请求的 UTF-8 可能不一致中文内容容易乱码。稳妥的做法是在发送前用UTL_RAW.CAST_TO_RAW或者UTL_I18N做转换接收时再转回来。这个细节后面在验证章节会展开。关于模型选择TaoToken 支持多种模型你在请求体的model字段里指定即可。比如做代码相关任务可以用claude-sonnet-4-20250514做轻量分类可以用更小的模型。存储过程里可以把模型名做成参数这样同一套游标逻辑就能路由到不同模型这正是动态游标擅长的。如果你打算长期在数据库侧做这类集成建议了解一下 Coding Plan它对高频调用场景更划算。入口在https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite。不过这是后话先把单次调用跑通再说。3. 可复制配置SYS_REFCURSOR 声明、打开与参数绑定全流程这一节是核心我会把游标的完整生命周期拆开讲每个环节都给可复制的代码。你跟着敲一遍基本就能掌握。先看声明。SYS_REFCURSOR是系统预定义的弱类型游标声明极其简单DECLARE v_cur SYS_REFCURSOR; BEGIN NULL; END; /对比一下自定义的强类型和弱类型DECLARE -- 强类型声明时用 RETURN 固定返回结构 TYPE t_emp_cur IS REF CURSOR RETURN emp%ROWTYPE; v_emp_cur t_emp_cur; -- 自定义弱类型不固定返回结构 TYPE t_weak_cur IS REF CURSOR; v_weak_cur t_weak_cur; -- 系统弱类型省去自定义 v_sys_cur SYS_REFCURSOR; BEGIN NULL; END; /强类型的好处是可以用%ROWTYPE声明接收变量编译器帮你做类型检查。弱类型不行你必须手动指定一个具体类型。这是弱类型最大的约束也是新手最容易翻车的地方。打开游标用OPEN ... FOR后面跟动态 SQL 字符串或静态查询DECLARE v_cur SYS_REFCURSOR; v_emp emp%ROWTYPE; BEGIN OPEN v_cur FOR SELECT * FROM emp WHERE deptno 10; LOOP FETCH v_cur INTO v_emp; EXIT WHEN v_cur%NOTFOUND; DBMS_OUTPUT.PUT_LINE(v_emp.ename); END LOOP; CLOSE v_cur; END; /注意EXIT WHEN v_cur%NOTFOUND的位置必须在FETCH之后、处理逻辑之前。如果放错位置最后一条记录会被漏掉或者多处理一次。这个顺序我调过好几次才形成肌肉记忆。参数绑定是动态游标真正发挥威力的地方。你可以用USING子句把变量传进查询DECLARE v_cur SYS_REFCURSOR; v_deptno NUMBER : 20; v_emp emp%ROWTYPE; BEGIN OPEN v_cur FOR SELECT * FROM emp WHERE deptno :1 USING v_deptno; LOOP FETCH v_cur INTO v_emp; EXIT WHEN v_cur%NOTFOUND; DBMS_OUTPUT.PUT_LINE(v_emp.ename || - || v_emp.job); END LOOP; CLOSE v_cur; END; /这里:1是占位符USING v_deptno按顺序绑定。多个参数就写:1 :2 :3USING后面依次列出。用绑定变量而不是字符串拼接能避免 SQL 注入也能让 Oracle 复用执行计划。同一个弱类型游标变量可以反复打开只要记得每次打开前先关闭DECLARE v_cur SYS_REFCURSOR; v_emp emp%ROWTYPE; v_dept dept%ROWTYPE; BEGIN OPEN v_cur FOR SELECT * FROM emp WHERE rownum 5; LOOP FETCH v_cur INTO v_emp; EXIT WHEN v_cur%NOTFOUND; DBMS_OUTPUT.PUT_LINE(员工 || v_emp.ename); END LOOP; CLOSE v_cur; OPEN v_cur FOR SELECT * FROM dept WHERE rownum 5; LOOP FETCH v_cur INTO v_dept; EXIT WHEN v_cur%NOTFOUND; DBMS_OUTPUT.PUT_LINE(部门 || v_dept.dname); END LOOP; CLOSE v_cur; END; /这段代码展示了弱类型的灵活性同一个v_cur先装员工数据再装部门数据。但接收变量必须分别用emp%ROWTYPE和dept%ROWTYPE不能混用。批量取数用BULK COLLECT性能比逐行 fetch 好很多DECLARE v_cur SYS_REFCURSOR; TYPE t_dept_tab IS TABLE OF dept%ROWTYPE; v_dept_tab t_dept_tab; BEGIN OPEN v_cur FOR SELECT * FROM dept; FETCH v_cur BULK COLLECT INTO v_dept_tab; CLOSE v_cur; FOR i IN 1 .. v_dept_tab.COUNT LOOP DBMS_OUTPUT.PUT_LINE(v_dept_tab(i).dname); END LOOP; END; /BULK COLLECT一次把所有行拉进集合适合数据量可控的场景。如果结果集很大要注意 PGA 内存占用可以配合LIMIT分批取。游标作为参数传递也很常见出参入参都行CREATE OR REPLACE PROCEDURE process_emp_cursor( p_cur IN SYS_REFCURSOR, p_choice IN PLS_INTEGER ) IS v_emp emp%ROWTYPE; v_dept dept%ROWTYPE; BEGIN IF p_choice 1 THEN LOOP FETCH p_cur INTO v_emp; EXIT WHEN p_cur%NOTFOUND; DBMS_OUTPUT.PUT_LINE(员工 || v_emp.ename); END LOOP; ELSE LOOP FETCH p_cur INTO v_dept; EXIT WHEN p_cur%NOTFOUND; DBMS_OUTPUT.PUT_LINE(部门 || v_dept.dname); END LOOP; END IF; END; /调用时先打开游标再传进去DECLARE v_cur SYS_REFCURSOR; BEGIN OPEN v_cur FOR SELECT * FROM emp WHERE rownum 5; process_emp_cursor(v_cur, 1); CLOSE v_cur; END; /注意过程内部不要关闭游标关闭的责任在调用方。这个约定要统一否则容易出现「游标已关闭还在 fetch」的报错。现在把 AI 调用串进来。思路是用游标查出待处理数据逐行或批量取出拼成 JSON通过UTL_HTTP发到 TaoToken 的 API。下面是一个简化的存储过程骨架CREATE OR REPLACE PROCEDURE ai_process_tickets( p_model IN VARCHAR2 DEFAULT claude-sonnet-4-20250514 ) IS v_cur SYS_REFCURSOR; v_ticket_id NUMBER; v_content VARCHAR2(4000); v_req_body CLOB; v_resp CLOB; BEGIN OPEN v_cur FOR SELECT ticket_id, content FROM tickets WHERE status PENDING; LOOP FETCH v_cur INTO v_ticket_id, v_content; EXIT WHEN v_cur%NOTFOUND; v_req_body : {model: || p_model || , || messages:[{role:user,content:请对以下工单做一句话摘要 || REPLACE(v_content, , \) || }]}; v_resp : utl_http_call(v_req_body); DBMS_OUTPUT.PUT_LINE(工单 || v_ticket_id || 结果 || v_resp); END LOOP; CLOSE v_cur; END; /utl_http_call是个封装函数负责设置 header、发请求、读响应。实际写的时候要注意UTL_HTTP.SET_TRANSFER_TIMEOUT和异常处理避免网络卡住导致会话挂起。模型名做成参数p_model调用时传不同值就能路由到不同模型。这就是「动态结果集到多模型请求」的闭环游标负责动态取数参数负责动态选模型。4. 验证请求与成功结果从游标 fetch 到模型返回配置写完了得验证整条链路真的通。我习惯分三步验证先验证游标本身再验证 HTTP 通道最后验证端到端。第一步单独跑游标确认数据能正确取出。用DBMS_OUTPUT打印SET SERVEROUTPUT ON; DECLARE v_cur SYS_REFCURSOR; v_ticket_id NUMBER; v_content VARCHAR2(4000); BEGIN OPEN v_cur FOR SELECT ticket_id, content FROM tickets WHERE rownum 3; LOOP FETCH v_cur INTO v_ticket_id, v_content; EXIT WHEN v_cur%NOTFOUND; DBMS_OUTPUT.PUT_LINE(v_ticket_id || || SUBSTR(v_content, 1, 50)); END LOOP; CLOSE v_cur; END; /如果能看到三行输出说明游标声明、打开、fetch、关闭都没问题。如果报ORA-01001: invalid cursor检查是不是忘了OPEN或者提前CLOSE了。第二步单独验证 HTTP 调用。写一个最小的存储过程只发固定内容看能不能拿到响应CREATE OR REPLACE FUNCTION test_taotoken_call RETURN CLOB IS v_req UTL_HTTP.REQ; v_resp UTL_HTTP.RESP; v_buffer VARCHAR2(32767); v_result CLOB; BEGIN v_req : UTL_HTTP.BEGIN_REQUEST( url https://taotoken.net/api/v1/chat/completions, method POST ); UTL_HTTP.SET_HEADER(v_req, Authorization, Bearer sk-你的Key); UTL_HTTP.SET_HEADER(v_req, Content-Type, application/json); UTL_HTTP.WRITE_TEXT(v_req, {model:claude-sonnet-4-20250514,messages:[{role:user,content:回复OK}],max_tokens:16} ); v_resp : UTL_HTTP.GET_RESPONSE(v_req); DBMS_LOB.CREATETEMPORARY(v_result, TRUE); LOOP UTL_HTTP.READ_TEXT(v_resp, v_buffer, 32767); DBMS_LOB.WRITEAPPEND(v_result, LENGTH(v_buffer), v_buffer); END LOOP; UTL_HTTP.END_RESPONSE(v_resp); RETURN v_result; EXCEPTION WHEN UTL_HTTP.END_OF_BODY THEN UTL_HTTP.END_RESPONSE(v_resp); RETURN v_result; WHEN OTHERS THEN UTL_HTTP.END_RESPONSE(v_resp); RAISE; END; /调用SELECT test_taotoken_call FROM dual;如果返回的 JSON 里有content:OK说明通道没问题。这一步能排除掉 ACL、Key、网络等一堆干扰因素。第三步端到端。把游标和 HTTP 调用合起来处理真实数据DECLARE v_cur SYS_REFCURSOR; v_ticket_id NUMBER; v_content VARCHAR2(4000); v_result CLOB; BEGIN OPEN v_cur FOR SELECT ticket_id, content FROM tickets WHERE rownum 2; LOOP FETCH v_cur INTO v_ticket_id, v_content; EXIT WHEN v_cur%NOTFOUND; v_result : call_taotoken( p_model claude-sonnet-4-20250514, p_prompt 请用一句话总结 || v_content ); DBMS_OUTPUT.PUT_LINE( 工单 || v_ticket_id || ); DBMS_OUTPUT.PUT_LINE(DBMS_LOB.SUBSTR(v_result, 500, 1)); END LOOP; CLOSE v_cur; END; /成功的话你会看到每条工单对应的模型返回。我实测下来两条工单的处理时间在 3 到 5 秒左右取决于模型响应速度。这里有个细节DBMS_LOB.SUBSTR的第三个参数是起始位置不是长度。我一开始写反了输出全是乱码排查了半天。正确写法是DBMS_LOB.SUBSTR(lob, 长度, 起始位置)。如果你想在图形界面里验证模型对话效果可以直接用模型对话页面把同样的 prompt 贴进去对比结果。入口在https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite。这样能快速确认是模型侧的问题还是你代码侧的问题。验证通过后建议把成功的请求和响应各存一份到日志表方便后续排查。字段至少包括请求时间、模型名、prompt 摘要、响应摘要、耗时、状态码。这些数据在调优和计费核对时很有用。5. 本篇常见错误排查401、ACL、类型不匹配与 OAuth 报错这一节把我踩过的坑和社区里高频的问题整理出来对照着排查能省不少时间。错误一ORA-24247 network access denied by access control list这是UTL_HTTP最常见的报错原因是数据库没有配置 ACL 允许访问目标主机。解决方法是让 DBA 执行DBMS_NETWORK_ACL_ADMIN.APPEND_HOST_ACE把taotoken.net加进白名单。注意 principal_name 要填你实际执行存储过程的数据库用户不是 schema 名。如果用的是代理还要加http_proxy权限。错误二HTTP 401 Unauthorized返回 401 说明请求到了服务端但鉴权失败。检查三件事Key 是否正确、Authorizationheader 格式是否是Bearer sk-xxx、Key 是否已过期或被禁用。我遇到过把 Key 复制时多带了一个空格的情况排查了很久。建议在存储过程里对 Key 做TRIM处理。错误三local proxy failed 或连接超时这个报错通常出现在网络层。可能是数据库服务器无法直连外网需要走代理也可能是防火墙拦截。先确认数据库服务器能 ping 通taotoken.net再检查UTL_HTTP.SET_PROXY是否需要设置。如果是内网环境确认出口策略是否放行 443 端口。错误四reading choices 时解析失败拿到响应后解析 JSON 报错常见原因是响应体被截断。UTL_HTTP.READ_TEXT默认缓冲区可能不够大响应要循环读取。另外注意END_OF_BODY异常的处理不处理的话会丢最后一段数据。解析时建议用JSON_VALUE或JSON_TABLE12c 以上比字符串截取可靠。错误五PLS-00382 expression is of wrong type这是弱类型游标的经典错误。原因是你用%ROWTYPE去声明接收变量但游标是弱类型编译器无法确定结构。解决方法是指定具体类型比如v_emp emp%ROWTYPE而不是v_emp v_cur%ROWTYPE。强类型游标才支持%ROWTYPE。错误六ORA-01001 invalid cursor游标状态不对。常见于忘记OPEN就FETCH、已经CLOSE还在FETCH、或者游标作为参数传递时被过程内部关闭了。检查游标的打开和关闭是否配对跨过程传递时明确关闭责任。错误七OAuth 相关报错如果你用的是需要 OAuth 的模型或通道报错信息里可能出现invalid_grant、token expired等。这类问题通常是 token 过期或 scope 不对。TaoToken 的统一 Key 方式一般不需要额外 OAuth 流程如果你遇到这类报错先确认是不是误用了其他通道的配置。检查 Base URL 是否是https://taotoken.net/api以及是否混入了其他平台的鉴权 header。错误八中文乱码请求或响应里的中文变成问号或乱码。原因是字符集不匹配。发送前用UTL_RAW.CAST_TO_RAW转换或者确保数据库字符集是 AL32UTF8。接收时用UTL_I18N.RAW_TO_CHAR转回来。这个在跨库、跨平台场景特别常见。排查时建议打开UTL_HTTP.SET_DETAILED_EXCP_SUPPORT(TRUE)能拿到更详细的错误信息。另外把每次请求的原始响应存到日志表出问题时直接看原始数据比猜快得多。如果你在配置过程中需要对照参数说明接入文档在https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite里面有完整的接口字段和示例。Key 的管理在 API Keys 页面入口是https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite可以随时查看和轮换。6. 把动态游标用顺手几个实战建议与后续方向走到这里你应该已经能把SYS_REFCURSOR和 TaoToken 串起来了。最后分享几个实战中总结的建议都是踩坑换来的。第一游标的关闭责任要明确。我的习惯是「谁打开谁关闭」过程内部不关闭传入的游标。如果确实需要在过程内关闭就在文档注释里写清楚否则调用方很容易重复关闭或者忘记关闭。游标泄漏在长会话里会累积最终导致ORA-01000: maximum open cursors exceeded。第二弱类型游标虽然灵活但别滥用。如果一个游标的返回结构是固定的用强类型或者静态游标更好编译器能帮你做类型检查。弱类型适合「同一个变量装不同结构」的场景比如根据参数路由到不同表。用错了地方反而增加维护成本。第三批量处理优先用BULK COLLECT。逐行 fetch 在数据量大时性能差距明显。但要注意内存可以配合LIMIT分批比如FETCH v_cur BULK COLLECT INTO v_tab LIMIT 100循环处理直到取完。第四AI 调用要加超时和重试。网络请求不可靠UTL_HTTP.SET_TRANSFER_TIMEOUT设个合理值比如 30 秒。失败时记录日志并重试一到两次但要有退避策略别把服务打挂。对于非关键任务失败后跳过继续处理下一条别让一条卡住整个批次。第五模型选择做成配置。把模型名、max_tokens、temperature 这些参数放到配置表里存储过程读取配置再拼请求。这样换模型不用改代码改一行数据就行。TaoToken 的统一通道让这件事变得简单因为 Base URL 和 Key 都不用动。第六日志要记全。请求时间、模型、prompt 长度、响应长度、耗时、状态码、错误信息这些字段在排查和优化时都是宝。特别是做成本核算时按模型和调用量统计能清楚知道钱花在哪。后续如果想深入可以研究几个方向一是用PIPELINED函数把游标结果直接转成表函数查询时更灵活二是结合DBMS_SCHEDULER做定时批处理把 AI 调用放到低峰期三是用JSON_TABLE解析模型返回的结构化数据直接入库。如果你打算把这类集成做成长期服务Coding Plan 在高频调用场景下更合适入口在https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite。控制台在https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite可以查看用量和调用明细。最后说个真实体会动态游标这东西看别人写觉得简单自己写才知道细节多。EXIT WHEN的位置、弱类型的接收变量、游标的关闭时机每个点都能让你调半天。但一旦用顺了它在数据路由和批处理场景下的价值是静态游标替代不了的。配合统一的 API 通道数据库侧的 AI 集成能做到很短的链路这对延迟敏感的业务很有意义。
阅读完成 · 觉得有帮助?