lundi 4 novembre 2019

Row Prefetching et PL/SQL

J'avais écrit il y'a plusieurs années déjà un article qui présentait le row prefetching avec en illustration un problème de performance d'une requête SQL observé chez mon client de l'époque.

Récemment j'ai eu affaire à un problème de performance sur la partie fetching d'une requête exécutée via un curseur PL/SQL. Le rapport SQL Monitor de cette requête indiquait un temps d'exécution global de presque une heure alors que le temps base de données consommé n'était que d'à peine quelques secondes.
De plus, le nombre de fetch calls correspondait quasiment au nombre de lignes retournées par la requête ce qui me faisait clairement comprendre qu'on était face à un problème de mauvaise configuration du row prefetching.

Le développeur avait connaissance du row prefeftching et de son intérêt puisqu'il avait défini l'ARRAYSIZE à 5000 dans son code SQL PLUS.
L'ennui c'est que ce qu'il executait n'était pas une requête SQL mais un code PL/SQL et dans ce cas là le row prefetching n'est plus défini par le paramètre ARRAYSIZE.

Le but de cet article est justement de voir comment fonctionne le row prefetching dans le code PL/SQL.

Commençons par créer une table de travail:
create /*+ parallel(8) */ table T1 as
select * from dba_objects;

select count(*) from T1;


  COUNT(*)
----------
   1378527


1er test: Avec un curseur explicite
Nous allons dans un premier temps utiliser un code PL/SQL qui va récupérer et afficher quelques champs d'1million de lignes de la table en utilisant un curseur explicite :
set serveroutput on
DECLARE

    CURSOR cur IS SELECT /*+ parallel(4) */   object_name, LAST_DDL_TIME
                FROM  t1 where rownum<=1000000;

       v_obj_name varchar2(255);
       v_last_ddl_time DATE;     

BEGIN

    OPEN cur;
    LOOP
        FETCH cur into v_obj_name,v_last_ddl_time  ;
        EXIT WHEN cur%notfound;
                     dbms_output.put_line(v_obj_name);
    END LOOP;
    CLOSE cur;
END;
/


Si l'on jette un oeil au Real-Time SQL Monitor report  ci-dessous on s'aperçoit que la requête s'est exécutée en 7s (DURATION) mais que seulement 2,8s de temps base de données ont été consommé pour executer la requête. Le reste du temps correspondant au fetching. D'ailleurs le nombre de fetch calls indiqué est de 1000001 ce qui correspond à une unité près au nombre de lignes retournées:

Global Information
------------------------------
Status              :  DONE (ALL ROWS)
Instance ID         :  4
Session             :  AHMED (1323:34963)
SQL ID              :  7npr6bxbj8b8k
SQL Execution ID    :  67108865
Execution Started   :  09/13/2019 16:28:31
First Refresh Time  :  09/13/2019 16:28:31
Last Refresh Time   :  09/13/2019 16:28:38
Duration            :  7s
Module/Action       :  SQL*Plus/-
Service             :  #####
Program             :  sqlplus.exe
Fetch Calls         :  1000001

Global Stats
=================================================
| Elapsed |   Cpu   |  Other   | Fetch | Buffer |
| Time(s) | Time(s) | Waits(s) | Calls |  Gets  |
=================================================
|    2.80 |    2.78 |     0.01 |    1M |  19924 |
================================================= 


Le problème avec le test ci-dessus est qu'il s'agit d'un curseur explicite, or avec ce type de curseur il est necessaire d'activer le row prefetching en activant la commande BULK COLLECT:
set serveroutput on
DECLARE
    CURSOR cur IS SELECT /*+ parallel(4) */  *
                FROM  t1 where rownum<=1000000;

    TYPE t_t1 IS   TABLE OF t1%rowtype;
    l_t1  t_t1;
BEGIN
    OPEN cur;
    LOOP
        FETCH cur BULK COLLECT INTO l_t1 LIMIT 1000;
        EXIT WHEN l_t1.count = 0;
        FOR i IN l_t1.first..l_t1.last LOOP
                     dbms_output.put_line(l_t1(i).OBJECT_NAME);
        END LOOP;
    END LOOP;
    CLOSE cur;
END;
/


 Global Information
------------------------------
Status              :  DONE (ALL ROWS)
Instance ID         :  4
Session             :  AHMED (1323:35631)
SQL ID              :  gpn4z88bqcqb2
SQL Execution ID    :  67108865
Execution Started   :  09/13/2019 16:32:37
First Refresh Time  :  09/13/2019 16:32:37
Last Refresh Time   :  09/13/2019 16:32:39
Duration            :  2s
Module/Action       :  SQL*Plus/-
Service             :  ####
Program             :  sqlplus.exe
Fetch Calls         :  1001


La requête a cette fois duré 2s et on voit que le nombre de fetch calls est d'environ 1000. Ce nombre s'explique par la commande LIMIT 1000 que j'ai ajouté dans mon code PL/SQL juste après le BULK COLLECT afin d'indiquer que je souhaite prefeecher 1000 lignes par fetch call. Comme j'ai 1M de lignes à récupérer, 1000000/1000=1000. Si j'avais mis une limite à 10k j'aurais eu besoin que de 100 fectch calls pour rapatrier mes 1M de rows.

Attention toutefois à ne pas mettre une valeur de prefetching trop élévé car les lignes prefetchées sont stockées dans la mémoire du client, et donc un nombre trop important pourrait saturer cette dernière.


2ème test: Avec un curseur implicite
Maintenant nous allons voir comment fonctionne le fecthing lorsqu'on utilise des curseurs implicites. Pour rappel, on appelle curseurs implicites les curseurs utilisés dans le cadre d'une boucle FOR, sans qu'il y'ait ouverture explicite du curseur via la commande OPEN.

set serveroutput on
BEGIN
    for cur in (SELECT /*+ parallel(4) */  *  FROM  t1 where rownum<=1000000)
       LOOP
                     dbms_output.put_line(cur.OBJECT_NAME);
    END LOOP;
END;
/

Global Information
------------------------------
Status              :  DONE (ALL ROWS)
Instance ID         :  4
Session             :  AHMED (1323:35631)
SQL ID              :  gpn4z88bqcqb2
SQL Execution ID    :  67108866
Execution Started   :  09/13/2019 16:37:57
First Refresh Time  :  09/13/2019 16:37:57
Last Refresh Time   :  09/13/2019 16:38:00
Duration            :  3s
Module/Action       :  SQL*Plus/-
Service             :  ####
Program             :  sqlplus.exe
Fetch Calls         :  10001


Cette fois, sans qu'on ait eu à spécifier une valeur de row prefetching, on s'aperçoit qu'Oracle a automatiquement utilisé un prefetching de 100 puisque le nombre de fetch calls constatés est de 10K (1M/100=10K).

CONCLUSION:
A chaque fois que vous avez à ramener des lignes côté client vous devez avoir en tête la notion de row prefetching. En fonction du client utilisé (SQL PLUS, JDBC, .NET etc.) le row prefetching va se configurer différemment.
Pour le PL/SQL le row prefetching est activé automatiquement pour tous les curseurs de type implicite (FOR loops cursors), pour les curseurs explicites il faut utiliser la clause BULK COLLECT et donc adapter son code pour utiliser des collections.







lundi 2 septembre 2019

Updates et Compression HCC (1)



Dans cet article je souhaiterais vous montrer ce qu'il se passe concrètement quand un update est effectué sur des lignes compressées en HCC (compression disponible sur Exadata uniquement) grâce à l'utilisation du package DBMS_COMPRESSION.

Je vais commencer par créer une table en mode compression HCC et y insérer des lignes en mode conventionnel (sans utiliser le hint APPEND):
DROP TABLE T1 PURGE;
create table T1 as select * from all_objects where 1=2;
alter table T1 compress for query high;
-- insert without using DIRECT PATH WRITE (data non compressed)
insert into T1 select * from all_objects;
commit;


Imaginons que je ne sache pas comment ces lignes aient été insérées. En regardant les propriétés de la table indiquant qu'il s'agit d'une table compressée en HCC je pourrais être amené à croire que cette table contient des lignes compressées alors que non puisque pour avoir des lignes compressées en HCC dans Exadata il ne suffit pas de créer une table en mode HCC, il faut également que les lignes soient insérées en mode direct path write (en mode APPEND). Faut vraiment bien garder cela à l'esprit.

La fonction GET_COMPRESSION_TYPE du package DBMS_COMPRESSION permet de savoir quel type de compression est appliqué pour chacune des lignes d'une table:
select  decode (dbms_compression.get_compression_type(user,'T1',rowid),
1, 'NOCOMPRESS',
2, 'COMP_FOR_OLTP',
4, 'COMP_FOR_QUERY_HIGH',
8, 'COMP_FOR_QUERY_LOW',
16,'COMP_FOR_ARCHIVE_HIGH',
32,'COMP_FOR_ARCHIVE_LOW',
64,'COMP_BLOCK',
'OTHER') comp_type, count(*)
from T1
where rownum < 100
group by decode (dbms_compression.get_compression_type(user,'T1',rowid),
1, 'NOCOMPRESS',
2, 'COMP_FOR_OLTP',
4, 'COMP_FOR_QUERY_HIGH',
8, 'COMP_FOR_QUERY_LOW',
16,'COMP_FOR_ARCHIVE_HIGH',
32,'COMP_FOR_ARCHIVE_LOW',
64,'COMP_BLOCK',
'OTHER');

COMP_TYPE          COUNT(*)
---------------- ----------
NOCOMPRESS               99

select trunc(bytes/power(1024,2)) m
from user_segments where segment_name ='T1'; 

         M
----------
       258


La fonction GET_COMPRESSION_TYPE appliquée sur les lignes de ma table indiquent un type de compression à 1 c'est à dire non compressées.

Je vais maintenant supprimer les lignes de la table et y insérer les mêmes lignes en utilisant cette fois le mode DIRECT PATH WRITE:
truncate table T1;
insert /*+ APPEND */ into T1 select * from all_objects;
commit;

select  decode (dbms_compression.get_compression_type(user,'T1',rowid),
1, 'NOCOMPRESS',
2, 'COMP_FOR_OLTP',
4, 'COMP_FOR_QUERY_HIGH',
8, 'COMP_FOR_QUERY_LOW',
16,'COMP_FOR_ARCHIVE_HIGH',
32,'COMP_FOR_ARCHIVE_LOW',
64,'COMP_BLOCK',
'OTHER') comp_type, count(*)
from T1
where rownum < 100
group by decode (dbms_compression.get_compression_type(user,'T1',rowid),
1, 'NOCOMPRESS',
2, 'COMP_FOR_OLTP',
4, 'COMP_FOR_QUERY_HIGH',
8, 'COMP_FOR_QUERY_LOW',
16,'COMP_FOR_ARCHIVE_HIGH',
32,'COMP_FOR_ARCHIVE_LOW',
64,'COMP_BLOCK',
'OTHER');

COMP_TYPE          COUNT(*)
---------------- ----------
FOR_QUERY_HIGH           99

select trunc(bytes/power(1024,2)) m
from user_segments where segment_name ='T1';

         M
----------
        11


Le package DBMS_COMPRESSION nous indique que la compression appliquée aux lignes de la table est bien de type HCC FOR QUERY HIGH, qui est le type de compression qu'on a définit pour notre table lors de sa création.

Notez comment la taille de la table est passée de 258MB à 11MB seulement. C'est assez impressionnant.
Maintenant, je vais exécuter un update sur toutes les lignes de la table:
update T1 set CREATED=SYSDATE;
commit;

select  decode (dbms_compression.get_compression_type(user,'T1',rowid),
1, 'NOCOMPRESS',
2, 'COMP_FOR_OLTP',
4, 'COMP_FOR_QUERY_HIGH',
8, 'COMP_FOR_QUERY_LOW',
16,'COMP_FOR_ARCHIVE_HIGH',
32,'COMP_FOR_ARCHIVE_LOW',
64,'COMP_BLOCK',
'OTHER') comp_type, count(*)
from T1
where rownum <100 br="">group by decode (dbms_compression.get_compression_type(user,'T1',rowid),
1, 'NOCOMPRESS',
2, 'COMP_FOR_OLTP',
4, 'COMP_FOR_QUERY_HIGH',
8, 'COMP_FOR_QUERY_LOW',
16,'COMP_FOR_ARCHIVE_HIGH',
32,'COMP_FOR_ARCHIVE_LOW',
64,'COMP_BLOCK',
'OTHER');

COMP_TYPE               COUNT(*)
--------------------- ----------
COMP_BLOCK                    99

select trunc(bytes/power(1024,2)) m
from user_segments where segment_name ='T1';

         M
----------
        96


Vous remarquez qu'après l'update le type de compression est passé au type 64 qui est en fait un type de compression OLTP.
Que s'est-il passé exactement?
En fait, pour pouvoir modifier des lignes compressées en HCC, Oracle migre ces lignes dans un nouveau block défini en mode OLTP compression. Un pointeur a été laissé dans le block d'origine. On est là devant un cas de ligne migrée c'est à dire qu'il faut lire 2 blocks pour lire une ligne. La taille de la table est passé de 11MB à 96MB ce qui est toujours mieux que la taille en mode non compressée (258MB) mais moins bien que le taux de compression obtenu en HCC.

De plus, lorsqu'un full scan est effectué sur une table et que le moteur Oracle tombe sur une ligne migrée, il bascule automatiquement en mode Block Shipping inhibant ainsi le smart scan.

En résumé, en modifiant des lignes compressées en HCC on casse en quelque sorte cette compression (les lignes sont migrées dans des blocks en mode compression OLTP) et on empêche le offloading de pouvoir s'effectuer sur ces lignes lors d'un SELECT. En gros, les 2 fonctionnalités phares de l'Exadata sont mis à mal à cause de ces updates. Voilà pourquoi les lignes compressées en HCC ne devraient jamais être modifiées. Si un update est necessaire il vaut mieux supprimer et réinsérer ces lignes.

Il existe d'autres contraintes liées aux updates de lignes compressées en HCC (Lock au niveau compression unit, entrées ITL limitées à 1 etc. ) mais j'en parlerai dans un autre post.

samedi 12 novembre 2016

L'importance de la contrainte NOT NULL

Il y'a quelques semaines de cela j'expliquais à un collègue, qui me posait des questions sur le rôle des index dans Oracle, que les accès indexés pouvaient aussi être utilisés par le CBO pour autre chose qu'une récherche de lignes, comme par exemple pour éviter un tri ou un Full Table Scan.
Je décidai alors de lui faire une petite démo rapide:
SQL> create table test1 as select * from dba_objects; 

Table created. 

SQL> select count(*) from test1; 

  COUNT(*)
----------
     92320
     
SQL> create index idx_test1 on test1(object_type); 

Index created. 

J'ai juste crée une table TEST1 de 92320 lignes qui est en fait une copie de DBA_OBJECTS. J'ai également créé un index sur la colonne OBJECT_TYPE.
Ce que je voulais montrer à mon collègue c'était que le CBO était capable de se servir de cet index lors d'un COUNT(*) sur cette table ou bien pour une requête effectuant un tri sur la colonne OBJECT_TYPE.
Sûr de moi je commençai par lui montrer le cas du COUNT(*):
SQL> explain plan for
  2  select count(*) from test1; 

Explained. 

SQL> @plan
SQL> SET LINES 500
SQL> SET PAGES 500
SQL> select * from table(dbms_xplan.display); 

PLAN_TABLE_OUTPUT
-----------------------------------------------------------------------------------
Plan hash value: 3896847026 

--------------------------------------------------------------------
| Id  | Operation          | Name  | Rows  | Cost (%CPU)| Time     |
--------------------------------------------------------------------
|   0 | SELECT STATEMENT   |       |     1 |   423   (1)| 00:00:01 |
|   1 |  SORT AGGREGATE    |       |     1 |            |          |
|   2 |   TABLE ACCESS FULL| TEST1 | 89162 |   423   (1)| 00:00:01 |
-------------------------------------------------------------------- 

Note
-----
   - dynamic statistics used: dynamic sampling (level=2)  
Aie!!!Le CBO a complètement ignoré mon index :-(
Moi qui voulait épater mon collègue j'avais plutôt l'air bête.
Au lieu de prendre le temps de réfléchir j'ai préféré utiliser un hint pour forcer l'utilisation de l'index mais là encore toujours le Full Scan.
Hum...très embarassé, je suis passé alors au test suivant consistant à trier mes lignes de la table sur la colonne indexée OBJECT_TYPE:
SQL> explain plan for select * from test1 order by object_type; 

Explained. 

SQL> @plan
SQL> SET LINES 500
SQL> SET PAGES 500
SQL> select * from table(dbms_xplan.display); 

PLAN_TABLE_OUTPUT
-------------------------------------------------------------------------------------------------------
Plan hash value: 1692556001
------------------------------------------------------------------------------------
| Id  | Operation          | Name  | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     |
------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |       | 89162 |    31M|       | 22161   (1)| 00:00:01 |
|   1 |  SORT ORDER BY     |       | 89162 |    31M|    34M| 22161   (1)| 00:00:01 |
|   2 |   TABLE ACCESS FULL| TEST1 | 89162 |    31M|       |   423   (1)| 00:00:01 |
------------------------------------------------------------------------------------ 

Note
-----
 - dynamic statistics used: dynamic sampling (level=2)
Mon index n'était toujours pas utilisé T_T
J'ai pourtant un index, trié par définition sur le champ OBJECT_TYPE, et le CBO préfère effectuer un full scan de la table suivi d'une opération de tri (SORT ORDER BY).
Pour info, l'opération SORT AGGREGATE du plan précédent n'effectue aucun tri contrairement à ce que son nom pourrait laisser entendre. Il effectue juste un comptage mais en aucun cas il ne trie quoi que ce soit.

Je vous avoue que je suis resté plusieurs minutes complètement incrédule à ce qu'il se passait. Et c'était pourtant d'une évidence absolue.
Un DESCRIBE sur ma table m'aida à refaire jaillir la lumière dans mon esprit:
SQL> desc test1

Name                                                                                                      Null?    Type
----------------------------------------------------------------------------------------------------------------- -------- ------------
OWNER                                                                                                              VARCHAR2(128)
OBJECT_NAME                                                                                                        VARCHAR2(128)
SUBOBJECT_NAME                                                                                                     VARCHAR2(128)
OBJECT_ID                                                                                                          NUMBER
DATA_OBJECT_ID                                                                                                     NUMBER
OBJECT_TYPE                                                                                                        VARCHAR2(23)
CREATED                                                                                                            DATE
LAST_DDL_TIME                                                                                                      DATE
TIMESTAMP                                                                                                          VARCHAR2(19)
STATUS                                                                                                             VARCHAR2(7)
TEMPORARY                                                                                                          VARCHAR2(1)
GENERATED                                                                                                          VARCHAR2(1)
SECONDARY                                                                                                          VARCHAR2(1)
NAMESPACE                                                                                                          NUMBER
EDITION_NAME                                                                                                       VARCHAR2(128)
SHARING                                                                                                            VARCHAR2(13)
EDITIONABLE                                                                                                        VARCHAR2(1)
ORACLE_MAINTAINED                                                                                                  VARCHAR2(1)

Toutes les colonnes sont NULLables.
Vous savez tous qu'un index ne stock pas de valeurs NULL et que donc pour qu'un index puisse être utilisé dans mes 2 requêtes il ne suffit pas qu'il y' ait réellement que des valeurs NULL, il faut que l'optimiseur en soit sûr et certain. C'est ce à quoi sert la contrainte NOT NULL pour le CBO. Elle lui indique qu'il ne peut y avoir de valeurs NULL pour la colonne en question et que donc l'utilisation de l'index est possible sur cette colonne car le CBO  a la certitude que l'index contiendra toutes les lignes de la table.
Ajoutons donc la contrainte NOT NULL à la colonne OBJECT_TYPE et regardons les plans des requêtes précédentes:
SQL> alter table test1 modify (object_type not null);

Table altered. 

SQL> explain plan for
  2  select count(*) from test1; 

Explained. 

SQL> @plan
SQL> SET LINES 500
SQL> SET PAGES 500
SQL> select * from table(dbms_xplan.display);

PLAN_TABLE_OUTPUT
-----------------------------------------------------------------------------------------------
Plan hash value: 2671621383 

---------------------------------------------------------------------------
| Id  | Operation             | Name      | Rows  | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT      |           |     1 |    71   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE       |           |     1 |            |          |
|   2 |   INDEX FAST FULL SCAN| IDX_TEST1 | 89162 |    71   (0)| 00:00:01 |
---------------------------------------------------------------------------

Note
-----
   - dynamic statistics used: dynamic sampling (level=2)



SQL> explain plan for select * from test1  order by object_type;
 
Explained.

SQL> @plan
SQL> SET LINES 500
SQL> SET PAGES 500

SQL> select * from table(dbms_xplan.display); 

PLAN_TABLE_OUTPUT
-----------------------------------------------------------------------------------------------------------
Plan hash value: 2541586222

-----------------------------------------------------------------------------------------
| Id  | Operation                   | Name      | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |           | 89162 |    31M|  4492   (1)| 00:00:01 |
|   1 |  TABLE ACCESS BY INDEX ROWID| TEST1     | 89162 |    31M|  4492   (1)| 00:00:01 |
|   2 |   INDEX FULL SCAN           | IDX_TEST1 | 89162 |       |   258   (1)| 00:00:01 |
----------------------------------------------------------------------------------------- 

Note
-----
   - dynamic statistics used: dynamic sampling (level=2)
Ha ha!!! mon honneur est sauf. Cette fois pour le count(*) le CBO a decidé de parcourir l'index pour compter les lignes au lieu de toute la table, ce qui est logique, le segment INDEX étant plus petit que la table il y'a moins de blocks à lire en faisant un INDEX FAST FULL SCAN. D'ailleurs on voit que le COST n'est que de 71 alors qu'il était de 423 avec le Full Table Scan.
Quant à la deuxième requête on voit que le CBO a opté pour un parcours séquentiel de l'index (INDEX FULL SCAN) évitant ainsi l'opération SORT ORDER BY effectuant le tri. On constate que grâce à cet index le COST du plan est passé de 22161 à 4492.

Pour un index composite il n'est pas necéssaire que toutes les colonnes soient NOT NULL pour qu'il soit pris en compte. Il suffit qu'au moins une des colonnes constituant l'index soit NOT NULL.

CONCLUSION:
Voilà donc comment malgré plusieurs années d'expérience sur Oracle on arrive à se faire avoir sur des choses évidentes qu'on sait depuis longtemps. Tom Kyte disait qu'il se forcait à lire au moins une fois par an la doc Database Concepts car on a tendance à les oublier et ils permettent de faire face à toutes les problématiques.

Cet article a également pour but de mettre en évidence à quel point il est important de donner un maximum d'informations au CBO pour que celui nous choisisse le plan optimal. Les statistiques objets sont certes indispensables mais l'optimiseur a également besoin de connaitre le type de contrainte (NOT NULL, FK, PK) qui lui permettent notamment d'estimer de meilleures cardinalités.

jeudi 27 octobre 2016

Exadata: Pourquoi mon ratio "Cell Offload Efficiency" est-il négatif?

Avec la 11G R2 il est devenu très pratique d'analyser le plan d'exécution d'une requête en utilisant le RTSM (Real-Time SQL Monitoring) puisqu'il permet notamment d'avoir des statistiques d'exécution pour chaque opération du plan ainsi que quelques métriques et ratios utiles. Dans un environnement Exadata un des ratios que l'on peut voir dans un rapport RTSM est le ratio "Cell Offload" qui est censé indiquer le pourcentage d'octects que les cells ont évité d'envoyer aux DB nodes grâce au smart scan.
Voici un exemple :

alter session set "_serial_direct_read"=always;


select /*+ monitor */ * from T1
where last_used_date>to_date('01/01/2014','DD/MM/YYYY');


Global Stats
===================================================================================================
| Elapsed |   Cpu   |    IO    | Application |  Other   | Fetch | Buffer | Read | Read  |  Cell   |
| Time(s) | Time(s) | Waits(s) |  Waits(s)   | Waits(s) | Calls |  Gets  | Reqs | Bytes | Offload |
===================================================================================================
|    0.16 |    0.01 |     0.01 |        0.00 |     0.14 |  1844 |     53 |    2 |   1MB |  12.28% |
===================================================================================================
 

SQL Plan Monitoring Details (Plan Hash Value=1644978049)
===========================================================================================================================================================================
| Id |          Operation          |     Name      |  Rows   | Cost |   Time    | Start  | Execs |   Rows   | Read | Read  |  Cell   |  Mem  | Activity | Activity Detail |
|    |                             |               | (Estim) |      | Active(s) | Active |       | (Actual) | Reqs | Bytes | Offload | (Max) |   (%)    |   (# samples)   |
===========================================================================================================================================================================
|  0 | SELECT STATEMENT            |               |         |      |        10 |     +0 |     1 |    27642 |      |       |         |       |          |                 |
|  1 |   TABLE ACCESS STORAGE FULL | T1            |   27642 |    4 |        10 |     +0 |     1 |    27642 |    2 |   1MB |  12.28% |    2M |          |                 |
===========================================================================================================================================================================
La requête que j'ai exécutée est une requête candidate au smart scan puisque le Direct Path Read a été forcé, et n'existant pas d'index sur T1, le CBO n'a pas d'autre choix que de choisir un full table scan. De plus j'ai appliqué un filtre sur un champ date qui peut être offloadé.
Le rapport RTSM m'indique que ma requête a mis 0.16 sec pour récupérer 27642 lignes. La dernière colonne intitulée Cell Offload dans la section "Global Status" m'informe également que ma requête a bien fait l'objet d'un smart scan et surtout que cet offloading a permis de réduire d'environ 12% le traffic entre les serveurs de stockage et les DB nodes.
Lors de mon précédent article j'avais mis en évidence le fait qu'on pouvait comparer le nombre d'octects lus par les cells sur disque et le nombre d'octects retournés par les cells aux DB nodes en s'appuyant sur les colonnes physical_read_bytes et io_interconnect_bytes de la vue v$sql:
 SQL> SELECT
  2  round(physical_read_bytes/1024) "KB_reads_from disk",
  3  round(io_cell_offload_eligible_bytes/1024) "KB_offloaded",
  4  round(io_interconnect_bytes/1024) "KB_returned_by_cells"
  5  FROM
  6  gv$sql
  7  WHERE sql_id = 'g6kavcjp0mua8' ;
 

KB_reads_from disk KB_offloaded KB_returned_by_cells
------------------ ------------ --------------------
              1312         1312                 1153

La requête a donc généré 1312KB de lectures sur disque mais seulement 1153KB ont été retournés à la couche base de données soit une économie d'environ 12% (1-(1153/1312) =~ 0.12 ), on retrouve le chiffre du ratio indiqué dans le rapport du RTSM.
Jusqu'ici rien d'extraordaire, mais maintenant regardez l'exécution de la table T2 qui est une exacte copie des données de T1:
select /*+ monitor */  * from T2
where last_used_date>to_date('01/01/2014','DD/MM/YYYY');


Global Stats
===============================================================================================================
| Elapsed |   Cpu   |    IO    | Application | Cluster  |  Other   | Fetch | Buffer | Read | Read  |   Cell   |
| Time(s) | Time(s) | Waits(s) |  Waits(s)   | Waits(s) | Waits(s) | Calls |  Gets  | Reqs | Bytes | Offload  |
===============================================================================================================
|    0.54 |    0.01 |     0.02 |        0.00 |     0.00 |     0.50 |  1844 |     11 |    2 | 128KB | -488.24% |
===============================================================================================================
 

SQL Plan Monitoring Details (Plan Hash Value=4114405226)
===============================================================================================================================================================================
| Id |          Operation          |    Name     |  Rows   | Cost |   Time    | Start  | Execs |   Rows   | Read | Read  |   Cell   |  Mem  | Activity |   Activity Detail    |
|    |                             |             | (Estim) |      | Active(s) | Active |       | (Actual) | Reqs | Bytes | Offload  | (Max) |   (%)    |     (# samples)      |
===============================================================================================================================================================================
|  0 | SELECT STATEMENT            |             |         |      |         9 |     +1 |     1 |    27642 |      |       |          |       |          |                      |
|  1 |   TABLE ACCESS STORAGE FULL | T2          |   27642 |    2 |         9 |     +1 |     1 |    27642 |    2 | 128KB | -488.24% |    1M |   100.00 | reliable message (1) |
===============================================================================================================================================================================
Le RTSM nous indique que la requête a mis 0.54 sec au lieu de 0.16 avec T1 pour récupérer le même nombre de lignes. Mais le plus inquiétant est de voir que le ratio est tombé à -488%. Bizarre vous ne trouvez pas?
Jetons un oeil aux colonnes de v$sql pour voir le traffic entre les cells et les DB nodes:
SQL> SELECT
  2  round(physical_read_bytes/1024) "KB_reads_from disk",
  3  round(io_cell_offload_eligible_bytes/1024) "KB_offloaded",
  4  round(io_interconnect_bytes/1024) "KB_returned_by_cells"
 5  FROM
  6  gv$sql
  7  WHERE sql_id = '83vyvg2cvg1b7' ;
 
KB_reads_from disk KB_offloaded KB_returned_by_cells
------------------ ------------ --------------------
               128           96                  768

Non seulement le nombre d'octects lus sur disque pour T2 est inférieur  au nombre d'octects lus pour T1 (128KB vs 1312KB) mais en plus le nombre d'octects renvoyés par les cells aux DB nodes est 6 fois plus élevé que ce qui a été lu sur disque.
Qu'a donc cette table T2 de si particulier? Et bien ce que j'ai ommis de vous dire c'est que la table T2 contairement à la table T1 est une table compressée en ARCHIVE HIGH (un des modes de la compression HCC):
 SQL> select compression from dba_tables where table_name='T1';
 
COMPRESS
--------
DISABLED
 

SQL> select compress_for from dba_tables where table_name='T2';
 
COMPRESS_FOR
------------------------------
ARCHIVE HIGH
  
L' un des avantage de la compression HCC disponible dans Exadata c'est qu'elle réduit sensiblement la taille occupée par les objets sur disque ce qui explique pourquoi le nombre d'octects lus par les cells est bien inférieur pour la table T2. L'autre avantage est que, lorsque la requête fait l'objet d'un smart scan, la décompression, qui est une opération extrêmement coûteuse en CPU, peut se faire au niveau des serveurs de stockage évitant ainsi au DB nodes d'avoir à effectuer ce travail. Dans le cas de ma requête sur T2 c'est ce qui s'est produit: la requête a été offloadée ce qui a permis aux cells de décompresser les données, et ce sont les données décompressées qui ont été envoyées aux DB nodes générant ainsi un traffic plus important. C'est ce qui explique pourquoi ici mon ratio "Cell Offload" est négatif.

Pour prouver que la décompression a été effectuée au niveau des cells il suffit de s'intéresser à la statistique cell CUs sent uncompressed dans v$sesstat qui indique le nombre de Compression Units (CUs) envoyés aux DB nodes  après avoir été décompressés. En comparant la valeur de cette statistique avant et après avoir executé ma requête je peux m'assurer que ce sont des données décompressées qui ont été envoyées:
SQL> SELECT sn.name, ss.value
  2  FROM v$statname sn, v$sesstat ss
  3  WHERE sn.statistic# = ss.statistic#
  4  AND sn.name like ('cell CUs sent uncompressed')
  5  AND ss.sid =(select sid from v$mystat where rownum=1);
 
NAME                                                                  VALUE
---------------------------------------------------------------- ----------
cell CUs sent uncompressed                                          2232010
 
select /*+ monitor */  * from T2
where last_used_date>to_date('01/01/2014','DD/MM/YYYY');
 
NAME                                                                  VALUE
---------------------------------------------------------------- ----------
cell CUs sent uncompressed                                          2232011  -->incrémenté
 
select /*+ monitor */  * from T1
where last_used_date>to_date('01/01/2014','DD/MM/YYYY');
 
NAME                                                                  VALUE
---------------------------------------------------------------- ----------
cell CUs sent uncompressed                                          2232011  --> non incrémenté
Le test ci-dessus nous montre qu'après avoir exécuté la requête sur T2 la statistique "cell CUs sent uncompressed" est incrémenté de 1 alors qu'après la requête sur T1 la stats n'est pas incrémentée.

Dans les environnements de type Data Warehouse il est très fréquent de voir les données compressées en mode HCC et donc des ratios dans vos rapports RTSM qui n'indiquent pas vraiment l'efficacité du smart scan.

La compression n'est pas la seule raison pouvant expliquer un ratio Cell Offload négatif. En effet, la metric physical_read_bytes indique non seulement les octects lus sur disque mais également les octects écrits sur disque. Par exemple, toutes les opérations qui induisent une écriture comme celles dans le tablespace temporaire suite à un HASH JOIN ou un tri vont avoir une incidence sur ce ratio d'autant plus que le mirroring ASM va doubler (voir tripler) le nombre d'octects mesurés.

C'est pour toutes ces raisons qu'il faut lire avec des pincettes les valeurs du ration Cell Offload que vous obtenez dans vos rapport RTSM. Elles peuvent ne pas refléter réellement l'efficacité de vos requêtes faisant l'objet d'un offloading.



dimanche 9 octobre 2016

Exadata - Plan d'exécution et Smart scan

J'ai été amené ces derniers mois à travailler sur des environnements Exadata et je me suis rendu compte que beaucoup de développeurs et DBAs se trompaient dans leurs manières de s'assurer qu'une requête avait été offloadée ou pas, ce qui est fort dommage car ne pas bénéficier du OFFLOADING (aka SMART SCAN), c'est se priver sans le savoir de ce pour quoi Exadata a été conçu au départ.

Pour ceux qui ne le savent pas, le smart scan est la fonctionnalité propre à la plateforme Exadata permettant de déporter au niveau de la couche de stockage le traitement de la donnée (d'où le terme anglais de offloading). La selection des colonnes ainsi que les filtres des lignes au niveau de la table ne se font ainsi plus au niveau de la base de données mais au niveau des serveurs de stockages (aka cells) permettant ainsi de réduire le volume de données renvoyées à la base. Ce smart scan ne s'applique qu'aux full scans de segments (tables, index, vues materialisées etc.).

Voici un plan d'exécution impliquant un full scan d'une table dans un environnement Exadata:
 
SQL> explain plan for
 2 select object_name from T1 where owner='SYS';
 

Explained.
 

SQL> @plan
SQL> SET LINES 500
SQL> SET PAGES 500
SQL> select * from table(dbms_xplan.display);
 

PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------
Plan hash value: 3617692013 

----------------------------------------------------------------------------------
| Id  | Operation                 | Name | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT          |      |  2124 | 59472 |   182   (5)| 00:00:01 |
|*  1 |  TABLE ACCESS STORAGE FULL| T1   |  2124 | 59472 |   182   (5)| 00:00:01 |
----------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
--------------------------------------------------- 

   1 - storage("OWNER"='SYS')
       filter("OWNER"='SYS')
Cette requête est une requête qui est censée bénéficier du smart scan car elle ne selectionne qu'une seule colonne et possède une clause de filtrage. Le smart scan consisterait donc pour les cells à ne renvoyer à la couche database que la colonne OBJECT_NAME et les lignes correspondant à la clause OWNER='SYS'. Dans un environnement classique (non-exadata), un full scan irait chercher sur disque tous les blocs de la table T1, et le process server se chargerait de filtrer les lignes et les colonnes.

Si on regarde le plan d'exécution ci-dessus on constate que l'opération TABLE ACCESS FULL a été renommée en TABLE ACCESS STORAGE FULL. Ce qui change ici c'est le mot STORAGE qui indique qu'on est dans un environnement Exadata mais, l'erreur qui est souvent commise est de croire que cela indique la réalisation d'un smart scan.

Notez également la clause storage("OWNER"='SYS') dans la section "Predicate Information", celle-ci est ajouté à la clause filter("OWNER"='SYS'). La clause storage indique que ce prédicat pourrait être offloadé au niveau de la couche storage mais ne garantit pas que cela a réellement eu lieu. Pour le prouver, je vais exécuter la requête, et bien que le plan indique STORAGE dans la partie predicate je peux vous garantir que le SMART SCAN n'aura pas lieu:
SQL_ID  72v0k4fxst8cu, child number 0
-------------------------------------
select /* TEST_01 */ object_name from T1 where owner='SYS'
 

Plan hash value: 3617692013 

--------------------------------------------------------------------------------------------
| Id  | Operation                 | Name | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
--------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT          |      |      1 |        |    206K|00:00:00.20 |   24136 |
|*  1 |  TABLE ACCESS STORAGE FULL| T1   |      1 |   2124 |    206K|00:00:00.20 |   24136 |
--------------------------------------------------------------------------------------------
 

Predicate Information (identified by operation id):
---------------------------------------------------
 

   1 - storage("OWNER"='SYS')

       filter("OWNER"='SYS')

 

SQL> select IO_CELL_OFFLOAD_ELIGIBLE_BYTES from gv$sql where sql_id='72v0k4fxst8cu';
 

IO_CELL_OFFLOAD_ELIGIBLE_BYTES
------------------------------
                             0
Voici donc un moyen sûr de savoir si le SMART SCAN a eu lieu. Il suffit de regarder la colonne IO_CELL_OFFLOAD_ELIGIBLE_BYTES de V$SQL qui indique le nombre d'octects traités via un smart scan. Si cette colonne a une valeur supéreure à zéro alors cela indique qu'un OFFLOADING a été effectué pour la requête en question.

Après l'exécution de ma requête la colonne
IO_CELL_OFFLOAD_ELIGIBLE_BYTES est à zéro on en conclut donc que le offloading ne s'est pas produit et pourtant j'ai bien le mot STORAGE présent dans le plan et dans la section predicate. Cela prouve bien qu'il ne faut surtout pas se fier à cette information pour s'assurer que le smart scan a été effectué.

Vous vous interrogez alors surement sur la raison pour laquelle la requête n'a pas pu être offloadée? Je suis un petit cachotier, et ce que je ne vous ai pas dit c'est qu'avant d'exécuter ma requête j'ai setté le paramètre "_serial_direct_read" à NEVER ce qui empêche les lectures en direct path d'avoir lieu pour les requêtes exécutées en mode serial.

Ce qu'il faut absolument assimiler c'est que non seulement le SMART SCAN ne peut avoir lieu que si l'opération est un full scan de segment mais il faut également que ce full scan se fasse en mode DIRECT PATH READ (c'est à dire lorsque le process server bypass le buffer cache pour mettre les données dans la PGA).

Ma requête ne s'exécutant pas en parallèle et le paramètre "_serial_direct_read" étant setté à NEVER le full scan de ma table T1 ne peut se faire en mode DIRECT PATH READ et le offloading est donc d'office inhibé.

Gardez également à l'esprit que le DIRECT PATH READ n'est pas une décision de l'optimiseur mais un choix effectué par le moteur Oracle à l'exécution de la requête.


Voyons maintenant ce que donne l'exécution de la requête lorsqu'on autorise le DIRECT PATH READ:
SQL> alter session set "_serial_direct_read"=always;
 
Session altered. 

SQL_ID  1tuc7v88bu0fq, child number 0
-------------------------------------
select /* TEST_02 */ object_name from T1 where owner='SYS'
 

Plan hash value: 3617692013 

--------------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                 | Name | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads  |  OMem |  1Mem | Used-Mem |
--------------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT          |      |      1 |        |    206K|00:00:00.20 |   10412 |  10393 |       |       |          |
|*  1 |  TABLE ACCESS STORAGE FULL| T1   |      1 |   2124 |    206K|00:00:00.20 |   10412 |  10393 |  1025K|  1025K| 7199K (0)|
--------------------------------------------------------------------------------------------------------------------------------
 

Predicate Information (identified by operation id):
---------------------------------------------------
 

   1 - storage("OWNER"='SYS')

       filter("OWNER"='SYS')
       

SQL> select IO_CELL_OFFLOAD_ELIGIBLE_BYTES from gv$sql where sql_id='1tuc7v88bu0fq';
 

IO_CELL_OFFLOAD_ELIGIBLE_BYTES
------------------------------
                     340525056
J'obtiens exactement le même plan d'exécution que precédemment mais cette fois la colonne IO_CELL_OFFLOAD_ELIGIBLE_BYTES de V$SQL est supérieure à zéro (340525056/1024/1024=324,7MB) ce qui m'indique que la requête a bien été offloadée et que donc le scan s'est fait en mode DIRECT PATH READ.

On a également d'autres colonnes dans V$SQL qui nous donnent plus d'information sur le smart scan opéré. Il s'agit des colonnes PHYSICAL_READ_BYTES et IO_INTERCONNECT_BYTES. La première indiquant le nombre d'octects lus sur disque et la deuxième le nombre d'octects retournés par les cells à la base. Donc en comparant la colonne PHYSICAL_READ_BYTES à la colonne IO_INTERCONNECT_BYTES on peut savoir le nombre d'octects envoyées réellement à la base grâce au smart scan:
SQL> SELECT
  2  round(physical_read_bytes/1024/1024),
  3  round(io_cell_offload_eligible_bytes/1024/1024),
  4  round(io_interconnect_bytes/1024/1024)
  5  FROM
  6  gv$sql
  7  WHERE sql_id = '1tuc7v88bu0fq' ;
 

ROUND(PHYSICAL_READ_BYTES/1024/1024) ROUND(IO_CELL_OFFLOAD_ELIGIBLE_BYTES/1024/1024) ROUND(IO_INTERCONNECT_BYTES/1024/1024)
------------------------------------ ----------------------------------------------- --------------------------------------
                                 325                                             325                              7 

Ainsi, on voit que sur les 325MB lus sur disque la totalité a été effectué en utilisant le smart scan et que grâce à cela seulement 7MB ont été retournés aux serveurs de données. Sans Exadata et sa capacité à offloader, la base aurait dû traiter les 325MB de données.

Il existe d'autres manières de s'assurer que le smart scan a bien été effectué (trace 10046, RealTime SQL Monitor) mais je ne les détaillerai pas ici.
Le but de cet article était surtout de mettre en évidence le fait de ne pas se fier aux opérations visibles dans le plan d'exécution d'une requête pour s'assurer que celle-ci avait été offloadée au niveau des cells. Le plan indique juste si la requête est candidate au smart scan ou pas.

Pour ceux d'entre vous qui découvrent Exadata à travers cet article vous vous êtes peut-être etonnés du fait que j'ai utilisé parfois le terme de OFFLOADING et parfois celui de SMART SCAN? Ces deux termes sont synonymes et désignent la même chose. Par contre, cette fonctionnalité d'Exadata renferment plus de fonctionnalités que celles que j'ai mentionnées dans cet article comme les storage indexes ou le join filtering mais j'aurai surement l'occasion d'en parler ultérieurement.

samedi 5 mars 2016

ORA-14257: cannot move partition other than a Range, List, System, or Hash

Si lors d'un move de partition vous tombez sur l'erreur suivante "ORA-14257: cannot move partition other than a Range, List, System, or Hash partition", il est fort probable que vous avez essayé de déplacer des partitions qui contiennent des sous partitions. C'est ce qui m'est arrivé récemment lorsque j'ai voulu déplacer les partitions P_MINVAL et P_MAXVAL d'une des tables de mon client dans un autre tablepsace:
SQL> ALTER TABLE FORCE.CLIENT_DATA_DETAILS_PART MOVE PARTITION P_MAXVAL TABLESPACE USERS_LC32_DATA01; 
ALTER TABLE FORCE.CLIENT_DATA_DETAILS_PART MOVE PARTITION P_MAXVAL TABLESPACE USERS_LC32_DATA01 
                                                          * 
ERROR at line 1: 
ORA-14257: cannot move partition other than a Range, List, System, or Hash 
partition
Je me suis alors rendu compte que cette table avait des sous partitions et j'ai donc généré grâce à la requête suivante les commandes qui vont faire le move de toutes les sous partitions de la table CLIENT_DATA_DETAILS_PART:
select 'ALTER TABLE FORCE.' || TABLE_NAME || ' MOVE SUBPARTITION ' || SUBPARTITION_NAME || ' TABLESPACE USERS_LC32_DATA01;' 
FROM dba_tab_subpartitions 
where table_owner = 'FORCE' 
and table_name = ('CLIENT_DATA_DETAILS_PART') 
and partition_name in ('P_MINVAL', 'P_MAXVAL'); 
 
ALTER TABLE FORCE.CLIENT_DATA_DETAILS_PART MOVE SUBPARTITION P_MAXVAL_ASIA TABLESPACE USERS_LC32_DATA01; 
ALTER TABLE FORCE.CLIENT_DATA_DETAILS_PART MOVE SUBPARTITION P_MAXVAL_US TABLESPACE USERS_LC32_DATA01; 
ALTER TABLE FORCE.CLIENT_DATA_DETAILS_PART MOVE SUBPARTITION P_MAXVAL_EUROPE TABLESPACE USERS_LC32_DATA01; 
ALTER TABLE FORCE.CLIENT_DATA_DETAILS_PART MOVE SUBPARTITION P_MAXVAL_EUROPE_4 TABLESPACE USERS_LC32_DATA01; 
ALTER TABLE FORCE.CLIENT_DATA_DETAILS_PART MOVE SUBPARTITION P_MAXVAL_EUROPE_5 TABLESPACE USERS_LC32_DATA01; 
ALTER TABLE FORCE.CLIENT_DATA_DETAILS_PART MOVE SUBPARTITION P_MAXVAL_EUROPE_6 TABLESPACE USERS_LC32_DATA01; 
............................... 
ALTER TABLE FORCE.CLIENT_DATA_DETAILS_PART MOVE SUBPARTITION P_MINVAL_EUROPE_15 TABLESPACE USERS_LC32_DATA01; 
ALTER TABLE FORCE.CLIENT_DATA_DETAILS_PART MOVE SUBPARTITION P_MINVAL_EUROPE_16 TABLESPACE USERS_LC32_DATA01; 
ALTER TABLE FORCE.CLIENT_DATA_DETAILS_PART MOVE SUBPARTITION P_MINVAL_EUROPE_17 TABLESPACE USERS_LC32_DATA01; 
ALTER TABLE FORCE.CLIENT_DATA_DETAILS_PART MOVE SUBPARTITION P_MINVAL_EUROPE_18 TABLESPACE USERS_LC32_DATA01; 
ALTER TABLE FORCE.CLIENT_DATA_DETAILS_PART MOVE SUBPARTITION P_MINVAL_EUROPE_19 TABLESPACE USERS_LC32_DATA01; 
ALTER TABLE FORCE.CLIENT_DATA_DETAILS_PART MOVE SUBPARTITION P_MINVAL_EUROPE_20 TABLESPACE USERS_LC32_DATA01; 
ALTER TABLE FORCE.CLIENT_DATA_DETAILS_PART MOVE SUBPARTITION P_MINVAL_EUROPE_21 TABLESPACE USERS_LC32_DATA01; 
ALTER TABLE FORCE.CLIENT_DATA_DETAILS_PART MOVE SUBPARTITION P_MINVAL_EUROPE_22 TABLESPACE USERS_LC32_DATA01; 
ALTER TABLE FORCE.CLIENT_DATA_DETAILS_PART MOVE SUBPARTITION P_MINVAL_EUROPE_23 TABLESPACE USERS_LC32_DATA01; 
ALTER TABLE FORCE.CLIENT_DATA_DETAILS_PART MOVE SUBPARTITION P_MINVAL_EUROPE_24 TABLESPACE USERS_LC32_DATA01; 
ALTER TABLE FORCE.CLIENT_DATA_DETAILS_PART MOVE SUBPARTITION P_MINVAL_EUROPE_25 TABLESPACE USERS_LC32_DATA01; 
ALTER TABLE FORCE.CLIENT_DATA_DETAILS_PART MOVE SUBPARTITION P_MINVAL_EUROPE_26 TABLESPACE USERS_LC32_DATA01; 
ALTER TABLE FORCE.CLIENT_DATA_DETAILS_PART MOVE SUBPARTITION P_MINVAL_EUROPE_27 TABLESPACE USERS_LC32_DATA01; 
ALTER TABLE FORCE.CLIENT_DATA_DETAILS_PART MOVE SUBPARTITION P_MINVAL_EUROPE_28 TABLESPACE USERS_LC32_DATA01; 
ALTER TABLE FORCE.CLIENT_DATA_DETAILS_PART MOVE SUBPARTITION P_MINVAL_EUROPE_29 TABLESPACE USERS_LC32_DATA01; 
ALTER TABLE FORCE.CLIENT_DATA_DETAILS_PART MOVE SUBPARTITION P_MINVAL_EUROPE_30 TABLESPACE USERS_LC32_DATA01; 
ALTER TABLE FORCE.CLIENT_DATA_DETAILS_PART MOVE SUBPARTITION P_MINVAL_OTHER TABLESPACE USERS_LC32_DATA01; 
Si mes sous partitions se trouvent désormais dans le tablespace USERS_LC32_DATA01 comme je le souhaitais j'ai toujours les partitions mères de ces sous partitions qui pointent dans le mauvais tablespace et bien sûr il est toujours impossible de faire un move de ces partitions:
SQL> select distinct tablespace_name 
  2  FROM dba_tab_subpartitions 
  3  where table_owner = 'FORCE' 
  4  and table_name = ('CLIENT_DATA_DETAILS_PART') 
  5  and partition_name in ('P_MINVAL', 'P_MAXVAL'); 
 
TABLESPACE_NAME 
------------------------------ 
USERS_LC32_DATA01 
 
SQL> select tablespace_name 
  2  FROM dba_tab_partitions 
  3  WHERE table_owner = 'FORCE' AND table_name in ('CLIENT_DATA_DETAILS_PART') 
  4   AND partition_name in ('P_MINVAL','P_MAXVAL'); 
 
TABLESPACE_NAME 
------------------------------ 
FORCE_HP32_CURRENT1 
FORCE_HP32_CURRENT1 
 
SQL> ALTER TABLE FORCE.CLIENT_DATA_DETAILS_PART MOVE PARTITION P_MAXVAL TABLESPACE USERS_LC32_DATA01; 
ALTER TABLE FORCE.CLIENT_DATA_DETAILS_PART MOVE PARTITION P_MAXVAL TABLESPACE USERS_LC32_DATA01 
                                                          * 
ERROR at line 1: 
ORA-14257: cannot move partition other than a Range, List, System, or Hash 
partition 
Je souhaiterais pourtant que ces partitions pointent sur le tablespace USERS_LC32_DATA01 et non pas FORCE_HP32_CURRENT1.
La solution consiste simplement à modifier le DEFAULT ATTRIBUTES pour ces partitions:
alter table FORCE.CLIENT_DATA_DETAILS_PART modify default attributes for partition P_MINVAL tablespace USERS_LC32_DATA01; 
alter table FORCE.CLIENT_DATA_DETAILS_PART modify default attributes for partition P_MAXVAL tablespace USERS_LC32_DATA01; 
 
SQL> alter table FORCE.CLIENT_DATA_DETAILS_PART modify default attributes for partition P_MAXVAL tablespace USERS_LC32_DATA01; 
 
Table altered. 
 
SQL> alter table FORCE.CLIENT_DATA_DETAILS_PART modify default attributes for partition P_MINVAL tablespace USERS_LC32_DATA01; 
 
Table altered. 
    
SQL> select tablespace_name 
  2  FROM dba_tab_partitions 
  3  WHERE table_owner = 'FORCE' AND table_name in ('CLIENT_DATA_DETAILS_PART') 
  4   AND partition_name in ('P_MINVAL','P_MAXVAL'); 
 
TABLESPACE_NAME 
------------------------------ 
USERS_LC32_DATA01 
USERS_LC32_DATA01
Comme vous pouvez le constater, mes partitions pointent désormais bien sur le nouveau tablespace USERS_LC32_DATA01.

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.

dimanche 20 décembre 2015

Les histogrammes (2): Frequency


Dans l’article précédent nous avons vu que la cardinalité pour la colonne C_FREQ était mal estimée par le CBO car il lui manquait l’information sur la distribution des valeurs de cette colonne :
select * from T1 where C_FREQ=3; 
 
------------------------------------------------------------------------------------ 
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | 
------------------------------------------------------------------------------------ 
| 0 | SELECT STATEMENT | | 1 | | 10 |00:00:00.01 | 35 | 
|* 1 | TABLE ACCESS FULL| T1 | 1 | 4913 | 10 |00:00:00.01 | 35 | 
------------------------------------------------------------------------------------

Cette information peut être apportée en calculant un histogramme sur la colonne. Jusqu’à la version 11G on avait 2 types d’histogrammes : FREQUENCY et HEIGHT-BALANCED. Les histogrammes de type FREQUENCY vont stocker le nombre de lignes (la fréquence) de chaque valeur ce qui permet d’obtenir des cardinalités très fiables. Cependant ces histogrammes sont limités à des colonnes ayant moins de 254 valeurs distinctes.

Voici comment on calcule des statistiques avec histogrammes pour la colonne C_FREQ :

exec dbms_stats.gather_table_stats(user,'T1', method_opt=>'for columns C_FREQ size 3');

Le chiffre 3 indique que je souhaite utiliser 3 buckets puisqu’on a que 3 valeurs distinctes. Même si j'utilisais une valeur supérieure Oracle ne prendrait en compte que 3 buckets pour cette colonne. 
On peut vérifier qu’un histogramme de type FREQUENCY a bien été calculé en questionnant la table USER_TAB_COL_STATISTICS :
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_FREQ'; 
 
COLUMN_NAME                    NUM_DISTINCT    DENSITY  NUM_NULLS NUM_BUCKETS SAMPLE_SIZE HISTOGRAM 
------------------------------ ------------ ---------- ---------- ----------- ----------- ---------- 
C_FREQ                                    3 ,000033924          0           3       14739 FREQUENCY


On peut également visualiser le contenu de l’histogramme en requêtant la vue USER_TAB_HISTOGRAMS : 
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_FREQ'; 
 
COLUMN_VALUE CUMMULATIVE_FREQUENCY  FREQUENCY 
------------ --------------------- ---------- 
           1                 13257      13257 
           2                 14729       1472 
           3                 14739         10

La colonne ENDPOINT_VALUE correspond à la valeur de la colonne et la colonne ENDPOINT_NUMBER correspond au nombre cumulé de lignes. Ainsi, pour obtenir le nombre de lignes (la fréquence) pour la valeur 2 il suffit de soustraire au ENDPOINT_NUMBER de la valeur 2 le ENDPOINT_NUMBER de la valeur 1 (14729-13257=1472), d’où l’utilisation de la fonction LAG dans la requête ci-dessus.
On avait vu dans l’article précédent que sans histogrammes le CBO n’arrivait pas à estimer la cardinalité pour C_FREQ=3:
select * from T1 where C_FREQ=3; 
 
------------------------------------------------------------------------------------ 
| Id  | Operation         | Name | Starts | E-Rows | A-Rows |   A-Time   | Buffers | 
------------------------------------------------------------------------------------ 
|   0 | SELECT STATEMENT  |      |      1 |        |     10 |00:00:00.01 |      35 | 
|*  1 |  TABLE ACCESS FULL| T1   |      1 |   4913 |     10 |00:00:00.01 |      35 | 
------------------------------------------------------------------------------------

Voyons donc maintenant ce qu’estime le CBO avec l’histogramme FREQUENCY en place pour cette colonne :
alter system flush shared_pool; 
select * from T1 where C_FREQ=3; 
 
-------------------------------------------------------------------------------------------------- 
| Id  | Operation                   | Name     | Starts | E-Rows | A-Rows |   A-Time   | Buffers | 
-------------------------------------------------------------------------------------------------- 
|   0 | SELECT STATEMENT            |          |      1 |        |     10 |00:00:00.01 |       5 | 
|   1 |  TABLE ACCESS BY INDEX ROWID| T1       |      1 |     10 |     10 |00:00:00.01 |       5 | 
|*  2 |   INDEX RANGE SCAN          | IDX_FREQ |      1 |     10 |     10 |00:00:00.01 |       3 | 
--------------------------------------------------------------------------------------------------

L’estimation match cette fois parfaitement la réalité (E-Rows=A-Rows=10). Du coup, le CBO opte pour un accès indexé et le nombre de logical reads est divisé par 5.

Cet exemple montre bien qu’un histogramme Frequency permet d’obtenir une cardinalité égale ou proche de la réalité. Cette justesse au niveau de l’estimation vient du fait que pour les histogrammes FREQUENCY il existe autant de buckets que de valeurs distinctes, ainsi, toutes les valeurs présentes dans la table sont populaires à condition bien sûr qu’un calcul de statistiques ait eu lieu au moment où cette distribution non uniforme des données était présente. En effet, imaginons une nouvelle ligne insérée dans la table T1 avec C_FREQ = 4, la valeur 4 n’étant pas présente dans l’histogramme FREQUENCY le CBO ne peut estimer correctement la cardinalité pour cette valeur. Il est donc primordial d’avoir un calcul de statistiques qui tourne toujours au juste moment.

Dans le prochain article nous verrons comment Oracle calcule les histogrammes HEIGHT-BALANCED pour répondre aux problématiques des colonnes de plus 254 valeurs distinctes.

lundi 30 novembre 2015

Les histogrammes (1): Introduction

J’ai remarqué que le sujet des histogrammes était souvent très mal assimilé même chez les DBAs et les développeurs les plus expérimentés alors qu’ils constituent un concept fondamental en matière de Tuning SQL.

Je vais tenter à travers une série d’articles de vous présenter ce qu’il est nécessaire de savoir sur le sujet : à quoi servent les histogrammes ? Comment on les calcule ? Quels sont les différents types d’histogrammes ? Comment sont-ils stockés en base ? Quels sont leurs avantages et inconvénients ?

Dans un premier temps je vais me limiter à la version 11g puis nous verrons dans un futur article que la 12c a apporté pas mal de changements. 

 Exemple:

create table T1 as  
select rownum id, decode(mod(rownum,10),0,2,1) c_freq, nvl(blocks,999) c_hb  
from dba_tables ; 
 
update T1 set c_freq=3 where rownum<=10; 
commit; 
 
create index idx_freq on T1(C_FREQ); 
create index idx_hb on T1(C_HB); 
 
select c_freq,count(*) from T1 group by c_freq order by 2 desc; 
 
    C_FREQ   COUNT(*) 
---------- ---------- 
         1      13257 
         2       1472 
         3         10 
 
          
select c_hb,count(*) from T1 group by c_hb order by 2 desc; 
 
      C_HB   COUNT(*) 
---------- ---------- 
       999       5710 
         0       4601 
         4       1853 
         5        237 
         8        195 
        19        169 
         1        152 
        16         84 
        13         70 
         6         47 
        32         34 
         7         29 
        20         26 
       256         24 
         9         23 
     .......................... 
      5046          1 
       
829 rows selected.

Notre exemple est basé sur une table de 14 739 lignes ayant 3 colonnes : une colonne ID qui est unique, une colonne C_FREQ possédant 3 valeurs distinctes et une colonne C_HB contenant 829 valeurs distinctes. On a également un index sur la colonne C_FREQ et un autre sur la colonne C_HB. Les deux requêtes de comptage mettent en évidence que la distribution des données des deux colonnes n’est pas répartie de manière uniforme, c’est-à-dire que certaines valeurs se répètent beaucoup plus que d’autres. Ainsi, selon la popularité de la valeur de la colonne utilisée dans la requête on utilisera soit un accès indexé soit un Full Scan de la table.
 
Estimations du CBO sans histogrammes
Dans un premier temps on collecte des statistiques pour la table T1 et ses colonnes mais sans calculer d’histogrammes :

exec dbms_stats.gather_table_stats (user, 'T1', method_opt=>'for all columns size 1'); 
 
select num_rows from user_tables where table_name='T1'; 
 
  NUM_ROWS 
---------- 
     14739 
 
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_FREQ'; 
 
COLUMN_NAME                    NUM_DISTINCT    DENSITY  NUM_NULLS NUM_BUCKETS SAMPLE_SIZE HISTOGRAM 
------------------------------ ------------ ---------- ---------- ----------- ----------- ----------- 
C_FREQ                                    3 ,333333333          0           1       14739 NONE
Le fait d’avoir écrit « FOR ALL COLUMNS SIZE 1 » pour le paramètre METHOD_OPT indique qu’on veut calculer des statistiques pour toutes les colonnes mais sans histogrammes (c’est ce que signifie le SIZE 1). La colonne HISTOGRAM de la vue USER_TAB_COL_STATISTICS est à NONE ce qui confirme bien qu’aucun histogramme n’a été calculé pour la colonne C_FREQ de la table T1.
Voyons maintenant ce qu’estime le CBO lorsqu’on requête les lignes de la table T1 avec C_FREQ = 3 :

select * from T1 where C_FREQ=3; 
 
------------------------------------------------------------------------------------ 
| Id  | Operation         | Name | Starts | E-Rows | A-Rows |   A-Time   | Buffers | 
------------------------------------------------------------------------------------ 
|   0 | SELECT STATEMENT  |      |      1 |        |     10 |00:00:00.01 |      35 | 
|*  1 |  TABLE ACCESS FULL| T1   |      1 |   4913 |     10 |00:00:00.01 |      35 | 
------------------------------------------------------------------------------------
En comparant les colonnes E-Rows et A-Rows du plan on s’aperçoit que le CBO s'est trompé puisqu’il a estimé une cardinalité de 4913 lignes au lieu de 10 réellement retournées. Cette erreur (qui n’en est pas vraiment une) s’explique par le fait qu’en l'absence d'histogrammes le CBO suppose que les lignes sont uniformément réparties dans la table et il applique donc la formule suivante pour estimer la cardinalité retournée:
NUM_ROWS/NUM_DISTINCT = 14739/3 = 4913
A cause du nombre important de lignes estimées, le CBO a opté pour un FULL TABLE SCAN alors qu’un accès via l’index aurait été plus efficace.
Voyons maintenant ce qu’estime le CBO lorsqu’on filtre sur la colonne C_HB qui contient un nombre bien plus important de valeurs distinctes que la colonne C_FREQ :
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 ,001206273          0           1       14739 NONE 
 
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 | 
------------------------------------------------------------------------------------------------
Là aussi le nombre de lignes estimées est loin de la réalité (18 vs 5710). Le nombre 18 a été obtenu en divisant le nombre de lignes de la table par le nombre de valeurs distinctes de la colonne (E-Rows = 14739/829 = 17.79 = 18). Du coup le CBO a choisi un accès indexé alors qu’un Full Table Scan aurait sûrement été plus judicieux.
On se rend donc compte avec ces exemples que lorsque les données sont biaisées en termes de distribution de valeurs, les statistiques classiques ne suffisent plus à l’optimiseur pour estimer des cardinalités proches de la réalité. C’est là que les histogrammes rentrent en jeu.
Nous allons voir dans les prochains articles comment calculer des histogrammes sur les deux colonnes en question et analyser ce que donnent les estimations du CBO grâce à ces nouvelles données.