Oracle 执行计划介绍

优化定义:
    是选择最有效的执行计划来执行SQL语句的过程,这是在处理任何数据的语句(SELECT,INSERT,UPDATE或DELETE)中的一个重要步骤。对Oracle来说,执行这样的语句有许多不同的方法,譬如说,将随着以什么顺序访问哪些表或索引的不同而不同。所使用的执行计划可以决定语句能执行得有多快。Oracle中称之为优化器(Optimizer)的组件用来选择这种它认为最有效的执行计划。

概念分析:
共享SQL语句:
    为了不重复解析相同的SQL语句(因为解析操作比较费资源,会导致性能下降),在第一次解析之后,ORACLE将SQL语句及解析后得到的执行计划存放在内存中。这块位于系统全局区域SGA(system global area)的共享池(shared buffer pool)中的内存可以被所有的数据库用户共享。因此,当你执行一个SQL语句(有时被称为一个游标)时,如果该语句和之前的执行过的某一语句完全相同,并且之前执行的该语句与其执行计划仍然在内存中存在,则ORACLE就不需要再进行分析,直接得到该语句的执行路径。ORACLE的这个功能大大地提高了SQL的执行性能并大大节省了内存的使用。使用这个功能的关键是将执行过的语句尽可能放到内存中,所以这要求有大的共享池(通过设置shared buffer pool参数值)和尽可能的使用绑定变量的方法执行SQL语句。

Rowid的概念:
    rowid是一个伪列,既然是伪列,那么这个列就不是用户定义,而是系统自己给加上的。对每个表都有一个rowid的伪列,但是表中并不物理存储ROWID列的值。不过你可以像使用其它列那样使用它,但是不能删除该列,也不能对该列的值进行修改、插入。一旦一行数据插入数据库,则rowid在该行的生命周期内是唯一的,即即使该行产生行迁移,行的rowid也不会改变。
    rowid对访问一个表中的给定的行提供了最快的访问方法,通过ROWID可以直接定位到相应的数据块上,然后将其读到内存。我们创建一个索引时,该索引不但存储索引列的值,而且也存储索引值所对应的行的ROWID,这样我们通过索引快速找到相应行的ROWID后,通过该ROWID,就可以迅速将数据查询出来。这也就是我们使用索引查询时,速度比较快的原因。
    在ORACLE8以前的版本中,ROWID由FILE 、BLOCK、ROW NUMBER构成。随着oracle8中对象概念的扩展,ROWID发生了变化,ROWID由OBJECT、FILE、BLOCK、ROW NUMBER构成。利用DBMS_ROWID可以将rowid分解成上述的各部分,也可以将上述的各部分组成一个有效的rowid。

Row Source(行源) :
    用在查询中,由上一操作返回的符合条件的行的集合,即可以是表的全部行数据的集合;也可以是表的部分行数据的集合;也可以为对上2个row source进行连接操作(如join连接)后得到的行数据集合。

Predicate(谓词) :
    一个查询中的WHERE限制条件

Driving Table(驱动表):
    又称为外层表(OUTER TABLE)。这个概念用于嵌套与HASH连接中。如果该row source返回较多的行数据,则对所有的后续操作有负面影响。一般说来,是应用查询的限制条件后,返回较少行源的表作为驱动表,所以如果一个大表在WHERE条件有限制条件(如等值限制),则该大表作为驱动表也是合适的,所以并不是只有较小的表可以作为驱动表,正确说法应该为应用查询的限制条件后,返回较少行源的表作为驱动表。

Probed Table(被探查表):
    又称为内层表(INNER TABLE)。在我们从驱动表中得到具体一行的数据后,在该表中寻找符合连接条件的行。所以该表应当为大表(实际上应该为返回较大row source的表)且相应的列上应该有索引。

组合索引(concatenated index):
    由多个列构成的索引,如create index idx_emp on emp(col1, col2, col3, ……),则我们称idx_emp索引为组合索引。在组合索引中有一个重要的概念:引导列(leading column),在上面的例子中,col1列为引导列。当我们进行查询时可以使用”where col1 = ? ”,也可以使用”where col1 = ? and col2 = ?”,这样的限制条件都会使用索引,但是”where col2 = ? ”查询就不会使用该索引。所以限制条件中包含先导列时,该限制条件才会使用该组合索引。

可选择性(selectivity):
    比较一下列中唯一键的数量和表中的行数,就可以判断该列的可选择性。如果该列的”唯一键的数量/表中的行数”的比值越接近1,则该列的可选择性越高,该列就越适合创建索引,同样索引的可选择性也越高。在可选择性高的列上进行查询时,返回的数据就较少,比较适合使用索引查询。

执行计划:
    为了执行语句,Oracle可能必须实现许多步骤。这些步骤中的每一步可能是从数据库中物理检索数据行,或者用某种方法准备数据行,供发出语句的用户使用。Oracle用来执行语句的这些步骤的组合被称之为执行计划。
    执行计划是SQL优化中最为复杂也是最为关键的部分,只有知道了ORACLE在内部到底是如何执行该SQL语句后,才能知道优化器选择的执行计划是否为最优的。执行计划对于DBA来说,就象财务报表对于财务人员一样重要。所以我们面临的问题主要是:
    1. 如何得到执行计划;
    2. 如何分析执行计划;
从而找出影响性能的主要问题。

访问路径(access path):
    优化器在形成执行计划时需要做的一个重要选择是如何从数据库查询出需要的数据。对于SQL语句存取的任何表中的任何行,可能存在许多存取路径(存取方法),通过它们可以定位和查询出需要的数据。优化器选择其中自认为是最优化的路径。
    在物理层,oracle读取数据,一次读取的最小单位为数据库块(由多个连续的操作系统块组成),一次读取的最大值由操作系统一次I/O的最大值与multiblock参数共同决定,所以即使只需要一行数据,也是将该行所在的数据库块读入内存。
    逻辑上,oracle用如下存取方法访问数据:
1   全表扫描(Full Table Scans, FTS) 
2   通过ROWID的表存取(Table Access by ROWID或rowid lookup)
3   索引扫描(Index Scan或index lookup)

全表扫描(Full Table Scans, FTS):
    为实现全表扫描,Oracle读取表中所有的行,并检查每一行是否满足语句的WHERE限制条件。Oracle顺序读取分配给表的每个数据块,直到读到表的最高水线处(high water mark, HWM,标识表的最后一个数据块)。一个多块读操作可以使一次I/O能读取多块数据块(db_block_multiblock_read_count参数设定),而非只读取一个数据块,这极大的减少了I/O总次数,提高了系统的吞吐量,所以利用多块读的方法可以高效实现全表扫描,而且只有在全表扫描的情况下才能使用多块读操作。在这种访问模式下,每个数据块只被读一次。由于HWM标识最后一块被读入的数据,而delete操作不影响HWM值,所以一个表的所有数据被delete后,其全表扫描的时间不会有改善,一般我们需要使用truncate命令来使HWM值归为0。
    由FTS模式读入的数据被放到高速缓存的Least Recently Used (LRU)列表的尾部,这样可以使其快速交换出内存,从而不使内存重要的数据被交换出内存。
    使用FTS的前提:在较大的表上不建议使用全表扫描,除非取出数据的比较多,超过总量的5% -- 10%,或你想使用并行查询功能时。

通过ROWID的表存取(Table Access by ROWID或rowid lookup):
    行的ROWID指出了该行所在的数据文件、数据块以及行在该块中的位置,所以通过ROWID来存取数据可以快速定位到目标数据上,是Oracle存取单行数据的最快方法。
    为通过ROWID存取表,Oracle 首先要获取被选择行的ROWID,或从语句的WHERE子句中得到,或者通过表的一个或多个索引的索引扫描得到。Oracle然后以得到的ROWID为依据定位每个被选择的行。
    此存取方法不会用到多块读操作,一次I/O只能读取一个数据块。我们经常在执行计划中看到该存取方法,如通过索引查询数据。

索引扫描(Index Scan或index lookup):
    通过index查找到数据对应的rowid值(对于非唯一索引可能返回多个rowid值),然后根据rowid直接从表中得到具体的数据。
    一个rowid唯一的表示一行数据,该行对应的数据块是通过一次i/o得到的,该次i/o只会读取一个数据库块。
    在索引中,除了存储每个索引的值外,索引还存储具有此值的行对应的ROWID值。索引扫由2步组成:
(1) 扫描索引得到对应的rowid值。
(2) 通过找到的rowid从表中读出具体的数据。

    每步都是单独的一次I/O,但对于索引,由于经常使用,绝大多数都已经CACHE到内存中,所以第1步的I/O经常是逻辑I/O,即数据可以从内存中得到。但是对于第2步来说,如果表比较大,则其数据不可能全在内存中,则其I/O很有可能是物理I/O,这是一个机械操作,相对逻辑I/O来说,极其费时间。所以如果对大表进行索引扫描,取出的数据如果大于总量的5% -- 10%,使用索引扫描会效率下降很多。


产生执行计划:
要为一个语句生成执行计划,可以有2种方法:
1).最简单的办法
    Sql> set autotrace on
   Sql> select * from dual;

    执行完语句后,会显示explain plan 与 统计信息。
    这个语句的优点就是它的缺点,这样在用该方法查看执行时间较长的sql语句时,需要等待该语句执行成功后,才返回执行计划,使优化的周期大大增长。
    如果不想执行语句而只是想得到执行计划可以采用:
    Sql> set autotrace traceonly
    这样,就只会列出执行计划,而不会真正的执行语句,大大减少了优化时间。虽然也列出了统计信息,但是因为没有执行语句,所以该统计信息没有用处。
    如果执行该语句时遇到错误,解决方法为:
    a. 在要分析的用户下:
    Sqlplus > @ ?\rdbms\admin\utlxplan.sql
    b. 用sys用户登陆
    Sqlplus > @ ?\sqlplus\admin\plustrce.sql
   Sqlplus > grant plustrace to user_name; -- user_name是上面所说的分析用户

2).用explain plan命令
     (1)  sqlplus > @ ?\rdbms\admin\utlxplan.sql
    (2)  sqlplus > explain plan set statement_id =’???’ for select    
      ………………

注意:
    此方法时,并不执行sql语句,所以只会列出执行计划,不会列出统计信息,并且执行计划只存在plan_table中。所以该语句比起set autotrace traceonly可用性要差。

    以上2种方法只能为在本会话中正在运行的语句产生执行计划,即我们需要已经知道了哪条语句运行的效率很差,我们是有目的只对这条SQL语句去优化。在很多情况下,我们只会听一个客户抱怨说现在系统运行很慢,而我们不知道是哪个SQL引起的。此时有许多现成的语句可以找出耗费资源比较多的语句,如:
SELECT ADDRESS,
substr(SQL_TEXT,1,20) Text,
buffer_gets,
executions,
buffer_gets/executions AVG
FROM v$sqlarea
WHERE executions>0
AND buffer_gets > 100000
ORDER BY 5;
    从而对找出的语句进行进一步优化。还可以为一个正在运行的会话中运行的所有SQL语句生成执行计划,这需要对该会话进行跟踪,产生trace文件,然后对该文件用tkprof程序格式化一下,这种得到执行计划的方式很有用,因为它包含其它额外信息,如SQL语句执行的每个阶段(如Parse、Execute、Fetch)分别耗费的各个资源情况(如CPU、DISK、elapsed等)。

分析执行计划:
    Query Plan    -----------------------------------------
    SELECT STATEMENT Optimizer=CHOOSE (Cost=1234 Card=1 Bytes=14)
    TABLE ACCESS FULL LARGE_TABLE [:Q65001] [ANALYZED]
    在这个例子中,TABLE ACCESS FULL LARGE_TABLE是第一个操作,意思是在LARGE_TABLE表上做全表扫描。当这个操作完成之后,产生的row source中的数据被送往下一步骤进行处理,在此例中,SELECT STATEMENT 操作是这个查询语句的最后一步。Optimizer=CHOOSE  指明这个查询的optimizer_mode,即optimizer_mode 初始化参数指定的值,它并不是指语句执行时真的使用了该优化器。决定该语句使用何种优化器的唯一方法是看后面的cost部分。如果给出的是下面的形式,则表明使用的是CBO优化器,此
处的cost表示优化器认为该执行计划的代价:
SELECT STATEMENT Optimizer=CHOOSE (Cost=1234 Card=1 Bytes=14)
    假如执行计划中给出的是类似下面的信息,则表明是使用RBO优化器,因为cost部分的值为空,或者压根就没有cost部分。
      SELECT STATEMENT Optimizer=CHOOSE Cost=
     SELECT STATEMENT Optimizer=CHOOSE
     这样我们从Optimizer后面的信息中可以得出执行该语句时到底用了什么样的优化器。特别的,如果Optimizer=ALL_ROWS|FIRST_ROWS| FIRST_ROWS_n,则使用的是CBO优化器;如果Optimizer=RULE,则使用的是RBO优化器。

    cost属性的值是一个在oracle内部用来比较各个执行计划所耗费代价的值,从而使优化器可以选择最好的执行计划。不同语句的cost值不具有可比性,只能对同一个语句的不同执行计划的cost值进行比较。
    [:Q65001] 表明该部分查询是以并行方式运行的。里面的数据表示这个操作是由并行查询的一个slave进程处理的,以便该操作可以区别于串行执行的操作。
    [ANALYZED] 表明操作中引用的对象被分析过了,在数据字典中有该对象的统计信息可以供CBO使用。

通过执行计划判断驱动表:
    在执行计划中,需要知道哪个操作是先执行的,哪个操作是后执行的,这对于判断哪个表为驱动表有用处。判断之前,如果对表的访问是通过rowid,且该rowid的值是从索引扫描中来得,则将该索引扫描先从执行计划中暂时去掉。然后在执行计划剩下的部分中, 判断执行顺序的指导原则就是:最右、最上的操作先执行。具体解释如下:
    得到去除妨碍判断的索引扫描后的执行计划:
Execution Plan
----------------------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE
1 0    MERGE JOIN
2 1       SORT (JOIN)
3 2 NESTED LOOPS
4 3     TABLE ACCESS (FULL) OF 'B'
5 3     TABLE ACCESS (BY INDEX ROWID) OF 'A'
7 1       SORT (JOIN)
8 7 TABLE ACCESS (FULL) OF 'C'

    谈论上下关系时,只对连续的、缩进一致的行有效。从上图中我们可以看到,对于NESTED LOOPS部分,最右、最上的操作是TABLE ACCESS (FULL) OF 'B',所以这一操作先执行,所以该操作对应的B表为第一个驱动表(外部表),自然,A表就为内部表了。从图中还可以看出,B与A表做嵌套循环后生成了新的row source ,对该row source进行来排序后,与C表对应的排序了的row source进行排序--合并连接操作。
    所以从上面可以得出如下事实:B表先与A表做嵌套循环,然后将生成的row source与C表做排序—合并连接。

    看执行计划时,我们的关键不是看哪个操作先执行,哪个操作后执行,而是关键看表之间连接的顺序、使用了何种类型的关联及具体的存取路径(如判断是否利用了索引),在从执行计划中判断出哪个表为驱动表后,根据我们的知识判断该表作为驱动表是否合适,如果不合适,对SQL语句进行更改,使优化器可以选择正确的驱动表。

猜你喜欢

转载自maosheng.iteye.com/blog/1922419