基于Oracle的SQL优化(崔华著)-整理笔记-第5章“Oracle里的统计信息”
第5章“Oracle里的统计信息”
详细介绍了Oracle数据库里与统计信息相关的各个方面的内容,包括
Oracle数据库中各种统计信息的分类、含义、收集和查看方法,以及如何在Oracle数据库里正确地收集统计信息。
列的统计信息用于描述Oracle数据库里列的详细信息,它包含了一些典型的维度,如列的distinct值的数量、列的null值的数量、列的最小值、列的最大值以及直方图等。
5.2 0racle里收集与查看统计信息的方法
5.2.1 收集统计信息
对系统内部表使用DBMS_STATS包收集统计信息则不存在上述问题:
5.2.1.1 用ANALYZE命令收集统计信息
环境准备
先创建测试表T2和索引IDX T2: soL> create table t2 as select t from dba_objects;
SQL> create index idx_t2 0n t2 (object_id) ; Index created |
删除索引的统计信息
SQL> analyze idnex idx_t2 delete statistics;
此时,表t2、t2的列、索引 idx_t2 均没有统计信息 |
表T2收集统计信息,并且是以估算模式,采样的比例为15%:
soL> analyze table t2 estimate statistics sample 15 percent for table;
现在只有表T2有统计信息,表T2的列和索引IDX T2均没有相关的统计信息。而且因为采用的是估算模式,所以估算结果和实际结果并不一定会完全匹配,比如表T2的实际数据量为71,964,这里Oracle估算出来T2的数据量为73,789: |
以计算模式对表T2收集统计信息:
soL> analyze table t2 compute statistics for table; |
对于同一个对象而言,新执行的analyze命令对抹掉之前analyze的结果!!!
可以用如下的命令同时以计算模式对表T2和列OBJECT NAME. OBJECT ID收集统计信息: soL> analyze table t2 compute statistics for table for columns object_name,object_id; Table analyzed |
如下命令可以删除表T2、表T2的所有列及表T2的所有索引韵统计信息:
soL> analyze table t2 delete statistics; |
如果想一次性以计算模式收集表T2、表T2的所有列和表T2上的所有索引的统计信息,执行如下的语句
就可以了:
soL> analyze table t2 compute statistics; |
5.2.1.2 用DBMS_STATS包收集统计信息
从Oracle 8.1.5开始,DBMS_STATS包被广泛用于统计信息的收集,用DBMS_STATS包收集统计信息也是Oracle官方推荐的方式。在收集CBO所需要的统计信息方面,可以简单地将DBMS_STATS包理解成是ANALYZE命令的增强版。
DBMS_STATS包里最常用的就是如下这4个存储过程。
·GATHER_TABLE_STATS:用于收集目标表、目标表的列和目标表上的索引的统计信息。
·GATHER_INDEX_STATS:用于收集指定索引的统计信息。
·GATHER_SCHEMA_STATS:月于收集指定schema下所有对象的统计信息。
-GATHER_DATABASE_STATS:用于收集全库所有对象的统计信息。
只对表T2收集统计信息,并且是以估算模式,采样的比例同样为I5%:
soL> exec dbms_stats.gather_table_stats (ownname => 'SCOTT',tabname => 'T2',estimate_percent=> 15,method_opt -> 'FOR TABLE',cascade => false) ;
PL/soL procedure successfully completed |
仅对表T2收集统计信息,并且是以计算模式收集,用DBMS_STATS包实现的方法就是将估算模式的采样比例(即参数ESTIMATE PERCENT)设为100%或NULL:
SQL> exec dbms_stats.gather_table_stats (ownname => 'SCOTT',tabname => 'T2',estimate_percent=> 100,method_opt => 'FOR TABLE',cascade => false) ;
PL/SQL procedure successfully completed
SQL> exec dbrns_stats.gather_table_stats (ownname => 'SCOTT',tabname => 'T2',estimate_percent=> NULL,method opt => 'FOR TABLE' , cascade => false) ;
PL/SQL procedure successfully completed
从如下内容可以看出,现在只有表T2有统计信息,表T2的列和索引IDX T2均没有相关的统计信息。 而且因为采用的是计算模式,计算模式会扫描目标对象的所有数据,所以统计结果和实际结果是匹配的,比如 表T2的实际数据量为71,964,这里Oracle计算出来的T2的数据量也是71,964: |
对表T2收集完统计信息后,对表T2的列OBJECT NAME和OBJECT ID以计算模式收集统计信息(不收集直方图):
soL> exec dbms_stats.gather_table_stats (ownname => 'SCOTT',tabname => 'T2',estimate_percent=> 100,method_opt => 'for columns size 1 object_name object_id',cascade => false) ;
PL/SQL procedure successfully completed |
以计算模式收集索引IDX_T2的统计信息:
SQL> exec dbms_stats .gather_index stats (ownname => ' SCOTT ' ,indname =>'IDX_T2 ',estimate_percent =>100) ;
PL/soL procedure successfully completed |
删除表T2、表T2的所有列及表T2的所有索引的统计信息
SQL> exec dbms_stats.delete_table_stats (ownname => 'SCOTT' ,tabname => 'T2 ' ) ;
PL/soL procedure successfuLly completed
从如下结果可以看到,我们刚才收集的表T2、表T2的列OBJECT NAME、OBJECT ID以及索引IDX T2 的统计信息已经全部被删除了:
|
一次性以计算模式收集表T2、表T2的所有列和表T2上的所有索引的统计信息,执行如下语句就可以了:
SQL> exec dbms_stats.gather_table_stats (ownname => 'SCOTT',tabname => 'T2',estimate_percent=> 100,cascade=> true) ;
PL/soL procedure successfully completed |
5.3.2表统计信息不准导致SQL性能问题的实例
表的历史统计信息可以通过查询AWR Repository的基表WRI$ OPTSTAT TAB HISTORY得到:
以下为分析过程:
从查询结果可以看到,上述系统每天白动收集统计信息的时间大概是在早上11点左右,表B2M PACNTNO在2010年7月26日14:18:08至2010年8月17日11:00:50这个时间段的统计信息所记录的记录数一直是 l,504,578,但它在2010年8月17日11:00:50至2010年8月17日16:16:54这个时间段的记录数确实为O,在2010年8月17日16:16:54这个时间点表B2M PACNTNO的记录数又变成了1,520,463。 第一次正式执行上述转换程序是在2010年8月17日下午13:30,此时表B2M PACNTNO的统计信息所记录的记录数确实为O,所以CBO在执行上述SQL时才采用了嵌套循环连接。当我们发现问题并于2010年8 月17日16:16:54这个时间点重新对表B2M PACNTNO收集完统计信息后,它的统计信息所记录的记录数就变为和实际相符的1,520,463,进而上述SQL的执行计划才变成了哈希连接。可是为什么之前每次测试执行都正常呢? 因为表B2M—PACNTNO在2010年4月21日11:08:02至2010年8月17日11:00:50这个时间段的统计信息所记录的记录数一直维持在1 50万条以上,所在在这个时间段执行上述SQL时Oracle始终会选择哈希 连接,当然每次执行就都正常了。 进一步询问相关的开发人员后,整个事情的来龙去脉我们就非常清楚了。上述转换程序每次测试执行时都是先truncate表B2M—PACNTNO,然后往表B2M—PACNTNO中导入大量测试数据,接着再执行相关的SQL,并且每次测试执行完毕后并不会删除表B2M PACNTNO中的测试数据。在Oracle星,一旦一个表被执行了truncate操作,那么它就会成为当天自动收集统计信息作业要收集统计信息的目标对象。虽然表B2M—PACNTNO每次都执行了truncate操作,但因为上述转换程序接着又往里导入了大量数据,并且执行完转换程序后并没有删除这些数据,所以当每天的自动收集统计信息作业自动运行完毕后,表B2M_PACNTNO 中未被删除的测试数据的数据量就被记录在了数据字典里。等转换程序下次再执行时,虽然同样会执行truncate操作,但执行上述SQL时所使用的统计信息仍然是上次转换程序执行完毕,并被当天自动收集统计信息作业收集的表B2M PACNTNO的统计信息,这部分统计信息虽然是陈旧的,并不能反映本次转换操作往表B2M—PACNTNO中实际导入的数据量,但因为表B2M PACNTNO在这段时间的统计信息所记录的记录数一直维持在150万条以上,所以在这个时间段执行上述SQL时Oracle始终会选择哈希连接,并不会选择嵌套循环连接,这就是上述SQL在每次测试执行时都能在20分钟内执行完毕的根本原因。 但等到了2010年8月16日,因为第二天(也就是2010年8月17日)生产环境就会正式土线,所以相关人员对表B2M—PACNTNO执行了truncate操作,这很正常,因为对正式的生产环境而言,以前所遗留的测试 数据是一定要清除的。但现在问题来了,这个清除数据的truncate操作一做,第二天上午11点开始执行的自动收集统计信息作业运行完毕后,表B2M—PACNTNO的统计信息所记录的记录数就变为了O,这意味着等转换程序当天下午13:30第一次在生产环境正式执行时,虽然往表B2M—PACNTNO里导入了150多万条数据,但在执行上述SQL时表B2M PACNTNO的统计信息所记录的记录数却还是0,即此时虽然导入了150多万条 数据,但CBO却看不到,它还以为表B2M PACNTNO的记录数是统计信息里记录的0,于是就选择了在此时效率不佳的嵌套循环连接,这就是上述SQL在第一次正式执行时用了1个小时都还没执行完毕的根本原因。 从这个例子我们可以看出:在导入大量数据后应及时收集统计信息后才进行相关的后续业务处理(包括查询和修改),否则可能会由于实际数据量和统计信息里记录的数据量存在巨大差异而导致CBO选择错误的执行计划。 这里需要注意的是:无论是用ANALYZE命令还是用DBMS STATS色来收集统计信息,它们均会提交当前事务(ANALYZE命令是DDL语句,DDL语句通常情况下就会隐式提交;而DBMS_STATS包的那些收集 统计信息的存储过程内部本身就包含了COMMIT语句)。所以如果应用对事务有强一致性要求,则在导入大量数据后就不能在当前事务中收集统计信息了,因为只要一收集统计信息,当前事务就被提交了,这意味着当前事务的一致性已经被破坏。 如果应用对事务有强一致性的要求,而且在当前事务中导入大量数据后又必须在这个事务中进行相关的后续业务处理,则可以在后续处理的相关SQL中加入Hint(或者使用SQL Profile/SPM来替换相关SQL的执行计划),以便让Oracle走出理想执行计划而不再受统计信息正确与否的干扰。 |
5.4索引的统计信息
5.4.1 索引统计信息的种类和含义
统计信息是用来描述Oracle数据库里索引的详细信息,它包含索引的层级、叶子块的数量、聚簇因子等典型的维度。这些维度信息实际上是存储在数据字典基表IND$、INDPART$、FNDCOMPART$、INDSUBPART$等中,你可以通过数据字典DBA—INDEXES、DBA—IND—PARTITIONS和DBA_IND_SUBPARTITIONS来分别查看索引、分区索引的分区和局部分区索引的子分区的统计信息。
从图5-1我们可以看出,BLEVEL的值越大,则从根节点到叶子块所需要访问的数据块的数量就会越多,耗费的I/O就会越多,访问索引的成本就会越大。BLEVEL
的值从O开始算起,当BLEVEL的值为O时,表示该B树索引只有一层,且根节点和叶子块就是同一个块。
很显然,图5-1中所示的B树索引的BLEVEL的值为2。
selectname,heiqht,1frows,Ifblks,delIfrowsfromindexstats HEIGHT LFROWS LFBLKS DELLFROWS IDXTl" width="576" height="67">
上述数据字典中的字段LEAF_BLOCKS存储的就是目标索引的叶子块的数量,它被CBO用于计算对目标索引做索引全扫描(Index Full Scan)和索引范围扫描(Index Range Scan)的成本。目标索引叶子块的数量越多,则对目标索引做索引全扫描和索引范围扫描的成本值就会越大。 上述数据字典中的字段CLUSTERING_FACTOR存储的就是目标索引的聚簇因子,聚簇因子是指按照索引键值排序的索引行和存储于对应表中的数据行的存储顺序的相似程度。 上述数据字典中的字段DISTINCT.KEYS存储的就是目标索引的索引键值的distinct值的数量。对于唯一性索引而言,在没有NULL值的情况下,DISTrNCT_KEYS的值就等于对应表的记录数。 上述数据字典中的字段AVG—LEAF—BLOCKS—PER_ KEY存储的就是目标索引的每个distinct索引键值所占用的叶子块数量的平均值,对于唯一性索引而言,AVG—LEAF—BLOCKS—PER.KEY显然只会是l。 上述数据字典中的字段AVG—DATA—BLOCKS—PER KEY存储的就是目标索引的每个distinct索引键值所对应表中的数据行所在数据块数量的平均值。 上述数据字典中的字段NUM_ROWS存储的就是目标索引的索引行的数量。 索引统计信息维度中我们需要重点关注的是BLEVEL、LEAF—BLOCKS和CLUSTERING FACTOR.它 们在CBO计算访问索引成本的过程中扮演着举足轻重的作用。 |
5.4.2 聚簇因子的含义及重要性
在Oracle数据库中,聚簇因子是指按照索引键值排序的索引行和存储于对应表中数据行的存储顺序的相似程度。Oracle是按照如下的算法来计算聚簇因子的值的。
(1) 聚簇因子的初始值为1。
(2) Oracle首先定位到目标索引处于最左边的叶子块。
- 从最左边的叶子块的第一个索引键值所在的索引行开始顺序扫描,在顺序扫描的过程中,Oracle会比对当前索引行的rowid和它之前的那个索引行(它们是相邻的关系)的rowid,如果这两个rowid并不是指向
同一个表块,那么Oracle就将聚簇因子的当前值递增l:如果这两个rowid是指向同一个表块,Oracle就不改变聚簇因子的当前值。注意,这里Oracle在比对rowid的时候并不需要回表去访问相应的表块。
(4)上述比对rowid的过程会一直持续下去,直到顺序扫描完目标索引所有叶子块里的所有索引行。
(5)上述顺序扫描操作完成后,聚簇因子的当前值就是索引统计信息中的CLUSTERING_FACTOR,Oracle会将其存储在数据字典里。
从上述聚簇因子的算法可以知道,如果聚簇因子的值接近对应表的表块的数量,则说明目标索引索引行和存储于对应表中数据行的存储顺序的相似程度非常高。这也就意味着Oracle走索引范围扫描后取得目标rowid
再回表去访问对应表块的数据时,相邻的索引杼所对应的rowid极有可能处于同一个表块中,即Oracle在通过索引行记录的rowid回表第一次去读取对应的表块并将该表块缓存在buffer cache中后,当再通过相邻索引行
记录的rowid回表第二次去读取对应的表块时,就不需要再产生物理I/O了,因为这次要访问的和上次已经访问过的表块是同一个块,Oracle已经将其缓存在了buffer cache中。而如果聚簇因子的值接近对应表的记录数,
则说明目标索引索引行和存储于对应表中数据行的存储顺序的相似程度非常低,这也就意味着Oracle走索引范围扫描取得目标rowid再回表去访问对应表块的数据时,相邻的索引行所对应的rowid极有可能不处于同一
个表块中,即Oracle在通过索引行记录的rowid回表第一次去读取对应的表块并将该表块缓存在buffer cache中后,当再通过相邻索引行记录的rowid回表第二次去读取对应的表块时,还需要产生物理I/O,因为这次要
访问的和上次已经访问过的表块并不是同一个块。
换句话说,聚簇因子高的索引走索引范围扫描时比相同条件下聚簇因子低的索引要耗费更多的物理I,O,所以聚簇因子高的索引走索引范围扫描的成本会比相同祭件下聚簇因子低的索引走索引范围扫描的成本高。
以下为测试case
1、创建测试表 create table tl (id number, name char (1200)) ;
2、插入测试数据,尽量使数据分散 insert into tl values (17, '17' ) ; insert into tl values (1, '1' ) ; insert into tl values (6, '6' ) ; insert into tl values (13, '13' ) ; insert into tl values (9, '9' ) ; insert into tl values (4, '4' ) ; insert into tl values (11, '11' ) ; insert into tl values (2, '2' ) ; insert into tl values (19, '19' ) ; insert into tl values (14, '14' ) ; insert into tl values (5, '5' ) ; insert into tl values (16, '16' ) ; insert into tl values (20, '20' ) ; insert into tl values (8, '8' ) ; insert into tl values (18, '18' ) ; insert into tl values (3, '3' ) ; insert into tl values (10, '10' ) ; insert into tl values (7, '7' ) ; insert into tl values (12, '12' ) ; insert into tl values (15, '15' ) ; create index idx_tl on tl (id) ;
3、创建单键值B树索引 create index idx_tl on tl (id) ;
4、查找数据的分布情况 select id,dbms_rowid.rowid_relative_fno ( rowid)||'_'||dbms_rowid.rowid_block_number(rowid) location from tl order by location,id;
共占用5个数据块
4、收集表的统计信息 SQL> exec dbms_stats.gather_table_stats (ownname=>'OWNER',tabname=>'Tl',method_opt=>'for all columns size auto',CASCADE=>true,estimate_percent=>100) ;
PL/SQL procedure successfully completed.
5、查看表的统计信息
|