Oracle的直方图

简介:

直方图的含义

Oracle数据库中,CBO会默认认为目标列的数据在其最小值LOW_VALUE和最大值HIGH_VALUE之间均匀分布的,并且会按照这个均匀分布原则来计算对目标列施加查询条件后的可选择率以及结果集的Cardinality,进而据此来计算成本值并选择执行计划。但目标列的数据是均匀分布这个原则并不总是正确的,在实际的系统中,我们很容易就能看到一些目标列的数据分布是不均匀的,甚至是极度倾斜、分布极度不均衡的。对这样的列如果还按照均匀分布的原则去计算可选择率与Cardinality,并据此来计算成本、选择执行计划,那么CBO所选择的执行计划就可能是不合理的,甚至是错误的。

看一个由于数据分布极不均衡而导致CBO选错执行计划的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
zx@ORCL> create  table  t1 (a number(5),b varchar2(5));
 
Table  created.
 
zx@ORCL> declare  cnt number(5) := 1;
   2   begin
   3  loop
   4   insert  into  t1  values (1, '1' );
   5  if cnt=10000  then 
   6  exit;
   7   end  if;
   8  cnt:=cnt+1;
   9   end  loop;
  10   insert  into  t1  values (2, '2' );
  11   commit ;
  12   end ;
  13  /
 
PL/SQL  procedure  successfully completed.
 
zx@ORCL> select  b, count (*)  from  t1  group  by  b;
 
B                  COUNT (*)
--------------- ----------
1                    10000
2                        1
 
zx@ORCL> create  index  t1_ix_b  on  t1(b);
 
Index  created.

对表T1不收集直方图统计信息的方式收集一下统计信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
zx@ORCL> exec  dbms_stats.gather_table_stats( USER , 'T1' ,estimate_percent=>100,method_opt=> 'for all columns size 1' );
 
PL/SQL  procedure  successfully completed.
 
zx@ORCL> select  from  t1  where  b= '2' ;
 
          A B
---------- ---------------
          2 2
 
zx@ORCL> select  from  table (dbms_xplan.display_cursor( null , null , 'all' ));
 
PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
SQL_ID  5p7b772tpcvm4, child number 0
-------------------------------------
select  from  t1  where  b= '2'
 
Plan hash value: 3617692013
 
--------------------------------------------------------------------------
| Id  | Operation         |  Name  Rows   | Bytes | Cost (%CPU)|  Time      |
--------------------------------------------------------------------------
|   0 |  SELECT  STATEMENT  |      |       |       |     7 (100)|          |
|*  1 |   TABLE  ACCESS  FULL | T1   |  5001 | 25005 |     7   (0)| 00:00:01 |
--------------------------------------------------------------------------
.....省略部分输出

从执行计划可以看出执行计划走的是全表扫描,但是很显然应该走索引T1_IX_B。这是因为CBO默认认为列B的数据是均匀分布的,而列B上的distinct值只有12这两值,所以CBO评估出来的对列B施加等值查询条件的可选择率就是1/2,进而评估出来对列B施加等值查询条件的结果集的Cardinality就是5001

1
2
3
4
5
zx@ORCL> select  round(10001*(1/2))  from  dual;
 
ROUND(10001*(1/2))
------------------
               5001

正因为CBO评估出上述等值查询要返回结果集的Cardinality5001,已经占了表T1总记录数的一半,所以CBO认为此时再走列B上的索引T1_IX_B就已经不合适了,进而就选择了对列T1的全表扫描。但实际上,CBO对上述等值查询要返回结果集的Cardinality的评估已经与事实严重不符,评估出来的值是5001,其实却只有1,差了好几个数量级。

CBO这里选择了执行计划,正确的执行计划应该是走索引T1_IX_BCBO选错执行计划的根本原因是表T1的列B的分布实际上是极度不均衡的(B一共就两值,其中100001,只有12)CBO在评估的一开始所用的原则就错了,当然结果也就错了。

为了解决上述问题,Oracle引入了直方图(Histogram)。直方图是一种特殊的列统计信息,它详细描述了目标列的数据分布情况。直方图实际上存储在数据字典基表HISTGRM$中,可以通过数据字典DBA_TAB_HISTOGRAMSDBA_PART_HISTOGRAMSDBA_SUBPART_HISTOGRAMS来分别查看表、分区表的分区和分区表的子分区的直方图统计信息。

如果对目标列收集了直方图,则意味着CBO将不再认为该目标列上的数据是均匀分布的了,CBO就会用该目标列上的直方图统计信息来计算对该列施加查询条件后的可选择率和返回结果集的Cardinality,进而据此计算成本并选择相应的执行计划。

还用上面的例子,对表T1的列B收集了直方图统计信息后,CBO正确地评估出了返回结果集的Cardinality不是5001而是1,进而就正确地选择了走索引T1_IX_B的执行计划:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
zx@ORCL> exec  dbms_stats.gather_table_stats( USER , 'T1' ,estimate_percent=>100,method_opt=> 'for all columns size auto' , cascade => true );
 
PL/SQL  procedure  successfully completed.
#清空shared_pool,生产系统不要随便执行
zx@ORCL> alter  system flush shared_pool;
 
System altered.
 
zx@ORCL> select  from  t1  where  b= '2' ;
 
          A B
---------- ---------------
          2 2
 
zx@ORCL> select  from  table (dbms_xplan.display_cursor( null , null , 'all' ));
 
PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
SQL_ID  5p7b772tpcvm4, child number 0
-------------------------------------
select  from  t1  where  b= '2'
 
Plan hash value: 3579362925
 
---------------------------------------------------------------------------------------
| Id  | Operation                   |  Name     Rows   | Bytes | Cost (%CPU)|  Time      |
---------------------------------------------------------------------------------------
|   0 |  SELECT  STATEMENT            |         |       |       |     2 (100)|          |
|   1 |   TABLE  ACCESS  BY  INDEX  ROWID| T1      |     1 |     5 |     2   (0)| 00:00:01 |
|*  2 |    INDEX  RANGE SCAN          | T1_IX_B |     1 |       |     1   (0)| 00:00:01 |
---------------------------------------------------------------------------------------
.....省略部分输出

所以,直方图就是专门为了准确评估这种分布不均匀的目标列的可选择率、结果集的Cardinality而被Oracle引入的,它详细描述了目标列的数据分布情况,并将这些分布情况记录在数据字典里,相当于直观地告诉了CBO这些列的数据分布情况,于是CBO就能据此来做出相对准确的判断。

直方图的类型

Oracle数据库里的直方图使用了一种称为Bucket()的方式来描述目标列的数据分布。这有点类似哈希算法的Bucket,它实际上是一个逻辑上的概念,相当于分组,每个Bucket就是一组,每个Bucket里会存储一个或多个目标列上的数据。Oracle会用两个维度来描述一个Bucket,这两个维度分别是ENDPOINT NUMBERENDPOINT VALUEOracle会将每个Bucket的维度ENDPOIONTNUMBERENDPOINT VALUE记录在数据字典基表HISTGRM$中,这样就达到了目标列的直方图统计信息记录在数据字典中的目的。维度ENDPOINT NUMBERENDPOINT VALUE分别对应于数据字典DBA_TAB_HISTOGRAMSDBA_PART_HISTOGRAMSDBA_SUBPART_HISTOGRAMS中的字段ENDPOINT_NUMBER/BUCKET_NUMBERENDPOINT_VALUE。同时,Oracle还会记录目标列的直方图统计信息所占用的Bucket的总数,可以通过数据字典DBA_TAB_COL_STATISTICSDBA_PART_COL_STATISTICSDBA_SUBPART_COL_STATISTICS中字段NUM_BUCKETS来查看目标列对应直方图的Bucket的总数。

Oracle 12c之前,Oracle数据库里的直方图分为两种类型,分别是FrequencyHeightBalanced(Oracle 12c中还存在名为Top-FrequencyHybrid类型的直方图)。在Oracle 12以之前,如果存储在数据字典里描述目标列直方图的Buckt的数量等于目标列的distinct值的数量,则这种类型的直方图就是Frequency类型的直方图。如果存储在数据字典里描述目标列直方图的Bucket的数量小于目标列的distinct值的数量,则这种类型的直方图就是Height Balanced类型的直方图。

2.1 Frequency类型的直方图

对于Frequency类型的直方图而言,目标列直方图的Bucket的数量就等于目标列的distinct的数量,此时目标列有多个个distinct值,Oracle在数据字典DBA_TAB_HISTOGRAMSDBA-PART_HISTOGRAMSDBA_SUBPART_HISTOGRAMS中就会存储多少条记录,每一条记录不代表了对其中的一个Bucket的描述,上述数据字典中的字段ENDPOINT_VALUE记录了这些distinct值,而字段ENDPOINT_NUMBER是一个累加值,实际上,我们可以用一条记录的ENDPOINT_NUMBER值减去它的上一条记录的ENDPOINT_NUMBER值来得到这条记录本身所对应的ENDPOINT_VALUE值的记录数。

实际上,Frequency类型的直方图就是把目标列的每一个distinct值都记录在数据字典里,同时在数据字典里记录记录每个distinct值在目标表里一共有多少条记录,这样CBO就能非常清楚地知道目标列在目标表里的实际数据分布情况了。这种Frequency类型的直方图所对应的收集方法并不适用于目标列的distinct值非常多的情形,所以OracleFrequence类型的直方图有如下限制:Frequency类型的直方图所对应的Bucket的数量不能超过254(注意,Oracle 12c 中将不再有这一限制,在Oracle 12cFrequency类型的直方图所对应的Bucket的数量可以超过254),即Frequency类型的直方图只适用于那些目标列的distinct值数量小于或等于254的情形。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
zx@ORCL> create  table  h (x number);
 
Table  created.
 
zx@ORCL> declare 
i number;
begin
for  in  1..3296 loop
insert  into  values (1);
   5    6   end  loop;
   7   for  in  1..100 loop
   8   insert  into  values (3);
   9   end  loop;
  10   for  in  1..798 loop
  11   insert  into  values (5);
  12   end  loop;
  13   for  in  1..3970 loop
  14   insert  into  values (7);
  15   end  loop;
  16   for  in  1..16293 loop
  17   insert  into  values (10);
  18   end  loop;
  19   for  in  1..3399 loop
  20   insert  into  values (16);
  21   end  loop;
  22   for  in  1..3651 loop
  23   insert  into  values (27);
  24   end  loop;
  25   for  in  1..3892 loop
  26   insert  into  values (32);
  27   end  loop;
  28   for  in  1..3521 loop
  29   insert  into  values (39);
  30   end  loop;
  31   for  in  1..1080 loop
  32   insert  into  values (49);
  33   end  loop;
  34   commit ;
  35   end ;
  36  /
 
PL/SQL  procedure  successfully completed.
 
zx@ORCL> select  count (*)  from  h;
 
   COUNT (*)
----------
      40000

按照Frequency类型直方图的定义,如果对列X收集Frequency类型的直方图,则DBA_TAB_HISTOGRAMS中应该有10条记录,而且这10条记录的ENDPOINT_VALUE记录的就是这10distinct值,对应的ENDPOINT_NUMBER就是到此distinct值为止累加的行记录数。这10条记录的ENDPOINT_VALUEENDPOINT_NUMBER实际上可以用如下SQL的显示结果来模拟:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
zx@ORCL> select  as  x, count (*)  as  cardinality, sum ( count (*)) over( order  by  x range unbounded preceding)  as  cum_cardinality  from  group  by  x;
 
          X CARDINALITY CUM_CARDINALITY
---------- ----------- ---------------
          1        3296            3296
          3         100            3396
          5         798            4194
          7        3970            8164
         10       16293           24457
         16        3399           27856
         27        3651           31507
         32        3892           35399
         39        3521           38920
         49        1080           40000
 
10  rows  selected.

上述查询结果中的列X就模拟了DBA_TAB_HISTOGRAMS中那10条记录的ENDPOINT_VALUE,列CUM_CARDINALITY就模拟了DBA_TAB_HISTOGRAMS中那10条记录的ENDPOINT_NUMBER

对表h的列x来实际收集一下直方图统计信息

1
2
3
zx@ORCL> exec  dbms_stats.gather_table_stats(ownname=> USER ,tabname=> 'H' ,method_opt=> 'for columns size auto X' , cascade => true ,estimate_percent=>100);
 
PL/SQL  procedure  successfully completed.

收集完统计信息后发现DBA_TAB_COL_STATISTICS中列x所对应的字段HISTOGRAM的值为NONE,这表明现在列x上依然没有直方图统计信息:

1
2
3
4
5
zx@ORCL> select  table_name,column_name,num_distinct,density,num_buckets,histogram  from  dba_tab_col_statistics  where  table_name= 'H' ;
 
TABLE_NAME COLUMN_NAM NUM_DISTINCT    DENSITY NUM_BUCKETS HISTOGRAM
---------- ---------- ------------ ---------- ----------- ---------------------------------------------
H          X                    10         .1           1 NONE

这种现象是正常的。因为Oracle在自动收集直方图统计信息时会秉承一个原则,那就是只对那些用过的列(即在SQL语句where条件中出现过的列)收集直方图统计信息。Oracle会在表SYS.COL_USAGE$中记录各表中各列的使用情况,在自动收集直方图统计信息时Oracle会查询SYS.COL_USAGE$,如果发现其中没有目标列的使用记录,那就不会对目标列收集直方图统计信息。表H刚刚建立,还没有在SQL语句的where条件中使用过列X,所以这里不会对列X收集直方图统计信息。

收集直方图的前提条件是:1.列上的数据分布不均匀,2.列在sqlwhere条件中被使用过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
zx@ORCL> select  name ,intcol#  from  sys.col$  where  obj# = ( select  object_id  from  dba_objects  where  object_name= 'H' );
 
NAME                                                                                           INTCOL#
------------------------------------------------------------------------------------------ ----------
X                                                                                                   1
 
zx@ORCL> select  obj#,intcol#,equality_preds  from  sys.col_usage$  where  obj# = ( select  object_id  from  dba_objects  where  object_name= 'H' );
 
no  rows  selected
 
zx@ORCL> select  count (*)  from  where  x=10;
 
   COUNT (*)
----------
      16293
 
zx@ORCL> select  obj#,intcol#,equality_preds  from  sys.col_usage$  where  obj# = ( select  object_id  from  dba_objects  where  object_name= 'H' );
 
       OBJ#    INTCOL# EQUALITY_PREDS
---------- ---------- --------------
      88766          1              1

再次对表H的列X自动收集直方图统计信息:

1
2
3
4
5
6
7
8
9
zx@ORCL> exec  dbms_stats.gather_table_stats(ownname=> USER ,tabname=> 'H' ,method_opt=> 'for columns size auto X' , cascade => true ,estimate_percent=>100);
 
PL/SQL  procedure  successfully completed.
 
zx@ORCL> select  table_name,column_name,num_distinct,density,num_buckets,histogram  from  dba_tab_col_statistics  where  table_name= 'H' ;
 
TABLE_NAME COLUMN_NAM NUM_DISTINCT    DENSITY NUM_BUCKETS HISTOGRAM
---------- ---------- ------------ ---------- ----------- ---------------------------------------------
H          X                    10   .0000125          10 FREQUENCY

另外DBA_TAB_COL_STATISTICS中列x所对应的字段HISTORAM的值已经由NONE变成了RREQUENCY,这说明现在列X上已经有了Frequency类型的直方图

可以从DBA_TAB_HISTOGRAMS中看到列xFrequence类型的直方图的具体信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16