1. Oracle 存储过程返回表数据的真实场景与三种路径
先说结论:Oracle 的存储过程本身不能像SELECT * FROM employees那样直接"返回一张表",但它可以通过三种成熟机制把结果集交给调用方——SYS_REFCURSOR游标、PIPELINED表函数、临时表。这三种路径各有适用边界,选错了要么代码难维护,要么性能塌方。
我在实际项目里遇到过这样的需求:一个 Java 服务需要拿到 Oracle 里某张宽表的全量数据做离线计算,最初同事写了个存储过程用DBMS_OUTPUT.PUT_LINE逐行打印,结果调用方根本拿不到结构化数据,只能去解析日志文本。这就是典型的"以为存储过程能返回表"的误区。后来改成SYS_REFCURSOR输出参数,问题当场解决。
所以这篇内容要解决的核心问题是:Oracle SQL 存储过程到底能不能返回表?能返回的话,三种路径分别怎么写、怎么调、怎么验证返回行数对不对?适合谁看?适合正在写 PL/SQL 包、需要把结果集暴露给外部程序(Java/Python/Go)、或者想把数据库查询能力通过统一 API 通道对外提供的开发者。
三种路径的定位差异,先用一张表说清楚:
| 路径 | 返回形态 | 适用场景 | 调用方限制 |
|---|---|---|---|
| SYS_REFCURSOR | 游标引用(OUT 参数) | 一次性查询结果集,行数不定 | 需支持游标读取的客户端 |
| PIPELINED 表函数 | 虚拟表(可TABLE()查询) | 需要像表一样 JOIN/WHERE | 可在 SQL 中直接引用 |
| 临时表 | 物理/全局临时表 | 多步骤中间结果、跨过程共享 | 需管理生命周期与并发 |
SYS_REFCURSOR是最常用的,因为它简单直接:存储过程声明一个OUT SYS_REFCURSOR参数,OPEN ... FOR SELECT把结果集挂上去,调用方拿到游标后逐行 FETCH。缺点是游标只能顺序读,不能像表一样被 SQL 引擎二次加工。
PIPELINED表函数则把结果集变成"虚拟表",调用方可以SELECT * FROM TABLE(my_func()),甚至和别的表 JOIN。代价是函数内部要用PIPE ROW逐行输出,写法比 REF CURSOR 啰嗦,且对复杂查询的优化器友好度需要实测。
临时表适合"过程内部多步计算、最后统一输出"的场景,比如先算聚合再关联维度表。但全局临时表(GTT)的数据在会话间隔离,如果调用方是连接池复用的会话,要特别小心ON COMMIT DELETE ROWS和ON COMMIT PRESERVE ROWS的选择。
理解了这三条路径,接下来的问题就是:怎么把它们接到一个统一的调用通道上做端到端验证。我选择用 TaoToken 的统一 Key/API 通道来演示,原因是它把模型调用和工具调用的 endpoint 收敛到一个 Base URL,改配置时只需要动一处,验证存储过程返回结果这件事就能和 AI 辅助生成 SQL、解析结果集串起来。
2. TaoToken 统一 Key 通道前置准备与 endpoint 配置
在动手写存储过程之前,先把调用通道准备好。TaoToken 的作用是提供一个统一的 API 入口,你不需要在多个服务之间来回切换 Key 和 Base URL。官网地址是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 根地址是 https://taotoken.net/api (这个不加 UTM 参数,直接用于配置)。
前置准备分三步:拿 Key、确认 Base URL、选模型 ID。这三件套在后面的配置片段里会反复出现,先记牢。
第一步,登录后进入控制台创建 API Key。访问 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,在 API Keys 页面生成一个新 Key。建议按项目命名,比如oracle-proc-test,方便后续排查是哪个调用方出的问题。生成后立刻复制保存,页面刷新后就不再完整显示。
第二步,确认 Base URL。所有请求都走https://taotoken.net/api,无论是模型对话还是工具调用,路径前缀统一。这一点很关键:很多接入失败是因为把 Base URL 写成了带/v1或带具体模型路径的形式,导致 404。
第三步,选模型 ID。如果你只是用 AI 辅助生成 PL/SQL 脚本、解析返回的 JSON 结果,选一个擅长代码的模型即可。模型 ID 在模型对话页面可以查到,访问 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 查看当前可用列表。
如果你打算长期做编码类任务,比如让 AI 持续帮你写存储过程、做 SQL 审查,可以考虑 Coding Plan,入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。它的定位是面向长期编码场景的套餐,比按次调用更适合高频使用。
这里要提醒一个常见误区:TaoToken 不是数据库代理,它不会替你连 Oracle。它的角色是统一 API 通道,你用它来调用模型能力(比如让模型生成建包脚本、解析结果集),而 Oracle 连接仍然由你自己的客户端(JDBC/OCI/Python cx_Oracle)负责。两者是配合关系,不是替代关系。
配置完成后,你可以先用一个最简单的请求验证通道是否通。下面这段是 curl 示例,把YOUR_API_KEY换成刚才生成的 Key:
curl https://taotoken.net/api/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "model": "YOUR_MODEL_ID", "messages": [ {"role": "user", "content": "用一句话说明 Oracle SYS_REFCURSOR 的作用"} ] }'如果返回了正常的 JSON 且choices数组里有内容,说明通道打通。如果报 401,检查 Key 是否复制完整;如果报 model not found,检查模型 ID 是否拼写正确。这一步过了,再进入存储过程的实操。
3. 可复制的建包建过程脚本与 settings 配置片段
这一节给出三种路径的完整可复制脚本。我建议你按顺序试:先跑SYS_REFCURSOR,再跑PIPELINED,最后跑临时表。每段脚本都可以直接在 SQL*Plus 或 SQL Developer 里执行。
先建一张测试表,避免依赖你现有的业务表:
CREATE TABLE emp_test ( emp_id NUMBER, emp_name VARCHAR2(50), dept_no NUMBER, salary NUMBER ); INSERT INTO emp_test VALUES (1, 'Alice', 10, 8000); INSERT INTO emp_test VALUES (2, 'Bob', 10, 9500); INSERT INTO emp_test VALUES (3, 'Carol', 20, 12000); INSERT INTO emp_test VALUES (4, 'Dave', 20, 7000); COMMIT;3.1 SYS_REFCURSOR 路径
建一个包,包含一个通过 OUT 参数返回游标的过程:
CREATE OR REPLACE PACKAGE emp_pkg IS PROCEDURE get_emp_by_dept ( p_dept_no IN NUMBER, p_cur OUT SYS_REFCURSOR ); END emp_pkg; / CREATE OR REPLACE PACKAGE BODY emp_pkg IS PROCEDURE get_emp_by_dept ( p_dept_no IN NUMBER, p_cur OUT SYS_REFCURSOR ) IS BEGIN OPEN p_cur FOR SELECT emp_id, emp_name, dept_no, salary FROM emp_test WHERE dept_no = p_dept_no ORDER BY emp_id; END get_emp_by_dept; END emp_pkg; /调用方式(在 PL/SQL 块里验证):
DECLARE v_cur SYS_REFCURSOR; v_id emp_test.emp_id%TYPE; v_name emp_test.emp_name%TYPE; v_dept emp_test.dept_no%TYPE; v_sal emp_test.salary%TYPE; v_cnt NUMBER := 0; BEGIN emp_pkg.get_emp_by_dept(10, v_cur); LOOP FETCH v_cur INTO v_id, v_name, v_dept, v_sal; EXIT WHEN v_cur%NOTFOUND; v_cnt := v_cnt + 1; DBMS_OUTPUT.PUT_LINE(v_id || ' | ' || v_name || ' | ' || v_sal); END LOOP; CLOSE v_cur; DBMS_OUTPUT.PUT_LINE('返回行数: ' || v_cnt); END; /预期输出两行(Alice、Bob),行数断言为 2。
3.2 PIPELINED 表函数路径
PIPELINED 需要先定义行类型和表类型,再写函数:
CREATE OR REPLACE TYPE emp_row_type AS OBJECT ( emp_id NUMBER, emp_name VARCHAR2(50), dept_no NUMBER, salary NUMBER ); / CREATE OR REPLACE TYPE emp_table_type AS TABLE OF emp_row_type; / CREATE OR REPLACE FUNCTION get_emp_pipelined (p_dept_no NUMBER) RETURN emp_table_type PIPELINED IS BEGIN FOR r IN (SELECT emp_id, emp_name, dept_no, salary FROM emp_test WHERE dept_no = p_dept_no ORDER BY emp_id) LOOP PIPE ROW (emp_row_type(r.emp_id, r.emp_name, r.dept_no, r.salary)); END LOOP; RETURN; END; /调用时可以直接当表用:
SELECT * FROM TABLE(get_emp_pipelined(20));预期返回 Carol、Dave 两行。这种写法的好处是能继续 JOIN:
SELECT t.emp_name, t.salary * 12 AS annual_salary FROM TABLE(get_emp_pipelined(20)) t WHERE t.salary > 8000;3.3 临时表路径
全局临时表适合多步计算:
CREATE GLOBAL TEMPORARY TABLE emp_gtt ( emp_id NUMBER, emp_name VARCHAR2(50), dept_no NUMBER, salary NUMBER ) ON COMMIT PRESERVE ROWS; CREATE OR REPLACE PROCEDURE load_emp_gtt (p_dept_no NUMBER) IS BEGIN DELETE FROM emp_gtt WHERE dept_no = p_dept_no; INSERT INTO emp_gtt (emp_id, emp_name, dept_no, salary) SELECT emp_id, emp_name, dept_no, salary FROM emp_test WHERE dept_no = p_dept_no; COMMIT; END; /调用后直接查emp_gtt即可。注意ON COMMIT PRESERVE ROWS表示提交后数据保留,适合跨语句读取;如果改成DELETE ROWS,每次 COMMIT 后表就空了。
3.4 统一通道的 settings 配置片段
如果你用支持自定义 Base URL 的客户端(比如某些 AI 编码插件、CLI 工具),配置通常长这样。以 JSON 格式为例:
{ "base_url": "https://taotoken.net/api", "api_key": "YOUR_API_KEY", "model": "YOUR_MODEL_ID", "timeout": 60 }如果是 TOML 格式(部分 CLI 工具用):
[provider] base_url = "https://taotoken.net/api" api_key = "YOUR_API_KEY" model = "YOUR_MODEL_ID"如果是 Claude Code 这类工具的 settings 文件,路径通常在用户目录下的配置文件夹里,字段名可能是ANTHROPIC_BASE_URL和ANTHROPIC_API_KEY,把值分别改成https://taotoken.net/api和你的 Key 即可。改完后重启工具,让它重新读取配置。
这里强调三件套的完整性:Base URL、Key、Model ID 缺一不可。我见过有人只改了 Base URL 没改 Key,结果一直 401;也有人 Key 对了但 Model ID 写了个不存在的名字,报 model not found。配置片段复制后,逐字段核对一遍。
4. 验证请求与返回行数断言
配置和脚本都就位后,做一次端到端验证。验证的目标有两个:一是存储过程确实返回了预期行数,二是通过 TaoToken 通道调用的模型能正确解析返回结果。
先做数据库侧的断言。写一个带断言的 PL/SQL 块,如果行数不对就抛异常:
DECLARE v_cur SYS_REFCURSOR; v_id emp_test.emp_id%TYPE; v_nm emp_test.emp_name%TYPE; v_dp emp_test.dept_no%TYPE; v_sl emp_test.salary%TYPE; v_cnt NUMBER := 0; v_expected NUMBER := 2; BEGIN emp_pkg.get_emp_by_dept(10, v_cur); LOOP FETCH v_cur INTO v_id, v_nm, v_dp, v_sl; EXIT WHEN v_cur%NOTFOUND; v_cnt := v_cnt + 1; END LOOP; CLOSE v_cur; IF v_cnt != v_expected THEN RAISE_APPLICATION_ERROR(-20001, '行数断言失败: 期望 ' || v_expected || ' 实际 ' || v_cnt); END IF; DBMS_OUTPUT.PUT_LINE('断言通过,返回行数: ' || v_cnt); END; /跑通后输出"断言通过,返回行数: 2"。这一步确认了存储过程本身没问题。
接下来做通道侧验证。用 Python 写一个脚本,先连 Oracle 拿数据,再把结果集转成 JSON,通过 TaoToken 通道让模型做一次结构化校验(比如检查字段是否齐全、行数是否匹配):
import cx_Oracle import requests import json # 1. 连 Oracle 拿数据 dsn = cx_Oracle.makedsn("localhost", 1521, service_name="ORCL") conn = cx_Oracle.connect(user="scott", password="tiger", dsn=dsn) cur = conn.cursor() ref_cur = conn.cursor() cur.callproc("emp_pkg.get_emp_by_dept", [10, ref_cur]) rows = ref_cur.fetchall() ref_cur.close() cur.close() conn.close() print("Oracle 返回行数:", len(rows)) # 2. 通过 TaoToken 通道做校验 payload = { "model": "YOUR_MODEL_ID", "messages": [ {"role": "user", "content": f"以下是 Oracle 存储过程返回的结果集 JSON,请检查行数是否为 2," f"并列出所有 emp_name:\n{json.dumps(rows, ensure_ascii=False)}"} ] } resp = requests.post( "https://taotoken.net/api/v1/chat/completions", headers={ "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, json=payload, timeout=60 ) print("通道返回状态:", resp.status_code) print(resp.json()["choices"][0]["message"]["content"])预期输出:Oracle 返回行数 2,通道返回状态 200,模型回复里包含 Alice 和 Bob。
如果你用的是 Claude Code 或类似 CLI 工具,验证方式更直接:在工具里让它读一段你贴进去的结果集 JSON,问它行数对不对。前提是 Base URL 已经指向https://taotoken.net/api,Key 和 Model ID 都配好了。
PIPELINED 路径的验证略有不同,因为它可以直接在 SQL 里查:
SELECT COUNT(*) FROM TABLE(get_emp_pipelined(20));预期返回 2。临时表路径则是:
BEGIN load_emp_gtt(20); END; / SELECT COUNT(*) FROM emp_gtt WHERE dept_no = 20;同样预期 2。三条路径都验证一遍,你就对"存储过程返回表"这件事有了完整的体感。
5. 本篇常见报错排查:401、local proxy failed、reading choices、OAuth
实操过程中最容易卡在几个报错上。我按出现频率排一下,逐个给排查思路。
401 Unauthorized。这是最高频的。原因通常是 Key 没带、Key 过期、或者 Key 复制时多了空格。排查步骤:先确认请求头里Authorization: Bearer YOUR_API_KEY格式正确,Bearer 后面有一个空格;再确认 Key 是从控制台完整复制的,没有换行符;最后确认这个 Key 对应的账号状态正常。如果用的是配置文件,检查字段名是否写对,有些工具用api_key,有些用apiKey,大小写敏感。
local proxy failed。这个报错通常出现在客户端配置了本地代理但代理没启动,或者代理地址写错。排查思路:先确认你的网络环境是否需要代理;如果不需要,把客户端里的代理配置清空;如果需要,确认代理进程在运行且端口对得上。注意,这里说的是客户端自身的网络配置,不是让你去搞什么特殊网络手段,纯粹是本地开发环境的代理设置问题。
reading choices 相关报错。典型表现是KeyError: 'choices'或list index out of range。这说明返回的 JSON 结构和你预期的不一样。原因可能是:请求路径写错了(比如漏了/v1),导致返回的是错误页而不是正常响应;或者模型 ID 不存在,返回了错误对象。排查方法:先把resp.text完整打印出来,看原始返回是什么。如果是 HTML 错误页,说明路径不对;如果是 JSON 但结构不同,看error字段的提示。
OAuth 相关报错。如果你用的是 Claude Code 这类走 OAuth 流程的工具,可能会遇到 token 刷新失败或授权过期。排查思路:确认 Base URL 已经改成https://taotoken.net/api,然后重新走一次授权流程。有些工具会缓存旧的 token,需要清掉缓存目录再重试。如果工具同时支持 API Key 和 OAuth 两种模式,优先用 API Key 模式,配置更简单。
除了通道侧报错,数据库侧也有几个坑。ORA-01000: maximum open cursors exceeded说明游标没关,检查每个OPEN是否都有对应的CLOSE。ORA-06550通常是 PL/SQL 编译错误,看具体行号。ORA-00942: table or view does not exist检查表名大小写和 schema 前缀。
还有一个隐蔽的坑:SYS_REFCURSOR作为 OUT 参数时,如果调用方是连接池,游标必须在同一个连接上读取完毕再关闭,不能跨连接。我见过有人在 A 连接打开游标,B 连接去 FETCH,结果报无效游标。记住:游标绑定在打开它的那个会话上。
排查完这些,如果还是不通,最有效的办法是分层验证:先用 curl 直接打 TaoToken 的 endpoint,确认通道本身通;再用 SQL*Plus 直接跑存储过程,确认数据库侧通;最后才把两者串起来。分层定位能省掉大量猜测时间。
6. 把 endpoint 改到 TaoToken 后完成一次端到端验证
最后把整个链路串一遍,给你一个可复制的端到端验证清单。
第一步,确认三件套。Base URL 是https://taotoken.net/api,Key 从 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 获取,Model ID 从模型列表里选。三个值写进你的客户端配置。
第二步,跑数据库侧断言。把第 4 节的 PL/SQL 断言块执行一遍,确认emp_pkg.get_emp_by_dept(10, ...)返回 2 行。这一步不涉及 TaoToken,纯粹验证存储过程。
第三步,跑通道侧验证。用第 4 节的 Python 脚本,或者直接在支持自定义 Base URL 的 CLI 工具里发一个请求,确认返回 200 且choices有内容。
第四步,做联合验证。把 Oracle 返回的结果集 JSON 喂给模型,让它做行数校验和字段检查。如果模型回复"行数为 2,包含 Alice 和 Bob",说明整条链路通了。
如果你需要更细的接入文档,访问 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 查看参数说明和示例。如果只是想快速试一下模型对话,直接去 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite 发一条消息即可。
关于长期使用,如果你发现自己频繁需要 AI 辅助写 PL/SQL、审查 SQL、解析结果集,Coding Plan 会比按次调用更划算,入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。它的定位就是面向持续编码场景。
最后分享一个我踩过的坑:一开始我把 Base URL 写成了https://taotoken.net/api/v1,结果请求路径变成/api/v1/v1/chat/completions,一直 404。后来改成https://taotoken.net/api,让客户端自己拼/v1/chat/completions,就通了。配置时注意 Base URL 和具体路径的分工,别重复拼接。
三条路径的选择上,我的经验是:单次查询用SYS_REFCURSOR,需要 SQL 二次加工用PIPELINED,多步计算用临时表。选对了路径,后面的调用和验证都会顺很多。