☰
Oracle合并多个sys_refcursor:XML序列化与DBMS_LOB实战方案
2026/10/2 11:16:20 网站建设 项目流程

简介:在Oracle数据库开发中,合并多个动态游标sys_refcursor是常见需求,尤其当存储过程逻辑复杂且需循环调用同一逻辑时,重写逻辑或复制代码往往造成冗余。此PDF文档正是为解决该问题而整理,面向有一定PL/SQL基础、希望避免重复开发的中高级开发人员。文档以序列化游标为XML为主线,详细讲解使用xmltype构造函数将游标转为XML、通过getClobVal获取序列化结果、利用XPath提取/ROWSET/ROW节点,以及结合DBMS_LOB.CREATETEMPORARY、WRITEAPPEND、APPEND等方法将多段行数据合并为一个CLOB,再经XMLTABLE解析并封装成新的sys_refcursor。此外还包含背景分析、代码示例与执行结果,可帮助读者掌握整个合并流程并直接应用到自己的存储过程改造中。资源为单个PDF文件,大小约75KB,篇幅紧凑、重点突出,适合快速查阅和落地实践。已有525人学习/浏览,是处理Oracle动态游标合并场景的实用参考。

1. 为什么要合并 sys_refcursor:两个存储过程之间的"代码复制"困境

在 Oracle 数据开发里,合并多个sys_refcursor这个需求,十有八九是从"复用存储过程逻辑"这个场景里长出来的。你手头有一个写好的PROC_A,业务逻辑复杂,代码几百行,跑得好好的;过段时间要写PROC_B,核心逻辑和PROC_A一样,但要在循环里反复调用PROC_A,还得把每次返回的动态游标攒到一起。这时候两条路摆在面前:一条是把PROC_A读透,在PROC_B里重写一遍,代码翻倍,维护翻车;另一条是直接复制PROC_A的代码,拼出一个更长更难看的存储过程。还有第三条路,建临时表往里头插数据,但每次游标返回的列结构不固定,临时表建起来就是一场灾难。这三条路都是黑匣子,要么费人,要么费性能。真正可行的办法,是借 Oracle 对 XML 的原生支持,把sys_refcursor序列化成 XML,再把多个游标的<ROW>节点合并到一个 CLOB 里,最后用XMLTABLE重新解析成新的sys_refcursor返回给调用方。这篇笔记就把这套方案的原理、完整 PL/SQL 代码和我在实际开发中踩过的坑一次讲透,适合被"动态游标无法直接拼接"卡住的中级 Oracle 开发人员。

2. sys_refcursor 合并的核心思路:为什么选中 XML 序列化方案

2.1 sys_refcursor 和普通 CURSOR 的本性差异

先搞清楚sys_refcursor和普通cursor的区别,这决定了你能不能直接拼接。普通cursor是强类型的,声明时就把查询语句和返回列固定死了,它能用OPEN、FETCH、CLOSE操作,但只能在存储过程、函数、包内部使用,不能作为参数传给外部。sys_refcursor是弱类型的动态游标,可以在存储过程参数里进进出出,灵活性高得多,但它是个"无法直接OPEN、FETCH、CLOSE"的黑匣子。正因为它是弱类型,两个sys_refcursor之间没有内置的合并机制,你不能写ref_cur1 UNION ALL ref_cur2这种 SQL。这也是为什么很多人被卡住——想拼数据,但游标本身不给直接操作的入口。

能把两个游标的数据拼在一起的思路,是绕道而行:把游标的内容先"倒"成一种可以被处理的数据结构。Oracle 对 XML 的良好支持就是这个绕道的桥。XMLTYPE类型可以直接接收SYS_REFCURSOR作为构造参数,把游标结果集序列化成 XML 文档,这就等于给动态游标装了一个"取数端口"。序列化之后,游标就不再是黑匣子了,而是一段结构化的 XML 文本,可以用 XPath 提取、用DBMS_LOB拼接、用XMLTABLE再查出来。整个方案的核心思路就是:动态游标 → XML 文档 → 按行节点提取 → CLOB 合并 → 再查回游标。

2.2 为什么临时表方案在这里是死路

有开发经验的朋友可能会想:我在循环里开一个临时表,每轮把游标里的数据 INSERT 进去,最后再 SELECT 出来返回一个游标,不也能合并吗?这个思路在数据列固定的时候完全可行,但问题恰恰出在"列不固定"上。PROC_A每次返回的sys_refcursor,可能列名、列数、数据类型都不同,今天返回F_USERNAME、F_USERCODE,明天可能就变成F_ORDER_ID、F_AMOUNT加一个日期列。临时表只能在建表时把列结构定义死,一旦列变了,要么ALTER TABLE动态加列,要么就得用动态 SQL 拼CREATE TABLE,后者的维护成本比复制代码还可怕。XML 方案对列结构完全免疫,因为 XML 是自描述的,每个<ROW>节点内部带着自己的列名和值,合并时你根本不需要关心列的数量和名字,只管提取节点、拼接节点就行。这就是 XML 序列化路线在这个场景里不可替代的原因。

2.3 前置知识准备:DBMS_LOB 和 XMLTYPE 两个关键包

动手写代码之前,有两个包的能力边界需要先对齐。第一个是DBMS_LOB,它负责处理 CLOB 类型的大对象。为什么需要它?因为游标序列化后的 XML 是一个完整的<?xml version="1.0"?><ROWSET>文档,如果业务表有几十个字段、上万行数据,这段 XML 的长度会轻松超过VARCHAR2的 4000 字节上限。我一般会明确建议:任何游标合并的场景,都不要用VARCHAR2去接序列化结果,直接上DBMS_LOB.CREATETEMPORARY创建临时 CLOB,再用DBMS_LOB.WRITEAPPEND和DBMS_LOB.APPEND往里写内容。第二个是XMLTYPE,它有从游标构造文档的能力,也有EXTRACT方法按 XPath 提取片段,还有GETCLOBVAL方法把 XML 转成 CLOB。这两个包配合起来,就完成了"游标 → XML → CLOB → 合并"的完整链路。下面是官方文档的相关地址,你可以收藏备查:

需要掌握的包官方文档用途
DBMS_LOBdocs.oracle.com/cd/E11882_01/timesten.112/e21645/d_lob.htmCLOB 创建、追加、读写
XMLTYPEdocs.oracle.com/cd/B19306_01/appdev.102/b14258/t_xml.htmXML 类型构造、XPath 提取、序列化
XMLTABLEdocs.oracle.com/cd/B19306_01/server.102/b14200/functions228.htmXML 反序列化为关系型结果集

我在实际开发中,遇到游标列数不确定、又要循环拼接的场景,基本都直接走这套组合,不再考虑临时表方案。

3. 完整实现:XML 序列化、节点提取与 CLOB 拼接的 PL/SQL 实战

3.1 第一步:单个游标序列化与行节点提取

合并的前提是能拿到单个游标的行数据。看这段基础代码,它演示了如何把游标变成 XML,再只提取<ROW>节点:

DECLARE x xmltype; rowxml clob; ref_cur SYS_REFCURSOR; BEGIN -- 打开一个动态游标,查询用户表 OPEN ref_cur FOR SELECT F_USERNAME, F_USERCODE, F_USERID FROM Tb_System_User WHERE F_USERID = 1; -- 关键步骤:把游标直接作为 XMLTYPE 构造参数 x := xmltype(ref_cur); -- 打印完整 XML 结构,观察序列化后的格式 DBMS_OUTPUT.PUT_LINE('=====完整的REFCURSOR结构====='); DBMS_OUTPUT.PUT_LINE(x.getClobVal()); -- 只提取 ROW 节点部分,用于后续合并 rowxml := x.extract('/ROWSET/ROW').getClobVal(0, 0); DBMS_OUTPUT.PUT_LINE('=====只提取行信息====='); DBMS_OUTPUT.PUT_LINE(rowxml); END; /

这段代码的输出结果是这样的:

=====完整的REFCURSOR结构===== <?xml version="1.0"?> <ROWSET> <ROW> <F_USERNAME>系统管理员</F_USERNAME> <F_USERCODE>admin</F_USERCODE> <F_USERID>1</F_USERID> </ROW> </ROWSET> =====只提取行信息===== <ROW> <F_USERNAME>系统管理员</F_USERNAME> <F_USERCODE>admin</F_USERCODE> <F_USERID>1</F_USERID> </ROW>

这里需要注意三个要点。第一,xmltype(ref_cur)的构造函数是 Oracle 内置支持的,不需要额外安装任何组件,它会把游标当前的结果集完整序列化为 XML。第二,x.extract('/ROWSET/ROW')用的是标准 XPath 语法,从文档根节点开始找ROWSET下的所有ROW节点,返回的是一个XMLTYPE片段。第三,getClobVal(0, 0)的两个参数是offset和length,传0, 0表示返回整个 CLOB 内容,我当时第一次用的时候传了别的值,结果只拿到了一部分 XML,排查了半天。提取行节点而不是提取整个文档,是为了最后拼回<ROWSET>时有控制权——你希望合并后的文档只有一个根节点,而不是一堆独立文档堆在一起。

3.2 第二步:对每个游标执行"提取 + 追加"合并操作

现在进入正式合并流程。假设业务上有两个游标,分别查不同用户,你需要把它们的结果合并成一个 XML 文档,最终能通过一个游标返回。这是完整的可执行代码:

DECLARE x xmltype; rowxml clob; mergeXml clob; ref_cur SYS_REFCURSOR; ref_cur2 SYS_REFCURSOR; ref_cur3 SYS_REFCURSOR; BEGIN -- 创建临时 CLOB,用于存放合并后的 XML 文档 DBMS_LOB.CREATETEMPORARY(mergeXml, TRUE); -- 写入根节点开标签 DBMS_LOB.WRITEAPPEND(mergeXml, 8, '<ROWSET>'); -- ---------- 第一个游标 ---------- OPEN ref_cur FOR SELECT F_USERNAME, F_USERCODE, F_USERID FROM Tb_System_User WHERE F_USERID = 1; x := xmltype(ref_cur); DBMS_OUTPUT.PUT_LINE('=====完整的REFCURSOR结构====='); DBMS_OUTPUT.PUT_LINE(x.getClobVal()); rowxml := x.extract('/ROWSET/ROW').getClobVal(0, 0); DBMS_OUTPUT.PUT_LINE('=====只提取行信息====='); DBMS_OUTPUT.PUT_LINE(rowxml); -- 把行节点追加到合并 CLOB 中 DBMS_LOB.APPEND(mergeXml, rowxml); -- ---------- 第二个游标 ---------- OPEN ref_cur2 FOR SELECT F_USERNAME, F_USERCODE, F_USERID FROM Tb_System_User WHERE F_USERID = 1000; x := xmltype(ref_cur2); rowxml := x.extract('/ROWSET/ROW').getClobVal(0, 0); DBMS_LOB.APPEND(mergeXml, rowxml); -- 写入根节点结束标签,形成完整 XML 文档 DBMS_LOB.WRITEAPPEND(mergeXml, 9, '</ROWSET>'); -- 观察合并结果 DBMS_OUTPUT.PUT_LINE('=====合并后的信息====='); DBMS_OUTPUT.PUT_LINE(mergeXml); -- 关闭游标,释放资源 CLOSE ref_cur; CLOSE ref_cur2; END; /

这段代码的核心逻辑分四层。第一层,DBMS_LOB.CREATETEMPORARY(mergeXml, TRUE)在临时表空间创建一个 CLOB,第二个参数TRUE表示这个临时 LOB 会在会话结束时自动释放,不需要手动FREE;如果你传FALSE,就必须要自己调用DBMS_LOB.FREETEMPORARY,否则会话内存会一直占着。第二层,DBMS_LOB.WRITEAPPEND(mergeXml, 8, '<ROWSET>')的第一个参数是目标 CLOB,第二个参数8是写入字符串的字节数,第三个参数是内容——这里的 8 是<ROWSET>的字符个数,结尾的</ROWSET>是 9 个字符,这两个数字千万不能写错,写多了会带入多余的字符,写少了会把字符串截断。第三层,DBMS_LOB.APPEND(mergeXml, rowxml)是纯粹的 CLOB 追加,把每个游标提取出来的<ROW>节点依次接到根节点内部。第四层,循环处理多个游标时,只需要把OPEN ref_cur FOR和xmltype(ref_cur)这两行放到LOOP结构里,每个游标都执行同样的"提取行节点 → 追加到 mergeXml"操作,最后再统一写闭合标签。

执行后的输出验证了方案的可行性:

=====合并后的信息===== <ROWSET> <ROW> <F_USERNAME>系统管理员</F_USERNAME> <F_USERCODE>admin</F_USERCODE> <F_USERID>1</F_USERID> </ROW> <ROW> <F_USERNAME>黄燕</F_USERNAME> <F_USERCODE>HUANGYAN</F_USERCODE> <F_USERID>1000</F_USERID> </ROW> </ROWSET>

到这一步,两个游标的数据已经在物理上拼到一起了,而且列结构完全不同也能兼容——XML 不在乎你的列对不对得上。但注意,现在mergeXml还只是一个 CLOB,调用方需要的是游标,所以还差最后一步。

3.3 第三步:用 XMLTABLE 把合并后的 CLOB 解析回游标

合并完 XML 只是中场休息,真正的考验是把这段 XML 重新变成调用方能用的sys_refcursor。Oracle 的XMLTABLE函数就是干这个的,它能把 XML 文档按 XPath 拆成关系型行集。接续上面代码:

DECLARE mergeXml clob; ref_cur3 SYS_REFCURSOR; BEGIN -- 假设 mergeXml 已经包含合并后的 XML 文档 -- 通过 XMLTABLE 把 ROW 节点解析为虚拟表,再返回为游标 OPEN ref_cur3 FOR SELECT * FROM xmltable( '/ROWSET/ROW' PASSING xmltype(mergeXml) COLUMNS F_USERNAME varchar2(100) PATH 'F_USERNAME', F_USERCODE varchar2(100) PATH 'F_USERCODE' ); -- 此时 ref_cur3 就可以作为存储过程的返回参数传给调用方了 END; /

XMLTABLE的语法有三个动词需要理解。第一个是第一个参数'/ROWSET/ROW',这是 XPath,告诉 OracleROW节点藏在文档的哪个位置,从根找就是/ROWSET/ROW;如果你想处理嵌套结构,比如找每行里的子表数据,那就要写相对路径,比如/ROWSET/ROW/ITEMS/ITEM。第二个是PASSING xmltype(mergeXml),它把 CLOB 内容实例化为一个XMLTYPE对象,这里要注意,如果mergeXml不是合法的 XML 文档,这一步会直接报ORA-31011: XML parsing failed。第三个是COLUMNS子句,它定义返回结果集的列名、类型和数据来源路径,PATH 'F_USERNAME'表示取当前<ROW>节点下<F_USERNAME>子节点的文本值;如果不写PATH,Oracle 默认用列名作为节点名去匹配。

一个容易忽视的地方:XMLTABLE返回的是虚表,OPEN ref_cur3 FOR SELECT * FROM xmltable(...)是把它包成一个动态游标的标准写法。这样一来,ref_cur3就完全等价于一个"把多个游标内容 UNION ALL 之后"的结果集,调用方拿到的就是一个干净的游标,完全不知道底层经历了 XML 序列化和反向解析。我在真实项目中,就是把这个三段式逻辑封装成一个函数,输入是一组游标,输出是合并后的新游标,业务代码里一行调用就搞定。

3.4 参数与选型对照:关键点的取舍

写这种代码时,参数选型直接决定调试体验。我把几个关键决策点整理成表,方便你对照自己的场景:

决策点推荐做法不推荐做法理由
存储中间 XML 的变量类型CLOB+DBMS_LOB操作VARCHAR2直接拼接XML 长度易超 4000,VARCHAR2 会报ORA-06502
提取行节点的方式.extract('/ROWSET/ROW')后取getClobVal(0,0)直接getClobVal()全量再截取全量拿的是完整文档,拼到目标 CLOB 里会重复根标签
合并多个游标的循环写法FOR循环逐个OPEN+extract+APPEND手写 N 个游标的重复代码游标数量变化时,循环只改数组定义
XMLTABLE的列传参按业务需要的列显式声明SELECT *不声明列游标是动态的,不声明列多,取数方反而不知道列名
临时 CLOB 生命周期会话结束自动释放(TRUE)手动FREETEMPORARY释放放在事务里手动释放容易漏掉,导致临时段膨胀

这套参数的取舍逻辑,本质是"把动态问题转化成静态问题"。你用XMLTABLE显式声明列,看起来是写死了结构,但每个游标传进来的时候列名和值都可以不同,解析时只要节点名对得上就行;这比临时表 DDL 的"写死列结构且无法变更"要灵活得多。

4. 避坑指南:游标合并中最容易翻车的五个细节

4.1 现象:ORA-06502: PL/SQL: numeric or value error上百行 XML 拼接时报错

现象:两个游标各自只有几十行数据,你以为 Varchar2 够用,直接用VARCHAR2变量接收getClobVal()的结果,结果一执行就报ORA-06502,提示字符缓冲区太小。
原因:序列化后的 XML 有文档头、根标签、所有行节点,列多行多时长度轻松破万,Varchar2 上限是 4000 字节,而且DBMS_OUTPUT.PUT_LINE对超长 CLOB 也有显示截断。
解决:所有中间变量统一用CLOB,拼接走DBMS_LOB.APPEND。我现在的习惯是:只要代码里出现xmltype(...).getClobVal(),立刻建一个CLOB变量去接,绝不用 Varchar2。DBMS_OUTPUT打日志时,如果需要看完整内容,用DBMS_LOB.SUBSTR(mergeXml, 2000, 1)分段打印。

4.2 现象:合并后的 XML 文档出现两个<ROWSET>根标签

现象:把每个游标的x.getClobVal()直接拼到目标 CLOB 里,最后得到的内容是<ROWSET>...</ROWSET><ROWSET>...</ROWSET>,用XMLTABLE解析时报ORA-00932或ORA-31011。
原因:getClobVal()返回的是完整 XML 文档,包含<ROWSET>开闭标签;拼接到目标 CLOB 时,等于把多个完整文档拼在了一起,形成了多根节点文档,这不符合 XML 规范。
解决:只用.extract('/ROWSET/ROW')提取行节点,绝不用完整文档参与拼接。我自己在开发中,会把提取行节点封装成一个私有函数,保证所有游标合并走同一道工序。

4.3 现象:DBMS_LOB.WRITEAPPEND后字符串多出来几个字符

现象:写入<ROWSET>后,打印合并结果发现变成了<ROWSETXYZ,后面跟着莫名奇妙的字符。
原因:WRITEAPPEND的第二个参数是字节数,不是字符数。<ROWSET>是 8 个字符,但你如果写成9,它会顺带把</ROWSET>的前几个字符也写入,或者把空字符填进去;中文字符一个占 3 字节(UTF-8),如果第三个参数里混了中文,字节数更难算。
解决:不要手动数长度,用LENGTH()函数动态计算:DBMS_LOB.WRITEAPPEND(mergeXml, LENGTH('<ROWSET>'), '<ROWSET>')。这是我踩过最冤枉的坑,数错一次,整个 XML 结构就变形了。

4.4 现象:XMLTABLE解析时报ORA-19279: XPTY0004 - invalid token

现象:合并的 XML 里有列名相同但值不同,甚至列值包含特殊字符(比如<、>、&),解析时报类型错误或 token 错误。
原因:原始数据里的特殊字符没有进行 XML 转义。游标序列化为 XML 时,Oracle 会自动把数据里的<转成&lt;,但如果你手动拼 CLOB,可能破坏了这种转义关系,导致 XML 结构不合法。
解决:不要手动拼接 CLOB 里的数据节点,尽量保持"xmltype(游标)→extract→APPEND"的完整链路,让 Oracle 来处理转义。如果你确实需要手动构造 XML 片段,记得用XMLTYPE的构造函数来包装。

4.5 现象:循环合并大量游标时临时表空间膨胀

现象:在循环里合并几百个游标,会话的临时表空间占用快速上涨,数据库告警日志出现临时表空间不足。
原因:每个xmltype(ref_cur)都会在内存中构造完整 XML,CLOB临时段也会随着APPEND不断增长;循环结束后没有及时释放游标和临时 LOB,导致临时段回收不及时。
解决:每个游标用完后立即CLOSE;每个 CLOB 变量在不再使用时调用DBMS_LOB.FREETEMPORARY;如果游标数量极大,考虑分批合并,每合并 50 个就落一次临时表,避免单个 CLOB 无限膨胀。这个坑在数据量小的时候完全看不出来,一旦上了生产环境就会被临时表空间告警打醒。

5. 进阶技巧:把合并逻辑封装成通用工具函数,一次定义到处复用

如果你只需要合并两三个游标,写上面的匿名块就够了。但真实业务里,合并游标的逻辑会被多个存储过程反复调用,每次都复制那一大段DBMS_LOB代码不现实。我通常的做法是,把它封装成一个独立的存储过程或函数,输入一个SYS_REFCURSOR集合(可以用TABLE OF SYS_REFCURSOR作为集合类型),输出一个合并后的SYS_REFCURSOR。核心逻辑是:循环遍历游标数组,对每个游标执行"提取行节点并追加到 CLOB",最后统一用XMLTABLE返回。

在封装时,有两点是需要特别留意的。第一,游标数组参数要定义成TABLE OF SYS_REFCURSOR,但存储过程的参数类型不能直接使用这个集合类型,必须先创建独立的 TYPE 或者使用包内定义的类型。第二,合并后的列结构取决于最后XMLTABLE中COLUMNS声明的列,因为游标是动态的,设计函数时就要约定好业务上游标必须包含哪些公共列,比如F_USERNAME、F_USERCODE,这样下游取数才稳定。以"每次返回的游标列可能都不相同"这个前提来看,函数设计上通常要约定一个最小公共列集合,比如F_USERNAME、F_USERCODE、F_USERID,取数方只消费这几个字段。

验证合并结果是否正确,我一直用这个方法:写一个测试脚本,先分别打印每个游标的getClobVal(),再打印合并后的mergeXml,最后FETCH合并游标的前十行,检查行数是否等于两个源游标行数之和,列值是否对得上。这个验证能同时发现"行节点漏提取"和"列映射错位"两类问题。我一般还会特意用一个列名带下划线的表做测试,因为<F_USERNAME>节点名和列名一致时,XMLTABLE的PATH可以省略,但一旦列名是USERNAME,和节点名对不上,省略就会拿不到数据。从那以后,我每次写XMLTABLE的都强制写全PATH子句,不省这个事,再小的列映射也显式声明,因为动态游标场景下,"默认按列名匹配"这个隐含约定是最容易翻车的地方。这套方案的核心价值在于,给"动态游标无法拼接"提供了一个不依赖表结构的通用解,你在自己的项目里,把源游标替换成实际业务查询,再把COLUMNS改成业务需要的列,就能直接跑通。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询