本实验基于 Oracle11.2.0.1 Scott 账户。
select * from v$version where rownum=1;
BANNER
----------------------------------------------------------------------
Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - Production
我们先创建一个表名为 t_indexscan_cost。
create table t_indexscan_cost as select * from dba_objects;
Table created.
我们在 object_id 列上建立索引如下。
create index idx_cost on t_indexscan_cost(object_id);
Index created.
收集表统计信息如下。
BEGIN
DBMS_STATS.GATHER_TABLE_STATS(ownname => 'SCOTT',
tabname => 'T_INDEXSCAN_COST',
estimate_percent => 100,
method_opt => 'for all columns size 1',
degree => 1,
cascade => TRUE);
END;
PL/SQL procedure successfully completed.
我们查看表总行数、object_id 最大值、object_id 最小值以及 null 值个数。
select b.num_rows,
a.num_distinct,
a.num_nulls,
utl_raw.cast_to_number(high_value) high_value,
utl_raw.cast_to_number(low_value) low_value,
utl_raw.cast_to_number(high_value) -
utl_raw.cast_to_number(low_value) "HIGH_VALUE-LOW_VALUE"
from dba_tab_col_statistics a, dba_tables b
where a.owner = b.owner
and a.table_name = b.table_name
and a.owner = 'SCOTT'
and a.table_name = ('T_INDEXSCAN_COST')
and a.column_name = 'OBJECT_ID';
NUM_ROWS NUM_DISTINCT NUM_NULLS HIGH_VALUE LOW_VALUE HIGH_VALUE-LOW_VALUE
---------- ------------ ---------- ---------- ---------- --------------------
72645 72645 0 76239 2 76237
我们查看下面 SQL 语句执行计划。
select owner from t_indexscan_cost where object_id<1000;
942 rows selected.
Execution Plan
----------------------------------------------------------
Plan hash value: 1756649757
-----------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)|
-----------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 951 | 10461 | 19 (0)|
| 1 | TABLE ACCESS BY INDEX ROWID| T_INDEXSCAN_COST | 951 | 10461 | 19 (0)|
|* 2 | INDEX RANGE SCAN | IDX_COST | 951 | | 4 (0)|
-----------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("OBJECT_ID"<1000)
执行计划中,T_INDEXSCAN_COST 表走的是索引范围扫描。Cost 为 19。那么这 Cost 是怎么算出来的呢?我们先来看一下索引范围扫描的成本计算公式。
cost =
blevel +
celiling(leaf_blocks *effective index selectivity) +
celiling(clustering_factor * effective table selectivity)
索引扫描成本计算公式中,blevel、leaf_blocks、clustering_factor 都可以通过下面查询得到。
select leaf_blocks, blevel, clustering_factor
from dba_indexes
where owner = 'SCOTT'
and index_name = 'IDX_COST';
LEAF_BLOCKS BLEVEL CLUSTERING_FACTOR
----------- ---------- -----------------
161 1 1113
blevel 表示索引的二元高度,blevel 等于索引高度 −1,leaf_blocks 表示索引的叶子块个数,clustering_factor 表示索引的集群因子,effective index selectivity 表示索引有效选择性,effective table selectivity 表示表的有效选择性。
那么这里有效选择性 =(1000−2)/( 76239−2)。
执行计划中,CBO 估算返回的 Rows 为 951,这 951 是怎么算出来的呢?
。
select ceil((1000-2)/(76239-2)*(72645-0)) from dual;
CEIL((1000-2)/(76239-2)*(72645-0))
----------------------------------
951
现在大家应该理解为什么我们曾在 1.3 节中提出执行计划中的 Rows 都是假的这个观点了。如果 where 条件较多,那么 CBO 在估算 Rows 的时候就会出现较大偏差,而且通常将 Rows 算小。因为当 where 条件变多的时候,CBO 估算返回的 **Rows= 某列选择性*某列选择性*某列选择性*...*表总行数**
。选择性一般来说都是小于 1 的分数,当 where 条件变多变复杂之后,CBO 估算的**Rows= 小于 1 的分数*小于 1 的分数*小于 1 的分数*...*表的总行数**
,这种情况下 Rows 当然会越算越小(很多时候 Rows 经常被估算为 1)。
根据上述信息,现在我们来计算索引扫描的成本。
select 1+ceil(161*998/76237)+ceil(1113*998/76237) from dual;
1+CEIL(161*998/76237)+CEIL(1113*998/76237)
------------------------------------------
19
手动计算出来的成本为 19,正好与执行计划中的 Cost 吻合。
在 1.4 节中我们曾经提到,如果回表次数太多,就不应该索引扫描,而应该走全表扫描。我们也可以从索引扫描的成本公式中验证该理论。clustering_factor * effective table selectivity
表示回表的 Cost,在示例中,回表的 Cost 为 15,回表的 Cost 占据整个索引扫描 Cost 的 79%。这就是回表次数太多不能走索引扫描的原因。
索引范围扫描成本计算公式的本质含义是什么呢?我们再来看一下索引范围扫描的成本计算公式。
cost =
blevel +
celiling(leaf_blocks *effective index selectivity) +
celiling(clustering_factor * effective table selectivity)
在 Oracle 数据库中,Btree 索引是树形结构,索引范围扫描需要从根扫描到分支,再扫描到叶子。叶子与叶子之间是双向指向的。blevel 等于索引高度 −1,正好是索引根块到分支块的距离。leaf_blocks *effective index selectivity
表示可能需要扫描多少叶子块。clustering_factor * effective table selectivity
表示回表可能需要耗费多少 I/O。
索引范围扫描是单块读,回表也是单块读,因此,我们得到如下结论:索引扫描成本计算公式其本质就是单块读物理 I/O 次数。
为什么全表扫描成本计算公式要除以单块读耗时呢?上文提到,全表扫描 COST= 多块读物理 I/O 次数*多块读耗时/单块读耗时
,索引范围扫描 COST= 单块读物理 I/O 次数
。现在我们对全表扫描 COST 以及索引范围扫描 COST 都乘以单块读耗时:
全表扫描 COST*单块读耗时 = 多块读物理 I/O 次数*多块读耗时 = 全表扫描总耗时
索引范围扫描 COST*单块读耗时 = 单块读物理 I/O 次数*单块读耗时 = 索引扫描总耗时
到此,大家应该明白优化器何时选择全表扫描,何时选择索引扫描,就是比较走全表扫描的总耗时与走索引扫描的总耗时,哪个快就选哪个。