ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Oracle动态游标实战:用SYS_REFCURSOR打通TaoToken多模型调用链

Oracle动态游标实战:用SYS_REFCURSOR打通TaoToken多模型调用链

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%ROWTYPE,fetch 的时候类型必须对得上,否则运行时报错。这个坑我在第一次写的时候就踩了,后面会详细说。

这篇文章的目标很明确:把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_source=taotoken_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_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite。不过这是后话,先把单次调用跑通再说。

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 :3,USING后面依次列出。用绑定变量而不是字符串拼接,能避免 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_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite。这样能快速确认是模型侧的问题还是你代码侧的问题。

验证通过后,建议把成功的请求和响应各存一份到日志表,方便后续排查。字段至少包括:请求时间、模型名、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_TABLE(12c 以上),比字符串截取可靠。

错误五: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_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite,里面有完整的接口字段和示例。Key 的管理在 API Keys 页面,入口是https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite,可以随时查看和轮换。

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_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite。控制台在https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite,可以查看用量和调用明细。

最后说个真实体会:动态游标这东西,看别人写觉得简单,自己写才知道细节多。EXIT WHEN的位置、弱类型的接收变量、游标的关闭时机,每个点都能让你调半天。但一旦用顺了,它在数据路由和批处理场景下的价值是静态游标替代不了的。配合统一的 API 通道,数据库侧的 AI 集成能做到很短的链路,这对延迟敏感的业务很有意义。

RELATED READING

延伸阅读

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