oracle优化器之执行计划

什么是执行计划
执行计划显示了执行一个sql语句所需步骤的详细信息.这些步骤代表了一组数据库操作它们会消费和生产行数据.这些操作的顺序以及它们的实现取决于查询优化器对查询转换和物理优化技术的联合使用.执行计划通常是以表格形式来显示,这个执行计划实际上是一个树形结构.例如下面是一个基于sh方案的查询:
SELECT prod_category, AVG(amount_sold)
FROM sales s, products p
WHERE p.prod_id = s.prod_id
GROUP BY prod_category;
下面的表格是上面语句的执行计划:

树形结构形式的执行计划如下:

上面的表格代表了一个自上而下,从左到右遍历的执行树.当读取一个执行计划树时你应该从底部的左边开始然后自下而上.在上面的这个例子中从查看树的叶子块开始.在这种情况下树的叶子块是对products和sales表进行全表扫描来实现的.由这些表扫描产生的行数据将被连接操作来消费使用.这里连接操作是一个哈希连接.最后group-by操作使用哈希来实现的它将消费由连接操作产生的行数据并将最终结果返回给终端用户.

显示执行计划
有两种常用的方法来显示一个sql语句的执行计划:
Explain plan命令—它能不用实际执行sql语句就能显示出其执行计划

V$sql_plan—在oracle9i中引入的一个动态性能视图它显示一个sql语句已经被编译成游标并存储在游标缓存中的执行计划.在特定条件下使用explain plan显示的执行计划可能不同于使用v$sql_plan所显示的执行计划.例如,当sql语句包含绑定变量时使用explain plan在显示执行计划时会忽略掉绑定变量的值,当使用v$sql_plan显示执行计划时会在生成执行计划的过程中考虑绑定变量的值.
在oracle9i中引入了dbms_xplan包使得显示执行计划变得更加容易,而且这个dbmx_xplan包在后续的版本中功能更加增强了.这个dbms_xplan包提供了一些pl/sql接口来显示不得来源的执行计划:
Explain plan命令
V$sql_plan
Automatic workload repostitory(Awr)
Sql tuning set(STS)
Sql plan baseline(spm)

使用explain plan命令与dbms_xplan.display函数
下面的例子将说明使用dbms_xplan包所提供的不同函数来怎样生成和显示执行计划

Dbms_xplan.display函数的参数如下:
Plan table name(缺省值是’plan_table’)
Statement_id(缺省值是null意味着最后一个被插入plan table的语句)
Format 控制着显示信息的总量(缺省值是’typical’)

为了利用explain plan的功能你需要有合适的权限来运行你要试图explain plan的语句.一个缺省的plan_table对于每一个用户都是存在的不需要提前创建.

使用dbms_xplan.display_cursor函数
一种替代的方法是真实执行sql语句来生产执行计划并使用dbms_xplan.display_cursor函数来显示执行计划.

Dbms_xplan.display_cursor函数的参数值如下:
Sql_id(缺省值是null,意味着在这个会话中最后一个执行的sql语句)
Child number(default 0),
Format 控制着显示信息的数量(缺省值是’typical’)
除了要有实际运行这个sql语句的权限之外还要有对v$sql_plan,v$sql_plan_detail和select_catalog_role的select权限.

格式化执行计划
Dbms_xplan包中函数的格式化参数是高度可定制的在执行计划输出中可以根据需要来显示少量或大量的详细信息.这里有三个预先定义的格式变量:
Basic 在执行计划中只会显示ID,operation和name列
Typical 在执行计划中显示了在basic级别的信息之外还包括了额外优化相关的内部信息比如,cost,cardinality,estimates等等.在执行计划中这些信息显示了每一个操作优化器所认为的操作成本,生成的行记录数等等.也显示了每一个操作的谓词评估.有两种类型的谓词:access和filter.access谓词对于索引来说将通过对合适的列应用搜索条件来检索相关的数据块.filter谓词在检索数据块后来进行评估.

All 在执行计划中显示了在typical级别的信息之外还包括了每一个操作产生的表达式(列)列表.提示别名和查询块名字属于outline信息.最后两个片段的信息可以作为参数对语句添加提示.

低级别的选项可以包括或者排除详细信息比如谓词和cost信息.下面的信息显示了基本的执行计划和谓词信息和优化器成本列cost

也可以使用低级别的参数来排除其它信息.下面的例子显示了排除优化器成本cost和bytes列信息:

Note部分
除了执行计划和谓词信息之外,dbms_xplan包在note部分显示了其它的信息.比如在查询优化或星型转换时应用于查询的动态抽样.例如下面的例子中表sales没有统计信息,所以优化器在查询优化时使用动态抽样,在显示执行计划时加上note信息:

Note部分的信息当格式选项被设置为typical或all时是自动显示的.

什么是成本
Oracle优化器是一个基于成本的优化器.对一个sql语句选择执行计划它其实只是优化器考虑的许多替代执行计划中的一个.优化器会选择成本值最低的执行计划.这里的成本代表了对执行计划的资源使用的评估.成本值越低的执行计划其执行效率会越高.优化器成本模型会对查询计算IO,CPU和网络资源使用情况.

在执行计划中整个执行计划的成本(在第0行显示),每一个单独的操作也显示了执行成本.然而它并不能可以调整.这个成本是一个内部单元用于执行计划的比较.

理解执行计划
为了判断你是否正在查找一个好的执行计划,需要理解优化器判断执行计划首先要考虑的是什么.如果优化器在其评估或者计算中有任何问题导致了选择了次优的执行计划,你应该查看执行计划和其评估.评估的组件有:
Cardinality—-评估每一个操作将产生的行记录数
Access method —-数据被访问的方法可以是表扫描或索引扫描
Join method –-用于表连接的方法(哈希,排序合并,嵌套循环)
Join order --表连接的先后顺序
Partition pruning –-对于查询来说只有必须要被访问的分区才会被访问
Parallel execution –在并行执行情况中,执行计划中的每一个操作是否正在被并行执行,是否使用了正确的数据分布方法

下面将详细说明在执行计划中的这些组件.
Cardinality
基数是评估每一个操作将返回的行记录数.优化器判断每一个操作的基数是基于输入的表和列级统计信息(或者通过动态抽样所得到的统计信息)并使用复杂的一组公式来评估的.在一个单表查询(没有直方图信息)中只有一个等值谓词时将会使用一个简化的公式.在这种情况下优化器会假设列的数据是均匀分布且计算这个查询的基数是通过将表的总行数除以谓词列中不重复值的个数.
下面的查询hr方案中的employees表中107行记录

在表employees中job_id有19个不相同的值所以优化器预测这个查询语句的基数为107/19=5.6因此用dbms_xplan显示为6行

评估基数是很重要的因此尽可能的准确因为他们会影响执行计划的访问路么,连接顺序.然而有多个因素可能导致错误的基数评估即使当基表和列统计统计信息及时更新的情况下.这些因素包括:
数据倾斜
对单个表使用多个单列谓词
在where子句谓词中使用函数加密的列
复杂的表达式

在前面的例子中在employees表中的job_id的数据是倾斜的.不是每一个job_id都有相同的雇员数.实际上在employees表中job_id为’AD_VP’的只有两个雇员,优化器评估的的大小是他的三倍.为了精确的反映数据倾斜,需要对job_id列生成直方图.直方图的出现将会改变优化器基数评估的公式.

缺省情况下oracle会基于列使用的统计数据和数据倾斜的出现来自动判断列需不需要生成直方图.如果要手动创建直方图可以使用下面的命令:
SQL > Exec DBMS_STATS.GATHER_TABLE_STATS(‘HR’,’EMPLOYEES’,
method_opt=>'FOR COLUMNS SIZE 254 JOB_ID’);
当有了直方图后优化器能评估出将要返回的正确行数:

尽管对于这个查询有了更准确的基数评估后执行计划没有发生变化但还是要准确的评估其基数.

判断正确的基数
为了手动判断优化器是否已经正确的评估了正确的基数,可以对于查询中的每一个表使用一个简单的select count(*) 查询并应用属于这个表的where子句来检查.对于这个例子可以先使用:

另外也可以在sql语句中使用gather_plan_statistics提示来自动收集全面的运行时统计信息.这个提示会记录在每一步操作中真实的基数(返回的行数).执行时(运行)基数使用格式化参数’allstats last’的dbms_xplan.display_cursor可以在执行计划中显示出来.额外的列叫做A-Rows它是实际返回的行数

注意使用gather_plan_statistics提示对sql语句的执行时间有影响,所以你应该只在分析的情况下使用它.当初始化参数statistics_level=all的情况下不使用gather_plan_statistics提示也是可以显A-Row列. SQL*Monitoring功能—oracle enterprise manager或者pl/sql接口对于sql语句总是会显示A-Rows列而不会有任何开销.

Access method
访问方法或访问路径—显示了每一个表(或索引)中的数据将怎样被访问.访问方法是在执行计划中的operation列显示的

Oracle支持九种常见的访问方法:
Full table scan –从一个表中读取所有的行记录并过滤掉不满足where子句中谓词条件的记录.一个全表扫描将使用多块IO(通常是1MB IOs).当要从一个表中返回大部分行记录,或者表中没有索引或者存在的索引不能被使用或者它的成本值最低时就会选择全表扫描.决定使用全全扫描也会受以下因素影响:
初始化参数db_multi_block_read_count
Parallel degree
Hints
缺少可用的索引
使用索引的成本更高

Table access by rowed—行的rowid指定的数据文件,数据块,以及行在数据块中的位置.oracle首先从where子句的谓词或者从表中的一个索引或多个索引中获得rowid.oracle然后会基于获得的rowid来回表定位所选的每一行记录的位置再一行一行访问.

Index unique scan—扫描唯一索引只会返回一行记录.在等值谓词用于一个唯一索引或一个主键列上会使用唯一索引扫描

Index range scan—oracle访问相邻的索引条目然后使用索引中的rowid值来从表中检索相关的行记录.索引范围扫描可以是有边界也可以是无边界.当对一个非唯一索引键使用等值谓词或者对一个唯一键使用非等值或范围谓词时将会使用索引范围扫描(=,< ,>,like),数据会以索引列的升序返回.

Index range scan descending 索引范围降序扫描—与索引范围扫描的概念是相同的,当order by … descending子句中的列是某个索引列表的子集时就会使用.

Index skip scan –正常情况下为了使用一个索引,索引键的前缀(索引的前导列)将要在查询中被引用.然而,除了索引中的第一列外其它的列在语句中被引用,oracle可以进行索引跳跃扫描来跳过索引的第一列而使用剩下的列.如果在一个复合索引中前导列只有几个不同的值而在非前导列中有大量不同值时是有用的.

Full index scan—完全索引扫描不会读取索引结构中的每一个索引块.索引完全扫描会处理索引的所有叶子块,但只在足够的分支块中找到第一个叶子块.当查询语句中所引用的列都在索引列中存在这时使用完全索引扫描比扫描表成本更低.在以下情况下可能使用单块IO:
一个orader by子句有索引中所有的列且顺序和索引相同(也可以是索引列中的子集)
查询要求执行一个排序合并连接且查询中所引用的所有列都出现在索引列中
查询中引用列的顺序与索引前导列的顺序相同
一个group by 子句出现在查询中,group by子句中的列出现在索引列中.

Fast full index scan—这是一种替代的完全表扫描当索引包含查询所需要的所有列时且在索引键中至少有一个列有not null约束.它不能用来消除一个排序操作,因为数据访问不遵循索引键.它将使用多块读来读取索引中的所有索引块,与完全索引扫描不一样.

Index join—连接相同表中的 多个索引这个集合包含了查询中所引用的所有列.如果索引连接被使用那么就不需要进行表访问.因为所有相关的列都能从索引中得到,索引连接操作不能消除排序操作.

Bitmap index—位图索引使用每一个索引键的一组位图值和一个映射函数将每一个位图转换成rowid.当where子句中的几个谓词使用boolean操作来解决and和or条件时oracle能够有效的合并位图索引

如果看到的执行计划中访问方法不是你所期望的,可以检查对这些对象的基数评估是不是正确的,连接顺序所允许的访问方法是不是你期待的

Join method
连接方法描述了来自两个数据生产者的数据怎样连接在一起.可以通过查看执行计划中的operations列来识别sql语句中的连接方法

Oracle数据库提供了几种连接方法和连接类型

Join methods

Hash joins—哈希连接用于大数据集的连接.oracle优化器在内存中基于连接键使用两个表或者两个数据源中的小者来构建一个哈希表,然后扫描大表基于连接键执行相同的哈希运算.对于每一个值都会探测之前构建的哈希表如果匹配就返回这一行.

Nested loops joins—嵌套循环连接当第一要访问的表有较小的数据集且对于第二个表存在一种有效的访问方式(例如索引扫描)时是很有用的.对于第一个表(外部表)中的每一行,oracle将要访问第二个表(内部表)中的所有行.可认将它看作是两个嵌入的for循环.在oracle11g中为了减少物理I/O的整体延迟嵌套循环连接的内部实现发生了改变,所以在执行计划中的operations列中会看到两个nested loops.在之前的oracle版本中只会看到一个.

Sort merge joins—当两个表的连接条件是等值条件时排序合并连接是非常有用的,比如< ,<=,>,>=.对于大数据集来说排序合并连接比嵌套循环连接性能更好. 排序合并连接是由两个步骤组成:
排序连接操作:所有输入会基于连接键进行排序
合并连接操作:排序列表将被合并

如果一个表中存在一个索引能够消除排序操作那么排序合并连接就更会被优化器所选择.在下面的例子中只有来自sales表的记录需要进行排序(ID 5),而products表中的记录基于连接键使用主键索引已经被排序了(ID 4).

Cartesian join—优化器用一个数据源中的每一行与另一个数据源中的所有行进行连接.通常情况下如果被调用的表较小或都一个或多个表与语句中的任何其它表没有连接条件的情况下会被优化器选择.笛卡尔连接不常见,所以当它出现时可能是基数评估出现了问题.

Join types
Oracle提供了几种连接类型:inner join,(left) outer join,full outerjoin,anti join,semi join,grouped outer join等等.其中inner join是最常见的连接类型,因此执行计划中没有出现关键字”inner”.

Outer join—外连接将返回满足条件的所有行和没有使用(+)标识的其它表中不满足连接条件的行记录.例如t1.x=t2.x(+),这里t1是左表它的不满足连接条件的行记录将会被返回.

Join order
连接顺序是在一个多表sql语句中每一个表被加入连接的一个顺序.为了判断一个执行计划中表的连接顺序可以查看operation列中表的缩进,在下面的图表中sales和products表的缩进是相同的且它们都比customers表更缩进.因此sales和products表首先使用一个哈希连接进行连接然后它们的连接结果再与customers表进行哈希连接.

在一些复杂的sql语句中通过查看operations列中表的缩进来判断表的连接顺序是不容易的.在这种情况下使用带有format参数的dbms_xplan过程来显示执行计划的outline信息,它包括了连接顺序.例如,下面的图表是使用带有format选项的dbms_xplan.display_cursor来生成outline信息.
DBMS_XPLAN.DISPLAY_CURSOR(FORMAT=>’Typical + outline’));

在outline信息中查看以leading开头的行.这一行显示了查询的连接顺序.在这个例子中可以看到”P”,”S”,”C”三个字母,这三个字母是查询中所引用的三个表的别名.P(products)表与S(sales)表进行连接然后再与C(customers)表连接.

连接顺序是基于成本来决定的所以基数评估和访问路径对连接顺序是有强烈影响的.优化器也总是遵守一些基本规则:
大多数产生一行结果集的连接总是会先被执行.优化器基于表的unique和primary key约束来进行判断.

当使用外连接时没有外连接操作符(+)的表必须在谓词中其它有外连接操作符的表之后这样才能确保不满足连接条件的行记录才能被加入到返回结果集中.

当一个子查询已经被转换成反连接或半连接时那么子查询中的表将在外部查询块中的表之后进行连接.然而,哈希反连接和哈希半连接在特定情况会覆盖这种连接顺序.

如果不能进行视图合并那么视图中的所有表会在与视图外部表连接之前进行连接

如果连接顺序不是你所期待的那么检查每一个对象的基数评估和访问方法是否正确.

Partitioning
分区允许一个表,索引或索引组织表被分成一些较小的片段.每一个数据库对象的片段叫作分区.分区修剪或分区消区是使用分区来提高性能的最简单的方法.例如一个程序有一个orders表包含了最近2年所有的订单记录.而且这个orders表通过day来进行分区.一个查询只查询一周的订单记录它只会访问orders表的7个分区而不是730个分区.
分区修剪在执行计划中的pstart和pstop列中是可见的.pstart列包含了将被访问的第一个分区,pstop列包含了将被访问的最后一个分区.在下面的图表中sales表有4个分区被访问,分区名是9,10,11,12

当对一个按day进行分区并且按cust_id列进行子分区的表进行一个简单查询那么在pstart和pstop列中会出现更多的数字,这些额外的数字意味着什么例如:

当使用组合分区时,oracle每一个分区的数字从1到n(绝对分区数字).对于一个只有一层分区的表,这些绝对数代表了单层分区表在磁盘上的真实物理段.

在组合分区表中,然而一个分区是一个逻辑条目不代表磁盘上的真实物理段.每一个分区又是被细分为多个子分区.每一个子分区的分区数字从1到m(单个分区的相对子分区数字).最终组合分区表中的所有子分区都被指定一个全局数字从1到(n*m)(绝对子分区数字).这些绝对数字代表了组合分区表在磁盘上的真实物理段.

所以在之前的执行计划中line 4这一行的pstart和pstop列中的数字为10,代表了全局分区数代表了磁盘上的物理段.执行计划中的line 2这一行中的pstart和pstop列的数字为5代表了分区号,执行计划中的line 3这一行中的pstart和pstop列的数字为2代表分区的了相对子分区号

在有些情况下在pstart和pstop列会出现字母或单词而不是数字.例如在这些列中可以看到单词KEY.这说明在解析时不能识别,查询将访问哪个分区但优化器相信在执行时(动态修剪)分区修剪将会发生.当查询中的基于分区键列等值谓词中包含一个函数时就会出现这种情况.例如time_id=sysdate,另一种情况动态修剪会发生当查询中在分区键列上有一个连接条件且这个表不是与分区表的所有分区进行连接时,例如,因为一个filter谓词,分区修剪将在执行时发生.在下面的例子中,time表与sales表基于分区键time_id进行连接,在where子句谓词被应用到time表且合适的time_ids被选择后那么分区修剪在执行时会发生.

如果分区修剪不象预期那样发生那么检查分区键列上的谓词.确保谓词使用了与分区键列相同的数据类型.也可以检查执行计划中的谓词信息部分的内容.如果表是哈希分区,如果基于分区键列的谓词是一个等值或in-list谓词那么分区修剪将会发生.所以如果一个表是多列哈希分区,那么只有哈希分区中的所有列都在谓词中出现才会发生分区修剪.

Parallel execution
Oracle中的并行执行是基于一组协调原则(通常叫作查询协调器或QC)和并行服务器进程的.QC在并行执行中对单个会话中初始化并行sql语句和并行服务器进程来执行工作.QC将工作分配给并行服务器进程且并可能要执行较小的不能被并行执行的工作.例如一个使用sum()操作的的并行查询要求将每个并行服务器进程的计算结果求和

QC在并行执行计划中是很容易识别的因为会出现它的名字.在下面的图表中可以查看ID 1这一行就会看到在operation列中的PX COORDINATOR.在执行计划中在这一行之上的所有操作都是由QC来完成的.因为这些操作是由单进程串行执行完成的.通常要最小化由QC所有完成的操作.在PX COORDINATOR之下的所有操作通常是由并行服务器进程所完成的.

Granules
粒度是指派给并行服务器进程工作的最小单元.为了在并行服务器进程之间得到均匀的工作分布粒度数量通常比请求的DOP高.每一个并行服务器进程将只会完成分配给它的工作粒度当它完成指定的工作粒度后会得到另外的工作粒度直到所有的工作粒度都被执行完成为止.oracle数据库对于并行执行分配工作使用的基本机制就是基于数据块范围或基于数据块粒度.在执行计划中可以看到粒度是怎样分配给并行服务器进程的.在下面图表中的执行计划中的line 7这一行的operation列中可以看到’PX BLOCK ITERATOR’,这意味着并行服务器进程将遍历完所生成的数据块范围粒度来完成表扫描.

虽然基于数据块粒度是最常见的方法,这里有些操作可能会受益于分区表的底层数据结构.这些情况下分区表将成为一个工作粒度.使用基于分区粒度一个并行服务器进程将以单个分区的所有数据为一个工作粒度.如果在操作中被访问的分区数至少等于DOP时oracle优化器会考虑基于分区的工作粒度,在下面的图表中在执行计划中的line 6这一行的operation列可以看到’PX PARTITION RANGE ALL’意味着每一个并行服务器进程将只会完成分区表中的一定范围的分区.

基于sql语句的并行度,oracle数据库将决定是使用基于数据块还是基于分区的工作粒度来完成更优的执行.
Producers and consumers
为了有效的并行执行一个sql语句,并行服务器进程实际上是以工作集来一起完成这项工作的.一组是生产行(生产者),一组是消费行记录(消费者).例如在下面的图表中,在sales和customers表之间的并行连接使用两组并行服务器进程.生产者将扫描两个表并应用所有谓词条件然后将结果发送给消费乾(lines 9-11和lines 5-7).可以很容易的识别生产者因为它们是在PX SEND操作(line 9 & 5)之下执行操作.消费者完成实际的哈希连接并将结果发送给QC(line 8和lines 2-4).消费者可以通过PX RECEIVE来识别因为在它们执行工作之先必须先执行PX RECEIVE操作(line 8 & 4).而它们总是通过PX SEND QC(line 2)

在执行计划中在TQ列显示了类似的信息,它显示了哪一组并行服务器进程被执行.在执行计划中Q100组并行服务器进程(生产者)将首先扫描customers表.它然后将结果发送给消费者(line 5)Q102,Q100组并行服务器进程然后变成Q101组并行服务器进程(另一组生产者),Q101扫描sales表然后发送给消费者(line 9),Q102组并行服务器进程(消费者)接收来自生产者(line 8 & 4),完成连接(line 3)然后将结果发送给查询协调器

Data redistribution
在这个例子中有两个大表customers和sales在连接中被调用.为了以并行来处理这个连接,在生产者和消费者之间重新分配行源是必要的.生产者将基于数据块范围来扫描表并应用谓词条件然后将结果发送给消费者.在执行计划中的IN-OUT和PQ Distrib两列中有关于数据在生产者和消费者之间数据是怎样重新分配的信息.PQ Distrib列最有用的列且有些情况下已经替代了IN-OUT列.

下面有五种最常用的数据重新分配方法
Hash:为了在并行服务器进程之间完成一个相等的重新分配哈希数据重新分配是非常常见.一个哈希函数被应用于连接列且结果指示了哪一个消费者并行服务器进程将接收这些行源.

Broadcast:广播重新分配发生在当连接操作中两个结果集中的一个比另一个小得多的时候.代替对两个结果集重新分配行记录.数据库为了保证单个并行服务器进程能够完成它的连接操作将小结果集发送给所有消费者并行服务器进程.

Range:范围重新分配通常用于并行排序操作.单个并行服务器进程以数据范围进行工作所以QC没有进行任何排序但只存在单个并行服务器进程的结果的顺序是正确的.

KEY:键值重新分配确保结果集中单个键值被集簇在一起.这是一种优化主要用于partial partition-wise joins来确保在连接中只有一边被重新分配.

Round robin:循环数据重新分配是在发送数据到请求进程之前最后的重新分配操作.当没有重新分配约束被请求时它也可以用于查询的早期阶段.

在RAC数据库中可以在重新分配方法中看到local后缀.在RAC中本地重新分配是为了对节点间的并行查询进行最小化互连通信.在这种情况下行只会被分配给与RAC相同节点的消费者.

在下面的图表中生产者使用一个hash重新分配方法将数据发送给消费者

你会注意到在执行计划中数据重新分配的位置IN-OUT列中的值有P->P(lines 5 & 9)或P->S(line 2).P->P意味着数据由一个并行操作发送给另一个并行操作.P->S意味着数据由一个并行操作发送给一个串行操作.在line 2这一行数据被发送给QC,它是一个单进程,因此是P->S.然而如果你看到一个P->S操作

结论
Oracle优化器的目的是用来判断查询的最有效执行计划的.它是基于查询数据的统计信息和oracle数据库功特性比如hash joins parallel query和partitioning来进行决策的.通过分析执行计划和评估四个关键因素:基数评估,访问路径,连接方法和连接顺序,能够判断一个执行计划是不是最有效的执行计划.

来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/26015009/viewspace-1132876/,如需转载,请注明出处,否则将追究法律责任。

转载于:http://blog.itpub.net/26015009/viewspace-1132876/

你可能感兴趣的:(oracle优化器之执行计划)