文章目录
只有大表才会产生性能问题,那么怎么才能让优化器知道某个表多大呢?这就需要对表收集统计信息。基数、直方图、集群因子等概念都需要事先收集统计信息才能得到。
1、统计信息
统计信息类似于战争中的侦察兵,如果情报工作没有做好,打仗就会输掉战争。同样的道理,如果没有正确地收集表的统计信息,或者没有及时地更新表的统计信息,SQL的执行计划就会跑偏,SQL也就会出现性能问题。收集统计信息是为了让优化器选择最佳执行计划,以最少的代价(成本)查询出表中的数据。
统计信息主要分为表的统计信息、列的统计信息、索引的统计信息、系统的统计信息、数据字典的统计信息以及动态性能视图基表的统计信息。
1.1、表的统计信息
表的统计信息主要包含表的总行数(num_rows)、表的块数(blocks)以及行平均长度(avg_row_len),我们可以通过查询数据字典DBA_TABLES获取表的统计信息。
创建一个测试表T_STATS:
sql
create table t_stats as select * from DBA_OBJECTS;
我们查看表T_STATS常用的表的统计信息:
sql
select owner, table_name, num_rows, blocks, avg_row_len from dba_tables where owner = 'SCOTT' and table_name = 'T_STATS';
因为T_STATS是新创建的表,没有收集过统计信息,所以从DBA_TABLES查询数据是空的。
现在我们来收集表T_STATS的统计信息。
sql
BEGIN
DBMS_STATS.GATHER_TABLE_STATS(ownname => 'SCOTT',
tabname => 'T_STATS',
estimate_percent => 100,
method_opt => 'for all columns size auto',
no_invalidate => FALSE,
degree => 1,
cascade => TRUE);
END;
这段SQL代码是一个PL/SQL块,它调用了DBMS_STATS
包中的GATHER_TABLE_STATS
过程,用于收集指定表的统计信息。下面是对这段代码的详细分析:
-
BEGIN ... END;
:这是一个匿名PL/SQL块,用于执行一组SQL语句。在这个块中,你可以执行多个命令,它们作为一个整体事务来处理。 -
DBMS_STATS.GATHER_TABLE_STATS
:这是Oracle提供的一个过程,用于收集表的统计信息。统计信息对于优化器来说非常重要,因为优化器依赖这些信息来选择最佳的执行计划。 -
ownname => 'SCOTT'
:这个参数指定了要收集统计信息的表的所有者(schema)名称。在这个例子中,所有者名称是SCOTT
。 -
tabname => 'T_STATS'
:这个参数指定了要收集统计信息的表的名称。在这个例子中,表的名称是T_STATS
。 -
estimate_percent => 100
:这个参数指定了统计信息的收集方式。100
表示使用全表扫描来收集统计信息,这将提供最准确的统计数据,但可能需要更多的时间。如果设置为DBMS_STATS.AUTO_SAMPLE_SIZE
,优化器将自动决定采样的行数。 -
method_opt => 'for all columns size auto'
:这个参数指定了收集统计信息的方法。for all columns
表示为表中的所有列收集统计信息,size auto
表示让优化器自动决定每个列的统计信息收集的粒度。 -
no_invalidate => FALSE
:这个参数指定在收集统计信息后是否使表的统计信息保持有效。FALSE
表示收集统计信息后,如果表的数据发生了变化,将使统计信息无效,以便在下次查询时重新收集。 -
degree => 1
:这个参数指定了用于收集统计信息的并行度。1
表示使用单线程(非并行)来收集统计信息。如果你的系统有多核处理器,增加这个值可能会加快统计信息收集的速度。 -
cascade => TRUE
:这个参数指定是否递归地收集依赖于该表的所有对象(如索引、视图、序列等)的统计信息。
总的来说,这段SQL代码的作用是为SCOTT
用户下的T_STATS
表收集详尽的统计信息,以便优化器可以更准确地评估查询成本并选择最佳的执行计划。通过设置estimate_percent
为100
,确保了统计信息的准确性,同时通过cascade
参数,确保了所有相关对象的统计信息也是最新的。
我们再次查看表的统计信息:
sql
select owner, table_name, num_rows, blocks, avg_row_len from dba_tables where owner = 'SCOTT' and table_name = 'T_STATS';
从查询中我们可以看到,表T_STATS一共有76508行数据,1115个数据块,平均行长度为98字节。
1.2、列的统计信息
列的统计信息主要包含列的基数、列中的空值数量以及列的数据分布情况(直方图)。我们可以通过数据字典DBA_TAB_COL_STATISTICS查看列的统计信息。
现在我们查看表T_STATS常用的列统计信息:
sql
select column_name, num_distinct, num_nulls, num_buckets, histogram
from dba_tab_col_statistics
where owner = 'SCOTT'
and table_name = 'T_STATS';
上面查询中,第一个列表示列名字,第二个列表示列的基数,第三个列表示列中NULL值的数量,第四个列表示直方图的桶数,最后一个列表示直方图类型。
在工作中,我们经常使用下面脚本查看表和列的统计信息:
sql
select a.column_name,
b.num_rows,
a.num_nulls,
a.num_distinct Cardinality,
round(a.num_distinct / b.num_rows * 100, 2) selectivity,
a.histogram,
a.num_buckets
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_STATS';
1.3、索引的统计信息
索引的统计信息主要包含索引blevel(索引高度-1)、叶子块的个数(leaf_blocks)以及集群因子(clustering_factor)。我们可以通过数据字典DBA_INDEXES查看索引的统计信息。
我们在OBJECT_ID列上创建一个索引:
sql
create index idx_t_stats_id on t_stats(object_id);
创建索引的时候会自动收集索引的统计信息,运行下面脚本查看索引的统计信息:
sql
select BLEVEL,LEAF_BLOCKS,CLUSTERING_FACTOR,STATUS from DBA_INDEXES where OWNER='SCOTT' and INDEX_NAME='IDX_T_STATS_ID';
如果要单独对索引收集统计信息,可以使用下面脚本收集:
sql
BEGIN
DBMS_STATS.GATHER_INDEX_STATS(ownname => 'SCOTT',
indname => 'IDX_T_STATS_ID');
END;
2、统计信息重要参数设置
我们通常使用下面脚本收集表和索引的统计信息:
sql
BEGIN
DBMS_STATS.GATHER_TABLE_STATS(
ownname => 'TAB_OWNER',
tabname => 'TAB_NAME',
estimate_percent => 根据表大小设置, -- 这里需要根据实际情况来设置一个具体的百分比值
method_opt => 'for all columns size repeat',
no_invalidate => FALSE,
granularity => 'AUTO',
degree => 根据表大小,CPU资源和负载设置, -- 这里需要根据实际情况来设置一个具体的数值
cascade => TRUE);
END;
- ownname表示表的拥有者,不区分大小写。
- tabname表示表名字,不区分大小写。
- granularity表示收集统计信息的粒度,该选项只对分区表生效,默认为AUTO,表示让Oracle根据表的分区类型自己判断如何收集分区表的统计信息。对于该选项,我们一般采用AUTO方式,也就是数据库默认方式,因此,在后面的脚本中,省略该选项。
- estimate_percent 表示采样率,范围是0.000 001~100。
我们一般对小于1GB的表进行100%采样,因为表很小,即使100%采样速度也比较快。有时候小表有可能数据分布不均衡,如果没有100%采样,可能会导致统计信息不准。因此我们建议对小表100%采样。
我们一般对表大小在1GB~5GB的表采样50%,对大于5GB的表采样30%。如果表特别大,有几十甚至上百GB,我们建议应该先对表进行分区,然后分别对每个分区收集统计信息。
一般情况下,为了确保统计信息比较准确,我们建议采样率不要低于30%。
我们可以使用下面脚本查看表的采样率:
sql
SELECT owner,
table_name,
num_rows,
sample_size,
round(sample_size / num_rows * 100) estimate_percent
FROM DBA_TAB_STATISTICS
WHERE owner = 'SCOTT'
AND table_name = 'T_STATS';
从上面查询我们可以看到,对表T_STATS是100%采样的。现在我们将采样率设置为30%。
sql
BEGIN
DBMS_STATS.GATHER_TABLE_STATS(ownname => 'SCOTT',
tabname => 'T_STATS',
estimate_percent => 30,
method_opt => 'for all columns size auto',
no_invalidate => FALSE,
degree => 1,
cascade => TRUE);
END;
再看表的采样率:
sql
SELECT owner,
table_name,
num_rows,
sample_size,
round(sample_size / num_rows * 100) estimate_percent
FROM DBA_TAB_STATISTICS
WHERE owner = 'SCOTT'
AND table_name = 'T_STATS';
从上面查询我们可以看到采样率为30%,表的总行数被估算为76890,而实际上表的总行数为76508。设置采样率30%的时候,一共分析了23076条数据,表的总行数等于round(23076*100/30),也就是76890。
除非一个表是小表,否则没有必要对一个表100%采样。因为表一直都会进行DML操作,表中的数据始终是变化的。
method_opt 用于控制收集直方图策略。
method_opt => 'for all columns size 1'表示所有列都不收集直方图,如下所示:
sql
BEGIN
DBMS_STATS.GATHER_TABLE_STATS(ownname => 'SCOTT',
tabname => 'T_STATS',
estimate_percent => 100,
method_opt => 'for all columns size 1',
no_invalidate => FALSE,
degree => 1,
cascade => TRUE);
END;
我们查看直方图信息:
sql
select a.column_name,
b.num_rows,
a.num_nulls,
a.num_distinct Cardinality,
round(a.num_distinct / b.num_rows * 100, 2) selectivity,
a.histogram,
a.num_buckets
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_STATS';
从上面查询我们看到,所有列都没有收集直方图。
method_opt => 'for all columns size skewonly'表示对表中所有列收集自动判断是否收集直方图,如下所示:
sql
BEGIN
DBMS_STATS.GATHER_TABLE_STATS(ownname => 'SCOTT',
tabname => 'T_STATS',
estimate_percent => 100,
method_opt => 'for all columns size skewonly',
no_invalidate => FALSE,
degree => 1,
cascade => TRUE);
END;
我们查看直方图信息,如下所示:
sql
select a.column_name,
b.num_rows,
a.num_nulls,
a.num_distinct Cardinality,
round(a.num_distinct / b.num_rows * 100, 2) selectivity,
a.histogram,
a.num_buckets
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_STATS';
从上面查询我们可以看到,除了OBJECT_ID列和EDITION_NAME列,其余所有列都收集了直方图。因为EDITION_NAME列全是NULL,所以没必要收集直方图。OBJECT_ID列选择性为100%,没必要收集直方图。
在实际工作中千万不要使用method_opt => 'for all columns size skewonly' 收集直方图信息,因为并不是表中所有的列都会出现在where条件中,对没有出现在where条件中的列收集直方图没有意义。
method_opt => 'for all columns size auto'表示对出现在where条件中的列自动判断是否收集直方图。
现在我们删除表中所有列的直方图:
sql
BEGIN
DBMS_STATS.GATHER_TABLE_STATS(ownname => 'SCOTT',
tabname => 'T_STATS',
estimate_percent => 100,
method_opt => 'for all columns size 1',
no_invalidate => FALSE,
degree => 1,
cascade => TRUE);
END;
我们执行下面SQL,以便将owner列放入where条件中:
sql
select count(*) from t_stats where owner='SYS';
接下来我们刷新数据库监控信息:
sql
begin
dbms_stats.flush_database_monitoring_info;
end;
我们使用method_opt => 'for all columns size auto'方式对表收集统计信息:
sql
BEGIN
DBMS_STATS.GATHER_TABLE_STATS(ownname => 'SCOTT',
tabname => 'T_STATS',
estimate_percent => 100,
method_opt => 'for all columns size auto',
no_invalidate => FALSE,
degree => 1,
cascade => TRUE);
END;
然后我们查看直方图信息:
sql
select a.column_name,
b.num_rows,
a.num_nulls,
a.num_distinct Cardinality,
round(a.num_distinct / b.num_rows * 100, 2) selectivity,
a.histogram,
a.num_buckets
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_STATS';
从上面查询我们可以看到,Oracle自动地对owner列收集了直方图。
思考,如果将选择性比较高的列放入where条件中,会不会自动收集直方图?现在我们将OBJECT_NAME列放入where条件中:
sql
select count(*) from t_stats where object_name='EMP';
然后我们刷新数据库监控信息:
sql
begin
dbms_stats.flush_database_monitoring_info;
end;
我们收集统计信息:
sql
SQL> BEGIN
2 DBMS_STATS.GATHER_TABLE_STATS(ownname => 'SCOTT',
3 tabname => 'T_STATS',
4 estimate_percent => 100,
5 method_opt => 'for all columns size auto',
6 no_invalidate => FALSE,
7 degree => 1,
8 cascade => TRUE);
9 END;
10 /
PL/SQL procedure successfully completed.
我们查看OBJECT_NAME列是否收集了直方图:
sql
select a.column_name,
b.num_rows,
a.num_nulls,
a.num_distinct Cardinality,
round(a.num_distinct / b.num_rows * 100, 2) selectivity,
a.histogram,
a.num_buckets
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_STATS';
从上面查询我们可以看到,OBJECT_NAME列没有收集直方图。由此可见,使用AUTO方式收集直方图很智能。mothod_opt默认的参数就是 for all columns size auto。
method_opt => 'for all columns size repeat'表示当前有哪些列收集了直方图,现在就对哪些列收集直方图。
当前只对OWNER列收集了直方图,现在我们使用REPEAT方式收集直方图:
sql
BEGIN
DBMS_STATS.GATHER_TABLE_STATS(ownname => 'SCOTT',
tabname => 'T_STATS',
estimate_percent => 100,
method_opt => 'for all columns size repeat',
no_invalidate => FALSE,
degree => 1,
cascade => TRUE);
END;
查看直方图信息:
从查询中我们可以看到,使用REPEAT方式延续了上次收集直方图的策略。对一个运行稳定的系统,我们应该采用REPEAT方式收集直方图。
method_opt => 'for columns object_type size skewonly'表示单独对OBJECT_TYPE列收集直方图,对于其余列,如果之前收集过直方图,现在也收集直方图。
在实际工作中,我们需要对列收集直方图就收集直方图,需要删除某列直方图就删除其直方图,当系统趋于稳定之后,使用REPEAT方式收集直方图。
no_invalidate表示共享池中涉及到该表的游标是否立即失效,默认值为DBMS_STATS. AUTO_INVALIDATE,表示让Oracle自己决定是否立即失效。我们建议将no_invalidate参数设置为FALSE,立即失效。因为我们发现有时候SQL执行缓慢是因为统计信息过期导致,重新收集了统计信息之后执行计划还是没有更改,原因就在于没有将这个参数设置为false。
degree表示收集统计信息的并行度,默认为NULL。如果表没有设置degree,收集统计信息的时候后就不开并行;如果表设置了degree,收集统计信息的时候就按照表的degree来开并行。可以查询DBA_TABLES.degree来查看表的degree,一般情况下,表的degree都为1。我们建议可以根据当时系统的负载、系统中CPU的个数以及表大小来综合判断设置并行度。
cascade表示在收集表的统计信息的时候,是否级联收集索引的统计信息,默认值为DBMS_STATS.AUTO_CASCADE,表示让Oracle自己判断是否级联收集索引的统计信息。我们一般将其设置为TRUE,在收集表的统计信息的时候,级联收集索引的统计信息。
3、检查统计信息是否过期
收集完表的统计信息之后,如果表中有大量数据发生变化,这时表的统计信息就过期了,我们需要重新收集表的统计信息,如果不重新收集,可能会导致执行计划走偏。
现在我们更新表中的数据,将object_id<=10000的owner更新为'SCOTT':
sql
update t_stats set owner='SCOTT' where object_id<=10000;
然后使用下面方法检查表统计信息是否过期,先刷新数据库监控信息:
sql
begin
dbms_stats.flush_database_monitoring_info;
end;
然后我们执行下面查询:
sql
select owner, table_name, object_type, stale_stats, last_analyzed
from dba_tab_statistics
where owner = 'SCOTT'
and table_name = 'T_STATS';
STALE_STATS显示为YES表示表的统计信息过期了。如果STALE_STATS显示为NO,表示表的统计信息没有过期。
我们可以通过下面查询找出统计信息过期的原因:
sql
select table_owner, table_name, inserts, updates, deletes, timestamp
from all_tab_modifications
where table_owner = 'SCOTT'
and table_name = 'T_STATS';
从查询结果我们可以看到,从上一次收集统计信息到现在,表被更新了19418行数据,所以表的统计信息过期了。
现在我们重新收集表的统计信息:
sql
BEGIN
DBMS_STATS.GATHER_TABLE_STATS(ownname => 'SCOTT',
tabname => 'T_STATS',
estimate_percent => 100,
method_opt => 'for columns owner size skewonly',
no_invalidate => FALSE,
degree => 1,
cascade => TRUE);
END;
我们再次查看SQL的执行计划:
sql
EXPLAIN PLAN for select * from t_stats where OWNER='SCOTT';
SELECT PLAN_TABLE_OUTPUT FROM TABLE(DBMS_XPLAN.DISPLAY);
重新收集完统计信息之后,优化器估算返回9 718行数据,这次SQL没走索引扫描而是走的全表扫描,SQL走了正确的执行计划。
细心的你可能会认为走索引扫描的性能高于全表扫描,因为索引扫描逻辑读为1 502,而全表扫描逻辑读为1 690,所以索引扫描性能高。其实这是不对的,衡量一个SQL的性能不能只看逻辑读,还要结合SQL的物理I/O次数综合判断。
Oracle是怎么判断一个表的统计信息过期了呢?当表中有超过10%的数据发生变化(INSERT,UPDATE,DELETE),就会引起统计信息过期。
在进行SQL优化的时候,我们需要检查表的统计信息是否过期,如果表的统计信息过期了,要及时更新表的统计信息。
数据字典all_tab_modifications还可以用来判断哪些表需要定期降低高水位,比如一个表经常进行insert、delete,那么这个表应该定期降低高水位,这个表的索引也应该定期重建。除此之外,all_tab_modifications还可以用来判断系统中哪些表是业务核心表、表的数据每天增长量等。
4、扩展统计信息
当where条件中有多个谓词过滤条件,但是这些谓词过滤条件彼此是有关系的而不是相互独立的,这时我们可能需要收集扩展统计信息以便优化器能够估算出较为准确的行数(Rows)。
我们创建一个表T:
sql
create table t as
select level as id, level || 'a' as a, level || level || 'b' as b
from dual
connect by level < 100;
查询一下这这张表:
sql
select * from T;
在T表中,知道A列的值就知道B列的值,A和B这样的列就叫作相关列。
需要注意的是,扩展统计信息只能用于等值查询,不能用于非等值查询。
5、动态采样
如果一个表从来没收集过统计信息,默认情况下Oracle会对表进行动态采样(Level=2)以便优化器估算出较为准确的Rows,动态采样的最终目的就是为了让优化器能够评估出较为准确的Rows。
动态采样的级别分为11级:
- level 0:不启用动态采样。
- level 1:当表(非分区表)没有收集过统计信息并且这个表要与另外的表进行关联(不能是单表访问),同时该表没有索引,表的数据块必须大于32个,满足这些条件的时候,Oracle会随机扫描表中32个数据块,然后评估返回的Rows。
- level 2:对没有收集过统计信息的表启用动态采样,采样的块数为64个,如果表的块数小于64个,表有多少个块就会采样多少个块。
- level 3:对没有收集过统计信息的表启用动态采样,采样的块数为64个。如果表已经收集过统计信息,但是优化器不能准确地估算出返回的Rows,而是靠猜,比如WHERE SUBSTR(owner,1,3),这时会随机扫描64个数据块进行采样。
- level 4:对没有收集过统计信息的表启用动态采样,采样的块数为64个。如果表已经收集过统计信息,但是表有两个或者两个以上过滤条件(AND/OR),这时会随机扫描64个数据块进行采样,相关列问题就必须启用至少level 4进行动态采样。level4采样包含了level 3的采样数据。
- level 5:收集满足level 4采样条件的数据,采样的块数为128个。
- level 6:收集满足level 4采样条件的数据,采样的块数为256个。
- level 7:收集满足level 4采样条件的数据,采样的块数为512个。
- level 8:收集满足level 4采样条件的数据,采样的块数为1 024个。
- level 9:收集满足level 4采样条件的数据,采样的块数为4 086个。
- level 10:收集满足level 4采样条件的数据,采样表中所有的数据块。
- level 11:Oracle自动判断如何采样,采样的块数由Oracle自动决定。
什么时候需要启用动态采样呢?
当系统中有全局临时表,就需要使用动态采样,因为全局临时表无法收集统计信息,我们建议对全局临时表至少启用level 4进行采样。
当执行计划中表的Rows估算有严重偏差的时候,例如相关列问题,或者两表关联有多个连接列,关联之后Rows算少,或者是where过滤条件中对列使用了substr、instr、like,又或者是where过滤条件中有非等值过滤,或者group by之后导致Rows估算错误,此时我们可以考虑使用动态采样,同样,我们建议动态采样至少设置为level 4。
在数据仓库系统中,有些报表SQL是采用Obiee/SAP BO/Congnos自动生成的,此类SQL一般都有几十行甚至几百行,SQL的过滤条件一般也比较复杂,有大量的AND和OR过滤条件,同时也可能有大量的where子查询过滤条件,SQL最终返回的数据量其实并不多。对于此类SQL,如果SQL执行缓慢,有可能是因为SQL的过滤条件太复杂,从而导致优化器不能估算出较为准确的Rows而产生了错误的执行计划。我们可以考虑启用动态采样level 6观察性能是否有所改善,我们曾利用该方法优化了大量的报表SQL。
最后,需要注意的是,不要在系统级更改动态采样级别,默认为2就行,如果某个表需要启用动态采样,直接在SQL语句中添加HINT即可。
6、定制统计信息收集策略
优化器在计算执行计划的成本时依赖于统计信息,如果没有收集统计信息,或者是统计信息过期了,那么优化器就会出现严重偏差,从而导致性能问题。因此要确保统计信息准确性。虽然数据库自带有JOB每天晚上会定时收集数据库中所有表的统计信息,但是如果数据库特别大,自带的JOB无法完成全库统计信息收集。一些资深的DBA会关闭数据库自带的统计信息收集JOB,根据实际情况自己定制收集统计信息策略。
下面脚本用于收集SCOTT账户下统计信息过期了或者是从没收集过统计信息的表的统计信息,采样率也根据表的段大小做出了相应调整。
sql
declare
cursor stale_table is
select owner,
segment_name,
case
when segment_size < 1 then
100
when segment_size >= 1 and segment_size <= 5 then
50
when segment_size > 5 then
30
end as percent,
6 as degree
from (select owner,
segment_name,
sum(bytes / 1024 / 1024 / 1024) segment_size
from DBA_SEGMENTS
where owner = 'SCOTT'
and segment_name in
(select table_name
from DBA_TAB_STATISTICS
where (last_analyzed is null or stale_stats = 'YES')
and owner = 'SCOTT')
group by owner, segment_name);
begin
dbms_stats.flush_database_monitoring_info;
for stale in stale_table
loop
dbms_stats.gather_table_stats(ownname => stale.owner,
tabname => stale.segment_name,
estimate_percent => stale.percent,
method_opt => 'for all columns size repeat',
degree => stale.degree,
cascade => true);
end loop;
end;
在实际工作中,我们可以根据自身数据库中实际情况,对以上脚本进行修改。
全局临时表无法收集统计信息,我们可以抓出系统中的全局临时表,抓出系统中使用到全局临时表的SQL,然后根据实际情况,对全局临时表进行动态采样,或者是人工对全局临时表设置统计信息(DBMS_STATS.SET_TABLE_STATS)。
下面脚本抓出系统中使用到全局临时表的SQL:
sql
select b.object_owner, b.object_name, a.temporary, sql_text
from dba_tables a, v$sql_plan b, v$sql c
where a.owner = b.object_owner
and a.temporary = 'Y'
and a.table_name = b.object_name
and b.sql_id = c.sql_id;