ARTICLE DETAIL

资讯详情

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

Oracle 11G 执行计划解读:dbms_xplan 包 display 与 display_cursor 实战

Oracle 11G 执行计划解读:dbms_xplan 包 display 与 display_cursor 实战 1. 从一次全表扫描告警说起dbms_xplan 到底能帮你做什么线上一条查询突然变慢开发同学第一反应往往是“加个索引不就行了”。可等你把索引建好执行时间纹丝不动这时候才意识到问题可能不在索引本身而在于优化器压根没走你期望的那条路。Oracle 11G 里判断这类问题最直接的手段就是看执行计划。而dbms_xplan包正是官方提供的一套“读计划”工具它把plan_table、v$sql_plan、v$sql_plan_statistics这些底层视图里的数据整理成人类可读的表格。很多刚接触 Oracle 的朋友会问执行计划不是用explain plan for就能看吗没错但explain plan得到的只是优化器“预估”的计划它不一定会真正执行也不带真实的 I/O 和行数统计。而dbms_xplan.display_cursor拿到的是游标缓存里“真实跑过”的计划两者结合才能定位全表扫描、索引失效、统计信息过期这些典型问题。这篇文章面向的是需要在测试库中独立复现并确认优化效果的读者。我会围绕display与display_cursor两个过程给出可复制的查询脚本、权限配置、输出对照表以及验证步骤。你不需要有 DBA 背景只要能连上测试库、有建表和查询权限就能跟着一步步做。核心检索词就三个Oracle 11G、dbms_xplan、执行计划解读。搞懂它们你排查慢 SQL 的效率会明显不一样。先说清楚两者的分工。dbms_xplan.display读取的是plan_table也就是你执行explain plan for后写入的那张表展示的是预估计划dbms_xplan.display_cursor读取的是库缓存中的游标展示的是真实执行计划还能带上A-Rows、Buffers、Reads这类运行时统计。一个看“打算怎么跑”一个看“实际怎么跑”配合使用才能形成完整判断。我试过在测试库上故意造一条走全表扫描的 SQL然后用两个函数分别输出差异非常直观预估计划里TABLE ACCESS FULL的Cost是 3真实计划里Buffers却是 200 多说明实际读的块远超预估。这种偏差往往就是统计信息不准或者绑定变量窥探导致的。下面从环境准备开始把每一步都拆开讲。2. 前置准备权限配置与测试表搭建在动手之前先把权限和测试数据准备好。dbms_xplan包本身是 Oracle 自带的PUBLIC用户默认就有执行权限所以大多数情况下你不需要额外授权就能调用display和display_cursor。但如果你要查询v$sql、v$sql_plan这些动态性能视图普通用户可能没有权限需要 DBA 授予SELECT权限或者直接给SELECT_CATALOG_ROLE。我建议在测试库里用一个独立 schema 来操作避免污染生产数据。先确认当前用户有没有查动态视图的权限-- 查看当前用户是否有查询 v$sql 的权限 SELECT * FROM v$sql WHERE ROWNUM 2;如果报ORA-00942: table or view does not exist说明权限不足让 DBA 执行GRANT SELECT ON v_$sql TO your_user; GRANT SELECT ON v_$sql_plan TO your_user; GRANT SELECT ON v_$sql_plan_statistics TO your_user; GRANT SELECT ON v_$session TO your_user;注意视图名是v_$sql而不是v$sql授权时要用带下划线的底层视图名。授权完成后用your_user重新登录即可。接下来建两张测试表模拟一个典型的“主表 维度表”场景。一张emp_test存员工一张dept_test存部门数据量不用大几百行就够看出全表扫描和索引访问的区别-- 建表 CREATE TABLE dept_test ( deptno NUMBER(2) PRIMARY KEY, dname VARCHAR2(14), loc VARCHAR2(13) ); CREATE TABLE emp_test ( empno NUMBER(4) PRIMARY KEY, ename VARCHAR2(10), job VARCHAR2(9), deptno NUMBER(2), sal NUMBER(7,2) ); -- 插入部门数据 INSERT INTO dept_test VALUES (10,ACCOUNTING,NEW YORK); INSERT INTO dept_test VALUES (20,RESEARCH,DALLAS); INSERT INTO dept_test VALUES (30,SALES,CHICAGO); INSERT INTO dept_test VALUES (40,OPERATIONS,BOSTON); COMMIT; -- 插入员工数据用循环批量造 BEGIN FOR i IN 1..500 LOOP INSERT INTO emp_test VALUES ( i, EMP || LPAD(i,4,0), CASE MOD(i,3) WHEN 0 THEN CLERK WHEN 1 THEN SALESMAN ELSE ANALYST END, CASE MOD(i,4) WHEN 0 THEN 10 WHEN 1 THEN 20 WHEN 2 THEN 30 ELSE 40 END, ROUND(DBMS_RANDOM.VALUE(1000,9000),2) ); END LOOP; COMMIT; END; / -- 收集统计信息这一步很关键 EXEC DBMS_STATS.GATHER_TABLE_STATS(USER,EMP_TEST); EXEC DBMS_STATS.GATHER_TABLE_STATS(USER,DEPT_TEST);统计信息一定要收集否则优化器可能因为“不知道表有多大”而做出离谱的选择。收集完之后先不建索引故意让emp_test处于无索引状态这样后面才能观察到全表扫描。dept_test有主键天然带唯一索引正好用来对比。再确认一下statistics_level参数这个参数决定了display_cursor能不能输出运行时统计SHOW PARAMETER statistics_level;如果值是TYPICAL默认不会收集行级统计。要拿到A-Rows、Buffers这些信息需要在会话级别临时改成ALLALTER SESSION SET statistics_level ALL;这个设置只影响当前会话退出后自动恢复测试库里放心用。生产环境慎用因为ALL会带来额外的性能开销。3. 可复制配置display 与 display_cursor 的完整调用脚本这一节给出可以直接复制粘贴的脚本包括explain plan写入、display读取、display_cursor读取以及带统计信息的格式组合。先看display的完整参数签名DBMS_XPLAN.DISPLAY( table_name IN VARCHAR2 DEFAULT PLAN_TABLE, statement_id IN VARCHAR2 DEFAULT NULL, format IN VARCHAR2 DEFAULT TYPICAL, filter_preds IN VARCHAR2 DEFAULT NULL );table_name默认就是PLAN_TABLE一般不用改。statement_id是你在explain plan set statement_id时指定的标识不指定就显示最近一条。format控制输出内容常用值有BASIC、TYPICAL、SERIAL、ALL、ADVANCED还可以用和-加减修饰符比如basic predicate、typical -bytes。filter_preds用来过滤plan_table里的记录比如plan_id 223。下面这段脚本把预估计划写入plan_table并显示出来-- 清理旧的计划数据 DELETE FROM plan_table; COMMIT; -- 生成预估执行计划指定 statement_id EXPLAIN PLAN SET STATEMENT_ID TSH FOR SELECT e.ename, d.dname FROM emp_test e, dept_test d WHERE e.deptno d.deptno AND e.ename EMP0001; -- 用 display 读取指定 statement_id 和格式 SELECT * FROM TABLE( DBMS_XPLAN.DISPLAY(PLAN_TABLE, TSH, BASIC PREDICATE) );输出里你会看到TABLE ACCESS FULL出现在EMP_TEST上因为ename没有索引。PREDICATE会把过滤条件也显示出来方便确认谓词有没有被正确下推。再看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不传则显示所有子游标。format除了支持display的那些值还多了IOSTATS、MEMSTATS、ALLSTATS、LAST、RUN_STATS_LAST、RUN_STATS_TOT这些和运行时统计相关的修饰符。要拿到真实计划先执行目标 SQL再调用display_cursor-- 先执行目标 SQL让它进入库缓存 SELECT e.ename, d.dname FROM emp_test e, dept_test d WHERE e.deptno d.deptno AND e.ename EMP0001; -- 显示当前会话最后一条 SQL 的真实计划 SELECT * FROM TABLE( DBMS_XPLAN.DISPLAY_CURSOR(NULL, NULL, TYPICAL) );如果要看带运行时统计的版本格式用ALLSTATS LASTSELECT * FROM TABLE( DBMS_XPLAN.DISPLAY_CURSOR(NULL, NULL, ALLSTATS LAST) );输出里会多出Starts、E-Rows、A-Rows、A-Time、Buffers、Reads这些列。E-Rows是预估行数A-Rows是实际行数两者差距大就说明统计信息可能有问题。Buffers是逻辑读Reads是物理读数值高说明 I/O 开销大。如果你知道sql_id也可以直接指定-- 从 v$sql 里找 sql_id SELECT sql_id, child_number, plan_hash_value, sql_text FROM v$sql WHERE sql_text LIKE %emp_test% AND sql_text NOT LIKE %v$sql%; -- 用 sql_id 查真实计划 SELECT * FROM TABLE( DBMS_XPLAN.DISPLAY_CURSOR(a67wqmkfb9j65, NULL, TYPICAL -PREDICATE -ROWS) );这里-PREDICATE -ROWS是去掉谓词和行数信息让输出更简洁。格式修饰符的加减号前面要留空格这是语法要求漏了空格会报错。为了让你更清楚两个函数的差异我整理了一张对照表对比项displaydisplay_cursor数据来源plan_tablev$sql_plan / 库缓存计划类型预估计划真实执行计划是否需先执行 SQL否explain plan 即可是SQL 必须跑过运行时统计无有需 statistics_levelALL绑定变量可靠性不可靠可靠典型用途快速看优化器意图定位实际性能瓶颈这张表建议收藏排查时先想清楚你要的是“预估”还是“真实”再决定用哪个函数。4. 验证请求与成功结果从输出中定位全表扫描与索引失效脚本跑通之后关键是怎么读输出。先看display的典型输出PLAN_TABLE_OUTPUT ------------------------------------------ Plan hash value: 3956160932 ------------------------------------------ | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| ------------------------------------------ | 0 | SELECT STATEMENT | | 1 | 33 | 7 (15)| | 1 | NESTED LOOPS | | 1 | 33 | 7 (15)| |* 2 | TABLE ACCESS FULL| EMP_TEST | 1 | 21 | 4 (0)| | 3 | TABLE ACCESS BY INDEX ROWID| DEPT_TEST | 1 | 12 | 1 (0)| |* 4 | INDEX UNIQUE SCAN| SYS_C001234 | 1 | | 0 (0)| ------------------------------------------Id2那行TABLE ACCESS FULL就是全表扫描作用在EMP_TEST上。Rows1是优化器预估返回 1 行Cost4是预估代价。因为ename没索引优化器只能全表扫。Id4的INDEX UNIQUE SCAN说明dept_test走了主键唯一索引这是正常的。再看display_cursor带ALLSTATS LAST的输出SQL_ID a67wqmkfb9j65, child number 0 ------------------------------------- SELECT e.ename, d.dname FROM emp_test e, dept_test d WHERE e.deptno d.deptno AND e.ename EMP0001 Plan hash value: 3956160932 | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads | | 0 | SELECT STATEMENT | | 1 | | 1 | 00:00:00.01 | 12 | 4 | | 1 | NESTED LOOPS | | 1 | 1 | 1 | 00:00:00.01 | 12 | 4 | |* 2 | TABLE ACCESS FULL| EMP_TEST | 1 | 1 | 1 | 00:00:00.01 | 11 | 4 | | 3 | TABLE ACCESS BY INDEX ROWID| DEPT_TEST | 1 | 1 | 1 | 00:00:00.01 | 1 | 0 | |* 4 | INDEX UNIQUE SCAN| SYS_C001234 | 1 | 1 | 1 | 00:00:00.01 | 1 | 0 |E-Rows1、A-Rows1预估和实际一致说明统计信息没问题。但Buffers11说明全表扫描读了 11 个逻辑块如果表有几十万行这个数字会非常大。这就是全表扫描的代价。现在建一个索引再对比一次CREATE INDEX idx_emp_ename ON emp_test(ename); EXEC DBMS_STATS.GATHER_TABLE_STATS(USER,EMP_TEST); -- 重新执行 SELECT e.ename, d.dname FROM emp_test e, dept_test d WHERE e.deptno d.deptno AND e.ename EMP0001; -- 再看真实计划 SELECT * FROM TABLE( DBMS_XPLAN.DISPLAY_CURSOR(NULL, NULL, ALLSTATS LAST) );这次输出里EMP_TEST那行应该变成INDEX RANGE SCAN或TABLE ACCESS BY INDEX ROWIDBuffers会从 11 降到 3 左右。这就是索引生效的直接证据。如果建了索引但计划没变还是全表扫描常见原因有几个统计信息没更新、索引列被函数包裹比如WHERE UPPER(ename)EMP0001、隐式类型转换ename是VARCHAR2却传了数字、或者优化器认为全表扫描代价更低。这时候用display_cursor的PREDICATE看谓词信息能快速判断是不是索引失效。验证优化效果的标准很简单A-Rows和E-Rows接近Buffers明显下降Operation从TABLE ACCESS FULL变成索引访问。三个条件满足优化就算到位。5. 本篇常见错排查401、local proxy failed、reading choices 与 OAuth 类报错这一节把实际排查中高频出现的报错列出来对照真实错误信息给出处理思路。虽然dbms_xplan本身不涉及网络请求但很多同学是在通过客户端工具或 API 方式连接数据库时遇到问题这里一并覆盖。ORA-00942: table or view does not exist。调用display_cursor时如果报这个错通常是当前用户没有查v$sql_plan的权限。解决方法是让 DBA 授予SELECT ON v_$sql_plan或者给SELECT_CATALOG_ROLE。注意授权时用v_$sql_plan而不是v$sql_plan。ORA-01427 或输出为空。display返回空行多半是plan_table里没有对应statement_id的记录。检查explain plan set statement_id是否执行成功以及statement_id大小写是否一致。Oracle 里字符串比较区分大小写TSH和tsh不是同一个。local proxy failed。这个报错常见于通过本地代理工具连接数据库时代理进程没启动或端口被占用。检查代理配置里的监听端口是否和客户端一致重启代理进程后重试。如果是容器环境确认端口映射有没有写对。401 Unauthorized。调用 API 方式访问数据库管理接口时出现说明认证信息缺失或过期。检查 API Key 是否填写正确有没有多余空格以及 Key 是否已过期。重新生成 Key 后更新配置即可。reading choices 相关报错。这类错误通常出现在解析返回结果时返回体不是预期的 JSON 结构可能是接口返回了 HTML 错误页。打印原始响应内容确认检查请求 URL 和参数是否正确。OAuth 认证失败。如果使用 OAuth 方式接入报错一般是 token 过期或 scope 不足。重新走一遍授权流程确认申请的 scope 包含数据库访问权限。token 有效期通常较短建议配置自动刷新。CC Switch / Cline MCP / Codex auth.json 配置三件套。如果你在用这些工具接入配置里必须写全三项Base URL、Key、Model ID。缺任何一项都会导致连接失败。Base URL 填https://taotoken.net/apiKey 填你生成的 API KeyModel ID 填具体模型名称。三件套齐全后保存配置重启工具生效。display_cursor 没有 A-Rows 列。检查statistics_level是否为ALL或者 SQL 里有没有加/* gather_plan_statistics */提示。两者满足其一才能收集运行时统计。另外ALLSTATS LAST格式必须配合statistics_levelALL使用否则只显示预估信息。绑定变量导致计划不准。explain plan对绑定变量的 SQL 给出的计划不可靠因为优化器在explain plan时可能用了默认值或窥探值。这种情况必须用display_cursor看真实计划必要时用DBMS_SQLTUNE做 SQL 诊断。排查的核心思路是先确认权限再确认数据来源最后确认格式参数。三步走完大部分问题都能定位。6. 语义一致 CTA把执行计划解读落到日常优化流程里执行计划解读不是一次性任务而是慢 SQL 排查的固定环节。我的习惯是拿到一条慢 SQL先用explain plan fordbms_xplan.display看预估计划快速判断优化器意图再执行一次用dbms_xplan.display_cursor带ALLSTATS LAST看真实开销。两者对比偏差大的地方就是问题所在。如果你需要频繁做这类分析可以考虑把常用脚本封装成视图或存储过程减少重复输入。比如建一个查询v$sql找sql_id的视图再建一个传入sql_id直接输出格式化计划的函数。这样每次排查只需要两步找sql_id调函数。对于需要长期做 SQL 优化和 Agent 辅助编码的场景可以了解下 Coding Plan 的接入方式把执行计划分析纳入日常工具链。模型对话入口适合快速验证 SQL 写法API Keys 和接入文档则提供了完整的配置说明。把这些资源用起来排查效率会稳定很多。最后留一个实用技巧在测试库复现问题时记得先ALTER SESSION SET statistics_level ALL再执行目标 SQL最后调display_cursor。顺序错了就拿不到运行时统计。另外每次改完索引或统计信息都要重新执行 SQL 再看计划因为库缓存里的旧计划可能还没失效。用ALTER SYSTEM FLUSH SHARED_POOL可以强制清空但生产环境慎用。测试库里放心折腾把全表扫描和索引失效两种场景都跑一遍手感就出来了。
返回列表