什么是global statistics?
大家都知道,dbms_stats是Oracle 9i及后续的版本中用于收集优化器统计信息的包,虽然analyze命令也一直可用,但是现在已经不推荐使用analyze来收集统计信息,而是使用dbms_stats。二者之间一个很大的不同,也是dbms_stats一个很突出的优点就是能够正确收集分区表的统计信息,换言之就是global statistics。而analyze命令只会收集最低层次对象的统计信息,然后推导和汇总出高一级的统计信息,如分区表只会收集分区信息,然后再汇总所有分区的统计信息,得到表一级的统计信息。
那什么是global statistics?简单地说global statistics就是指直接从对象本身这一级收集到的统计信息,而不是从下一级对象“推导”或“汇总”出来的统计信息。比如,表的global statistics指直接通过表收集到的统计信息,而不是从分区收集的统计信息进行汇总或推导出的。同样,分区的global statistics是指直接通过分区收集到的统计信息,而不是从子分区收集的统计信息进行汇总或推导出的。global statistics对优化器来说是非常重要的,一个SQL,除非其查询条件限定了数据只在部分分区上,否则在大多数情况下需要global statistics才能得到正确的执行计划。
有的统计值可以从对象的下一级对象进行汇总后得到,比如表的总行数,可以通过各分区的行数相加得到。但有的统计值则不能从下一级对象得到,比如列上的唯一值数量(distinct value)以及密度值(density)。
怎样收集global statistics?
global statistics只能通过dbms_stats包来收集。注意,用analyze命令得到的统计信息,虽然也会有表一级的统计值,但是,那些值是从分区或子分区推导和汇总出来的,是不精确的。后面的实验中,将会验证这一点。
使用dbms_stats收集统计信息时,参数granularity(比如gather_table_stats过程) 指定了哪个级别上的统计信息会被收集:
比如,要在一个做为子分区的表上,以’ALL’级别收集统计信息时,会收集”表+分区+子分区“上的统计信息,相当于需要执行下面的三类SQL:
可以看到,dbms_stats需要比analyze命令更多的时间来进行统计信息的收集。对于有子分区的表,dbms_stats至少要多花三倍的时间。
怎样查看表是否收集了global statistics?
统计信息(下面主要描述表和分区,不涉及索引)一般是通过查看下列视图来检查的:
[DBA|ALL|USER]_TABLES
[DBA|ALL|USER]_TAB_COL_STATISTICS
[DBA|ALL|USER]_TAB_PARTITIONS
[DBA|ALL|USER]_PART_COL_STATISTICS
[DBA|ALL|USER]_TAB_SUBPARTITIONS
[DBA|ALL|USER]_SUBPART_COL_STATISTICS
在以上的视图中,如果GLOBAL_STATS列值为”YES“,表示该级对象上收集了global statistics,而值为”NO“则表示没有收集global statistics。
下面用一些实验来更深入地验证dbms_stats,analyze以及global statistics三者之间的联系。测试环境为Linux AS4上的Oracle 10.2.0.3。注意不同的Oracle可能有不同的行为。
我们先用下面的SQL创建一个测试表,这个表有2个分区,每个分区有2个子分区:
CREATE TABLE T3(i number, p number,sp number)
PARTITION BY RANGE(p)
SUBPARTITION BY HASH(sp) SUBPARTITIONS 2
(
PARTITION q1 VALUES LESS THAN(3) TABLESPACE USERS,
PARTITION q2 VALUES LESS THAN(MAXVALUE) TABLESPACE USERS
);declare
i number;
begin
for i in 1..100000 loop
insert into T3 values(i,mod(i,7), mod(i,8));
if( mod(i, 1000) = 0) then commit; end if;
end loop;
for i in 1..50000 loop
insert into T3 values(i,mod(i,7), mod(i,8)+5);
if( mod(i, 1000) = 0) then commit; end if;
end loop;
end;
/
我们向测试表中插入15W行数据。I列有100,000个不同的值,P列有7个不同的值,SP列有13个不同的值。为了能够方便地查看收集的统计信息,我们使用了一段来自Metalink的sql代码,首先用@sosi可以看到,T3没有任何统计信息存在。
使用analyze table t3 compute statistics;收集T3表的统计信息,可以看到如下的统计信息(由于版面原因,去除了一些与本文无关的信息):
SQL> analyze table t3 compute statistics;
SQL> @sosi
SQL> set echo offTables owned by TEST
------------------------------
T3Please enter Name of Table Owner (Null = TEST):
Please enter Table Name to show Statistics for: T3***********
Table Level
***********Table Number Empty Average Chain Average Global User Sample
Name of Rows Blocks Blocks Space Count Row Len Stats Stats Size
------------- -------- ------ ------- ----- ------- ------ ------ ------
T3 150,000 328 32 921 0 13 NO NO 0Column Column Distinct Number Number Global User Sample
Name Details Values Density Buckets Nulls Stats Stats Size
------ ------------------ ------- ------- ------ ------ ------ ------
I NUMBER(22) 59,845 0 1 0 NO NO
P NUMBER(22) 7 0 1 0 NO NO
SP NUMBER(22) 8 0 1 0 NO NO***************
Partition Level
***************
Partition Number Empty Average Chain Average Global User Sample
Name of Rows Blocks Blocks Space Count Row Len Stats Stats Size
--------- ------- -------- ------ ------- ----- ------- ------ ------ ------
Q1 64,285 138 14 878 0 13 NO NO
Q2 85,715 190 18 954 0 14 NO NOPartition Column Distinct Number Number Global User Sample
Name Name Values Density Buckets Nulls Stats Stats Size
--------- ------ -------- ------- ------- ------ ------ ------ ------
Q1 I 44,878 0 1 0 NO NO
P 3 0 1 0 NO NO
SP 8 0 1 0 NO NO
Q2 I 59,844 0 1 0 NO NO
P 4 0 1 0 NO NO
SP 8 0 1 0 NO NO
***************
SubPartition Level
***************Partition SubPartition Number Empty Average Average Global User Sample
Name Name of Rows Blocks Blocks Space Row Len Stats Stats Size
--------- ------------ ------- -------- ------ ------- ------- ------ ------ ------
Q2 SYS_SUBP31 35,715 80 8 992 14 NO NO 35,715
Q1 SYS_SUBP29 26,785 58 6 902 14 NO NO 26,785
Q2 SYS_SUBP32 50,000 110 10 928 14 NO NO 50,000
Q1 SYS_SUBP30 37,500 80 8 861 13 NO NO 37,500Partition SubPartition Column Distinct Number Number Global User Sample
Name Name Name Values Density Buckets Nulls Stats Stats Size
--------- --------------- ------- -------- ------- ------- ------ ------ ------ ------
Q1 SYS_SUBP29 I 24,106 0 1 0 NO NO 26,785
SYS_SUBP29 P 3 0 1 0 NO NO 26,785
SYS_SUBP29 SP 5 0 1 0 NO NO 26,785
SYS_SUBP30 I 32,142 0 1 0 NO NO 37,500
SYS_SUBP30 P 3 0 1 0 NO NO 37,500
SYS_SUBP30 SP 8 0 1 0 NO NO 37,500
Q2 SYS_SUBP31 I 32,144 0 1 0 NO NO 35,715
SYS_SUBP31 P 4 0 1 0 NO NO 35,715
SYS_SUBP31 SP 5 0 1 0 NO NO 35,715
SYS_SUBP32 I 42,858 0 1 0 NO NO 50,000
SYS_SUBP32 P 4 0 1 0 NO NO 50,000
SYS_SUBP32 SP 8 0 1 0 NO NO 50,000
由上面的表可以看到,analyze只是收集了sub partition的统计信息,然后经过汇总和推导得到了partition和table级别的统计信息。global_stats列值均为”NO”。在表级的I字段的统计信息上看到,I的唯一值个数为59,845,而实际的值应该为100,000,这里差距是比较大的;SP的唯一值个数为8,而实际的值应该为13。
下面再将统计信息删除,然后使用dbms_stats重新收集统计信息:
SQL> exec dbms_stats.gather_table_stats(ownname=>user,tabname=>’T3′,granularity=>’ALL’);
SQL> @sosi
SQL> set echo offTables owned by TEST
------------------------------
T3Please enter Name of Table Owner (Null = TEST):
Please enter Table Name to show Statistics for: T3***********
Table Level
***********Table Number Empty Average Chain Average Global User Sample
Name of Rows Blocks Blocks Space Count Row Len Stats Stats Size
------ ------- -------- ------ ------- ----- ------- ------ ------ -------
T3 150,000 328 0 0 0 10 YES NO 150,000Column Column Distinct Number Number Global User Sample Date
Name Details Values Density Buckets Nulls Stats Stats Size MM-DD-YYYY
------ ----------- -------- ------- ------- ------ ------ ------ ------- ----------
I NUMBER(22) 100,000 0 1 0 YES NO 150,000 12-15-2008
P NUMBER(22) 7 0 1 0 YES NO 5,497 12-15-2008
SP NUMBER(22) 13 0 1 0 YES NO 5,497 12-15-2008
***************
Partition Level
***************Partition Number Empty Average Chain Average Global User Sample
Name of Rows Blocks Blocks Space Count Row Len Stats Stats Size
--------- ------- -------- ------ ------- ----- ------- ------ ------ ------
Q1 64,285 138 0 0 0 10 YES NO 64,285
Q2 85,715 190 0 0 0 10 YES NO 85,715Partition Column Distinct Number Number Global User Sample
Name Name Values Density Buckets Nulls Stats Stats Size
--------- ------- -------- ------- ------- ------ ------ ------ ------
Q1 I 42,857 0 1 0 YES NO 64,285
P 3 0 1 0 YES NO 5,584
SP 13 0 1 0 YES NO 5,584
Q2 I 57,143 0 1 0 YES NO 85,715
P 4 0 1 0 YES NO 5,443
SP 13 0 1 0 YES NO 5,443***************
SubPartition Level
***************Partition SubPartition Number Empty Average Average Global User Sample
Name Name of Rows Blocks Blocks Space Row Len Stats Stats Size
--------- ------------- ------- -------- ------ ------- ------- ------ ----- ------
Q2 SYS_SUBP31 35,715 80 0 0 10 YES NO 35,715
Q1 SYS_SUBP29 26,595 58 0 0 10 YES NO 5,461
Q2 SYS_SUBP32 50,000 110 0 0 10 YES NO 50,000
Q1 SYS_SUBP30 37,500 80 0 0 10 YES NO 37,500Partition SubPartition Column Distinct Number Number Global User Sample
Name Name Name Values Density Buckets Nulls Stats Stats Size
---------- --------------- ------- -------- ------- ------- ------ ------ ------ ------
Q1 SYS_SUBP29 I 26,084 0 1 0 YES NO 5,461
SYS_SUBP29 P 3 0 1 0 YES NO 5,461
SYS_SUBP29 SP 5 0 1 0 YES NO 5,461
SYS_SUBP30 I 32,142 0 1 0 YES NO 37,500
SYS_SUBP30 P 3 0 1 0 YES NO 5,459
SYS_SUBP30 SP 8 0 1 0 YES NO 5,459
Q2 SYS_SUBP31 I 32,144 0 1 0 YES NO 35,715
SYS_SUBP31 P 4 0 1 0 YES NO 5,605
SYS_SUBP31 SP 5 0 1 0 YES NO 5,605
SYS_SUBP32 I 42,858 0 1 0 YES NO 50,000
SYS_SUBP32 P 4 0 1 0 YES NO 5,500
SYS_SUBP32 SP 8 0 1 0 YES NO 5,500
从上面的统计信息可以看到,不管是在partition级,还是在table级,I列和SP列的唯一值个数都得到了正确的值。在三种级别(table,partition,subpartition)上,global_stats列值都为“YES”,表示三个级别收集的都是global statistics。
将统计信息删除,使用exec dbms_stats.gather_table_stats(ownname=>user,tabname=>’T3′,granularity=>’SUBPARTITION’);
收集子分区级别上的统计信息(此处不再列出具体的统计信息数据),则子分区统计信息中的global_stats值为”YES”,而分区和表级统计信息中的global_stats值为”NO”,这两级的统计数据则是由子分区级统计信息汇总和推导得出。
而使用exec dbms_stats.gather_table_stats(ownname=>user,tabname=>’T3′,granularity=>’PARTITION’);收集分区级上的统计信息,则子分区没有统计信息,表级的统计信息也是由分区级统计信息推导和汇总而来。
而在缺省情况下,即granularity参数为default值时,分区和表都有global statistics,而子分区级没有任何统计信息 。
在以上的测试中,在进行新的统计信息收集之前,都是先删除了原来的统计信息。但是如果我们在收集统计信息之前没有删除原来的统计数据,并且不同的统计策略,会有不同的影响。但大部分都是很不好的影响,容易造成统计信息的不准确。比如:
SQL> exec dbms_stats.gather_table_stats(ownname=>user,tabname=>’T3′,granularity=>’ALL’);
SQL> delete from t3 where i<3000;
SQL> commit;
SQL> exec dbms_stats.gather_table_stats(ownname=>user,tabname=>’T3′,granularity=>’PARTITION’);
SQL>
然后对比统计信息,可以发现,表级和子分区级仍然保留着原来的过时的统计信息。综合其他的几个测试(此处不再列出),我们可以得到这个结论:使用dbms_stats收集统计信息,粒度(granularity)为较低级别时,更低级别的统计信息保持不变,而高级别的统计信息由收集的这一级别的统计信息推导和汇总得出,但不能跨级别推导和汇总。但如果高级别的统计信息之前是global statistics,则那一级别的统计信息保持不变。analyze始终是在子分区级别收集统计信息,其高级别统计信息的推导和汇总的行为与dbms_stats一样。比如,现在表级的统计信息global_stats为NO,分区级的统计信息global_stats为YES,使用SUBPARTITION级别或使用analyze命令分析后,分区级的统计信息仍然是原来的统计信息,而表级的统计信息其global_stats为NO,但由于不能跨级推导和汇总,所以表这一级的统计信息仍然是原来的统计信息。
SQL> exec dbms_stats.gather_table_stats(ownname=>user,tabname=>’T3′,granularity=>’PARTITION’);
SQL> analyze table t3 delete statistics;
SQL> exec dbms_stats.gather_table_stats(ownname=>user,tabname=>’T3′,granularity=>’ALL’);
SQL> analyze table t3 delete statistics;
执行上面的命令序列之后,我们可以发现,analyze ..delete statistics命令总是会删除子分区的统计信息,但分区级和表级,如果有global statistics统计信息,则不会删除。并且不能跨级删除,比如分区有global statistics,由表级没有global statistics,表级的统计信息也不会被删除。综合其他几个测试,也可以得到:如果某一级(partition,subpartition)上有统计信息,那么比它更高的所有级别上都会有统计信息。
dbms_stats.delete_table_stats总是可以删除表上所有的三个级别的统计信息。
analyze命令收集的统计信息,global_stats总是为”NO“。
综合以上测试和得到的结果,总结出我们在收集优化统计信息需要注意以下几个方面:
本文涉及到的内容,只有表(并且是普通的HEAP表),不包括索引。也没有涉及到直方图。本文部分内容来源于Metalink Note ID:236935.1 ”Global statistics - An Explanation“
如果需要深入研究analyze和dbms_stats,感兴趣的可以使用sql trace或10046事件来跟踪二者的行为。
文章地址:http://www.laoxiong.net/dbms_stats_and_analyze_and_global_statistics.html
sosi.sql代码如下:
set echo off
set scan on
set lines 150
set pages 66
set verify off
set feedback off
set termout off
column uservar new_value Table_Owner noprint
select user uservar from dual;
set termout on
column TABLE_NAME heading "Tables owned by &Table_Owner" format a30
select table_name from dba_tables where owner=upper('&Table_Owner') order by 1
/
undefine table_name
undefine owner
prompt
accept owner prompt 'Please enter Name of Table Owner (Null = &Table_Owner): '
accept table_name prompt 'Please enter Table Name to show Statistics for: '
column TABLE_NAME heading "Table|Name" format a15
column PARTITION_NAME heading "Partition|Name" format a15
column SUBPARTITION_NAME heading "SubPartition|Name" format a15
column NUM_ROWS heading "Number|of Rows" format 9,999,999,990
column BLOCKS heading "Blocks" format 999,990
column EMPTY_BLOCKS heading "Empty|Blocks" format 999,999,990
column AVG_SPACE heading "Average|Space" format 9,990
column CHAIN_CNT heading "Chain|Count" format 999,990
column AVG_ROW_LEN heading "Average|Row Len" format 990
column COLUMN_NAME heading "Column|Name" format a25
column NULLABLE heading Null|able format a4
column NUM_DISTINCT heading "Distinct|Values" format 999,999,990
column NUM_NULLS heading "Number|Nulls" format 9,999,990
column NUM_BUCKETS heading "Number|Buckets" format 990
column DENSITY heading "Density" format 990
column INDEX_NAME heading "Index|Name" format a15
column UNIQUENESS heading "Unique" format a9
column BLEV heading "B|Tree|Level" format 90
column LEAF_BLOCKS heading "Leaf|Blks" format 990
column DISTINCT_KEYS heading "Distinct|Keys" format 9,999,999,990
column AVG_LEAF_BLOCKS_PER_KEY heading "Average|Leaf Blocks|Per Key" format 99,990
column AVG_DATA_BLOCKS_PER_KEY heading "Average|Data Blocks|Per Key" format 99,990
column CLUSTERING_FACTOR heading "Cluster|Factor" format 999,999,990
column COLUMN_POSITION heading "Col|Pos" format 990
column col heading "Column|Details" format a24
column COLUMN_LENGTH heading "Col|Len" format 9,990
column GLOBAL_STATS heading "Global|Stats" format a6
column USER_STATS heading "User|Stats" format a6
column SAMPLE_SIZE heading "Sample|Size" format 9,999,999,990
column to_char(t.last_analyzed,'MM-DD-YYYY') heading "Date|MM-DD-YYYY" format a10
prompt
prompt ***********
prompt Table Level
prompt ***********
prompt
select
TABLE_NAME,
NUM_ROWS,
BLOCKS,
EMPTY_BLOCKS,
AVG_SPACE,
CHAIN_CNT,
AVG_ROW_LEN,
GLOBAL_STATS,
USER_STATS,
SAMPLE_SIZE,
to_char(t.last_analyzed,'MM-DD-YYYY')
from dba_tables t
where
owner = upper(nvl('&&Owner',user))
and table_name = upper('&&Table_name')
/
select
COLUMN_NAME,
decode(t.DATA_TYPE,
'NUMBER',t.DATA_TYPE||'('||
decode(t.DATA_PRECISION,
null,t.DATA_LENGTH||')',
t.DATA_PRECISION||','||t.DATA_SCALE||')'),
'DATE',t.DATA_TYPE,
'LONG',t.DATA_TYPE,
'LONG RAW',t.DATA_TYPE,
'ROWID',t.DATA_TYPE,
'MLSLABEL',t.DATA_TYPE,
t.DATA_TYPE||'('||t.DATA_LENGTH||')') ||' '||
decode(t.nullable,
'N','NOT NULL',
'n','NOT NULL',
NULL) col,
NUM_DISTINCT,
DENSITY,
NUM_BUCKETS,
NUM_NULLS,
GLOBAL_STATS,
USER_STATS,
SAMPLE_SIZE,
to_char(t.last_analyzed,'MM-DD-YYYY')
from dba_tab_columns t
where
table_name = upper('&Table_name')
and owner = upper(nvl('&Owner',user))
/
select
INDEX_NAME,
UNIQUENESS,
BLEVEL BLev,
LEAF_BLOCKS,
DISTINCT_KEYS,
NUM_ROWS,
AVG_LEAF_BLOCKS_PER_KEY,
AVG_DATA_BLOCKS_PER_KEY,
CLUSTERING_FACTOR,
GLOBAL_STATS,
USER_STATS,
SAMPLE_SIZE,
to_char(t.last_analyzed,'MM-DD-YYYY')
from
dba_indexes t
where
table_name = upper('&Table_name')
and table_owner = upper(nvl('&Owner',user))
/
break on index_name
select
i.INDEX_NAME,
i.COLUMN_NAME,
i.COLUMN_POSITION,
decode(t.DATA_TYPE,
'NUMBER',t.DATA_TYPE||'('||
decode(t.DATA_PRECISION,
null,t.DATA_LENGTH||')',
t.DATA_PRECISION||','||t.DATA_SCALE||')'),
'DATE',t.DATA_TYPE,
'LONG',t.DATA_TYPE,
'LONG RAW',t.DATA_TYPE,
'ROWID',t.DATA_TYPE,
'MLSLABEL',t.DATA_TYPE,
t.DATA_TYPE||'('||t.DATA_LENGTH||')') ||' '||
decode(t.nullable,
'N','NOT NULL',
'n','NOT NULL',
NULL) col
from
dba_ind_columns i,
dba_tab_columns t
where
i.table_name = upper('&Table_name')
and owner = upper(nvl('&Owner',user))
and i.table_name = t.table_name
and i.column_name = t.column_name
order by index_name,column_position
/
prompt
prompt ***************
prompt Partition Level
prompt ***************
select
PARTITION_NAME,
NUM_ROWS,
BLOCKS,
EMPTY_BLOCKS,
AVG_SPACE,
CHAIN_CNT,
AVG_ROW_LEN,
GLOBAL_STATS,
USER_STATS,
SAMPLE_SIZE,
to_char(t.last_analyzed,'MM-DD-YYYY')
from
dba_tab_partitions t
where
table_owner = upper(nvl('&&Owner',user))
and table_name = upper('&&Table_name')
order by partition_position
/
break on partition_name
select
PARTITION_NAME,
COLUMN_NAME,
NUM_DISTINCT,
DENSITY,
NUM_BUCKETS,
NUM_NULLS,
GLOBAL_STATS,
USER_STATS,
SAMPLE_SIZE,
to_char(t.last_analyzed,'MM-DD-YYYY')
from
dba_PART_COL_STATISTICS t
where
table_name = upper('&Table_name')
and owner = upper(nvl('&Owner',user))
/
break on partition_name
select
t.INDEX_NAME,
t.PARTITION_NAME,
t.BLEVEL BLev,
t.LEAF_BLOCKS,
t.DISTINCT_KEYS,
t.NUM_ROWS,
t.AVG_LEAF_BLOCKS_PER_KEY,
t.AVG_DATA_BLOCKS_PER_KEY,
t.CLUSTERING_FACTOR,
t.GLOBAL_STATS,
t.USER_STATS,
t.SAMPLE_SIZE,
to_char(t.last_analyzed,'MM-DD-YYYY')
from
dba_ind_partitions t,
dba_indexes i
where
i.table_name = upper('&Table_name')
and i.table_owner = upper(nvl('&Owner',user))
and i.owner = t.index_owner
and i.index_name=t.index_name
/
prompt
prompt ***************
prompt SubPartition Level
prompt ***************
select
PARTITION_NAME,
SUBPARTITION_NAME,
NUM_ROWS,
BLOCKS,
EMPTY_BLOCKS,
AVG_SPACE,
CHAIN_CNT,
AVG_ROW_LEN,
GLOBAL_STATS,
USER_STATS,
SAMPLE_SIZE,
to_char(t.last_analyzed,'MM-DD-YYYY')
from
dba_tab_subpartitions t
where
table_owner = upper(nvl('&&Owner',user))
and table_name = upper('&&Table_name')
order by SUBPARTITION_POSITION
/
break on partition_name
select
p.PARTITION_NAME,
t.SUBPARTITION_NAME,
t.COLUMN_NAME,
t.NUM_DISTINCT,
t.DENSITY,
t.NUM_BUCKETS,
t.NUM_NULLS,
t.GLOBAL_STATS,
t.USER_STATS,
t.SAMPLE_SIZE,
to_char(t.last_analyzed,'MM-DD-YYYY')
from
dba_SUBPART_COL_STATISTICS t,
dba_tab_subpartitions p
where
t.table_name = upper('&Table_name')
and t.owner = upper(nvl('&Owner',user))
and t.subpartition_name = p.subpartition_name
and t.owner = p.table_owner
and t.table_name=p.table_name
/
break on partition_name
select
t.INDEX_NAME,
t.PARTITION_NAME,
t.SUBPARTITION_NAME,
t.BLEVEL BLev,
t.LEAF_BLOCKS,
t.DISTINCT_KEYS,
t.NUM_ROWS,
t.AVG_LEAF_BLOCKS_PER_KEY,
t.AVG_DATA_BLOCKS_PER_KEY,
t.CLUSTERING_FACTOR,
t.GLOBAL_STATS,
t.USER_STATS,
t.SAMPLE_SIZE,
to_char(t.last_analyzed,'MM-DD-YYYY')
from
dba_ind_subpartitions t,
dba_indexes i
where
i.table_name = upper('&Table_name')
and i.table_owner = upper(nvl('&Owner',user))
and i.owner = t.index_owner
and i.index_name=t.index_name
/