lundi 1 février 2016

Les histogrammes (3): Height-Balanced

Dans l’article précédent nous avions vu qu’avec un histogramme de type FREQUENCY on avait un bucket alloué pour chaque valeur distincte ce qui permettait au CBO d’estimer une cardinalité juste. Cependant, le nombre de buckets étant limité à 254 il n’est plus possible d’avoir un histogramme FREQUENCY pour les colonnes ayant un nombre de valeurs distinctes supérieur à cette limite. Oracle calcule à la place des histogrammes de type height-balanced (HB).

La colonne C_HB de ma table T1 contenant 829 valeurs distinctes est candidate pour un histogramme height-balanced.
exec dbms_stats.gather_table_stats(user,'T1', method_opt=>'for columns C_HB size 829');

ERROR at line 1:
ORA-20000: Cannot parse for clause: for columns C_HB size 829
ORA-06512: at "SYS.DBMS_STATS", line 24281
ORA-06512: at "SYS.DBMS_STATS", line 24332
ORA-06512: at line 1
L’erreur précédente nous montre qu’en 11g, il est inutile de positionner un nombre de buckets égale au nombre de valeurs distinctes lorsque ce dernier est supérieur à 254. Il faut donc calculer les statistiques pour cette colonne avec 254 buckets :
 exec dbms_stats.gather_table_stats(user,'T1', method_opt=>'for columns C_HB size 254'); 
 
select column_name,num_distinct,density,num_nulls,num_buckets,sample_size,histogram  
from user_tab_col_statistics  
where table_name='T1' and column_name='C_HB'; 
                                                                                                                                                                                        
COLUMN_NAME                    NUM_DISTINCT    DENSITY  NUM_NULLS NUM_BUCKETS SAMPLE_SIZE HISTOGRAM 
------------------------------ ------------ ---------- ---------- ----------- ----------- --------------- 
C_HB                                    829 ,000748974          0         254       14739 HEIGHT BALANCED
La colonne HISTOGRAM de la vue USER_TAB_COL_STATISTICS nous montre que c’est bel et bien un histogramme height-balanced qui a été calculé.
Jetons un œil aux 10 valeurs les plus récurrentes dans la table :
 select * from ( 
SELECT C_HB, count(*) AS frequency, trunc(ratio_to_report(count(*)) OVER ()*100,2) AS percent 
 FROM t1 
 GROUP BY C_HB 
 ORDER BY 3 desc 
 ) 
 where rownum<=10 
 ; 
  
      C_HB  FREQUENCY    PERCENT 
--------- ---------- ---------- 
      999       5710      38,74 
        0       4601      31,21 
        4       1853      12,57 
        5        237        1,6 
        8        195       1,32 
       19        169       1,14 
        1        152       1,03 
       16         84        ,56 
       13         70        ,47 
        6         47        ,31
On s’aperçoit que la valeur 999 qui est la valeur la plus présente apparait 5710 fois, la valeur 0 apparait 4601 fois et la valeur 4 apparait 1853 fois. A elles trois ces valeurs représentent plus de 80% des données.


Principe de calcul des histogrammes height-balanced
Pour calculer l’histogramme sur la colonne C_HB, Oracle commence par trier les valeurs de la colonne C_HB puis il divise ces données triées en buckets de 58 valeurs (num_rows/num_buckets=14739/254=58). Pour chaque bucket Oracle conserve la valeur la plus grande, c’est ce qu’on appelle le ENDPOINT_VALUE. La première valeur triée est la valeur 0 qui apparait 4601 fois. La taille du bucket étant de 58, la valeur 0 correspondra au ENDPOINT_VALUE de 79 buckets (4601/58=79). Ensuite, Oracle arrive à la valeur 1 qui apparait 152 fois dans la table et donc cette valeur apparaitra comme ENDPOINT_VALUE dans 2 buckets (152/58=2.6). En continuant avec ce mécanisme on aura dans notre histogramme 254 valeurs et on comprend qu’en fonction de l'évolution des données dans la table on peut avoir un ENDPOINT_VALUE qui diffère après chaque exécution du calcul de statistiques. Le fameux problème d’instabilité des histogrammes height-balanced vient de là.

Requêtons maintenant la vue USER_TAB_HISTOGRAMS pour la colonne C_HB et observons ce qu'on obtient dans notre histogramme :

select ENDPOINT_NUMBER, ENDPOINT_VALUE 
from USER_TAB_HISTOGRAMS 
where table_name = 'T1' and column_name = 'C_HB' 
order by ENDPOINT_NUMBER; 
 
ENDPOINT_NUMBER ENDPOINT_VALUE 
--------------- -------------- 
             79              0 
             81              1 
             82              2 
            114              4 
            118              5 
            119              6 
            122              8 
            123              9 
            124             12 
            125             13 
            127             16 
            130             19 
            131             21 
            132             25 
            133             32 
            134             40 
            135             53 
            136             70 
            137             88 
            138            110 
            139            138 
            140            216 
            141            256 
            142            370 
            143            460 
            144            640 
            243            999 
            244           1414 
            245           2150 
            246           3333 
            247           5484 
            248           8038 
            249          14660 
            250          27455 
            251          64093 
            252         187520 
            253         986698 
            254       63681020 
 
38 rows selected. 
Là vous vous demandez peut-être pourquoi nous n’obtenons que 38 lignes dans notre histogramme au lieu de 254 ? Effectivement, bien que la règle consiste à avoir une valeur pour chaque bucket, la colonne ENDPOINT_NUMBER étant une valeur cumulative, Oracle ne conservera que le dernier ENDPOINT_NUMBER lorsque plusieurs ENDPOINT_VALUE se succèdent avec la même valeur. Ainsi, on peut déduire en regardant notre histogramme que la fréquence de la valeur 0 est de 79 car il n’existe pas de ENDPOINT_NUMBER avant ce nombre.
La valeur 1 qui apparait au ENDPOINT_NUMBER 81 juste après le 79 a une fréquence de 2 (81-79), en d’autres termes on peut dire que la valeur 1 apparait à deux reprises comme endpoint_value d’un bucket.

La valeur 4 apparait au ENDPOINT_NUMBER de 114 après la valeur 2 qui apparait au endpoint 82. Cette ligne dans la vue USER_TAB_HISTOGRAMS nous permet de déduire 2 choses :
  • La valeur 3 n’est pas présente dans l’histogramme (même si la valeur apparait 9 fois dans la table)
  • La valeur 4 a une fréquence de 32 (114-82)
La valeur 999 qui est la valeur la plus présente dans la table apparait au ENDPOINT_NUMBER 243 juste après le ENDPOINT_NUMBER 144 ce qui nous permet d’en déduire sa fréquence: 243-144=99.

Pour afficher les fréquences de chaque valeur capturée on peut utiliser la requête suivante qui utilise notamment la fonction LAG:
select endpoint_value as column_value, 
endpoint_number as cummulative_frequency, 
endpoint_number - lag(endpoint_number,1,0) over (order by endpoint_number) as frequency 
from user_tab_histograms 
where table_name = 'T1' and column_name = 'C_HB'; 
 
COLUMN_VALUE CUMMULATIVE_FREQUENCY  FREQUENCY

------------ --------------------- ----------

           0                    79         79

           1                    81          2

           2                    82          1

           4                   114         32

           5                   118          4

           6                   119          1

           8                   122          3

           9                   123          1

          12                   124          1

          13                   125          1

          16                   127          2

          19                   130          3

          21                   131          1

          25                   132          1

          32                   133          1

          40                   134          1

          53                   135          1

          70                   136          1

          88                   137          1

         110                   138          1

         138                   139          1

         216                   140          1

         256                   141          1

         370                   142          1

         460                   143          1

         640                   144          1

         999                   243         99

        1414                   244          1

        2150                   245          1

        3333                   246          1

        5484                   247          1

        8038                   248          1

       14660                   249          1

       27455                   250          1

       64093                   251          1

      187520                   252          1

      986698                   253          1

    63681020                   254          1


Histogrammes Height-Balanced et cardinalités :

Dans l’article sur les histogrammes FREQUENCY j’avais écrit qu’à partir du moment où l’on avait autant de buckets que de valeurs distinctes, on pouvait considérer toutes les valeurs de l'histogramme comme populaires ce qui permettait au CBO d’estimer une bonne cardinalité. Malheureusement, il n’en va pas de même pour les histogrammes height-balanced car seules les valeurs ayant une fréquence supérieure à 1 sont considérées comme populaires. Les valeurs apparaissant une seule fois ou bien celles n’ayant pas été capturées lors du calcul de statistiques sont donc considérées comme non populaires. De plus, puisqu’il est nécessaire qu’une valeur apparaisse au moins 2 fois comme endpoint_value pour devenir populaire on peut en déduire qu’Oracle n’est capable de capturer au mieux que 127 valeurs populaires.

Voyons ce que donnent les estimations du CBO lorsqu’on requête une valeur populaire :
-- popular value 
alter system flush shared_pool; 
select * from T1 where C_HB=999; 
 
------------------------------------------------------------------------------------ 
| Id  | Operation         | Name | Starts | E-Rows | A-Rows |   A-Time   | Buffers | 
------------------------------------------------------------------------------------ 
|   0 | SELECT STATEMENT  |      |      1 |        |   5710 |00:00:00.01 |     415 | 
|*  1 |  TABLE ACCESS FULL| T1   |      1 |   5745 |   5710 |00:00:00.01 |     415 | 
------------------------------------------------------------------------------------ 
On constate que la cardinalité estimée est très proche de la réalité et que le CBO a choisi de faire un Full Table Scan en ne générant que 415 logical reads. Pour rappel, voici le plan obtenu lorsque le CBO ne disposait d’aucun histogramme pour la colonne C_HB. L’optimiseur avait sous-estimé la cardinalité retournée et avait opté pour un accès indexé générant 806 logical I/Os :
select * from T1 where C_HB=999; 
 
------------------------------------------------------------------------------------------------ 
| Id  | Operation                   | Name   | Starts | E-Rows | A-Rows |   A-Time   | Buffers | 
------------------------------------------------------------------------------------------------ 
|   0 | SELECT STATEMENT            |        |      1 |        |   5710 |00:00:00.01 |     806 | 
|   1 |  TABLE ACCESS BY INDEX ROWID| T1     |      1 |     18 |   5710 |00:00:00.01 |     806 | 
|*  2 |   INDEX RANGE SCAN          | IDX_HB |      1 |     18 |   5710 |00:00:00.01 |     394 | 
------------------------------------------------------------------------------------------------ 
Voyons maintenant ce que donnent les estimations du CBO pour une valeur non populaire mais présente dans l’histogramme:
-- Non popular presente dans l'histogramme 
select * from T1 where C_HB=256; 
 ------------------------------------------------------------------------------------------------ 
| Id  | Operation                   | Name   | Starts | E-Rows | A-Rows |   A-Time   | Buffers | 
------------------------------------------------------------------------------------------------ 
|   0 | SELECT STATEMENT            |        |      1 |        |     24 |00:00:00.01 |      17 | 
|   1 |  TABLE ACCESS BY INDEX ROWID| T1     |      1 |      2 |     24 |00:00:00.01 |      17 | 
|*  2 |   INDEX RANGE SCAN          | IDX_HB |      1 |      2 |     24 |00:00:00.01 |       4 | 
------------------------------------------------------------------------------------------------
Cette fois à l’inverse de la valeur popular le CBO se trompe dans son estimation : 2 lignes estimées contre 24 réellement.

Regardons enfin ce qu’on obtient lorsqu’on utilise une valeur non populaire et n’ayant pas été capturée par le calcul de statistiques :
 -- Non popular non capturée 
select * from T1 where C_HB=3; 
 
------------------------------------------------------------------------------------------------ 
| Id  | Operation                   | Name   | Starts | E-Rows | A-Rows |   A-Time   | Buffers | 
------------------------------------------------------------------------------------------------ 
|   0 | SELECT STATEMENT            |        |      1 |        |      9 |00:00:00.01 |       7 | 
|   1 |  TABLE ACCESS BY INDEX ROWID| T1     |      1 |      2 |      9 |00:00:00.01 |       7 | 
|*  2 |   INDEX RANGE SCAN          | IDX_HB |      1 |      2 |      9 |00:00:00.01 |       3 | 
------------------------------------------------------------------------------------------------
Là encore le CBO ne peut estimer une cardinalité juste et c’est là où le bât blesse avec les histogrammes height-balanced : le CBO n’est capable d’estimer le bon nombre de lignes retournées si et seulement si la valeur requêtée est une valeur populaire, or comme nous l’avons dit précédemment Oracle n’est capable de capturer au mieux que 127 valeurs populaires. Imaginons une table contenant plusieurs millions de valeurs distinctes avec un histogramme height-balanced, le CBO ne sera capable d’estimer une cardinalité fiable que lorsqu’une valeur populaire sera requêtée (c’est-à-dire 0.001% des valeurs dans ce cas). Si cette table faisait l’objet d’une requête très complexe avec beaucoup de jointures le plan d’exécution choisi par le CBO pourrait s’avérer catastrophique.
Pour ceux que ça intéresse je donne ci-dessous les formules appliquées par le CBO pour calculer les cardinalités selon les cas :

Cas d’une valeur populaire :

BUCKET_SIZE = SAMPLE_SIZE / NUM_BUCKETS

BUCKET_SIZE = 14739/254 = 58.027

Cardinalité estimée = FREQUENCY * BUCKET_SIZE

Cardinalité estimée = 99 * 58.027 = 5744.72 ~= 5745 => correspond bien à ce qu’on voit dans le plan pour la colonne E-Rows

Cas d’une valeur NON populaire (capturée ou pas) :

Cardinalité estimée = NUM_ROWS * NewDensity

NewDensity est une fonction interne à Oracle dont le nom apparait dans la 10053 et qui lui permet de faire une estimation en cas de valeurs non populaires pour les colonnes ayant un histogramme de type height-balanced.



CONCLUSION :

La notion d’histogrammes height-balanced est une notion plus complexe à assimiler que celle des histogrammes FREQUENCY néanmoins voici les points qu'il me semble essentiel de retenir :
  • Oracle est dans l’incapacité de calculer des histogrammes de type frequency et calcule donc des histogrammes height-balanced dès lors que le nombre de buckets est inférieur au nombre de valeurs distinctes ou dès lors que le nombre de valeurs distinctes est supérieur à 254 (car le nombre de buckets est limité à 254).
  • Dans les histogrammes height-balanced la colonne ENDPOINT_NUMBER correspond à l’id du bucket et la colonne ENDPOINT_VALUE correspond à la plus grande valeur du bucket.
  • Lorsque le ENDPOINT_VALUE est identique sur plusieurs buckets, Oracle ne stocke que le dernier bucket.
  • Oracle n’est capable de capturer au mieux que 127 valeurs populaires
  • Oracle est capable d’estimer une bonne cardinalité uniquement pour les valeurs populaires
  • Puisque seule la plus grande valeur de chaque bucket est prise en compte une valeur peut devenir populaire un jour puis non populaire un autre jour en fonction du fait qu’elle ait été capturée par le processus de calcul de statistiques ou pas. C’est ce qui rend les histogrammes HB très versatiles et qui en font une des premières sources d’instabilité dans les plans d’exécution.

Nous verrons dans un prochain article comment, avec la 12c et les histogrammes TOP-frequency et Hybrides, Oracle a tenté d’améliorer ses histogrammes.

Aucun commentaire:

Enregistrer un commentaire