J’ai toujours du mal à expliquer concrètement et à me souvenir des explications sur les méthodes utilisées par Oracle pour le calcul des histogrammes sur les colonnes. Voici donc une tentative de simplification et de synthèse qui sera j’espère compréhensible pour le plus grand nombre, nous ne rentrerons pas ici dans le débat de la pertinence de ces histogrammes mais vous pourrez trouver plusieurs informations sur le sujet dans les références citées en fin d’article.
L’idée de départ est claire et simple, il faut que l’optimiseur puisse établir de la manière la plus précise possible la cardinalité (le nombre de valeur) d’un critère sur une colonne. En français, ces informations peuvent être écrites de la manière suivante : dans la colonne “Etat” de ma table des ventes composée de 100 000 lignes, il y en a 90 0000 avec le statut “Close”, 7500 en “En cours” et 2500 en “Anomalie” et c’est parfaitement compréhensible. Pour du code , il a fallu trouver une représentation la plus juste possible tout en étant performant. Particulièrement quand le nombre de possibilités devient important et ou même pour nous avec notre intelligence et notre compréhension, la répétition de l’information va nous faire perdre le fil. Ainsi on imagine très bien que pour une colonne de type “date”, l’énumération du nombre de lignes par date deviendra vite insupportable pour tout auditeur, il faut donc une méthode efficace pour calculer et représenter cette information. Entre l’apparition des histogrammes et la version 12c Oracle utilise maintenant quatre techniques pour répondre à ces problématiques, nous allons voir les principes fondamentaux pour chacune d’entre elle avec plus ou moins de détails. On peut retrouvé les types d’histogramme utilisés dans une des vues *_TAB_COL_STATISTICS (par exemple DBA_TAB_COL_STATISTICS) , la colonne HISTOGRAM de type VARCHAR2(15) peut prendre en version 12C, les valeurs NONE, FREQUENCY, HEIGHT BALANCED, TOP-FREQUENCY et HYBRID <more>.
1) Frequency
Autrement dit par fréquence de valeur. C’est la plus simple à comprendre et sans doute à coder. Lors du calcul , le moteur tri toutes les lignes de la table par la colonne concernée puis compte et enregistre le nombre de lignes par valeur de la colonne. Il le fait en parcourant la liste triée et en notant le numéro de la dernière ligne ou apparait une même valeur (ENDPOINT_NUMBER) . Le nombre d’élément (n) pour une valeur (ENDPOINT_VALUE) est obtenu par la différence entre les deux points de rupture :
n = ENDPOINT_NUMBER (ENDPOINT_VALUE) – ENDPOINT_NUMBER (ENDPOINT_VALUE précédent) .
Les informations END_POINT_VALUE et ENDPOINT_VALUE collectées sont stockées dans le dictionnaire et accessibles au travers des vues *_TAB_HISTOGRAMS.
Voici un exemple pour concrétiser le tout :
Création de la table T1 :
create table T1 tablespace USERS as select * from dba_objects ;
Obtention de la répartition et du nombre de valeurs distinctes avec l’ordre suivant :
select count(*), object_type from T1 group by OBJECT_TYPE order by OBJECT_TYPE ;
Soit 44 types d’objets différents et une distribution bien déséquilibrée dont voici un extrait :
Nombre d’élément | Type |
10 | CLUSTER |
25 | CONSUMER GROUP |
6 | CONTEXT |
2 | DESTINATION |
3 | DIRECTORY |
1 | EDITION |
12 | EVALUATION CONTEXT |
3256 | INDEX |
… | … |
Calcul de l’histogramme par fréquence sur la colonne “OBJECT_TYPE” de la table “T1” :
begin dbms_stats.gather_table_stats( 'SYSTEM', 'T1', method_opt => 'for columns object_type size 254', estimate_percent=>100 ); end; /
On retrouve alors dans le dictionnaire les informations suivantes (extrait des neuf premières ) :
select ENDPOINT_NUMBER , ENDPOINT_VALUE, ENDPOINT_ACTUAL_VALUE from user_tab_histograms where table_name='T1' and column_name='OBJECT_TYPE' ;
ENDPOINT_NUMBER | ENDPOINT_VALUE | ENDPOINT_ACTUAL_VALUE |
10 | 349432112834658000000000000000000000 | CLUSTER |
35 | 349492405467577000000000000000000000 | CONSUMER GROUP |
41 | 349492405757772000000000000000000000 | CONTEXT |
43 | 354482274665871000000000000000000000 | DESTINATION |
46 | 354563320426623000000000000000000000 | DIRECTORY |
47 | 359653496833182000000000000000000000 | EDITION |
59 | 360017943919309000000000000000000000 | EVALUATION CONTEXT |
204 | 365190985547816000000000000000000000 | FUNCTION |
3460 | 380625107598029000000000000000000000 | INDEX |
… | … | … |
le nombre de ligne par type d’objet n’est pas direct , il faut réaliser la différence entre les deux “ENDPOINT_NUMBER” , ainsi le nombre d’index est de (3460 – 204 ) soit 3256 , comme nous l’avions vu précédemment.
Noter que la colonne ENDPOINT_VALUE correspond au codage interne de la valeur (utilisé par le moteur) , ENDPOINT_ACTUAL_VALUE est l’information lisible pour la valeur de la colonne.
Une petite fonction analytique dans une sous-requête permet d’obtenir le résultat directement avec la construction de la colonne “frequency” :
select endpoint_number, endpoint_number - nvl(prev_endpoint,0) frequency, endpoint_value, endpoint_actual_value from ( select endpoint_number, lag(endpoint_number,1) over(order by endpoint_number) prev_endpoint, endpoint_value,endpoint_actual_value from user_tab_histograms where table_name = 'T1' and column_name = 'OBJECT_TYPE' ) order by endpoint_number;
ENDPOINT_NUMBER | FREQUENCY | ENDPOINT_VALUE | ENDPOINT_ACTUAL_VALUE |
10 | 10 | 349432112834658000000000000000000000 | CLUSTER |
35 | 25 | 349492405467577000000000000000000000 | CONSUMER GROUP |
41 | 6 | 349492405757772000000000000000000000 | CONTEXT |
43 | 2 | 354482274665871000000000000000000000 | DESTINATION |
46 | 3 | 354563320426623000000000000000000000 | DIRECTORY |
47 | 1 | 359653496833182000000000000000000000 | EDITION |
59 | 12 | 360017943919309000000000000000000000 | EVALUATION CONTEXT |
204 | 145 | 365190985547816000000000000000000000 | FUNCTION |
3460 | 3256 | 380625107598029000000000000000000000 | INDEX |
… | … | … | … |
Ce type d’histogramme est très pertinent car il donne une image précise de la distribution d’une colonne à un instant t .
2) Height Balanced
Traduction proposée: histogramme de hauteur équilibrée.
Quand le nombre de valeur possible devient élevé, la méthode par fréquence n’est plus performante, il fallait donc trouver autre chose. Le seuil du changement est important à connaitre , c’est 254 valeurs distinctes pour les versions inférieures a la 12C et 1024 ensuite. La première opération du moteur est toujours la même, il va trier les lignes en fonction des valeurs de la colonne. Il subdivise ensuite le résultat en cellule, le nombre de cellules (la fameuse valeur du “Bucket” ) va déterminé l’intervalle des échantillons, car en parcourant la liste, Oracle va considéré la première ligne puis toutes les n lignes suivantes avec
n = nombre de valeurs non nul de la colonne/ nombre de cellules voulues.
Chaque ligne scrutée est associée à une cellule avec un numéro de référence s’incrémentant (c’est l’information « end point value » qui contiendra la valeur contenue). De plus si la même valeur apparaît consécutivement dans plusieurs cellules, seul le numéro de la dernière sera enregistré, en voici un exemple concret :
Soit les 20 valeurs suivante à ranger dans 5 cellules :
5, 6, 6, 6, 9, 11, 11, 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 16, 17, 17
On prend la première ligne et ensuite toutes les 20/5 , 4 lignes suivantes:
5, 6, 6, 6, 9, 11, 11, 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 16, 17, 17.
La première valeur de la liste est enregistré dans la cellule 0 , la seconde dans le cellule 1 et ainsi de suite pour donné le tableau suivant
Cellule | Valeur |
0 | 5 |
1 | 6 |
2 | 12 |
3 | 12 |
4 | 13 |
5 | 17 |
Éliminons ensuite les premières cellules ou l’on retrouve une valeur identique pour ne conserver que la dernière et voilà comment sont enregistrés les informations par le moteur:
Cellule | Valeur |
0 | 5 |
1 | 6 |
3 | 12 |
4 | 13 |
5 | 17 |
Grâce à ces données , Oracle voit que la valeur 12 est une valeur populaire (elle revient dans plus d’une cellule).
On se rend vite compte de l’approximation faite: la valeur 13 apparait autant de fois que la valeur 12 , mais n’est pas considérée comme populaire. Si on change l’un des 11 en 16 , le nouvel arrangement de la liste fait que la valeur 12 n’est plus populaire mais c’est la valeur 13 qui le devient, de même si on remplace le 16 en 11 aucune des valeurs ne sera vue comme populaire:
5, 6, 6, 6, 9, 11, 11,11,12, 12, 12, 12,12, 13, 13, 13, 13, 13, 17, 17.
Pour le calcul de la cardinalité Oracle prend en compte la taille de la cellule , ainsi dans notre exemple ,chaque cellule représente 4 lignes , sachant que la valeur 12 apparait deux fois , la cardinalité (le nombre de ligne avec la valeur 12) est donc de 8 ( 4 * 2) . Le calcul pour les valeurs non populaire a varié suivant les versions , pour la 11.2.0.3 c’est :
(nombre de ligne valeur non populaire / nombre des autres valeurs)
Les informations à disposition pour l’optimiseur serait pour notre petite liste :
- Valeur populaire : 1 (la valeur 12)
- Valeur non populaires : 7 (les valeurs 5,6,9,11,13,16 et 17)
- Cardinalité de la valeur populaire : 8 (nous l’avons vu, 2 casiers de 4)
- Nombre de ligne des autres valeurs : 20 (nombre total) – 8 soit 12
- La cardinalité pour les valeurs non populaires est donc de 12 / 7 soit 1,714 (arrondi à 2)
Ceci est une exemple simple et il existe beaucoup de cas différent traité par le code, imaginer par exemple que le calcul des statistiques soit fait sur un échantillon de 10% de la taille de la table , il faut que le moteur le prenne en compte et mette à l’échelle ses résultats.
Le résultat de cette technique est bien moins précis que la méthode par fréquence, et peut rendre très instable le comportement de l’optimiseur, particulièrement si les valeurs populaires sont majoritaires dans les données, les développeurs ont donc continué à travaillé pour l’améliorer.
3) Top-Frequency
Histogramme de fréquence de niveau supérieur
Cet algorithme et cette méthode qui apparait à partir de la version 12c est une extension du premier, il est choisi par le moteur lorsque le nombre de valeur distincte dépasse le seuil des 254 et les valeurs les plus populaires représentent la majorité des données. Ce serait le cas d’une table représentant les langues les plus parlés dans le monde , sur les près de 6000 existantes, il ne doit y en avoir qu’une petite vingtaine de significatives. Dans ce cadre, seul les valeurs populaires sont traitées comme pour les histogrammes de fréquences classique, les informations pour les valeurs non populaires sont enregistrées dans deux cellules, une pour la valeur haute , l’autre pour la valeur basse.
Les critères de déclenchement sont :
- Le taux d’échantillonnage est de type “AUTOMATIC”
- Le nombre de valeur distinctes est supérieur à 254 (ou au nombre de cellules (“bucket”) demandé)
- Le pourcentage de ligne contenant les valeurs les plus populaires est supérieur au seuil p, calculé de la manière suivante : p = ( 1 – ( 1/nb)) * 100 . nb étant le nombre de cellules utilisées, le nombre de valeurs populaire étant nb –2. Pour 10 cellules demandées (bucket de 10) , le nombre de lignes associé aux 8 valeurs les plus populaires doit être supérieur à 92% du nombre de ligne total .
Ainsi pour notre exemple de départ concernant la table T1 duplication de la vue DBA_OBJECTS, on peut déclencher le calcul de type “TOP-FREQUENCY” par la commande suivante :
begin dbms_stats.gather_table_stats( 'SYSTEM', 'T1', method_opt => 'FOR COLUMNS object_type size 7' ); end; /
Vérification du mode pris par le moteur :
select table_name , column_name, histogram from user_tab_col_statistics where table_name='T2' ;
TABLE_NAME COLUMN_NAME HISTOGRAM ---------- -------------------- --------------- T2 OBJECT_TYPE TOP-FREQUENCY
Le mode d’échantillonnage est bien AUTOMATIC (le défaut de la procédure), le nombre de valeur distinctes (44) est bien supérieur au nombre de cellules demandé (7), les 5 Tops valeurs représentent bien plus de 95% du nombre de lignes , la requête suivante et son résultat sur l’histogramme calculé nous le prouve :
select endpoint_number, endpoint_number - nvl(prev_endpoint,0) frequency, endpoint_actual_value "ACTUAL_VALUE" from ( select endpoint_number, lag(endpoint_number,1) over( order by endpoint_number ) prev_endpoint, endpoint_value, endpoint_actual_value from user_tab_histograms where table_name = 'T2' and column_name = 'OBJECT_TYPE' ) order by endpoint_number ;
ENDPOINT_NUMBER | FREQUENCY | ACTUAL_VALUE |
1 | 1 | CLUSTER |
4428 | 4427 | INDEX |
35486 | 31058 | JAVA CLASS |
72685 | 37199 | SYNONYM |
75222 | 2537 | TYPE |
81434 | 6212 | VIEW |
81435 | 1 | XML SCHEMA |
- Nombre de lignes associé aux valeurs populaires : 4427 + 31058 + 37199 + 2537 + 6212 = 81 433
- Pourcentage de ligne total a dépassé : (1 – (1/7))*100 soit 85,714 % de 91539 lignes = 78 462
4) Hybrid
Histogramme de type hybride, dernier apparu, lui aussi avec la version 12c, il combine les caractéristiques des histogrammes de fréquences et des histogrammes basés sur la hauteur. La première opération est toujours l’ordonnancement des lignes en fonction des valeurs de la colonne. Ensuite on parcours la liste pour remplir nos cellules, le nombre et le pas initial n de la cellule suivent toujours la règle :
n = nombre de valeurs non nul de la colonne/ nombre de cellules voulues.
Par contre la taille de la cellule va pouvoir varier dynamiquement: si le moteur ce rend compte que la dernière valeur de la cellule s’étend sur la cellule suivante, il va agrandir la cellule pour englober complètement la valeur, il en profite au passage pour compter et stocker le nombre de répétition de la valeur la plus haute de la cellule.
Ainsi en reprenant notre exemple des histogrammes basés sur la hauteur et notre série de 20 valeurs à ranger dans 5 cellules :
5, 6, 6, 6, 9, 11, 11, 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 16, 17, 17
Le pas pour se déplacer de cellule en cellule est de 4 lignes, pour la première cellule la valeur est 5 , pour la suivante , la valeur est 6 , pour la troisième, la valeur est 12 (valeur entre parenthèse) mais la valeur suivante est aussi 12 , le moteur agrandit alors la troisième cellule pour qu’elle se termine sur la dernière occurrence de la valeur 12 (celle entre crochet) . la scrutation continue avec le pas de 4 pour la quatrième cellule contenant la valeur 13 (valeur entre parenthèse) , la valeur suivante étant aussi un 13 le moteur agrandit la quatrième cellule jusqu’à la dernière valeur 13 (celle entre crochet) et termine par une dernière cellule contenant la valeur 17.
5, 6, 6, 6, 9, 11, 11, (12) , 12, 12, 12, [12], 13, 13, 13, (13),[13], 16, 17, 17.
Ce qui donne comme information pour notre liste et les valeurs associées aux cellules :
ENDPOINT_NUMBER | ENDPOINT_VALUE | REPEAT_COUNT |
1 | 5 | 1 |
4 | 6 | 3 |
12 | 12 | 5 |
17 | 13 | 5 |
20 | 17 | 2 |
En utilisant cette stratégie, on obtient beaucoup plus d’information que précédemment :
- Les valeurs populaires sont obtenues directement et aucune n’est oubliée via un tri sur le nombre de répétition (repeat_count).
- Pour chaque cellule on détermine le nombre de ligne de l’intervalle en soustrayant le “end_point_number” de la cellule précédente : la cellule 3 est un intervalle de 12 – 4 , 8 lignes
- La valeur la plus haute de l’intervalle correspond à “endpoint_value” (valeur 12 pour la cellule 3)
- La cardinalité de la valeur la plus haute de l’intervalle considéré est immédiate via la valeur de “repeat_count” (le nombre de fois ou apparait la valeur 12 dans la cellule 3 est 5) .
les conditions de déclenchement de cette stratégie de calcul par le moteur sont :
- Le taux d’échantillonnage est de type “AUTOMATIC”
- Le nombre de valeurs distinctes est supérieur à 254 (ou au nombre de cellules (“bucket”) demandé)
- Les conditions du mode “Top frequency” ne s’applique pas (les valeurs populaires ne représente pas la majorité des lignes)
C’est la fin de la présentation de ces quatre méthodes, j’espère que c’est maintenant plus clair pour vous avec ces explications et exemples. Bien sur pour couvrir tous les cas et revoir les avantages des uns par rapport aux autres ou leurs manques il faudrait développer beaucoup plus et s’appuyer sur les lois mathématiques utilisées par Oracle (voir Mr Bernoulli entre autres) mais ce n’était pas le sujet de cet article qui ce veut être seulement un premier pas dans ce domaine.
Remerciements et références: Je n’aurais pas pu comprendre aussi bien les mécanismes en jeux sans les articles pertinents de jonhatan Lewis, particulièrement ceux-ci : http://allthingsoracle.com/histograms-part-1-why/, http://allthingsoracle.com/histograms-pt-2, http://jonathanlewis.wordpress.com/2013/07/30/12c-histograms-pt-2/, http://jonathanlewis.wordpress.com/2013/10/09/12c-histograms-pt-3/ ; Il faut cependant aussi reconnaitre les efforts de l’éditeur dans sa documentation sur le sujet, chapitre 11 de “Oracle Database Tuning Guide” de la version 12c http://docs.oracle.com/cd/E16655_01/server.121/e15858/tgsql_histo.htm#TGSQL366 et si vous souhaitez compléter votre connaissance du sujet, mes collègues ont déjà travaillé le sujet ici http://arkzoyddev.com/2012/10/29/oracle-database-comprendre-les-histogrammes-en-11gr2/?lang=fr ou la : http://arkzoyddev.com/2008/03/25/supprimer-un-histogramme-sur-une-colonne-ou-modifier-des-statistiques-manuellement/?lang=fr