Mercredi 12 août 2009 3 12 /08 /Août /2009 10:33
Nous inaugurons aujourd'hui cette section, dont l'objectif est... hmmm... aucun.

Nous allons d'ailleurs le prouver tout de suite, car le sujet du jour est : dessiner un coeur avec une requête SQL.
C'est dédié à mon ami Vodiem, un grand spécialiste du cosmetic SQL.

Bien sûr, il serait possible de faire des select de caractères ligne par ligne... mais on va se compliquer un peu la tâche, car c'est aussi cela le cosmetic SQL.
Donc en généralisant, il s'agit de dessiner une courbe quelconque en une requête.

Pour cela, on commence par décomposer la courbe en portions de courbes représentant des fonctions usuelles (Droite, Cosinus, Sinus, ...)
Dans le cas précis du coeur, on peut considérer par exemple qu'il s'agit de deux arcs de sinus / cosinus, plus deux segments de droite.

Il faut donc formaliser la représentation d'une fonction y = f(x) dans un repère orthonormé. Une fois cela fait, il faudra coller ensemble les morceaux (plus facile à dire qu'à faire...)

1) Représentation d'une fonction
Faisons le parallèle avec notre repère Euclidien habituel :
La coordonnée "y" est gérée par le numéro de ligne décroissant.
La coordonnée x est gérée par le positionnement d'un caractère grâce à LPAD.

Cependant, une différence notable : on génère ligne par ligne les "x", donc l'équation entrée dans le LPAD correspond à :
x = f(y)

Exemple :

select x
from (
select lpad('o', 2 * level) as x, level as y
from dual
connect by level <= 10
)
order by y desc
/


X
-----------------------
                   o
                 o
               o
             o
           o
         o
       o
     o
   o
 o

10 rows selected.


En somme, pour afficher la courbe correspond à l'équation y = f(x), il faut que f soit inversible.
Cependant, toutes les fonctions ne sont pas inversibles. Par contre, une grande partie d'entre elles sont décomposables en intervalles sur lesquels elles sont strictement monotones.
Par exemple,  f: x-> sinus(x) entre [0, pi/2] et [pi/2, pi].

with p as (
            select 'o' as pt, 20 as sc from dual)
,intv as (            
            select 1 as id, 5 as ymaxi, 4 as ymini from dual union all
            select 2 as id, 5 as ymaxi, 4 as ymini from dual
            )
,intvsc as (
            select pt, sc, sc * ymaxi as ymax, sc * ymini as ymin, ymini, ymaxi, id
            from p cross join intv)
, intvm as (
            select max(ymax) as ymax from intvsc)
,t as (
select ymax - level  as y --, ymax, level
from intvm
connect by level <= ymax
union all select ymax from intvm
)
, res1 as (
select case   when id = 1 then asin((y /*+ ymini-4*/- ymin)/sc) * sc                                                       
               when id = 2 then (3.141592  - asin((y - ymin)/sc)) * sc               
      end as x            
            , y
            , pt
            , id
from t join intvsc on t.y between intvsc.ymin and intvsc.ymax
)
, res2 as (
select x, y, coalesce(lag(x, 1) over(partition by y order by x, id), 0) as xprev, pt, id
, row_number() over(partition by y, x order by id) as dedoubl
from res1
)
select substr(REPLACE(REPLACE(XMLAgg(XMLElement("x", coalesce(lpad(pt, x - xprev), pt)) order by x),'</x>', ' '),'<x>',' '),2) AS x
from res2
where dedoubl = 1
group by y
order by y desc
;

X
------------------------------------------------------------------
                              o  o
                        o             o
                     o                   o
                   o                       o
                 o                          o
               o                             o
              o                                o
             o                                   o
           o                                      o
          o                                        o
         o                                          o
        o                                             o
       o                                               o
      o                                                 o
     o                                                   o
    o                                                     o
   o                                                       o
  o                                                         o
 o                                                           o
o                                                             o
o                                                               o

21 rows selected.


Les sections de la requête traitent les problématiques rencontrées :
- intv : décrit les intervalles sur lesquels les fonctions sont définies
- intvsc : il s'agit de recadrer l'intervalle.
    D'une part, on lève la difficulté de gérer des coordonnées négatives (soit en abscisse, soit en ordonnée) en réalisant une translation des bornes.
    D'autre, on applique un facteur d'échelle sc(ale) (dans cet exemple 20) : du fait que la granularité des coordonnées est forcément de "1" (soit le numéro de ligne en ordonnée, soit le décalage "LPAD" en abscisse), il faut pouvoir "zoomer".
- t : on traduit l'union des intervalles de définition par la génération "à la volée" de lignes par CONNECT BY LEVEL
- res1 : pour chaque intervalle et chaque fonction s'appliquant dessus, on génère la coordonnée x = f(y) correspondante
    A noter l'utilisation de arcsin, la fonction réciproque de sinus, ainsi que la composition x = arc(g(y)), qui matérialise les translations et homothéties décrites plus haut
- On arrive aux parties finales, les plus sensibles : recoller les morceaux ! (res2 et SELECT final)
    Pour cela, il faut aggréger les lignes qui correspondent à une même ordonnée y. Plusieurs méthodes sont possibles.
    Ici, la solution retenue (Merci Waldar qui me l'a fait découvrir !), est celle de l'aggrégation XML : on représente d'abord les différentes valeurs sous forme de feuilles XML (XMLElement), puis on les concatène (XMLAgg), puis on nettoie les balises dans le résultat.
    Deux problèmes supplémentaires sont traités par l'intermédiaire de res2 (et des fonctions analytiques) : le fait qu'il s'agit plus d'une juxtaposition que d'une aggrégation, dans la mesure où il faut retrancher les LPAD de l'élément précédent ; Et le fait que si deux points sont "confondus", l'aggrégation simple les affichera tous les deux...

Maintenant qu'on sait comment ça marche, il ne reste plus qu'à tracer le coeur en entier !

with p as (
            select chr(3) as pt, 10 as sc from dual)
,intv as (            
            select 1 as id, 5 as ymaxi, 4 as ymini from dual union all
            select 2 as id, 5 as ymaxi, 4 as ymini from dual union all
            select 3 as id, 5 as ymaxi, 4 as ymini from dual union all
            select 4 as id, 5 as ymaxi, 4 as ymini from dual union all
            select 5 as id, 4 as ymaxi, 0 as ymini from dual union all
            select 6 as id, 4 as ymaxi, 0 as ymini from dual
            )
,intvsc as (          
            select pt, sc, sc * ymaxi as ymax, sc * ymini as ymin, ymini, ymaxi, id
            from p cross join intv)
, intvm as (
            select max(ymax) as ymax from intvsc)
,t as (
select ymax - level  as y
from intvm
connect by level <= ymax
union all select ymax from intvm
)
, res1 as (
select case    when id = 1 then asin((y - ymin)/sc) * sc                                                       
               when id = 2 then (3.141592  - asin((y - ymin)/sc)) * sc
               when id = 3 then (3.141592  + asin((y - ymin)/sc)) * sc                                        
               when id = 4 then (6.283185  - asin((y - ymin)/sc)) * sc
               when id = 5 then (-3.141592 * y / 4  /sc + 3.141592  ) * sc
               when id = 6 then (3.141592 * y / 4  /sc + 3.141592  ) * sc               
      end as x            
            , y
            , pt
            , id
from t join intvsc on t.y between intvsc.ymin and intvsc.ymax
)
, res2 as (
select x, y, coalesce(lag(x, 1) over(partition by y order by x, id), 0) as xprev, pt, id
, row_number() over(partition by y, x order by id) as dedoubl
from res1
)
select substr(REPLACE(REPLACE(XMLAgg(XMLElement("x", coalesce(lpad(pt, x - xprev), pt)) order by x),'</x>', ' '),'<x>',' '),2) AS x
from res2
where dedoubl = 1
group by y
order by y desc


X
----------------------------------------------------------------------
              ♥  ♥                                ♥  ♥
          ♥          ♥                       ♥          ♥
        ♥             ♥                   ♥             ♥
      ♥                ♥                ♥                ♥
     ♥                   ♥             ♥                   ♥
    ♥                     ♥           ♥                     ♥
   ♥                        ♥         ♥                        ♥
  ♥                          ♥       ♥                          ♥
 ♥                            ♥     ♥                            ♥
♥                              ♥   ♥                              ♥
♥                                ♥                                ♥  ♥
♥                                                              ♥
♥                                                            ♥
 ♥                                                           ♥
  ♥                                                         ♥
  ♥                                                       ♥
   ♥                                                      ♥
    ♥                                                    ♥
     ♥                                                   ♥
      ♥                                                 ♥
      ♥                                                ♥
       ♥                                              ♥
        ♥                                            ♥
         ♥                                           ♥
         ♥                                         ♥
          ♥                                        ♥
           ♥                                      ♥
            ♥                                     ♥
             ♥                                   ♥
             ♥                                 ♥
              ♥                                ♥
               ♥                              ♥
                ♥                             ♥
                 ♥                           ♥
                 ♥                          ♥
                  ♥                        ♥
                   ♥                      ♥
                    ♥                     ♥
                    ♥                   ♥
                     ♥                  ♥
                      ♥                ♥
                       ♥               ♥
                        ♥             ♥
                        ♥           ♥
                         ♥          ♥
                          ♥        ♥
                           ♥       ♥
                            ♥     ♥
                            ♥    ♥
                             ♥  ♥
                              ♥


Voilà !
C'est très imparfait, mais ça s'explique :
- Il faut soigner les intersections entre les intervalles de définition
- Il faut ajuster la fonction d'aggrégation : on remarque en effet que plus on colle de morceaux, plus la courbe devient irrégulière.
En fait, lpad('o', f(y)) correspond à x = f(y) + 1/ sc...

Je vous laisse paufiner ces détails.
Quoi qu'il en soit, à présent, vous pourrez faire tomber le coeur de votre jolie DBA en lui envoyant cette requête !
(Petit conseil : faites un script qui balance la requête en boucle afin de faire clignoter ses écrans de surveillance. Au mieux, vous lancez en EXECUTE IMMEDIATE en faisant varier le paramètre sc pour faire craquer le shared pool :))
Par Pacman - Publié dans : SQL cosmétique
Ecrire un commentaire - Voir les 4 commentaires
Lundi 3 août 2009 1 03 /08 /Août /2009 14:34
Rechercher, en ordonnant sur une colonne indexée, les N premières lignes... un problème classique, et facile à résoudre !
Parmi les méthodes classique, il y a la limitation par ROWNUM bien sûr, ou son pendant "SQL Normé" : les fonctions analytiques.

On va s'intéresser ici à la méthode analytique et des petites difficultés qu'on peut rencontrer :
1) Utilisation de l'index :
  a. La contrainte NOT NULL
  b. Comportements 9i / 10g
  c. Les tris sur colonnes alphanumériques
2) Cardinalité du TOP

Pour commencer, on va se créer un petit jeu de tests basé sur dba_objects :

SQL> CREATE TABLE test_top AS
  2  SELECT *
  3  FROM dba_objects
  4  WHERE object_id IS NOT NULL
  5  /
Table créée.

SQL> CREATE INDEX test_top_ii ON test_top(object_id) COMPUTE STATISTICS
  2  /
Index créé.

SQL> exec dbms_stats.gather_table_stats(NULL, 'TEST_TOP')
Procédure PL/SQL terminée avec succès.


1) a. Les valeurs NULL ne sont pas stockées dans l'index. Ainsi, lorsqu'on demande toutes les données ordonnées par une colonne définie en NULL, l'index ne peut être utilisé : (Illustration avec la méthode ROWNUM)

SQL> set autotrace traceonly explain
SQL> SELECT * FROM
  2      (SELECT *
  3      FROM test_top
  4      ORDER BY object_id)
  5  WHERE rownum <= 5
  6  /
                                                                                                              
---------------------------------------------------------------------------------
| Id  | Operation               | Name     | Rows  | Bytes |TempSpc| Cost (%CPU)|
---------------------------------------------------------------------------------
|   0 | SELECT STATEMENT        |          |     5 |   885 |       |   313   (2)|
|*  1 |  COUNT STOPKEY          |          |       |       |       |            |
|   2 |   VIEW                  |          | 12879 |  2226K|       |   313   (2)|
|*  3 |    SORT ORDER BY STOPKEY|          | 12879 |  1081K|  2952K|   313   (2)|
|   4 |     TABLE ACCESS FULL   | TEST_TOP | 12879 |  1081K|       |    51   (2)|
---------------------------------------------------------------------------------

Donc le premier pré-requis pour l'utilisation de l'index : avoir au moins une colonne NOT NULL !


SQL> ALTER TABLE test_top MODIFY object_id NOT NULL;
Table modifiée.

SQL> SELECT * FROM
  2      (SELECT *
  3      FROM test_top
  4      ORDER BY object_id)
  5  WHERE rownum <= 5
  6  /
                                                                                                                                   
----------------------------------------------------------------------------------
| Id  | Operation                     | Name        | Rows  | Bytes | Cost (%CPU)|
----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |             |     5 |   885 |     3   (0)|
|*  1 |  COUNT STOPKEY                |             |       |       |            |
|   2 |   VIEW                        |             |     5 |   885 |     3   (0)|
|   3 |    TABLE ACCESS BY INDEX ROWID| TEST_TOP    | 12879 |  1081K|     3   (0)|
|   4 |     INDEX FULL SCAN           | TEST_TOP_II |     5 |       |     2   (0)|
----------------------------------------------------------------------------------



On note au passage que le comportement recherché est bien INDEX FULL SCAN + STOPKEY : on lit l'index en commençant par la première feuille, et on limite la recherche. Pour une recherche de borne sur une colonne indexée non null, on aura de la même manière INDEX FULL SCAN (MIN/MAX)...
Autre remarque : pour contourner cette histoire de contrainte NOT NULL, j'ai tenté le ORDER BY NULL LAST... en vain (ce qui se comprend un peu, car même si les NULL sont en derniers, on ne peut affirmer avant exécution qu'ils ne sont pas dans le périmètre de la requête !)

1)b.
A présent, le vif du sujet, qui constitua pour moi une des surprises agréables de la 10g.
Voyons le résultat du la version analytique :

SQL> EXPLAIN PLAN FOR
  2  SELECT *
  3  FROM
  4      (SELECT a.*, row_number() over(order by object_id) rk
  5      FROM test_top a) t
  6  WHERE rk <= 5
  7  /


Sous 9i :

SQL> select * from table(dbms_xplan.display)
  2  /
------------------------------------------------------------------------------
| Id  | Operation                     |  Name        | Rows  | Bytes | Cost  |
------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |              | 11974 |  2221K|   254 |
|*  1 |  VIEW                         |              | 11974 |  2221K|   254 |
|   2 |   WINDOW NOSORT               |              | 11974 |   970K|   254 |
|   3 |    TABLE ACCESS BY INDEX ROWID| TEST_TOP     | 11974 |   970K|   254 |
|   4 |     INDEX FULL SCAN           | TEST_TOP_II  | 11974 |       |    54 |
------------------------------------------------------------------------------


Snif, ça ne marche pas... alors que sous 10g :

SQL> set autot on
SQL> /
---------------------------------------------------------------------------------------
| Id  | Operation                     | Name        | Rows  | Bytes | Cost (%CPU)|
----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |             | 12879 |  2389K|   231   (1)|
|*  1 |  VIEW                         |             | 12879 |  2389K|   231   (1)|
|*  2 |   WINDOW NOSORT STOPKEY       |             | 12879 |  1081K|   231   (1)|
|   3 |    TABLE ACCESS BY INDEX ROWID| TEST_TOP    | 12879 |  1081K|   231   (1)|
|   4 |     INDEX FULL SCAN           | TEST_TOP_II | 12879 |       |    30   (4)|
----------------------------------------------------------------------------------
                                                                                                                                  

Statistiques
----------------------------------------------------------                                                                          
          1  recursive calls                                                                                                        
          0  db block gets                                                                                                          
          5  consistent gets                                                                                                        
          0  physical reads                                                                                                         
          0  redo size                                                                                                              
       1410  bytes sent via SQL*Net to client                                                                                       
        380  bytes received via SQL*Net from client                                                                                 
          2  SQL*Net roundtrips to/from client                                                                                      
          0  sorts (memory)                                                                                                         
          0  sorts (disk)                                                                                                           
          5  rows processed    
                                                                                   

Le NOSORT STOPKEY ! (et on vérifie sur les consistent gets que ce n'est pas que de la poudre aux yeux :))
Remarquez ici la valeur de la colonne "Rows" du plan d'exécution : ça sera l'objet du point 2 !

1)c.
Ce point n'est pas vraiment lié au type de requête ni aux fonctions analytiques... mais juste à la clause ORDER BY de manière générale. Mais je le cite ici parce que je me suis fait piéger :)

Trier sur une colonne alphanumérique, c'est piégeux. Pourquoi ?
Ben parce que l'ordre des lettres (avec la casse, les caractères bizarres, ...) n'est pas aussi unanime que l'ordre des nombres !
Et du coup, l'ordre réel dans l'index...

Allez, on se refait un test avec la méthode rownum.
Pour cela, on va commencer par créer un index sur object_name :

SQL> ALTER TABLE test_top MODIFY object_name NOT NULL
  2  /
Table modifiée.

SQL> CREATE INDEX test_top_iia ON test_top(object_name) COMPUTE STATISTICS
  2  /
Index créé.

La requête TOP / ROWNUM donne :
SQL> set autotrace traceonly explain
SQL> SELECT *
  2  FROM
  3      (SELECT *
  4      FROM test_top
  5      ORDER BY object_name)
  6  WHERE rownum <= 5
  7  /

---------------------------------------------------------------------------------
| Id  | Operation               | Name     | Rows  | Bytes |TempSpc| Cost (%CPU)|
---------------------------------------------------------------------------------
|   0 | SELECT STATEMENT        |          |     5 |   885 |       |   313   (2)|
|*  1 |  COUNT STOPKEY          |          |       |       |       |            |
|   2 |   VIEW                  |          | 12879 |  2226K|       |   313   (2)|
|*  3 |    SORT ORDER BY STOPKEY|          | 12879 |  1081K|  2952K|   313   (2)|
|   4 |     TABLE ACCESS FULL   | TEST_TOP | 12879 |  1081K|       |    51   (2)|
---------------------------------------------------------------------------------
                                                                                

Ben ouais, ça chie...
La manière de trier les alphanumériques est définie par le paramètre NLS_SORT. (En fait, c'est un peu plus compliqué puisqu'il y a ausse d'autre NLS_ qui peuvent se mettre des batons dans les roues les uns les autres...)
Mais bien entendu, l'ordonnancement effectif physique des clefs de l'index ne dépend pas de ce paramètre : le tri est effectué en binaire.
Donc pour utiliser l'index, il faut également demander le tri en binaire :


SQL> alter session set nls_sort=binary
  2  /
Session modifiée.


Et du coup, ça marche !

SQL> SELECT *
  2  FROM
  3      (SELECT *
  4      FROM test_top
  5      ORDER BY object_name)
  6  WHERE rownum <= 5
  7  /
-----------------------------------------------------------------------------------
| Id  | Operation                     | Name         | Rows  | Bytes | Cost (%CPU)|
-----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |              |     5 |   885 |     5   (0)|
|*  1 |  COUNT STOPKEY                |              |       |       |            |
|   2 |   VIEW                        |              |     5 |   885 |     5   (0)|
|   3 |    TABLE ACCESS BY INDEX ROWID| TEST_TOP     | 12879 |  1081K|     5   (0)|
|   4 |     INDEX FULL SCAN           | TEST_TOP_IIA |     5 |       |     2   (0)|
-----------------------------------------------------------------------------------
                                                                                                                                   



2) Dans la première partie de cette article, j'ai tenté d'attirer votre attention sur une différence notable entre les méthodes ROWNUM et ROW_NUMBER :
La cardinalité !
L'optimiseur estime que la requête renvoie : 5 lignes avec ROWNUM contre... 12879 avec ROW_NUMBER !

Cela pose bien évidemment un problème lorsqu'on réutilise ce morceau de requête.
Un exemple un peu bidon : on fait l'autojointure sur object_id.

SQL> SELECT *
  2  FROM
  3  (SELECT *
  4      FROM (SELECT a.*, row_number() over(order by object_id) rk
  5          from test_top a) where rk <= 5
  6  ) t
  7* JOIN test_top b on t.object_id = b.object_id
SQL> /
------------------------------------------------------------------------------------
| Id  | Operation                      | Name        | Rows  | Bytes |TempSpc| Cost
------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT               |             | 12879 |  3471K|       |   468
|*  1 |  HASH JOIN                     |             | 12879 |  3471K|  1240K|   468
|   2 |   TABLE ACCESS FULL            | TEST_TOP    | 12879 |  1081K|       |    51
|*  3 |   VIEW                         |             | 12879 |  2389K|       |   231
|*  4 |    WINDOW NOSORT STOPKEY       |             | 12879 |  1081K|       |   231
|   5 |     TABLE ACCESS BY INDEX ROWID| TEST_TOP    | 12879 |  1081K|       |   231
|   6 |      INDEX FULL SCAN           | TEST_TOP_II | 12879 |       |       |    30
------------------------------------------------------------------------------------                                                                
                                     

Et bien entendu, ça finit par un FULL SCAN alors qu'on devait lire 5 lignes par un index...
D'ailleurs, pour le confirmer, on peut lancer la version ROWNUM :

-----------------------------------------------------------------------------
| Id  | Operation                        | Name        | Rows  | Bytes | Cost
------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                 |             |     5 |  1275 |   241
|   1 |  TABLE ACCESS BY INDEX ROWID     | TEST_TOP    |     1 |    86 |     2
|   2 |   NESTED LOOPS                   |             |     5 |  1275 |   241
|   3 |    VIEW                          |             |     5 |   845 |   231
|*  4 |     COUNT STOPKEY                |             |       |       |     
|   5 |      VIEW                        |             | 12879 |  2125K|   231
|   6 |       TABLE ACCESS BY INDEX ROWID| TEST_TOP    | 12879 |  1081K|   231
|   7 |        INDEX FULL SCAN           | TEST_TOP_II | 12879 |       |    30
|*  8 |    INDEX RANGE SCAN              | TEST_TOP_II |     1 |       |     1
------------------------------------------------------------------------------


Et là, d'un seul coup, une hypothèse qui provoque la sueur froide : certes le WINDOW STOPKEY permet de limiter le scan... mais le coût associé est celui du scan complet de l'index ??
Petit test :

SQL> select *
  2  from test_top
  3  order by object_id
  4  /

-------------------------------------------------------------------------
| Id  | Operation                   | Name        | Rows  | Bytes | Cost
-------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |             | 12879 |  1081K|   231
|   1 |  TABLE ACCESS BY INDEX ROWID| TEST_TOP    | 12879 |  1081K|   231
|   2 |   INDEX FULL SCAN           | TEST_TOP_II | 12879 |       |    30
-------------------------------------------------------------------------


Eh oui, le coût est exactement le même que celui de la requête top...

Pour ce convaincre, de l'absurdité de la chose, on passe le clustering factor de l'index de 200 à 300, et on retente :

SQL> exec dbms_stats.set_index_stats(NULL, 'TEST_TOP_II', clstfct => 300)
Procédure PL/SQL terminée avec succès.

SQL> set autotrace traceonly explain
SQL> SELECT *
  2  FROM (
  3      SELECT a.*, row_number() over(order by object_id) rk
  4      from test_top a)
  5  where rk <= 5
  6  /

                                                                                                                   
---------------------------------------------------------------------------
| Id  | Operation                | Name     | Rows  | Bytes |TempSpc| Cost
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT         |          | 12879 |  2389K|       |   313
|*  1 |  VIEW                    |          | 12879 |  2389K|       |   313
|*  2 |   WINDOW SORT PUSHED RANK|          | 12879 |  1081K|  2952K|   313
|   3 |    TABLE ACCESS FULL     | TEST_TOP | 12879 |  1081K|       |    51
---------------------------------------------------------------------------


J'ai essayé de déchiffrer la trace 10053, et comme d'habitude, je nage. J'ai simplement cru comprendre qu'avec ROWNUM l'optimiseur détecte le plan de type "First K rows", et dans le second cas, il considère rk <= 5 comme un filtre sur un resultset non analysé...

Enfin bref l'amère conclusion : il faut probablement préférer ROWNUM pour les top queries simples...
Par Pacman - Publié dans : SGBD
Ecrire un commentaire - Voir les 0 commentaires
Jeudi 30 juillet 2009 4 30 /07 /Juil /2009 14:11
Pour la définition des index composites, on rencontre souvent la recommandation :
"En premières positions, les colonnes les plus sélectives !"
Bien entendu, ça dépend du contexte... mais certaines personnes prennent cette règle comme vérité ultime.

Cet article s'attache donc à donner quelques arguments pour faire le contraire (encore une fois, ce ne seront pas des règles absolues, mais vraiment en fonction du cas concret...) :
  1. Le skip scan (A partir de Oracle 9i)
  2. La compression d'index (A partir de Oracle 8i)
  3. L'amélioration du clustering factor

(Euh par contre, je retravaillerai la mise en forme une autre fois)

1) L'index skip scan :
Dans les versions antérieures à 9i, un index composite ne pouvait être utilisé que si les prédicats de filtres / jointures s'appuient sur la première colonne de cet index.
Cependant, si la recherche porte sur la deuxième colonne d'un index, et qu'il y a peu de valeurs distinctes sur la première, il peut être très avantageux de tenter toutes les combinaisons de la première colonne avec la valeur recherchée sur la seconde.
C'est le principe de l'index skip scan.
Exemple en "image" :

- Trois catégories, 10000 valeurs par catégorie
SQL> CREATE TABLE test_iss AS
  2  SELECT 'A' AS categ, level AS n FROM DUAL CONNECT BY level <= 10000 UNION ALL
  3  SELECT 'B' AS categ, level AS n FROM DUAL CONNECT BY level <= 10000 UNION ALL
  4  SELECT 'C' AS categ, level AS n FROM DUAL CONNECT BY level <= 10000
  5  /

Table créée.

SQL> CREATE INDEX test_iss_i1 ON test_iss(categ, n) COMPUTE STATISTICS
  2  /

Index créé.

SQL> exec dbms_stats.gather_table_stats(NULL, 'TEST_ISS')

Procédure PL/SQL terminée avec succès.

SQL> EXPLAIN PLAN FOR SELECT * FROM test_iss WHERE n = 1234
  2  /

Explicité.

SQL> SELECT * FROM TABLE(dbms_xplan.display)
  2  /

PLAN_TABLE_OUTPUT
------------------------------------------------------------------------------------------------------------------------------------
Plan hash value: 2032351434

--------------------------------------------------------------------------------
| Id  | Operation        | Name        | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------
|   0 | SELECT STATEMENT |             |     3 |    15 |     4   (0)| 00:00:01 |
|*  1 |  INDEX SKIP SCAN | TEST_ISS_I1 |     3 |    15 |     4   (0)| 00:00:01 |
--------------------------------------------------------------------------------

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

   1 - access("N"=1234)
       filter("N"=1234)

14 ligne(s) sélectionnée(s).



Bien entendu, pour cette recherche, l'index avec la colonne "n" en première position aurait été plus efficace.
Mais en s'appuyant sur l'index de l'exemple, on couvre un plus grand nombre de requêtes tout en limitant le nombre d'index créés.

2) La compression d'indexes
Un index associe à une valeur de clef (éventuellement composite) l'adresse "physique" de la ligne dans la table (le rowid).
Les valeurs des clefs sont triées. Donc, potentiellement, dans les blocs feuilles, Les "préfixes" de ces valeurs peuvent se répéter un grand nombre de fois...
Dans l'exemple précédent, ça donnerait :
A;1 -> rowid1
A;2 -> rowid2
...
A;10000 -> rowid10000

Depuis Oracle 8.1, il est possible d'exploiter ces répétitions en les factorisant : c'est la compression d'index.
Ce que cela apporte ? Un index plus petit, et donc une économie de lecture de blocs pour les opérations de masse ! (soit sur les INDEX RANGE SCAN, mais aussi sur les INDEX FAST FULL SCAN)
(Bien entendu, si on ne cible qu'une seule valeur, ça ne change pas grand chose...)

Un petit exemple sur une table réelle.
La table HISTO_PTF est alimentée mensuellement pour historiser les performances des portefeuilles.
L'index de clef primaire est (ID_PTF , DATE_HISTO) : ID_PTF est la colonne la plus sélective (19000 valeurs distinctes sur 240000 lignes au total), alors que la colonne DT_HISTO comporte l'est très peu (13)
C'est donc un excellent exemple pour montrer l'effet de la compression : créer l'index par colonnes permutées.

SQL> CREATE INDEX test_col_ind ON histo_ptf(DATE_HISTO, ID_PTF) COMPRESS 1 COMPUTE STATISTICS
  2  /
Index created.

SQL> SELECT index_name, compression, leaf_blocks FROM dba_indexes WHERE table_name = 'HISTO_PTF'
  2  /

INDEX_NAME                     COMPRESS LEAF_BLOCKS
------------------------------ -------- -----------
PK_HISTO_PTF                   DISABLED        1941
TEST_COMP                      ENABLED         1239


36 % de gain !

3) Le clustering factor
Le clustering factor (CF) d'un index mesure la corrélation entre l'ordre des valeurs des clefs de l'index et l'ordre des lignes dans la table.
Lorsque le CF est proche du nombre de lignes dans la table, la corrélation est quasi nulle.
A l'inverse, lorsque le CF est proche du nombre de blocs, les lignes sont à peu près rangées dans le même ordre que les valeurs de la clef.

A quoi ça sert ?
Imaginez que vous devez récupérer un grand nombre de lignes dans une table en suivant un index.
A chaque lecture d'une valeur, vous devrez lire la ligne dans la table.
Si la corrélation CF est forte, vous devrez charger beaucoup moins de blocs.
A l'inverse, si vous devez en lire beaucoup, il sera peut être plus rentable de lire directement la table en entier !
Cette donnée statistique (toujours le CF) est utilisée par l'optimiseur pour évaluer le nombre de blocs à lire pour une requête.

Le lien avec la sélectivité des colonnes sur les indexes composites ?
Certaine colonnes peu sélectives représentent des catégories, ou des groupements de lignes.
Par exemple, si vous votre table stocke des factures contenant des références produit, il se peut que la référence produit soit plus sélective (pour peu que vous distribuiez une grande diversité de produits). Cependant, les lignes dans la table seront regroupées par référence facture, puisque celles-ci risquent d'être saisies en un bloc.

Pour visualiser la chose, nous reprendrons l'exemple du 2) : les performances sont historisée chaque mois, et donc l'insertion est faite par date.

SQL> SELECT index_name, clustering_factor FROM dba_indexes WHERE table_name = 'HISTO_PTF'
  2  /

INDEX_NAME                     CLUSTERING_FACTOR
------------------------------ -----------------
PK_HISTO_RISQUE_PTF                       232337
TEST_COMP                                   3090

SQL> SELECT index_name, clustering_factor FROM dba_indexes WHERE table_name = 'HISTO_PTF'
  2  /

INDEX_NAME                     CLUSTERING_FACTOR
------------------------------ -----------------
PK_HISTO_RISQUE_PTF                       232337
TEST_COMP                                   3090


L'index de test présente un CF parfait, l'autre le pire CF qu'on puisse imagine...
Ca en jette, non ?
Par Pacman - Publié dans : SGBD
Ecrire un commentaire - Voir les 0 commentaires
Vendredi 24 juillet 2009 5 24 /07 /Juil /2009 16:29
Un exercice assez courant : mettre à jour une table A avec les valeurs d'une table B, en faisant la correspondance sur un certain nombre de colonnes.
MySQL permet l'UPDATE de jointure... mais pas Oracle !
J'ai toujours trouvé ça assez naze, car la requête doit exécuter les mêmes accés deux fois pour :
- Rechercher les valeurs dans le SET
- délimiter le périmètre de mise à jour

Mais avec l'évolution de l'instruction MERGE sous Oracle 10g, le problème est enfin résolu !

Gros Erratum :
Sous 9i, il existe déjà une syntaxe pour le faire ! Merci à Mnitu pour l'info : son commentaire donne la syntaxe.
Cela dit, il faut que la table à modifier soit "key preserved" (et surtout le démontrer à Oracle...).
Vous trouverez un petit complément sur le sujet ici.


Allez, un petit exemple pour illustrer la chose.
Création des jeux de tests :

SQL> CREATE TABLE test_merge AS
  2  SELECT level AS id, 0 as val
  3  FROM DUAL
  4  CONNECT BY level <= 1000000
  5  /

Table créée.

SQL> CREATE UNIQUE INDEX test_merge_ii ON test_merge(id)
  2  /

Index créé.

SQL> CREATE TABLE test_merge_source AS
  2  SELECT id, id as val
  3  FROM test_merge
  4  WHERE mod(id, 1000) = 0
  5  /

Table créée.

SQL> CREATE UNIQUE INDEX test_merge_source_ii ON test_merge_source(id)
  2  /

Index créé.

Un petit coup de stats :
SQL> exec dbms_stats.gather_table_stats(NULL, 'TEST_MERGE')

Procédure PL/SQL terminée avec succès.

SQL> exec dbms_stats.gather_table_stats(NULL, 'TEST_MERGE_SOURCE')

Procédure PL/SQL terminée avec succès.



On active l'autotrace, et c'est parti !
SQL> set autotrace on

Première méthode, l'UPDATE simple :
SQL> UPDATE test_merge a
  2  SET val = (SELECT val
  3          FROM test_merge_source b
  4          WHERE a.id = b.id)
  5  /

1000000 ligne(s) mise(s) à jour.


Plan d'exécution
----------------------------------------------------------                                                                         
Plan hash value: 1824631898       
                                  
-----------------------------------------------------------------------------------
| Id  | Operation                    | Name                 | Rows  | Bytes | Cost
-----------------------------------------------------------------------------------
|   0 | UPDATE STATEMENT             |                      |   996K|  5839K|   501
|   1 |  UPDATE                      | TEST_MERGE           |       |              
|   2 |   TABLE ACCESS FULL          | TEST_MERGE           |   996K|  5839K|   501
|   3 |   TABLE ACCESS BY INDEX ROWID| TEST_MERGE_SOURCE    |     1 |     7 |     2
|*  4 |    INDEX UNIQUE SCAN         | TEST_MERGE_SOURCE_II |     1 |       | 
         1
-----------------------------------------------------------------------------------
                                                                                                                                   
Predicate Information (identified by operation id):                                                                                
---------------------------------------------------                                                                                
                                                                                                                                   
   4 - access("B"."ID"=:B1)                                                                                                        


Statistiques
----------------------------------------------------------                                                                         
       2089  recursive calls                                                                                                       
    2073432  db block gets                                                                                                         
    1192599  consistent gets                                                                                                       
          0  physical reads                                                                                                        
  444612940  redo size                                                                                                             
        681  bytes sent via SQL*Net to client                                                                                      
        620  bytes received via SQL*Net from client                                                                                
          4  SQL*Net roundtrips to/from client                                                                                     
          6  sorts (memory)                                                                                                        
          0  sorts (disk)                                                                                                          
    1000000  rows processed                                                                                                        

SQL> rollback

  2  /


Comme on n'a pas délimité le périmètre, toutes les valeurs sans correspondance sont mises à NULL !

C'est pourquoi on est obligé d'ajouter :

SQL> UPDATE test_merge a
  2  SET val = (SELECT val
  3          FROM test_merge_source b
  4          WHERE a.id = b.id)
  5  WHERE EXISTS (SELECT NULL
  6             FROM test_merge_source b
  7             WHERE a.id = b.id)
  8  /

1000 ligne(s) mise(s) à jour.


Plan d'exécution
----------------------------------------------------------                                                                         
Plan hash value:2830137269                                                                                                        
                                                                                                                                   
-----------------------------------------------------------------------------------
| Id  | Operation                    | Name                 | Rows  | Bytes | Cost
-----------------------------------------------------------------------------------
|   0 | UPDATE STATEMENT             |                      |  1000 | 10000 |   508
|   1 |  UPDATE                      | TEST_MERGE           |       |       |      
|   2 |   NESTED LOOPS               |                      |  1000 | 10000 |   508
|   3 |    SORT UNIQUE               |                      |  1000 |  4000 |     2
|   4 |     INDEX FAST FULL SCAN     | TEST_MERGE_SOURCE_II |  1000 |  4000 |     2
|*  5 |    INDEX UNIQUE SCAN         | TEST_MERGE_II        |     1 |     6 |     1
|   6 |   TABLE ACCESS BY INDEX ROWID| TEST_MERGE_SOURCE    |     1 |     7 |     2
|*  7 |    INDEX UNIQUE SCAN         | TEST_MERGE_SOURCE_II |     1 |       |     1
-----------------------------------------------------------------------------------

                                                                                                                                   
Predicate Information (identified by operation id):                                                                                
---------------------------------------------------                                                                                
                                                                                                                                   
   5 - access("A"."ID"="B"."ID")                                                                                                   
   7 - access("B"."ID"=:B1)                                                                                                        


Statistiques
----------------------------------------------------------                                                                         
         10  recursive calls                                                                                                       
       1032  db block gets                                                                                                         
       4013  consistent gets                                                                                                       
          0  physical reads                                                                                                        
     247328  redo size                                                                                                             
        686  bytes sent via SQL*Net to client                                                                                      
        700  bytes received via SQL*Net from client                                                                                
          4  SQL*Net roundtrips to/from client                                                                                     
          2  sorts (memory)                                                                                                        
          0  sorts (disk)                                                                                                          
       1000  rows processed                                                                                                        

SQL> rollback
  2  /

Annulation (rollback) effectuée.


En regardant le plan d'exécution, on voit bien que la jointure est réalisée 2 fois, ce qui est un véritable drame humain...
Du coup, sous 9i ou moins, il était plus efficace de faire CURSOR UPDATE en parcourant la jointure, un gros aveu d'échec.

Mais depuis 10g, il est possible d'utiliser MERGE (merci SQL 2003 !)pour faire faire un UPSERT... sans le SERT !
(Il n'est plus obligatoire d'implémenter les deux clauses MATCHED et NOT MATCHED.

La petite démo :
SQL> MERGE INTO test_merge a
  2  USING test_merge_source b
  3  ON (a.id = b.id)
  4  WHEN MATCHED THEN UPDATE SET a.val = b.val
  5  /
1000 lignes fusionnées.

Plan d'exécution
----------------------------------------------------------                                                                         
Plan hash value: 2484597143                                                                                                        
                                                                                                                                   
------------------------------------------------------------------------
| Id  | Operation            | Name              | Rows  | Bytes | Cost
------------------------------------------------------------------------                                         
|   0 | MERGE STATEMENT      |                   |  1000 | 15000 |   526
|   1 |  MERGE               | TEST_MERGE        |       |       |      
|   2 |   VIEW               |                   |       |       |      
|*  3 |    HASH JOIN         |                   |  1000 | 13000 |   526
|   4 |     TABLE ACCESS FULL| TEST_MERGE_SOURCE |  1000 |  7000 |     3
|   5 |     TABLE ACCESS FULL| TEST_MERGE        |   996K|  5839K|   501
------------------------------------------------------------------------

                                                                                                                                   
Predicate Information (identified by operation id):                                                                                
---------------------------------------------------                                                                                
                                                                                                                                   
   3 - access("A"."ID"="B"."ID")                                                                                                   


Statistiques
----------------------------------------------------------                                                                         
          1  recursive calls                                                                                                       
       1021  db block gets                                                                                                         
       1667  consistent gets                                                                                                       
          0  physical reads                                                                                                        
     248796  redo size                                                                                                             
        688  bytes sent via SQL*Net to client                                                                                      
        640  bytes received via SQL*Net from client                                                                                
          4  SQL*Net roundtrips to/from client                                                                                     
          1  sorts (memory)                                                                                                        
          0  sorts (disk)                                                                                                          
       1000  rows processed                                                                                                        

SQL> COMMIT
  2  /

Validation effectuée.


Ils ne sont pas mignons, le plan d'exécution / les stats ?



Par Pacman - Publié dans : SQL
Ecrire un commentaire - Voir les 2 commentaires
Samedi 18 juillet 2009 6 18 /07 /Juil /2009 16:01
Voilà voilà, j'ai toujours rêvé d'avoir un journal intime, et de le laisser traîner un peu partout pour que tout le monde puisse le lire.
C'est bien à ça que ça sert, un blog ?
J'ai enfin fait le pas. Je suis tellement heureux que je vais m'ouvrir une bière (et changer de caleçon éventuellement).

Bref plus sérieusement, j'ai vu qu'un collègue de forum avait créé un blog pour y poster des petits tours de magie en SQL...

Donc ici, à part mes élucubrations et les expressions de mes perversions les plus étranges, on va parler d'Oracle et de SQL.
Par Pacman - Publié dans : Journal intime
Ecrire un commentaire - Voir les 2 commentaires

Catégories

Derniers Commentaires

Recherche

Calendrier

Mai 2012
L M M J V S D
  1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31      
<< < > >>

Présentation

  • : Le blog de Pacman
  • Le blog de Pacman
  • : Yet Another Stupid Oracle Blog
  • Contact
Créer un blog gratuit sur over-blog.com - Contact - C.G.U. - Rémunération en droits d'auteur - Signaler un abus - Articles les plus commentés