1. 从 v$sql 到真实执行计划:为什么 DISPLAY_CURSOR 更靠谱
线上 SQL 变慢时,很多人第一反应是拿 SQL 文本去EXPLAIN PLAN FOR跑一遍。但这样得到的只是「优化器现在会怎么执行」,而不是「刚才那条 SQL 实际怎么执行的」。两者可能因为绑定变量、统计信息变化、游标共享等原因完全不同。真正要排查性能问题,得看游标缓存里那条已经执行过的 SQL 的真实执行计划,这时候DBMS_XPLAN.DISPLAY_CURSOR()就是主力工具。
它的作用很直接:从游标缓存(cursor cache)里把某个已加载游标的执行计划捞出来,还能带上 I/O、内存、耗时等运行时统计。适合谁用?DBA、后端开发、做 SQL 调优的工程师,尤其是遇到「同一条 SQL 有时快有时慢」这种典型场景。整个链路是:先从v$sql拿到SQL_ID和CHILD_NUMBER,再用DISPLAY_CURSOR展示计划,最后把排查记录整理成可分析的结构化文本。
这篇会给出可复制的查询片段、参数配置骨架,以及一次从SQL_ID到执行计划的完整验证动作。同时我会把排查记录通过 TaoToken 的统一 Key 通道接到 AI 辅助分析上,让「看计划」和「解读计划」串成一条链路,而不是看完一堆表格还得自己硬啃。
2. 前置准备:TaoToken 统一 Key 与排查链路
在动手之前,先把工具链准备好。TaoToken 在这里的角色是提供一个统一的 API Key 和调用通道,把 Oracle 排查过程中产生的文本(执行计划、等待事件、SQL 文本)交给模型做辅助解读。它不替代数据库客户端,也不碰你的生产库连接,只是把「分析」这一步接上。
你需要准备两样东西:
第一,一个可用的 TaoToken API Key。登录控制台后在 API Keys 页面创建,地址是 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=apikeys 。创建后复制保存,后面调用时放在请求头里。
第二,确认你的调用入口。模型对话走 https://taotoken.net/api ,接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=doc 。如果你打算长期做编码和 Agent 类任务,可以看 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=codingplan 。
注意:TaoToken 只负责模型调用通道,数据库连接、SQL 执行仍然在你自己的 Oracle 客户端或应用里完成。不要把生产库连接串交给任何外部服务。
准备就绪后,整个排查链路是这样的:Oracle 里执行 SQL → 查v$sql拿标识 →DISPLAY_CURSOR出计划 → 把计划文本通过 TaoToken 发给模型做解读 → 回到数据库验证优化。下面进入具体操作。
3. 可复制配置:从 SQL_ID 到 DISPLAY_CURSOR 参数骨架
先看DISPLAY_CURSOR的函数签名,这是所有操作的起点:
DBMS_XPLAN.DISPLAY_CURSOR( sql_id IN VARCHAR2 DEFAULT NULL, child_number IN NUMBER DEFAULT NULL, format IN VARCHAR2 DEFAULT 'TYPICAL' );三个参数的含义:sql_id是游标缓存里的 SQL 标识;child_number是子游标号,同一条 SQL 因绑定变量或环境不同可能有多个子游标;format控制输出详细程度,常用值有BASIC、TYPICAL、ALL、ADVANCED。想看运行时统计,用ALL或ALLSTATS LAST。
第一步,执行一条带标记的 SQL,方便后面定位:
SELECT /* TOTO */ ename, dname FROM dept d JOIN emp e USING (deptno);第二步,从v$sql拿到SQL_ID和CHILD_NUMBER:
SELECT sql_id, child_number, hash_value, executions FROM v$sql WHERE sql_text LIKE '%TOTO%';输出类似:
SQL_ID CHILD_NUMBER HASH_VALUE EXECUTIONS --------------- ------------ ----------- ---------- gwp663cqh5qbf 0 3693697075 1这里SQL_ID和HASH_VALUE本质上是同一套东西的不同表示,DISPLAY_CURSOR的入参虽然写的是sql_id,但传hash_value也能定位到同一个游标。实际用的时候我一般优先用SQL_ID,因为它更稳定、可读性也更好。
第三步,直接展示执行计划:
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('gwp663cqh5qbf', 0, 'ALLSTATS LAST'));如果你手上只有HASH_VALUE,可以这样写:
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR( (SELECT sql_id FROM v$sql WHERE hash_value = 3693697075 AND ROWNUM = 1), 0, 'TYPICAL' ));第四步,把v$sql和DISPLAY_CURSOR关联起来,一次查出所有匹配 SQL 的计划:
SELECT t.* FROM v$sql s, TABLE(DBMS_XPLAN.DISPLAY_CURSOR(s.sql_id, s.child_number, 'ALLSTATS LAST')) t WHERE s.sql_text LIKE '%TOTO%';这种写法适合批量排查,但要注意v$sql里可能有多个子游标,输出会比较多。个人更推荐先精确定位单个SQL_ID,再单独展示,结果更干净。
4. 验证请求:一次完整的 SQL_ID 到执行计划动作
光看语法不够,走一遍完整流程。假设线上有个慢查询,你从 AWR 或v$sql里拿到了SQL_ID是gwp663cqh5qbf,CHILD_NUMBER是 0。
先确认这个游标还在缓存里:
SELECT sql_id, child_number, plan_hash_value, executions, buffer_gets, elapsed_time FROM v$sql WHERE sql_id = 'gwp663cqh5qbf';如果查不到,说明游标已经被挤出缓存,这时候DISPLAY_CURSOR会返回空或者报错,需要从 AWR 历史里找,或者重新执行一次 SQL 再抓。
确认存在后,展示执行计划:
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('gwp663cqh5qbf', 0, 'ALLSTATS LAST'));输出大致如下:
Plan hash value: 3693697075 | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | |----|--------------------|------|------|-------|------------|----------| | 0 | SELECT STATEMENT | | | | 7 (100) | | | 1 | SORT GROUP BY | | 4 | 64 | 7 (43) | 00:00:01 | |* 2 | HASH JOIN | | 14 | 224 | 6 (34) | 00:00:01 | | 3 | TABLE ACCESS FULL| DEPT | 4 | 44 | 3 (34) | 00:00:01 | | 4 | TABLE ACCESS FULL| EMP | 14 | 70 | 3 (34) | 00:00:01 | Predicate Information (identified by operation id): 2 - access("E"."DEPTNO"="D"."DEPTNO")看到TABLE ACCESS FULL出现在大表上,通常就是优化点。这时候把这段计划文本复制出来,通过 TaoToken 发给模型做解读。调用示例(以 curl 为例):
curl https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer YOUR_TAOTOKEN_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "claude-sonnet-4-20250514", "messages": [ {"role": "user", "content": "这是 Oracle 执行计划,请分析瓶颈并给出索引建议:\n| Id | Operation | Name | Rows |\n| 3 | TABLE ACCESS FULL | DEPT | 4 |\n| 4 | TABLE ACCESS FULL | EMP | 14 |"} ] }'模型返回后,你会得到类似「EMP 表全表扫描,建议在 DEPTNO 上建索引」的建议。然后回到数据库验证:
CREATE INDEX idx_emp_deptno ON emp(deptno);再执行一次原 SQL,重新用DISPLAY_CURSOR看计划是否变成INDEX RANGE SCAN。这就是一次完整的「定位 → 展示 → 解读 → 验证」闭环。
5. 本篇常见错排查
实际操作中,DISPLAY_CURSOR有几个高频坑,我踩过也见别人踩过。
第一个,DISPLAY_CURSOR返回空。最常见原因是游标已经不在缓存里了。v$sql是循环使用的,SQL 执行完一段时间没再执行,或者缓存压力大,就会被挤出去。解决办法是重新执行一次原 SQL,或者从dba_hist_sqlstat配合 AWR 报告里找历史计划。
第二个,SQL_ID传了但报ORA-01403: no data found。这通常是因为child_number不对。同一条 SQL 可能有多个子游标,child_number从 0 开始编号。先用v$sql查出所有子游标,再逐个展示:
SELECT child_number, plan_hash_value, executions FROM v$sql WHERE sql_id = 'gwp663cqh5qbf';第三个,format参数用了ALLSTATS LAST但没看到统计信息。这是因为该游标执行时没有开启统计收集。ALLSTATS依赖V$SQL_PLAN_STATISTICS_ALL,只有 SQL 在statistics_level=TYPICAL或ALL且游标被标记收集时才有数据。如果看不到,改用TYPICAL或BASIC先看结构。
第四个,权限问题。普通用户可能没有查v$sql的权限,需要SELECT_CATALOG_ROLE或SELECT ANY DICTIONARY。报ORA-00942: table or view does not exist时先确认权限。
第五个,HASH_VALUE和SQL_ID混用导致定位错误。虽然两者本质一样,但HASH_VALUE是数字,SQL_ID是字符串,传参时类型要对。用HASH_VALUE查v$sql时记得加ROWNUM限制,避免多行返回。
提示:排查时把
v$sql的sql_text、plan_hash_value、executions、buffer_gets一起查出来,和DISPLAY_CURSOR的输出对照,能快速判断是计划变了还是数据量变了。
6. 把排查记录接上 AI:统一 Key 的语义一致用法
排查完一条 SQL,记录往往散落在各个客户端里。我的做法是:每次用DISPLAY_CURSOR拿到计划后,把计划文本、SQL_ID、PLAN_HASH_VALUE、执行时间一起整理成一段结构化文本,通过 TaoToken 的统一 Key 发给模型,让它做三件事:识别全表扫描和笛卡尔积、对比前后计划差异、给出索引或改写建议。
调用入口统一走 https://taotoken.net/api ,Key 在控制台管理。如果你做的是长期编码和 Agent 任务,Coding Plan 会更合适:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=codingplan 。模型对话可以直接在 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=models 里试。
这样做的价值在于:DISPLAY_CURSOR给你事实,模型给你解读,两者用同一个 Key 串起来,排查记录不再是一次性的。下次遇到同类SQL_ID,翻出之前的分析记录,对比PLAN_HASH_VALUE就能判断计划是否退化。整个链路里,TaoToken 只做通道,数据库的事还是数据库自己解决,边界清晰,用起来也放心。