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 里。AWR(Automatic 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_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 地址是 https://taotoken.net/api (这个不加 UTM)。注意,TaoToken 不是数据库工具,它不碰你的 Oracle 实例,也不替代 SQL Developer 或 SQL*Plus,它只是帮你把“查文档、解释输出、生成脚本”这些周边动作做得更顺。
如果你只是想验证某个模型能不能正确解释执行计划,可以直接用模型对话页面试:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 。如果你打算长期把模型能力嵌进日常编码和排查流程,比如写一个自动解析 AWR 报告的小工具,那更适合走 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。需要管理多个 Key、区分不同项目的调用额度,就去控制台:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,Key 的创建和轮换在 API Keys 页面:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 。
这里要强调一点:TaoToken 是合规的 API 接入服务,不是任何形式的网络中转工具,也不涉及访问受限资源。它的价值在于让你用一套凭证对接多种模型,减少在排查过程中被工具切换打断思路的次数。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面有各语言 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_id
SQL 被置换出内存后,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_id:
SELECT 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_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 。
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_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 。打算长期做自动化排查工具、把模型调用嵌进脚本的,走 Coding Plan 更合适:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。接入细节和 SDK 示例在文档里:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。
最后留一个实用技巧:把本文 3.2 节那个查plan_hash_value的 SQL 存成一个脚本文件,命名成find_plan_by_sqlid.sql,下次排查时直接@find_plan_by_sqlid.sql加参数就能跑。排查这件事,快一步拿到关键信息,就少一分线上压力。