尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

在Oracle存储过程中实现分页:TaoToken辅助排查分页SQL性能问题

在Oracle存储过程中实现分页:TaoToken辅助排查分页SQL性能问题 1. Oracle 存储过程分页到底难在哪从 ROWNUM 到 OFFSET FETCH 的慢 SQL 现场Oracle 存储过程分页说白了就是「在数据库里把一张大表按页切出来只返回当前页需要的几十条」。听起来简单真正落到生产环境问题往往出在三件事上写法选错、排序字段没走索引、以及分页 SQL 拼错之后报错信息看不懂。这篇内容适合正在写 Oracle 存储过程、被分页查询拖慢响应、或者想搞清楚 ROWNUM、ROW_NUMBER、OFFSET FETCH 三种写法差异的后端和 DBA 同学。我先把三种主流写法摆出来你心里有个谱ROWNUM 双层嵌套是最经典的写法兼容性最好Oracle 8i 以上都能跑。它的逻辑是先用子查询把数据查出来并排序外层用ROWNUM endRecord截断再套一层用r startRecord取区间。注意这里有个坑ROWNUM 是在结果集生成过程中逐行分配的所以必须嵌套两层否则ROWNUM 1永远为空。ROW_NUMBER() 分析函数写法更直观ROW_NUMBER() OVER (ORDER BY col) AS rn先给每行编号外层再按 rn 过滤。可读性好但排序开销一样跑不掉而且如果 ORDER BY 字段没有索引全表排序照样慢。OFFSET FETCH 是 Oracle 12c 之后才有的语法OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY写起来最像 MySQL 的 LIMIT。但要注意它在深分页场景下并不会自动变快本质还是扫描并丢弃前 N 行。真正让分页变慢的通常不是分页语法本身而是「排序字段无索引 深分页 回表」。比如一张 500 万行的订单表按create_time倒序取第 10000 页每页 20 条数据库需要先排序 500 万行再丢弃前 20 万行最后返回 20 条。这个代价是实打实的。我在实际排查时习惯先把存储过程里拼出来的 SQL 单独拿出来用EXPLAIN PLAN看执行计划确认是全表扫描还是索引范围扫描。但问题在于很多团队的存储过程是动态拼 SQL参数一多你根本不知道线上那次慢查询到底拼成了什么样。这时候就需要一个统一的通道把「调用存储过程」和「观察返回结果/报错」这两件事串起来TaoToken 在这里的作用就是提供统一的 Key 和 API 通道让你在验证分页结果一致性、复现报错时不用来回切换工具和账号。下面我会先给一套可复制的分页存储过程模板再讲怎么用执行计划对比三种写法最后讲怎么通过 TaoToken 的接口通道去验证分页结果和定位报错。每一步都有完整命令和参数你可以直接跟做。2. TaoToken 前置准备统一 Key 与 API 通道为分页排查铺路在开始写存储过程之前先把 TaoToken 这条通道准备好。它的定位不是替代你的数据库客户端而是给你一个统一的入口当你需要调用模型对话来帮你分析执行计划、或者用 Coding Plan 跑一段脚本去批量验证分页结果时不用每个工具单独配一套 Key。你需要准备三样东西Base URL、API Key、Model ID。这三件套在后面的配置片段里会反复出现先记牢。Base URL 用https://taotoken.net/api注意这个地址不带任何查询参数。API Key 需要你去控制台创建入口在https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite。创建的时候建议按用途命名比如oracle-page-debug方便后面区分。Model ID 根据你的场景选。如果你只是想让模型帮你读执行计划、解释报错用模型对话通道就行入口在https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite。如果你要长期跑分页验证脚本、做 Agent 类的自动化排查那更适合 Coding Plan入口在https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite。如果你用的是 Claude Code 这类工具需要走 Anthropic 兼容通道配置入口在https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaude_codeutm_campaignrewrite。接入文档在https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite里面有各语言的调用示例。这里给一个通用的配置片段你可以直接复制到你的环境变量或者配置文件里。以 JSON 格式为例{ base_url: https://taotoken.net/api, api_key: sk-你的Key, model_id: 你的模型ID, timeout: 60 }如果你用的是 Codex 的auth.json结构类似把base_url和api_key填进去即可。注意base_url后面不要加/v1之类的后缀保持https://taotoken.net/api原样。为什么要先做这一步因为分页排查的痛点在于「复现」。线上报了一个ORA-00933: SQL command not properly ended你光看日志不知道拼出来的 SQL 长什么样。这时候你可以把存储过程的入参记录下来通过 TaoToken 的通道调用模型让它根据你的模板和参数反推可能的 SQL 拼接结果再对照执行计划。这比你在 PL/SQL Developer 里手动拼参数快得多。另外验证分页结果一致性时你可能需要对比「存储过程返回的第 N 页」和「直接查 SQL 的第 N 页」是否一致。这个对比脚本可以用 Coding Plan 跑把两次结果做 diff。统一 Key 的好处是你不用在多个工具之间复制粘贴 Key也不会因为某个工具的额度用完而中断排查。准备好这三件套之后我们进入正题写分页存储过程。3. 可复制的分页存储过程模板ROWNUM、ROW_NUMBER、OFFSET FETCH 三版对照这一节给你三版存储过程模板都是可以直接复制执行的。我会把包定义、存储过程主体、以及调用方式写全。你先建包再建过程最后测试。3.1 包定义三版共用同一个包定义游标类型CREATE OR REPLACE PACKAGE page_query AS TYPE cur_query IS REF CURSOR; END page_query; /3.2 ROWNUM 双层嵌套版这是兼容性最好的写法适合 Oracle 11g 及以下CREATE OR REPLACE PROCEDURE pro_query_rownum ( p_tableName IN VARCHAR2, p_strWhere IN VARCHAR2, p_orderColumn IN VARCHAR2, p_orderStyle IN VARCHAR2, p_curPage IN OUT NUMBER, p_pageSize IN OUT NUMBER, p_totalRecords OUT NUMBER, p_totalPages OUT NUMBER, v_cur OUT page_query.cur_query ) IS v_sql VARCHAR2(4000) : ; v_countSql VARCHAR2(4000) : ; v_startRecord NUMBER; v_endRecord NUMBER; BEGIN -- 总记录数 v_countSql : SELECT COUNT(*) FROM || p_tableName || WHERE 11 ; IF p_strWhere IS NOT NULL AND p_strWhere THEN v_countSql : v_countSql || p_strWhere; END IF; EXECUTE IMMEDIATE v_countSql INTO p_totalRecords; -- 页大小校验 IF p_pageSize IS NULL OR p_pageSize 0 THEN p_pageSize : 20; END IF; -- 总页数 p_totalPages : CEIL(p_totalRecords / p_pageSize); -- 页码校验 IF p_curPage IS NULL OR p_curPage 1 THEN p_curPage : 1; END IF; IF p_curPage p_totalPages THEN p_curPage : p_totalPages; END IF; -- 计算区间 v_startRecord : (p_curPage - 1) * p_pageSize 1; v_endRecord : p_curPage * p_pageSize; -- 分页 SQL v_sql : SELECT * FROM ( || SELECT A.*, ROWNUM r FROM ( || SELECT * FROM || p_tableName || WHERE 11 ; IF p_strWhere IS NOT NULL AND p_strWhere THEN v_sql : v_sql || p_strWhere; END IF; IF p_orderColumn IS NOT NULL AND p_orderColumn THEN v_sql : v_sql || ORDER BY || p_orderColumn || || p_orderStyle; END IF; v_sql : v_sql || ) A WHERE ROWNUM || v_endRecord || ) B WHERE r || v_startRecord; DBMS_OUTPUT.PUT_LINE(SQL: || v_sql); OPEN v_cur FOR v_sql; END pro_query_rownum; /注意几个细节p_pageSize和p_curPage是IN OUT过程内部会修正非法值。v_sql用VARCHAR2(4000)如果你的表名和条件特别长可以调到 32767。DBMS_OUTPUT.PUT_LINE把拼出来的 SQL 打出来方便你复制到执行计划里分析。3.3 ROW_NUMBER 分析函数版适合 Oracle 11g 及以上可读性更好CREATE OR REPLACE PROCEDURE pro_query_rownumber ( p_tableName IN VARCHAR2, p_strWhere IN VARCHAR2, p_orderColumn IN VARCHAR2, p_orderStyle IN VARCHAR2, p_curPage IN OUT NUMBER, p_pageSize IN OUT NUMBER, p_totalRecords OUT NUMBER, p_totalPages OUT NUMBER, v_cur OUT page_query.cur_query ) IS v_sql VARCHAR2(4000) : ; v_countSql VARCHAR2(4000) : ; v_startRecord NUMBER; v_endRecord NUMBER; BEGIN v_countSql : SELECT COUNT(*) FROM || p_tableName || WHERE 11 ; IF p_strWhere IS NOT NULL AND p_strWhere THEN v_countSql : v_countSql || p_strWhere; END IF; EXECUTE IMMEDIATE v_countSql INTO p_totalRecords; IF p_pageSize IS NULL OR p_pageSize 0 THEN p_pageSize : 20; END IF; p_totalPages : CEIL(p_totalRecords / p_pageSize); IF p_curPage IS NULL OR p_curPage 1 THEN p_curPage : 1; END IF; IF p_curPage p_totalPages THEN p_curPage : p_totalPages; END IF; v_startRecord : (p_curPage - 1) * p_pageSize 1; v_endRecord : p_curPage * p_pageSize; v_sql : SELECT * FROM ( || SELECT A.*, ROW_NUMBER() OVER (ORDER BY || NVL(p_orderColumn, NULL) || || NVL(p_orderStyle, ASC) || ) AS rn FROM || p_tableName || WHERE 11 ; IF p_strWhere IS NOT NULL AND p_strWhere THEN v_sql : v_sql || p_strWhere; END IF; v_sql : v_sql || ) WHERE rn BETWEEN || v_startRecord || AND || v_endRecord; DBMS_OUTPUT.PUT_LINE(SQL: || v_sql); OPEN v_cur FOR v_sql; END pro_query_rownumber; /这里ROW_NUMBER() OVER (ORDER BY ...)必须放在子查询里外层才能用rn过滤。如果你把rn过滤放在同一层Oracle 会报ORA-30483: window functions are not allowed here。3.4 OFFSET FETCH 版Oracle 12c 及以上CREATE OR REPLACE PROCEDURE pro_query_offset ( p_tableName IN VARCHAR2, p_strWhere IN VARCHAR2, p_orderColumn IN VARCHAR2, p_orderStyle IN VARCHAR2, p_curPage IN OUT NUMBER, p_pageSize IN OUT NUMBER, p_totalRecords OUT NUMBER, p_totalPages OUT NUMBER, v_cur OUT page_query.cur_query ) IS v_sql VARCHAR2(4000) : ; v_countSql VARCHAR2(4000) : ; v_offset NUMBER; BEGIN v_countSql : SELECT COUNT(*) FROM || p_tableName || WHERE 11 ; IF p_strWhere IS NOT NULL AND p_strWhere THEN v_countSql : v_countSql || p_strWhere; END IF; EXECUTE IMMEDIATE v_countSql INTO p_totalRecords; IF p_pageSize IS NULL OR p_pageSize 0 THEN p_pageSize : 20; END IF; p_totalPages : CEIL(p_totalRecords / p_pageSize); IF p_curPage IS NULL OR p_curPage 1 THEN p_curPage : 1; END IF; IF p_curPage p_totalPages THEN p_curPage : p_totalPages; END IF; v_offset : (p_curPage - 1) * p_pageSize; v_sql : SELECT * FROM || p_tableName || WHERE 11 ; IF p_strWhere IS NOT NULL AND p_strWhere THEN v_sql : v_sql || p_strWhere; END IF; IF p_orderColumn IS NOT NULL AND p_orderColumn THEN v_sql : v_sql || ORDER BY || p_orderColumn || || p_orderStyle; END IF; v_sql : v_sql || OFFSET || v_offset || ROWS FETCH NEXT || p_pageSize || ROWS ONLY; DBMS_OUTPUT.PUT_LINE(SQL: || v_sql); OPEN v_cur FOR v_sql; END pro_query_offset; /OFFSET FETCH 的坑在于如果ORDER BY字段不唯一分页结果可能不稳定同一页两次查询返回不同行。所以排序字段最好带上主键比如ORDER BY create_time DESC, id DESC。三版建好之后你可以用下面的匿名块测试SET SERVEROUTPUT ON; DECLARE v_cur page_query.cur_query; v_page NUMBER : 1; v_size NUMBER : 10; v_total NUMBER; v_pages NUMBER; v_id NUMBER; v_name VARCHAR2(100); BEGIN pro_query_rownum(YOUR_TABLE, , ID, ASC, v_page, v_size, v_total, v_pages, v_cur); DBMS_OUTPUT.PUT_LINE(total || v_total || , pages || v_pages); LOOP FETCH v_cur INTO v_id, v_name; EXIT WHEN v_cur%NOTFOUND; DBMS_OUTPUT.PUT_LINE(v_id || - || v_name); END LOOP; CLOSE v_cur; END; /把YOUR_TABLE换成你的表名列名按实际调整。跑通之后我们进入执行计划对比。4. 验证请求与成功结果执行计划对比 TaoToken 接口验证分页一致性这一节分两步先用EXPLAIN PLAN对比三种写法的执行计划再用 TaoToken 的接口通道验证分页结果一致性。4.1 执行计划对比假设你有一张ORDERS表CREATE_TIME上有索引数据量 500 万行。分别对三种写法取第 1000 页每页 20 条。ROWNUM 版拼出来的 SQLSELECT * FROM ( SELECT A.*, ROWNUM r FROM ( SELECT * FROM ORDERS WHERE 11 ORDER BY CREATE_TIME DESC ) A WHERE ROWNUM 20000 ) B WHERE r 19981;ROW_NUMBER 版SELECT * FROM ( SELECT A.*, ROW_NUMBER() OVER (ORDER BY CREATE_TIME DESC) AS rn FROM ORDERS WHERE 11 ) WHERE rn BETWEEN 19981 AND 20000;OFFSET FETCH 版SELECT * FROM ORDERS WHERE 11 ORDER BY CREATE_TIME DESC OFFSET 19980 ROWS FETCH NEXT 20 ROWS ONLY;对每条 SQL 执行EXPLAIN PLAN FOR 你的SQL; SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);你会看到类似这样的输出简化| Id | Operation | Name | Rows | | 0 | SELECT STATEMENT | | 20 | |* 1 | VIEW | | 20000 | |* 2 | COUNT STOPKEY | | | | 3 | VIEW | | 20000 | | 4 | TABLE ACCESS BY INDEX| ORDERS | 5000K| | 5 | INDEX FULL SCAN | IDX_CREATE | 5000K|关键看两点COUNT STOPKEY表示 ROWNUM 截断生效INDEX FULL SCAN表示走了索引。如果ORDER BY字段没索引你会看到SORT ORDER BY代价高很多。三种写法在深分页时执行计划差异不大都是「索引扫描 丢弃前 N 行」。真正的优化手段是「延迟关联」先用索引查出主键再回表取数据。这个后面排障部分会讲。4.2 TaoToken 接口验证分页一致性执行计划只能告诉你「快不快」不能告诉你「对不对」。分页结果一致性验证是确认存储过程返回的第 N 页和直接查 SQL 的第 N 页是否完全一致。你可以写一个 Python 脚本通过 TaoToken 的接口通道调用模型让它帮你生成对比逻辑或者直接用 Coding Plan 跑对比脚本。这里给一个用requests调用的示例import requests import json BASE_URL https://taotoken.net/api API_KEY sk-你的Key MODEL_ID 你的模型ID headers { Authorization: fBearer {API_KEY}, Content-Type: application/json } payload { model: MODEL_ID, messages: [ { role: user, content: 我有两个分页查询结果集A 是存储过程返回的B 是直接 SQL 返回的。请帮我写一段 Python 代码对比两个列表是否完全一致如果不一致输出差异行的主键。 } ] } resp requests.post(f{BASE_URL}/chat/completions, headersheaders, jsonpayload, timeout60) print(resp.json()[choices][0][message][content])如果你用的是 Claude Code 的 Anthropic 兼容通道请求体格式略有不同参考接入文档里的示例。核心是三件套Base URL 用https://taotoken.net/apiKey 用你创建的Model ID 按场景选。验证通过的标准是存储过程返回的 20 条记录和直接 SQL 返回的 20 条记录主键集合完全一致顺序也一致。如果顺序不一致检查ORDER BY字段是否唯一。如果记录数不一致检查ROWNUM或rn的边界计算。成功结果示例total5000000, pages250000 page 1000, size 20 存储过程返回主键: [19981, 19982, ..., 20000] 直接SQL返回主键: [19981, 19982, ..., 20000] 一致性校验: PASS到这里分页存储过程模板、执行计划对比、结果一致性验证就串起来了。接下来讲排障。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth 对照这一节把分页排查和 TaoToken 调用过程中最容易遇到的报错列出来对照真实错误信息给解法。5.1 ORA-00933: SQL command not properly ended这是分页 SQL 拼接最常见的错。原因通常是ORDER BY后面多了分号或者OFFSET FETCH用在了 11g 上。检查你的v_sql最后有没有多余字符。如果是 11g改用 ROWNUM 或 ROW_NUMBER 版。5.2 ORA-00904: invalid identifier通常是p_orderColumn传了不存在的列名或者列名带了表别名但子查询里没暴露。比如你传A.CREATE_TIME但子查询里表别名是ORDERS就会报这个错。解法是排序字段只传列名不带别名。5.3 ORA-30483: window functions are not allowed hereROW_NUMBER 版把rn过滤写在了同一层。必须嵌套子查询外层再过滤。5.4 401 UnauthorizedTaoToken 调用返回 401说明 Key 不对或没带。检查Authorization头是不是Bearer sk-xxx格式Key 有没有复制完整。如果 Key 刚创建等几秒再试。控制台入口在https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite。5.5 local proxy failed这个报错通常出现在你本地网络环境有代理设置但请求没走通。检查你的环境变量HTTP_PROXY、HTTPS_PROXY是否指向了不可用的地址。如果你在容器里跑检查容器网络是否能访问外网。解法是清掉代理环境变量或者确认网络策略允许访问taotoken.net。5.6 reading choices 相关报错如果你在解析响应时遇到reading choices或choices is undefined说明返回体不是预期的 OpenAI 格式。先打印完整响应print(resp.status_code) print(resp.text)常见原因是 Model ID 填错或者请求路径少了/chat/completions。Base URL 是https://taotoken.net/api完整路径是https://taotoken.net/api/chat/completions。5.7 OAuth 相关报错如果你用 Claude Code 走 Anthropic 通道遇到 OAuth 报错检查你的配置文件里base_url是不是写成了https://taotoken.net/api而不是带/v1的地址。Anthropic 兼容通道的配置参考https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaude_codeutm_campaignrewrite。5.8 分页结果重复或丢失如果同一页两次查询结果不同检查ORDER BY字段是否唯一。解法是加上主键比如ORDER BY CREATE_TIME DESC, ID DESC。如果记录丢失检查ROWNUM的r startRecord和r endRecord边界startRecord应该是(page-1)*size1不是(page-1)*size。5.9 深分页慢查询第 10000 页慢不是分页写法的问题是「丢弃前 N 行」的代价。解法是延迟关联SELECT * FROM ORDERS O JOIN ( SELECT ID FROM ( SELECT ID, ROWNUM r FROM ( SELECT ID FROM ORDERS WHERE 11 ORDER BY CREATE_TIME DESC ) WHERE ROWNUM 200000 ) WHERE r 199981 ) T ON O.ID T.ID ORDER BY O.CREATE_TIME DESC;先用索引查出主键区间再回表取数据减少回表次数。排障的核心思路是先看报错信息定位是 SQL 拼接问题还是通道调用问题。SQL 问题看执行计划和拼接日志通道问题看状态码和响应体。TaoToken 在这里的价值是给你一个统一的调用入口让你在验证分页结果、复现报错时不用来回切换工具。6. 长期编码与 Agent 排查用 Coding Plan 把分页验证自动化如果你只是偶尔排查一次分页问题上面的步骤够用了。但如果你在团队里长期维护多个存储过程每次改完都要手动验证分页一致性那就值得把这件事自动化。Coding Plan 适合这种场景你写一个脚本输入表名、排序字段、页码范围自动跑三种分页写法对比结果输出差异报告。脚本通过 TaoToken 的通道调用模型让模型帮你分析执行计划里的瓶颈或者根据报错信息给出修复建议。入口在https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite。配置还是那三件套Base URL 用https://taotoken.net/apiKey 用你创建的Model ID 选适合代码场景的。一个实用的自动化流程是这样的每次存储过程变更后CI 里跑一个脚本用EXPLAIN PLAN抓取执行计划通过 TaoToken 让模型判断是否出现SORT ORDER BY或TABLE ACCESS FULL如果有就告警。同时跑分页一致性校验对比存储过程返回和直接 SQL 返回的主键集合。这样你就不用每次改完都手动去 PL/SQL Developer 里点一遍。模型对话通道适合临时问问题Coding Plan 适合把重复排查固化成流程。两者配合分页问题的定位时间能从半小时压缩到几分钟。最后给一个实用技巧把DBMS_OUTPUT.PUT_LINE打出来的 SQL 存到一张日志表里每次分页查询都记录入参和拼出的 SQL。出问题时直接查日志表不用去猜参数。这张日志表加上 TaoToken 的通道基本能覆盖分页排查的绝大多数场景。
返回列表