ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Oracle Ref Cursor 使用详解:如何准确获取返回记录数

Oracle Ref Cursor 使用详解:如何准确获取返回记录数 1. Oracle Ref Cursor 返回记录数到底卡在哪先说结论Oracle 的 Ref Cursor 是一种动态游标它把「查询语句」和「结果集」解耦让存储过程可以把一个结果集像参数一样传给调用方。它最典型的用法是存储过程返回一个SYS_REFCURSORJava 或 C# 端拿去做流式读取。问题就出在这里——游标是「流式」的打开的那一刻 Oracle 并没有把全部行都算出来所以%ROWCOUNT在 FETCH 之前永远是 0FETCH 之后也只是「到目前为止取了多少行」而不是「总共有多少行」。这个特性让很多刚接触 PL/SQL 的人踩坑。你写了一个OPEN cur FOR SELECT ...想立刻知道这次查询会返回多少条记录结果发现cur%ROWCOUNT是 0SELECT COUNT(*)又得再查一遍。更麻烦的是如果结果集很大循环两次游标的做法会带来双倍 I/O在报表类场景里性能直接崩掉。我试过在一个客户对账的存储过程里原本用「先 COUNT 再 OPEN」的写法结果两张表都是千万级COUNT 那一步就跑了 40 多秒。后来改成COUNT(1) OVER()把总数塞进每一行一次扫描搞定整体耗时降到 6 秒左右。这就是本篇要讲清楚的核心Ref Cursor 本身不提供「总行数」这个元信息你得用别的手段把它带出来。适合谁看正在写 Oracle 存储过程、需要把结果集返回给上层应用、又想在返回前或返回时知道总记录数的开发者。下面我会给出可复制的声明、打开、FETCH 计数、关闭的完整片段并说明在 SQL*Plus 和 PL/SQL Developer 里怎么验证结果。2. TaoToken 前置准备把模型接进你的 PL/SQL 排障流程写 Oracle 存储过程时遇到ORA-01001: invalid cursor、ORA-06511: cursor already open这类报错光靠猜很费时间。我的做法是本地挂一个能对话的模型把报错原文和上下文贴进去让它帮我定位是游标没关还是 FETCH 越界。这里用 TaoToken 来做接入它提供 OpenAI 兼容的接口配置简单适合放在开发机上当排障助手。你需要准备三样东西Base URL、API Key、Model ID。Base URL 用https://taotoken.net/api注意这个地址不带任何查询参数。API Key 在控制台的 API Keys 页面生成路径是https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content。Model ID 根据你选的模型填比如对话场景常用的通用模型标识。如果你更习惯在命令行里干活TaoToken 也支持 Coding Plan适合长期做代码补全和 Agent 任务入口在https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content。想先试试模型对话效果可以直接开https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content把一段 Ref Cursor 代码丢进去问「这段为什么 %ROWCOUNT 拿不到总数」通常能给出可用的排查方向。接入文档在https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content里面有各语言的调用示例。API 基础地址统一用https://taotoken.net/api不要加 UTM 参数避免签名校验失败。下面第三节我会给出具体的配置文件片段你可以直接复制到自己的工具里。3. 可复制配置Ref Cursor 声明、打开、计数、关闭全流程这一节是重点我把 Ref Cursor 的完整生命周期拆开写每一段都能直接跑。先看类型声明和变量定义DECLARE TYPE refcursor IS REF CURSOR; -- 定义 Ref Cursor 类型 cur_info refcursor; -- 游标变量 v_row bi_customer%ROWTYPE; -- 行类型跟表结构一致 v_count PLS_INTEGER : 0; -- 手动计数器 v_total PLS_INTEGER : 0; -- 总数来自窗口函数 BEGIN -- 打开游标注意这里把总数用 COUNT(1) OVER() 带出来 OPEN cur_info FOR SELECT bi.*, COUNT(1) OVER () AS total_rows FROM bi_customer bi WHERE bi.status A; LOOP FETCH cur_info INTO v_row; EXIT WHEN cur_info%NOTFOUND; v_count : v_count 1; v_total : v_row.total_rows; -- 每一行都带同一个总数 DBMS_OUTPUT.PUT_LINE( 第 || v_count || / || v_total || 行客户编号 || v_row.customercode || 地址 || v_row.address ); END LOOP; DBMS_OUTPUT.PUT_LINE(实际 FETCH 行数 || v_count); CLOSE cur_info; END; /关键点在于COUNT(1) OVER ()这一列。它是分析函数不带 PARTITION BY所以对整个结果集求总数并且把总数附加到每一行上。这样你 FETCH 第一行的时候就已经知道总共有多少条了不需要二次扫描。注意v_row的类型要能容纳多出来的total_rows列如果bi_customer%ROWTYPE装不下就改用显式字段列表或者定义一个记录类型。如果你用的是存储过程返回SYS_REFCURSOR给外部程序写法略有不同CREATE OR REPLACE PROCEDURE get_customer_list( p_status IN VARCHAR2, p_cur OUT SYS_REFCURSOR ) AS BEGIN OPEN p_cur FOR SELECT bi.*, COUNT(1) OVER () AS total_rows FROM bi_customer bi WHERE bi.status p_status; END; /调用方拿到游标后第一行就能读到total_rows用来做分页展示或者进度条。这里要提醒一句COUNT(1) OVER ()会让 Oracle 在返回第一行之前先扫完整个结果集所以它适合「结果集不算特别大、但你想一次拿到总数」的场景。如果结果集是百万级且你只想流式处理那就老老实实用手动计数器处理完再报总数。再给一个 JSON 配置片段用于把 TaoToken 接进你的编辑器或 CLI 工具路径按你本地实际位置放{ provider: taotoken, base_url: https://taotoken.net/api, api_key: sk-你的Key, model_id: 你的模型ID, timeout: 60 }如果你用的是 TOML 风格的配置等价写法[provider.taotoken] base_url https://taotoken.net/api api_key sk-你的Key model_id 你的模型ID timeout 60三件套记牢Base URL 是https://taotoken.net/apiKey 从控制台拿Model ID 按你选的模型填。缺任何一个都会报 401 或者 model not found。4. 验证请求与成功结果在 SQL*Plus 和 PL/SQL Developer 里对照写完代码必须验证不然你不知道total_rows到底对不对。先看 SQL*Plus 的步骤。登录后先开输出SET SERVEROUTPUT ON SIZE UNLIMITED; SET LINESIZE 200;然后把第三节的匿名块贴进去执行。如果bi_customer表里statusA的记录有 5 条你应该看到类似输出第 1/5 行客户编号C001地址北京市朝阳区 第 2/5 行客户编号C002地址上海市浦东新区 第 3/5 行客户编号C003地址广州市天河区 第 4/5 行客户编号C004地址深圳市南山区 第 5/5 行客户编号C005地址杭州市西湖区 实际 FETCH 行数5注意最后一行「实际 FETCH 行数」和每行里的分母5必须一致。如果不一致说明COUNT(1) OVER ()的过滤条件和 FETCH 的过滤条件对不上常见原因是 WHERE 子句里用了绑定变量但打开游标时没传对。在 PL/SQL Developer 里验证更直观新建一个 Test Window把匿名块粘进去按 F8 执行。下方 DBMS Output 面板会打印结果。如果你想单独看游标返回的列可以先用OPEN ... FOR打开然后在 Test Window 里用「Cursor」标签页查看但注意这种方式下total_rows列也会显示出来方便你核对。再给一个纯 SQL 的对照验证确认COUNT(1) OVER ()的值和COUNT(*)一致SELECT COUNT(*) AS real_total FROM bi_customer WHERE status A; SELECT DISTINCT total_rows FROM ( SELECT COUNT(1) OVER () AS total_rows FROM bi_customer WHERE status A );两个查询的结果必须相同。如果不同检查是不是有 NULL 值影响了 COUNT 的语义或者 WHERE 条件里用了函数导致索引失效但结果集变化。对于存储过程返回SYS_REFCURSOR的场景可以在匿名块里接收并 FETCH 验证DECLARE v_cur SYS_REFCURSOR; v_row bi_customer%ROWTYPE; v_total PLS_INTEGER; BEGIN get_customer_list(A, v_cur); LOOP FETCH v_cur INTO v_row; EXIT WHEN v_cur%NOTFOUND; v_total : v_row.total_rows; DBMS_OUTPUT.PUT_LINE(总数 || v_total || 客户 || v_row.customercode); END LOOP; CLOSE v_cur; END; /跑通后你会看到每一行都打印相同的总数这就是我们要的效果。5. 本篇常见错排查401、invalid cursor、%ROWCOUNT 为 0排障部分我按真实报错来写你对号入座。ORA-01001: invalid cursor。这个通常发生在游标已经关闭后又 FETCH或者游标变量没 OPEN 就 FETCH。检查你的CLOSE cur_info是不是放在了 LOOP 里面或者异常处理里重复关闭。正确做法是 CLOSE 只出现一次放在 LOOP 之后。ORA-06511: cursor already open。同一个游标变量被 OPEN 了两次。Ref Cursor 变量在 OPEN 之前如果已经是打开状态必须先 CLOSE。可以在 OPEN 前加判断但更稳妥的是保证逻辑上只 OPEN 一次。%ROWCOUNT 为 0。这是本篇的核心痛点。cur%ROWCOUNT在 OPEN 之后、第一次 FETCH 之前就是 0这是设计如此不是 bug。想要总数就用COUNT(1) OVER ()带出来或者 FETCH 完再读%ROWCOUNT。注意%ROWCOUNT在 FETCH 之后表示「已取行数」循环结束后它等于总行数但那时候你已经处理完数据了对分页展示没帮助。401 Unauthorized。如果你在调用 TaoToken 接口时遇到 401先检查 API Key 是否复制完整有没有多余空格。Base URL 必须是https://taotoken.net/api不要写成带 UTM 的地址也不要漏掉/api。Model ID 填错会报 model not found去控制台确认一下模型标识。local proxy failed。这个报错一般出现在本地网络配置层面检查你的工具是否设置了额外的网络转发规则。TaoToken 的接口是标准 HTTPS不需要额外配置。如果公司网络有出口限制联系网络管理员放行taotoken.net域名。reading choices 相关报错。这类错误通常出现在解析模型返回的 JSON 时choices字段为空或者结构不对。检查你的请求体里model参数是否和 Model ID 一致messages数组是否为空。空 messages 会导致部分模型直接返回错误。OAuth 相关报错。如果你用的是需要 OAuth 的工具确认 token 没过期。TaoToken 的 API Key 方式不需要 OAuth直接用 Key 即可。如果工具强制走 OAuth检查回调地址配置。CC Switch / Cline MCP / Codex auth.json 三件套。如果你在这些工具里接入必须同时配好 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 按实际填。CC Switch 里切换配置时注意别把 Base URL 和 Key 配串了。最后提醒一个 PL/SQL 层面的坑COUNT(1) OVER ()在结果集为空时不会返回任何行所以你的v_total会保持初始值 0。这是符合预期的空结果集总数就是 0。但如果你在 FETCH 循环外直接读v_total记得给它一个默认值。6. 把 Ref Cursor 计数接进你的日常开发流到这里Ref Cursor 的声明、打开、FETCH 计数、关闭以及COUNT(1) OVER ()带出总数的做法都讲完了。核心就一句话Ref Cursor 不直接给你总行数你得自己带。要么循环两次不推荐I/O 翻倍要么用分析函数把总数附加到每一行推荐一次扫描。日常开发里我建议把「排障助手」和「代码生成」分开用。排障时用模型对话把报错和代码贴进去快速定位写新存储过程时用 Coding Plan 做补全和 Agent 任务减少重复劳动。模型对话入口在https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentCoding Plan 在https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentAPI Key 管理在https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content接入文档在https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content。最后一个实用技巧如果你的结果集确实很大又必须知道总数可以先用COUNT(*)查一次总数存到变量再 OPEN 游标流式处理。这样虽然多一次查询但 COUNT 走索引的话很快而且不会把整个结果集物化到内存里。具体选哪种看你的数据量和响应时间要求。
返回列表