Clustering Factor聚簇因子是Oracle索引的一种重要参数指标。对CBO而言,clustering factor的取值能够影响到索引路径访问成本值,最后影响到SQL语句的执行计划。
本篇对clustering factor的两个细节问题进行简单讨论,供有兴趣的朋友参考。
1、Clustering Factor的计数
通常,我们认为clustering factor是一个影响CBO进行索引路径成本计算的统计量。Clustering Factor是归属在索引对象的统计量中,在收集统计量时合并的计入索引的数据字典中。
Clustering Factor在索引路径成本公式中扮演着重要的作用。普遍认可的索引路径成本公式为:
Cost=1+
blevel+
ceil(effective index selectivity * leaf_blocks)+
ceil(effective table selectivity * clustering_factor)
在公式中,我们看到clustering_factor在成本公式中的作用。如果该值增加,会引起索引路径成本计算值的增加。反之,cost值就会减少。Cost计算值比较会最后影响CBO的决策。CBO的生命力其实就在于选择路径多、优胜劣汰。在笔者之前的一篇文章里面,已经讨论过这个现象。http://space.itpub.net/17203031/viewspace-680936
SQL> select index_name, clustering_factor, NUM_ROWS from dba_indexes where rownum<5;
INDEX_NAME CLUSTERING_FACTOR NUM_ROWS
---------- ----------------- ----------
I_COL2 3473 161848
I_USER# 9 120
I_OBJ# 3809 17633
I_CON2 181 25934
从含义上看,clustering factor反映的是数据块顺序程度和索引叶子节点的相合程度。索引叶子节点是有序的,其实就是反映数据行在数据块里面的离散程度,相同或者相邻的索引值行是不是在相同的数据块上。
例如说,我们从索引树叶子节点上面获取到10个数据行rowid,根据他们进行回表操作。而在堆表(heap table)的环境下,这10行数据保存位置离散程度我们是不知道的。可能这10行是在10个数据块,也可能是1个数据块。但是,落实到估算成本和实际成本上,就有很大差距。
在Oracle中,Index路径回表的时候是选择单块读操作,也就是一次只读一个数据块。这就体现出成本I/O的差别了,读1个块和读10个块肯定是有差别。就是这样的一出一入,计算成本和实际成本就上去了,索引路径也许就不如全表扫描了。
那么,clustering factor的计量标准是什么呢?我们普遍关注的就是factor和num_rows的对比关系。行业中的一条传言是“优化factor近似要等于num_rows的一半”。Clustering factor是一个介于0到num_rows之间的取值。
Clustering factor究竟是如何计数的呢?过程如下:Oracle会按照索引的叶子节点顺序,从左侧扫描到右侧,这样就可以获得顺序化的索引列rowid列表。之后,按照这样的顺序去依次定位数据行位置。在定位数据块的过程中,如果一次读取行所在数据块和上一次读的行不在一个数据块上,也就是发生了“跳块”动作,统计计数量就加1。
下面,我们通过一个实验来模拟这个运算过程。首先我们选择11.2作为实验环境,创建实验数据表。
SQL> select * from v$version;
BANNER
--------------------------------------------------------------------------------
Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - Production
PL/SQL Release 11.2.0.1.0 - Production
CORE 11.2.0.1.0 Production
TNS for 32-bit Windows: Version 11.2.0.1.0 - Production
NLSRTL Version 11.2.0.1.0 - Production
SQL> create table t (id number, owner varchar2(1000), object_name varchar2(1000)) pctfree 99 pctused 1;
Table created
SQL> insert into t select object_id, owner, object_name from dba_objects where rownum<10;
9 rows inserted
SQL> commit;
Commit complete
SQL> exec dbms_stats.gather_table_stats(user,'T',cascade => true);
PL/SQL procedure successfully completed
注意,我们使用pctfree和pctused参数来控制数据行在数据块中的分布。我们实现了将9行数据分散在多个数据块上。
SQL> select blocks from dba_tables where owner='SYS' and table_name='T';
BLOCKS
----------
5
SQL> select extents, blocks from dba_segments where owner='SYS' and segment_name='T';
EXTENTS BLOCKS
---------- ----------
1 8
SQL> create index idx_t_id on t(id);
Index created
SQL> exec dbms_stats.gather_table_stats(user,'T',cascade => true);
PL/SQL procedure successfully completed
数据表T上面创建了索引idx_t_id,数据表本身分配一个分区extent,数据分布在水位线(HWM)下的5个数据块中。
此时,我们查看索引数据字典中的clustering factor取值。
SQL> select blevel, num_rows, clustering_factor from dba_indexes where owner='SYS' and index_name='IDX_T_ID';
BLEVEL NUM_ROWS CLUSTERING_FACTOR
---------- ---------- -----------------
0 9 9
此时,clustering factor取值和num_rows相同。那么,这个值是怎么计数出来的呢?我们可以使用rowid定位有序object_id情况下,数据块的变化情况。
SQL> select dbms_rowid.rowid_block_number(rowid) block_num, dbms_rowid.rowid_row_number(rowid) row_num, id from t order by id;
BLOCK_NUM ROW_NUM ID
---------- ---------- ----------
89347 1 3
89346 1 15
89345 0 20
89348 0 25
89346 0 28
89347 0 29
89348 1 41
89345 1 46
89349 0 54
9 rows selected
可以发现,每次id的变化,都会发生块的跳跃,所以扫描9行数据发生的块跳跃一共是9次。
Oracle在收集统计量时候是怎么做的呢?一些观点认为如果每个索引都进行处理一遍,消耗时间和资源过多。笔者猜想可能是采用抽样或者计算索引叶子节点rowid的信息变化类似的方法。
2、Rebuild索引与Clustering Factor
Clustering Factor优化是很多运维人员的目标。同时,一些运维人员认为rebuild索引是下降优化clustering factor的好方法。这样的想法其实是错误的。
Clustering Factor虽然是索引的统计属性,但是反映的却是数据行在数据段里面的相对位置。Rebuild索引不会影响有效叶子节点之间的相对顺序关系,索引在rebuild前后,都会维持相对的顺序一致性。而数据表中数据行的顺序关系,完全不能通过rebuild工作完成。所以,rebuild不会优化clustering factor。
下面通过实验来证明。
SQL> create table t as select * from dba_objects where 1=0;
Table created
--先创建索引
SQL> create index idx_t_id on t(object_id);
Index created
--后插入数据
SQL> insert into t select * from dba_objects;
84332 rows inserted
SQL> commit;
Commit complete
SQL> exec dbms_stats.gather_table_stats(user,'T',cascade => true);
PL/SQL procedure successfully completed
注意,我们这里面使用的是“先创建索引,后插入数据”的过程。根据Oracle索引叶子节点分裂算法,此时依据的“5/5”分裂原则进行分裂。
“5/5”分裂原则下,索引叶子节点相对比较松散。我们检查Clustering Factor取值情况。
SQL> select blevel, num_rows, clustering_factor, leaf_blocks from dba_indexes where owner='SYS' and index_name='IDX_T_ID';
BLEVEL NUM_ROWS CLUSTERING_FACTOR LEAF_BLOCKS
---------- ---------- ----------------- -----------
1 84330 1588 298
容纳84330行数据的索引列值,索引idx_t_id分配了叶子节点个数为298个,clustering factor取值为1588。下面我们rebuild一下。
SQL> alter index idx_t_id rebuild;
Index altered
SQL> exec dbms_stats.gather_table_stats(user,'T',cascade => true);
PL/SQL procedure successfully completed
SQL> select extents, blocks from dba_segments where owner='SYS' and segment_name='T';
EXTENTS BLOCKS
---------- ----------
25 1280
SQL> select blevel, num_rows, clustering_factor, leaf_blocks from dba_indexes where owner='SYS' and index_name='IDX_T_ID';
BLEVEL NUM_ROWS CLUSTERING_FACTOR LEAF_BLOCKS
---------- ---------- ----------------- -----------
1 84330 1588 187
在索引进行rebuild的时候,Oracle索引列是全部被取出排序之后,依据顺序生成索引。这样的方式下,分裂算法是“9/1”算法。叶子节点相对紧密,叶子块个数和索引段相对小。
我们发现,rebuild之后,lead_blocks叶子节点个数从298下降到187。但是clustering factor取值却没有变化,说明rebuild不会优化其恶化情况。
3、结论
对clustering factor的研究,有助于我们对于CBO工作原理和优化原则的理解。更有意义的是可以进一步做好优化策略和运维策略。