【IT168 服务器学院】第5章 ORACLE的执行计划
背景知识:
为了更好的进行下面的内容我们必须了解一些概念性的术语:
共享sql语句
为了不重复解析相同的SQL语句(因为解析操作比较费资源,会导致性能下降),在第一次解析之后,ORACLE将SQL语句及解析后得到的执行计划存放在内存中。这块位于系统全局区域SGA(system global area)的共享池(shared buffer pool)中的内存可以被所有的数据库用户共享。因此,当你执行一个SQL语句(有时被称为一个游标)时,如果该语句和之前的执行过的某一语句完全相同,并且之前执行的该语句与其执行计划仍然在内存中存在,则ORACLE就不需要再进行分析,直接得到该语句的执行路径。ORACLE的这个功能大大地提高了SQL的执行性能并大大节省了内存的使用。使用这个功能的关键是将执行过的语句尽可能放到内存中,所以这要求有大的共享池(通过设置shared buffer pool参数值)和尽可能的使用绑定变量的方法执行SQL语句。
当你向ORACLE 提交一个SQL语句,ORACLE会首先在共享内存中查找是否有相同的语句。这里需要注明的是,ORACLE对两者采取的是一种严格匹配,要达成共享,SQL语句必须完全相同(包括空格,换行等)。
下面是判断SQL语句是否与共享内存中某一SQL相同的步骤:
1). 对所发出语句的文本串进行hashed。如果hash值与已在共享池中SQL语句的hash值相同,则进行第2步:
2) 将所发出语句的文本串(包括大小写、空白和注释)与在第1步中识别的所有
已存在的SQL语句相比较。
例如:
SELECT * FROM emp WHERE empno = 1000;
和下列每一个都不同
SELECT * from emp WHERE empno = 1000;
SELECT * FROM EMP WHERE empno = 1000;
SELECT * FROM emp WHERE empno = 2000;
在上面的语句中列值都是直接SQL语句中的,今后我们将这类sql成为硬编码SQL或字面值SQL
使用绑定变量的SQL语句中必须使用相同的名字的绑定变量(bind variables) ,
例如:
a. 该2个sql语句被认为相同
select pin , name from people where pin = :blk1.pin;
select pin , name from people where pin = :blk1.pin;
b. 该2个sql语句被认为不相同
select pin , name from people where pin = :blk1.ot_ind;
select pin , name from people where pin = :blk1.ov_ind;
今后我们将上面的这类语句称为绑定变量SQL。
3). 将所发出语句中涉及的对象与第2步中识别的已存在语句所涉及对象相比较。
例如:
如用户user1与用户user2下都有EMP表,则
用户user1发出的语句:SELECT * FROM EMP; 与
用户user2发出的语句:SELECT * FROM EMP; 被认为是不相同的语句,
因为两个语句中引用的EMP不是指同一个表。
4). 在SQL语句中使用的捆绑变量的捆绑类型必须一致。
如果语句与当前在共享池中的另一个语句是等同的话,Oracle并不对它进行语法分析。而直接执行该语句,提高了执行效率,因为语法分析比较耗费资源。
注意的是,从oracle 8i开始,新引入了一个CURSOR_SHARING参数,该参数的主要目的就是为了解决在编程过程中已大量使用的硬编码SQL问题。因为在实际开发中,很多程序人员为了提高开发速度,而采用类似下面的开发方法:
str_sql string;
int_empno int;
int_empno = 2000;
str_sql = ‘SELECT * FROM emp WHERE empno = ‘ + int_empno;
…………
int_empno = 1000;
str_sql = ‘SELECT * FROM emp WHERE empno = ‘ + int_empno;
上面的代码实际上使用了硬编码SQL,使我们不能使用共享SQL的功能,结果是数据库效率不高。但是从上面的2个语句来看,产生的硬编码SQL只是列值不同,其它部分都是相同的,如果仅仅因为列值不同而导致这2个语句不能共享是很可惜的,为了解决这个问题,引入了CURSOR_SHARING参数,使这类问题也可以使用共享SQL,从而使这样的开发也可以利用共享SQL功能。听起来不错,ORACLE真为用户着想,使用户在不改变代码的情况下还可以利用共享SQL的功能。真的如此吗?天上不会无缘无故的掉一个馅饼的,ORACLE对该参数的使用做了说明,建议在经过实际测试后再改该参数的值(缺省情况下,该参数的值为EXACT,语句完全一致才使用共享SQL)。因为有可能该变该值后,你的硬编码SQL是可以使用共享SQL了,但数据库的性能反而会下降。 我在实际应用中已经遇到这种情况。所以建议编写需要稳定运行程序的开发人员最好还是一开始就使用绑定变量的SQL。
Rowid的概念:
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。
Recursive SQL概念
有时为了执行用户发出的一个sql语句,Oracle必须执行一些额外的语句,我们将这些额外的语句称之为''recursive calls''或''recursive SQL statements''。如当一个DDL语句发出后,ORACLE总是隐含的发出一些recursive SQL语句,来修改数据字典信息,以便用户可以成功的执行该DDL语句。当需要的数据字典信息没有在共享内存中时,经常会发生Recursive calls,这些Recursive calls会将数据字典信息从硬盘读入内存中。用户不比关心这些recursive SQL语句的执行情况,在需要的时候,ORACLE会自动的在内部执行这些语句。当然DML语句与SELECT都可能引起recursive SQL。简单的说,我们可以将触发器视为recursive SQL。
Row Source(行源)
用在查询中,由上一操作返回的符合条件的行的集合,即可以是表的全部行数据的集合;也可以是表的部分行数据的集合;也可以为对上2个row source进行连接操作(如join连接)后得到的行数据集合。
Predicate(谓词)
一个查询中的WHERE限制条件
Driving Table(驱动表)
该表又称为外层表(OUTER TABLE)。这个概念用于嵌套与HASH连接中。如果该row source返回较多的行数据,则对所有的后续操作有负面影响。注意此处虽然翻译为驱动表,但实际上翻译为驱动行源(driving row source)更为确切。一般说来,是应用查询的限制条件后,返回较少行源的表作为驱动表,所以如果一个大表在WHERE条件有有限制条件(如等值限制),则该大表作为驱动表也是合适的,所以并不是只有较小的表可以作为驱动表,正确说法应该为应用查询的限制条件后,返回较少行源的表作为驱动表。在执行计划中,应该为靠上的那个row source,后面会给出具体说明。在我们后面的描述中,一般将该表称为连接操作的row source 1。
Probed Table(被探查表)
该表又称为内层表(INNER TABLE)。在我们从驱动表中得到具体一行的数据后,在该表中寻找符合连接条件的行。所以该表应当为大表(实际上应该为返回较大row source的表)且相应的列上应该有索引。在我们后面的描述中,一般将该表称为连接操作的row source 2。
组合索引(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来说,就象财务报表对于财务人员一样重要。所以我们面临的问题主要是:如何得到执行计划;如何分析执行计划,从而找出影响性能的主要问题。下面先从分析树型执行计划开始介绍,然后介绍如何得到执行计划,再介绍如何分析执行计划。
举例:这个例子显示关于下面SQL语句的执行计划。
SELECT ename, job, sal, dname
FROM emp, dept
WHERE emp.deptno = derpt.deptno
AND NOT EXISTS
( SELECT *
FROM salgrade
WHERE emp.sal BETWEEN losal AND hisal );
此语句查询薪水不在任何建议薪水范围内的所有雇员的名字,工作,薪水和部门名。下图5-1显示了一个执行计划的图形表示:
执行计划的步骤
执行计划的每一步返回一组行,它们或者为下一步所使用,或者在最后一步时返回给发出SQL语句的用户或应用。由每一步返回的一组行叫做行源(row source)。图5-1树状图显示了从一步到另一步行数据的流动情况。每步的编号反映了在你观察执行计划时所示步骤的顺序(如何观察执行计划将被简短地说明)。一般来说这并不是每一步被执行的先后顺序。执行计划的每一步或者从数据库中检索行,或者接收来自一个或多个行源的行数据作为输入:由红色字框指出的步骤从数据库中的数据文件中物理检索数据。这种步骤被称之为存取路径,后面会详细介绍在Oracle可以使用的存取路径:
l 第3步和第6步分别的从EMP表和SALGRADE表读所有的行。
l 第5步在PK_DEPTNO索引中查找由步骤3返回的每个DEPTNO值。它找出与DEPT表中相关联的那些行的ROWID。
l 第4步从DEPT表中检索出ROWID为第5步返回的那些行。
由黑色字框指出的步骤在行源上操作,如做2表之间的关联,排序,或过滤等操作,后面也会给出详细的介绍:
l 第2步实现嵌套的循环操作(相当于C语句中的嵌套循环),接收从第3步和第4步来的行源,把来自第3步源的每一行与它第4步中相应的行连接在一起,返回结果行到第1步。
l 第1步完成一个过滤器操作。它接收来自第2步和第6步的行源,消除掉第2步中来的,在第6步有相应行的那些行,并将来自第2步的剩下的行返回给发出语句的用户或应用。
实现执行计划步骤的顺序
执行计划中的步骤不是按照它们编号的顺序来实现的:Oracle首先实现图5-1树结构图形里作为叶子出现的那些步骤(例如步骤3、5、6)。由每一步返回的行称为它下一步骤的行源。然后Oracle实现父步骤。
举例来说,为了执行图5-1中的语句,Oracle以下列顺序实现这些步骤:
l 首先,Oracle实现步骤3,并一行一行地将结果行返回给第2步。
l 对第3步返回的每一行,Oracle实现这些步骤:
-- Oracle实现步骤5,并将结果ROWID返回给第4步。
-- Oracle实现步骤4,并将结果行返回给第2步。
-- Oracle实现步骤2,将接受来自第3步的一行和来自第4步的一行,并返回给第1步一行。
-- Oracle实现步骤6,如果有结果行的话,将它返回给第1步。
-- Oracle实现步骤1,如果从步骤6返回行,Oracle将来自第2步的行返回给发出SQL语句的用户。
注意Oracle对由第3步返回的每一行实现步骤5,4,2,6一次。许多父步骤在它们能执行之前只需要来自它们子步骤的单一行。对这样的父步骤来说,只要从子步骤已返回单一行时立即实现父步骤(可能还有执行计划的其余部分)。如果该父步骤的父步骤同样可以通过单一行返回激活的话,那么它也同样被执行。所以,执行可以在树上串联上去,可能包含执行计划的余下部分。对于这样的操作,可以使用first_rows作为优化目标以便于实现快速响应用户的请求。
对每个由子步骤依次检索出来的每一行,Oracle就实现父步骤及所有串联在一起的步骤一次。对由子步骤返回的每一行所触发的父步骤包括表存取,索引存取,嵌套的循环连接和过滤器。
有些父步骤在它们被实现之前需要来自子步骤的所有行。对这样的父步骤,直到所有行从子步骤返回之前Oracle不能实现该父步骤。这样的父步骤包括排序,排序一合并的连接,组功能和总计。对于这样的操作,不能使用first_rows作为优化目标,而可以用all_rows作为优化目标,使该中类型的操作耗费的资源最少。
有时语句执行时,并不是象上面说的那样一步一步有先有后的进行,而是可能并行运行,如在实际环境中,3、5、4步可能并行运行,以便取得更好的效率。从上面的树型图上,是很难看出各个操作执行的先后顺序,而通过ORACLE生成的另一种形式的执行计划,则可以很容易的看出哪个操作先执行,哪个后执行,这样的执行计划是我们真正需要的,后面会给出详细说明。现在先来看一些预备知识。
访问路径(方法) -- access path
优化器在形成执行计划时需要做的一个重要选择是如何从数据库查询出需要的数据。对于SQL语句存取的任何表中的任何行,可能存在许多存取路径(存取方法),通过它们可以定位和查询出需要的数据。优化器选择其中自认为是最优化的路径。
在物理层,oracle读取数据,一次读取的最小单位为数据库块(由多个连续的操作系统块组成),一次读取的最大值由操作系统一次I/O的最大值与multiblock参数共同决定,所以即使只需要一行数据,也是将该行所在的数据库块读入内存。逻辑上,oracle用如下存取方法访问数据:
1) 全表扫描(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。幸运的是oracle 10G后,可以人工收缩HWM的值。
由FTS模式读入的数据被放到高速缓存的Least Recently Used (LRU)列表的尾部,这样可以使其快速交换出内存,从而不使内存重要的数据被交换出内存。使用FTS的前提条件:在较大的表上不建议使用全表扫描,除非取出数据的比较多,超过总量的5% -- 10%,或你想使用并行查询功能时。
使用全表扫描的例子:
~~~~~~~~~~~~~~~~~~~~~~~~
SQL> explain plan for select * from dual;
Query Plan
-----------------------------------------
SELECT STATEMENT [CHOOSE] Cost=
TABLE ACCESS FULL DUAL
2) 通过ROWID的表存取(Table Access by ROWID或rowid lookup)
行的ROWID指出了该行所在的数据文件、数据块以及行在该块中的位置,所以通过ROWID来存取数据可以快速定位到目标数据上,是Oracle存取单行数据的最快方法。为了通过ROWID存取表,Oracle 首先要获取被选择行的ROWID,或者从语句的WHERE子句中得到,或者通过表的一个或多个索引的索引扫描得到。Oracle然后以得到的ROWID为依据定位每个被选择的行。
这种存取方法不会用到多块读操作,一次I/O只能读取一个数据块。我们会经常在执行计划中看到该存取方法,如通过索引查询数据。
使用ROWID存取的方法:
SQL> explain plan for select * from dept where rowid = ''AAAAyGAADAAAAATAAF'';
Query Plan
------------------------------------
SELECT STATEMENT [CHOOSE] Cost=1
TABLE ACCESS BY ROWID DEPT [ANALYZED]
3)索引扫描(Index Scan或index lookup)
我们先通过index查找到数据对应的rowid值(对于非唯一索引可能返回多个rowid值),然后根据rowid直接从表中得到具体的数据,这种查找方式称为索引扫描或索引查找(index lookup)。一个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%,使用索引扫描会效率下降很多。
如下列所示:
SQL> explain plan for select empno, ename from emp where empno=10;
Query Plan
------------------------------------
SELECT STATEMENT [CHOOSE] Cost=1
TABLE ACCESS BY ROWID EMP [ANALYZED]
INDEX UNIQUE SCAN EMP_I1
注意TABLE ACCESS BY ROWID EMP部分,这表明这不是通过FTS存取路径访问数据,而是通过rowid lookup存取路径访问数据的。在此例中,所需要的rowid是由于在索引查找empno列的值得到的,这种方式是INDEX UNIQUE SCAN查找,后面给予介绍,EMP_I1为使用的进行索引查找的索引名字。
但是如果查询的数据能全在索引中找到,就可以避免进行第2步操作,避免了不必要的I/O,此时即使通过索引扫描取出的数据比较多,效率还是很高的,因为这只会在索引中读取。所以上面我在介绍基于规则的优化器时,使用了select count(id) from SWD_BILLDETAIL where cn <''6'',而没有使用select count(cn) from SWD_BILLDETAIL where cn <''6''。因为在实际情况中,只查询被索引列的值的情况极为少,所以,如果我在查询中使用count(cn),则不具有代表性。
SQL> explain plan for select empno from emp where empno=10; -- 只查询empno列值
Query Plan
------------------------------------
SELECT STATEMENT [CHOOSE] Cost=1
INDEX UNIQUE SCAN EMP_I1
进一步讲,如果sql语句中对索引列进行排序,因为索引已经预先排序好了,所以在执行计划中不需要再对索引列进行排序
SQL> explain plan for select empno, ename from emp
where empno > 7876 order by empno;
Query Plan
--------------------------------------------------------------------------------
SELECT STATEMENT [CHOOSE] Cost=1
TABLE ACCESS BY ROWID EMP [ANALYZED]
INDEX RANGE SCAN EMP_I1 [ANALYZED]
从这个例子中可以看到:因为索引是已经排序了的,所以将按照索引的顺序查询出符合条件的行,因此避免了进一步排序操作。