lundi 23 décembre 2013

Les statistiques étendues (2)

Il y'a un peu plus de 2 ans déjà j'avais rédigé un article décrivant le principe des statistiques étendues en 11g.
J'avais tenté d'expliquer comment les stats étendues pouvaient aider le CBO à estimer de meilleures cardinalités lorsqu'on avait des colonnes corrélées ou des expressions dans nos prédicats.

Ces derniers jours j'ai justement eu affaire à 2 problèmes de performances (sur des bases 11g) liés à l'absence de stats sur des colonnes corrélées et des fonctions appliquées à certaines colonnes.

Je me suis donc dit que ces 2 cas réels pouvaient constituer un second article permettant d'illustrer l'article que j'avais écrit.


Cas 1: Statistiques étendues sur une expression

Le premier problème concernait la requête suivante:
SELECT
   to_char(SYSDATE, 'YYYYMMDD') AS DATE_EXTRACTION,
   RBQ.RBQ_NUM_CONTR AS CPP_NUM_CONTR,
   RBQ.RBQ_NUM_CONTR,
   CB.CBQ_CD_ETABLISSEMENT,
   CB.CBQ_CD_GUICHET,
   CB.CBQ_CLE_COMPTE,
   RBQ.RBQ_CD_NAT_COORD_BQE,
   Substr(CB.CBQ_NUM_COMPTE,1,7) AS CBQ_NUM_COMPTE1,
   Substr(CB.CBQ_NUM_COMPTE,8,3) AS CBQ_NUM_COMPTE2,
   Substr(CB.CBQ_NUM_COMPTE,11,1) AS CBQ_NUM_COMPTE3 FROM
   TDO_D_ASSIN_COORD_BQE      CB,
   TDO_D_ASSIN_ROLE_COORD_BQE RBQ,
   TDO_D_ASSIN_CONTR_ASSU     CON,
   TDO_R_PDV_DISTRIB          PDVD
WHERE
   RBQ.RBQ_NUM_CONTR in  (select NUM_CONTRAT from    GIC_DEM_LISTE_CONTRATS  )
   AND substr(RBQ.RBQ_NUM_CONTR, 1, 3) <> '398'
   AND substr(RBQ.RBQ_NUM_CONTR, 1, 3) <> '399'
   AND substr(RBQ.RBQ_NUM_CONTR, 1, 3) <> '400'
   AND substr(RBQ.RBQ_NUM_CONTR, 1, 3) <> '412'
   --
   AND CB.CBQ_ID_PERSONNE = RBQ.RBQ_ID_PERSONNE
   AND CB.CBQ_ID_TEC_COORD_BQE = RBQ.RBQ_ID_TEC_COORD_BQE
   --
   AND CON_NUM_CONTR(+) = RBQ.RBQ_NUM_CONTR
   AND (      CON.CON_CD_PVE_SOUS IS NULL
            OR (       PDVD_CD_POINT_VENTE = CON.CON_CD_PVE_SOUS
                  AND PDVD_CD_DISTRIB = '400000'
                  )
            );
Cette requête existait depuis longtemps mais, suite à l'implémentation du partitioning sur certaines tables, elle s'était mise à tourner pendant des dizaines d'heures.
Même si la clause de partitioning n'était effectivement pas indiquée dans la clause WHERE de la requête (et ça c'est pas bien!!!) le client voulait quand même que cette requête puisse s'exécuter correctement en attendant que la correction soit apportée.
Dans la requête on peut noter les expressions suivantes:
substr(RBQ.RBQ_NUM_CONTR, 1, 3) <> '398'    AND substr(RBQ.RBQ_NUM_CONTR, 1, 3) <> '399'    AND substr(RBQ.RBQ_NUM_CONTR, 1, 3) <> '400'    AND substr(RBQ.RBQ_NUM_CONTR, 1, 3) <> '412'
En fait le critère de partitionnement est un champ qui correspond justement aux 3 premiers caractères du champ RBQ_NUM_CONTR manipulé dans les clauses ci-dessus.
Jetons un œil au plan d’exécution de la requête(la requête n'ayant pas pu aboutir j'ai uniquement le plan sans les stats d’exécutions):
----------------------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                               | Name                         | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |
----------------------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                        |                              |    89 | 12460 | 31322   (8)| 00:01:50 |       |       |
|   1 |  NESTED LOOPS                           |                              |    89 | 12460 | 31322   (8)| 00:01:50 |       |       |
|   2 |   NESTED LOOPS                          |                              |    89 | 12460 | 31322   (8)| 00:01:50 |       |       |
|*  3 |    HASH JOIN                            |                              |    89 |  7565 | 31056   (8)| 00:01:49 |       |       |
|   4 |     NESTED LOOPS                        |                              |    89 |  6675 | 31033   (8)| 00:01:49 |       |       |
|   5 |      NESTED LOOPS OUTER                 |                              |   115 |  7015 | 26689   (8)| 00:01:34 |       |       |
|   6 |       PARTITION LIST ALL                |                              |   115 |  5060 | 26459   (8)| 00:01:33 |     1 |   227 |
|*  7 |        TABLE ACCESS FULL                | TDO_D_ASSIN_ROLE_COORD_BQE   |   115 |  5060 | 26459   (8)| 00:01:33 |     1 |   227 |
|   8 |       TABLE ACCESS BY GLOBAL INDEX ROWID| TDO_D_ASSIN_CONTR_ASSU       |     1 |    17 |     2   (0)| 00:00:01 | ROWID | ROWID |
|*  9 |        INDEX UNIQUE SCAN                | PK_ASSIN_CON_NUM_CONTR       |     1 |       |     1   (0)| 00:00:01 |       |       |
|* 10 |      INDEX FAST FULL SCAN               | UK_TDO_R_PDV_DISTRIB         |     1 |    14 |    38   (6)| 00:00:01 |       |       |
|  11 |     INDEX FAST FULL SCAN                | PK_GIC_DEM_LISTE_CONTRATS    | 20000 |   195K|    22   (5)| 00:00:01 |       |       |
|* 12 |    INDEX RANGE SCAN                     | PK_ASSIN_CBQ_TEC_COORD_PERSO |     1 |       |     2   (0)| 00:00:01 |       |       |
|  13 |   TABLE ACCESS BY INDEX ROWID           | TDO_D_ASSIN_COORD_BQE        |     1 |    55 |     3   (0)| 00:00:01 |       |       |
----------------------------------------------------------------------------------------------------------------------------------------


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


   3 - access("RBQ"."RBQ_NUM_CONTR"="NUM_CONTRAT")
   7 - filter(SUBSTR("RBQ"."RBQ_NUM_CONTR",1,3)<>'398' AND SUBSTR("RBQ"."RBQ_NUM_CONTR",1,3)<>'399' AND
              SUBSTR("RBQ"."RBQ_NUM_CONTR",1,3)<>'400' AND SUBSTR("RBQ"."RBQ_NUM_CONTR",1,3)<>'412')
   9 - access("CON_NUM_CONTR"(+)="RBQ"."RBQ_NUM_CONTR")
  10 - filter("CON"."CON_CD_PVE_SOUS" IS NULL OR "PDVD_CD_POINT_VENTE"="CON"."CON_CD_PVE_SOUS" AND "PDVD_CD_DISTRIB"='400000')
  12 - access("CB"."CBQ_ID_PERSONNE"="RBQ"."RBQ_ID_PERSONNE" AND "CB"."CBQ_ID_TEC_COORD_BQE"="RBQ"."RBQ_ID_TEC_COORD_BQE")
On voit que le CBO a choisi d'attaquer en premier lieu la table TDO_D_ASSIN_ROLE_COORD_BQE en estimant que celle-ci retournerait (après avoir appliqué les critères avec la fonction SUBSTR) seulement 115 lignes .
Si on effectue un comptage sur cette table on se rend compte que le nombre de lignes retournées est en réalité de 18 millions:
select count(*) from TDO_D_ASSIN_ROLE_COORD_BQE RBQ
where substr(RBQ.RBQ_NUM_CONTR, 1, 3) <> '398'
AND substr(RBQ.RBQ_NUM_CONTR, 1, 3) <> '399'
AND substr(RBQ.RBQ_NUM_CONTR, 1, 3) <> '400'
AND substr(RBQ.RBQ_NUM_CONTR, 1, 3) <> '412';
   
  COUNT(*)
----------
  18449114
  
-------------------------------------------------------------------------------------------------------------------
| Id  | Operation             | Name                   | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads  |
-------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT      |                        |      1 |        |      1 |00:00:48.70 |   31949 |  31825 |
|   1 |  SORT AGGREGATE       |                        |      1 |      1 |      1 |00:00:48.70 |   31949 |  31825 |
|*  2 |   INDEX FAST FULL SCAN| CK_ASSIN_RBQ_NUM_CONTR |      1 |    115 |     18M|00:00:44.48 |   31949 |  31825 |
-------------------------------------------------------------------------------------------------------------------


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


   2 - filter((SUBSTR("RBQ"."RBQ_NUM_CONTR",1,3)<>'398' AND SUBSTR("RBQ"."RBQ_NUM_CONTR",1,3)<>'399' AND
              SUBSTR("RBQ"."RBQ_NUM_CONTR",1,3)<>'400' AND SUBSTR("RBQ"."RBQ_NUM_CONTR",1,3)<>'412'))
Puisque le CBO est incapable d'estimer une sélectivité correcte lorsqu'une fonction est appliquée à une colonne, d'où diable sort-il ces 115 lignes?
D'après le chapitre 5 du livre de Jonathan Lewis, l'optimiseur appliquerait une sélectivité de 5% pour chaque prédicat (avec expression) de type NOT EQUAL.
Vu qu'on a 4 prédicats on se retrouve au final avec une sélectivité de 0.05*0.05*0.05*0.05 = 0.00000625
La colonne NUM_ROWS de DBA_TABLES pour la table TDO_D_ASSIN_ROLE_COORD_BQE renvoyant 18 449 114 lignes, si on y applique la sélectivité précédente on obtient bien 115 lignes:
18449114*0.00000625 = 115.30

Cette mauvaise estimation conduit le CBO à choisir cette table comme table directrice du NESTED LOOP pour la joindre avec la table TDO_D_ASSIN_CONTR_ASSU.
Les estimations étant totalement biaisées c'est tout le reste du plan qui est faussé.
La solution en 11g consiste à calculer des statistiques étendues sur l'expression substr(RBQ.RBQ_NUM_CONTR, 1, 3):
BEGIN
 DBMS_STATS.gather_table_stats
 (ownname => 'ADWH00',
 tabname => 'TDO_D_ASSIN_ROLE_COORD_BQE',
 method_opt => 'FOR COLUMNS (substr(RBQ_NUM_CONTR, 1, 3)) size 1'
 );
END;
/
J'ai mis le paramètre d'instance ENABLE_DDL_LOGGING à TRUE et voici ce qu'on voit dans le fichier ALERT lorsqu'on calcule des stats étendues sur l'expression substr(RBQ_NUM_CONTR, 1, 3):
alter table "ADWH00"."TDO_D_ASSIN_ROLE_COORD_BQE" add (SYS_STUXQ$Y7DH65G7U2BC3$3Y_3NF as (substr(RBQ_NUM_CONTR, 1, 3)) virtual BY USER for statistics)

Le calcul de stats étendues sur l'expression génère en réalité la création d'une colonne virtuelle sur la table TDO_D_ASSIN_ROLE_COORD_BQE.

Une fois les stats étendues calculées (c-a-d une fois la colonne virtuelle créée), on obtient le plan suivant lorsqu'on exécute de nouveau la requête:
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                                | Name                         | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads  |  OMem |  1Mem | Used-Mem |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                         |                              |      1 |        |  18229 |00:03:09.28 |     429K|    311K|       |       |          |
|   1 |  CONCATENATION                           |                              |      1 |        |  18229 |00:03:09.28 |     429K|    311K|       |       |          |
|*  2 |   HASH JOIN                              |                              |      1 |  20717 |  18229 |00:02:03.16 |     288K|    228K|  2494K|  1736K| 2540K (0)|
|*  3 |    INDEX FAST FULL SCAN                  | UK_TDO_R_PDV_DISTRIB         |      1 |  28501 |  28497 |00:00:00.02 |     167 |      0 |       |       |          |
|   4 |    NESTED LOOPS OUTER                    |                              |      1 |  20717 |  18229 |00:02:03.07 |     288K|    228K|       |       |          |
|*  5 |     HASH JOIN                            |                              |      1 |  20717 |  18229 |00:02:02.62 |     232K|    228K|  2327K|  1129K| 3297K (0)|
|*  6 |      HASH JOIN                           |                              |      1 |  20717 |  18261 |00:01:22.93 |   86307 |  83282 |  2129K|  2004K| 2289K (0)|
|   7 |       INDEX FAST FULL SCAN               | PK_GIC_DEM_LISTE_CONTRATS    |      1 |  20000 |  20000 |00:00:00.02 |    2718 |      0 |       |       |          |
|   8 |       PARTITION LIST ALL                 |                              |      1 |     18M|     18M|00:01:09.89 |   83589 |  83282 |       |       |          |
|*  9 |        TABLE ACCESS FULL                 | TDO_D_ASSIN_ROLE_COORD_BQE   |    227 |     18M|     18M|00:01:02.25 |   83589 |  83282 |       |       |          |
|  10 |      TABLE ACCESS FULL                   | TDO_D_ASSIN_COORD_BQE        |      1 |     16M|     16M|00:00:27.26 |     146K|    145K|       |       |          |
|  11 |     TABLE ACCESS BY GLOBAL INDEX ROWID   | TDO_D_ASSIN_CONTR_ASSU       |  18229 |      1 |  18229 |00:00:00.41 |   55907 |      0 |       |       |          |
|* 12 |      INDEX UNIQUE SCAN                   | PK_ASSIN_CON_NUM_CONTR       |  18229 |      1 |  18229 |00:00:00.23 |   37677 |      0 |       |       |          |
|  13 |   NESTED LOOPS                           |                              |      1 |      1 |      0 |00:01:06.09 |     141K|  83282 |       |       |          |
|  14 |    NESTED LOOPS                          |                              |      1 |      1 |      0 |00:01:06.09 |     141K|  83282 |       |       |          |
|  15 |     NESTED LOOPS                         |                              |      1 |      1 |      0 |00:01:06.09 |     141K|  83282 |       |       |          |
|* 16 |      FILTER                              |                              |      1 |        |      0 |00:01:06.09 |     141K|  83282 |       |       |          |
|  17 |       NESTED LOOPS OUTER                 |                              |      1 |      1 |  18261 |00:01:06.08 |     141K|  83282 |       |       |          |
|* 18 |        HASH JOIN                         |                              |      1 |  20717 |  18261 |00:01:05.80 |   86306 |  83282 |  2129K|  2004K| 2283K (0)|
|  19 |         INDEX FAST FULL SCAN             | PK_GIC_DEM_LISTE_CONTRATS    |      1 |  20000 |  20000 |00:00:00.02 |    2718 |      0 |       |       |          |
|  20 |         PARTITION LIST ALL               |                              |      1 |     18M|     18M|00:00:52.92 |   83588 |  83282 |       |       |          |
|* 21 |          TABLE ACCESS FULL               | TDO_D_ASSIN_ROLE_COORD_BQE   |    227 |     18M|     18M|00:00:45.32 |   83588 |  83282 |       |       |          |
|  22 |        TABLE ACCESS BY GLOBAL INDEX ROWID| TDO_D_ASSIN_CONTR_ASSU       |  18261 |      1 |  18261 |00:00:00.26 |   54798 |      0 |       |       |          |
|* 23 |         INDEX UNIQUE SCAN                | PK_ASSIN_CON_NUM_CONTR       |  18261 |      1 |  18261 |00:00:00.13 |   36537 |      0 |       |       |          |
|* 24 |      INDEX FAST FULL SCAN                | UK_TDO_R_PDV_DISTRIB         |      0 |  36746 |      0 |00:00:00.01 |       0 |      0 |       |       |          |
|* 25 |     INDEX RANGE SCAN                     | PK_ASSIN_CBQ_TEC_COORD_PERSO |      0 |      1 |      0 |00:00:00.01 |       0 |      0 |       |       |          |
|  26 |    TABLE ACCESS BY INDEX ROWID           | TDO_D_ASSIN_COORD_BQE        |      0 |      1 |      0 |00:00:00.01 |       0 |      0 |       |       |          |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
   2 - access("PDVD_CD_POINT_VENTE"="CON"."CON_CD_PVE_SOUS")
   3 - filter("PDVD_CD_DISTRIB"='400000')
   5 - access("CB"."CBQ_ID_PERSONNE"="RBQ"."RBQ_ID_PERSONNE" AND "CB"."CBQ_ID_TEC_COORD_BQE"="RBQ"."RBQ_ID_TEC_COORD_BQE")
   6 - access("RBQ"."RBQ_NUM_CONTR"="NUM_CONTRAT")
   9 - filter((SUBSTR("RBQ_NUM_CONTR",1,3)<>'398' AND SUBSTR("RBQ_NUM_CONTR",1,3)<>'399' AND SUBSTR("RBQ_NUM_CONTR",1,3)<>'400' AND 
              SUBSTR("RBQ_NUM_CONTR",1,3)<>'412'))
  12 - access("CON_NUM_CONTR"="RBQ"."RBQ_NUM_CONTR")
  16 - filter("CON"."CON_CD_PVE_SOUS" IS NULL)
  18 - access("RBQ"."RBQ_NUM_CONTR"="NUM_CONTRAT")
  21 - filter((SUBSTR("RBQ_NUM_CONTR",1,3)<>'398' AND SUBSTR("RBQ_NUM_CONTR",1,3)<>'399' AND SUBSTR("RBQ_NUM_CONTR",1,3)<>'400' AND 
              SUBSTR("RBQ_NUM_CONTR",1,3)<>'412'))
  23 - access("CON_NUM_CONTR"="RBQ"."RBQ_NUM_CONTR")
  24 - filter((LNNVL("PDVD_CD_POINT_VENTE"="CON"."CON_CD_PVE_SOUS") OR LNNVL("PDVD_CD_DISTRIB"='400000')))
  25 - access("CB"."CBQ_ID_PERSONNE"="RBQ"."RBQ_ID_PERSONNE" AND "CB"."CBQ_ID_TEC_COORD_BQE"="RBQ"."RBQ_ID_TEC_COORD_BQE")

Tout d'abord on note que la requête s'exécute en 3 minutes (alors qu'elle n'aboutissait pas sans les stats étendues). Ensuite on voit que la cardinalité est bien estimée pour la table TDO_D_ASSIN_ROLE_COORD_BQE (18M pour les colonnes E-ROWS et A-ROWS).
Enfin, on constate que grâce à cette bonne estimation la table n'est plus attaquée via un NESTED LOOP mais que le CBO a opté pour un HASH JOIN tout à fait justifié.


Cas 2: Stats étendues sur des colonnes corrélées

Le second problème concernait la requête suivante qui mettait 2h34 pour s'exécuter:
SELECT DISTINCT
 FLC_DT_FIN_ECH_FLUX Date_arrete,
 ASS_NUM_CONTR_COLLECT_CNP Num_contrat,
 ASS_NUM_CONTRACTANT Id_coll,
 SUM (
 DECODE (ASS_NUM_CONTRACTANT,
 '00698', 12 * ADH_SOMME_MVT_RELATIFS_TTC,
 '77009', 12 * ADH_SOMME_MVT_RELATIFS_TTC,
 '90074', 12 * ADH_SOMME_MVT_RELATIFS_TTC,
 4 * ADH_SOMME_MVT_RELATIFS_TTC))
FROM odd_flux_cid a, odd_assure b, odd_info_adhesion d
where a.num_integration in (8118,8112,8069,8186,8148,8119,8094,8070,8187,8149,8120,8095,8071,8121,8096,8072,8188,8150,8122,8097,8073,8189,8151,8123,8098,8074,8190,8152,8125,8099,8075,8191,8153,8127,
8100,8192,8154,8129,8101,8077,8193,8155,8130,8102,8078,8194,8156,8131,8103,8079,8195,8158,8132,8104,8080,8197,8159,8133,8105,8081,8162,8134,8106,8082,8198,8163,8135,8107,8083,8200,8164,8136,8108,8084,
8137,8110,8085,8165,8138,8109,8086,8202,8166,8139,8111,8087)
 AND b.num_integration = d.num_integration
 AND ASS_ID_FLUX = ADH_ID_FLUX
 AND ASS_NUM_CCOLTE = ADH_NUM_CCOLTE
 AND ASS_NUM_REF_ASS = ADH_NUM_REF_ASS
 AND a.num_integration = b.num_integration
 AND ASS_ID_FLUX = flc_id_flux
 AND ASS_ID_DELEGATAIRE = flc_id_delegataire
GROUP BY FLC_DT_FIN_ECH_FLUX, ASS_NUM_CONTR_COLLECT_CNP, ASS_NUM_CONTRACTANT;




1469 rows selected.


Elapsed: 02:34:56.28


Plan hash value: 863059560


-------------------------------------------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                              | Name                 | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads  |  OMem |  1Mem | Used-Mem |
-------------------------------------------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                       |                      |      1 |        |   1469 |00:11:35.38 |      13M|    392K|       |       |          |
|   1 |  HASH GROUP BY                         |                      |      1 |  11546 |   1469 |00:11:35.38 |      13M|    392K|   855K|   855K| 2582K (0)|
|   2 |   NESTED LOOPS                         |                      |      1 |        |     11M|02:34:29.63 |      13M|    392K|       |       |          |
|   3 |    NESTED LOOPS                        |                      |      1 |  11546 |     11M|02:32:29.42 |    2327K|    258K|       |       |          |
|   4 |     NESTED LOOPS                       |                      |      1 |   7504 |   7949K|00:01:24.63 |    1442K|    184K|       |       |          |
|   5 |      PARTITION RANGE INLIST            |                      |      1 |   1310 |     86 |00:00:01.02 |     173 |    172 |       |       |          |
|*  6 |       TABLE ACCESS FULL                | ODD_FLUX_CID         |     86 |   1310 |     86 |00:00:01.02 |     173 |    172 |       |       |          |
|   7 |      PARTITION RANGE AND               |                      |     86 |      6 |   7949K|00:01:20.55 |    1442K|    184K|       |       |          |
|*  8 |       TABLE ACCESS BY LOCAL INDEX ROWID| ODD_ASSURE           |     86 |      6 |   7949K|00:01:17.40 |    1442K|    184K|       |       |          |
|*  9 |        INDEX RANGE SCAN                | PK_ASS_ASSURE        |     86 |     56 |   7949K|00:00:17.86 |   34929 |  34746 |       |       |          |
|  10 |     PARTITION RANGE AND                |                      |   7949K|      1 |     11M|02:30:54.44 |     884K|  74033 |       |       |          |
|* 11 |      INDEX RANGE SCAN                  | PK_ADH_INFO_ADHESION |   7949K|      1 |     11M|00:01:04.85 |     884K|  74033 |       |       |          |
|  12 |    TABLE ACCESS BY LOCAL INDEX ROWID   | ODD_INFO_ADHESION    |     11M|      2 |     11M|00:01:46.81 |      11M|    134K|       |       |          |
-------------------------------------------------------------------------------------------------------------------------------------------------------------


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


   6 - filter(("A"."NUM_INTEGRATION"=8069 OR "A"."NUM_INTEGRATION"=8070 OR "A"."NUM_INTEGRATION"=8071 OR "A"."NUM_INTEGRATION"=8072 OR
              "A"."NUM_INTEGRATION"=8073 OR "A"."NUM_INTEGRATION"=8074 OR "A"."NUM_INTEGRATION"=8075 OR "A"."NUM_INTEGRATION"=8077 OR "A"."NUM_INTEGRATION"=8078
              OR "A"."NUM_INTEGRATION"=8079 OR "A"."NUM_INTEGRATION"=8080 OR "A"."NUM_INTEGRATION"=8081 OR "A"."NUM_INTEGRATION"=8082 OR
              "A"."NUM_INTEGRATION"=8083 OR "A"."NUM_INTEGRATION"=8084 OR "A"."NUM_INTEGRATION"=8085 OR "A"."NUM_INTEGRATION"=8086 OR "A"."NUM_INTEGRATION"=8087
              OR "A"."NUM_INTEGRATION"=8094 OR "A"."NUM_INTEGRATION"=8095 OR "A"."NUM_INTEGRATION"=8096 OR "A"."NUM_INTEGRATION"=8097 OR
              "A"."NUM_INTEGRATION"=8098 OR "A"."NUM_INTEGRATION"=8099 OR "A"."NUM_INTEGRATION"=8100 OR "A"."NUM_INTEGRATION"=8101 OR "A"."NUM_INTEGRATION"=8102
              OR "A"."NUM_INTEGRATION"=8103 OR "A"."NUM_INTEGRATION"=8104 OR "A"."NUM_INTEGRATION"=8105 OR "A"."NUM_INTEGRATION"=8106 OR
              "A"."NUM_INTEGRATION"=8107 OR "A"."NUM_INTEGRATION"=8108 OR "A"."NUM_INTEGRATION"=8109 OR "A"."NUM_INTEGRATION"=8110 OR "A"."NUM_INTEGRATION"=8111
              OR "A"."NUM_INTEGRATION"=8112 OR "A"."NUM_INTEGRATION"=8118 OR "A"."NUM_INTEGRATION"=8119 OR "A"."NUM_INTEGRATION"=8120 OR
              "A"."NUM_INTEGRATION"=8121 OR "A"."NUM_INTEGRATION"=8122 OR "A"."NUM_INTEGRATION"=8123 OR "A"."NUM_INTEGRATION"=8125 OR "A"."NUM_INTEGRATION"=8127
              OR "A"."NUM_INTEGRATION"=8129 OR "A"."NUM_INTEGRATION"=8130 OR "A"."NUM_INTEGRATION"=8131 OR "A"."NUM_INTEGRATION"=8132 OR
              "A"."NUM_INTEGRATION"=8133 OR "A"."NUM_INTEGRATION"=8134 OR "A"."NUM_INTEGRATION"=8135 OR "A"."NUM_INTEGRATION"=8136 OR "A"."NUM_INTEGRATION"=8137
              OR "A"."NUM_INTEGRATION"=8138 OR "A"."NUM_INTEGRATION"=8139 OR "A"."NUM_INTEGRATION"=8148 OR "A"."NUM_INTEGRATION"=8149 OR
              "A"."NUM_INTEGRATION"=8150 OR "A"."NUM_INTEGRATION"=8151 OR "A"."NUM_INTEGRATION"=8152 OR "A"."NUM_INTEGRATION"=8153 OR "A"."NUM_INTEGRATION"=8154
              OR "A"."NUM_INTEGRATION"=8155 OR "A"."NUM_INTEGRATION"=8156 OR "A"."NUM_INTEGRATION"=8158 OR "A"."NUM_INTEGRATION"=8159 OR
              "A"."NUM_INTEGRATION"=8162 OR "A"."NUM_INTEGRATION"=8163 OR "A"."NUM_INTEGRATION"=8164 OR "A"."NUM_INTEGRATION"=8165 OR "A"."NUM_INTEGRATION"=8166
              OR "A"."NUM_INTEGRATION"=8186 OR "A"."NUM_INTEGRATION"=8187 OR "A"."NUM_INTEGRATION"=8188 OR "A"."NUM_INTEGRATION"=8189 OR
              "A"."NUM_INTEGRATION"=8190 OR "A"."NUM_INTEGRATION"=8191 OR "A"."NUM_INTEGRATION"=8192 OR "A"."NUM_INTEGRATION"=8193 OR "A"."NUM_INTEGRATION"=8194
              OR "A"."NUM_INTEGRATION"=8195 OR "A"."NUM_INTEGRATION"=8197 OR "A"."NUM_INTEGRATION"=8198 OR "A"."NUM_INTEGRATION"=8200 OR
              "A"."NUM_INTEGRATION"=8202))
   8 - filter("ASS_ID_DELEGATAIRE"="FLC_ID_DELEGATAIRE")
   9 - access("A"."NUM_INTEGRATION"="B"."NUM_INTEGRATION" AND "ASS_ID_FLUX"="FLC_ID_FLUX")
       filter(("B"."NUM_INTEGRATION"=8069 OR "B"."NUM_INTEGRATION"=8070 OR "B"."NUM_INTEGRATION"=8071 OR "B"."NUM_INTEGRATION"=8072 OR
              "B"."NUM_INTEGRATION"=8073 OR "B"."NUM_INTEGRATION"=8074 OR "B"."NUM_INTEGRATION"=8075 OR "B"."NUM_INTEGRATION"=8077 OR "B"."NUM_INTEGRATION"=8078
              OR "B"."NUM_INTEGRATION"=8079 OR "B"."NUM_INTEGRATION"=8080 OR "B"."NUM_INTEGRATION"=8081 OR "B"."NUM_INTEGRATION"=8082 OR
              "B"."NUM_INTEGRATION"=8083 OR "B"."NUM_INTEGRATION"=8084 OR "B"."NUM_INTEGRATION"=8085 OR "B"."NUM_INTEGRATION"=8086 OR "B"."NUM_INTEGRATION"=8087
              OR "B"."NUM_INTEGRATION"=8094 OR "B"."NUM_INTEGRATION"=8095 OR "B"."NUM_INTEGRATION"=8096 OR "B"."NUM_INTEGRATION"=8097 OR
              "B"."NUM_INTEGRATION"=8098 OR "B"."NUM_INTEGRATION"=8099 OR "B"."NUM_INTEGRATION"=8100 OR "B"."NUM_INTEGRATION"=8101 OR "B"."NUM_INTEGRATION"=8102
              OR "B"."NUM_INTEGRATION"=8103 OR "B"."NUM_INTEGRATION"=8104 OR "B"."NUM_INTEGRATION"=8105 OR "B"."NUM_INTEGRATION"=8106 OR
              "B"."NUM_INTEGRATION"=8107 OR "B"."NUM_INTEGRATION"=8108 OR "B"."NUM_INTEGRATION"=8109 OR "B"."NUM_INTEGRATION"=8110 OR "B"."NUM_INTEGRATION"=8111
              OR "B"."NUM_INTEGRATION"=8112 OR "B"."NUM_INTEGRATION"=8118 OR "B"."NUM_INTEGRATION"=8119 OR "B"."NUM_INTEGRATION"=8120 OR
              "B"."NUM_INTEGRATION"=8121 OR "B"."NUM_INTEGRATION"=8122 OR "B"."NUM_INTEGRATION"=8123 OR "B"."NUM_INTEGRATION"=8125 OR "B"."NUM_INTEGRATION"=8127
              OR "B"."NUM_INTEGRATION"=8129 OR "B"."NUM_INTEGRATION"=8130 OR "B"."NUM_INTEGRATION"=8131 OR "B"."NUM_INTEGRATION"=8132 OR
              "B"."NUM_INTEGRATION"=8133 OR "B"."NUM_INTEGRATION"=8134 OR "B"."NUM_INTEGRATION"=8135 OR "B"."NUM_INTEGRATION"=8136 OR "B"."NUM_INTEGRATION"=8137
              OR "B"."NUM_INTEGRATION"=8138 OR "B"."NUM_INTEGRATION"=8139 OR "B"."NUM_INTEGRATION"=8148 OR "B"."NUM_INTEGRATION"=8149 OR
              "B"."NUM_INTEGRATION"=8150 OR "B"."NUM_INTEGRATION"=8151 OR "B"."NUM_INTEGRATION"=8152 OR "B"."NUM_INTEGRATION"=8153 OR "B"."NUM_INTEGRATION"=8154
              OR "B"."NUM_INTEGRATION"=8155 OR "B"."NUM_INTEGRATION"=8156 OR "B"."NUM_INTEGRATION"=8158 OR "B"."NUM_INTEGRATION"=8159 OR
              "B"."NUM_INTEGRATION"=8162 OR "B"."NUM_INTEGRATION"=8163 OR "B"."NUM_INTEGRATION"=8164 OR "B"."NUM_INTEGRATION"=8165 OR "B"."NUM_INTEGRATION"=8166
              OR "B"."NUM_INTEGRATION"=8186 OR "B"."NUM_INTEGRATION"=8187 OR "B"."NUM_INTEGRATION"=8188 OR "B"."NUM_INTEGRATION"=8189 OR
              "B"."NUM_INTEGRATION"=8190 OR "B"."NUM_INTEGRATION"=8191 OR "B"."NUM_INTEGRATION"=8192 OR "B"."NUM_INTEGRATION"=8193 OR "B"."NUM_INTEGRATION"=8194
              OR "B"."NUM_INTEGRATION"=8195 OR "B"."NUM_INTEGRATION"=8197 OR "B"."NUM_INTEGRATION"=8198 OR "B"."NUM_INTEGRATION"=8200 OR
              "B"."NUM_INTEGRATION"=8202))
  11 - access("B"."NUM_INTEGRATION"="D"."NUM_INTEGRATION" AND "ASS_ID_FLUX"="ADH_ID_FLUX" AND "ASS_NUM_CCOLTE"="ADH_NUM_CCOLTE" AND
              "ASS_NUM_REF_ASS"="ADH_NUM_REF_ASS")
       filter(("D"."NUM_INTEGRATION"=8069 OR "D"."NUM_INTEGRATION"=8070 OR "D"."NUM_INTEGRATION"=8071 OR "D"."NUM_INTEGRATION"=8072 OR
              "D"."NUM_INTEGRATION"=8073 OR "D"."NUM_INTEGRATION"=8074 OR "D"."NUM_INTEGRATION"=8075 OR "D"."NUM_INTEGRATION"=8077 OR "D"."NUM_INTEGRATION"=8078
              OR "D"."NUM_INTEGRATION"=8079 OR "D"."NUM_INTEGRATION"=8080 OR "D"."NUM_INTEGRATION"=8081 OR "D"."NUM_INTEGRATION"=8082 OR
              "D"."NUM_INTEGRATION"=8083 OR "D"."NUM_INTEGRATION"=8084 OR "D"."NUM_INTEGRATION"=8085 OR "D"."NUM_INTEGRATION"=8086 OR "D"."NUM_INTEGRATION"=8087
              OR "D"."NUM_INTEGRATION"=8094 OR "D"."NUM_INTEGRATION"=8095 OR "D"."NUM_INTEGRATION"=8096 OR "D"."NUM_INTEGRATION"=8097 OR
              "D"."NUM_INTEGRATION"=8098 OR "D"."NUM_INTEGRATION"=8099 OR "D"."NUM_INTEGRATION"=8100 OR "D"."NUM_INTEGRATION"=8101 OR "D"."NUM_INTEGRATION"=8102
              OR "D"."NUM_INTEGRATION"=8103 OR "D"."NUM_INTEGRATION"=8104 OR "D"."NUM_INTEGRATION"=8105 OR "D"."NUM_INTEGRATION"=8106 OR
              "D"."NUM_INTEGRATION"=8107 OR "D"."NUM_INTEGRATION"=8108 OR "D"."NUM_INTEGRATION"=8109 OR "D"."NUM_INTEGRATION"=8110 OR "D"."NUM_INTEGRATION"=8111
              OR "D"."NUM_INTEGRATION"=8112 OR "D"."NUM_INTEGRATION"=8118 OR "D"."NUM_INTEGRATION"=8119 OR "D"."NUM_INTEGRATION"=8120 OR
              "D"."NUM_INTEGRATION"=8121 OR "D"."NUM_INTEGRATION"=8122 OR "D"."NUM_INTEGRATION"=8123 OR "D"."NUM_INTEGRATION"=8125 OR "D"."NUM_INTEGRATION"=8127
              OR "D"."NUM_INTEGRATION"=8129 OR "D"."NUM_INTEGRATION"=8130 OR "D"."NUM_INTEGRATION"=8131 OR "D"."NUM_INTEGRATION"=8132 OR
              "D"."NUM_INTEGRATION"=8133 OR "D"."NUM_INTEGRATION"=8134 OR "D"."NUM_INTEGRATION"=8135 OR "D"."NUM_INTEGRATION"=8136 OR "D"."NUM_INTEGRATION"=8137
              OR "D"."NUM_INTEGRATION"=8138 OR "D"."NUM_INTEGRATION"=8139 OR "D"."NUM_INTEGRATION"=8148 OR "D"."NUM_INTEGRATION"=8149 OR
              "D"."NUM_INTEGRATION"=8150 OR "D"."NUM_INTEGRATION"=8151 OR "D"."NUM_INTEGRATION"=8152 OR "D"."NUM_INTEGRATION"=8153 OR "D"."NUM_INTEGRATION"=8154
              OR "D"."NUM_INTEGRATION"=8155 OR "D"."NUM_INTEGRATION"=8156 OR "D"."NUM_INTEGRATION"=8158 OR "D"."NUM_INTEGRATION"=8159 OR
              "D"."NUM_INTEGRATION"=8162 OR "D"."NUM_INTEGRATION"=8163 OR "D"."NUM_INTEGRATION"=8164 OR "D"."NUM_INTEGRATION"=8165 OR "D"."NUM_INTEGRATION"=8166
              OR "D"."NUM_INTEGRATION"=8186 OR "D"."NUM_INTEGRATION"=8187 OR "D"."NUM_INTEGRATION"=8188 OR "D"."NUM_INTEGRATION"=8189 OR
              "D"."NUM_INTEGRATION"=8190 OR "D"."NUM_INTEGRATION"=8191 OR "D"."NUM_INTEGRATION"=8192 OR "D"."NUM_INTEGRATION"=8193 OR "D"."NUM_INTEGRATION"=8194
              OR "D"."NUM_INTEGRATION"=8195 OR "D"."NUM_INTEGRATION"=8197 OR "D"."NUM_INTEGRATION"=8198 OR "D"."NUM_INTEGRATION"=8200 OR
              "D"."NUM_INTEGRATION"=8202))
Si on regarde le plan on voit que sur les 2h34 d'exécution on a 2h30 passées sur l'accès à la table ODD_INFO_ADHESION via l'opération "PARTITION RANGE AND".
Cette opération est exécutée 7949000 fois (cf. colonne STARTS) car la jointure précédente entre les tables ODD_FLUX_CID et ODD_ASSURE retourne 7949000 lignes (cf. opération 4 du plan).
Si on regarde la colonne E-ROWS on voit que l'optimiseur estime que la jointure ne retournerait que 7504 lignes. C'est cette mauvaise estimation qui induit derrière le NESTED LOOP hyper couteux sur la table ODD_INFO_ADHESION.
La jointure avec la table ODD_ASSURE s'effectue sur les colonnes NUM_INTEGRATION, ASS_ID_FLUX et ASS_ID_DELEGATAIRE.
Effectuons un comptage sur la table ODD_ASSURE juste avec la clause NUM_INTEGRATION:
select count(*) from ODD_ASSURE b
where b.num_integration in (8118,8112,8069,8186,8148,8119,8094,8070,8187,8149,8120,8095,8071,8121,8096,8072,8188,8150,8122,8097,8073,8189,8151,8123,8098,8074,8190,8152,8125,8099,8075,8191,8153,8127,
   8100,8192,8154,8129,8101,8077,8193,8155,8130,8102,8078,8194,8156,8131,8103,8079,8195,8158,8132,8104,8080,8197,8159,8133,8105,8081,8162,8134,8106,8082,8198,8163,8135,8107,8083,8200,8164,8136,8108,8084,
   8137,8110,8085,8165,8138,8109,8086,8202,8166,8139,8111,8087);
   
---------------------------------------------------------------------------------------------------------------
| Id  | Operation                  | Name          | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads  |
---------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT           |               |      1 |        |      1 |00:00:12.96 |   34926 |  34925 |
|   1 |  SORT AGGREGATE            |               |      1 |      1 |      1 |00:00:12.96 |   34926 |  34925 |
|   2 |   INLIST ITERATOR          |               |      1 |        |   7949K|00:00:11.72 |   34926 |  34925 |
|   3 |    PARTITION RANGE ITERATOR|               |     86 |   7967K|   7949K|00:00:09.01 |   34926 |  34925 |
|*  4 |     INDEX RANGE SCAN       | PK_ASS_ASSURE |     86 |   7967K|   7949K|00:00:06.37 |   34926 |  34925 |
---------------------------------------------------------------------------------------------------------------
On voit que le nombre de lignes retournées est toujours de 7949K alors que cette fois on a utilisé uniqument la colonne NUM_INTEGRATION.
Il semble que les 2 autres colonnes n'influent pas sur la cardinalité. Cela revient donc à dire qu'il existe une corrélation entre les 3 colonnes NUM_INTEGRATION, ASS_ID_FLUX et ASS_ID_DELEGATAIRE.
Il serait donc intéressant de calculer des stats étendues pour la table ODD_ASSURE sur ces 3 colonnes ainsi que pour la table ODD_FLUX_CID:
BEGIN
 DBMS_STATS.gather_table_stats('AODD02','ODD_ASSURE',
 method_opt => 'FOR COLUMNS (NUM_INTEGRATION,ASS_ID_FLUX,ASS_ID_DELEGATAIRE) size 1',
 NO_INVALIDATE => FALSE,
 FORCE => TRUE
 );
END;
/


BEGIN
 DBMS_STATS.gather_table_stats('AODD02','ODD_FLUX_CID',
 method_opt => 'FOR COLUMNS (NUM_INTEGRATION,FLC_ID_FLUX,FLC_ID_DELEGATAIRE) size 1',
 NO_INVALIDATE => FALSE,
 FORCE => TRUE
 );
END;
/
Si on relance la requête on obtient désormais le plan suivant:
------------------------------------------------------------------------------------------------------------------------------------------------------------
| Id  | Operation             | Name              | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads  | Writes |  OMem |  1Mem | Used-Mem | Used-Tmp|
------------------------------------------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT      |                   |      1 |        |   1469 |00:01:35.85 |     283K|    288K|   5872 |       |       |          |         |
|   1 |  HASH GROUP BY        |                   |      1 |    355K|   1469 |00:01:35.85 |     283K|    288K|   5872 |   855K|   855K|   17M (0)|         |
|   2 |   PARTITION RANGE AND |                   |      1 |     53M|     11M|00:01:19.64 |     283K|    288K|   5872 |       |       |          |         |
|*  3 |    HASH JOIN          |                   |     86 |     53M|     11M|00:01:15.20 |     283K|    288K|   5872 |   972K|   972K|  372K (0)|         |
|*  4 |     TABLE ACCESS FULL | ODD_FLUX_CID      |     86 |   1322 |     86 |00:00:00.52 |     172 |    172 |      0 |       |       |          |         |
|*  5 |     HASH JOIN         |                   |     86 |     53M|     11M|00:01:01.45 |     282K|    288K|   5872 |  1889M|    35M|   20M (0)|   12288 |
|*  6 |      TABLE ACCESS FULL| ODD_ASSURE        |     86 |     37M|   7949K|00:00:11.45 |     146K|    146K|      0 |       |       |          |         |
|*  7 |      TABLE ACCESS FULL| ODD_INFO_ADHESION |     86 |     54M|     11M|00:00:19.97 |     136K|    136K|      0 |       |       |          |         |
------------------------------------------------------------------------------------------------------------------------------------------------------------
Les stats étendues ont permis d'obtenir une cardinalité pas tout à fait exact mais en tout cas plus proche de la réalité.
Les NESTED LOOP ont laissé place à des HASH JOIN ce qui permet de faire tourner la requête en 1 minute 35 au lieu de 2h34.

J'espère que ces 2 exemples tirés de la réalité vous permettront de comprendre comment grâce aux statistiques étendues on peut aider l'optimiseur à avoir une connaissance plus intelligente des données et ainsi lui permettre de nous trouver un plan adequat.

mardi 10 décembre 2013

Impact du Clustering Factor dans la mise à jour de tables


J'ai eu un petit débat avec mes collègues DBAs la semaine dernière sur le fait que le clustering factor d'un index pouvait expliquer de mauvaises performances sur la mise à jour d'une table. Pour eux, le clustering factor n'impactait que l'opération RANGE SCAN de l'index lors d'un SELECT, alors que j’avançais l'idée que les mauvaises performances d'un insert par exemple pouvait être lié au fait d'avoir un index (à mettre à jour suite à la mise à jour de la table), et que cette mise à jour générait plus de logical reads selon la qualité du clustering factor (CF).

Je n'ai donc pas pu résister à l'idée de faire un petit test case pour le leur prouver.

On commence par créér une table T1 avec 2 colonnes. La table sera ordonnée selon la première colonne tandis que la 2ème colonne sera l'inverse de la première colonne.

Bien sûr on va créer un index sur chacune des 2 colonnes et vous vous en doutez surement, la première colonne aura un clustering factor très bon c-a-d proche du nombre de blocs de la table (car l'ordre d'insertion dans la table correspond aux entrées du 1er index) tandis que la seconde colonne aura un CF très mauvais c’est-à-dire proche du nombre de lignes.
SQL> CREATE TABLE T1
2  AS
3  SELECT LPAD (object_id, 10, '0') AS C1, reverse (LPAD (object_id, 10, '0')) AS C2
4   FROM dba_objects where 1=2;

Table created.

SQL> create index idx1 on T1(C1);

Index created.

SQL> create index idx2 on T1(C2);

Index created.

Pour l'instant la table ne contient pas de lignes.
On va ensuite insérer les lignes (insert en mode conventionnel) et on va regarder le nombre de logical reads générés pour chaque index dans la vue V$SEGMENT_STATISTICS:
SQL> insert into T1
  2  SELECT LPAD (object_id, 10, '0') AS C1, reverse (LPAD (object_id, 10, '0')) AS C2
  3   FROM dba_objects
  4   order by 1;

57633 rows created.

SQL> commit;

Commit complete.

SQL> select statistic_name,object_name,value from v$segment_statistics
  2      where owner='APAE01' and object_name in ('T1','IDX1','IDX2') and value>0
  3      order by 1,2;

STATISTIC_NAME                                                   OBJECT_NAME                         VALUE
---------------------------------------------------------------- ------------------------------ ----------
db block changes                                                 IDX1                                 2272
db block changes                                                 IDX2                                59520
db block changes                                                 T1                                   1504
logical reads                                                    IDX1                                 3840
logical reads                                                    IDX2                               116752
logical reads                                                    T1                                   3328
space allocated                                                  IDX1                              2097152
space allocated                                                  IDX2                              3145728
space allocated                                                  T1                                2097152
space used                                                       IDX1                              1389672
space used                                                       IDX2                              1768402
space used                                                       T1                                1542726

BINGO!!!!
On s'aperçoit que le 2ème index a généré 30 fois plus de logical reads (116 752 vs 3 840)
Mais c'est en fait très logique: comme le premier index est trié selon le même ordre que les données insérées dans la table on a plus de chances d'insérer une entrée d'index dans le même bloc feuille que l'entrée précédente du coup on change beaucoup de moins de bloc à chaque mise à jour de l'index et on a ainsi moins de logical reads puisque le bloc feuille de l'index à modifier est déjà pinné dans le cache par le process server. A l'inverse, pour le 2ème index qui ne suit pas du tout l'ordre des données insérées dans la table, on aura à chaque nouvelle ligne insérée dans la table un nouveau logical read car la ligne a de fortes chances de se trouver dans un bloc feuille différent.

Un petit calcul de stats sur la table et ses index nous indique qu'effectivement l'index 1 a un clustering factor proche du nombre de blocs feuilles alors que l'index 2 a un CF proche du nombre de lignes:
SQL> exec dbms_stats.gather_table_stats('APAE01','T1',cascade=>TRUE);

PL/SQL procedure successfully completed.

SQL> select index_name,blevel,leaf_blocks,distinct_keys,clustering_factor,num_rows
2  from dba_indexes where index_name in ('IDX1','IDX2')
3  order by 1;

INDEX_NAME                         BLEVEL LEAF_BLOCKS DISTINCT_KEYS CLUSTERING_FACTOR   NUM_ROWS
------------------------------ ---------- ----------- ------------- ----------------- ----------
IDX1                                    1         160         57633               215      57633
IDX2                                    1         256         57633             57633      57633

La solution dans ce cas pour éviter le nombre important de logical reads consiste à effectuer un insert en mode DIRECT PATH car dans ce cas l'index est mis à jour seulement à la fin une fois que les données ont été triées pour chaque index.
SQL> insert /*+ APPEND */ into T1
  2  SELECT LPAD (object_id, 10, '0') AS C1, reverse (LPAD (object_id, 10, '0')) AS C2
3   FROM dba_objects
4   order by 1;

57633 rows created.

SQL> commit;

Commit complete.

SQL> select statistic_name,object_name,value from v$segment_statistics
2      where owner='APAE01' and object_name in ('T1','IDX1','IDX2') and value>0
3      order by 1,2;

STATISTIC_NAME                                                   OBJECT_NAME                         VALUE
---------------------------------------------------------------- ------------------------------ ----------
db block changes                                                 IDX1                                 6496
db block changes                                                 IDX2                                63488
db block changes                                                 T1                                   1536
logical reads                                                    IDX1                                 8944
logical reads                                                    IDX2                               122400
logical reads                                                    T1                                   3904
physical write requests                                          IDX1                                16
physical write requests                                          IDX2                                25
physical write requests                                          T1                                  31
physical writes                                                  IDX1                                  208
physical writes                                                  IDX2                                  336
physical writes                                                  T1                                    472
physical writes direct                                           T1                                    216
space allocated                                                  IDX1                              4194304
space allocated                                                  IDX2                              5242880
space allocated                                                  T1                                4194304
space used                                                       IDX1                              3004846
space used                                                       IDX2                              3326326
space used                                                       T1                                3312198


On voit en effet que le nombre de logical reads pour le 2ème index est passé de 116 752 à 122 400 soit seulement 5648 logical reads au lieu de 116 752 lors de l'insert en mode conventionnel pour le même nombre de lignes insérées.

CONCLUSION:

Il ne faut pas sous estimer le coût de maintenance d'un index notamment si cet index a un clustering factor qui est proche du nombre de lignes de la table.