tag:blogger.com,1999:blog-4566861551501375882024-03-13T18:00:52.763+01:00Le blog Oracle d'Ahmed AANGOURUn blog pour contribuer à la communauté francophone d'Oracle sur les thèmes suivants:
- Concepts Oracle
- SQL/PL SQL
- Analyse des problèmes de performanceAhmed AANGOURhttp://www.blogger.com/profile/03378785645074450748noreply@blogger.comBlogger82125tag:blogger.com,1999:blog-456686155150137588.post-11915272595576329142019-11-04T16:18:00.000+01:002019-11-04T16:18:20.934+01:00Row Prefetching et PL/SQL<div dir="ltr" style="text-align: left;" trbidi="on">
J'avais écrit il y'a plusieurs années déjà un <a href="http://ahmedaangour.blogspot.com/2011/03/row-prefetching-arraysize.html" target="_blank">article</a> 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.<br />
<br />
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.<br />
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.<br />
<br />
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.<br />
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.<br />
<br />
Le but de cet article est justement de voir comment fonctionne le row prefetching dans le code PL/SQL.<br />
<br />
Commençons par créer une table de travail:<br />
<span style="font-size: x-small;"><span style="font-family: "Courier New", Courier, monospace;">create /*+ parallel(8) */ table T1 as<br />select * from dba_objects;<br /><br />select count(*) from T1;</span></span><br />
<span style="font-size: x-small;"><span style="font-family: "Courier New", Courier, monospace;"><br /> COUNT(*)<br />----------<br /> 1378527</span></span><br />
<b><br /></b>
<b><u>1er test</u>: Avec un curseur explicite</b><br />
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 :<br />
<span style="font-size: x-small;"><span style="font-family: "Courier New", Courier, monospace;">set serveroutput on</span></span><br /><span style="font-size: x-small;"><span style="font-family: "Courier New", Courier, monospace;">DECLARE<br /><br /> CURSOR cur IS SELECT /*+ parallel(4) */ object_name, LAST_DDL_TIME<br /> FROM t1 where rownum<=1000000; <br /><br /> v_obj_name varchar2(255);<br /> v_last_ddl_time DATE; <br /><br />BEGIN<br /><br /> OPEN cur;<br /> LOOP<br /> FETCH cur into v_obj_name,v_last_ddl_time ;<br /> EXIT WHEN cur%notfound;<br /> dbms_output.put_line(v_obj_name);<br /> END LOOP;<br /> CLOSE cur;<br />END;<br />/</span></span><br />
<br />
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:<br />
<br /><span style="font-size: x-small;"><span style="font-family: "Courier New", Courier, monospace;">Global Information<br />------------------------------<br />Status : DONE (ALL ROWS)<br />Instance ID : 4<br />Session : AHMED (1323:34963)<br />SQL ID : 7npr6bxbj8b8k<br />SQL Execution ID : 67108865<br />Execution Started : 09/13/2019 16:28:31<br />First Refresh Time : 09/13/2019 16:28:31<br />Last Refresh Time : 09/13/2019 16:28:38<br /><span style="background-color: yellow;">Duration : 7s</span><br />Module/Action : SQL*Plus/-<br />Service : #####<br />Program : sqlplus.exe<br /><span style="background-color: yellow;">Fetch Calls : 1000001 </span><br /><br />Global Stats<br />=================================================<br />| <span style="background-color: yellow;">Elapsed</span> | Cpu | Other | Fetch | Buffer |<br />| <span style="background-color: yellow;">Time(s)</span> | Time(s) | Waits(s) | Calls | Gets |<br />=================================================<br />| <span style="background-color: yellow;">2.80</span> | 2.78 | 0.01 | 1M | 19924 |<br />================================================= </span></span><br />
<br />
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:<br />
<span style="font-family: "Courier New", Courier, monospace;"><span style="font-size: x-small;">set serveroutput on<br />DECLARE<br /> CURSOR cur IS SELECT /*+ parallel(4) */ *<br /> FROM t1 where rownum<=1000000; <br /><br /> TYPE t_t1 IS TABLE OF t1%rowtype;<br /> l_t1 t_t1;<br />BEGIN<br /> OPEN cur;<br /> LOOP<br /> FETCH cur <span style="background-color: yellow;">BULK COLLECT INTO l_t1 LIMIT 1000</span>;<br /> EXIT WHEN l_t1.count = 0;<br /> FOR i IN l_t1.first..l_t1.last LOOP<br /> dbms_output.put_line(l_t1(i).OBJECT_NAME);<br /> END LOOP;<br /> END LOOP;<br /> CLOSE cur;<br />END;<br />/<br /><br /><br /> Global Information<br />------------------------------<br />Status : DONE (ALL ROWS)<br />Instance ID : 4<br />Session : AHMED (1323:35631)<br />SQL ID : gpn4z88bqcqb2<br />SQL Execution ID : 67108865<br />Execution Started : 09/13/2019 16:32:37<br />First Refresh Time : 09/13/2019 16:32:37<br />Last Refresh Time : 09/13/2019 16:32:39<br /><span style="background-color: yellow;">Duration : 2s</span><br />Module/Action : SQL*Plus/-<br />Service : ####<br />Program : sqlplus.exe<br /><span style="background-color: yellow;">Fetch Calls : 1001 </span><br /><span style="font-size: xx-small;"></span></span></span><br />
<br />
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.<br />
<br />
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.<br />
<br />
<b><br /></b>
<b><u>2ème test</u>: Avec un curseur implicite</b><br />
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.<br />
<br />
<span style="font-family: "Courier New", Courier, monospace;"><span style="font-size: x-small;">set serveroutput on<br />BEGIN<br /> for cur in (SELECT /*+ parallel(4) */ * FROM t1 where rownum<=1000000)<br /> LOOP<br /> dbms_output.put_line(cur.OBJECT_NAME);<br /> END LOOP;<br />END;<br />/ <br /><br />Global Information<br />------------------------------<br />Status : DONE (ALL ROWS)<br />Instance ID : 4<br />Session : AHMED (1323:35631)<br />SQL ID : gpn4z88bqcqb2<br />SQL Execution ID : 67108866<br />Execution Started : 09/13/2019 16:37:57<br />First Refresh Time : 09/13/2019 16:37:57<br />Last Refresh Time : 09/13/2019 16:38:00<br /><span style="background-color: yellow;">Duration : 3s</span><br />Module/Action : SQL*Plus/-<br />Service : ####<br />Program : sqlplus.exe<br /><span style="background-color: yellow;">Fetch Calls : 10001 </span></span></span><br /><br />
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).<br />
<br />
<u><b>CONCLUSION</b></u>:<br />
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.<br />
Pour le PL/SQL <b>le row prefetching est activé automatiquement pour tous les curseurs de type implicite</b> (FOR loops cursors), <b>pour les curseurs explicites il faut utiliser la clause BULK COLLECT</b> et donc adapter son code pour utiliser des collections. <br />
<br />
<br />
<br />
<br />
<br />
<br />
<br /></div>
Ahmed AANGOURhttp://www.blogger.com/profile/03378785645074450748noreply@blogger.com0tag:blogger.com,1999:blog-456686155150137588.post-17535317712303029602019-09-02T10:18:00.002+02:002019-09-02T10:18:57.705+02:00Updates et Compression HCC (1)<div dir="ltr" style="text-align: left;" trbidi="on">
<br /><br />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.<br />
<br />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):<br /><span style="color: #741b47;"><span style="font-size: x-small;"><span style="font-family: "Courier New", Courier, monospace;">DROP TABLE T1 PURGE;<br />create table T1 as select * from all_objects where 1=2;<br />alter table T1 compress for query high;<br /><span style="color: #38761d;">-- insert without using DIRECT PATH WRITE (data non compressed)</span><br />insert into T1 select * from all_objects;<br />commit;</span></span></span><br /><br />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.<br />
<br /><b>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</b>:<br /><span style="color: #741b47;"><span style="font-size: x-small;"><span style="font-family: "Courier New", Courier, monospace;">select decode (dbms_compression.get_compression_type(user,'T1',rowid),<br />1, 'NOCOMPRESS',<br />2, 'COMP_FOR_OLTP',<br />4, 'COMP_FOR_QUERY_HIGH',<br />8, 'COMP_FOR_QUERY_LOW',<br />16,'COMP_FOR_ARCHIVE_HIGH',<br />32,'COMP_FOR_ARCHIVE_LOW',<br />64,'COMP_BLOCK',<br />'OTHER') comp_type, count(*)<br />from T1<br />where rownum < 100<br />group by decode (dbms_compression.get_compression_type(user,'T1',rowid),<br />1, 'NOCOMPRESS',<br />2, 'COMP_FOR_OLTP',<br />4, 'COMP_FOR_QUERY_HIGH',<br />8, 'COMP_FOR_QUERY_LOW',<br />16,'COMP_FOR_ARCHIVE_HIGH',<br />32,'COMP_FOR_ARCHIVE_LOW',<br />64,'COMP_BLOCK',<br />'OTHER'); <br /><br />COMP_TYPE COUNT(*)<br />---------------- ----------<br />NOCOMPRESS 99<br /><br />select trunc(bytes/power(1024,2)) m<br />from user_segments where segment_name ='T1'; </span></span></span><br />
<span style="color: #741b47;"><span style="font-size: x-small;"><span style="font-family: "Courier New", Courier, monospace;"> M<br />----------<br /> 258</span></span></span><br /> <br />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.<br />
<br />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:<br />
<span style="color: #741b47;"><span style="font-size: x-small;"><span style="font-family: "Courier New", Courier, monospace;">truncate table T1;<br />insert /*+ APPEND */ into T1 select * from all_objects;<br />commit;<br /><br />select decode (dbms_compression.get_compression_type(user,'T1',rowid),<br />1, 'NOCOMPRESS',<br />2, 'COMP_FOR_OLTP',<br />4, 'COMP_FOR_QUERY_HIGH',<br />8, 'COMP_FOR_QUERY_LOW',<br />16,'COMP_FOR_ARCHIVE_HIGH',<br />32,'COMP_FOR_ARCHIVE_LOW',<br />64,'COMP_BLOCK',<br />'OTHER') comp_type, count(*)<br />from T1<br />where rownum < 100 <br />group by decode (dbms_compression.get_compression_type(user,'T1',rowid),<br />1, 'NOCOMPRESS',<br />2, 'COMP_FOR_OLTP',<br />4, 'COMP_FOR_QUERY_HIGH',<br />8, 'COMP_FOR_QUERY_LOW',<br />16,'COMP_FOR_ARCHIVE_HIGH',<br />32,'COMP_FOR_ARCHIVE_LOW',<br />64,'COMP_BLOCK',<br />'OTHER'); <br /><br />COMP_TYPE COUNT(*)<br />---------------- ----------<br />FOR_QUERY_HIGH 99 <br /><br />select trunc(bytes/power(1024,2)) m<br />from user_segments where segment_name ='T1'; <br /><br /> M<br />----------<br /> 11</span></span></span><br /><br />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.<br /><br />
Notez comment la taille de la table est passée de 258MB à 11MB seulement. C'est assez impressionnant.<br />
Maintenant, je vais exécuter un update sur toutes les lignes de la table:<br /><span style="color: #741b47;"><span style="font-size: x-small;"><span style="font-family: "Courier New", Courier, monospace;">update T1 set CREATED=SYSDATE;<br />commit; <br /><br />select decode (dbms_compression.get_compression_type(user,'T1',rowid),<br />1, 'NOCOMPRESS',<br />2, 'COMP_FOR_OLTP',<br />4, 'COMP_FOR_QUERY_HIGH',<br />8, 'COMP_FOR_QUERY_LOW',<br />16,'COMP_FOR_ARCHIVE_HIGH',<br />32,'COMP_FOR_ARCHIVE_LOW',<br />64,'COMP_BLOCK',<br />'OTHER') comp_type, count(*)<br />from T1<br />where rownum <100 br="">group by decode (dbms_compression.get_compression_type(user,'T1',rowid),<br />1, 'NOCOMPRESS',<br />2, 'COMP_FOR_OLTP',<br />4, 'COMP_FOR_QUERY_HIGH',<br />8, 'COMP_FOR_QUERY_LOW',<br />16,'COMP_FOR_ARCHIVE_HIGH',<br />32,'COMP_FOR_ARCHIVE_LOW',<br />64,'COMP_BLOCK',<br />'OTHER'); <br /><br />COMP_TYPE COUNT(*)<br />--------------------- ----------<br />COMP_BLOCK 99 <br /><br />select trunc(bytes/power(1024,2)) m<br />from user_segments where segment_name ='T1'; <br /><br /> M<br />----------<br /> 96</100></span></span></span><br /><br />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.<br />
Que s'est-il passé exactement?<br />En fait, pour pouvoir modifier des lignes compressées en HCC, <b>Oracle migre ces lignes dans un nouveau block défini en mode OLTP compression</b>. 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.<br />
<br />De plus, lorsqu'un full scan est effectué sur une table et que le moteur Oracle tombe sur une ligne migrée,<b> il bascule automatiquement en mode Block Shipping inhibant ainsi le smart scan.</b><br /><br />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.<br /><br />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.</div>
Ahmed AANGOURhttp://www.blogger.com/profile/03378785645074450748noreply@blogger.com0tag:blogger.com,1999:blog-456686155150137588.post-52446724617741783802016-11-12T17:59:00.000+01:002016-11-12T17:59:24.706+01:00L'importance de la contrainte NOT NULL<div dir="ltr" style="text-align: left;" trbidi="on">
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.<br />
Je décidai alors de lui faire une petite démo rapide:<br />
<pre>
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. </pre>
<br />
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.<br />
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.<br />
Sûr de moi je commençai par lui montrer le cas du COUNT(*):<br />
<pre>
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) </pre>
Aie!!!Le CBO a complètement ignoré mon index :-(<br />
Moi qui voulait épater mon collègue j'avais plutôt l'air bête.<br />
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.<br />
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:<br />
<pre>
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)</pre>
Mon index n'était toujours pas utilisé T_T<br />
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).<br />
<span style="color: #274e13;"><span style="background-color: white;"><i>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.</i></span></span><br />
<br />
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. <br />
Un DESCRIBE sur ma table m'aida à refaire jaillir la lumière dans mon esprit:<br />
<pre>
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)</pre>
<br />
Toutes les colonnes sont NULLables. <br />
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.<br />
Ajoutons donc la contrainte NOT NULL à la colonne OBJECT_TYPE et regardons les plans des requêtes précédentes:<br />
<pre>
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)</pre>
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.<br />
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.<br />
<br />
<span style="color: #274e13;"><span style="background-color: white;"><i>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.</i></span></span><br />
<br />
<u><b>CONCLUSION</b></u>: <br />
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.<br />
<br />
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.<br />
<br /></div>
Ahmed AANGOURhttp://www.blogger.com/profile/03378785645074450748noreply@blogger.com3tag:blogger.com,1999:blog-456686155150137588.post-44416757390876950262016-10-27T20:39:00.000+02:002016-10-27T20:39:16.064+02:00Exadata: Pourquoi mon ratio "Cell Offload Efficiency" est-il négatif?<div dir="ltr" style="text-align: left;" trbidi="on">
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.<br />
Voici un exemple :<br />
<br />
<pre>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 | | |
===========================================================================================================================================================================</pre>
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é.<br />
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.<br />
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 <b><i>physical_read_bytes</i></b> et <b><i>io_interconnect_bytes</i></b> de la vue v$sql:<br />
<pre> 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</pre>
<br />
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.<br />
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:<br />
<pre>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) |
===============================================================================================================================================================================</pre>
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?<br />
Jetons un oeil aux colonnes de v$sql pour voir le traffic entre les cells et les DB nodes:<br />
<pre>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</pre>
<br />
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.<br />
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):<br />
<pre> 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
</pre>
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.<br />
<br />
Pour prouver que la décompression a été effectuée au niveau des cells il suffit de s'intéresser à la statistique <b><i>cell CUs sent uncompressed</i></b> 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:<br />
<pre>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é</pre>
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.<br />
<br />
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.<br />
<br />
La compression n'est pas la seule raison pouvant expliquer un ratio Cell Offload négatif. En effet, la metric <i><b>physical_read_bytes</b></i> 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.<br />
<br />
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.<br />
<br />
<br />
<br /></div>
Ahmed AANGOURhttp://www.blogger.com/profile/03378785645074450748noreply@blogger.com1tag:blogger.com,1999:blog-456686155150137588.post-75538744062895541122016-10-09T19:03:00.002+02:002016-10-25T20:22:16.849+02:00Exadata - Plan d'exécution et Smart scan<div dir="ltr" style="text-align: left;" trbidi="on">
<div dir="ltr" style="text-align: left;" trbidi="on">
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.<br />
<br />
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.).<br />
<br />
Voici un plan d'exécution impliquant un full scan d'une table dans un environnement Exadata:<br />
<pre>
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')</pre>
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. <br />
<br />
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.<br />
<br />
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:<br />
<pre>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</pre>
Voici donc un moyen sûr de savoir si le SMART SCAN a eu lieu. Il suffit de regarder la colonne <b>IO_CELL_OFFLOAD_ELIGIBLE_BYTES</b> 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.<br />
<br />
Après l'exécution de ma requête la colonne <br />
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é. <br />
<br />
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 <b>"_serial_direct_read"</b> à NEVER ce qui empêche les lectures en direct path d'avoir lieu pour les requêtes exécutées en mode serial.<br />
<br />
Ce qu'il faut absolument assimiler c'est que non seulement <span style="color: red;"><b>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</b></span> (c'est à dire lorsque le process server bypass le buffer cache pour mettre les données dans la PGA).<br />
<br />
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é.<br />
<br />
Gardez également à l'esprit que le DIRECT PATH READ n<span style="color: red;"><b>'est pas une décision de l'optimiseur</b></span> mais un choix effectué par le moteur Oracle à l'exécution de la requête.<br />
<br />
<br />
Voyons maintenant ce que donne l'exécution de la requête lorsqu'on autorise le DIRECT PATH READ:<br />
<pre>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</pre>
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.<br />
<br />
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 <b>PHYSICAL_READ_BYTES </b>et <b>IO_INTERCONNECT_BYTES</b>. 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:<br />
<pre>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 </pre>
<br /></div>
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.<br />
<br />
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.<br />
<b>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.</b><br />
<br />
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.<br />
<br /></div>
Ahmed AANGOURhttp://www.blogger.com/profile/03378785645074450748noreply@blogger.com1tag:blogger.com,1999:blog-456686155150137588.post-20466366461085039772016-03-05T19:17:00.001+01:002016-03-05T19:17:30.295+01:00ORA-14257: cannot move partition other than a Range, List, System, or Hash<div dir="ltr" style="text-align: left;" trbidi="on">
Si lors d'un move de partition vous tombez sur l'erreur suivante <b>"ORA-14257: cannot move partition other than a Range, List, System, or Hash partition"</b>, 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:<br />
<pre>
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</pre>
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: <br /><pre>
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; </pre>
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:<br />
<pre>
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 </pre>
Je souhaiterais pourtant que ces partitions pointent sur le tablespace USERS_LC32_DATA01 et non pas FORCE_HP32_CURRENT1. <br />La solution consiste simplement à modifier le <b>DEFAULT ATTRIBUTES</b> pour ces partitions:<br /><pre>
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</pre>
Comme vous pouvez le constater, mes partitions pointent désormais bien sur le nouveau tablespace USERS_LC32_DATA01.<br />
</div>
Ahmed AANGOURhttp://www.blogger.com/profile/03378785645074450748noreply@blogger.com1tag:blogger.com,1999:blog-456686155150137588.post-3170609560777032632016-02-01T22:55:00.000+01:002016-02-01T22:55:34.710+01:00Les histogrammes (3): Height-Balanced<div dir="ltr" style="text-align: left;" trbidi="on">
Dans l<a href="http://ahmedaangour.blogspot.fr/2015/12/les-histogrammes-2-frequency.html" target="_blank">’article précédent</a> 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). <br /><br />La colonne C_HB de ma table T1 contenant 829 valeurs distinctes est candidate pour un histogramme height-balanced. <pre>
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</pre>
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 :<br />
<pre>
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</pre>
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é.<br />
Jetons un œil aux 10 valeurs les plus récurrentes dans la table :<br /> <pre> 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</pre>
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.<br />
<br />
<div style="line-height: 115%; margin-bottom: 0.14in;">
<b><u><br /><span style="font-size: large;">Principe de calcul des histogrammes height-balanced</span></u><span style="font-size: large;"> : </span></b></div>
<div style="line-height: 115%; margin-bottom: 0.14in;">
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à.<br /><br />Requêtons maintenant la vue USER_TAB_HISTOGRAMS pour la colonne C_HB et observons ce qu'on obtient dans notre histogramme : <pre>
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. </pre>
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.</div>
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. <br /><br />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 : <br /><ul style="text-align: left;">
<li>La valeur 3 n’est pas présente dans l’histogramme (même si la valeur apparait 9 fois dans la table) </li>
</ul>
<ul style="text-align: left;">
<li>La valeur 4 a une fréquence de 32 (114-82) </li>
</ul>
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. <br /><br />Pour afficher les fréquences de chaque valeur capturée on peut utiliser la requête suivante qui utilise notamment la fonction LAG:<br />
<pre>
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</pre>
<br />
<br />
<span style="font-size: large;"><b><u>Histogrammes Height-Balanced et cardinalités</u> :</b></span> <br /><br />Dans l’article sur les <a href="http://ahmedaangour.blogspot.fr/2015/12/les-histogrammes-2-frequency.html" target="_blank">histogrammes FREQUENCY</a> 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 <b>seules les valeurs ayant une fréquence supérieure à 1 sont considérées comme populaires</b>. 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’<b>Oracle n’est capable de capturer au mieux que 127 valeurs populaires.</b> <br /><br />Voyons ce que donnent les estimations du CBO lorsqu’on requête une valeur populaire :<br /><pre>
-- 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 |
------------------------------------------------------------------------------------ </pre>
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 :<br /><pre>
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 |
------------------------------------------------------------------------------------------------ </pre>
Voyons maintenant ce que donnent les estimations du CBO pour une valeur non populaire mais présente dans l’histogramme: <br />
<pre>
-- 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 |
------------------------------------------------------------------------------------------------</pre>
Cette fois à l’inverse de la valeur popular le CBO se trompe dans son estimation : 2 lignes estimées contre 24 réellement. <br /><br />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 :<br />
<div style="line-height: 115%; margin-bottom: 0.14in;">
<pre>
-- 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 |
------------------------------------------------------------------------------------------------</pre>
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.
<br />
</div>
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 :<br /><br /><u>Cas d’une valeur populaire</u> : <br /><br />BUCKET_SIZE = SAMPLE_SIZE / NUM_BUCKETS <br /><br />BUCKET_SIZE = 14739/254 = 58.027 <br /><br /><b>Cardinalité estimée = FREQUENCY * BUCKET_SIZE </b><br /><br />Cardinalité estimée = 99 * 58.027 = 5744.72 ~= 5745 => correspond bien à ce qu’on voit dans le plan pour la colonne E-Rows <br /><br /><u>Cas d’une valeur NON populaire (capturée ou pas)</u> : <br /><br /><b>Cardinalité estimée = NUM_ROWS * NewDensity </b><br /><br />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. <br />
<br />
<br />
<br />
<span style="font-size: large;"><u><b>CONCLUSION</b></u> <b>: </b></span><br /><br />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 : <br />
<ul style="text-align: left;">
<li>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).</li>
</ul>
<ul style="text-align: left;">
<li>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. </li>
</ul>
<ul style="text-align: left;">
<li>Lorsque le ENDPOINT_VALUE est identique sur plusieurs buckets, Oracle ne stocke que le dernier bucket. </li>
</ul>
<ul style="text-align: left;">
<li>Oracle n’est capable de capturer au mieux que 127 valeurs populaires </li>
</ul>
<ul style="text-align: left;">
<li>Oracle est capable d’estimer une bonne cardinalité uniquement pour les valeurs populaires </li>
</ul>
<ul style="text-align: left;">
<li>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. </li>
</ul>
<br />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.<br /><br /></div>
Ahmed AANGOURhttp://www.blogger.com/profile/03378785645074450748noreply@blogger.com0tag:blogger.com,1999:blog-456686155150137588.post-80288977546366592472015-12-20T08:14:00.000+01:002015-12-20T08:14:17.902+01:00Les histogrammes (2): Frequency<div dir="ltr" style="text-align: left;" trbidi="on">
<br />Dans <a href="http://ahmedaangour.blogspot.fr/2015/11/les-histogrammes-1-introduction.html" target="_blank">l’article précédent</a> 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 :<br />
<pre>
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 |
------------------------------------------------------------------------------------</pre>
<br />
<div style="text-align: justify;">
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. <br /><br />Voici comment on calcule des statistiques avec histogrammes pour la colonne C_FREQ :</div>
<pre>
exec dbms_stats.gather_table_stats(user,'T1', method_opt=>'for columns C_FREQ size 3');
</pre>
<br />
<div style="text-align: justify;">
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. </div>
<div style="text-align: justify;">
On peut vérifier qu’un histogramme de type FREQUENCY a bien été calculé en questionnant la table USER_TAB_COL_STATISTICS :</div>
<pre>
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
<pre>
</pre>
</pre>
<br />
<div style="text-align: justify;">
On peut également visualiser le contenu de l’histogramme en requêtant la vue USER_TAB_HISTOGRAMS : </div>
<pre>
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</pre>
<br />
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. <br />On avait vu dans l’article précédent que sans histogrammes le CBO n’arrivait pas à estimer la cardinalité pour C_FREQ=3:<br /><pre>
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 |
------------------------------------------------------------------------------------</pre>
<br />Voyons donc maintenant ce qu’estime le CBO avec l’histogramme FREQUENCY en place pour cette colonne :<br />
<pre>
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 |
--------------------------------------------------------------------------------------------------</pre>
<br />
<div style="text-align: justify;">
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. <br /><br />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. <br /><br />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. </div>
</div>
Ahmed AANGOURhttp://www.blogger.com/profile/03378785645074450748noreply@blogger.com0tag:blogger.com,1999:blog-456686155150137588.post-57715344039715430622015-11-30T10:30:00.000+01:002015-11-30T10:30:42.720+01:00Les histogrammes (1): Introduction<div dir="ltr" style="text-align: left;" trbidi="on">
<div style="text-align: justify;">
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. <br /><br />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 ? <br /><br />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. </div>
<div style="text-align: justify;">
<br /></div>
<u><b>Exemple</b></u>:<br />
<pre>
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.
</pre>
<br />
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.<br />
<div style="text-align: justify;">
</div>
<div style="text-align: justify;">
<style type="text/css">p { margin-bottom: 0.1in; direction: ltr; line-height: 120%; text-align: left; orphans: 2; widows: 2; }</style>
<div style="line-height: 115%; margin-bottom: 0.14in;">
<u><b>Estimations
du CBO sans histogrammes</b></u></div>
<div style="line-height: 115%; margin-bottom: 0.14in;">
Dans un premier
temps on collecte des statistiques pour la table T1 et ses colonnes
mais sans calculer d’histogrammes :</div>
<pre>
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
</pre>
<div style="line-height: 115%; margin-bottom: 0.14in;">
<style type="text/css">p { margin-bottom: 0.1in; direction: ltr; line-height: 120%; text-align: left; orphans: 2; widows: 2; }</style>
</div>
<div style="line-height: 115%; margin-bottom: 0.14in; text-align: justify;">
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.</div>
<div style="line-height: 115%; margin-bottom: 0.14in; text-align: justify;">
Voyons
maintenant ce qu’estime le CBO lorsqu’on requête les lignes de
la table T1 avec C_FREQ = 3 :</div>
<pre>
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 |
------------------------------------------------------------------------------------
</pre>
<div align="justify" style="line-height: 115%; margin-bottom: 0.14in;">
<style type="text/css">p { margin-bottom: 0.1in; direction: ltr; line-height: 120%; text-align: left; orphans: 2; widows: 2; }</style>
</div>
<div style="line-height: 115%; margin-bottom: 0.14in; text-align: justify;">
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:</div>
<div style="line-height: 115%; margin-bottom: 0.14in; text-align: justify;">
NUM_ROWS/NUM_DISTINCT
= 14739/3 = 4913</div>
<div style="line-height: 115%; margin-bottom: 0.14in; text-align: justify;">
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.</div>
<div style="line-height: 115%; margin-bottom: 0.14in; text-align: justify;">
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 :</div>
<pre>
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 |
------------------------------------------------------------------------------------------------</pre>
</div>
<div style="text-align: justify;">
<style type="text/css">p { margin-bottom: 0.1in; direction: ltr; line-height: 120%; text-align: left; orphans: 2; widows: 2; }</style>
<div style="line-height: 115%; margin-bottom: 0.14in; text-align: justify;">
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.</div>
<div style="line-height: 115%; margin-bottom: 0.14in; text-align: justify;">
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.</div>
<div style="line-height: 115%; margin-bottom: 0.14in; text-align: justify;">
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.</div>
</div>
</div>
Ahmed AANGOURhttp://www.blogger.com/profile/03378785645074450748noreply@blogger.com2tag:blogger.com,1999:blog-456686155150137588.post-77821180218171292702015-09-28T21:55:00.002+02:002015-09-28T21:57:06.793+02:00Calcul de statistiques et mode concurrent<div dir="ltr" style="text-align: left;" trbidi="on">
<br />
<div style="text-align: justify;">
Beaucoup d’entre vous utilisent surement le paramètre DEGREE dans les procédures du package DBMS_STATS afin de paralléliser leur traitement de calcul de statistiques. Mais ce que beaucoup ignorent c’est que le parallélisme ne se fait qu’au niveau du segment. Il ne peut y avoir un calcul de statistiques sur plusieurs segments à la fois. Donc, le parallélisme appliqué au calcul de statistiques n’est intéressant que lorsqu’on a affaire à de très grosses tables. Heureusement, depuis la 11.2.0.2 il est possible d’avoir un calcul de statistiques pouvant s’effectuer sur plusieurs tables et partitions en même temps, c’est ce qu’on appelle le mode CONCURRENT. Ce mode permet d’accélérer formidablement le calcul de statistiques en profitant pleinement des ressources de la machine d’autant plus qu’on peut le combiner avec le mode parallel.<br />
<br />
J’ai récemment pu améliorer le temps de calcul de statistiques d’une des bases de mon client grâce au mode concurrent. Pour se rendre compte de l’amélioration apportée voyons d’abord ce que donne le calcul de statistiques effectué sans le mode concurrent sur un schéma de 800GB et dont les plus grosses tables font plusieurs dizaine de GB.</div>
<div style="text-align: justify;">
<u><br /></u></div>
<div style="text-align: justify;">
<h2>
<u>Test du calcul de stats séquentiel</u> :</h2>
</div>
<div style="text-align: justify;">
<pre>
SQL> select sum(bytes)/1024/1024/1024 from dba_segments where owner='SCHEMA1';
SUM(BYTES)/1024/1024/1024
-------------------------
801,098389
exec dbms_stats.gather_schema_stats(ownname => 'SCHEMA1', method_opt => 'for all columns size 1');
PL/SQL procedure successfully completed.
Elapsed: 12:55:42.16</pre>
</div>
<div style="text-align: justify;">
Le calcul de statistiques sur le schéma a mis un peu de moins de 13 heures.<br />
<br />
Pendant que le calcul de statistiques tournait j’ai lancé plusieurs requêtes à des moments différents pour voir les sessions actives sur ma base et on constate bien que le calcul de statistiques se faisait en mode séquentiel car seule une session était en train de travailler:</div>
<div style="text-align: justify;">
<pre>SQL> @sql_cur_stmt
SID SERIAL# PROG ADDRESS HASH_VALUE SQL_ID CHILD PLAN_HASH_VALUE EXECS AVG_ETIME EVENT SQL_TEXT
----- ---------- ---------- ---------------- ---------- ------------- ------ --------------- ---------- ----------- -------------------- -----------------------------------------
2 45373 sqlplus.ex 0000002D356943A8 3121805777 85j1xbkx15yfj 0 226853730 1 692.32 db file sequential r select /*+ no_parallel_index(t, "HISCRN1</pre>
</div>
<div style="text-align: justify;">
<h2>
<u><b>Test du calcul de stats en parallel </b></u>:</h2>
<br />
J’ai ensuite testé le calcul de stats en mode parallel avec un DEGREE de 16<br />
<pre>exec dbms_stats.gather_schema_stats(ownname => 'SCHEMA1', degree=> 16, method_opt => 'for all columns size 1');
Elapsed: 11:15:20.68</pre>
</div>
<div style="text-align: justify;">
Cette fois le calcul de statistiques a mis un peu plus de 11 heures. C’est mieux que le mode séquentiel mais ça reste néanmoins toujours trop long. Pendant que le calcul de statistiques en mode parallèle tournait j’ai également lancé quelques requêtes pour voir ce que j’avais dans mes sessions actives :</div>
<pre> SID SERIAL# PROG ADDRESS HASH_VALUE SQL_ID CHILD PLAN_HASH_VALUE EXECS AVG_ETIME EVENT SQL_TEXT
----- ---------- ---------- ---------------- ---------- ------------- ------ --------------- ---------- ----------- -------------------- -----------------------------------------
5 12814 oracle@eqd 0000002DDAA90A90 3664547584 9k9zph3d6t3s0 0 2617663258 1 50.30 db file scattered re /* SQL Analyze(0) */ select /*+ full(t)
1163 42516 oracle@eqd 0000002DDAA90A90 3664547584 9k9zph3d6t3s0 0 2617663258 1 50.30 db file scattered re /* SQL Analyze(0) */ select /*+ full(t)
235 40059 oracle@eqd 0000002DDAA90A90 3664547584 9k9zph3d6t3s0 0 2617663258 1 50.30 db file scattered re /* SQL Analyze(0) */ select /*+ full(t)
237 46728 oracle@eqd 0000002DDAA90A90 3664547584 9k9zph3d6t3s0 0 2617663258 1 50.30 db file scattered re /* SQL Analyze(0) */ select /*+ full(t)
314 18466 oracle@eqd 0000002DDAA90A90 3664547584 9k9zph3d6t3s0 0 2617663258 1 50.30 db file scattered re /* SQL Analyze(0) */ select /*+ full(t)
391 30356 oracle@eqd 0000002DDAA90A90 3664547584 9k9zph3d6t3s0 0 2617663258 1 50.30 db file scattered re /* SQL Analyze(0) */ select /*+ full(t)
468 20257 oracle@eqd 0000002DDAA90A90 3664547584 9k9zph3d6t3s0 0 2617663258 1 50.30 db file scattered re /* SQL Analyze(0) */ select /*+ full(t)
540 28533 sqlplus.ex 0000002DDAA90A90 3664547584 9k9zph3d6t3s0 0 2617663258 1 50.30 PX Deq: Execute Repl /* SQL Analyze(0) */ select /*+ full(t)
547 15123 oracle@eqd 0000002DDAA90A90 3664547584 9k9zph3d6t3s0 0 2617663258 1 50.30 db file scattered re /* SQL Analyze(0) */ select /*+ full(t)
623 29688 oracle@eqd 0000002DDAA90A90 3664547584 9k9zph3d6t3s0 0 2617663258 1 50.30 db file scattered re /* SQL Analyze(0) */ select /*+ full(t)
702 62908 oracle@eqd 0000002DDAA90A90 3664547584 9k9zph3d6t3s0 0 2617663258 1 50.30 db file scattered re /* SQL Analyze(0) */ select /*+ full(t)
774 31336 oracle@eqd 0000002DDAA90A90 3664547584 9k9zph3d6t3s0 0 2617663258 1 50.30 db file scattered re /* SQL Analyze(0) */ select /*+ full(t)
852 17484 oracle@eqd 0000002DDAA90A90 3664547584 9k9zph3d6t3s0 0 2617663258 1 50.30 db file scattered re /* SQL Analyze(0) */ select /*+ full(t)
931 1917 oracle@eqd 0000002DDAA90A90 3664547584 9k9zph3d6t3s0 0 2617663258 1 50.30 db file scattered re /* SQL Analyze(0) */ select /*+ full(t)
1005 45861 oracle@eqd 0000002DDAA90A90 3664547584 9k9zph3d6t3s0 0 2617663258 1 50.30 db file scattered re /* SQL Analyze(0) */ select /*+ full(t)
1081 37925 oracle@eqd 0000002DDAA90A90 3664547584 9k9zph3d6t3s0 0 2617663258 1 50.30 db file scattered re /* SQL Analyze(0) */ select /*+ full(t)
163 26835 oracle@eqd 0000002DDAA90A90 3664547584 9k9zph3d6t3s0 0 2617663258 1 50.30 ON CPU /* SQL Analyze(0) */ select /*+ full(t)</pre>
<br />
<div style="text-align: justify;">
On constate bien que le calcul de statistiques se faisait en parallèle puisque j’ai bien 16 sessions correspondant à mes process slaves et une session correspondant à ma session principale. On s’aperçoit également que bien que j’ai plusieurs sessions en parallèles elles travaillent toutes sur un seul objet qui est la table T. Cela rejoint donc ce que je disais en préambule de mon article : <b>le mode parallel ne peut s’effectuer qu’au niveau segment et non pas sur plusieurs tables à la fois.</b></div>
<div style="text-align: justify;">
<br /></div>
<div style="text-align: justify;">
<br /></div>
<h2>
<u><b>Test du calcul de stats en mode concurrent </b></u>:</h2>
<h2>
</h2>
<div style="text-align: justify;">
</div>
<h2 style="text-align: left;">
</h2>
<div style="text-align: justify;">
Pour utiliser le mode CONCURRENT il suffit de positionner le paramètre global CONCURRENT qui est désactivé par défaut (même en 12c). Le nombre de sessions concurrentes va dépendre de la valeur du paramètre job_queue_processes.<br />
<br />
Voyons ce que le mode CONCURRENT donne pour mon schéma SCHEMA1 :</div>
<div style="text-align: justify;">
<pre>exec DBMS_STATS.SET_GLOBAL_PREFS('CONCURRENT','TRUE');
exec dbms_stats.gather_schema_stats(ownname => 'SCHEMA1', method_opt => 'for all columns size 1');
ORA-20000: Unable to gather statistics concurrently: Resource Manager is not enabled.
ORA-06512: at "SYS.DBMS_STATS", line 35980
ORA-06512: at line 1</pre>
</div>
<div style="text-align: justify;">
Le message d’erreur m’indique qu’il n’y a pas de Resource Plan de défini, or pour que le calcul de statistiques se fasse en mode concurrent il faut que Ressource Manager puisse gérer les ressources consommées par chaque job. On peut soit définir son propre Resource Plan soit utiliser celui fourni par Oracle qui est le DEFAULT_PLAN :</div>
<div style="text-align: justify;">
<pre>ALTER SYSTEM SET RESOURCE_MANAGER_PLAN = default_plan;
exec DBMS_STATS.SET_GLOBAL_PREFS('CONCURRENT','TRUE');
exec dbms_stats.gather_schema_stats(ownname => 'SCHEMA1', method_opt => 'for all columns size 1');
PL/SQL procedure successfully completed.
Elapsed: 02:25:16.99</pre>
</div>
<div style="text-align: justify;">
<b>Cette fois ci le calcul de stats a pris 2h25 au lieu de 12h55 en mode séquentiel et 11h15 en mode parallèle.</b> Le gain est énorme.<br />
<br />
Voici ce qu’on pouvait voir dans V$SESSION pendant que les jobs concurrents tournaient :</div>
<div style="text-align: justify;">
<pre>SQL> @sql_cur_stmt
SID SERIAL# PROG ADDRESS HASH_VALUE SQL_ID CHILD PLAN_HASH_VALUE EXECS AVG_ETIME EVENT SQL_TEXT
----- ---------- ---------- ---------------- ---------- ------------- ------ --------------- ---------- ----------- -------------------- -----------------------------------------
623 9359 oracle@eqd 0000002D32EF62F8 2948485349 3g5vrh6rvwn75 0 968244646 1 12.76 db file scattered re /* SQL Analyze(1) */ select /*+ full(t)
1085 50128 oracle@eqd 0000002D3518A618 4209280689 4abxz8gxf91pj 1 1515091901 1 113.57 db file sequential r select /*+ no_parallel_index(t, "IHSPOS1
701 8020 oracle@eqd 0000002CD975A2D8 3982881736 4qj5dp7qqbwy8 1 2053917943 1 1,079.45 db file sequential r select /*+ no_parallel_index(t, "IAVOPEI
86 30708 oracle@eqd 0000002D148BDE88 1780980331 59p1yadp2g6mb 0 0 36 .02 Streams AQ: waiting call DBMS_AQADM_SYS.REGISTER_DRIVER ( )
2 45373 sqlplus.ex 0000002DFC7337D0 3466778119 8kruwyz7a5ph7 0 0 2 23,393.51 Streams AQ: waiting BEGIN dbms_stats.gather_schema_stats(ownn
930 47139 oracle@eqd 0000002E0DE75D10 2559624494 96u7bryc91j9f 1 614356722 1 1,905.43 db file sequential r select /*+ no_parallel_index(t, "HISMVC1
548 12639 oracle@eqd 0000002CD8900280 1387000160 auv55cp9arwb0 1 981273022 1 65.32 db file sequential r select /*+ no_parallel_index(t, "ITPCRO9
775 22196 oracle@eqd 0000002C7FC02950 2839555010 d92sd5qnn0ay2 1 3067632440 1 42.85 db file sequential r select /*+ no_parallel_index(t, "RTPSPR1
773 32465 oracle@eqd 0000002CD3983ED0 1227027996 dh8dxq14k5xhw 0 3635621864 1 294.04 db file scattered re /* SQL Analyze(1) */ select /*+ full(t)
317 19697 oracle@eqd 0000002C7E6227C0 2780047690 dtkjh72kv8aaa 0 3588248285 1 .01 db file scattered re /* SQL Analyze(1) */ select /*+ full(t)
469 42289 oracle@eqd 0000002CDB8A0F60 1695986908 fzp1yupkjdd6w 1 43487272 1 862.32 db file sequential r select /*+ no_parallel_index(t, "AVICNF8</pre>
<br />
Comme le paramètre job_queue_processes est positionné à 10 j’ai uniquement 10 sessions concurrentes, et on s’aperçoit que contrairement au calcul de statistiques en parallèle chaque session travaille sur des tables différentes.<br />
<br />
On peut penser qu’avec un paramètre job_queue_processes supérieur à 10 il aurait été possible d’obtenir un meilleur résultat.<br />
<br />
Il est également possible de combiner le mode concurrent avec le calcul de statistiques en parallèle. Je l’ai testé pour mon schéma mais le gain n’est que de 2 minutes :<br />
<pre>-- test calcul de stats concurrent+parallel
exec DBMS_STATS.SET_GLOBAL_PREFS('CONCURRENT','TRUE');
alter system set parallel_adaptive_multi_user=false;
exec dbms_stats.gather_schema_stats(ownname => 'SCHEMA1', degree=> 16, method_opt => 'for all columns size 1');
PL/SQL procedure successfully completed.
Elapsed: 02:23:41.27</pre>
</div>
</div>
Ahmed AANGOURhttp://www.blogger.com/profile/03378785645074450748noreply@blogger.com1tag:blogger.com,1999:blog-456686155150137588.post-20683469116701482362015-09-10T21:06:00.001+02:002015-09-10T21:09:27.204+02:00Direct path reads et "enq KO – fast object checkpoint"<div dir="ltr" style="text-align: left;" trbidi="on">
<br />
Bien que cet event commence par le mot « enq » il ne s’agit pas d’un enqueue au sens attente sur un lock. Je dis ça car récemment un développeur est venu me voir pour se plaindre de locks au niveau de sa base car il avait vu que certaines de ses requêtes étaient en attente sur cet event.<br />
<br />
En réalité, si vous rencontrez cet event c’est que le moteur Oracle a choisi d’effectuer son full scan en utilisant du Direct Path Read et donc de bypasser le buffer cache en lisant les blocks directement depuis les datafiles. Cet event se déclenche si, parmi les blocs à lire, certains sont dirty (blocs modifiés dans le buffer cache mais pas encore envoyés par le processus DB Writer dans le disque). Pour avoir une lecture consistante un checkpoint doit être effectué pour mettre à jour les dirty blocs de la table à lire au niveau du disque. <b>Pendant cette opération le process server en charge du direct path read attend sur l’event <i>« enq KO – fast object checkpoint »</i>.</b><br />
<br />
Un exemple valant mieux qu’un long discours je me suis amusé à faire le petit test suivant sur ma base 12.1.0.2:<br />
<div style="text-align: justify;">
<pre>SQL> create table T1 as select * from dba_objects;
Table created.
SQL> update T1 set created=sysdate where owner<>'SYS';
784976 rows updated.</pre>
</div>
<div style="text-align: justify;">
Dans une session 1 j’ai créé une table T1 puis j’ai lancé un update de 78976 lignes pour avoir des dirty blocs dans le buffer cache.<br />
<br />
La requête ci-dessous nous indique qu’on a 16 925 blocks qui sont dirty :</div>
<div style="text-align: justify;">
<pre>SQL> SELECT count(*)
2 FROM v$bh b, dba_objects o
3 WHERE b.objd = o.data_object_id and o.object_name='T1' and dirty='Y';
COUNT(*)
----------
16925</pre>
</div>
<div style="text-align: justify;">
Dans une autre session je fais un count de la table en forçant le moteur SQL à choisir un Direct Path read en jouant sur le paramètre caché <b><i>_serial_direct_read</i></b> :</div>
<div style="text-align: justify;">
<pre>-- session 2
SQL> alter session set "_serial_direct_read"=always;
Session altered.
SQL> @10046.sql
SQL> set echo on feed on
SQL> ALTER SESSION SET max_dump_file_size = UNLIMITED;
Session altered.
SQL> ALTER SESSION SET TRACEFILE_IDENTIFIER = 'ahmed_10046_sql_trace';
Session altered.
SQL> -- enables SQL trace at level 12 for the session executing it.
SQL> ALTER SESSION SET events '10046 trace name context forever, level 12';
Session altered.
SQL> select count(*) from T1;
COUNT(*)
----------
1465568
SQL> @dis_10046.sql
SQL> set echo on feed on
SQL> -- disables SQL trace for the session executing it.
SQL> ALTER SESSION SET events '10046 trace name context off';
Session altered.</pre>
</div>
<div style="text-align: justify;">
</div>
<br />
J’ai préalablement activé une trace 10046 level 12 pour capturer les wait event et voici un extrait de la trace :<br />
<pre>WAIT #427376872: <b>nam='enq: KO - fast object checkpoint'</b> ela= 769931 name|mode=1263468550 2=65568 0=1 obj#=-1 tim=131292719765
WAIT #427376872: nam='Disk file operations I/O' ela= 10331 FileOperation=2 fileno=1 filetype=2 obj#=104369 tim=131292730309
WAIT #427376872: nam='direct path read' ela= 3831 file number=1 first dba=99529 block cnt=55 obj#=104369 tim=131292734495
WAIT #427376872: nam='direct path read' ela= 903 file number=1 first dba=102528 block cnt=72 obj#=104369 tim=131292737068
WAIT #427376872: nam='direct path read' ela= 2035 file number=1 first dba=102656 block cnt=128 obj#=104369 tim=131292741080
WAIT #427376872: nam='direct path read' ela= 716 file number=1 first dba=102784 block cnt=128 obj#=104369 tim=131292745168</pre>
<br />
<div style="text-align: justify;">
Jetez un oeil à la première ligne: avant de voir les wait events correspondant à l’accès disque en direct path on voit que le process server a attendu 0.76 seconde sur l’event <i>« enq: KO - fast object checkpoint »</i> car il a fallu que le process DB writer flush les dirty blocks de la table sur disque avant qu'il puisse faire ses direct I/Os.<br />
<br />
D’ailleurs lorsque j'interroge de nouveau la vue v$bh je constate qu’il n’existe plus de dirty blocks en mémoire pour la table T1 :</div>
<div style="text-align: justify;">
<pre>SQL> SELECT count(*)
2 FROM v$bh b, dba_objects o
3 WHERE b.objd = o.data_object_id and o.object_name='T1' and dirty='Y';
COUNT(*)
----------
0</pre>
</div>
<div style="text-align: justify;">
Par contre on a toujours des dirty blocks appartenant à d’autres segments que ma table T1 cela prouve que le checkpoint engendré par le direct path read n’est pas globale au buffer cache mais local aux blocs de la table requête (ce qui est plutôt rassurant pour la performance des requêtes en direct path reads, je pense notamment à l'exadata):</div>
<div style="text-align: justify;">
<pre>SQL> SELECT count(*)
2 FROM v$bh b, dba_objects o
3 WHERE b.objd = o.data_object_id and o.object_name<>'T1' and dirty='Y';
COUNT(*)
----------
37</pre>
</div>
<div style="text-align: justify;">
<br /></div>
<div style="text-align: justify;">
<br /></div>
</div>
Ahmed AANGOURhttp://www.blogger.com/profile/03378785645074450748noreply@blogger.com1tag:blogger.com,1999:blog-456686155150137588.post-73066268853102177932015-07-27T22:27:00.000+02:002015-07-27T22:27:19.759+02:00Oracle 12c : ORA-65096 lors de la création d’un user<div dir="ltr" style="text-align: left;" trbidi="on">
<div style="text-align: justify;">
Si vous obtenez l’erreur ORA-65096 après une commande CREATE USER c’est que vous êtes sur une instance oracle 12c multinenant et que vous tentez de créer un user local dans le root container. Le multitenant de la 12c intègre une nouvelle différenciation entre les users dits "communs" et les users dits "locaux". <b>Le local user est celui qui est créé sous une pluggable database (PDB) et dont les privilèges sont limités à cette PDB alors que le common user est celui créé sous le container root et dont les privilèges peuvent s’appliquer sur toutes les PDBs.</b><br /><br />Voici un exemple pour bien comprendre :<br /><br />Disons que j’ai besoin de créer un user AHMED dans ma base de données 12c multitenant. En 11g ou avec une base non-multitenant je n’ai qu’à me connecter en sys et exécuter une commande de type CREATE USER mais si j’agis de la même manière sur ma base multitenant voilà ce que j’obtiens : </div>
<pre>
C:\Ahmed\Scripts\sqlplus>sqlplus / as sysdba
SQL*Plus: Release 12.1.0.2.0 Production on Jeu. Juil. 23 09:21:41 2015
Copyright (c) 1982, 2014, Oracle. All rights reserved.
Connecté :
Oracle Database 12c Enterprise Edition Release 12.1.0.2.0 - 64bit Production
With the Partitioning, OLAP, Advanced Analytics and Real Application Testing options
SQL> show user
USER est "SYS"
SQL> create user ahmed identified by "ahmed#1" container=current;
create user ahmed identified by "ahmed#1" container=current
*
ERROR at line 1:
ORA-65049: creation of local user or role is not allowed in CDB$ROOT
SQL> create user ahmed identified by "ahmed#1" container=all;
create user ahmed identified by "ahmed#1" container=all
*
ERROR at line 1:
ORA-65096: invalid common user or role name</pre>
<br />
</div>
<br /> J’obtiens un message d’erreur qui me dit qu’il m’est impossible de créer un local user ou un role sous le container root. En effet, si j’affiche le nom du container je m’aperçois que je suis bien sous le ROOT :<br /><pre>
SQL> show con_name
CON_NAME
------------------------------
CDB$ROOT</pre>
<br /><div style="text-align: justify;">
Si je souhaite créer un user pour une PDB en particulier alors je dois me connecter à cette PDB et créér un user qui sera local :</div>
<pre>
SQL> alter session set container=PDB1;
Session altered.
SQL> show con_name
CON_NAME
--------------------
PDB1
SQL> create user ahmed identified by "ahmed#1";
User created.</pre>
<div style="text-align: justify;">
Cette fois-ci mon user AHMED a bien été créé.<br /><br />Si j’avais voulu créé un common user c’est-à-dire un user se trouvant dans le container ROOT, j’aurais dû préfixer le nom du user par c## : </div>
<pre>
SQL> show user
USER est "SYS"
SQL> sho con_name
CON_NAME
----------------------
CDB$ROOT
SQL> create user c##ahmed identified by "ahmed#1";
User created.</pre>
Ahmed AANGOURhttp://www.blogger.com/profile/03378785645074450748noreply@blogger.com7tag:blogger.com,1999:blog-456686155150137588.post-73058702465875143572015-06-08T16:41:00.000+02:002015-06-08T16:41:00.876+02:00Disjunctive Subquery<div dir="ltr" style="text-align: left;" trbidi="on">
<br />Il y’a quelques semaines j’ai eu à analyser la requête suivante sur une base de production:<br />
<pre>
SELECT Entities.EntityId,
Entities.EntityName,
Entities.LegalName
FROM Entities
WHERE (Entities.EntityId IN
(SELECT Agreements.PrincipalId
FROM Agreements
WHERE ( ( (Agreements.BusinessLine IN (1, 2, 3)
AND Agreements.PrincipalManagingLocationId IN (144, 15, 16))
AND Agreements.AgreementGroupId IS NULL)
AND Agreements.BusinessLine NOT IN (4))
)
OR Entities.EntityId IN
(SELECT Agreements.CounterpartyId
FROM Agreements
WHERE ( ( (Agreements.BusinessLine IN (1, 2, 3)
AND Agreements.PrincipalManagingLocationId IN (144, 15, 16))
AND Agreements.AgreementGroupId IS NULL)
AND Agreements.BusinessLine NOT IN (4)) )) ;</pre>
<br />
<div style="text-align: justify;">
Cette requête s’exécutait en un peu moins de 21 minutes pour retourner 258 lignes. Quand on regarde la requête de plus près on se rend compte qu’elle contient un bloc principal accédant à la table ENTITIES et une double sous requête séparée par une clause OR.</div>
<div style="text-align: justify;">
Voici son plan d’exécution :</div>
<pre>
-------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 258 |00:20:52.02 | 210M|
|* 1 | FILTER | | 1 | | 258 |00:20:52.02 | 210M|
| 2 | TABLE ACCESS FULL| ENTITIES | 1 | 64326 | 64368 |00:00:00.08 | 1400 |
|* 3 | TABLE ACCESS FULL| AGREEMENTS | 64368 | 1 | 19 |00:10:22.58 | 105M|
|* 4 | TABLE ACCESS FULL| AGREEMENTS | 64349 | 1 | 239 |00:10:28.89 | 104M|
-------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter(( IS NOT NULL OR IS NOT NULL))
3 - filter(("AGREEMENTS"."PRINCIPALID"=:B1 AND
INTERNAL_FUNCTION("AGREEMENTS"."PRINCIPALMANAGINGLOCATIONID") AND
"AGREEMENTS"."BUSINESSLINE"<>4 AND INTERNAL_FUNCTION("AGREEMENTS"."BUSINESSLINE")
AND "AGREEMENTS"."AGREEMENTGROUPID" IS NULL))
4 - filter(("AGREEMENTS"."COUNTERPARTYID"=:B1 AND
INTERNAL_FUNCTION("AGREEMENTS"."PRINCIPALMANAGINGLOCATIONID") AND
"AGREEMENTS"."BUSINESSLINE"<>4 AND INTERNAL_FUNCTION("AGREEMENTS"."BUSINESSLINE")
AND "AGREEMENTS"."AGREEMENTGROUPID" IS NULL))</pre>
<br />
<div style="text-align: justify;">
La première chose qui nous marque dans le plan c’est que l’optimiseur (CBO) n’a pas unnesté la subquery pour effectuer une opération de semi-jointure (voir mon article sur les <a href="http://ahmedaangour.blogspot.fr/2011/04/semi-joins-in-vs-exists.html" target="_blank">semi-joins</a>) plus performante que l’opération FILTER qu’on voit dans le plan. En effet, les statistiques d’exécution du plan montrent que la table ENTITIES parcouru via un Full Table Scan retourne 64368 lignes (cf. colonne A-ROWS de l’opération 2) et la table AGREEMENTS est accédée à 2 reprises autant de fois qu’il y’a de lignes dans la table ENTITIES (cf. colonne STARTS du plan). Le nombre de logical I/Os qui en résulte est très important (210M). On est ici dans un cas de Disjunctive Subquery, c’est-à-dire qu’à cause de la clause OR le CBO ne peut unnester la sous-requête. Le seul moyen d’obtenir un plan efficace pour cette requête est de la réécrire afin de remplacer le OR par un UNION ALL. </div>
<div style="text-align: justify;">
Voici la requête réécrite :</div>
<pre>
SELECT
Entities.EntityId,
Entities.EntityName,
Entities.LegalName
FROM Entities
WHERE (Entities.EntityId IN
(SELECT Agreements.PrincipalId
FROM Agreements
WHERE ( ( (Agreements.BusinessLine IN (1, 2, 3)
AND Agreements.PrincipalManagingLocationId IN (144, 15, 16))
AND Agreements.AgreementGroupId IS NULL)
AND Agreements.BusinessLine NOT IN (4))
))
UNION all
SELECT
Entities.EntityId,
Entities.EntityName,
Entities.LegalName
FROM Entities
WHERE (Entities.EntityId NOT IN
(SELECT Agreements.PrincipalId
FROM Agreements
WHERE ( ( (Agreements.BusinessLine IN (1, 2, 3)
AND Agreements.PrincipalManagingLocationId IN (144, 15, 16))
AND Agreements.AgreementGroupId IS NULL)
AND Agreements.BusinessLine NOT IN (4))
))
AND (Entities.EntityId IN
(SELECT Agreements.CounterpartyId
FROM Agreements
WHERE ( ( (Agreements.BusinessLine IN (1, 2, 3)
AND Agreements.PrincipalManagingLocationId IN (144, 15, 16))
AND Agreements.AgreementGroupId IS NULL)
AND Agreements.BusinessLine NOT IN (4))
)) ;</pre>
<br />
<br />
<div style="text-align: justify;">
L’idée est de supprimer la clause OR en écrivant 2 requêtes distinctes séparées par un UNION ALL. </div>
<div style="text-align: justify;">
Le plan qui en résulte est le suivant :</div>
<pre>
-----------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
-----------------------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 258 |00:00:00.04 | 4308 | | | |
| 1 | SORT UNIQUE | | 1 | 466 | 258 |00:00:00.04 | 4308 | 38912 | 38912 |34816 (0)|
| 2 | UNION-ALL | | 1 | | 536 |00:00:00.04 | 4308 | | | |
| 3 | NESTED LOOPS | | 1 | | 268 |00:00:00.02 | 2134 | | | |
| 4 | NESTED LOOPS | | 1 | 233 | 268 |00:00:00.02 | 1866 | | | |
|* 5 | TABLE ACCESS FULL | AGREEMENTS | 1 | 233 | 268 |00:00:00.02 | 1634 | | | |
|* 6 | INDEX UNIQUE SCAN | PK_ENTITIES | 268 | 1 | 268 |00:00:00.01 | 232 | | | |
| 7 | TABLE ACCESS BY INDEX ROWID| ENTITIES | 268 | 1 | 268 |00:00:00.01 | 268 | | | |
| 8 | NESTED LOOPS | | 1 | | 268 |00:00:00.01 | 2174 | | | |
| 9 | NESTED LOOPS | | 1 | 233 | 268 |00:00:00.01 | 1906 | | | |
|* 10 | TABLE ACCESS FULL | AGREEMENTS | 1 | 233 | 268 |00:00:00.01 | 1634 | | | |
|* 11 | INDEX UNIQUE SCAN | PK_ENTITIES | 268 | 1 | 268 |00:00:00.01 | 272 | | | |
| 12 | TABLE ACCESS BY INDEX ROWID| ENTITIES | 268 | 1 | 268 |00:00:00.01 | 268 | | | |
-----------------------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
5 - filter((INTERNAL_FUNCTION("AGREEMENTS"."PRINCIPALMANAGINGLOCATIONID") AND "AGREEMENTS"."BUSINESSLINE"<>4 AND
INTERNAL_FUNCTION("AGREEMENTS"."BUSINESSLINE") AND "AGREEMENTS"."AGREEMENTGROUPID" IS NULL))
6 - access("ENTITIES"."ENTITYID"="AGREEMENTS"."PRINCIPALID")
10 - filter((INTERNAL_FUNCTION("AGREEMENTS"."PRINCIPALMANAGINGLOCATIONID") AND "AGREEMENTS"."BUSINESSLINE"<>4 AND
INTERNAL_FUNCTION("AGREEMENTS"."BUSINESSLINE") AND "AGREEMENTS"."AGREEMENTGROUPID" IS NULL))
11 - access("ENTITIES"."ENTITYID"="AGREEMENTS"."COUNTERPARTYID") </pre>
<br />
<div style="text-align: justify;">
On constate que le nombre de logical reads est passé de 210M à seulement 4308 et que la requête répond de manière quasi instantané au lieu de 20 minutes. Dans le nouveau plan on voit que l’opération FILTER a disparu pour laisser place à de vraies jointures (NESTED LOOP en l’occurrence). <br /><br />L’inconvénient pour mon client était que cette requête était générée par un progiciel et qu’il n’était donc pas possible de la réécrire. On a donc laissé la requête telle quelle en espérant que l’éditeur nous fournisse rapidement une nouvelle version du progiciel intégrant la réécriture de la requête telle que je l’avais préconisée. Quelques semaines plus tard j’apprends que la base en question a migré de 11.2.0.2 à 11.2.0.4 et que les perfs s’en trouvent largement améliorées. En comparant les bases je me suis rendu compte que la requête précédente s’exécutait désormais très bien sans que le code n'ait été touchée mais avec le plan suivant :</div>
<pre>
---------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
---------------------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 41 |00:00:00.01 | 3354 | | | |
| 1 | NESTED LOOPS | | 1 | 40 | 41 |00:00:00.01 | 3354 | | | |
| 2 | NESTED LOOPS | | 1 | 40 | 41 |00:00:00.01 | 3313 | | | |
| 3 | VIEW | VW_NSO_1 | 1 | 40 | 41 |00:00:00.01 | 3268 | | | |
| 4 | HASH UNIQUE | | 1 | 40 | 41 |00:00:00.01 | 3268 | 2170K| 2170K| 1340K (0)|
| 5 | UNION-ALL | | 1 | | 618 |00:00:00.01 | 3268 | | | |
|* 6 | TABLE ACCESS FULL | AGREEMENTS | 1 | 20 | 309 |00:00:00.01 | 1634 | | | |
|* 7 | TABLE ACCESS FULL | AGREEMENTS | 1 | 20 | 309 |00:00:00.01 | 1634 | | | |
|* 8 | INDEX UNIQUE SCAN | PK_ENTITIES | 41 | 1 | 41 |00:00:00.01 | 45 | | | |
| 9 | TABLE ACCESS BY INDEX ROWID| ENTITIES | 41 | 1 | 41 |00:00:00.01 | 41 | | | |
---------------------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
6 - filter(("AGREEMENTS"."PRINCIPALMANAGINGLOCATIONID"=162 AND "AGREEMENTS"."BUSINESSLINE"=1 AND
"AGREEMENTS"."BUSINESSLINE"<>4 AND "AGREEMENTS"."AGREEMENTGROUPID" IS NULL))
7 - filter(("AGREEMENTS"."PRINCIPALMANAGINGLOCATIONID"=162 AND "AGREEMENTS"."BUSINESSLINE"=1 AND
"AGREEMENTS"."BUSINESSLINE"<>4 AND "AGREEMENTS"."AGREEMENTGROUPID" IS NULL))
8 - access("ENTITIES"."ENTITYID"="COUNTERPARTYID")</pre>
<br />On constate que désormais l’optimiseur a pu merger la sous requête pour pouvoir la joindre avec la table ENTITIES. On voit dans le plan les opérations suivantes :<br /><br />- VIEW VW_NSO_1<br />- UNION-ALL<br />
<br />On comprend que le CBO a transformé la requête en instanciant une vue qu’il a nommé VW_NSO_1 et qu’il a créé 2 blocs dans la vue séparés par un UNION. D’ailleurs dans la trace 10053 on voit que la requête transformée par le CBO est la suivante :<br />
<pre>
SELECT "ENTITIES"."ENTITYID" "ENTITYID",
"ENTITIES"."ENTITYNAME" "ENTITYNAME",
"ENTITIES"."LEGALNAME" "LEGALNAME"
FROM (
(SELECT "AGREEMENTS"."COUNTERPARTYID" "COUNTERPARTYID"
FROM "ALGOV5_CIB"."AGREEMENTS" "AGREEMENTS"
WHERE "AGREEMENTS"."BUSINESSLINE" =1
AND "AGREEMENTS"."PRINCIPALMANAGINGLOCATIONID"=162
AND "AGREEMENTS"."AGREEMENTGROUPID" IS NULL
AND "AGREEMENTS"."BUSINESSLINE" <>4
)
UNION
(SELECT "AGREEMENTS"."PRINCIPALID" "PRINCIPALID"
FROM "ALGOV5_CIB"."AGREEMENTS" "AGREEMENTS"
WHERE "AGREEMENTS"."BUSINESSLINE" =1
AND "AGREEMENTS"."PRINCIPALMANAGINGLOCATIONID"=162
AND "AGREEMENTS"."AGREEMENTGROUPID" IS NULL
AND "AGREEMENTS"."BUSINESSLINE" <>4
)) "VW_NSO_1",
"ALGOV5_CIB"."ENTITIES" "ENTITIES"
WHERE "ENTITIES"."ENTITYID"="VW_NSO_1"."COUNTERPARTYID";</pre>
<br />
Dans la trace on voit également les éléments suivants :<br />
<pre>
Dans la trace on voit également les éléments suivants :
Registered qb: SET$7FD77EFD 0xd9e73be0 (SUBQ INTO VIEW FOR COMPLEX UNNEST SET$E74BECDC)
SU: Checking validity of unnesting subquery SET$E74BECDC (#6)
*** 2015-06-01 10:49:47.061
SU: Passed validity checks.
SU: Transform an ANY subquery to semi-join or distinct.</pre>
<br />Je pense que le <i>"SUBQ INTO VIEW FOR COMPLEX UNNEST"</i> correspond à la nouvelle fonctionnalité du CBO à l’origine de cette réécriture intelligente. Pour m’assurer que le nouveau plan était bien lié au code du CBO en 11.2.0.4 j’ai testé la requête en settant le parameter optimizer_features_enable à “11.2.0.2” et j’ai effectivement obtenu le mauvais plan à savoir celui sans le unnest de la subquery:<br /><pre>
alter session set optimizer_features_enable='11.2.0.2'
Plan hash value: 289829209
-------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 41 |00:07:24.66 | 214M|
|* 1 | FILTER | | 1 | | 41 |00:07:24.66 | 214M|
| 2 | TABLE ACCESS FULL| ENTITIES | 1 | 65613 | 65637 |00:00:00.02 | 1447 |
|* 3 | TABLE ACCESS FULL| AGREEMENTS | 65637 | 1 | 3 |00:01:56.25 | 107M|
|* 4 | TABLE ACCESS FULL| AGREEMENTS | 65634 | 1 | 38 |00:05:28.24 | 107M|
-------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter(( IS NOT NULL OR IS NOT NULL))
3 - filter(("AGREEMENTS"."PRINCIPALMANAGINGLOCATIONID"=162 AND
"AGREEMENTS"."PRINCIPALID"=:B1 AND "AGREEMENTS"."BUSINESSLINE"=1 AND
"AGREEMENTS"."BUSINESSLINE"<>4 AND "AGREEMENTS"."AGREEMENTGROUPID" IS NULL))
4 - filter(("AGREEMENTS"."COUNTERPARTYID"=:B1 AND
"AGREEMENTS"."PRINCIPALMANAGINGLOCATIONID"=162 AND "AGREEMENTS"."BUSINESSLINE"=1
AND "AGREEMENTS"."BUSINESSLINE"<>4 AND "AGREEMENTS"."AGREEMENTGROUPID" IS NULL))</pre>
<br />
A vrai dire la rééecriture par le CBO de la requête s’effectue dès la version 11.2.0.3. et non pas à partir de la version 11.2.0.4. J’ai pu valider ça en mettant le parametre <i>optimizer_features_enable</i> à “11.2.0.3” et observer que le plan observé en 11.2.0.4 était choisi. <br /><br />Pour plus d’informations concernant le Disjunctive Subquery, je vous invite à lire ces 2 articles écrits par Mohamed Houri: <br /><a href="http://www.toadworld.com/platforms/oracle/w/wiki/11081.tuning-a-disjunctive-subquery.aspx" target="_blank">http://www.toadworld.com/platforms/oracle/w/wiki/11081.tuning-a-disjunctive-subquery.aspx</a><a href="https://www.blogger.com/null"> </a><br />
<a href="https://hourim.wordpress.com/2014/05/12/disjunctive-subquery/" target="_blank">https://hourim.wordpress.com/2014/05/12/disjunctive-subquery/</a><br />
<br /><br />
<br />
</div>
Ahmed AANGOURhttp://www.blogger.com/profile/03378785645074450748noreply@blogger.com0tag:blogger.com,1999:blog-456686155150137588.post-4154510190710600212015-02-10T19:07:00.000+01:002015-02-10T19:07:12.613+01:00Forcer un plan d'une requête à partir d'une autre requête<div dir="ltr" style="text-align: left;" trbidi="on">
<div dir="ltr" trbidi="on">
<div dir="ltr" trbidi="on">
Dans un <a href="http://ahmedaangour.blogspot.fr/2012/07/ajouter-un-hint-sans-toucher-la-requete.html" target="_blank">article précèdent</a>, j'avais montré comment il était possible grâce aux SQL profiles d'ajouter un hint à une requête identifiée par son SQL_ID sans avoir à toucher au code de la requête.</div>
<div dir="ltr" trbidi="on">
<br /></div>
<div dir="ltr" trbidi="on">
Cette fois il s'agit d'une autre problématique. J'ai deux requêtes similaires mais chacune avec un SQL_ID différent et je veux que la première utilise le plan de la 2ème. Grâce à un script de Kerry Osborne et l'utilisation de la procédure DBMS_SQLTUNE il est possible d'attacher le plan d'une requête A à une requête B.</div>
<div dir="ltr" trbidi="on">
<br /></div>
<div dir="ltr" trbidi="on">
Pour améliorer les performances d'une requête d'un client, j'ai récemment eu à utiliser cette technique. Je vais tenter dans cet article de vous expliquer comment j'ai procédé.</div>
<div dir="ltr" trbidi="on">
<br /></div>
<div dir="ltr" trbidi="on">
Mon client m'a envoyé un email la semaine dernière car il se plaignait d'une requête s'exécutant lentement en PROD alors qu'elle était plutôt rapide en recette. En me connectant sur les 2 bases et en exécutant la requête j'ai pu m'apercevoir qu'effectivement en PROD le plan différait de celui en RECETTE. </div>
<div dir="ltr" trbidi="on">
<br /></div>
<div dir="ltr" trbidi="on">
Voici un extrait du plan en PROD. On voit qu'il génère 1992K logical I/Os:</div>
<pre>
Plan hash value: 685980531
------------------------------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads | OMem | 1Mem | Used-Mem |
------------------------------------------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 18 |00:01:18.50 | 1992K| 2 | | | |
| 1 | SORT AGGREGATE | | 18 | 1 | 18 |00:00:00.01 | 74 | 0 | | | |
|* 2 | TABLE ACCESS BY INDEX ROWID | CI_FT | 18 | 4 | 44 |00:00:00.01 | 74 | 0 | | | |</pre>
<br />Le plan en recette ne génère quant à lui que 2K logical reads.<br /><pre>
Plan hash value: 3994356561
------------------------------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads | OMem | 1Mem | Used-Mem |
------------------------------------------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 18 |00:00:01.40 | 2158 | 41 | | | |
| 1 | SORT AGGREGATE | | 18 | 1 | 18 |00:00:00.01 | 55 | 0 | | | |
|* 2 | TABLE ACCESS BY INDEX ROWID | CI_FT | 18 | 4 | 44 |00:00:00.01 | 55 | 0 | | | |</pre>
</div>
<div dir="ltr" trbidi="on">
Tout d'abord ma première idée a été de tester le plan de la base de recette en prod. Pour ce faire j'ai récupéré l'outline (c'est à dire l'ensemble des hints qui constituent le plan) du plan de la base de rectte en utilisant l'opion ADVANCED de la fonction DBMS_XPLAN:</div>
<pre>
SQL> explain plan for
........
SQL> SELECT * FROM table(dbms_xplan.display(NULL, NULL, 'advanced'));
.......
Outline Data
-------------
/*+
BEGIN_OUTLINE_DATA
USE_HASH_AGGREGATION(@"SEL$16")
INDEX_RS_ASC(@"SEL$16" "FT2"@"SEL$16" ("CI_FT"."BILL_ID"))
PUSH_SUBQ(@"SEL$16")
NLJ_BATCHING(@"SEL$15" "PAY"@"SEL$15")
USE_NL(@"SEL$15" "PAY"@"SEL$15")
USE_NL(@"SEL$15" "FT"@"SEL$15")
LEADING(@"SEL$15" "BILL2"@"SEL$15" "FT"@"SEL$15" "PAY"@"SEL$15")
INDEX(@"SEL$15" "PAY"@"SEL$15" ("CI_PAY"."PAY_ID"))
INDEX_RS_ASC(@"SEL$15" "FT"@"SEL$15" ("CI_FT"."MATCH_EVT_ID"))
INDEX_RS_ASC(@"SEL$15" "BILL2"@"SEL$15" ("CI_BILL"."BILL_ID"))
USE_HASH_AGGREGATION(@"SEL$17")
USE_NL(@"SEL$17" "PAY"@"SEL$17")
LEADING(@"SEL$17" "BILL2"@"SEL$17" "PAY"@"SEL$17")
INDEX_RS_ASC(@"SEL$17" "PAY"@"SEL$17" ("CI_PAY"."ACCT_ID"))
INDEX_RS_ASC(@"SEL$17" "BILL2"@"SEL$17" ("CI_BILL"."BILL_ID"))
USE_HASH_AGGREGATION(@"SEL$0D753FAC")
NLJ_BATCHING(@"SEL$0D753FAC" "FT"@"SEL$18")
USE_NL(@"SEL$0D753FAC" "FT"@"SEL$18")
USE_NL(@"SEL$0D753FAC" "FT2"@"SEL$19")
LEADING(@"SEL$0D753FAC" "BILL2"@"SEL$18" "FT2"@"SEL$19" "FT"@"SEL$18")
INDEX(@"SEL$0D753FAC" "FT"@"SEL$18" ("CI_FT"."MATCH_EVT_ID"))
INDEX_RS_ASC(@"SEL$0D753FAC" "FT2"@"SEL$19" ("CI_FT"."BILL_ID"))
INDEX_RS_ASC(@"SEL$0D753FAC" "BILL2"@"SEL$18" ("CI_BILL"."BILL_ID"))
INDEX_RS_ASC(@"SEL$267CE17A" "A1"@"SEL$11" ("CI_FT"."BILL_ID"))
NLJ_BATCHING(@"SEL$7B312CD2" "MATCH"@"SEL$12")
USE_NL(@"SEL$7B312CD2" "MATCH"@"SEL$12")
USE_NL(@"SEL$7B312CD2" "FT2"@"SEL$12")
LEADING(@"SEL$7B312CD2" "A1"@"SEL$13" "FT2"@"SEL$12" "MATCH"@"SEL$12")
INDEX(@"SEL$7B312CD2" "MATCH"@"SEL$12" ("CI_MATCH_EVT"."MATCH_EVT_ID"))
INDEX_RS_ASC(@"SEL$7B312CD2" "FT2"@"SEL$12" ("CI_FT"."MATCH_EVT_ID"))
INDEX_RS_ASC(@"SEL$7B312CD2" "A1"@"SEL$13" ("CI_FT"."BILL_ID"))
USE_HASH_AGGREGATION(@"SEL$10")
USE_NL(@"SEL$10" "TST"@"SEL$10")
LEADING(@"SEL$10" "BILL2"@"SEL$10" "TST"@"SEL$10")
NO_ACCESS(@"SEL$10" "TST"@"SEL$10")
INDEX_RS_ASC(@"SEL$10" "BILL2"@"SEL$10" ("CI_BILL"."BILL_ID"))
INDEX_RS_ASC(@"SEL$2" "CI_FT"@"SEL$2" ("CI_FT"."BILL_ID"))
INDEX(@"SEL$3" "CI_BSEG"@"SEL$3" ("CI_BSEG"."BILL_ID"))
INDEX_RS_ASC(@"SEL$4" "CI_BSEG"@"SEL$4" ("CI_BSEG"."BILL_ID"))
INDEX_RS_ASC(@"SEL$5" "CI_BSEG"@"SEL$5" ("CI_BSEG"."BILL_ID"))
INDEX_RS_ASC(@"SEL$6" "CI_BSEG"@"SEL$6" ("CI_BSEG"."BILL_ID"))
NLJ_BATCHING(@"SEL$7" "ME"@"SEL$7")
USE_NL(@"SEL$7" "ME"@"SEL$7")
LEADING(@"SEL$7" "FT"@"SEL$7" "ME"@"SEL$7")
INDEX(@"SEL$7" "ME"@"SEL$7" ("CI_MATCH_EVT"."MATCH_EVT_ID"))
INDEX_RS_ASC(@"SEL$7" "FT"@"SEL$7" ("CI_FT"."BILL_ID"))
NLJ_BATCHING(@"SEL$8" "L"@"SEL$8")
USE_NL(@"SEL$8" "L"@"SEL$8")
LEADING(@"SEL$8" "BCHAR"@"SEL$8" "L"@"SEL$8")
INDEX(@"SEL$8" "L"@"SEL$8" ("CI_CHAR_VAL_L"."CHAR_TYPE_CD" "CI_CHAR_VAL_L"."CHAR_VAL"
"CI_CHAR_VAL_L"."LANGUAGE_CD"))
INDEX_RS_ASC(@"SEL$8" "BCHAR"@"SEL$8" ("CI_BILL_CHAR"."BILL_ID" "CI_BILL_CHAR"."CHAR_TYPE_CD"
"CI_BILL_CHAR"."SEQ_NUM"))
NO_ACCESS(@"SEL$9" "MA_BALANCE"@"SEL$9")
NO_ACCESS(@"SEL$14" "ELEMENTS"@"SEL$14")
INDEX_RS_ASC(@"SEL$20" "CI_FT"@"SEL$20" ("CI_FT"."BILL_ID"))
NLJ_BATCHING(@"SEL$21" "ME"@"SEL$21")
USE_NL(@"SEL$21" "ME"@"SEL$21")
LEADING(@"SEL$21" "FT"@"SEL$21" "ME"@"SEL$21")
INDEX(@"SEL$21" "ME"@"SEL$21" ("CI_MATCH_EVT"."MATCH_EVT_ID"))
INDEX_RS_ASC(@"SEL$21" "FT"@"SEL$21" ("CI_FT"."BILL_ID"))
INDEX_RS_ASC(@"SEL$1" "BILL"@"SEL$1" ("CI_BILL"."ACCT_ID"))
OUTLINE(@"SEL$13")
OUTLINE(@"SEL$12")
OUTLINE(@"SEL$19")
OUTLINE(@"SEL$18")
OUTLINE(@"SEL$10")
OUTLINE(@"SET$1")
MERGE(@"SEL$13")
OUTLINE(@"SEL$61262C81")
OUTLINE(@"SEL$11")
OUTLINE_LEAF(@"SEL$1")
OUTLINE_LEAF(@"SEL$21")
OUTLINE_LEAF(@"SEL$20")
OUTLINE_LEAF(@"SEL$14")
OUTLINE_LEAF(@"SET$2")
UNNEST(@"SEL$19")
OUTLINE_LEAF(@"SEL$0D753FAC")
OUTLINE_LEAF(@"SEL$17")
OUTLINE_LEAF(@"SEL$15")
OUTLINE_LEAF(@"SEL$16")
OUTLINE_LEAF(@"SEL$9")
OUTLINE_LEAF(@"SEL$10")
PUSH_PRED(@"SEL$10" "TST"@"SEL$10" 1)
OUTLINE_LEAF(@"SET$5715CE2E")
OUTLINE_LEAF(@"SEL$7B312CD2")
OUTLINE_LEAF(@"SEL$267CE17A")
OUTLINE_LEAF(@"SEL$8")
OUTLINE_LEAF(@"SEL$7")
OUTLINE_LEAF(@"SEL$6")
OUTLINE_LEAF(@"SEL$5")
OUTLINE_LEAF(@"SEL$4")
OUTLINE_LEAF(@"SEL$3")
OUTLINE_LEAF(@"SEL$2")
ALL_ROWS
OPT_PARAM('optimizer_index_caching' 50)
OPT_PARAM('optimizer_index_cost_adj' 30)
DB_VERSION('11.2.0.3')
OPTIMIZER_FEATURES_ENABLE('11.2.0.3')
IGNORE_OPTIM_EMBEDDED_HINTS
END_OUTLINE_DATA
*/
...................</pre>
<div>
<div>
(Je vous ai épargné ce qui n'était pas utile dans l'output)</div>
<div>
<br /></div>
<div>
Ensuite j'ai copié cet outline pour le mettre sous forme de hint dans la requête et je l'ai exécutée en PROD. J'ai obtenu le même plan avec des stats d'exécution équivalents à celle de la recette.</div>
<div>
<br /></div>
<div>
L'idéal aurait été de faire une analyse approfondie pour comprendre pourquoi un mauvais plan était choisi en PROD mais le temps ne nous le permettait pas et mon client était satisfait du plan en recette d'autant plus que la requête n'est pas censé être modifiée et que les tables ont une volumétrie stable.</div>
<div>
<br /></div>
<div>
La solution la plus efficace dans ce cas était donc de forcer le bon plan en demandant au client d'ajouter l'ensemble des hints constituant l'outline du bon plan dans le code de la requête. L'inconvénient c'est que mon client n'avait pas la possibilité de modifier cette requête et il n'y avait pas dans l'historique d'exécution de la requête en PROD le bon plan ou un plan avec des statistiques d'exécution satisfaisantes.</div>
<div>
<br /></div>
<div>
Et c'est là que le script de Kerry Osborne entre en jeu:</div>
</div>
<pre>
----------------------------------------------------------------------------------------
--
-- File name: move_sql_profile.sql
--
-- Purpose: Moves a SQL Profile from one statement to another.
-
-- Author: Kerry Osborne
--
-- Usage: This scripts prompts for four values.
--
-- profile_name: the name of the profile to be attached to a new statement
--
-- sql_id: the sql_id of the statement to attach the profile to
--
-- category: the category to assign to the new profile
--
-- force_macthing: a toggle to turn on or off the force_matching feature
--
-- Description: This script is based on a script originally written by Randolf Giest.
-- It's purpose is to allow a statements text to be manipulated in whatever
-- manner necessary (typically with hints) to get the desired plan. Then
-- once a SQL Profile has been created on the new statement, it's SQL Profile
-- can be moved (or attached) to the orignal statement with unmodified text.
--
-- Mods: This script should now work wirh all flavors of 10g and 11g.
--
--
-- See kerryosborne.oracle-guy.com for additional information.
-----------------------------------------------------------------------------------------
accept profile_name -
prompt 'Enter value for profile_name: ' -
default 'X0X0X0X0'
accept sql_id -
prompt 'Enter value for sql_id: ' -
default 'X0X0X0X0'
accept category -
prompt 'Enter value for category (DEFAULT): ' -
default 'DEFAULT'
accept force_matching -
prompt 'Enter value for force_matching (false): ' -
default 'false'
----------------------------------------------------------------------------------------
--
-- File name: profile_hints.sql
--
---------------------------------------------------------------------------------------
--
set sqlblanklines on
declare
ar_profile_hints sys.sqlprof_attr;
cl_sql_text clob;
version varchar2(3);
l_category varchar2(30);
l_force_matching varchar2(3);
b_force_matching boolean;
begin
select regexp_replace(version,'\..*') into version from v$instance;
if version = '10' then
-- dbms_output.put_line('version: '||version);
execute immediate -- to avoid 942 error
'select attr_val as outline_hints '||
'from dba_sql_profiles p, sqlprof$attr h '||
'where p.signature = h.signature '||
'and name like (''&&profile_name'') '||
'order by attr#'
bulk collect
into ar_profile_hints;
elsif version = '11' then
-- dbms_output.put_line('version: '||version);
execute immediate -- to avoid 942 error
'select hint as outline_hints '||
'from (select p.name, p.signature, p.category, row_number() '||
' over (partition by sd.signature, sd.category order by sd.signature) row_num, '||
' extractValue(value(t), ''/hint'') hint '||
'from sys.sqlobj$data sd, dba_sql_profiles p, '||
' table(xmlsequence(extract(xmltype(sd.comp_data), '||
' ''/outline_data/hint''))) t '||
'where sd.obj_type = 1 '||
'and p.signature = sd.signature '||
'and p.name like (''&&profile_name'')) '||
'order by row_num'
bulk collect
into ar_profile_hints;
end if;
select
sql_fulltext
into
cl_sql_text
from
v$sqlarea
where
sql_id = '&&sql_id';
dbms_sqltune.import_sql_profile(
sql_text => cl_sql_text
, profile => ar_profile_hints
, category => '&&category'
, name => 'PROFILE_'||'&&sql_id'||'_moved'
-- use force_match => true
-- to use CURSOR_SHARING=SIMILAR
-- behaviour, i.e. match even with
-- differing literals
, force_match => &&force_matching
);
end;
/
undef profile_name
undef sql_id
undef category
undef force_matching</pre>
<div>
<div>
L'idée de ce script est d'attacher un plan d'une requête (qu'on a réussi à obtenir d'une manière ou d'une autre) à une requête s'exécutant avec un plan non satisfaisant et qu'on ne peut modifier.</div>
<div>
L'exécution de ce script est en fait la dernière étape d'un plan en 3 étapes:</div>
<div>
<br /></div>
<div>
<b>1) Exécuter la requête avec le bon plan (en ajoutant les hints de l'outline)</b></div>
<div>
<b>2) Création d'un SQL profile pour y coller le plan obtenu en (1)</b></div>
<div>
<b>3) Coller le SQL Profile créé en (2) à la requête exécutée par l'application</b></div>
<div>
<br /></div>
<div>
La 3ème étape correspond en fait à l'exécution du script de Kerry Osborne.</div>
<div>
L'étape 1 je l'ai réalisée lorsque j'ai exécuté la requête avec l'outline.</div>
<div>
L'étape 2 consiste à exécuter un autre script de kerry Osborne que j'avais expliqué dans un de mes tous <a href="http://ahmedaangour.blogspot.fr/2011/01/forcer-un-plan-dexecution-via-un-sql.html" target="_blank">premiers articles</a>. A l'étape 1 j'ai obtenu un SQL_ID dccyz592gpzpq pour lequel je veux créer un SQL profile qui va me permettre de figer le plan obtenu:</div>
</div>
<pre>
SQL> @sp_create_sql_profile.sql
SQL> ----------------------------------------------------------------------------------------
SQL> --
SQL> -- File name: create_sql_profile.sql
SQL> --
SQL> -- Purpose: Create SQL Profile based on Outline hints in V$SQL.OTHER_XML.
SQL> --
SQL> -- Author: Kerry Osborne
SQL> --
SQL> -- Usage: This scripts prompts for four values.
SQL> --
SQL> -- sql_id: the sql_id of the statement to attach the profile to (must be in the shared pool)
SQL> --
SQL> -- child_no: the child_no of the statement from v$sql
SQL> --
SQL> -- profile_name: the name of the profile to be generated
SQL> --
SQL> -- category: the name of the category for the profile
SQL> --
SQL> -- force_macthing: a toggle to turn on or off the force_matching feature
SQL> --
SQL> -- Description:
SQL> --
SQL> -- Based on a script by Randolf Giest.
SQL> --
SQL> -- Mods: This is the 2nd version of this script which removes dependency on rg_sqlprof1.sql.
SQL> --
SQL> -- See kerryosborne.oracle-guy.com for additional information.
SQL> ---------------------------------------------------------------------------------------
SQL> --
SQL>
SQL> -- @rg_sqlprof1 '&&sql_id' &&child_no '&&category' '&force_matching'
SQL>
SQL> set feedback off
SQL> set sqlblanklines on
SQL>
SQL> accept sql_id -
> prompt 'Enter value for sql_id: ' -
> default 'X0X0X0X0'
Enter value for sql_id: dccyz592gpzpq
SQL> accept child_no -
> prompt 'Enter value for child_no (0): ' -
> default '0'
Enter value for child_no (0):
SQL> accept profile_name -
> prompt 'Enter value for profile_name (PROF_sqlid_planhash): ' -
> default 'X0X0X0X0'
Enter value for profile_name (PROF_sqlid_planhash):
SQL> accept category -
> prompt 'Enter value for category (DEFAULT): ' -
> default 'DEFAULT'
Enter value for category (DEFAULT):
SQL> accept force_matching -
> prompt 'Enter value for force_matching (FALSE): ' -
> default 'false'
Enter value for force_matching (FALSE): TRUE
SQL>
SQL> declare
2 ar_profile_hints sys.sqlprof_attr;
3 cl_sql_text clob;
4 l_profile_name varchar2(30);
5 begin
6 select
7 extractvalue(value(d), '/hint') as outline_hints
8 bulk collect
9 into
10 ar_profile_hints
11 from
12 xmltable('/*/outline_data/hint'
13 passing (
14 select
15 xmltype(other_xml) as xmlval
16 from
17 v$sql_plan
18 where
19 sql_id = '&&sql_id'
20 and child_number = &&child_no
21 and other_xml is not null
22 )
23 ) d;
24
25 select
26 sql_fulltext,
27 decode('&&profile_name','X0X0X0X0','PROF_&&sql_id'||'_'||plan_hash_value,'&&profile_name')
28 into
29 cl_sql_text, l_profile_name
30 from
31 v$sql
32 where
33 sql_id = '&&sql_id'
34 and child_number = &&child_no;
35
36 dbms_sqltune.import_sql_profile(
37 sql_text => cl_sql_text,
38 profile => ar_profile_hints,
39 category => '&&category',
40 name => l_profile_name,
41 force_match => &&force_matching
42 -- replace => true
43 );
44
45 dbms_output.put_line(' ');
46 dbms_output.put_line('SQL Profile '||l_profile_name||' created.');
47 dbms_output.put_line(' ');
48
49 exception
50 when NO_DATA_FOUND then
51 dbms_output.put_line(' ');
52 dbms_output.put_line('ERROR: sql_id: '||'&&sql_id'||' Child: '||'&&child_no'||' not found in v$sql.');
53 dbms_output.put_line(' ');
54
55 end;
56 /
old 19: sql_id = '&&sql_id'
new 19: sql_id = 'dccyz592gpzpq'
old 20: and child_number = &&child_no
new 20: and child_number = 0
old 27: decode('&&profile_name','X0X0X0X0','PROF_&&sql_id'||'_'||plan_hash_value,'&&profile_name')
new 27: decode('X0X0X0X0','X0X0X0X0','PROF_dccyz592gpzpq'||'_'||plan_hash_value,'X0X0X0X0')
old 33: sql_id = '&&sql_id'
new 33: sql_id = 'dccyz592gpzpq'
old 34: and child_number = &&child_no;
new 34: and child_number = 0;
old 39: category => '&&category',
new 39: category => 'DEFAULT',
old 41: force_match => &&force_matching
new 41: force_match => TRUE
old 52: dbms_output.put_line('ERROR: sql_id: '||'&&sql_id'||' Child: '||'&&child_no'||' not found in v$sql.');
new 52: dbms_output.put_line('ERROR: sql_id: '||'dccyz592gpzpq'||' Child: '||'0'||' not found in v$sql.');</pre>
<br />On vérifie qu'un SQL profile nommé PROF_dccyz592gpzpq_3994356561 a bien été créé:<br /><pre>
SQL> @sp_list_sql_profiles.sql
SQL> col category for a15
SQL> col sql_text for a70 trunc
SQL> select name, category, status, sql_text, force_matching
2 from dba_sql_profiles
3 where sql_text like nvl('&sql_text','%')
4 and name like nvl('&name',name)
5 order by last_modified
6 /
Enter value for sql_text:
old 3: where sql_text like nvl('&sql_text','%')
new 3: where sql_text like nvl('','%')
Enter value for name:
old 4: and name like nvl('&name',name)
new 4: and name like nvl('',name)
NAME CATEGORY STATUS SQL_TEXT FOR
------------------------------ --------------- -------- ---------------------------------------------------------------------- ---
PROF_dccyz592gpzpq_3994356561 DEFAULT ENABLED SELECT YES</pre>
<br /><div>
Et c'est ce SQL profile qu'on veut coller à la requête exécutée par l'application et pour ce faire on utilise le script que j'ai affiché plus haut:</div>
<pre>
SQL> @sp_move_sql_profile.sql
SQL> ----------------------------------------------------------------------------------------
SQL> --
SQL> -- File name: move_sql_profile.sql
SQL> --
SQL> -- Purpose: Moves a SQL Profile from one statement to another.
SQL> -
> -- Author: Kerry Osborne
SQL> --
SQL> -- Usage: This scripts prompts for four values.
SQL> --
SQL> -- profile_name: the name of the profile to be attached to a new statement
SQL> --
SQL> -- sql_id: the sql_id of the statement to attach the profile to
SQL> --
SQL> -- category: the category to assign to the new profile
SQL> --
SQL> -- force_macthing: a toggle to turn on or off the force_matching feature
SQL> --
SQL> -- Description: This script is based on a script originally written by Randolf Giest.
SQL> -- It's purpose is to allow a statements text to be manipulated in whatever
SQL> -- manner necessary (typically with hints) to get the desired plan. Then
SQL> -- once a SQL Profile has been created on the new statement, it's SQL Profile
SQL> -- can be moved (or attached) to the orignal statement with unmodified text.
SQL> --
SQL> -- Mods: This script should now work wirh all flavors of 10g and 11g.
SQL> --
SQL> --
SQL> -- See kerryosborne.oracle-guy.com for additional information.
SQL> -----------------------------------------------------------------------------------------
SQL>
SQL> accept profile_name -
> prompt 'Enter value for profile_name: ' -
> default 'X0X0X0X0'
Enter value for profile_name: PROF_dccyz592gpzpq_3994356561
SQL> accept sql_id -
> prompt 'Enter value for sql_id: ' -
> default 'X0X0X0X0'
Enter value for sql_id: 0496r075a27c8
SQL> accept category -
> prompt 'Enter value for category (DEFAULT): ' -
> default 'DEFAULT'
Enter value for category (DEFAULT):
SQL> accept force_matching -
> prompt 'Enter value for force_matching (false): ' -
> default 'false'
Enter value for force_matching (false): true
SQL>
SQL>
SQL> ----------------------------------------------------------------------------------------
SQL> --
SQL> -- File name: profile_hints.sql
SQL> --
SQL> ---------------------------------------------------------------------------------------
SQL> --
SQL> set sqlblanklines on
SQL>
SQL> declare
2 ar_profile_hints sys.sqlprof_attr;
3 cl_sql_text clob;
4 version varchar2(3);
5 l_category varchar2(30);
6 l_force_matching varchar2(3);
7 b_force_matching boolean;
8 begin
9 select regexp_replace(version,'\..*') into version from v$instance;
10
11 if version = '10' then
12
13 -- dbms_output.put_line('version: '||version);
14 execute immediate -- to avoid 942 error
15 'select attr_val as outline_hints '||
16 'from dba_sql_profiles p, sqlprof$attr h '||
17 'where p.signature = h.signature '||
18 'and name like (''&&profile_name'') '||
19 'order by attr#'
20 bulk collect
21 into ar_profile_hints;
22
23 elsif version = '11' then
24
25 -- dbms_output.put_line('version: '||version);
26 execute immediate -- to avoid 942 error
27 'select hint as outline_hints '||
28 'from (select p.name, p.signature, p.category, row_number() '||
29 ' over (partition by sd.signature, sd.category order by sd.signature) row_num, '||
30 ' extractValue(value(t), ''/hint'') hint '||
31 'from sys.sqlobj$data sd, dba_sql_profiles p, '||
32 ' table(xmlsequence(extract(xmltype(sd.comp_data), '||
33 ' ''/outline_data/hint''))) t '||
34 'where sd.obj_type = 1 '||
35 'and p.signature = sd.signature '||
36 'and p.name like (''&&profile_name'')) '||
37 'order by row_num'
38 bulk collect
39 into ar_profile_hints;
40
41 end if;
42
43
44 /*
45 declare
46 ar_profile_hints sys.sqlprof_attr;
47 cl_sql_text clob;
48 begin
49 select attr_val as outline_hints
50 bulk collect
51 into
52 ar_profile_hints
53 from dba_sql_profiles p, sqlprof$attr h
54 where p.signature = h.signature
55 and name like ('&&profile_name')
56 order by attr#;
57 */
58
59 select
60 sql_fulltext
61 into
62 cl_sql_text
63 from
64 v$sqlarea
65 where
66 sql_id = '&&sql_id';
67
68 dbms_sqltune.import_sql_profile(
69 sql_text => cl_sql_text
70 , profile => ar_profile_hints
71 , category => '&&category'
72 , name => 'PROFILE_'||'&&sql_id'||'_moved'
73 -- use force_match => true
74 -- to use CURSOR_SHARING=SIMILAR
75 -- behaviour, i.e. match even with
76 -- differing literals
77 , force_match => &&force_matching
78 );
79 end;
80 /
old 18: 'and name like (''&&profile_name'') '||
new 18: 'and name like (''PROF_dccyz592gpzpq_3994356561'') '||
old 36: 'and p.name like (''&&profile_name'')) '||
new 36: 'and p.name like (''PROF_dccyz592gpzpq_3994356561'')) '||
old 55: and name like ('&&profile_name')
new 55: and name like ('PROF_dccyz592gpzpq_3994356561')
old 66: sql_id = '&&sql_id';
new 66: sql_id = '0496r075a27c8';
old 71: , category => '&&category'
new 71: , category => 'DEFAULT'
old 72: , name => 'PROFILE_'||'&&sql_id'||'_moved'
new 72: , name => 'PROFILE_'||'0496r075a27c8'||'_moved'
old 77: , force_match => &&force_matching
new 77: , force_match => true
PL/SQL procedure successfully completed.</pre>
<div>
<div>
Ce script prend notamment en paramètre le nom du SQL profile qu'on veut attacher et le SQL_ID de la requête pourlaquelle on veut forcer le plan. Dans mon cas la requête en question avait pour SQL_ID <i>0496r075a27c8</i>.</div>
<div>
Si j'affiche les SQL profiles de ma base je vois que j'ai maintenant un 2ème SQL Profile nommé <i>PROFILE_0496r075a27c8_moved</i> et qui est attaché au SQL_ID <i>0496r075a27c8</i>:</div>
</div>
<pre>
NAME CATEGORY STATUS SQL_TEXT FOR
------------------------------ --------------- -------- ---------------------------------------------------------------------- ---
PROF_dccyz592gpzpq_3994356561 DEFAULT ENABLED SELECT YES
PROFILE_0496r075a27c8_moved DEFAULT ENABLED SELECT DECODE (bill.alt_bill_id, 0, ' ', alt_bill_id) alt_bill_id, YES</pre>
<div>
<div>
Maintenant lorsque mon client lance son application c'est le bon plan qui est exécuté. D'ailleurs lorsque j'affiche le plan exécuté désormais pour cette requête je vois la note suivante à la fin plan qui m'indique que c'est bien grâce au SQL profile que ce plan a été généré:</div>
<pre>
Note
-----
- SQL profile PROFILE_0496r075a27c8_moved used for this statement</pre>
</div>
<div>
<br /></div>
<div>
<br /></div>
<div>
Dans le même thème, voir aussi les articles suivants:</div>
<div>
<a href="http://ahmedaangour.blogspot.fr/2011/01/forcer-un-plan-dexecution-via-un-sql.html" target="_blank">Forcer un plan d'exécution via un SQL profile</a></div>
<div>
<a href="http://ahmedaangour.blogspot.fr/2012/07/ajouter-un-hint-sans-toucher-la-requete.html" target="_blank">Ajouter un hint sans toucher à la requête</a></div>
<div>
<a href="http://ahmedaangour.blogspot.fr/2012/07/ajouter-un-hint-sans-toucher-la-requete_09.html" target="_blank">Ajouter un hint sans toucher à la requête (grâce au SQL patch)</a></div>
<div>
<br /></div>
<div>
Voir également l' article de mon ami <a href="https://hourim.wordpress.com/" target="_blank">Mohamed Houri</a> expliquant comment utiliser SPM pour obtenir à peu près le même résultat que moi avec les SQL profiles:</div>
<div>
<a href="https://hourim.wordpress.com/2014/02/11/how-to-attach-a-hinted-spm-baseline-to-a-non-hinted-sql-query/" target="_blank">How to attach a hinted SPM baseline to a non hinted sql query?</a></div>
<div>
<br /></div>
</div>
Ahmed AANGOURhttp://www.blogger.com/profile/03378785645074450748noreply@blogger.com0tag:blogger.com,1999:blog-456686155150137588.post-40191287171714994232014-05-30T17:02:00.001+02:002014-05-30T17:02:38.115+02:00L'importance du LOCAL_LISTENER<div dir="ltr" style="text-align: left;" trbidi="on">
J'ai été alerté aujourd'hui par des utilisateurs qui obtenaient l'erreur ci-dessous lorsqu'ils souhaitaient accéder à une base de pré-production:<br /><pre>
ERROR:
ORA-01034: ORACLE not available
ORA-27101: shared memory realm does not exist
HPUX-ia64 Error: 2: No such file or directory
ID de processus : 0
ID de session : 0, Numéro de série : 0</pre>
<br />En général je vois cette erreur lorsqu'on tente d'accéder à une base qui est arrêtée. Pourtant pour cette base j'arrivais à me connecter localement sans problème et la base est bien OPEN. Néanmoins, lorsque je me connectais à distance j'obtenais le même message que les utilisateurs:<br /><pre>
ihgbdd@vp2186:/projets/ihg/home/ihgbdd $ sqlplus user_test/USER_TEST_#1@PMIP00
SQL*Plus: Release 11.2.0.3.0 Production on Mar. Mai 27 14:42:41 2014
Copyright (c) 1982, 2011, Oracle. All rights reserved.
ERROR:
ORA-01034: ORACLE not available
ORA-27101: shared memory realm does not exist
HPUX-ia64 Error: 2: No such file or directory
ID de processus : 0
ID de session : 0, Numéro de série : 0</pre>
<br />C'est donc que le problème ne se situait pas au niveau de l'instance elle-même mais plutôt au niveau de la configuration OracleNet.<br />Une connexion Easy Connect me donnait le même message d'erreur:<br /><pre>
ihgbdd@vp2186:/projets/ihg/home/ihgbdd $ sqlplus user_test/USER_TEST_#1@psu459:1550/PMIP00
SQL*Plus: Release 11.2.0.3.0 Production on Mar. Mai 27 14:43:37 2014
Copyright (c) 1982, 2011, Oracle. All rights reserved.
ERROR:
ORA-01034: ORACLE not available
ORA-27101: shared memory realm does not exist
HPUX-ia64 Error: 2: No such file or directory
ID de processus : 0
ID de session : 0, Numéro de série : 0</pre>
<br />Le TNSPING par contre fonctionnait bien.<br /><pre>
ihgbdd@vp2186:/projets/ihg/home/ihgbdd/RUB/RUB1.100.11 $ tnsping pmip00
TNS Ping Utility for Linux: Version 11.2.0.3.0 - Production on 27-MAI -2014 14:38:52
Copyright (c) 1997, 2011, Oracle. All rights reserved.
Fichiers de paramètres utilisés :
/soft/oracle/product/client/11.2.0.3/network/admin/sqlnet.ora
Adaptateur TNSNAMES utilisé pour la résolution de l'alias
Tentative de contact de (DESCRIPTION = (ADDRESS_LIST = (ADDRESS = (PROTOCOL = TCP)(HOST = psu459)(PORT = 1550))) (CONNECT_DATA = (SERVICE_NAME = PMIP00)))
OK (20 msec)</pre>
<br />J'en concluais donc que le listener était bien démarré et qu'il écoutait sur le port 1550 qui (vous l'aurez sans doute noter) ne correspond pas au numéro de port par défaut.<br /><br />A ce moment là me sont revenus mes souvenirs des cours d'admin Oracle DBA1: Pour que l'enregistrement dynamique d'une instance auprès du listener se fasse il faut <div>
- soit utiliser le nom et le port du listener par défaut </div>
<div>
- soit (si le nom ou le port ne sont pas ceux par défaut) définir une entrée TNS comme valeur du paramètre LOCAL_LISTENER.<br />Je suis donc allé vérifier ce que me donnait la valeur de ce paramètre pour ma base en question:<br /><pre>
SQL> sho parameter listener
NAME TYPE VALUE
------------------------------------ ----------- ------------------------------
listener_networks string
local_listener string
remote_listener string</pre>
<br />Comme je me doutais, le paramètre n'était pas setté.<br /><br />Voilà donc la cause de mon problème. Sans cette indication l'instance ne sait pas auprès de quel listener il doit s'enregistrer ni comment le contacter. Ce paramètre est censé lui indiquer le nom du listener ainsi que le numéro du port écouté par ce listener.<br /><br />Comme j'ai un fichier TNSNAMES.ora sur mon serveur de données j'ai pu lui indiquer directement le nom de l'alias TNS pour la base en question :<br /><pre>
SQL> alter system set local_listener='PMIP00';
System altered.</pre>
<br />Si je n'avais pas de fichier TNSNAMES.ora il aurait fallu que j'indique comme valeur du paramètre la partie ADDRESS de l'entrée TNS: (ADDRESS = (PROTOCOL = TCP)(HOST = psu459)(PORT = 1550)).<br /><br />Une fois le paramètre setté l'accès à la base peut s'effectuer sans problème<br /><pre>
hgbdd@vp2186:/projets/ihg/home/ihgbdd $ sqlplus user_test/USER_TEST_#1@PMIP00
SQL*Plus: Release 11.2.0.3.0 Production on Mar. Mai 27 15:19:25 2014
Copyright (c) 1982, 2011, Oracle. All rights reserved.
Connecté à :
Oracle Database 11g Enterprise Edition Release 11.2.0.3.0 - 64bit Production
With the Partitioning, OLAP, Data Mining and Real Application Testing options
SQL></pre>
<br /><br /><b><u>CONCLUSION</u></b>:<br />Si vous n'utilisez pas les valeurs par défaut pour un LISTENER il faut bien penser à setter le paramètre LOCAL_LISTENER sinon l'enregistrement automatique de votre instance auprès du LISTENER ne pourra se faire et vos connexions distantes à la base ne fonctionneront pas.<br /></div>
</div>
Ahmed AANGOURhttp://www.blogger.com/profile/03378785645074450748noreply@blogger.com1tag:blogger.com,1999:blog-456686155150137588.post-15411959837783219282014-03-02T17:02:00.007+01:002014-03-02T17:29:53.620+01:00Quand le CBO choisit l'index non-unique à la place de l'index unique<div dir="ltr" style="text-align: left;" trbidi="on">
J'ai constaté cette semaine sur une des bases sur lesquelles je travaille que la requête suivante s'est mis à bien tourner du jour au lendemain:<br />
<pre>DELETE FROM SCDAT.TRANSRPDATES WHERE TRANSRPDATES.TRANSIK = :v1 AND TRANSRPDATES.ACCIK = :v2;</pre>
<br />
On peut constater cela en regardant les stats au niveau de l'AWR:<br />
<pre>SNAP_ID NODE BEGIN_INTERVAL_TIME SQL_ID PLAN_HASH_VALUE EXECS AVG_ETIME AVG_LIO AVG_PIO AVG_ROWS
---------- ------ ------------------------------ ------------- --------------- ------------ ------------ -------------- ---------- ------------
55 1 20/02/14 17:00:25,756 9ytthuuffcgy5 2252980867 1,263 1.271 15,226.1 ,406967538 1
56 1 20/02/14 18:00:41,362 9ytthuuffcgy5 2,915 1.238 15,213.3 ,208919383 1
57 1 20/02/14 19:00:54,602 9ytthuuffcgy5 2,822 1.258 15,195.6 ,242026931 1
58 1 20/02/14 20:00:07,686 9ytthuuffcgy5 2,895 1.248 15,178.0 ,12193437 1
59 1 20/02/14 21:00:21,148 9ytthuuffcgy5 2,918 1.242 15,160.2 ,257710761 1
60 1 20/02/14 22:00:36,358 9ytthuuffcgy5 2,888 1.248 15,142.3 ,233379501 1
61 1 20/02/14 23:00:51,105 9ytthuuffcgy5 2,881 1.232 15,124.6 ,144741409 1
62 1 21/02/14 00:00:04,016 9ytthuuffcgy5 2,932 1.231 15,106.6 ,224079127 1
63 1 21/02/14 01:00:17,342 9ytthuuffcgy5 2,949 1.224 15,084.1 ,143438454 1
64 1 21/02/14 02:00:30,694 9ytthuuffcgy5 2,952 1.223 15,070.2 ,103319783 1
65 1 21/02/14 03:00:43,780 9ytthuuffcgy5 2,959 1.220 15,051.9 ,100033795 1
66 1 21/02/14 04:00:56,915 9ytthuuffcgy5 2,924 1.214 15,033.7 ,157660739 1
67 1 21/02/14 05:00:09,943 9ytthuuffcgy5 2,971 1.216 15,015.5 ,100302928 1
68 1 21/02/14 06:00:23,053 9ytthuuffcgy5 2,989 1.208 14,997.1 ,096353295 1
69 1 21/02/14 07:00:36,366 9ytthuuffcgy5 2,991 1.207 14,978.6 ,235372785 1
70 1 21/02/14 08:00:49,468 9ytthuuffcgy5 2,943 1.207 14,960.2 ,285762827 1
71 1 21/02/14 09:00:02,396 9ytthuuffcgy5 3,001 1.203 14,941.7 ,154948351 1
72 1 21/02/14 10:00:15,559 9ytthuuffcgy5 2,803 1.201 14,920.2 ,172315376 1
148 1 24/02/14 17:00:54,112 9ytthuuffcgy5 2250495236 63,142 .001 70.2 ,077428653 1</pre>
<div>
<div>
On voit qu'on a eu un switch de plan le 24/02/2014. Avant cette date le plan utilisé générait environ 15000 logical reads par exécution alors que le 24/02/14 le nouveau plan ne générait plus que 70 LR par exécution.</div>
<div>
<br /></div>
<div>
Voyons à quoi ressemble ces 2 plans:</div>
</div>
<pre>Plan hash value: 2250495236
-----------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------------------------
| 0 | DELETE STATEMENT | | | | 3 (100)| |
| 1 | DELETE | TRANSRPDATES | | | | |
| 2 | TABLE ACCESS BY INDEX ROWID| TRANSRPDATES | 1 | 40 | 3 (0)| 00:00:01 |
| 3 | INDEX UNIQUE SCAN | P_TRANSRPDATES | 1 | | 2 (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------
Plan hash value: 2252980867
-----------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------------------------------
| 0 | DELETE STATEMENT | | | | 2 (100)| |
| 1 | DELETE | TRANSRPDATES | | | | |
| 2 | TABLE ACCESS BY INDEX ROWID| TRANSRPDATES | 1 | 92 | 2 (0)| 00:00:01 |
| 3 | INDEX RANGE SCAN | R_TRANSRPDATES_ACCIK | 1 | | 2 (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------------</pre>
<div>
<div>
On remarque que le bon plan consiste en une opération INDEX UNIQUE SCAN alors que le mauvais correspond à une opération INDEX RANGE SCAN. En gros, la requête s'exécute bien lorsque l'index unique P_TRANSRPDATES est utilisé, et s'exécute nettement moins bien lorsque l'index non-unique R_TRANSRPDATES_ACCIK est utilisé.</div>
<div>
L'index unique est un index composite sur les 2 colonnes impliquées dans la requête alors que l'index non-unique est un index sur la colonne ACCIK uniquement.</div>
<div>
La question légitime qu'on se pose c'est "pourquoi le CBO a décidé d'utiliser l'index non-unique avant la date du 24/02 alors que l'index unique existait bien?".</div>
<div>
Pour répondre à cette question j'ai fait un comparatif des stats entre la date du jour et la date du 21/02 en utilisant la procédure DIFF_TABLE_STATS_IN_HISTORY du package DBMS_STATS:</div>
</div>
<pre>SELECT *
FROM table(dbms_stats.diff_table_stats_in_history(
ownname => 'SCDAT',
tabname => 'TRANSRPDATES',
time1 => systimestamp - to_dsinterval('5 00:00:00'),
time2 => NULL,
pctthreshold => 10));
###############################################################################
STATISTICS DIFFERENCE REPORT FOR:
.................................
TABLE : TRANSRPDATES
OWNER : SCDAT
SOURCE A : Statistics as of 21/02/14 09:19:30,901433 +01:00
SOURCE B : Current Statistics in dictionary
PCTTHRESHOLD : 10
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
TABLE / (SUB)PARTITION STATISTICS DIFFERENCE:
.............................................
OBJECTNAME TYP SRC ROWS BLOCKS ROWLEN SAMPSIZE
...............................................................................
TRANSRPDATES T A 0 33172 0 0
B 63293 33172 40 63293
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
COLUMN STATISTICS DIFFERENCE:
.............................
COLUMN_NAME SRC NDV DENSITY HIST NULLS LEN MIN MAX SAMPSIZ
...............................................................................
ACCIK A 0 0 NO 0 0 0
B 1 ,000008023 YES 0 4 C2020 C2020 5415
RPDEFIK A 0 0 NO 0 0 0
B 1 ,000008023 YES 0 2 80 80 5415
TRANSIK A 0 0 NO 0 0 0
B 63048 ,000016047 YES 0 6 C4043 C4054 5415
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
INDEX / (SUB)PARTITION STATISTICS DIFFERENCE:
.............................................
OBJECTNAME TYP SRC ROWS LEAFBLK DISTKEY LF/KY DB/KY CLF LVL SAMPSIZ
...............................................................................
INDEX: P_TRANSRPDATES
.....................
P_TRANSRPDATES I A 0 0 0 0 0 0 2 0
B 63293 763 63293 1 1 31603 2 63293
INDEX: R_TRANSRPDATES_ACCIK
...........................
R_TRANSRPDATES_ I A 0 0 0 0 0 0 2 0
B 63293 223 1 223 580 580 2 63293
INDEX: R_TRANSRPDATES_RPDEFIK
.............................
R_TRANSRPDATES_ I A 0 0 0 0 0 0 2 0
B 63293 214 1 214 580 580 2 63293
###############################################################################</pre>
<div>
<div>
On constate qu'en réalité à la date du 21 les stats étaient à zéro c'est à dire que les stats avaient été calculées alors que la table était vide.</div>
<div>
Il ne faut pas confondre ici les stats à zéro et les stats à NULL. Les stats à NULL signifient une absence de stats et donc dans ce cas le Dynamic Sampling peut être activé selon la valeur du paramètre OPTIMIZER_DYNAMIC_SAMPLING.</div>
<div>
Dans notre cas les stats étaient à zéro avant le 24 ce qui veut dire que le CBO estimait qu'il n'y avait pas de données dans la table alors qu'en réalité bien sûr il y'en avait.</div>
<div>
<br /></div>
<div>
L'idée ensuite est de pouvoir reproduire le mauvais plan en activant une trace 10053 lorsque les stats sont à zéro.</div>
<div>
J'ai pour cela restauré les stats à la date du 21/02:</div>
<pre>exec DBMS_STATS.RESTORE_TABLE_STATS (ownname=>'SCDAT', tabname=>'TRANSRPDATES', as_of_timestamp=>TO_DATE('21/02/2014 09:19:30', 'DD/MM/YYYY HH24:MI:SS'));
@10053
explain plan for
DELETE FROM SCDAT.TRANSRPDATES WHERE TRANSRPDATES.TRANSIK = :v1 AND TRANSRPDATES.ACCIK = :v2;
@dis_10053
Plan hash value: 2252980867
-----------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------------------------------
| 0 | DELETE STATEMENT | | 1 | 92 | 2 (0)| 00:00:01 |
| 1 | DELETE | TRANSRPDATES | | | | |
|* 2 | TABLE ACCESS BY INDEX ROWID| TRANSRPDATES | 1 | 92 | 2 (0)| 00:00:01 |
|* 3 | INDEX RANGE SCAN | R_TRANSRPDATES_ACCIK | 1 | | 2 (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - filter("TRANSRPDATES"."TRANSIK"=TO_NUMBER(:V1))
3 - access("TRANSRPDATES"."ACCIK"=TO_NUMBER(:V2))</pre>
<br />
<div>
<div>
BINGO!! L'optimiseur a choisi l'index non-unique.</div>
<div>
Jettons un oeil à la trace du CBO:</div>
</div>
</div>
<pre>***************************************
BASE STATISTICAL INFORMATION
***********************
Table Stats::
Table: TRANSRPDATES Alias: TRANSRPDATES
#Rows: 0 #Blks: 33172 AvgRowLen: 0.00 ChainCnt: 0.00
Index Stats::
Index: P_TRANSRPDATES Col#: 1 2
LVLS: 2 #LB: 0 #DK: 0 LB/K: 0.00 DB/K: 0.00 CLUF: 0.00
Index: R_TRANSRPDATES_ACCIK Col#: 2
LVLS: 2 #LB: 0 #DK: 0 LB/K: 0.00 DB/K: 0.00 CLUF: 0.00
Index: R_TRANSRPDATES_RPDEFIK Col#: 6
LVLS: 2 #LB: 0 #DK: 0 LB/K: 0.00 DB/K: 0.00 CLUF: 0.00
***************************************
1-ROW TABLES: TRANSRPDATES[TRANSRPDATES]#0
Access path analysis for TRANSRPDATES
***************************************
SINGLE TABLE ACCESS PATH
Single Table Cardinality Estimation for TRANSRPDATES[TRANSRPDATES]
Column (#1): TRANSIK(
AvgLen: 22 NDV: 0 Nulls: 0 Density: 0.000000 Min: 0 Max: 0
Column (#2): ACCIK(
AvgLen: 22 NDV: 0 Nulls: 0 Density: 0.000000 Min: 0 Max: 0
ColGroup (#1, Index) P_TRANSRPDATES
Col#: 1 2 CorStregth: 0.00
ColGroup Usage:: PredCnt: 2 Matches Full: #1 Partial: Sel: 1.0000
Table: TRANSRPDATES Alias: TRANSRPDATES
Card: Original: 0.000000 Rounded: 1 Computed: 0.00 Non Adjusted: 0.00
Access Path: TableScan
Cost: 9540.36 Resp: 9540.36 Degree: 0
Cost_io: 9479.00 Cost_cpu: 236232408
Resp_io: 9479.00 Resp_cpu: 236232408
Access Path: index (UniqueScan)
Index: P_TRANSRPDATES
resc_io: 2.00 resc_cpu: 15583
ix_sel: 0.000000 ix_sel_with_filters: 0.000000
Cost: 2.00 Resp: 2.00 Degree: 1
ColGroup Usage:: PredCnt: 2 Matches Full: #1 Partial: Sel: 1.0000
ColGroup Usage:: PredCnt: 2 Matches Full: #1 Partial: Sel: 1.0000
Access Path: index (AllEqUnique)
Index: P_TRANSRPDATES
resc_io: 2.00 resc_cpu: 15583
ix_sel: 1.000000 ix_sel_with_filters: 1.000000
Cost: 2.00 Resp: 2.00 Degree: 1
Access Path: index (AllEqRange)
Index: R_TRANSRPDATES_ACCIK
resc_io: 2.00 resc_cpu: 14443
ix_sel: 0.010000 ix_sel_with_filters: 0.010000
Cost: 2.00 Resp: 2.00 Degree: 1
Best:: AccessPath: IndexRange
Index: R_TRANSRPDATES_ACCIK
Cost: 2.00 Degree: 1 Resp: 2.00 Card: 0.00 Bytes: 0</pre>
<br />
<div>
<div>
Le CBO calcule le COST pour chaque index et on voit que le COST est à 2 pour chacun d'eux.</div>
<div>
Logiquement on s'imagine qu'il prendrait l'index unique mais en fait pas du tout...il prend l'index non unique.</div>
<div>
Mon sentiment au départ était de dire que le CBO choisissait l'index qui était potentiellement le plus petit c'est à dire celui qui contenait le moins de clés d'index. </div>
<div>
En effet dans mon cas l'index non unique est définie sur seulement une seule colonne alors que l'index unique est définie sur 2 colonnes.</div>
<div>
<br /></div>
<div>
Pour en savoir plus j'ai décidé d'ouvrir <a href="https://community.oracle.com/thread/3522013" target="_blank">une discussion sur le forum d'OTN</a> et j'ai envoyé un mail au spécialiste du CBO Jonathan LEWIS pour l'inviter à y répondre.</div>
<div>
<br /></div>
<div>
Bien sûr Jonathan a répondu et voici sa réponse:</div>
<blockquote class="tr_bq">
For a tie in the cost of the index: at one time the choice was alphabetical by name but a fix came in some time in 10g to select the index with the larger number of distinct keys.<br />
<br />
At present is seems to be:<br />
<br />
If all indexes are unique and the costs are the same then tie-break on number of distinct keys, if those match then alphabetical.<br />
If all indexes are non-unique and the costs are the same then tie-break on number of distinct keys, if those match then alphabetical.<br />
If there is a mixture of unique and non-unique then NON-unique are preferred</blockquote>
Donc, selon Jonathan, lorsqu'on a un COST identique et qu'on est en présence à la fois d'un index UNIQUE et d'un index non-unique, le CBO choisirait automatiquement l'index non-unique.<br />
Je trouve ça complètement illogique surtout lorsque l'on sait que les index range scan par rapport aux index unique scan entrainent un surcoût lié notamment au nombre de latchs plus importants générés et au fait qu'en cas d'index range scan oracle doit checker l'entrée d'index suivant (juste au cas où) car cette opération par définition peut retourner plusieurs lignes alors qu'avec un index unqiue scan Oracle est sûr de n'avoir au plus qu'une seule entrée d'index correspondante.<br />
<br />
Je vous invite à lire <a href="http://richardfoote.wordpress.com/2007/12/21/differences-between-unique-and-non-unique-indexes-part-ii/" target="_blank">l'article</a> de Richard FOOTE sur ce sujet.<br />
<br />
Pour en revenir à mon cas et pour confirmer mon hypothèse de départ sur le nombre de colonnes, j'ai (sur les conseils de mon ami Guy-Georges DROGBA) généré de nouveau le plan après avoir cette fois désactivé le CPU costing:<br />
<pre>exec DBMS_STATS.RESTORE_TABLE_STATS (ownname=>'SCDAT', tabname=>'TRANSRPDATES', as_of_timestamp=>TO_DATE('21/02/2014 09:19:30', 'DD/MM/YYYY HH24:MI:SS'));
alter session set "_optimizer_cost_model"=io;
@10053
explain plan for DELETE FROM SCDAT.TRANSRPDATES WHERE TRANSRPDATES.TRANSIK = :v1 AND TRANSRPDATES.ACCIK = :v2;
@dis_10053
@plan
Plan hash value: 2250495236
-------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost |
-------------------------------------------------------------------------------
| 0 | DELETE STATEMENT | | 1 | 92 | 2 |
| 1 | DELETE | TRANSRPDATES | | | |
| 2 | TABLE ACCESS BY INDEX ROWID| TRANSRPDATES | 1 | 92 | 2 |
|* 3 | INDEX UNIQUE SCAN | P_TRANSRPDATES | 1 | | 2 |
-------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
3 - access("TRANSRPDATES"."TRANSIK"=TO_NUMBER(:V1) AND
"TRANSRPDATES"."ACCIK"=TO_NUMBER(:V2))
alter session set "_optimizer_cost_model"=cpu;
explain plan for DELETE FROM SCDAT.TRANSRPDATES WHERE TRANSRPDATES.TRANSIK = :v1 AND TRANSRPDATES.ACCIK = :v2;
@plan
Plan hash value: 2252980867
-----------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------------------------------
| 0 | DELETE STATEMENT | | 1 | 92 | 2 (0)| 00:00:01 |
| 1 | DELETE | TRANSRPDATES | | | | |
|* 2 | TABLE ACCESS BY INDEX ROWID| TRANSRPDATES | 1 | 92 | 2 (0)| 00:00:01 |
|* 3 | INDEX RANGE SCAN | R_TRANSRPDATES_ACCIK | 1 | | 2 (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - filter("TRANSRPDATES"."TRANSIK"=TO_NUMBER(:V1))
3 - access("TRANSRPDATES"."ACCIK"=TO_NUMBER(:V2))
exec DBMS_STATS.RESTORE_TABLE_STATS (ownname=>'SCDAT', tabname=>'TRANSRPDATES', as_of_timestamp=>systimestamp-1, force=> TRUE);</pre>
<br />
Ah! cette fois lorsque le CPU costing est désactivé c'est bien l'index unique qui est pris en compte. Jonathan Lewis dans la discussion que j'ai ouverte sur OTN disait qu'il était possible que cette fois le choix ait été effectué en prenant en compte l'ordre alphabétique.<br />
Vu qu'alphabétiquement P_TRANSRPDATES soit avant R_TRANSRPDATES_ACCIK on peut effectivement penser cela.<br />
<br />
J'ai donc refait le test en renommant l'index non unique en A_TRANSRPDATES_ACCIK pour qu'il soit alphabétiquement avant l'index unique:<br />
<pre>exec DBMS_STATS.RESTORE_TABLE_STATS (ownname=>'SCDAT', tabname=>'TRANSRPDATES', as_of_timestamp=>TO_DATE('21/02/2014 09:19:30', 'DD/MM/YYYY HH24:MI:SS'));
alter session set "_optimizer_cost_model"=io;
alter index SCDAT.R_TRANSRPDATES_ACCIK rename to A_TRANSRPDATES_ACCIK;
@10053
explain plan for DELETE FROM SCDAT.TRANSRPDATES WHERE TRANSRPDATES.TRANSIK = :v1 AND TRANSRPDATES.ACCIK = :v2;
@dis_10053
@plan
Plan hash value: 2250495236
-------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost |
-------------------------------------------------------------------------------
| 0 | DELETE STATEMENT | | 1 | 92 | 2 |
| 1 | DELETE | TRANSRPDATES | | | |
| 2 | TABLE ACCESS BY INDEX ROWID| TRANSRPDATES | 1 | 92 | 2 |
|* 3 | INDEX UNIQUE SCAN | P_TRANSRPDATES | 1 | | 2 |
-------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
3 - access("TRANSRPDATES"."TRANSIK"=TO_NUMBER(:V1) AND
"TRANSRPDATES"."ACCIK"=TO_NUMBER(:V2))
Note
-----
- cpu costing is off (consider enabling it)
alter session set "_optimizer_cost_model"=cpu;
explain plan for DELETE FROM SCDAT.TRANSRPDATES WHERE TRANSRPDATES.TRANSIK = :v1 AND TRANSRPDATES.ACCIK = :v2;
@plan
Plan hash value: 1992853963
-----------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------------------------------
| 0 | DELETE STATEMENT | | 1 | 92 | 2 (0)| 00:00:01 |
| 1 | DELETE | TRANSRPDATES | | | | |
|* 2 | TABLE ACCESS BY INDEX ROWID| TRANSRPDATES | 1 | 92 | 2 (0)| 00:00:01 |
|* 3 | INDEX RANGE SCAN | A_TRANSRPDATES_ACCIK | 1 | | 2 (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - filter("TRANSRPDATES"."TRANSIK"=TO_NUMBER(:V1))
3 - access("TRANSRPDATES"."ACCIK"=TO_NUMBER(:V2))
alter index SCDAT.A_TRANSRPDATES_ACCIK rename to R_TRANSRPDATES_ACCIK;
exec DBMS_STATS.RESTORE_TABLE_STATS (ownname=>'SCDAT', tabname=>'TRANSRPDATES', as_of_timestamp=>systimestamp-1, force=> TRUE);</pre>
</div>
<div>
<div>
C'est toujours l'index unique qui est pris en compte. Donc on peut dire que dans mon cas lorsque le CPU costing est désactivé c'est l'index unique qui est pris en compte alors que si le CPU costing est activé c'est l'index non unique qui est pris en compte.</div>
<div>
Et la raison c'est surement qu'il est moins couteux d'un point de vue CPU de parcourir un index sur une seule colonne (mon index non-unique) qu'un index sur 2 colonnes (mon index unique).</div>
<div>
<br /></div>
J'ai écrit cet article pour montrer que l'optimiseur d'Oracle a des secrets au niveau de son algorithme que même le grand Jonathan LEWIS n'a pas encore totalement percé car le comportement du CBO varie énormément selon les paramètres, les modèles de données et les requêtes impliquées.<br />
Néanmoins, la vraie conclusion de cette petite expérience c'est qu'il faut absolument se méfier des stats calculés sur des tables vides pour éviter des mésaventures qui peuvent s'avérer extrêmement coûteuses dans un environnement de production.</div>
</div>
Ahmed AANGOURhttp://www.blogger.com/profile/03378785645074450748noreply@blogger.com2tag:blogger.com,1999:blog-456686155150137588.post-71846588821298986182013-12-23T15:22:00.001+01:002013-12-23T15:22:47.072+01:00Les statistiques étendues (2)<div dir="ltr" style="text-align: left;" trbidi="on">
Il y'a un peu plus de 2 ans déjà j'avais rédigé <a href="http://ahmedaangour.blogspot.fr/2011/08/les-statistiques-etendues.html" target="_blank">un article</a> décrivant le principe des statistiques étendues en 11g.<br />
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.<br />
<br />
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.<br />
<br />
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.<br />
<div>
<br /></div>
<div>
<br />
<b><u>Cas 1</u>: Statistiques étendues sur une expression</b></div>
<br />
Le premier problème concernait la requête suivante:<br />
<pre>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'
)
);</pre>
<div>
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.<br />
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.<br />
Dans la requête on peut noter les expressions suivantes:</div>
<pre>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'</pre>
<div>
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.<br />
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):</div>
<pre>----------------------------------------------------------------------------------------------------------------------------------------
| 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")</pre>
<div>
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 .<br />
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:<br />
<pre>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'))</pre>
</div>
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?<br />
D'après le chapitre 5 du <a href="http://www.amazon.com/Cost-Based-Oracle-Fundamentals-Experts-Voice/dp/1590596366" target="_blank">livre de Jonathan Lewis</a>, l'optimiseur appliquerait une sélectivité de 5% pour chaque prédicat (avec expression) de type NOT EQUAL.<br />
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<br />
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:<br />
18449114*0.00000625 = 115.30<br />
<br />
<div>
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.<br />
Les estimations étant totalement biaisées c'est tout le reste du plan qui est faussé.<br />
La solution en 11g consiste à calculer des statistiques étendues sur l'expression substr(RBQ.RBQ_NUM_CONTR, 1, 3):</div>
<pre>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;
/</pre>
<div>
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):<br />
<pre>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)</pre>
<br />
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.<br />
<br />
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:<br />
<pre>-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
| 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")</pre>
<br />
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).<br />
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é.</div>
<div>
<br /></div>
<div>
<br /></div>
<div>
<b><u>Cas 2</u>: Stats étendues sur des colonnes corrélées</b></div>
<div>
<b><br /></b></div>
Le second problème concernait la requête suivante qui mettait 2h34 pour s'exécuter:<br />
<div>
<pre>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))</pre>
</div>
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".<br />
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).<br />
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.<br />
La jointure avec la table ODD_ASSURE s'effectue sur les colonnes NUM_INTEGRATION, ASS_ID_FLUX et ASS_ID_DELEGATAIRE.<br />
Effectuons un comptage sur la table ODD_ASSURE juste avec la clause NUM_INTEGRATION:<br />
<div>
<pre>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 |
---------------------------------------------------------------------------------------------------------------</pre>
</div>
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.<br />
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.<br />
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:<br />
<div>
<pre>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;
/</pre>
</div>
Si on relance la requête on obtient désormais le plan suivant:<br />
<div>
<pre>------------------------------------------------------------------------------------------------------------------------------------------------------------
| 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 | | | | |
------------------------------------------------------------------------------------------------------------------------------------------------------------</pre>
</div>
Les stats étendues ont permis d'obtenir une cardinalité pas tout à fait exact mais en tout cas plus proche de la réalité.<br />
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.<br />
<br />
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.</div>
Ahmed AANGOURhttp://www.blogger.com/profile/03378785645074450748noreply@blogger.com3tag:blogger.com,1999:blog-456686155150137588.post-61325256950217044112013-12-10T17:56:00.000+01:002013-12-10T17:56:38.333+01:00Impact du Clustering Factor dans la mise à jour de tables<div dir="ltr" style="text-align: left;" trbidi="on">
<br />
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).<br /><br />Je n'ai donc pas pu résister à l'idée de faire un petit test case pour le leur prouver.<br /><br />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. <br /><br />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.<div>
<pre>
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);
<div>
Index created.</div>
</pre>
<br />Pour l'instant la table ne contient pas de lignes. <br />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:<br /><pre>
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</pre>
<br />
<div class="MsoNormal">
</div>
BINGO!!!!<br />On s'aperçoit que le 2ème index a généré 30 fois plus de logical reads (116 752 vs 3 840)<br />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.<br /><br />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:<br /><pre>
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</pre>
<br />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.<br /><pre>
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</pre>
<br /><br />
<div class="MsoNormal">
</div>
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.<br /><br /><b><u>CONCLUSION</u></b>:<br /><br />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.<br />
</div>
</div>
Ahmed AANGOURhttp://www.blogger.com/profile/03378785645074450748noreply@blogger.com2tag:blogger.com,1999:blog-456686155150137588.post-86092660961601300112013-11-19T15:40:00.000+01:002013-11-19T15:58:56.545+01:00Attention au partitioning sans pruning<div dir="ltr" style="text-align: left;" trbidi="on">
<div style="text-align: left;">
Ce matin mon client m'a remonté un problème sur 3 process qui tournaient depuis une dizaine d'heures sur une base venant juste d'être migrée en 11g R2 et dont les tables venaient d'être partitionnées.</div>
<div style="text-align: left;">
En listant les sessions actives j'ai remarqué qu'effectivement on avait 3 requêtes qui tournaient respectivement depuis 33963, 36617 et 57709 secondes:</div>
<div>
<pre> <span style="font-family: Courier New, Courier, monospace; font-size: x-small;"> SID PROG ADDRESS HASH_VALUE SQL_ID CHILD PLAN_HASH_VALUE EXECS AVG_ETIME EVENT SQL_TEXT
----- ---------- ---------------- ---------- ------------- ------ --------------- ---------- ----------- -------------------- -----------------------------------------
606 sqlplus@ps C000000CE743FC08 1936645007 00jchz9tqxqwg 3 3981773947 3013 39.00 PL/SQL lock timer SELECT DISTINCT H.GROUP# FROM SYS.WRI$_OP
590 oracle@psp C000000CE743FC08 1936645007 00jchz9tqxqwg 3 3981773947 3013 39.00 PL/SQL lock timer SELECT DISTINCT H.GROUP# FROM SYS.WRI$_OP
1354 uvsh@psp20 C000000CE464D8A8 1685049779 8kgwdsdk6zndm 0 132338241 1 33,963.97 db file scattered re SELECT distinct EVC.EVC_NUM_CONTR,
408 uvsh@psp20 C0000002ADB65E28 3265361904 bwba60z1a2xzh 0 132338241 1 36,617.62 db file sequential r SELECT distinct EVC.EVC_NUM_CONTR,
969 uvsh@psp20 C000000C7E6776B0 1248359757 gwfkut956hxad 0 132338241 1 57,709.34 direct path read SELECT distinct EVC.EVC_NUM_CONTR,</span></pre>
<div>
En regardant de plus près les 3 requêtes je me suis aperçu qu'elles étaient quasiment identiques.<br />
Voilà justement à quoi ressemble une des 3 requêtes:<br />
<pre>SELECT DISTINCT
EVC.EVC_NUM_CONTR,
-- NOM CLIENT par NI
(SELECT MAX (NVL (PPH.PPH_NOM_MARITAL, PPH.PPH_NOM_PATRONYMIQUE))
FROM AODS00.TDO_D_ASSIN_PERS_PHYS PPH,
TDO_D_ASSIN_ROLE_PERSONNE RPE
WHERE RPE.RPE_NUM_CONTR = EVC.EVC_NUM_CONTR
AND RPE.RPE_CD_ROLE LIKE 'O%'
AND RPE.NUM_INTEGRATION = EVC.NUM_INTEGRATION
AND PPH.NUM_INTEGRATION = RPE.NUM_INTEGRATION
AND PPH.PPH_ID_PERSONNE = RPE.RPE_ID_PERSONNE
AND (RPE.RPE_ID_PERSONNE, rpe.RPE_NUM_CONTR) IN
( SELECT MAX (RPE2.RPE_ID_PERSONNE), rpe2.RPE_NUM_CONTR
FROM AODS00.TDO_D_ASSIN_ROLE_PERSONNE RPE2
WHERE RPE2.RPE_CD_ROLE LIKE 'O%'
GROUP BY rpe2.RPE_NUM_CONTR))
AS NOM_CONTRACTANT,
-- NOM personne morale
(SELECT PMO.PMO_RAISON_SOCIALE
FROM TDO_D_ASSIN_ROLE_PERSONNE RPE, TDO_D_ASSIN_PERS_MORA PMO
WHERE RPE.RPE_NUM_CONTR = EVC.EVC_NUM_CONTR
AND RPE.NUM_INTEGRATION = EVC.NUM_INTEGRATION
AND PMO.NUM_INTEGRATION = RPE.NUM_INTEGRATION
AND PMO.PMO_ID_PERSONNE = RPE.RPE_ID_PERSONNE
AND (RPE.RPE_ID_PERSONNE, rpe.RPE_NUM_CONTR) IN
( SELECT MAX (RPE2.RPE_ID_PERSONNE), rpe2.RPE_NUM_CONTR
FROM TDO_D_ASSIN_ROLE_PERSONNE RPE2
GROUP BY rpe2.RPE_NUM_CONTR))
AS NOM2_CONTRACTANT,
-- ID CLIENT par NI
(SELECT MAX (RPe.RPE_ID_PERSONNE)
FROM AODS00.TDO_D_ASSIN_ROLE_PERSONNE RPE
WHERE RPE.NUM_INTEGRATION = EVC.NUM_INTEGRATION
AND RPE.RPE_NUM_CONTR = EVC.EVC_NUM_CONTR
AND RPE.RPE_CD_ROLE LIKE 'O%')
AS ID_CONTRACTANT,
-- ID CLIENT
(SELECT MAX (RPE.RPE_ID_PERSONNE)
FROM AODS00.TDO_D_ASSIN_ROLE_PERSONNE RPE
WHERE RPE.NUM_INTEGRATION <> EVC.NUM_INTEGRATION
AND RPE.RPE_NUM_CONTR = EVC.EVC_NUM_CONTR
AND RPE.RPE_CD_ROLE LIKE 'O%')
AS ID2_CONTRACTANT,
-- PRENOM CLIENT par NI
(SELECT MAX (PPH.PPH_PRENOM)
FROM AODS00.TDO_D_ASSIN_PERS_PHYS PPH,
TDO_D_ASSIN_ROLE_PERSONNE RPE
WHERE RPE.RPE_NUM_CONTR = EVC.EVC_NUM_CONTR
AND RPE.RPE_CD_ROLE LIKE 'O%'
AND RPE.NUM_INTEGRATION = EVC.NUM_INTEGRATION
AND PPH.NUM_INTEGRATION = RPE.NUM_INTEGRATION
AND PPH.PPH_ID_PERSONNE = RPE.RPE_ID_PERSONNE
AND (RPE.RPE_ID_PERSONNE, rpe.RPE_NUM_CONTR) IN
( SELECT MAX (RPE2.RPE_ID_PERSONNE), rpe2.RPE_NUM_CONTR
FROM AODS00.TDO_D_ASSIN_ROLE_PERSONNE RPE2
WHERE RPE2.RPE_CD_ROLE LIKE 'O%'
GROUP BY rpe2.RPE_NUM_CONTR))
AS PRENOM,
-- PRENOM CLIENT sans NI
(SELECT MAX (PPH.PPH_PRENOM)
FROM AODS00.TDO_D_ASSIN_PERS_PHYS PPH,
TDO_D_ASSIN_ROLE_PERSONNE RPE
WHERE RPE.RPE_NUM_CONTR = EVC.EVC_NUM_CONTR
AND RPE.RPE_CD_ROLE LIKE 'O%'
AND RPE.NUM_INTEGRATION <> EVC.NUM_INTEGRATION
AND PPH.NUM_INTEGRATION = RPE.NUM_INTEGRATION
AND PPH.PPH_ID_PERSONNE = RPE.RPE_ID_PERSONNE
AND (RPE.RPE_ID_PERSONNE, rpe.RPE_NUM_CONTR) IN
( SELECT MAX (RPE2.RPE_ID_PERSONNE), rpe2.RPE_NUM_CONTR
FROM AODS00.TDO_D_ASSIN_ROLE_PERSONNE RPE2
WHERE RPE2.RPE_CD_ROLE LIKE 'O%'
GROUP BY rpe2.RPE_NUM_CONTR))
AS PRENOM2,
-- DATE NAISSANCE CLIENT par NI
(SELECT TO_CHAR (MAX (PPH.PPH_DT_NAISSANCE), 'YYYY-MM-DD HH24:MI:SS')
FROM AODS00.TDO_D_ASSIN_PERS_PHYS PPH,
TDO_D_ASSIN_ROLE_PERSONNE RPE
WHERE RPE.RPE_NUM_CONTR = EVC.EVC_NUM_CONTR
AND RPE.RPE_CD_ROLE LIKE 'O%'
AND RPE.NUM_INTEGRATION = EVC.NUM_INTEGRATION
AND PPH.NUM_INTEGRATION = RPE.NUM_INTEGRATION
AND PPH.PPH_ID_PERSONNE = RPE.RPE_ID_PERSONNE
AND (RPE.RPE_ID_PERSONNE, rpe.RPE_NUM_CONTR) IN
( SELECT MAX (RPE2.RPE_ID_PERSONNE), rpe2.RPE_NUM_CONTR
FROM AODS00.TDO_D_ASSIN_ROLE_PERSONNE RPE2
WHERE RPE2.RPE_CD_ROLE LIKE 'O%'
GROUP BY rpe2.RPE_NUM_CONTR))
AS DT_NAISSANCE,
-- DATE NAISSANCE CLIENT sans NI
(SELECT TO_CHAR (MAX (PPH.PPH_DT_NAISSANCE), 'YYYY-MM-DD HH24:MI:SS')
FROM AODS00.TDO_D_ASSIN_PERS_PHYS PPH,
TDO_D_ASSIN_ROLE_PERSONNE RPE
WHERE RPE.RPE_NUM_CONTR = EVC.EVC_NUM_CONTR
AND RPE.RPE_CD_ROLE LIKE 'O%'
AND RPE.NUM_INTEGRATION <> EVC.NUM_INTEGRATION
AND PPH.NUM_INTEGRATION = RPE.NUM_INTEGRATION
AND PPH.PPH_ID_PERSONNE = RPE.RPE_ID_PERSONNE
AND (RPE.RPE_ID_PERSONNE, rpe.RPE_NUM_CONTR) IN
( SELECT MAX (RPE2.RPE_ID_PERSONNE), rpe2.RPE_NUM_CONTR
FROM AODS00.TDO_D_ASSIN_ROLE_PERSONNE RPE2
WHERE RPE2.RPE_CD_ROLE LIKE 'O%'
GROUP BY rpe2.RPE_NUM_CONTR))
AS DT2_NAISSANCE
FROM AODS00.TDO_D_ASSIN_EVT_CONTR EVC
WHERE EVC.NUM_INTEGRATION = 597033 AND EVC.FLAG_MAJ_ODS = '1'
ORDER BY 1;</pre>
</div>
<div>
On note que la clause FROM ne contient qu'une seule table (TDO_D_ASSIN_EVT_CONTR) et que la complexité réside dans le fait que le SELECT contient plusieurs scalar subqueries.<br />
La particularité des scalar subqueries dans une clause SELECT est qu'elles sont exécutées autant de fois qu'il y'a de lignes retournées par la requête principale.<br />
Voici une des scalar subqueries executées dans cette requête:</div>
<pre>(SELECT MAX (NVL (PPH.PPH_NOM_MARITAL, PPH.PPH_NOM_PATRONYMIQUE))
FROM AODS00.TDO_D_ASSIN_PERS_PHYS PPH,
TDO_D_ASSIN_ROLE_PERSONNE RPE
WHERE RPE.RPE_NUM_CONTR = EVC.EVC_NUM_CONTR
AND RPE.RPE_CD_ROLE LIKE 'O%'
AND RPE.NUM_INTEGRATION = EVC.NUM_INTEGRATION
AND PPH.NUM_INTEGRATION = RPE.NUM_INTEGRATION
AND PPH.PPH_ID_PERSONNE = RPE.RPE_ID_PERSONNE
AND (RPE.RPE_ID_PERSONNE, rpe.RPE_NUM_CONTR) IN
( SELECT MAX (RPE2.RPE_ID_PERSONNE), rpe2.RPE_NUM_CONTR
FROM AODS00.TDO_D_ASSIN_ROLE_PERSONNE RPE2
WHERE RPE2.RPE_CD_ROLE LIKE 'O%'
GROUP BY rpe2.RPE_NUM_CONTR))</pre>
<div>
La table TDO_D_ASSIN_ROLE_PERSONNE est celle qui apparait dans chacune des scalar subqueries.<br />
Elle apparait même 2 fois à chaque fois: une fois dans la clause FROM principal de la scalar subquery et une autre fois dans la sous-requête de la clause WHERE.<br />
<br /></div>
Essayons maintenant de voir ce que donne le plan d'exécution.<br />Vu que la requête était en train de tourner et afin d'avoir un plan avec des stats d'exécution j'ai récupéré le plan en utilisant le SQL Monitoring (DBMS_SQLTUNE.report_sql_monitor):<br /><br /><pre>
<span style="font-size: x-small;">Global Stats
=================================================================================
| Elapsed | Cpu | IO | Concurrency | Other | Buffer | Read | Read |
| Time(s) | Time(s) | Waits(s) | Waits(s) | Waits(s) | Gets | Reqs | Bytes |
=================================================================================
| 38031 | 36675 | 5.71 | 0.19 | 1350 | 42M | 2094 | 237MB |
=================================================================================
SQL Plan Monitoring Details (Plan Hash Value=132338241)
=======================================================================================================================================================================================================================
| Id | Operation | Name | Rows | Cost | Time | Start | Execs | Rows | Read | Read | Mem | Activity | Activity Detail | Progress |
| | | | (Estim) | | Active(s) | Active | | (Actual) | Reqs | Bytes | | (%) | (# samples) | |
=======================================================================================================================================================================================================================
| -> 0 | SELECT STATEMENT | | | | 38026 | +6 | 16483 | 16483 | | | | | | |
| -> 1 | SORT AGGREGATE | | 1 | | 38026 | +6 | 16483 | 16483 | | | | | | |
| 2 | NESTED LOOPS | | | | | | 16483 | | | | | | | |
| 3 | NESTED LOOPS | | 1 | 194 | | | 16483 | | | | | | | |
| 4 | NESTED LOOPS | | 1 | 193 | 37989 | +32 | 16483 | 0 | | | | | | |
| 5 | VIEW | VW_NSO_1 | 1 | 192 | 37989 | +32 | 16483 | 392 | | | | | | |
| 6 | SORT GROUP BY | | 1 | 192 | 37989 | +32 | 16483 | 392 | | | | | | |
| 7 | PARTITION RANGE ALL | | 1 | 192 | 37989 | +32 | 16483 | 508 | | | | 2.21 | Cpu (195) | |
| 8 | TABLE ACCESS BY LOCAL INDEX ROWID | TDO_D_ASSIN_ROLE_PERSONNE | 1 | 192 | 37989 | +32 | 4G | 508 | 389 | 6MB | | 11.89 | Cpu (1050) | |
| 9 | INDEX SKIP SCAN | PK_ASSIN_RPE_CONTR_PERS | 2 | 190 | 37989 | +32 | 3M | 508 | 49 | 784KB | | 0.32 | Cpu (28) | |
| 10 | PARTITION RANGE SINGLE | | 1 | 1 | | | 392 | | | | | | | |
| 11 | TABLE ACCESS BY LOCAL INDEX ROWID | TDO_D_ASSIN_ROLE_PERSONNE | 1 | 1 | | | 392 | | | | | | | |
| 12 | INDEX UNIQUE SCAN | PK_ASSIN_RPE_CONTR_PERS | 1 | | | | | | | | | | | |
| 13 | PARTITION RANGE SINGLE | | 1 | | | | | | | | | | | |
| 14 | INDEX UNIQUE SCAN | PK_ASSIN_PPH_ID_PERSONNE | 1 | | | | | | | | | | | |
| 15 | TABLE ACCESS BY LOCAL INDEX ROWID | TDO_D_ASSIN_PERS_PHYS | 1 | 1 | | | | | | | | | | |
| 16 | NESTED LOOPS | | | | | | 16483 | | | | | | | |
| 17 | NESTED LOOPS | | 1 | 191 | | | 16483 | | | | | | | |
| 18 | NESTED LOOPS | | 1 | 190 | 37989 | +32 | 16483 | 0 | | | | | | |
| 19 | VIEW | VW_NSO_2 | 1 | 190 | 37989 | +32 | 16483 | 392 | | | | | | |
| 20 | SORT GROUP BY | | 1 | 190 | 37989 | +32 | 16483 | 392 | | | | | | |
| 21 | PARTITION RANGE ALL | | 2 | 190 | 37989 | +32 | 16483 | 508 | | | | 2.63 | Cpu (232) | |
| 22 | INDEX SKIP SCAN | PK_ASSIN_RPE_CONTR_PERS | 2 | 190 | 37989 | +32 | 4G | 508 | 1 | 16384 | | 8.89 | Cpu (785) | |
| 23 | PARTITION RANGE SINGLE | | 1 | | | | 392 | | | | | | | |
| 24 | INDEX UNIQUE SCAN | PK_ASSIN_RPE_CONTR_PERS | 1 | | | | 392 | | | | | | | |
| 25 | PARTITION RANGE SINGLE | | 1 | | | | | | | | | | | |
| 26 | INDEX UNIQUE SCAN | PK_ASSIN_PMO_ID_PERSONNE | 1 | | | | | | | | | | | |
| 27 | TABLE ACCESS BY LOCAL INDEX ROWID | TDO_D_ASSIN_PERS_MORA | 1 | 1 | | | | | | | | | | |
| -> 28 | SORT AGGREGATE | | 1 | | 38026 | +6 | 16483 | 16483 | | | | | | |
| 29 | PARTITION RANGE SINGLE | | 1 | 2 | 1 | +35732 | 16483 | 0 | | | | 0.01 | Cpu (1) | |
| 30 | TABLE ACCESS BY LOCAL INDEX ROWID | TDO_D_ASSIN_ROLE_PERSONNE | 1 | 2 | | | 16483 | | | | | | | |
| 31 | INDEX RANGE SCAN | PK_ASSIN_RPE_CONTR_PERS | 1 | 1 | | | | | | | | | | |
| -> 32 | SORT AGGREGATE | | 1 | | 38026 | +6 | 16483 | 16483 | | | | | | |
| 33 | PARTITION RANGE ALL | | 1 | 192 | 37989 | +32 | 16483 | 508 | | | | 2.25 | Cpu (199) | |
| 34 | TABLE ACCESS BY LOCAL INDEX ROWID | TDO_D_ASSIN_ROLE_PERSONNE | 1 | 192 | 37989 | +32 | 4G | 508 | | | | 12.28 | Cpu (1085) | |
| 35 | INDEX SKIP SCAN | PK_ASSIN_RPE_CONTR_PERS | 2 | 190 | 37989 | +32 | 3M | 508 | | | | 0.25 | Cpu (22) | |
| -> 36 | SORT AGGREGATE | | 1 | | 38026 | +6 | 16483 | 16483 | | | | | | |
| 37 | NESTED LOOPS | | | | | | 16483 | | | | | | | |
| 38 | NESTED LOOPS | | 1 | 194 | | | 16483 | | | | | | | |
| 39 | NESTED LOOPS | | 1 | 193 | 37989 | +32 | 16483 | 0 | | | | | | |
| 40 | VIEW | VW_NSO_3 | 1 | 192 | 37989 | +32 | 16483 | 392 | | | | | | |
| 41 | SORT GROUP BY | | 1 | 192 | 37989 | +32 | 16483 | 392 | | | | | | |
| 42 | PARTITION RANGE ALL | | 1 | 192 | 37989 | +32 | 16483 | 508 | | | | 2.58 | Cpu (228) | |
| 43 | TABLE ACCESS BY LOCAL INDEX ROWID | TDO_D_ASSIN_ROLE_PERSONNE | 1 | 192 | 37989 | +32 | 4G | 508 | | | | 11.96 | Cpu (1056) | |
| 44 | INDEX SKIP SCAN | PK_ASSIN_RPE_CONTR_PERS | 2 | 190 | 37989 | +32 | 3M | 508 | | | | 0.15 | Cpu (13) | |
| 45 | PARTITION RANGE SINGLE | | 1 | 1 | | | 392 | | | | | | | |
| 46 | TABLE ACCESS BY LOCAL INDEX ROWID | TDO_D_ASSIN_ROLE_PERSONNE | 1 | 1 | | | 392 | | | | | | | |
| 47 | INDEX UNIQUE SCAN | PK_ASSIN_RPE_CONTR_PERS | 1 | | | | | | | | | | | |
| 48 | PARTITION RANGE SINGLE | | 1 | | | | | | | | | | | |
| 49 | INDEX UNIQUE SCAN | PK_ASSIN_PPH_ID_PERSONNE | 1 | | | | | | | | | | | |
| 50 | TABLE ACCESS BY LOCAL INDEX ROWID | TDO_D_ASSIN_PERS_PHYS | 1 | 1 | | | | | | | | | | |
| -> 51 | SORT AGGREGATE | | 1 | | 38026 | +6 | 16483 | 16483 | | | | | | |
| 52 | NESTED LOOPS | | | | 37989 | +32 | 16483 | 508 | | | | | | |
| 53 | NESTED LOOPS | | 1 | 197 | 37989 | +32 | 16483 | 508 | | | | | | |
| 54 | NESTED LOOPS | | 1 | 196 | 37989 | +32 | 16483 | 508 | | | | 0.01 | Cpu (1) | |
| 55 | VIEW | VW_NSO_4 | 1 | 192 | 37989 | +32 | 16483 | 392 | | | | | | |
| 56 | SORT GROUP BY | | 1 | 192 | 37989 | +32 | 16483 | 392 | | | | | | |
| 57 | PARTITION RANGE ALL | | 1 | 192 | 37989 | +32 | 16483 | 508 | | | | 2.24 | Cpu (198) | |
| 58 | TABLE ACCESS BY LOCAL INDEX ROWID | TDO_D_ASSIN_ROLE_PERSONNE | 1 | 192 | 37989 | +32 | 4G | 508 | | | | 12.36 | Cpu (1092) | |
| 59 | INDEX SKIP SCAN | PK_ASSIN_RPE_CONTR_PERS | 2 | 190 | 37989 | +32 | 3M | 508 | | | | 0.19 | Cpu (17) | |
| 60 | PARTITION RANGE ALL | | 1 | 207 | 37989 | +32 | 392 | 508 | | | | 0.10 | Cpu (9) | |
| 61 | TABLE ACCESS BY LOCAL INDEX ROWID | TDO_D_ASSIN_ROLE_PERSONNE | 1 | 207 | 37989 | +32 | 234M | 508 | | | | 0.28 | Cpu (25) | |
| 62 | INDEX RANGE SCAN | I1_TDO_D_ASSIN_ROLE_PERSONNE | 1 | 206 | 37989 | +32 | 79968 | 508 | 46 | 736KB | | | | |
| 63 | PARTITION RANGE ITERATOR | | 1 | | 37989 | +32 | 584 | 508 | | | | | | |
| 64 | INDEX UNIQUE SCAN | PK_ASSIN_PPH_ID_PERSONNE | 1 | | 37989 | +32 | 584 | 508 | 321 | 5MB | | | | |
| 65 | TABLE ACCESS BY LOCAL INDEX ROWID | TDO_D_ASSIN_PERS_PHYS | 1 | 1 | 37989 | +32 | 566 | 508 | 302 | 5MB | | | | |
| -> 66 | SORT AGGREGATE | | 1 | | 38026 | +6 | 16483 | 16483 | | | | | | |
| 67 | NESTED LOOPS | | | | | | 16483 | | | | | | | |
| 68 | NESTED LOOPS | | 1 | 194 | | | 16483 | | | | | | | |
| 69 | NESTED LOOPS | | 1 | 193 | 37989 | +32 | 16483 | 0 | | | | | | |
| 70 | VIEW | VW_NSO_5 | 1 | 192 | 37989 | +32 | 16483 | 392 | | | | | | |
| 71 | SORT GROUP BY | | 1 | 192 | 37989 | +32 | 16483 | 392 | | | | | | |
| 72 | PARTITION RANGE ALL | | 1 | 192 | 37989 | +32 | 16483 | 508 | | | | 2.38 | Cpu (210) | |
| 73 | TABLE ACCESS BY LOCAL INDEX ROWID | TDO_D_ASSIN_ROLE_PERSONNE | 1 | 192 | 37989 | +32 | 4G | 508 | | | | 11.90 | Cpu (1051) | |
| 74 | INDEX SKIP SCAN | PK_ASSIN_RPE_CONTR_PERS | 2 | 190 | 37989 | +32 | 3M | 508 | | | | 0.23 | Cpu (20) | |
| 75 | PARTITION RANGE SINGLE | | 1 | 1 | | | 392 | | | | | | | |
| 76 | TABLE ACCESS BY LOCAL INDEX ROWID | TDO_D_ASSIN_ROLE_PERSONNE | 1 | 1 | | | 392 | | | | | | | |
| 77 | INDEX UNIQUE SCAN | PK_ASSIN_RPE_CONTR_PERS | 1 | | | | | | | | | | | |
| 78 | PARTITION RANGE SINGLE | | 1 | | | | | | | | | | | |
| 79 | INDEX UNIQUE SCAN | PK_ASSIN_PPH_ID_PERSONNE | 1 | | | | | | | | | | | |
| 80 | TABLE ACCESS BY LOCAL INDEX ROWID | TDO_D_ASSIN_PERS_PHYS | 1 | 1 | | | | | | | | | | |
| -> 81 | SORT AGGREGATE | | 1 | | 38026 | +6 | 16483 | 16483 | | | | | | |
| 82 | NESTED LOOPS | | | | 37989 | +32 | 16483 | 508 | | | | | | |
| 83 | NESTED LOOPS | | 1 | 197 | 37989 | +32 | 16483 | 508 | | | | | | |
| 84 | NESTED LOOPS | | 1 | 196 | 37989 | +32 | 16483 | 508 | | | | | | |
| 85 | VIEW | VW_NSO_6 | 1 | 192 | 37989 | +32 | 16483 | 392 | | | | | | |
| 86 | SORT GROUP BY | | 1 | 192 | 37989 | +32 | 16483 | 392 | | | | | | |
| 87 | PARTITION RANGE ALL | | 1 | 192 | 37989 | +32 | 16483 | 508 | | | | 2.30 | Cpu (203) | |
| 88 | TABLE ACCESS BY LOCAL INDEX ROWID | TDO_D_ASSIN_ROLE_PERSONNE | 1 | 192 | 37989 | +32 | 4G | 508 | | | | 11.96 | Cpu (1056) | |
| 89 | INDEX SKIP SCAN | PK_ASSIN_RPE_CONTR_PERS | 2 | 190 | 37989 | +32 | 3M | 508 | | | | 0.25 | Cpu (22) | |
| 90 | PARTITION RANGE ALL | | 1 | 207 | 37989 | +32 | 392 | 508 | | | | 0.05 | Cpu (4) | |
| 91 | TABLE ACCESS BY LOCAL INDEX ROWID | TDO_D_ASSIN_ROLE_PERSONNE | 1 | 207 | 37989 | +32 | 234M | 508 | | | | 0.33 | Cpu (29) | |
| 92 | INDEX RANGE SCAN | I1_TDO_D_ASSIN_ROLE_PERSONNE | 1 | 206 | 37989 | +32 | 79968 | 508 | | | | | | |
| 93 | PARTITION RANGE ITERATOR | | 1 | | 37989 | +32 | 508 | 508 | | | | | | |
| 94 | INDEX UNIQUE SCAN | PK_ASSIN_PPH_ID_PERSONNE | 1 | | 37989 | +32 | 508 | 508 | | | | | | |
| 95 | TABLE ACCESS BY LOCAL INDEX ROWID | TDO_D_ASSIN_PERS_PHYS | 1 | 1 | 37989 | +32 | 508 | 508 | | | | | | |
| 96 | SORT ORDER BY | | 25124 | 13231 | | | 1 | | | | | | | |
| -> 97 | HASH UNIQUE | | 25124 | 13223 | 38026 | +6 | 1 | 0 | | | 3M | | | |
| -> 98 | PARTITION RANGE SINGLE | | 39388 | 13082 | 38026 | +6 | 1 | 16495 | | | | | | |
| -> 99 | TABLE ACCESS FULL | TDO_D_ASSIN_EVT_CONTR | 39388 | 13082 | 38026 | +6 | 1 | 16495 | 978 | 220MB | | 0.01 | db file scattered read (1) | 36% |
=======================================================================================================================================================================================================================
Predicate Information (identified by operation id):
---------------------------------------------------
8 - filter("RPE2"."RPE_CD_ROLE" LIKE 'O%')
9 - access("RPE2"."RPE_NUM_CONTR"=:B1)
filter("RPE2"."RPE_NUM_CONTR"=:B1)
11 - filter("RPE"."RPE_CD_ROLE" LIKE 'O%')
12 - access("RPE"."NUM_INTEGRATION"=:B1 AND "RPE"."RPE_NUM_CONTR"="RPE_NUM_CONTR" AND
"RPE"."RPE_ID_PERSONNE"="MAX(RPE2.RPE_ID_PERSONNE)")
filter("RPE"."RPE_NUM_CONTR"=:B1)
14 - access("PPH"."NUM_INTEGRATION"=:B1 AND "PPH"."PPH_ID_PERSONNE"="RPE"."RPE_ID_PERSONNE")
filter("PPH"."NUM_INTEGRATION"="RPE"."NUM_INTEGRATION")
22 - access("RPE2"."RPE_NUM_CONTR"=:B1)
filter("RPE2"."RPE_NUM_CONTR"=:B1)
24 - access("RPE"."NUM_INTEGRATION"=:B1 AND "RPE"."RPE_NUM_CONTR"="RPE_NUM_CONTR" AND
"RPE"."RPE_ID_PERSONNE"="MAX(RPE2.RPE_ID_PERSONNE)")
filter("RPE"."RPE_NUM_CONTR"=:B1)
26 - access("PMO"."NUM_INTEGRATION"=:B1 AND "PMO"."PMO_ID_PERSONNE"="RPE"."RPE_ID_PERSONNE")
filter("PMO"."NUM_INTEGRATION"="RPE"."NUM_INTEGRATION")
30 - filter("RPE"."RPE_CD_ROLE" LIKE 'O%')
31 - access("RPE"."NUM_INTEGRATION"=:B1 AND "RPE"."RPE_NUM_CONTR"=:B2)
34 - filter("RPE"."RPE_CD_ROLE" LIKE 'O%')
35 - access("RPE"."RPE_NUM_CONTR"=:B1)
filter(("RPE"."RPE_NUM_CONTR"=:B1 AND "RPE"."NUM_INTEGRATION"<>:B2))
43 - filter("RPE2"."RPE_CD_ROLE" LIKE 'O%')
44 - access("RPE2"."RPE_NUM_CONTR"=:B1)
filter("RPE2"."RPE_NUM_CONTR"=:B1)
46 - filter("RPE"."RPE_CD_ROLE" LIKE 'O%')
47 - access("RPE"."NUM_INTEGRATION"=:B1 AND "RPE"."RPE_NUM_CONTR"="RPE_NUM_CONTR" AND
"RPE"."RPE_ID_PERSONNE"="MAX(RPE2.RPE_ID_PERSONNE)")
filter("RPE"."RPE_NUM_CONTR"=:B1)
49 - access("PPH"."NUM_INTEGRATION"=:B1 AND "PPH"."PPH_ID_PERSONNE"="RPE"."RPE_ID_PERSONNE")
filter("PPH"."NUM_INTEGRATION"="RPE"."NUM_INTEGRATION")
58 - filter("RPE2"."RPE_CD_ROLE" LIKE 'O%')
59 - access("RPE2"."RPE_NUM_CONTR"=:B1)
filter("RPE2"."RPE_NUM_CONTR"=:B1)
61 - filter(("RPE"."RPE_CD_ROLE" LIKE 'O%' AND "RPE"."NUM_INTEGRATION"<>:B1 AND
"RPE"."RPE_ID_PERSONNE"="MAX(RPE2.RPE_ID_PERSONNE)"))
62 - access("RPE"."RPE_NUM_CONTR"="RPE_NUM_CONTR")
filter("RPE"."RPE_NUM_CONTR"=:B1)
64 - access("PPH"."NUM_INTEGRATION"="RPE"."NUM_INTEGRATION" AND
"PPH"."PPH_ID_PERSONNE"="RPE"."RPE_ID_PERSONNE")
filter("PPH"."NUM_INTEGRATION"<>:B1)
73 - filter("RPE2"."RPE_CD_ROLE" LIKE 'O%')
74 - access("RPE2"."RPE_NUM_CONTR"=:B1)
filter("RPE2"."RPE_NUM_CONTR"=:B1)
76 - filter("RPE"."RPE_CD_ROLE" LIKE 'O%')
77 - access("RPE"."NUM_INTEGRATION"=:B1 AND "RPE"."RPE_NUM_CONTR"="RPE_NUM_CONTR" AND
"RPE"."RPE_ID_PERSONNE"="MAX(RPE2.RPE_ID_PERSONNE)")
filter("RPE"."RPE_NUM_CONTR"=:B1)
79 - access("PPH"."NUM_INTEGRATION"=:B1 AND "PPH"."PPH_ID_PERSONNE"="RPE"."RPE_ID_PERSONNE")
filter("PPH"."NUM_INTEGRATION"="RPE"."NUM_INTEGRATION")
88 - filter("RPE2"."RPE_CD_ROLE" LIKE 'O%')
89 - access("RPE2"."RPE_NUM_CONTR"=:B1)
filter("RPE2"."RPE_NUM_CONTR"=:B1)
91 - filter(("RPE"."RPE_CD_ROLE" LIKE 'O%' AND "RPE"."NUM_INTEGRATION"<>:B1 AND
"RPE"."RPE_ID_PERSONNE"="MAX(RPE2.RPE_ID_PERSONNE)"))
92 - access("RPE"."RPE_NUM_CONTR"="RPE_NUM_CONTR")
filter("RPE"."RPE_NUM_CONTR"=:B1)
94 - access("PPH"."NUM_INTEGRATION"="RPE"."NUM_INTEGRATION" AND
"PPH"."PPH_ID_PERSONNE"="RPE"."RPE_ID_PERSONNE")
filter("PPH"."NUM_INTEGRATION"<>:B1)
99 - filter(("EVC"."FLAG_MAJ_ODS"='1' AND "EVC"."NUM_INTEGRATION"=597033))</span></pre>
</div>
<div>
<br /><div style="text-align: justify;">
On constate que la table principale doit retourner 39 388 lignes (colonne Rows Estim) mais qu'au moment où j'ai généré le plan 16 495 lignes avaient été traitées (colonne Rows Actual).</div>
<div style="text-align: justify;">
<br /></div>
<div style="text-align: justify;">
Pour chacune de ces 39 388 lignes retournées Oracle doit exécuter les scalar subqueries présentes dans le SELECT principal.</div>
<div style="text-align: justify;">
<br /></div>
<div style="text-align: justify;">
On voit dans le plan que pour chaque scalar subquery on a un accès à TDO_D_ASSIN_ROLE_PERSONNE via l'index PK_ASSIN_RPE_CONTR_PERS en mode PARTITION RANGE ALL et PARTITION RANGE SINGLE.</div>
<div style="text-align: justify;">
<br /></div>
<div style="text-align: justify;">
Le PARTITION RANGE SINGLE a lieu lorsque la clause de partitionnement (colonne NUM_INTEGRATION) est précisée dans la clause WHERE alors que le PARTITION RANGE ALL a lieu lorsque la clause n'est pas précisée.</div>
<div style="text-align: justify;">
<br /></div>
<div style="text-align: justify;">
Quand on regarde la scalar subquery que j'ai postée plus haut on note que dans la clause WHERE principale on a bien un filtre sur le champ NUM_INTEGRATION d'où l'accès direct à la partition ciblée alors que dans la sous requête le predicat sur le NUM_INTEGRATION n'existe pas.</div>
<div style="text-align: justify;">
<br /></div>
<div style="text-align: justify;">
Le PARTITION RANGE ALL indique que pour récupérer les lignes de la table TDO_D_ASSIN_ROLE_PERSONNE toutes les partitions de l'index sont parcourues (il y'en a 224). Au moment où j'ai généré mon plan, le moteur SQL avait traité 16495 lignes.</div>
<div style="text-align: justify;">
<br /></div>
<div style="text-align: justify;">
Si on multiplie 16495 par 224, on obtient 3 694 880. Ca a l'air de correspondre aux 3 millions qu'on voit dans la colonne Execs du plan pour chaque accès à l'index PK_ASSIN_RPE_CONTR_PERS. </div>
<div style="text-align: justify;">
Si on multiplie ces 3 millions d'accès par le nombre de scalar subqueries et par le fait que 3 requêtes similaires tournaient en même temps, ça donne une explication au problème de performances qu'a connu mon client ce matin.</div>
<div style="text-align: justify;">
<br /></div>
<div style="text-align: justify;">
Dans ce genre de requête, plus on a une table dans la requête principale retournant beaucoup de lignes ainsi qu'un nombre de partitions attaquées dans la scalar sbquery important et plus les performances sont dégradées du fait du nombre d'I/O générés.</div>
</div>
<div>
<div>
<div style="text-align: justify;">
<br /></div>
<div style="text-align: justify;">
Le but de cet article est de rappeler que lorsqu'on décide de partitionner une table et ses index (index locaux) il faut s'assurer que la clause de partitionnement soit bien prise en compte dans les requêtes de l'application car ainsi notre SGBD préféré sera en mesure de n'attaquer que la partition requise en éliminant à la source les partitions inutiles. C'est ce qu'on appelle le "partition pruning". Si vous partitionnez vos tables et index localement et que vos requêtes ne prunent pas vous risquez d'obtenir des problèmes de performances encore plus importants qu'avant d'avoir partitionné. Le problème rencontré par mon client en est un exemple flagrant. D'ailleurs ce problème m'a rappelé <a href="http://hourim.wordpress.com/2013/11/07/partitioned-index-global-or-local/" target="_blank">un article très intéressant de Mohamed Houri</a> que j'ai lu récemment et qui traite d'un problème du même type.</div>
</div>
</div>
</div>
Ahmed AANGOURhttp://www.blogger.com/profile/03378785645074450748noreply@blogger.com0tag:blogger.com,1999:blog-456686155150137588.post-50552192513203723112013-08-06T16:26:00.000+02:002013-08-06T16:26:18.720+02:00Supprimer un DB Link appartenant à un autre user<div dir="ltr" style="text-align: left;" trbidi="on">
Si vous êtes DBA de prod il est fort probable que pour supprimer des objets d'un schéma applicatif d'une base de données vous utilisiez un compte nominatif avec le role DBA, voir même le compte SYS. La plupart du temps les mots de passe des schémas applicatifs ne sont pas connus des DBAs.<br />
<br />
Presque tous les objets d'un schéma peuvent être supprimés avec un autre compte ayant le role DBA en préfixant l'objet par le nom du USER propriétaire. J'ai dit "presque" car ce n'est pas vrai pour les DB Links et les jobs. En effet, il est impossible même pour le user SYS de supprimer un DB Link privé ou un job appartenant à un autre USER.<br />
<br />
Voici un exemple concret d'un DB Link que j'ai eu à supprimer ce matin:<br />
<pre>
SQL>select OWNER,DB_LINK from dba_db_links where DB_LINK='DBL_TEST';
OWNER DB_LINK
------------------------------ ------------------------------
ASDRVMAT DBL_TEST</pre>
Le DB Link DBL_TEST appartient au schema ASDRVMAT.<br />
Si, en me connectant en sysdba, j'essaye de le supprimer en préfixant le nom du db link par le nom du schéma propriétaire j'obtiens l'erreur suivante:<br />
<pre>
DROP DATABASE LINK ASDRVMAT.DBL_TEST;
ERREUR à la ligne 1 :
ORA-02024: lien de base de données introuvable</pre>
Oracle ne trouve pas le db link car il interprète "ASDRVMAT.DBL_TEST" comme étant le nom du db link.<br />
<br />
En modifiant le schéma par défaut au niveau de la session j'obtiens un autre message d'erreur indiquant que je ne suis pas autorisé à supprimer le db link en question:<br />
<pre>
alter session set current_schema=ASDRVMAT;
DROP DATABASE LINK DBL_TEST;
ERREUR à la ligne 1 :
ORA-01031: privilèges insuffisants</pre>
<br />
Il n'est donc pas possible de supprimer un db link privé appartenant à un autre user sauf bien sûr en se connectant avec le user propriétaire directement.<br />
D'ailleurs <a href="http://docs.oracle.com/cd/B28359_01/server.111/b28286/statements_8010.htm" target="_blank">la doc Oracle</a> est claire à ce sujet:<br />
<blockquote class="tr_bq">
<span class="subhead3"><b>Restriction on Dropping Database Links</b> </span>You cannot drop a database link in another user's schema, and you cannot qualify <code><span class="codeinlineitalic">dblink</span></code> with the name of a schema, because periods are permitted in names of database links </blockquote>
<br />
La doc Oracle ne donne pas de solution de contournement mais comme souvent dans Oracle il existe un moyen officieux d'arriver à ses fins.<br />
Ici la solution va consister à utiliser le package non documenté <i>DBMS_SYS_SQL</i>.<br />
En effet, dans ce package on a la fonction <i>PARSE_AS_USER</i> qui permet d’exécuter une commande SQL en tant que n'importe quel autre user.<br />
Voici le script que j'utilise et que j'ai appelé <i>drop_db_link.sql</i> dans ma toolbox:<br />
<pre>
declare
sql_text varchar2(1000);
cur number;
user_id number;
res number;
begin
select
u.user_id into user_id
from dba_users u
where u.username = '&schema_name';
sql_text := 'drop database link '||'&db_link';
cur := SYS.DBMS_SYS_SQL.open_cursor;
SYS.DBMS_SYS_SQL.parse_as_user(
c => cur,
statement => sql_text,
language_flag => DBMS_SQL.native,
userID => user_id );
res := SYS.DBMS_SYS_SQL.execute(cur);
SYS.DBMS_SYS_SQL.close_cursor(cur);
end;
/</pre>
Ce script prend en paramètre le nom du schéma propriétaire et le nom du db link à dropper.<br />
Grace au nom du schéma propriétaire il récupère le USER_ID qui sera utilisé comme un des paramètres de la fonction <i>SYS.DBMS_SYS_SQL.parse_as_user</i>.<br />
On peut ainsi exécuter la commande DROP DATABASE LINK comme si on était connecté avec le compte propriétaire:<br />
<pre>
SQL>declare
sql_text varchar2(1000);
cur number;
user_id number;
res number;
--dblk varchar2(30);
begin
--dblk :=
select
u.user_id into user_id
from dba_users u
where u.username = '&schema_name';
sql_text := 'drop database link '||'&db_link';
cur := SYS.DBMS_SYS_SQL.open_cursor;
SYS.DBMS_SYS_SQL.parse_as_user(
c => cur,
statement => sql_text,
language_flag => DBMS_SQL.native,
userID => user_id );
res := SYS.DBMS_SYS_SQL.execute(cur);
SYS.DBMS_SYS_SQL.close_cursor(cur);
end;
/
Entrez une valeur pour schema_name : ASDRVMAT
ancien 12 : where u.username = '&schema_name';
nouveau 12 : where u.username = 'ASDRVMAT';
Entrez une valeur pour db_link : DBL_TEST
ancien 14 : sql_text := 'drop database link '||'&db_link';
nouveau 14 : sql_text := 'drop database link '||'DBL_TEST';
Procédure PL/SQL terminée avec succès.</pre>
<br />
Après avoir exécuté le script on peut vérifier que le db link a bien été supprimé:<br />
<pre>
SQL>select OWNER,DB_LINK from dba_db_links where DB_LINK='DBL_TEST';
aucune ligne sélectionnée</pre>
</div>
Ahmed AANGOURhttp://www.blogger.com/profile/03378785645074450748noreply@blogger.com4tag:blogger.com,1999:blog-456686155150137588.post-18418113414367105992013-04-28T11:58:00.000+02:002013-04-28T11:58:26.348+02:00Régler un problème de performance en 5 étapes<div dir="ltr" style="text-align: left;" trbidi="on">
<div dir="ltr" style="text-align: left;" trbidi="on">
<div dir="ltr" style="text-align: left;" trbidi="on">
Faire face à un problème de performance peut être une tâche extrêmement difficile car chaque problème peut avoir une nature et des causes diverses. Toutefois avec une bonne approche et une connaissance des outils à notre disposition on peut y faire face un peu plus aisément.<br />
<br />
La méthode que j'utilise est une méthode très connue chez les experts Oracle. Elle est composée des 5 étapes suivantes:<br />
1) Définition du problème<br />
2) Investigation<br />
3) Analyse<br />
4) Résolution<br />
5) Implementation<br />
<br />
Je vais tenter à travers cet article d'expliquer cette approche et de l'illustrer avec un exemple de problème de performance que j'ai eu à régler il y' a quelques mois.<br />
<br />
<b><u>Etape 1</u>: Définition du problème</b><br />
<br />
Cette étape consiste à poser à son interlocuteur (un utilisateur, un développeur, un client etc.) les bonnes questions afin de définir le problème en question. On est ici un peu dans la peau du médecin qui, pour être sûr d'avoir bien compris ce dont souffre le patient, lui pose un certain nombre de questions.<br />
On va se demander ici si le problème est en cours ou pas, si le problème de performance est général ou bien limité à un module applicatif. On va essayer de savoir si ce problème s'est déjà produit par le passé, si des changements ont eu lieu au niveau de l'application (nouvelle version applicative), au niveau de la base (ex:migration, modification de paramètres d'instance), au niveau de l'OS etc.<br />
Toutes ces questions vont permettre de bien diagnostiquer la situation en forçant notre interlocuteur à bien exprimer son problème.<br />
<br />
<u>Exemple</u>:<br />
J'ai reçu un mail il y'a quelques mois d'un chef de projet qui se plaignait que la base de prod était "lente". Pour bien identifier son problème j'avais besoin de comprendre ce qu'il entendait par "lent". Lorsqu'il m'a dit "Depuis midi nos process de calculs mettent plus de temps à s'exécuter que d'habitude" j'avais déjà là quelque chose de plus précis. Ensuite en lui posant d'autres questions sur notamment si des changements avaient été opérés sur la base ou autre, j'ai pu découvrir que la volumétrie de données traitées par le programme avait en effet augmenté. Ce genre d'informations hyper importantes ne sont souvent pas mentionnées par nos interlocuteurs lorsqu'on nous fait part d'un problème de performance. C'est à nous de leur tirer les vers du nez... <br />
<b><br /></b>
<b><u>Etape 2</u>: Investigation</b><br />
<br />
Une fois que le problème est bien défini vous devez vêtir l'imperméable du lieutenant Columbo et tenter de collecter le maximum d'information permettant de quantifier le problème. Au cours de cette étape vous allez choisir (en fonction des symptômes et des info récupérées à l'étape précédente) d'utiliser le ou les outils que vous avez à votre disposition.<br />
Si par un exemple le problème de performance a eu lieu dans le passé vous pouvez éditer un rapport AWR ou statspacks. Si le problème est en cours vous allez plutôt requêter les vues V$SESSION et V$SQL pour voir les sessions actives et les évènement sur lesquelles elles sont en attente. A cette étape il est possible de constater qu'en réalité le problème de performance n'existe pas ou en tout cas pas lié à la base de données.<br />
<br />
<u>Exemple</u>:<br />
Pour étudier le problème de performance évoqué à l'étape précédente j'avais opté pour une génération de rapport AWR entre midi et 15h car mon échange avec le chef de projet m'avait permis de cerner le problème sur cette plage horaire.<br />
Voici un extrait du rapport obtenu à l'époque:<br />
<pre> Snap Id Snap Time Sessions Curs/Sess
--------- ------------------- -------- ---------
Begin Snap: 16906 02-Jul-12 12:00:56 490 14.2
End Snap: 16909 02-Jul-12 15:00:15 539 17.5
Elapsed: 179.33 (mins)
DB Time: 4,896.89 (mins)
Top 5 Timed Events Avg %Total
~~~~~~~~~~~~~~~~~~ wait Call
Event Waits Time (s) (ms) Time Wait Class
------------------------------ ------------ ----------- ------ ------ ----------
CPU time 206,506 70.3
db file sequential read 3,514,166 28,081 8 9.6 User I/O
log file sync 1,260,469 13,534 11 4.6 Commit
latch: cache buffers chains 1,641,252 10,941 7 3.7 Concurrenc
log file parallel write 912,993 4,260 5 1.4 System I/O
Time Model Statistics DB/Inst: LNSX11/LNSX11 Snaps: 16906-16909
-> Total time in database user-calls (DB Time): 293813.4s
-> Statistics including the word "background" measure background process
time, and so do not contribute to the DB time statistic
-> Ordered by % or DB time desc, Statistic name
Statistic Name Time (s) % of DB Time
------------------------------
------------ ------------------ ------------
sql execute elapsed time 270,349.0 92.0
DB CPU 206,505.8 70.3
parse time elapsed 4,037.1 1.4
PL/SQL execution elapsed time 2,661.8 .9
hard parse elapsed time 2,155.4 .7
RMAN cpu time (backup/restore) 440.9 .2
PL/SQL compilation elapsed time 424.5 .1
connection management call elapsed time 244.6 .1
hard parse (sharing criteria) elapsed time 115.9 .0
repeated bind elapsed time 60.6 .0
inbound PL/SQL rpc elapsed time 59.5 .0
hard parse (bind mismatch) elapsed time 56.9 .0
sequence load elapsed time 14.9 .0
failed parse elapsed time 0.1 .0
DB time 293,813.4 N/A
background elapsed time 7,240.5 N/A
background cpu time 844.5 N/A
-------------------------------------------------------------
</pre>
<br />
On note que durant ces 3 heures on a eu 81 heures de DB time.<br />
<br />
La section "Top 5 Timed Events" indique que 70% du DB Time concerne du CPU Time. La partie "Time Model Statistics", elle, montre que 92% de ce DB Time est lié à l'exécution de code SQL.<br />
Pour avoir plus d'informations sur les requêtes SQL qui sont responsables de cette consommation accrue de DB Time il faut aller regarder du côté de la section "SQL ordered by Elapsed Time":<br />
<pre>
SQL ordered by Elapsed Time DB/Inst: LNSX11/LNSX11 Snaps: 16906-16909
-> Resources reported for PL/SQL code includes the resources used by all SQL
statements called by the code.
-> % Total DB Time is the Elapsed Time of the SQL statement divided
into the Total Database Time multiplied by 100
-> Total DB Time (s): 293,813
-> Captured SQL account for 66.1% of Total
Elapsed CPU Elap per % Total
Time (s) Time (s) Executions Exec (s) DB Time SQL Id
---------- ---------- ------------ ---------- ------- -------------
86,698 69,051 674 128.6 29.5 5ydxnct2swxau
Module: LNS_X01_cal.exe
Select HIR_MTM,ROO_ID,sja_fieldid FIELDID, (Select STV_VALUE from MUT_STATUS_
VALOSIMPLE, MUT_STATUS_PONDERATION, MUTATION Where STV_DATE=(Select Max(STV_DATE
) From MUT_STATUS_VALOSIMPLE, MUT_STATUS_PONDERATION, MUTATION Where STV_OBJ_ID
=SJA_ID And STV_DATE <=TO_DATE(:"SYS_B_00", :"SYS_B_01") And STV_STATUS_ID=PON_S
45,771 36,420 458 99.9 15.6 g0kfhsfnh3kvv
Module: LNS_X01_cal.exe
Select HIR_MTM,ROO_ID,sja_fieldid FIELDID, (Select STV_VALUE from MUT_STATUS_
VALOSIMPLE, MUT_STATUS_PONDERATION, MUTATION Where STV_DATE=(Select Max(STV_DATE
) From MUT_STATUS_VALOSIMPLE, MUT_STATUS_PONDERATION, MUTATION Where STV_OBJ_ID
=SJA_ID And STV_DATE <=TO_DATE(:"SYS_B_00", :"SYS_B_01") And STV_STATUS_ID=PON_S </pre>
</div>
Les 2 requêtes ci dessus sont les requêtes à tuner car elles sont responsables à elles deux de 45% du DB Time (29.5+15.6).<br />
<br />
<br />
<b><u>Etape 3</u>: Analyse</b><br />
<br />
Après avoir collecté les informations permettant d'identifier le problème il faut entrer dans une étape d'analyse afin de trouver les causes principales de la lenteur de nos requêtes. Là aussi, une bonne connaissance des outils à notre disposition est primordial (traces 10046 et 10053, plans d'exécution, vues V$ etc.).<br />
Cette étape est à la fois la plus difficile et la plus passionnante. <br />
<br />
Concernant notre exemple on sait grâce à l'étape précédente que le problème de performance est lié essentiellement à 2 requêtes. Il faut donc analyser l'exécution et le plan de ces 2 requêtes pour identifier la cause de la lenteur.<br />
<br />
<u>Analyse de la première requête</u> (sql_id=5ydxnct2swxau)<br />
<br />
Le rapport AWR nous indique que cette requête s'exécute en moyenne en 128.6 secondes. Avec 674 exécutions, le temps total d'exécution s'élève à 86 698 secondes (soit 24 heures).<br />
Jetons un œil aux statistiques d'exécutions actuelles pour cette requête (stats récupérées dans V$SQL):<br /><pre>SQL> @sql_find_stats
Enter value for sql_text:
Enter value for sql_id: 5ydxnct2swxau
SQL_ID CHILD PLAN_HASH_VALUE EXECS ROWS_PROCESSED AVG_ETIME AVG_PIO AVG_LIO
------------- ------ --------------- ---------- -------------- ---------- ---------- ----------
5ydxnct2swxau 0 3968018239 867 2197243 123.92 .49 1,632,031</pre>
<br />Nous avons un temps d'exécution moyen de 123.92 secondes ce qui correspond pratiquement au temps d'exécution observé dans le rapport AWR.<br /><br />Regardons de plus près l'historique d'exécution pour cette requête (grâce à la vue DBA_HIST_SQLSTAT):<br />
<pre>
SQL> @awr_plan_change
Enter value for sql_id: 5ydxnct2swxau
Enter value for instance_number:
SNAP_ID NODE BEGIN_INTERVAL_TIME SQL_ID PLAN_HASH_VALUE EXECS AVG_ETIME AVG_LIO AVG_PIO AVG_ROWS
---------- ------ ------------------------------ ------------- --------------- ------------ ------------ -------------- ---------- ------------
16096 1 29-MAY-12 07.00.12.099 AM 5ydxnct2swxau 3968018239 92 36.012 2,100,168.0 54.43 848
16120 1 30-MAY-12 07.00.08.939 AM 5ydxnct2swxau 25 28.695 1,494,734.7 106.84 601
16144 1 31-MAY-12 07.00.09.862 AM 5ydxnct2swxau 67 33.522 1,986,261.7 44.06 800
16262 1 05-JUN-12 07.00.03.542 AM 5ydxnct2swxau 23 15.572 801,967.7 52.65 322
16311 1 07-JUN-12 08.00.55.576 AM 5ydxnct2swxau 151 44.639 2,538,922.9 36.52 1,020
16428 1 12-JUN-12 07.00.15.397 AM 5ydxnct2swxau 55 31.000 1,682,545.4 102.27 671
16476 1 14-JUN-12 07.00.58.869 AM 5ydxnct2swxau 62 33.211 1,841,182.8 79.66 735
16618 1 20-JUN-12 07.00.09.276 AM 5ydxnct2swxau 36 27.819 1,522,616.9 129.00 607
16737 1 25-JUN-12 08.00.29.501 AM 5ydxnct2swxau 12 27.617 1,628,256.7 .00 648
16844 1 29-JUN-12 07.00.54.189 PM 5ydxnct2swxau 492 84.000 4,824,879.5 4.89 1,837
16845 1 29-JUN-12 08.00.57.512 PM 5ydxnct2swxau 710 87.038 4,153,332.8 .14 1,601
16846 1 29-JUN-12 09.00.00.273 PM 5ydxnct2swxau 764 78.419 4,199,976.2 .00 1,593
16847 1 29-JUN-12 10.00.06.863 PM 5ydxnct2swxau 758 76.995 4,426,137.8 .05 1,691
16848 1 29-JUN-12 11.00.10.768 PM 5ydxnct2swxau 774 75.453 4,432,480.4 .01 1,689
16849 1 30-JUN-12 12.00.13.539 AM 5ydxnct2swxau 618 96.493 5,709,939.6 .00 2,197
16850 1 30-JUN-12 01.00.16.443 AM 5ydxnct2swxau 510 110.112 6,552,217.1 .00 2,515
16851 1 30-JUN-12 02.00.19.187 AM 5ydxnct2swxau 496 111.173 6,393,527.4 .00 2,447
16901 1 02-JUL-12 06.00.20.638 AM 5ydxnct2swxau 564 65.402 3,961,601.9 15.80 1,499
16904 1 02-JUL-12 09.00.42.918 AM 5ydxnct2swxau 680 74.424 4,268,174.0 .10 1,623
16905 1 02-JUL-12 10.00.49.137 AM 5ydxnct2swxau 644 85.137 4,495,420.0 .05 1,718
16906 1 02-JUL-12 11.00.52.661 AM 5ydxnct2swxau 87 127.208 6,788,947.7 .00 2,634
16907 1 02-JUL-12 12.00.56.370 PM 5ydxnct2swxau 3 62.627 3,607,740.0 .00 1,335
16908 1 02-JUL-12 01.00.01.946 PM 5ydxnct2swxau 347 121.837 6,638,634.2 .12 2,552
16909 1 02-JUL-12 02.00.09.803 PM 5ydxnct2swxau 324 136.520 6,896,529.5 .24 2,660
16910 1 02-JUL-12 03.00.15.936 PM 5ydxnct2swxau 230 107.049 5,809,572.0 1.39 2,228</pre>
</div>
On peut noter qu'il n'y a pas de problème d'instabilité de plan pour cett requête puisque le même plan est utilisé à chaque fois. <br />
Il faut donc analyser de plus près ce plan d'exécution.<br />
Tout d'abord vérifions qu'on obtient bien le même plan si on exécute cette requête de manière isolée.<br />
Comme cette requête utilise des binds variables nous devons tenter de récupérer les valeurs enregistrées lors de l'opération de bind peeking en requêtant la vue V$SQL_BIND_CAPTURE:<br />
<pre>
col VALUE_STRING for A20
select name,position,datatype_string,value_string
from V$SQL_BIND_CAPTURE where sql_id='5ydxnct2swxau';
NAME POSITION DATATYPE_STRING VALUE_STRING
------------------------------ ---------- --------------- --------------------
:SYS_B_00 1 VARCHAR2(32) 29-06-2012
:SYS_B_01 2 VARCHAR2(32) dd-mm-yyyy
:SYS_B_02 3 NUMBER 2
:SYS_B_03 4 NUMBER 1
:SYS_B_04 5 NUMBER 2
:SYS_B_05 6 NUMBER 1
:SYS_B_06 7 NUMBER 164826982
:SYS_B_07 8 VARCHAR2(32) 29-06-2012
:SYS_B_08 9 VARCHAR2(32) dd-mm-yyyy
:SYS_B_09 10 VARCHAR2(32) 29-06-2012
:SYS_B_10 11 VARCHAR2(32) dd-mm-yyyy
:SYS_B_11 12 VARCHAR2(32) 29-06-2012
:SYS_B_12 13 VARCHAR2(32) dd-mm-yyyy
:SYS_B_13 14 NUMBER 50000
:SYS_B_14 15 NUMBER 40001
:SYS_B_15 16 VARCHAR2(32) 29-06-2012
:SYS_B_16 17 VARCHAR2(32) dd-mm-yyyy
:SYS_B_17 18 NUMBER 14</pre>
Maintenant qu'on a les dernières valeurs peekées on peut remplacer dans la requête les binds variables par les valeurs obtenues:<br />
<pre>
Select HIR_MTM,
ROO_ID,
sja_fieldid FIELDID,
(Select STV_VALUE
from MUT_STATUS_VALOSIMPLE, MUT_STATUS_PONDERATION, MUTATION
Where STV_DATE =
(Select Max(STV_DATE)
From MUT_STATUS_VALOSIMPLE,
MUT_STATUS_PONDERATION,
MUTATION
Where STV_OBJ_ID = SJA_ID
And STV_DATE <= TO_DATE('29-06-2012', 'dd-mm-yyyy')
And STV_STATUS_ID = PON_STATUS_ID
And MUT_ID = STV_STATUS_ID
And MUT_TYPESTATUS_ID = 2
And MUT_LASTFLAG = 1)
And STV_OBJ_ID = SJA_ID
And STV_STATUS_ID = PON_STATUS_ID
And MUT_ID = STV_STATUS_ID
And MUT_TYPESTATUS_ID = 2
And MUT_LASTFLAG = 1) POND,
SPO_SPOT
From MUT_HISTO_ROOT histo, MUT_SOUS_JACENT, MUT_ROOT2, SPOT
Where SJA_PARENT_ID = 164826982
And SJA_STARTDATE <= TO_DATE('29-06-2012', 'dd-mm-yyyy')
And (SJA_STOPDATE >= TO_DATE('29-06-2012', 'dd-mm-yyyy') or
SJA_STOPDATE is NULL)
And TRUNC(histo.LYX_DAY(+)) = TO_DATE('29-06-2012', 'dd-mm-yyyy')
And SJA_ROOTID_SJ = histo.LYX_OWNER_ID(+)
And HIR_TYPE_ID(+) = 50000
And HIR_TIMETYPE_ID(+) = 40001
And ROO_ID = SJA_ROOTID_SJ
AND SPOT.LYX_DAY(+) = TO_DATE('29-06-2012', 'dd-mm-yyyy')
AND SPO_DEVISE_A(+) = 14
AND SPO_DEVISE_B(+) = ROO_DEVISE_ID;
1680 rows selected.
Elapsed: 00:01:12.56
SQL> select * from table(dbms_xplan.display_cursor(null,null,'iostats last'));
SQL_ID du7t6514za5rs, child number 1
-------------------------------------
Plan hash value: 3968018239
--------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
--------------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1680 |00:00:00.20 | 10993 |
| 1 | NESTED LOOPS | | 1680 | 1 | 1680 |00:01:09.95 | 4752K|
| 2 | NESTED LOOPS | | 1680 | 1 | 1782 |00:01:09.91 | 4747K|
| 3 | TABLE ACCESS BY INDEX ROWID | MUT_STATUS_VALOSIMPLE | 1680 | 1 | 1782 |00:01:09.88 | 4743K|
|* 4 | INDEX RANGE SCAN | IDX_OBJ_DATE | 1680 | 1 | 1782 |00:01:09.85 | 4741K|
| 5 | SORT AGGREGATE | | 1680 | 1 | 1680 |00:01:09.80 | 4738K|
| 6 | NESTED LOOPS | | 1680 | 529 | 4351 |00:01:09.79 | 4738K|
| 7 | NESTED LOOPS | | 1680 | 528 | 4623 |00:01:09.67 | 4724K|
| 8 | TABLE ACCESS BY INDEX ROWID| MUT_STATUS_VALOSIMPLE | 1680 | 528 | 4623 |00:01:09.59 | 4715K|
|* 9 | INDEX SKIP SCAN | IDX_OBJ_DATE | 1680 | 528 | 4623 |00:01:09.54 | 4710K|
|* 10 | INDEX UNIQUE SCAN | PK_POND | 4623 | 1 | 4623 |00:00:00.06 | 9359 |
|* 11 | TABLE ACCESS BY INDEX ROWID | MUTATION | 4623 | 1 | 4351 |00:00:00.10 | 13982 |
|* 12 | INDEX UNIQUE SCAN | PK_MUTATION | 4623 | 1 | 4623 |00:00:00.06 | 9359 |
|* 13 | INDEX UNIQUE SCAN | PK_POND | 1782 | 1 | 1782 |00:00:00.02 | 3677 |
|* 14 | TABLE ACCESS BY INDEX ROWID | MUTATION | 1782 | 1 | 1680 |00:00:00.04 | 5459 |
|* 15 | INDEX UNIQUE SCAN | PK_MUTATION | 1782 | 1 | 1782 |00:00:00.02 | 3677 |
| 16 | NESTED LOOPS OUTER | | 1 | 127 | 1680 |00:00:00.20 | 10993 |
|* 17 | HASH JOIN OUTER | | 1 | 97 | 1680 |00:00:00.02 | 3406 |
| 18 | NESTED LOOPS | | 1 | 97 | 1680 |00:00:00.01 | 3400 |
|* 19 | TABLE ACCESS BY INDEX ROWID | MUT_SOUS_JACENT | 1 | 97 | 1680 |00:00:00.01 | 38 |
|* 20 | INDEX RANGE SCAN | IDX_SOUS_JACENT_PARENT_ID | 1 | 101 | 1791 |00:00:00.01 | 6 |
| 21 | TABLE ACCESS BY INDEX ROWID | MUT_ROOT2 | 1680 | 1 | 1680 |00:00:00.01 | 3362 |
|* 22 | INDEX UNIQUE SCAN | PK_ROOT2 | 1680 | 1 | 1680 |00:00:00.01 | 1682 |
| 23 | TABLE ACCESS BY INDEX ROWID | SPOT | 1 | 8 | 21 |00:00:00.01 | 6 |
|* 24 | INDEX RANGE SCAN | PK_SPOT | 1 | 8 | 21 |00:00:00.01 | 4 |
| 25 | TABLE ACCESS BY INDEX ROWID | MUT_HISTO_ROOT | 1680 | 1 | 1563 |00:00:00.18 | 7587 |
|* 26 | INDEX RANGE SCAN | IDX_MHR_LOHTHTLD | 1680 | 1 | 1563 |00:00:00.16 | 6024 |
--------------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
4 - access("STV_DATE"= AND "STV_OBJ_ID"=:B1)
9 - access("STV_OBJ_ID"=:B1 AND "STV_DATE"<=TO_DATE(:SYS_B_00,:SYS_B_01))
filter("STV_OBJ_ID"=:B1)
10 - access("STV_STATUS_ID"="PON_STATUS_ID")
11 - filter(("MUT_TYPESTATUS_ID"=:SYS_B_02 AND "MUT_LASTFLAG"=:SYS_B_03))
12 - access("MUT_ID"="STV_STATUS_ID")
13 - access("STV_STATUS_ID"="PON_STATUS_ID")
14 - filter(("MUT_TYPESTATUS_ID"=:SYS_B_04 AND "MUT_LASTFLAG"=:SYS_B_05))
15 - access("MUT_ID"="STV_STATUS_ID")
17 - access("SPO_DEVISE_B"="ROO_DEVISE_ID")
19 - filter(("SJA_STARTDATE"<=TO_DATE(:SYS_B_07,:SYS_B_08) AND ("SJA_STOPDATE">=TO_DATE(:SYS_B_09,:SYS_B_10) OR
"SJA_STOPDATE" IS NULL)))
20 - access("SJA_PARENT_ID"=:SYS_B_06)
22 - access("ROO_ID"="SJA_ROOTID_SJ")
24 - access("SPOT"."LYX_DAY"=TO_DATE(:SYS_B_15,:SYS_B_16) AND "SPO_DEVISE_A"=:SYS_B_17)
26 - access("SJA_ROOTID_SJ"="HISTO"."LYX_OWNER_ID" AND "HIR_TIMETYPE_ID"=:SYS_B_14 AND "HIR_TYPE_ID"=:SYS_B_13)
filter(TRUNC(INTERNAL_FUNCTION("HISTO"."LYX_DAY"))=TO_DATE(:SYS_B_11,:SYS_B_12))</pre>
La requête a mis plus d'une minute pour s'exécuter et a généré 4752K logical reads. Le problème se situe au niveau au niveau de l'INDEX SKIP SKAN de l'index IDX_OBJ_DATE (operation 9). Cette opération génère à elle seule 4710K logical reads. Cet index permet d'accéder à la table MUT_STATUS_VALOSIMPLE qui est appelée au niveau de la scalar subquery de la requête.<br />
Si on exécute la requête sans la scalar subquery le problème de performance ne se produit plus:<br />
<pre>
Select HIR_MTM,
ROO_ID,
sja_fieldid FIELDID,
SPO_SPOT
From MUT_HISTO_ROOT histo, MUT_SOUS_JACENT, MUT_ROOT2, SPOT
Where SJA_PARENT_ID = 164826982
And SJA_STARTDATE <= TO_DATE('29-06-2012', 'dd-mm-yyyy')
And (SJA_STOPDATE >= TO_DATE('29-06-2012', 'dd-mm-yyyy') or
SJA_STOPDATE is NULL)
And TRUNC(histo.LYX_DAY(+)) = TO_DATE('29-06-2012', 'dd-mm-yyyy')
And SJA_ROOTID_SJ = histo.LYX_OWNER_ID(+)
And HIR_TYPE_ID(+) = 50000
And HIR_TIMETYPE_ID(+) = 40001
And ROO_ID = SJA_ROOTID_SJ
AND SPOT.LYX_DAY(+) = TO_DATE('29-06-2012', 'dd-mm-yyyy')
AND SPO_DEVISE_A(+) = 14
AND SPO_DEVISE_B(+) = ROO_DEVISE_ID;
1680 rows selected.
Elapsed: 00:00:01.01
SQL> select * from table(dbms_xplan.display_cursor(null,null,'iostats last'));
SQL_ID 7y1fqw64vu4xv, child number 0
-------------------------------------
Plan hash value: 593715089
----------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
----------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1680 |00:00:00.33 | 10993 |
| 1 | NESTED LOOPS OUTER | | 1 | 127 | 1680 |00:00:00.33 | 10993 |
|* 2 | HASH JOIN OUTER | | 1 | 97 | 1680 |00:00:00.01 | 3406 |
| 3 | NESTED LOOPS | | 1 | 97 | 1680 |00:00:00.01 | 3400 |
|* 4 | TABLE ACCESS BY INDEX ROWID| MUT_SOUS_JACENT | 1 | 97 | 1680 |00:00:00.01 | 38 |
|* 5 | INDEX RANGE SCAN | IDX_SOUS_JACENT_PARENT_ID | 1 | 101 | 1791 |00:00:00.01 | 6 |
| 6 | TABLE ACCESS BY INDEX ROWID| MUT_ROOT2 | 1680 | 1 | 1680 |00:00:00.01 | 3362 |
|* 7 | INDEX UNIQUE SCAN | PK_ROOT2 | 1680 | 1 | 1680 |00:00:00.01 | 1682 |
| 8 | TABLE ACCESS BY INDEX ROWID | SPOT | 1 | 8 | 21 |00:00:00.01 | 6 |
|* 9 | INDEX RANGE SCAN | PK_SPOT | 1 | 8 | 21 |00:00:00.01 | 4 |
| 10 | TABLE ACCESS BY INDEX ROWID | MUT_HISTO_ROOT | 1680 | 1 | 1563 |00:00:00.31 | 7587 |
|* 11 | INDEX RANGE SCAN | IDX_MHR_LOHTHTLD | 1680 | 1 | 1563 |00:00:00.29 | 6024 |
----------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("SPO_DEVISE_B"="ROO_DEVISE_ID")
4 - filter(("SJA_STARTDATE"<=TO_DATE(:SYS_B_01,:SYS_B_02) AND
("SJA_STOPDATE">=TO_DATE(:SYS_B_03,:SYS_B_04) OR "SJA_STOPDATE" IS NULL)))
5 - access("SJA_PARENT_ID"=:SYS_B_00)
7 - access("ROO_ID"="SJA_ROOTID_SJ")
9 - access("SPOT"."LYX_DAY"=TO_DATE(:SYS_B_09,:SYS_B_10) AND "SPO_DEVISE_A"=:SYS_B_11)
11 - access("SJA_ROOTID_SJ"="HISTO"."LYX_OWNER_ID" AND "HIR_TIMETYPE_ID"=:SYS_B_08 AND
"HIR_TYPE_ID"=:SYS_B_07)
filter(TRUNC(INTERNAL_FUNCTION("HISTO"."LYX_DAY"))=TO_DATE(:SYS_B_05,:SYS_B_06))</pre>
La requête a généré seulement 10K logical reads au lieu de 4752K lorsqu'on avait la scalar subquery.<br />
Maintenant qu'on a isolé le problème on peut regarder de plus près comment améliorer l'accès à la table MUT_STATUS_VALOSIMPLE sans passer par un INDEX SKIP SCAN.<br />
Collectons d'abord quelques informations sur les index existants pour cette table:<br />
<pre>
Index Name Pos# Order Column Name
------------------------------ ---------- ----- ------------------------------
idx_obj_date 1 ASC stv_date
2 ASC stv_obj_id
pk_valosimple 1 ASC stv_status_id</pre>
<br />
L'index utilisé via l'opération SKIP SCAN est l'index IDX_OBJ_DATE. Cet index est en fait un index composite sur les colonnes STV_DATE et STV_OBJ_ID.<br />
La requête utilise un prédicat sur STV_OBJ_ID qui correspond à la deuxième colonne de l'index. D'où un accès en INDEX SKIP SCAN beaucoup moins efficace qu'un INDEX RANGE SCAN.<br />
<br />
<b><u>Etape 4</u>: Trouver une solution</b><br />
<b><br /></b>
Une fois le problème identifié et la cause du problème isolé, cette étape consiste à trouver une solution à la cause du problème qui puisse être implémentable.<br />
Dans notre exemple cette étape va consister à trouver un moyen d'optimiser l'accès à la table MUT_STATUS_VALOSIMPLE.<br />
Crééons un index sur la colonne STV_OBJ_ID uniquement et voyons ce que donne la requête lorsqu'on l'exécute:<br />
<pre>
create index IDX_STV_OBJ_ID on MUT_STATUS_VALOSIMPLE(STV_OBJ_ID);
Plan hash value: 1150780941
-----------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads |
-----------------------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1680 |00:00:00.38 | 9320 | 0 |
| 1 | NESTED LOOPS | | 1680 | 1 | 1680 |00:00:00.65 | 45714 | 37 |
| 2 | NESTED LOOPS | | 1680 | 1 | 1782 |00:00:00.61 | 40255 | 37 |
| 3 | TABLE ACCESS BY INDEX ROWID | MUT_STATUS_VALOSIMPLE | 1680 | 1 | 1782 |00:00:00.59 | 36578 | 37 |
|* 4 | INDEX RANGE SCAN | IDX_OBJ_DATE | 1680 | 1 | 1782 |00:00:00.56 | 34832 | 37 |
| 5 | SORT AGGREGATE | | 1680 | 1 | 1680 |00:00:00.52 | 31355 | 37 |
| 6 | NESTED LOOPS | | 1680 | 519 | 4351 |00:00:00.51 | 31355 | 37 |
| 7 | NESTED LOOPS | | 1680 | 518 | 4623 |00:00:00.39 | 17373 | 36 |
|* 8 | TABLE ACCESS BY INDEX ROWID| MUT_STATUS_VALOSIMPLE | 1680 | 518 | 4623 |00:00:00.33 | 8014 | 36 |
|* 9 | INDEX RANGE SCAN | IDX_STV_OBJ_ID | 1680 | 518 | 4623 |00:00:00.28 | 3485 | 36 |
|* 10 | INDEX UNIQUE SCAN | PK_POND | 4623 | 1 | 4623 |00:00:00.05 | 9359 | 0 |
|* 11 | TABLE ACCESS BY INDEX ROWID | MUTATION | 4623 | 1 | 4351 |00:00:00.10 | 13982 | 1 |
|* 12 | INDEX UNIQUE SCAN | PK_MUTATION | 4623 | 1 | 4623 |00:00:00.07 | 9359 | 1 |
|* 13 | INDEX UNIQUE SCAN | PK_POND | 1782 | 1 | 1782 |00:00:00.02 | 3677 | 0 |
|* 14 | TABLE ACCESS BY INDEX ROWID | MUTATION | 1782 | 1 | 1680 |00:00:00.03 | 5459 | 0 |
|* 15 | INDEX UNIQUE SCAN | PK_MUTATION | 1782 | 1 | 1782 |00:00:00.02 | 3677 | 0 |
| 16 | NESTED LOOPS OUTER | | 1 | 86 | 1680 |00:00:00.38 | 9320 | 0 |
|* 17 | HASH JOIN OUTER | | 1 | 81 | 1680 |00:00:00.04 | 3406 | 0 |
| 18 | NESTED LOOPS | | 1 | 81 | 1680 |00:00:00.04 | 3400 | 0 |
|* 19 | TABLE ACCESS BY INDEX ROWID | MUT_SOUS_JACENT | 1 | 81 | 1680 |00:00:00.01 | 38 | 0 |
|* 20 | INDEX RANGE SCAN | IDX_SOUS_JACENT_PARENT_ID | 1 | 85 | 1791 |00:00:00.01 | 6 | 0 |
| 21 | TABLE ACCESS BY INDEX ROWID | MUT_ROOT2 | 1680 | 1 | 1680 |00:00:00.02 | 3362 | 0 |
|* 22 | INDEX UNIQUE SCAN | PK_ROOT2 | 1680 | 1 | 1680 |00:00:00.01 | 1682 | 0 |
| 23 | TABLE ACCESS BY INDEX ROWID | SPOT | 1 | 7 | 21 |00:00:00.01 | 6 | 0 |
|* 24 | INDEX RANGE SCAN | PK_SPOT | 1 | 7 | 21 |00:00:00.01 | 4 | 0 |
| 25 | TABLE ACCESS BY INDEX ROWID | MUT_HISTO_ROOT | 1680 | 1 | 0 |00:00:00.33 | 5914 | 0 |
|* 26 | INDEX RANGE SCAN | IDX_MHR_LOHTHTLD | 1680 | 1 | 0 |00:00:00.33 | 5914 | 0 |
-----------------------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
4 - access("STV_DATE"= AND "STV_OBJ_ID"=:B1)
8 - filter("STV_DATE"<=TO_DATE(:SYS_B_00,:SYS_B_01))
9 - access("STV_OBJ_ID"=:B1)
10 - access("STV_STATUS_ID"="PON_STATUS_ID")
11 - filter(("MUT_TYPESTATUS_ID"=:SYS_B_02 AND "MUT_LASTFLAG"=:SYS_B_03))
12 - access("MUT_ID"="STV_STATUS_ID")
13 - access("STV_STATUS_ID"="PON_STATUS_ID")
14 - filter(("MUT_TYPESTATUS_ID"=:SYS_B_04 AND "MUT_LASTFLAG"=:SYS_B_05))
15 - access("MUT_ID"="STV_STATUS_ID")
17 - access("SPO_DEVISE_B"="ROO_DEVISE_ID")
19 - filter(("SJA_STARTDATE"<=TO_DATE(:SYS_B_07,:SYS_B_08) AND ("SJA_STOPDATE">=TO_DATE(:SYS_B_09,:SYS_B_10) OR
"SJA_STOPDATE" IS NULL)))
20 - access("SJA_PARENT_ID"=:SYS_B_06)
22 - access("ROO_ID"="SJA_ROOTID_SJ")
24 - access("SPOT"."LYX_DAY"=TO_DATE(:SYS_B_15,:SYS_B_16) AND "SPO_DEVISE_A"=:SYS_B_17)
26 - access("SJA_ROOTID_SJ"="HISTO"."LYX_OWNER_ID" AND "HIR_TIMETYPE_ID"=:SYS_B_14 AND "HIR_TYPE_ID"=:SYS_B_13)
filter(TRUNC(INTERNAL_FUNCTION("HISTO"."LYX_DAY"))=TO_DATE(:SYS_B_11,:SYS_B_12))</pre>
La requête ne génère plus que 45K logical reads et on a bien un INDEX RANGE SCAN sur notre nouvel index à l'opération 9.<br />
On peut noter aussi que des tables sont accédées 2 fois à cause la subquery retournant le MAX(STV_DATE). On peut éviter ce double accès sur ces tables en utilisant une requête analytique:<br />
<pre>
Select HIR_MTM,
ROO_ID,
sja_fieldid FIELDID,
(select STV_VALUE from
(select STV_VALUE,STV_DATE,STV_OBJ_ID,max(STV_DATE) over (partition by STV_OBJ_ID) THE_MAX
From MUT_STATUS_VALOSIMPLE,
MUT_STATUS_PONDERATION,
MUTATION
Where STV_DATE <= TO_DATE('29-06-2012', 'dd-mm-yyyy')
And STV_STATUS_ID = PON_STATUS_ID
And MUT_ID = STV_STATUS_ID
And MUT_TYPESTATUS_ID = 2
And MUT_LASTFLAG = 1) v
where v.STV_DATE=v.THE_MAX and v.STV_OBJ_ID=SJA_ID) POND,
SPO_SPOT
From MUT_HISTO_ROOT histo, MUT_SOUS_JACENT, MUT_ROOT2, SPOT
Where SJA_PARENT_ID = 164826982
And SJA_STARTDATE <= TO_DATE('29-06-2012', 'dd-mm-yyyy')
And (SJA_STOPDATE >= TO_DATE('29-06-2012', 'dd-mm-yyyy') or
SJA_STOPDATE is NULL)
And TRUNC(histo.LYX_DAY(+)) = TO_DATE('29-06-2012', 'dd-mm-yyyy')
And SJA_ROOTID_SJ = histo.LYX_OWNER_ID(+)
And HIR_TYPE_ID(+) = 50000
And HIR_TIMETYPE_ID(+) = 40001
And ROO_ID = SJA_ROOTID_SJ
AND SPOT.LYX_DAY(+) = TO_DATE('29-06-2012', 'dd-mm-yyyy')
AND SPO_DEVISE_A(+) = 14
AND SPO_DEVISE_B(+) = ROO_DEVISE_ID;
Plan hash value: 1110971357
-----------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-----------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1680 |00:00:00.34 | 9320 |
|* 1 | VIEW | | 1680 | 519 | 1680 |00:00:00.32 | 31355 |
| 2 | WINDOW BUFFER | | 1680 | 519 | 4351 |00:00:00.31 | 31355 |
| 3 | NESTED LOOPS | | 1680 | 519 | 4351 |00:00:00.25 | 31355 |
| 4 | NESTED LOOPS | | 1680 | 518 | 4623 |00:00:00.14 | 17373 |
|* 5 | TABLE ACCESS BY INDEX ROWID| MUT_STATUS_VALOSIMPLE | 1680 | 518 | 4623 |00:00:00.08 | 8014 |
|* 6 | INDEX RANGE SCAN | IDX_STV_OBJ_ID | 1680 | 518 | 4623 |00:00:00.03 | 3485 |
|* 7 | INDEX UNIQUE SCAN | PK_POND | 4623 | 1 | 4623 |00:00:00.05 | 9359 |
|* 8 | TABLE ACCESS BY INDEX ROWID | MUTATION | 4623 | 1 | 4351 |00:00:00.09 | 13982 |
|* 9 | INDEX UNIQUE SCAN | PK_MUTATION | 4623 | 1 | 4623 |00:00:00.06 | 9359 |
| 10 | NESTED LOOPS OUTER | | 1 | 86 | 1680 |00:00:00.34 | 9320 |
|* 11 | HASH JOIN OUTER | | 1 | 81 | 1680 |00:00:00.02 | 3406 |
| 12 | NESTED LOOPS | | 1 | 81 | 1680 |00:00:00.02 | 3400 |
|* 13 | TABLE ACCESS BY INDEX ROWID | MUT_SOUS_JACENT | 1 | 81 | 1680 |00:00:00.01 | 38 |
|* 14 | INDEX RANGE SCAN | IDX_SOUS_JACENT_PARENT_ID | 1 | 85 | 1791 |00:00:00.01 | 6 |
| 15 | TABLE ACCESS BY INDEX ROWID | MUT_ROOT2 | 1680 | 1 | 1680 |00:00:00.01 | 3362 |
|* 16 | INDEX UNIQUE SCAN | PK_ROOT2 | 1680 | 1 | 1680 |00:00:00.01 | 1682 |
| 17 | TABLE ACCESS BY INDEX ROWID | SPOT | 1 | 7 | 21 |00:00:00.01 | 6 |
|* 18 | INDEX RANGE SCAN | PK_SPOT | 1 | 7 | 21 |00:00:00.01 | 4 |
| 19 | TABLE ACCESS BY INDEX ROWID | MUT_HISTO_ROOT | 1680 | 1 | 0 |00:00:00.32 | 5914 |
|* 20 | INDEX RANGE SCAN | IDX_MHR_LOHTHTLD | 1680 | 1 | 0 |00:00:00.31 | 5914 |
-----------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter("V"."STV_DATE"="V"."THE_MAX")
5 - filter("STV_DATE"<=TO_DATE(:SYS_B_00,:SYS_B_01))
6 - access("STV_OBJ_ID"=:B1)
7 - access("STV_STATUS_ID"="PON_STATUS_ID")
8 - filter(("MUT_TYPESTATUS_ID"=:SYS_B_02 AND "MUT_LASTFLAG"=:SYS_B_03))
9 - access("MUT_ID"="STV_STATUS_ID")
11 - access("SPO_DEVISE_B"="ROO_DEVISE_ID")
13 - filter(("SJA_STARTDATE"<=TO_DATE(:SYS_B_05,:SYS_B_06) AND ("SJA_STOPDATE">=TO_DATE(:SYS_B_07,:SYS_B_08)
OR "SJA_STOPDATE" IS NULL)))
14 - access("SJA_PARENT_ID"=:SYS_B_04)
16 - access("ROO_ID"="SJA_ROOTID_SJ")
18 - access("SPOT"."LYX_DAY"=TO_DATE(:SYS_B_13,:SYS_B_14) AND "SPO_DEVISE_A"=:SYS_B_15)
20 - access("SJA_ROOTID_SJ"="HISTO"."LYX_OWNER_ID" AND "HIR_TIMETYPE_ID"=:SYS_B_12 AND
"HIR_TYPE_ID"=:SYS_B_11)
filter(TRUNC(INTERNAL_FUNCTION("HISTO"."LYX_DAY"))=TO_DATE(:SYS_B_09,:SYS_B_10))</pre>
Cette réecriture de la requête n'entraine plus qu'un seul accès aux tables MUT_STATUS_VALOSIMPLE, MUT_STATUS_PONDERATION et MUTATION.<br />
Et le nombre de logical reads n'est plus que de 31K.<br />
<br />
<u><br /></u><b><u>Etape 5</u>: Implementer la solution</b><br />
<br />
Cette étape consiste à implémenter la solution de manière contrôlée afin de s'assurer que le problème est bien résolu et que des regressions ne sont pas notées.<br />
Dans notre exemple cette étape a consisté à voir avec le chef de projet si l'index pouvait être crée directement en prod en tant que patch ou bien s'il était préférable d'attendre d'effectuer une batterie de tests supplémentaires.<br />
Vu la criticité du problème la création de l'index en prod avait été décidé ce qui a conduit à une nette amélioration du process applicatif. Par contre, la réecriture de la requête avait été laissée à plus tard lors d'une livraison applicative.<br />
Enfin, lors de cette étape on tente de quantifier l'amélioration apportée pour remonter l'information aux managers et recevoir en retour <strike>des compliments méritées</strike> RIEN. </div>
Ahmed AANGOURhttp://www.blogger.com/profile/03378785645074450748noreply@blogger.com6tag:blogger.com,1999:blog-456686155150137588.post-59273716892695394502013-04-09T22:02:00.003+02:002013-04-09T22:02:58.000+02:00DBLink et ORA-12154<div dir="ltr" style="text-align: left;" trbidi="on">
Si vous tombez sur ce lien après une recherche sur google il est fort probable que avez eu affaire au même problème que moi.<br />
<pre>
SQL> create public database link MOX_SOXS12.WORLD
2 connect to MOX$OWNER identified by moxowner
3 using 'OPARSOXS12';
Database link created.
SQL> desc mox_change@MOX_SOXS12.WORLD
ERROR:
ORA-12154: TNS:could not resolve the connect identifier specified</pre>
Après avoir crée un DB link pointant sur une base dont l'alias est OPARSOXS12 je me suis retrouvé dans l'incapacité d'accéder aux données de ma base distante. Il semble que l'alias OPARSOXS12 ne puisse être résolu.<br />Un <i>tnsping</i> sur cet alias montre pourtant que la résolution s'effectue correctement:<br />
<pre>
C:\>tnsping oparsoxs12
TNS Ping Utility for 32-bit Windows: Version 11.2.0.1.0 - Production on 09-APR-2013 15:08:06
Copyright (c) 1997, 2010, Oracle. All rights reserved.
Used parameter files:
C:\oracle\ora11.2\network\admin\sqlnet.ora
Used TNSNAMES adapter to resolve the alias
Attempting to contact (DESCRIPTION = (ADDRESS_LIST = (ADDRESS = (PROTOCOL = TCP) (Host = SOXDBGDB01.fr.world.socgen) (Port = 1524))) (CONNECT_DATA = (SID = SOXS12)))
OK (80 msec)</pre>
<br />
Jettons un oeil dans mon TNSNAMES.ora:<br />
<pre>
IFILE =S:\_Tools\ora11201\network\admin\tnsnames.ora </pre>
<br />
Mon TNSNAMES.ora pointe en fait sur un autre fichier tnsnames grâce à la clause IFILE.<br />L'alias marche très bien lorsqu'on se connecte via SqlPlus mais ça a l'air de poser problème lorsqu'on utilise un DBLink.<br /><br />Essayons de mettre directement les informations dans le TNSNAMES.ORA du client (la base où se trouve le dblink que j'ai crée):<br />
<pre>
IFILE =S:\_Tools\ora11201\network\admin\tnsnames.ora
### MACHINE
OPARSOXS12.WORLD =
(DESCRIPTION =
(ADDRESS_LIST =
(ADDRESS =
(PROTOCOL = TCP)
(Host = SOXDBGDB01.fr.world.socgen)
(Port = 1524)
)
)
(CONNECT_DATA =
(SID = SOXS12)
)
)</pre>
Maintenant mon DB link fonctionne correctement:<br />
<pre>
SQL> desc mox_change@MOX_SOXS12.WORLD
Name
Null? Type
---------------------------------------------------------------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------------------------------------------------------------- -----
--- ------------------------------------------------------------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------------------------------------------------------------------
ID
NOT NULL NUMBER
JOB_ID
NUMBER
CHANGETYPE_ID
NOT NULL NUMBER
CHANGE_USER
NOT NULL VARCHAR2(200)
CHANGE_DATE
NOT NULL DATE
CHANGE_COMMENT
NOT NULL VARCHAR2(2000)</pre>
En faisant une petite recherche sur google je suis tombé sur la page suivante du site AskTom:<br /><a href="http://asktom.oracle.com/pls/asktom/f?p=100:11:0::::P11_QUESTION_ID:489021635775">http://asktom.oracle.com/pls/asktom/f?p=100:11:0::::P11_QUESTION_ID:489021635775</a><br /><br />Tom Kyte y dit que la clause IFILE n'est en fait pas supporté pour les fichiers de configuration Oracle Net (*.ORA). A la date où Tom Kyte a écrit ces mots il s'agissait de la version 8i.<br />La note MOS 1339269.1 semble indiquer qu'en 10g ça n'était toujours pas supporté:<br />
<blockquote class="tr_bq">
ifiles are not officially supported with Oracle Net admin files<br />Enhancement request Bug 12676140 Would Like the Use of ifiles to be officially supported for net, is currently outstanding. </blockquote>
<br />Si on effectue une recherche sur la doc Oracle 11g R2 on s'aperçoit que la clause IFILE n'est décrite que pour les fichier de paramétrage d'instance (init.ora).<br /> </div>
Ahmed AANGOURhttp://www.blogger.com/profile/03378785645074450748noreply@blogger.com0tag:blogger.com,1999:blog-456686155150137588.post-381756825484875842013-03-19T00:20:00.001+01:002013-03-19T00:23:39.292+01:00un select qui génère du redo?<div dir="ltr" style="text-align: left;" trbidi="on">
On sait tous que les REDO records sont générés normalement dès qu'une mise à jour est effectuée au niveau de la base pour pouvoir être rejoués lors d'un recovery mais surement pas lors d'un SELECT n'est-ce pas? C'est peut-être ce que certains d'entre vous doivent penser. Et pourtant si. Un SELECT tout simple sans clause FOR UPDATE peut générer du REDO.<br />
<br />
Avant d'expliquer la raison de ce phénomène voyons d'abord la preuve avec un exemple:<br />
<pre>zizou-SQL>create table T1 as select * from dba_objects;
Table created.
zizou-SQL>insert into t1 select * from t1;
69269 rows created.
zizou-SQL>/
138538 rows created.
zizou-SQL>/
277076 rows created.
zizou-SQL>/
554152 rows created.
zizou-SQL>create index idx1 on t1(owner);
Index created.
zizou-SQL>exec dbms_stats.gather_table_stats(user,'T1');
PL/SQL procedure successfully completed.
zizou-SQL>select COMPONENT,CURRENT_SIZE/1024/1024,MAX_SIZE/1024/1024 from v$sga_dynamic_components;
COMPONENT CURRENT_SIZE/1024/1024 MAX_SIZE/1024/1024
------------------------- ---------------------- ------------------
shared pool 184 184
large pool 4 4
java pool 4 4
streams pool 0 0
DEFAULT buffer cache 340 340
KEEP buffer cache 0 0
RECYCLE buffer cache 0 0
DEFAULT 2K buffer cache 0 0
DEFAULT 4K buffer cache 0 0
DEFAULT 8K buffer cache 0 0
DEFAULT 16K buffer cache 0 0
DEFAULT 32K buffer cache 0 0
Shared IO Pool 0 0
ASM Buffer Cache 0 0
-- Nb de blocks dans le segment table
zizou-SQL>select blocks from dba_tables where table_name='T1';
BLOCKS
----------
15729</pre>
<br />
J'ai crée dans ma base ZIZOU une table T1 contenant un peu plus de 1 million de lignes. Mon buffer cache fait à peine 340MB et ma table contient 15 729 blocks.<br />
Ma base a une taille de block de 8K.<br />
<br />
Pour vérifier que chaque requête génère ou pas du redo je vais utiliser la commande AUTOTRACE.<br />
J'effectue d'abord un select de ma table T1:<br />
<pre>zizou-SQL>set autot trace stat </pre>
<pre>zizou-SQL>select * from t1 where owner='SYSTEM';
8384 rows selected.
Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
1502 consistent gets
0 physical reads
<b>0 redo size</b>
918591 bytes sent via SQL*Net to client
6557 bytes received via SQL*Net from client
560 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
8384 rows processed</pre>
Le premier SELECT sur ma table n'a pas généré de redo (la stat redo size = 0).<br />
Maintenant j'effectue un update massif de ma table T1 et j'exécute de nouveau le même select:<br />
<pre>zizou-SQL>update t1 set last_ddl_time=SYSDATE;
1108304 rows updated.
Elapsed: 00:00:50.73
zizou-SQL>commit;
Commit complete.
zizou-SQL>select * from t1 where owner='SYSTEM';
8384 rows selected.
Statistics
----------------------------------------------------------
1 recursive calls
0 db block gets
1767 consistent gets
23 physical reads
<b>19124 redo size</b>
918591 bytes sent via SQL*Net to client
6557 bytes received via SQL*Net from client
560 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
8384 rows processed</pre>
Je constate cette fois que le SELECT a bien généré du redo.<br />
Je relance une deuxième fois la requête:<br />
<pre>zizou-SQL>select * from t1 where owner='SYSTEM';
8384 rows selected.
Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
1502 consistent gets
0 physical reads
0 redo size
918591 bytes sent via SQL*Net to client
6557 bytes received via SQL*Net from client
560 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
8384 rows processed</pre>
Là plus rien. La requête ne génère plus de redo.<br />
Pour comprendre ce phénomène il faut se rappeler les concepts sur la gestion des transactions dans Oracle.<br />
Quand une session modifie une ligne dans une table Oracle crée un lock au niveau row en ajoutant une entrée dans l'ITL (Interested Transaction List) dans l'entête du bloc de données contenant la ligne modifiée. Cette entrée ITL contient notamment l'identifiant de la transaction (permettant d'accéder à la table des transactions situées dans le bloc en-tête de l'undo segment) et l'adresse du dernier undo record généré par cette transaction. Si une deuxième session tente de lire cette ligne modifiée (pas encore committée) il verra en checkant l'entrée ITL qu'une transaction active existe pour cette ligne et utilisera les informations dans l'undo segment pour restituer l'image d'avant la modification (consistent read). Quand la transaction est committée, la table des transactions est modifiée dans l'entête du rollback segment pour indiquer que la transaction est committée. Oracle tente également de retrouver le data block modifié par la transaction afin de modifier l'entrée ITL correspondant à la transaction et y indiquer que les changements apportées au bloc par la transaction ont été committés. On dit alors que le "nettoyage" au niveau du bloc de donnée a été effectué lors du commit c'est ce qu'on appelle le <b>commit cleanout</b>. Cependant, si le bloc n'est plus en mémoire au moment du commit (le process DBWR peut avoir écrit le bloc dans le datafile suite à un checkpoint par exemple) ou si le nombre de blocs modifiés par la transaction est trop important, alors Oracle ne va pas s'autoriser à modifier tous les en-têtes des data blocs modifiées et va juste modifier le slot au niveau de la table des transaction dans le bloc en-tête de l'undo segment. La prochaine session qui lira les blocs se rendra compte que ce qu'on a dans l'entrée ITL (transaction non committée) n'est pas cohérent avec ce qu'on a dans la table des transactions de l'undo segment (transaction committée avec indication du numéro SCN) et va faire le travail de nettoyage du bloc lu c'est à dire qu'il va indiquer dans l'entrée ITL que la transaction est en fait bien commitée. Comme le nettoyage de l'en-tête du bloc se fait bien après que le commit ait eu lieu on appelle ce mécanisme le <b>Delayed Block Cleanout</b>.<br />
<br />
Puisque le cleanout consiste à modifier l'entrée ITL et donc à modifier le bloc, du redo est forcément généré. <br />
C'est ce qui s'est passé dans mon exemple précédent: le premier SELECT n'a pas généré de redo puisque les blocs étaient "cleans" ensuite lorsque j'ai modifié massivement plus d'un million de lignes le commit n'a pas fait le travail de nettoyage des blocs (Oracle n'a pas pu se permettre de revisiter tous les blocs pour modifier les entrées ITL et puis vu la petite taille de mon cache les blocs étaient pour la plus part déjà ecrits sur disc). Lors du select qui a suivi mon update le process server s'est rendu compte que les entrées ITL n'avaient pas été cleanés et a donc fait le travail de block cleanout pour chaque bloc récupérés par ma requête.<br />
<br />
Si j'avais modifié un faible nombre de lignes lors de l'update il y' a de forte chances que le cleanout des blocks ait été fait au moment du commit.<br />
Faisons le test pour s'en assurer:<br />
<pre>zizou-SQL>update t1 set last_ddl_time=SYSDATE where owner='APPQOSSYS';
48 rows updated.
zizou-SQL>commit;
Commit complete.
zizou-SQL>select * from t1 where owner='APPQOSSYS';
48 rows selected.
Statistics
----------------------------------------------------------
1 recursive calls
0 db block gets
27 consistent gets
0 physical reads
<b> 0 redo size</b>
6457 bytes sent via SQL*Net to client
452 bytes received via SQL*Net from client
5 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
48 rows processed</pre>
Effectivement on constate que cette fois le SELECT n'a pas généré de redo c'est donc que le cleanout a bien été fait au moment du COMMIT.</div>
Ahmed AANGOURhttp://www.blogger.com/profile/03378785645074450748noreply@blogger.com0tag:blogger.com,1999:blog-456686155150137588.post-82280050127988197172013-02-28T22:35:00.001+01:002013-02-28T22:35:32.138+01:00ORA-12899 et character set<div dir="ltr" style="text-align: left;" trbidi="on">
J'ai obtenu aujourd'hui l'erreur suivante en tentant de mettre à jour une table à partir d'une autre table située sur une base distante (via un dblink):<br /><pre>
SQL> insert /*+ append */ into LNS$OWNER.MUT_ERROR_LOG_FILE
2 select * from MUT_ERROR_LOG_FILE_SAV@LNS_LNSD12;
select * from MUT_ERROR_LOG_FILE_SAV@LNS_LNSD12
*
ERROR at line 2:
ORA-12899: value too large for column
"LNS$OWNER"."MUT_ERROR_LOG_FILE"."ERR_COMMENT" (actual: 242, maximum: 240)</pre>
<br />L'erreur précise que je souhaite insérer une chaine de caractères d'une taille de 242 (on verra après à quoi correspond ce nombre) alors que mon champ est défini par un VARCHAR2(240). Et pourtant la table source et la table cible ont étés crées à partir du même code SQL:<br /><pre>
create table MUT_ERROR_LOG_FILE
(
ERR_ID NUMBER(10) not null,
ERR_CLASSNAME VARCHAR2(150) not null,
ERR_USUALNAME VARCHAR2(200) not null,
ERR_MUTEX_ID NUMBER(10),
ERR_USER_ID NUMBER(10),
ERR_POST_DATE DATE,
ERR_DATE DATE,
ERR_OWNER_ERROR_ID NUMBER(10) not null,
ERR_OWNER_ENTITY_ID NUMBER(10),
ERR_ENTITY_ID NUMBER(10),
ERR_LEVEL NUMBER(10),
ERR_COMMENT VARCHAR2(240),
ERR_AUTO_SOLUTION NUMBER(1),
ERR_LINKEDEVENT_ID NUMBER(10),
ERR_WARNING_DATE DATE
)</pre>
<br />Comment se fait-il alors que mon INSERT plante si les 2 champs sont en VARCHAR2(240)?<br />La cause est à chercher du côté du jeu de caractères défini pour chacune des 2 bases.<br />Ma base source utilise un character set single-byte(WE8DEC) alors que ma base cible utilise un character set multi-bytes, en l'occurence AL32UTF8.<br />Ce qu'il faut savoir c'est qu'en single-byte, 1 caractère correspond toujours à un seul octect. Par contre en mult-byte un caractère peut être défini sur plusieurs octects (1 à 4 bytes pour UTF8).<br />
Illustrons cela avec un exemple.<br />
Dans ma base en single-byte je crée une table avec une colonne de type VARCHAR2(10):<br />
<pre>
SQL> create table t1 (c1 varchar2(10));
Table created
SQL> insert into t1 values ('à');
1 row inserted
SQL> select dump(c1) from t1;
DUMP(C1)
--------------------------------------------------------------------------------
Typ=1 Len=1: 224</pre>
<br />
J'ai inséré dans le champ C1 la valeur 'à' et le résultat de la fonction dump indique bien que la taille stockée pour ce caractère est de 1 octect (Len=1).<br />
Faisons la même chose sur la base définie avec un jeu de caractères multi-bytes:<br /><pre>
SQL> create table t1 (c1 varchar2(10));
Table created
SQL> insert into t1 values ('à');
1 row inserted
SQL> select dump(c1) from t1;
DUMP(C1)
--------------------------------------------------------------------------------
Typ=1 Len=2: 195,160 </pre>
<br />
Cette fois ci le même caractère est stocké sur 2 octects (Len=2).<br />
<br />Revenons maintenant au nombre 240 déterminant la taille de ma colonne ERR_COMMENT.<br />Le type VARCHAR2 peut être défini selon 2 types de taille: BYTES ou CHAR. Avec un jeu de caractères single_byte un caractère vaut forcément un BYTE. Par contre avec un characterset multibytes un caractère peut être stocké sur plusieurs bytes. <br />Par défaut si on ne précise pas le type de taille c'est celle définie par le paramètre NLS_LENGTH_SEMANTICS qui est utilisée.<br />Sur mes 2 bases ce paramètre était défini à BYTE.<br />Donc dans ma table source j'avais des lignes dont la colonne ERR_COMMENT stockait jusqu'à 240 BYTES soit donc 240 caractères puisqu'elle utilise un jeu de caractères single-byte.<br />Par contre ces 240 caractères vont consommer plus que 240 BYTES sur ma base cible puisqu'elle utilise un jeu de caractères multi-bytes. <br />Comme ma colonne est définie avec une taille max de 240 BYTES (puisque NLS_LENGTH_SEMANTICS=BYTE) elle ne peut stocker tous les caractères que ma table source contenait d'où mon erreur <i>ORA-12899: value too large for column</i>.<br />La solution consiste à créer la table en précisant le type de taille CHAR pour indiquer qu'on souhaite stocker 240 caractères maximum et non 240 Bytes:<br /><pre>
create table MUT_ERROR_LOG_FILE
(
ERR_ID NUMBER(10) not null,
ERR_CLASSNAME VARCHAR2(150 CHAR) not null,
ERR_USUALNAME VARCHAR2(200 CHAR) not null,
ERR_MUTEX_ID NUMBER(10),
ERR_USER_ID NUMBER(10),
ERR_POST_DATE DATE,
ERR_DATE DATE,
ERR_OWNER_ERROR_ID NUMBER(10) not null,
ERR_OWNER_ENTITY_ID NUMBER(10),
ERR_ENTITY_ID NUMBER(10),
ERR_LEVEL NUMBER(10),
ERR_COMMENT VARCHAR2(240 CHAR),
ERR_AUTO_SOLUTION NUMBER(1),
ERR_LINKEDEVENT_ID NUMBER(10),
ERR_WARNING_DATE DATE
)</pre>
<br />
<u><b>CONCLUSION</b></u>: <br />
<br />
La morale de cette histoire c'est que lorsque vous utilisez des bases avec un jeu de caractères
multi-bytes il vaut mieux préciser (pour les colonnes de type VARCHAR2) une taille de type CHAR plutôt que de type BYTE.<br />Pour ce
faire vous pouvez soit définir le paramètre NLS_LENGTH_SEMANTICS à CHAR
ou bien ajouter la clause CHAR lorsque vous définissez vos colonnes de
type VARCHAR2.</div>
Ahmed AANGOURhttp://www.blogger.com/profile/03378785645074450748noreply@blogger.com2tag:blogger.com,1999:blog-456686155150137588.post-51715676606954069302013-02-24T23:24:00.000+01:002013-02-24T23:28:57.925+01:00Astuce: personnaliser son prompt Linux et SQL Plus<div dir="ltr" style="text-align: left;" trbidi="on">
Lorsqu'on travaille sur les bases Oracle nous avons généralement
plusieurs terminaux Linux ouverts et connectés sur différentes bases. On
peut avoir ainsi à la fois des fenêtres connectées sur des bases de
PROD et d'autres sur des bases DEV.<br />
<br />
Pour éviter la
confusion entre ces différentes connexions j'aime modifier mon prompt
pour y afficher notamment le nom de l'instance sur laquelle pointe mon
$ORACLE_SID. En ouvrant mon terminal je suis sûr de savoir sur quelle
base je pointe.<br />
<br />
Pour pouvoir modifier l'affichage du
prompt il suffit de modifier la variable système $PS1 en ajoutant des
variables BASH et/ou des variables d'environnement telles que
$ORACLE_SID.<br />
<br />
Personnellement je définie ma variable $PS1 de la manière suivante:<br />
PS1='[\u@\W $ORACLE_SID]$ '<br />
<br />
J'obtiens ainsi le prompt suivant:<br />
<pre>[oracle@oradata orcl1]$ </pre>
<br />
La
variable BASH <i>\u</i> indique le username Linux, la variable<i> \W</i> indique le
répertoire courant. Si je me connecte en SYSDBA je sais d'avance que je
pointerai sur l'instance ORCL1.<br />
<br />
Bien sûr il est préférable
de définir cette variable dans votre fichier .bash_profile pour que
votre prompt soit défini automatiquement.<br />
<br />
Vous pouvez
personnaliser votre prompt à votre guise en choisissant parmi les
variables BASH dont la liste se trouve dans le manuel BASH (man bash):<br />
<pre>PROMPTING
When executing interactively, bash displays the primary prompt PS1 when it
is ready to read a command, and the secondary prompt PS2 when it needs
more input to complete a command. Bash allows these prompt strings to be
customized by inserting a number of backslash-escaped special characters
that are decoded as follows:
\a an ASCII bell character (07)
\d the date in "Weekday Month Date" format (e.g., "Tue May 26")
\D{format}
the format is passed to strftime(3) and the result is
inserted into the prompt string; an empty format results in
a locale-specific time representation. The braces are
required
\e an ASCII escape character (033)
\h the hostname up to the first ‘.’
\H the hostname
\j the number of jobs currently managed by the shell
\l the basename of the shell’s terminal device name
\n newline
\r carriage return
\s the name of the shell, the basename of $0 (the portion fol-
lowing the final slash)
\t the current time in 24-hour HH:MM:SS format
\T the current time in 12-hour HH:MM:SS format
\@ the current time in 12-hour am/pm format
\A the current time in 24-hour HH:MM format
\u the username of the current user
\v the version of bash (e.g., 2.00)
\V the release of bash, version + patch level (e.g., 2.00.0)
\w the current working directory, with $HOME abbreviated with a
tilde (uses the value of the PROMPT_DIRTRIM variable)
\W the basename of the current working directory, with $HOME
abbreviated with a tilde
\! the history number of this command
\# the command number of this command
\$ if the effective UID is 0, a #, otherwise a $
\nnn the character corresponding to the octal number nnn
\\ a backslash
\[ begin a sequence of non-printing characters, which could be
used to embed a terminal control sequence into the prompt
\] end a sequence of non-printing characters</pre>
En
plus de personnaliser le prompt du terminal linux vous pouvez modifier
le prompt SQL Plus en affichant par exemple le nom de l'instance.<br />
Pour ce faire il suffit de modifier la variable SQL Plus <i>sqlprompt</i> de la manière suivante:<br />
<pre>SQL> SET sqlprompt &_CONNECT_IDENTIFIER>
orcl1></pre>
En
ajoutant cette commande dans le fichier glogin.sql (qui se trouve dans
$ORACLE_HOME/sqlplus/admin) la personnalisation du prompt se fera
automatiquement à chaque connexion SQL Plus.</div>
Ahmed AANGOURhttp://www.blogger.com/profile/03378785645074450748noreply@blogger.com0