
1. 共享池老化后执行计划去哪了display_cursor 查不到的真实场景线上慢 SQL 排查最尴尬的一幕往往不是 SQL 本身有多复杂而是你拿到 sql_id 兴冲冲去查执行计划结果数据库冷冷回你一句SQL_ID: c0q6wv27b51yh, child number: 0 cannot be found这句话的含义很明确这个游标的执行计划已经不在共享池Shared Pool里了。共享池是一块会被持续置换的内存区域SQL 执行计划、游标、解析树都放在里面当新 SQL 不断涌入、内存压力上来时老旧的游标就会被 age out 淘汰出去。等你回头想复盘的时候v$sql、v$sqlarea、v$sql_plan这些动态性能视图里早就没有它的踪影了。这时候很多人第一反应是加大共享池但那是治标。真正的问题是事后排查怎么把已经消失的执行计划捞回来答案就在 AWR 里。AWRAutomatic Workload Repository会按快照周期把 SQL 的统计信息和执行计划持久化到DBA_HIST_*系列视图中只要 SQL 在快照间隔内被执行过它的执行计划就有机会被保存下来。而读取这份历史计划的入口就是dbms_xplan.display_awr。这篇内容适合谁日常做 Oracle 性能排查的 DBA、需要事后复盘慢 SQL 的运维同学、以及被“执行计划查不到”卡住过的开发者。核心检索词就是共享池、执行计划、dbms_xplan.display_cursor、dbms_xplan.display_awr和 AWR 这几件事的组合。我会把从定位 sql_id 到捞出历史计划的完整链路拆开每一步都给可复制的脚本并且用对比验证的方式告诉你 display_awr 的输出和 display_cursor 到底差在哪。先建立一个直觉display_cursor是“看现在”display_awr是“看过去”。前者依赖共享池里还留着游标后者依赖 AWR 快照里存过记录。两者不是替代关系而是时间维度上的互补。理解了这一点后面所有操作都会顺理成章。2. TaoToken 前置用统一入口管理排查脚本与模型辅助做 Oracle 排查时我经常需要一边翻文档一边让模型帮我解释某个执行计划里的操作符含义或者把一段复杂的DBA_HIST查询改写成更易读的形式。这时候如果每次都要在多个平台之间切换、反复粘贴 API Key效率会很低。我的做法是把这类辅助能力收敛到一个统一入口TaoToken 就是我在用的那个。它的定位很简单一个兼容主流大模型调用方式的 API 网关你拿到一个 Base URL 和一个 Key就能在脚本、IDE 插件、命令行工具里调用模型。对 DBA 来说最实用的场景是把它接进你已有的排查工具链比如让模型帮你把 AWR 查询结果做二次归纳或者解释DBMS_XPLAN输出里某个TABLE ACCESS FULL背后的代价模型。官网入口在这里https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 这个不加 UTM。注意TaoToken 不是数据库工具它不碰你的 Oracle 实例也不替代 SQL Developer 或 SQL*Plus它只是帮你把“查文档、解释输出、生成脚本”这些周边动作做得更顺。如果你只是想验证某个模型能不能正确解释执行计划可以直接用模型对话页面试https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 。如果你打算长期把模型能力嵌进日常编码和排查流程比如写一个自动解析 AWR 报告的小工具那更适合走 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。需要管理多个 Key、区分不同项目的调用额度就去控制台https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite Key 的创建和轮换在 API Keys 页面https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 。这里要强调一点TaoToken 是合规的 API 接入服务不是任何形式的网络中转工具也不涉及访问受限资源。它的价值在于让你用一套凭证对接多种模型减少在排查过程中被工具切换打断思路的次数。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有各语言 SDK 的调用示例照着改 Base URL 和 Key 就能跑。把这一步做完你就有了一套“数据库排查 模型辅助”的组合。接下来进入正题怎么从 AWR 里把消失的执行计划捞回来。3. 可复制配置display_awr 查询脚本与 sql_id 定位步骤这一节是全文的核心操作区。我会按“先定位 sql_id再捞执行计划最后做对比验证”的顺序给脚本。所有脚本都可以直接在 SQL*Plus 或 SQL Developer 里跑前提是你有访问DBA_HIST_*视图的权限通常需要 DBA 角色或SELECT_CATALOG_ROLE。3.1 第一步确认共享池里确实查不到了先用display_cursor试一次确认执行计划已经不在内存SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(c0q6wv27b51yh, NULL, ALL));如果返回cannot be found说明游标已被置换。这时候不要急着加大共享池先转向 AWR。3.2 第二步从 DBA_HIST 视图定位 sql_idSQL 被置换出内存后v$sql和v$sqlarea都查不到但 AWR 快照里可能还有。三个关键视图的分工是视图作用DBA_HIST_SQLTEXT存 SQL 文本按 sql_id 关联DBA_HIST_SQLSTAT存 SQL 统计信息含 plan_hash_value、执行次数、耗时DBA_HIST_SNAPSHOT存快照时间点用来把 snap_id 翻译成时间如果你已经知道 sql_id直接跳到 3.3。如果只知道 SQL 文本片段可以这样反查SELECT sql_id, sql_text FROM dba_hist_sqltext WHERE sql_text LIKE %你的SQL片段% AND sql_text NOT LIKE %dba_hist_sqltext%;拿到 sql_id 后用DBA_HIST_SQLSTAT确认它在哪些快照里出现过同时把plan_hash_value一起取出来SELECT s.snap_id, s.sql_id, s.plan_hash_value, s.executions_delta, s.elapsed_time_delta / 1000000 AS elapsed_sec, sn.begin_interval_time FROM dba_hist_sqlstat s JOIN dba_hist_snapshot sn ON s.snap_id sn.snap_id AND s.dbid sn.dbid AND s.instance_number sn.instance_number WHERE s.sql_id c0q6wv27b51yh ORDER BY s.snap_id;这一步很关键plan_hash_value是执行计划的指纹。同一个 sql_id 在不同时间段可能对应不同的 plan_hash_value说明执行计划发生过变化。事后排查时你要还原的往往是“出问题那个时间段”的计划而不是最新的那个。3.3 第三步用 display_awr 捞出历史执行计划拿到 sql_id 后最直接的捞取方式SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_AWR(c0q6wv27b51yh));如果这个 sql_id 在 AWR 里有多个 plan_hash_value可以指定具体的一个SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_AWR(c0q6wv27b51yh, 1234567890, NULL, ALL));参数顺序是sql_id, plan_hash_value, dbid, format。plan_hash_value传 NULL 表示显示所有已知计划传具体值则只显示那一个。format用ALL可以看到更多细节包括谓词信息Predicate Information和列投影Column Projection这对判断索引是否被正确使用很有帮助。3.4 第四步把结果落到文件里便于对比排查时我习惯把两次输出都存下来做 diff。在 SQL*Plus 里可以这样SET LINESIZE 200 SET PAGESIZE 1000 SET TRIMSPOOL ON SPOOL plan_awr.txt SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_AWR(c0q6wv27b51yh, NULL, NULL, ALL)); SPOOL OFF同样的方式把display_cursor的输出也存一份两份文件并排看差异一目了然。3.5 关于 settings 配置片段如果你是用脚本或工具批量调用可以把连接信息和查询封装成配置。比如一个简单的 JSON 配置用来描述“针对哪个 sql_id、查哪个快照区间”{ database: ORCL, sql_id: c0q6wv27b51yh, plan_hash_value: null, snap_range: { begin_snap: 1001, end_snap: 1005 }, format: ALL, output: plan_awr.txt }这个结构可以直接喂给你自己写的 Python 脚本用cx_Oracle或oracledb读取后拼 SQL。注意plan_hash_value为 null 时走全量查询指定时走精确查询。4. 验证请求与成功结果对比 display_cursor 与 display_awr 输出光把计划捞出来还不够你得确认捞出来的确实是“当时真实执行的那个计划”而不是 AWR 里存的某个近似版本。这一节讲怎么验证。4.1 先看 display_awr 的典型输出成功执行后你会看到类似这样的结构PLAN_TABLE_OUTPUT ------------------------------------------ SQL_ID c0q6wv27b51yh, plan hash value: 1234567890 ------------------------------------------ | Id | Operation | Name | Rows | ------------------------------------------ | 0 | SELECT STATEMENT | | 1 | | 1 | TABLE ACCESS BY INDEX ROWID | ORDERS | 1 | | 2 | INDEX RANGE SCAN | IDX_ORD_DT | 1 | ------------------------------------------ Note ----- - dynamic sampling used for this statement注意几个关键字段SQL_ID、plan hash value、操作符层级、访问路径INDEX RANGE SCAN 还是 FULL TABLE SCAN、以及底部的 Note。这些和display_cursor的输出格式是一致的因为两者底层都调用DBMS_XPLAN的格式化逻辑区别只在于数据来源。4.2 对比验证的三个动作动作一核对 plan_hash_value。从DBA_HIST_SQLSTAT里查到的 plan_hash_value应该和display_awr输出头部的plan hash value一致。如果不一致说明你查的不是同一个计划需要指定 plan_hash_value 重新查。动作二核对执行次数与时间窗口。用DBA_HIST_SQLSTAT的executions_delta确认这个计划在目标快照区间内确实被执行过。如果executions_delta为 0说明这个计划只是被解析过但没真正跑参考价值有限。动作三和 display_cursor 做结构对比。如果 SQL 现在还在共享池里比如你重新执行了一次可以同时跑display_cursor和display_awr把两份输出并排看。正常情况下如果执行计划没变两者的操作符层级、访问路径、谓词信息应该高度一致。差异通常出现在统计信息相关的行数估算上因为 AWR 存的是快照时刻的估算值而当前游标用的是最新统计信息。4.3 一个真实的对比案例假设某条订单查询 SQL 在上午 10 点突然变慢你从 AWR 里捞出的计划显示走了全表扫描| 1 | TABLE ACCESS FULL | ORDERS | 100K |而当前重新执行后display_cursor显示走了索引| 2 | INDEX RANGE SCAN | IDX_ORD_DT | 1 |这个对比直接说明问题出在上午 10 点那个时间窗口优化器选择了错误的计划可能是统计信息过期或绑定变量窥探导致。你要做的是回到那个时间点检查当时的统计信息和绑定变量值而不是在当前状态下瞎调。4.4 用 SQL 把验证过程自动化可以把上面的核对逻辑写成一个查询一次性把 sql_id、plan_hash_value、执行次数、时间窗口都列出来SELECT s.sql_id, s.plan_hash_value, SUM(s.executions_delta) AS total_exec, MIN(sn.begin_interval_time) AS first_seen, MAX(sn.begin_interval_time) AS last_seen FROM dba_hist_sqlstat s JOIN dba_hist_snapshot sn ON s.snap_id sn.snap_id AND s.dbid sn.dbid WHERE s.sql_id c0q6wv27b51yh GROUP BY s.sql_id, s.plan_hash_value ORDER BY total_exec DESC;这个结果告诉你这个 sql_id 在 AWR 里一共有几个 plan_hash_value每个计划被执行了多少次第一次和最后一次出现是什么时候。有了这张表你就能精准定位到“出问题的那个计划”。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth 等真实报错排查过程中会遇到各种报错有些来自数据库有些来自你用来辅助的模型调用工具。这一节把常见的几类列出来对照解决。5.1 ORA-00942: table or view does not exist执行DBA_HIST_SQLSTAT查询时报这个错说明当前用户没有访问 AWR 视图的权限。解决方式是让 DBA 授予SELECT_CATALOG_ROLE或者单独授权GRANT SELECT ON sys.dba_hist_sqlstat TO your_user; GRANT SELECT ON sys.dba_hist_sqltext TO your_user; GRANT SELECT ON sys.dba_hist_snapshot TO your_user;注意 AWR 视图的 owner 是SYS查询时要带SYS.前缀或者建同义词。5.2 display_awr 返回空结果如果display_awr查出来是空的可能原因有三个一是这个 sql_id 在 AWR 保留期内从未被快照捕获比如 SQL 执行频率太低没达到捕获阈值二是 AWR 快照被清理了默认保留 8 天可通过DBMS_WORKLOAD_REPOSITORY.MODIFY_SNAPSHOT_SETTINGS调整三是你查的 dbid 不对RAC 环境下要指定正确的 dbid。先确认 AWR 里到底有没有这个 sql_idSELECT COUNT(*) FROM dba_hist_sqlstat WHERE sql_id c0q6wv27b51yh;如果返回 0那就是真的没被捕获只能从其他渠道比如 SQL 监控报告、ASH 报告找线索。5.3 模型调用侧的 401 与 local proxy failed如果你在排查脚本里集成了模型调用来解释执行计划可能会遇到401 Unauthorized。这通常是 API Key 没配对或者过期了。检查你的请求头里Authorization: Bearer key是否正确Key 是否在有效期内。在 TaoToken 的 API Keys 页面可以重新生成https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 。local proxy failed这类报错通常出现在本地网络配置有问题时。检查你的 HTTP 代理环境变量http_proxy、https_proxy是否指向了一个不可用的地址把它清掉再试。注意这里说的是本地开发环境的代理配置问题不涉及任何网络访问工具。5.4 reading choices 与响应解析错误调用模型 API 时如果报reading choices相关的解析错误一般是响应体格式和你的解析代码不匹配。比如你按 OpenAI 格式解析choices[0].message.content但实际返回的结构不同。解决办法是先打印原始响应体确认字段路径再改解析逻辑。用curl直接调一次最直观curl -s https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_KEY \ -H Content-Type: application/json \ -d {model:gpt-4o-mini,messages:[{role:user,content:解释一下 INDEX RANGE SCAN}]}5.5 OAuth 与认证配置问题如果你用的是某些 IDE 插件或 CLI 工具可能会走 OAuth 流程。报错通常是因为回调地址不匹配或 token 过期。检查插件配置里的 Base URL 是否指向https://taotoken.net/api以及是否完成了授权回调。对于 Claude Code 这类工具接入时三件套要写全Base URL、API Key、Model ID。缺任何一个都会导致认证失败。5.6 三件套配置示例以 Cline 或类似支持自定义 API 的插件为例配置项通常长这样{ apiProvider: openai-compatible, baseUrl: https://taotoken.net/api, apiKey: sk-xxxxxxxx, modelId: claude-3-5-sonnet }Base URL 指向 TaoToken 的 API 地址Key 用你在控制台生成的Model ID 填你要用的模型标识。三个都对上请求才能通。如果只填了 Key 没填 Base URL请求会打到默认地址自然 401。6. 语义一致 CTA把排查链路和辅助工具接起来回到最初的问题共享池里的执行计划查不到怎么办现在你应该有完整的答案了。核心链路是display_cursor确认游标已消失 → 从DBA_HIST_SQLTEXT定位 sql_id → 从DBA_HIST_SQLSTAT拿到 plan_hash_value 和时间窗口 → 用display_awr捞出历史计划 → 通过 plan_hash_value 和执行次数做交叉验证。这套流程我在多次事后排查里用过最深的体会是不要等到出问题才去查 AWR 保留期。默认 8 天的保留窗口对很多业务来说太短如果你们的慢 SQL 复盘周期超过一周建议提前调整快照保留策略。另外DBA_HIST_SQLSTAT里的plan_hash_value是排查的钥匙养成拿到 sql_id 先查它的习惯能省掉很多来回试的时间。如果你想把模型辅助能力接进这套排查流程比如让模型帮你解释display_awr输出里的执行计划树或者把 AWR 查询结果整理成报告可以从模型对话页面先试一下效果https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 。打算长期做自动化排查工具、把模型调用嵌进脚本的走 Coding Plan 更合适https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。接入细节和 SDK 示例在文档里https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。最后留一个实用技巧把本文 3.2 节那个查plan_hash_value的 SQL 存成一个脚本文件命名成find_plan_by_sqlid.sql下次排查时直接find_plan_by_sqlid.sql加参数就能跑。排查这件事快一步拿到关键信息就少一分线上压力。