Cost Based Optimizer - Common Misconceptions and Issues
基于成本的优化器 —— 一般错误概念和问题
Introduction 介绍
~~~~~~~~~~~~
本短文着意于消除一些关于基于成本的优化器(CBO)的错误说法,强调一般的错误和问题。
Background 背景
~~~~~~~~~~
为了执行任何一个SQL语句,Oracle都要先导出一个“执行计划(execution plan)”。它基本上就是Oracle如何检索出符合给定SQL语句要求的数据的执行计划。
Oracle7和Oracle8 都有两种可以为SQL语句导出执行计划的优化器:
- 基于规则的优化器(RBO)
继承自Oracle6,它使用一系列严格的规则来决定每个SQL语句的执行计划。如果你知道这些规则,你可以构造一个SQL查询使其以指定的方式访问数据。表的内容对于执行计划没有影响。
这个优化器已经不再被增强了,所以不能使用很多oracle8的特性。
- 基于成本的优化器(CBO)
从Oracle7开始引入,该优化器尝试找到最低成本的访问数据的方法,为了最大的吞吐量或最快的初始响应时间。计算使用不同的执行计划的成本,并选择成本最低的一个。关于表的数据内容的统计被用于确定执行计划。
Fundamental Points 基本点
~~~~~~~~~~~~~~~~~~
对于每个SQL语句,都有很多可能的执行计划。
“最佳计划”永远是“最佳计划”,无论它如何到达。
最佳计划可以由两个意思:
1 此计划使用最小的资源来处理此语句涉及到的所有行。 [叫做ALL_ROWS]
2 此计划以最短的时间返回这个语句的第一行 。 [叫做FIRST_ROWS]
CBO不理解应用的相关特性,也不能完全理解关联表之间的复杂关系的影响。仅有有限的信息可以用来确定最佳计划。
CBO通过计算不同执行方案的估计成本来确定最佳计划,并选用最低成本的计划。因为这个关系到相关成本的假设,所选的计划不一定是真的最好的计划。这种情况经常被当作BUG报告给oracle 技术支持,因为 CBO没有为一个指定方案选择一个最佳的计划。人们通常可以证实因为给定的输入统计试有效的并且缺省的“成本”被牵扯进来。所选中的计划被计算成最佳计划,虽然它不是。无论CBO如何改进提高,总也会有所选的计划不是最优的这种情况。所以,你必须经常地准备优化语句。
RBO的功能已经不再增强。这就意味着一些执行计划只对CBO有效。然而,RBO还将在Oracle 8中继续存在。
Before you Continue 在你继续之前
~~~~~~~~~~~~~~~~~~~
不建议你在Oracle releases 7.0.X中使用CBO.
本文中的信息适用于Oracle releases 7.1 以上(包括Oracle 8.0)。
Base Statistics 基础统计
~~~~~~~~~~~~~~~
为了要给CBO最多的信息(有机会选择好的执行计划),你必须对所有将被查询的表做ANALYZE。
带有ESTIMATE选项的ANALYZE操作对于一些表能够产生不正确的结果,尤其是那些取样较小的表。这不是个BUG,而是每个统计取样方法的特性。如果所选取样不能代表整个数据集,你就不能期待产生正确的统计。
在Oracle 7.1 和7.2 中,列的值被假定为是均匀分布的。这是在这些版本中的一个重要的限制,完全和精确的统计也不能指出实际数据的分布情况。这一限制在Oracle release 7.3 以上版本被部分解决了,能够保存列值的分布信息 - 但是这些额外的信息可能对某些类型的查询没有实际的帮助,请看后面的章节中关于Bind Variables 的注意事项。
在考虑使用ANALYZE时,要考虑如下的重要问题:
- 对一个带索引的表的ANALYZE,将分析其相关索引。 (在Oracle 7.3 中可能值分析表而不分析索引。)
- 如果你对一个表进行ANALYZE ... ESTIMATE 分析,那么然后在其相关索引上做ANALYZE COMPUTE分析是很明智的。这样可以确保被索引字段的统计是准确的。
- 分析索引不用到临时表空间
- 如果分析一个索引而不分析其基表,在这一单一基础上CBO不会被选中。
- 如果你需要使用ESTIMATE- 估计(例如,由于时间的限制),建议你在几个不同的取样大小上进行 ANALYZE ... ESTIMATE, 来确定每个对象的理想的取样大小。总的目标是找到一个能在最短的时间内产生准确的统计的取样大小。较好的开始点是 10% - 15%。
- 进行超过50%的ANALYZE ... ESTIMATE 就会导致/变成ANALYZE ... COMPUTE。
- 7.1.6 以前的版本在进行ANALYZE ... ESTIMATE 时,会有ORA 600 错。
- 不要分析数据字典表(SYS表)除非你有足够充分的理由。关于这一点有和一些文档或README文件相矛盾,他们说DBMS_UTILITY.ANALYZE_SCHEMA 可以用于分析SYS表。虽然DBMS_UTILITY.ANALYZE_SCHEMA 可以分析SYS用户,但Oracle没有对这些被分析的表进行衰退测试,可能会造成死锁或效率问题。
Optimizer Goal / Mode 优化目标和模式
~~~~~~~~~~~~~~~~~~~~~
采用什么样的优化器和其操作方式是由下面的因素决定的:
Object Type 对象类型
- 某些对象类型是基于规则的优化所不知道的。例如:索引表(IOT)RBO根本不认识,在牵扯IOT的查询中将自动使用CBO.
Parallel Degree > 1 on a table 表上的并行度大于1
- 如果查询中的某个表的并行度大于一,CBO都将被采用而不管提示、OPTIMIZER_MODE或OPTIMIZER_GOAL的值是否为"RULE"。适用于Oracle 7.3 以上。
- 在Oracle 8.0.5 和 Oracle 8.1.5 releases 中如果任何索引的并行度超过1,也将采用CBO。仅适用于Oracle 8.0.5和Oracle 8.1.5。
Hints 提示
- 除了RULE之外的任何提示都会导致使用CBO。HINT不能被任何参数关掉,这一点非常重要。
Session level会话级 OPTIMIZER_GOAL
- 如果没有给定以上的条件,优化器的选用由会话级的参数OPTIMIZER_GOAL决定。如果上面的一个条件给定了,OPTIMIZER_GOAL就不起作用了。
如果OPTIMIZER_GOAL设为RULE,将采用RBO,而不管任何表的统计。
如果OPTIMIZER_GOAL设为CHOOSE,对于只要有一个表被分析过的查询,都将选用ALL_ROWS 。
Init.Ora
OPTIMIZER_MODE 参数
- 会话级的OPTIMIZER_GOAL参数的缺省设置是init.ora文件中的OPTIMIZER_MODE的值。
PL/SQL 块(包括匿名块和存储过程)应使用显式的提示(hint)来确定实际的优化方法。没有指定提示、并行的或“CBO-only”的对象的情况下,PL/SQL 块中的SQL语句采用的优化器,见下:
INIT.ORA OPTIMIZER_MODE |
Mode used in PLSQL |
RULE |
RULE |
CHOOSE |
ALL_ROWS |
ALL_ROWS |
ALL_ROWS |
FIRST_ROWS |
ALL_ROWS |
Summary Optimizer Mode: 优化模式的总结
~~~~~~~~~~~~~~~~~~~~~~~
对于以上的文章使我们清楚的确定采用何种优化器的一些事情,总结如下:
Description |
Table Statistics |
Mode Used |
Non-RBO Object(Eg:IOT) |
n/a |
#1 |
Parallelism > 1 |
n/a |
#1 |
RULE hint |
n/a |
RULE |
ALL_ROWS hint |
n/a |
ALL_ROWS |
FIRST_ROWS hint |
n/a |
FIRST_ROWS |
*Other Hint |
n/a |
#1 |
OPTIMIZER_GOAL=RULE |
n/a |
RULE |
OPTIMIZER_GOAL=ALL_ROWS |
n/a |
ALL_ROWS |
OPTIMIZER_GOAL=FIRST_ROWS |
n/a |
FIRST_ROWS |
OPTIMIZER_GOAL=CHOOSE |
NO |
RULE |
OPTIMIZER_GOAL=CHOOSE |
YES |
ALL_ROWS |
#1 除非OPTIMIZER_GOAL 设置为FIRST_ROWS,都将采用ALL_ROWS 。在PLSQL中,将采用ALL_ROWS。
*Other Hint 其他提示的意思是指除了RULE, ALL_ROWS 或FIRST_ROWS之外的提示。
General Optimizer Notes 优化器的一般注意事项
~~~~~~~~~~~~~~~~~~~~~~
在看待优化器问题的时候,应考虑如下几点:
- ALL_ROWS 倾向于全表扫描(full table scans)。
- FIRST_ROWS 倾向于索引访问( index access)。
- CBO缺省使用ALL_ROWS计算成本。
- 在Oracle 7.3之前,CBO不会为了迎合并行查询( Parallel Queries)而调整成本。
- 在Oracle 7.3之前,CBO认为字段的值载最大和最小之间是均匀分布的;这之后,可以根据请求存储柱状图统计。
- 所有等于RBO的情况,以表在FROM子句中从右到左的顺序为驱动顺序(Driving Order)。
CBO根据由收集到的统计信息而导出的成本,来确定连接顺序(Join Order).
如果没有统计信息,CBO就将以表在FROM子句中从左到右的顺序为驱动顺序(Driving Order),正好和RBO相反。
- CBO将结合当前表的高水位信息使用ANALYZE信息。因此,一个语句的执行计划是可能因时间的不同而改变的。
- 注意:TRUNCATE重置了表的“高水位”,但是不修改表的统计信息,而是留下了该表的旧的CBO信息。
- 当执行各种连接时,一些连接组合将被排除以降低确定一个执行计划所需要的整体时间花费。总之,每个连接顺序都要和目前为止最好的一个做比较,显然部分优化的方案将被排除。
Problem SQL Statements 问题SQL语句
~~~~~~~~~~~~~~~~~~~~~~
如果 CBO返回的是个部分优化的执行计划,对于这样的问题SQL语句,应采取如下的处理步骤:
a) 检查你是否真的ANALYZE过所涉及的表,这样CBO才有可依据的有用信息。
b) 检查分析的信息是否精确
例如:使用COMPUTE选项,和比较重新分析前后的表的统计信息。
c)检查你是否涉及到了字典表。字典表缺省情况时不被分析的,所以,在CBO方式下SQL访问他们可能产生非常差的计划。
d) 确定你是否希望使用RBO, ALL_ROWS 或FIRST_ROWS 作为你的优化目标(OPTIMIZER_GOAL)。
e) 使用提示(hint)帮助指导优化。
如果你有有问题的SQL语句,并且指引CBO的尝试也没有结果,首先的行动应该是隔离问题语句,在SQL*PLUS 中使用语句的最简单的形式。然后使用EXPLAIN命令或TKPROF来确定实际的执行计划,并改进它。
如果它突然在SQL*PLUS中运行的很好,那么要注意后续章节中表明的一般的错误。
Explain Plan 和 TKPROF
~~~~~~~~~~~~~~~~~~~~~~~
EXPLAIN PLAN 一个SQL语句和TKPROF一些trace文件对于确定给定语句的执行计划是非常有用的,但要注意以下的限制:
a)他们使用当前信息来推导执行计划,不能在必要时显示原来采用的计划。
例如:如果你分析过任何一个表或创建/删除了一个索引,计划将不会相同。如果OPTIMIZER_MODE 不同,也将产生不同的计划。
b) 他们不知道绑定变量(bind variable)的类型,将所有的绑定变量都假设为字符类型。因此,你可能得到错误提示或者误导的结果。
Eg: SELECT 1 FROM DUAL WHERE sysdate < :b2 + 1
这将导致一个错误,因为将把':b2' 解释为一个字符类型的。
为了避免这类的问题,你应该用相应的类型转换函数来包起这样的绑定变量来。
Eg: SELECT 1 FROM dual WHERE sysdate < to_date(:b2) + 1;
这是避免隐式类型转换的好方法。
Making Comparisons 比较
~~~~~~~~~~~~~~~~~~
SQL语句和PL/SQL的比较:
PL/SQL当在SQL语句中引用一个PL/SQl比变量时使用绑定变量(bind variables)。这是非常重要的,因为绑定变量的解释影响到执行计划的确定。
Eg: 在 PL/SQL中的部分语句:
SELECT ename FROM emp WHERE empno > myempno;
其中, 'myempno' 是一个PL/SQL 变量, 所以语句实际上等同于:
SELECT ename FROM emp WHERE empno > :bind1;
绑定变量(Bind Variables): 通常我们会把问题SQL语句拿到SQL*PLUS中,并用常量替换绑定变量。多数情况下这是无效的比较。
Eg: 比较:
SELECT ename FROM emp WHERE empno > 9999;
和
SELECT ename FROM emp WHERE empno > :bind1;
假设表已经被分析过,CBO知道EMPNO的最大最小值。对于第一句,CBO可以确定子句'WHERE empno > 9999'的选择性(selectivity),而对于第二句,CBO不知道':bind1' 是否介于1-9999之间,因此使用缺省的选择性- 例如:他假设子句将返回表的四分之一记录。即使对于使用柱状图的oracle 7.3以上的版本也是这样,如果CBO不知道绑定变量的值的话。因此得出的执行计划是完全不同的。
一般来说,如果使用绑定变量,所有的RANGE SCANS和LIKE比较操作使用缺省的选择性。构成无效的比较。你应该在SQL*PLUS中也使用BIND variable 。
例如:
VARIABLE bind1 varchar2(10)
EXPLAIN PLAN FOR SELECT ename FROM emp WHERE empno > :bind1;
而不是:
EXPLAIN PLAN FOR SELECT ename FROM emp WHERE empno > 9999;
数据库之间的比较:
如果你有分离的两个数据库,表的基本统计也会不一样。你不能把统计从一个数据库传到另外一个数据库。影响到数据库比较的主要因素是:
- 数据库块的大小(DB_BLOCK_SIZE)。这是CBO计算的基础,所以你不能比较DB_BLOCK_SIZE值不同的数据库。
-DB_FILE_MULTIBLOCK_READ_COUNT. 这是Oracle 7.1以上的init.ora参数,用于确定全表扫描的成本。
- DB_FILE_MULTIBLOCK_READ_COUNT. This init.ora parameter is used in determining the cost of full table scans in Oracle 7.1 onwards.
- 表/索引的统计。卸出再导入数据将会导致表和索引占用不同数目的数据块。这影响到计算出的或估计出的统计信息,因而影响到CBO执行计划。(Oracle8i允许导出实际的统计信息,从而解决这个问题。)
Init.ora 参数:
下列的 init.ora参数会影响到成本计算:
<Parameter:ALWAYS_ANTI_JOIN> <Parameter:B_TREE_BITMAP_PLANS> <Parameter:COMPLEX_VIEW_MERGING> <Parameter:DB_FILE_MULTIBLOCK_READ_COUNT> <Parameter:FAST_FULL_SCAN_ENABLED> <Parameter:HASH_AREA_SIZE> <Parameter:HASH_JOIN_ENABLED> <Parameter:HASH_MULTIBLOCK_IO_COUNT> <Parameter:OPTIMIZER_FEATURES_ENABLE> <Parameter:OPTIMIZER_INDEX_CACHING> <Parameter:OPTIMIZER_INDEX_COST_ADJ> <Parameter:OPTIMIZER_MODE> / GOAL <Parameter:OPTIMIZER_PERCENT_PARALLEL> <Parameter:OPTIMIZER_SEARCH_LIMIT> <Parameter:PARTITION_VIEW_ENABLED> <Parameter:PUSH_JOIN_PREDICATE> <Parameter:SORT_AREA_SIZE> <Parameter:SORT_DIRECT_WRITES> <Parameter:SORT_WRITE_BUFFER_SIZE> <Parameter:STAR_TRANSFORMATION_ENABLED> <Parameter:V733_PLANS_ENABLED> <Parameter:CURSOR_SHARING>
版本之间的比较:
在Oracle的不同的版本之间,成本算法和缺省成本目标又一些细小的变化。因此,在升级时,执行计划可能发生变化。比如,带绑定变量的LIKE子句的缺省选择性的变化就是个很好的例子。
和RBO相比较:
和RBO计划相比,执行计划只有相同的很少,你会得到一个不同的执行计划。幸运的是,现在很清楚了:CBO使用各执行计划的相关成本来选择执行计划。
There is little point comparing an execution plan to the RBO plan and being surprised if you get a different execution plan. Hopefully it is fairly clear by now that the CBO uses relative 'costs' of execution plans to determine an execution plan.
例如:
对于RBO,一个索引存在,就是使用它的足够理由。(如果它是和查询相关的话)
而对于CBO,仅仅索引存在还不够充分,还要比较通过索引访问期待数量的数据的成本和其他方法(如:全表扫描)的成本。
如果RBO已经给出了最佳计划,你应该使用'RULE'提示或其他形式的提示来指定访问路径。
RBO和CBO之间的主要区别是:CBO不使用索引。没有使用那些期望被使用的索引的主要原因是:
a) 索引不具有‘选择性。通过计算访问期望数量的数据的成本,CBO发现使用索引块的查找这种方法的成本比多块读(multi-block reads)方式的全表扫描的成本更高。
b)CBO期望返回的'值的范围'和相应的记录数不正确,可能是由于不正确的统计或使用BIND值来限制范围。
注意:如果RBO和CBO返回的结果数据物理上不相同,就是个严重的问题,请作为一个bug报告Oracle。
解析时间的比较:
如果解析时间占了执行时间的很大的比例,建议你检查一下是否执行了一个连接很多表的查询。如果是这样,使用ORDERED提示来设定访问表的顺序,以降低连接时考虑的表的数目,是个很明智的做法。一旦你有了一个好的执行计划,你可以很快地通过重新排列FROM 子句的顺序并使用ORDERED提示来实现同样的计划。
Hints 优化提示
~~~~~
提示使你可以给CBO提供应如何访问数据的信息。不幸的是,无效的提示会使得提示再没有任何警告的情况下被忽略 ,所以大家必须非常注意它的语法。使用提示时,需要记住的主要几点是:
- 提示必须在注释中并以这个严格的格式开始/*+ ... */
- 所有的提示(RULE除外)将使你使用CBO。因此,除非这些表都被分析过或你给了查询完全的提示,使用提示不是个好办法。
- All hints (except RULE) cause you to use the CBO. Hence, it is not a good idea to use hints unless the tables are analyzed or you are fully hinting the query.
- 在提示中不要引用模式(用户)名。
例如:SELECT /*+ index(scott.emp emp1) */ 是不对的, 应给表起别名并在提示中使用别名。
- 如果表使用了别名,在提示中必须使用别名。
例如:
错误写法:
SELECT /*+ FULL (
emp ) */ empno FROM emp myalias
WHERE empno > 10;
正确写法:
SELECT /*+ FULL (
myalias ) */ empno FROM emp myalias
WHERE empno > 10;
- 在PL/SQL块中的提示,在'+'后面必须跟一个空格。这是很重要的,否则提示可能会被忽略,因为一些版本的PL/SQL在把查询传给SQL引擎的时候,会忽略掉'+'号后面的第一个字符。
例如:使用 "SELECT /*+ FULL(a) */" 而不是"SELECT /*+FULL(a)*/" 。
- 被提示的访问路径必须是有效的访问路径。
例如:
表A和B都在可空列IND_COL上有索引。给定的索引建议应该使用这些索引。两表中的VALUE 字段都未被索引。
Tables A & B both have indexes on the nullable IND_COL column. A hint has been supplied suggesting that these indexes should be used. The VALUE column in both tables is unindexed.
SELECT /*+ index(A) index(b) */ *
FROM A,B
WHERE A.ind_col = B.ind_col
AND A.value = 1
AND B.value = 2
Query Plan 执行计划
----------------------------------------------------------
SELECT STATEMENT [CHOOSE] Cost = 83
NESTED LOOPS
TABLE ACCESS FULL A
TABLE ACCESS BY ROWID B
INDEX RANGE SCAN B1
如果这个查询从A驱动,我们不能使用A.IND_COL上的索引,因为可空列的索引会使我们丢掉一些记录。所以提示'index(A)' 是无效的,因而被忽略。B上的索引可以被使用,因为它是访问B表的有效的方式。
If we drive the query from A, then we cannot use the index on A.IND_COL as the column is nullable as we may miss some rows. Hence the 'index(A)' hint is invalid and is ignored. The index on B can be used as it is a valid way to access table B.
- 无效的提示可能不是立即明显地表现出来的。
例如: 在有ORDER BY 子句的语句中使用FIRST_ROWS提示。(你以为起了作用其实没有)
- 在不支持HINTS的第三方工具(和老版本的PLSQL V1)中,解决这个问题的方法是将提示嵌入到视图中,并在工具中引用视图。
例如: 对于语句:
SELECT /*+ FULL( mytab ) */ col1, col2
FROM mytab
WHERE col1 > var1;
你可以创建一个视图:
CREATE or REPLACE VIEW myview AS
SELECT /*+ FULL( mytab ) */ *
FROM mytab;
并将语句改为:
SELECT col1, col2 FROM myview WHERE col1 > var1;
Summary 总结
~~~~~~~
要有效的使用CBO,我们必须:
-定期的分析所有的表
- 设置要求的优化目标 OPTIMIZER_GOAL (FIRST_ROWS 或 ALL_ROWS).
- 在必要的地方,使用提示指导CBO
- 在PL/SQL中使用提示,以使期望的优化器被使用。
- 使用绑定变量时要注意
对于即席的查询(ad-hoc query),CBO工作的很好。对于硬编码的,重复执行的SQL语句,应该进行调优以获得可重复的优化的计划。
你必须经常的监控执行效率,在RDBMS 版本升级的时候进行认真的测试。知道你的应用的关键联机语句和对他们你所期望的执行计划是什么,这是非常重要的。