mercredi 27 juillet 2011

TRUNCATE : Appel de Procédure Stockée via db link

Alors que le delete, lui, fonctionne sans problème, la commande truncate en tant que DDL ne passe pas au travers d'un db link.


Sur la base cible :

SQL> SELECT COUNT(*) FROM toto;

  COUNT(*)
----------
         1

Sur la base source :
 
SQL> DELETE FROM toto@versqua;

1 row deleted.

SQL> ROLLBACK;

Rollback complete.

SQL> TRUNCATE TABLE toto@versqua;
TRUNCATE TABLE toto@versqua
                    *
ERROR at line 1:
ORA-02021: DDL operations are not allowed on a remote database


Il existe une solution qui consiste à disposer une procédure stockée sur la base cible qui effectue le truncate. Cette procédure étant appelée depuis la base source via db link.
 
Sur la base cible :

SQL> SELECT COUNT(*) FROM toto;

  COUNT(*)
----------
         1
 
SQL> CREATE PROCEDURE p_u_truncate (p_table IN VARCHAR2)
  2     IS
  3     BEGIN
  4        EXECUTE IMMEDIATE 'TRUNCATE TABLE ' || p_table;
  5     END;
  6  /

Procedure created.

Sur la base source :

SQL> EXEC p_u_truncate@versqua('TOTO');

PL/SQL procedure successfully completed.


Sur la base cible :

SQL> SELECT COUNT(*) FROM toto;

  COUNT(*)
----------
         0

Explication : la procédure stockée sur la base cible est appelée depuis la base source et s'exécute sur la base cible.

mardi 19 juillet 2011

DOUBLONS : détecter les doublons partiels

Comment détecter les lignes d'une table faisant doublon sur une liste de colonne (et non pas sur l'ensemble de la ligne).

Exemple :

SQL> CREATE TABLE toto (
  2     pays VARCHAR2(3),
  3     ville VARCHAR2(10),
  4     note NUMBER
  5  );

Table created.

SQL> INSERT INTO toto VALUES ('FRA','PARIS',1);

1 row created.

SQL> INSERT INTO toto VALUES ('FRA','PARIS',2);

1 row created.

SQL> INSERT INTO toto VALUES ('FRA','PARIS',3);

1 row created.

SQL> INSERT INTO toto VALUES ('FRA','PARIS',3);

1 row created.

SQL> INSERT INTO toto VALUES ('FRA','LYON',4);

1 row created.

SQL> INSERT INTO toto VALUES ('FRA','LYON',5);

1 row created.

SQL> INSERT INTO toto VALUES ('FRA','MARSEILLE',5);

1 row created.

SQL> INSERT INTO toto VALUES ('USA','NEW YORK',5);

1 row created.

SQL> COMMIT;

Commit complete.

Pour le cas classique :

SQL> SELECT t1.pays, t1.ville, t1.note, count(*)
  2  FROM toto t1
  3  GROUP BY t1.pays, t1.ville, t1.note
  4  HAVING COUNT(*) > 1
  5  ;

PAY VILLE            NOTE   COUNT(*)
--- ---------- ---------- ----------
FRA PARIS               3          2

Et la liste :

SQL> SELECT t1.* FROM toto t1 WHERE t1.rowid >= (
  2     SELECT MIN(t2.rowid) FROM toto t2
  3     WHERE t1.pays=t2.pays
  4       AND t1.ville=t2.ville
  5       AND t1.note=t2.note
  6     HAVING COUNT(*) > 1
  7  )
  8  ;

PAY VILLE            NOTE
--- ---------- ----------
FRA PARIS               3
FRA PARIS               3

Et par extension, pour le cas des doublons partiels :

SQL> SELECT t1.pays, t1.ville, COUNT(*)
  2  FROM toto t1
  3  GROUP BY t1.pays, t1.ville
  4  HAVING COUNT(*) > 1
  5  ;

PAY VILLE        COUNT(*)
--- ---------- ----------
FRA LYON                2
FRA PARIS               4

Et la liste :

SQL> SELECT t1.* FROM toto t1 WHERE t1.rowid >= (
  2     SELECT MIN(t2.rowid) FROM toto t2
  3     WHERE t1.pays=t2.pays
  4       AND t1.ville=t2.ville
  5     HAVING COUNT(*) > 1
  6  )
  7  ;

PAY VILLE            NOTE
--- ---------- ----------
FRA PARIS               1
FRA PARIS               2
FRA PARIS               3
FRA PARIS               3
FRA LYON                4
FRA LYON                5

APPEND NOLOGGING : accélérer les insertions

Pour les chargements massifs, il peut être intéressant d'utiliser certaines options qui permettent d'accélérer le temps de traitement. Bien sûr ces options ont des contre-parties.

L'option APPEND pour l'insert force Oracle à passer directement au-delà du HWM (High Water Mark) pour insérer les nouvelles lignes. Elle évite ainsi la recherche des blocks libres et réduit fortement le nombre de blocks à sauvegarder dans les UNDO. Mais elle conduit à une éventuelle perte d'espace disque.

L'option NOLOGGING indique à Oracle qu'il n'est pas nécessaire de logguer dans les REDO les changements qui ont lieu dans la table. Il s'ensuit une écriture moins importante dans les REDO, mais également l'impossibilité de récupérer la table avec les nouvelles lignes en cas de restauration de cette dernière.

Exemple

Préparation :

SQL> CREATE TABLE toto AS
  2  SELECT rownum ID, dbms_random.string('A', 12) STR,
  3  trunc(dbms_random.value(1,1000)) NUM
  4  FROM all_objects
  5  WHERE rownum <= 200000
  6  ;

Table created.
SQL> INSERT INTO toto SELECT * FROM toto;

200000 rows created.
SQL> INSERT INTO toto SELECT * FROM toto;

400000 rows created.
SQL> COMMIT;

Commit complete.

Session 1 :

SQL> SET TIMING ON
SQL> INSERT INTO toto SELECT * FROM toto;

800000 rows created.

Elapsed: 00:00:31.04
SQL> ROLLBACK;

Rollback complete.

Elapsed: 00:00:02.09
SQL> SELECT name, value FROM v$mystat NATURAL JOIN v$statname WHERE name = 'undo change vector size';

NAME                                                       VALUE
----------------------------------------------------- ----------
undo change vector size                                  2270900
 
SQL> SELECT name, value FROM v$mystat NATURAL JOIN v$statname WHERE name = 'rollback changes - undo records applied';

NAME                                                       VALUE
----------------------------------------------------- ----------
rollback changes - undo records applied                     6087

SQL> SELECT name, value FROM v$mystat NATURAL JOIN v$statname WHERE name = 'redo size';

NAME                                                       VALUE
----------------------------------------------------- ----------
redo size                                               31320224


Session 2 :

SQL> SET TIMING ON
SQL> INSERT /*+ APPEND */ INTO toto SELECT * FROM toto;

800000 rows created.

Elapsed: 00:00:12.06
SQL> ROLLBACK;

Rollback complete.

Elapsed: 00:00:00.00
SQL> SELECT name, value FROM v$mystat NATURAL JOIN v$statname WHERE name = 'undo change vector size';

NAME                                                       VALUE
----------------------------------------------------- ----------
undo change vector size                                   258516

SQL> SELECT name, value FROM v$mystat NATURAL JOIN v$statname WHERE name = 'rollback changes - undo records applied';

NAME                                                       VALUE
----------------------------------------------------- ----------
rollback changes - undo records applied                        0

SQL> SELECT name, value FROM v$mystat NATURAL JOIN v$statname WHERE name = 'redo size';

NAME                                                       VALUE
----------------------------------------------------- ----------
redo size                                               25438660


Session 3 :

SQL> SET TIMING ON
SQL> INSERT /*+ APPEND NOLOGGING */ INTO toto SELECT * FROM toto;

800000 rows created.

Elapsed: 00:00:04.01
SQL> ROLLBACK;

Rollback complete.

Elapsed: 00:00:00.00
SQL> SELECT name, value FROM v$mystat NATURAL JOIN v$statname WHERE name = 'undo change vector size';

NAME                                                       VALUE
----------------------------------------------------- ----------
undo change vector size                                      208

SQL> SELECT name, value FROM v$mystat NATURAL JOIN v$statname WHERE name = 'rollback changes - undo records applied';

NAME                                                       VALUE
----------------------------------------------------------------
rollback changes - undo records applied                        0
 
SQL> SELECT name, value FROM v$mystat NATURAL JOIN v$statname WHERE name = 'redo size';

NAME                                                       VALUE
----------------------------------------------------- ----------
redo size                                               24488552

en résumé :

APPEND   NOLOGGING  INSERT(sec)  UNDO_VECTOR  REDO_SIZE
     N           N           31      2270900   31320224
     Y           N           12       258516   25438660
     Y           Y            4          208   24488552


ENABLE ROW MOVEMENT : tables partitionnées

Oracle garantit qu'une ligne ne sera pas déplacée physiquement par un update et donc que son ROWID ne sera pas changé. Ce phénomène pourrait en effet survenir dans une table partitionnée avec un update sur la colonne de partitionnement, forçant Oracle à déplacer la ligne dans la bonne partition. (Déplacement physique, changement de ROWID).
Exemple :

SQL> CREATE TABLE toto(a NUMBER)
  2  PARTITION BY RANGE (a)(
  3  PARTITION p1 VALUES LESS THAN (10),
  4  PARTITION p2 VALUES LESS THAN (20),
  5  PARTITION p3 VALUES LESS THAN (MAXVALUE));

Table created.

SQL> INSERT INTO toto VALUES (5);

1 row created.

SQL> UPDATE toto SET a=15;
UPDATE toto SET a=15
       *
ERROR at line 1:
ORA-14402: updating partition key column would cause a partition change
 
Pourquoi Oracle provoque-t'il cette erreur ?

A priori on ne voit pas de raison pour laquelle on ne pourrait pas tranquillement mettre à jour une table, même sur la colonne de partitionnement, sans provoquer une erreur... Ceci est en fait un héritage, qu'Oracle nous a transmis pour que lors de l'introduction de tables partitionnées, les programmes qui utilisaient les ROWID n'aient pas de mauvaise surprise en partitionnant leurs tables. Le changement du ROWID lors d'un update pouvant être catastrophique avec ce genre de conception de programme.

Mais comment faire alors ?

Par défaut, donc, Oracle empêche les mises à jour des colonnes de partitionnement des tables partitionnées. Pour autoriser ces mises à jour, il est donc nécessaire de passer la commande ENABLE ROW MOVEMENT en préalable :

Exemple :

SQL> ALTER TABLE toto ENABLE ROW MOVEMENT;

Table altered.

SQL> UPDATE toto SET a=15;

1 row updated.

Bien qu'il existe la commande inverse qui rétablit l'impossibilité de mettre à jour ces colonnes (DISABLE ROW MOVEMENT), si aucun de vos programmes n'utilise les ROWID, il n'est pas nécessaire de repasser les tables en mode par défaut (disable).

Voici tout de même la commande :

SQL> ALTER TABLE toto DISABLE ROW MOVEMENT;

Table altered.

mardi 12 juillet 2011

VARIABLES : bind, substitution, programme

Dans cet exemple sont illustrés les emplois des différents types de variable suivants :
 
- variable de substitution (define) - interprétée par SqlPlus
- bind variable (variable) - SQL - PL/SQL
- variable de programme (declare) - PL/SQL

Ces variables sont utilisées pour des raisons bien spécifiques, et ne doivent en aucun cas être confondues...

Exemple
  
SQL> SET VERIFY OFF
SQL> DEFINE matable=dba_users;
SQL> VARIABLE b NUMBER;
SQL> DECLARE
  2  res VARCHAR2(30);
  3
  4  BEGIN
  5  :b :=0;
  6  SELECT username INTO res FROM &matable WHERE user_id=:b;
  7  dbms_output.put_line('USER IS : '||res);
  8  END;
  9  /
USER IS : SYS

PL/SQL procedure successfully completed.


Autre exemple pour une bind variable en SQL pur

SQL> VARIABLE b NUMBER;
SQL> EXECUTE :b:=0;

PL/SQL procedure successfully completed.

SQL> SELECT username
  2  FROM all_users
  3  WHERE user_id = :b;

USERNAME
------------------------------
SYS


OUT : valeur de retour d'une procedure

Les procédures Oracle offrent la possibilité de renvoyer une valeur en sortie au travers d'une variable OUT définie dans la liste des paramètres de la procédure.

CREATE OR REPLACE PROCEDURE p_sqrt(
    a IN NUMBER, b OUT NUMBER
) AS
BEGIN
    b:=a*a;
END;
/

SET SERVEROUTPUT ON SIZE 2000

DECLARE
  entree NUMBER;
  sortie NUMBER;
BEGIN
  entree:=9;
  p_sqrt(entree,sortie);
  dbms_output.put_line(entree||'² = '||sortie);
END;
/
9² = 81

PL/SQL procedure successfully completed.

CUBE : requête analytique

Il existe de nombreuses fonctions analytiques permettant d'obtenir des rapports multi-dimensionnels plus ou moins complexes.

Exemple de Cube :

SET LINES 150
SET PAGES 30
SET FEED OFF
SET ESCAPE \

BREAK ON typ SKIP 1 ON tbs SKIP 0
COL tbs FOR A10 TRUNC

SELECT
    NVL(t.typ,'ALL') AS typ,
    NVL(t.tablespace_name,'________') AS tbs,
    ROUND(SUM(t.bytes)/1024/1024,0) AS siz_mo
FROM (
    SELECT
   (CASE
        WHEN s.segment_type LIKE 'TAB%' THEN 'TAB'
        WHEN s.segment_type LIKE 'IND%' THEN 'IND'
        ELSE 'OTH'
    END
    ) AS typ,
    s.tablespace_name,
    s.bytes
    FROM dba_segments S
    WHERE owner='SYS'
) T
GROUP BY CUBE(t.typ, t.tablespace_name)
ORDER BY 1 DESC, 2 DESC
/

SET FEED ON
SET ESCAPE OFF
 

Résultat :
 
TYP TBS            SIZ_MO
--- ---------- ----------
TAB TOOLS              23
    SYSTEM            402
    SYSAUX            205
    ________          629

OTH UNDOTBS            52
    SYSTEM            218
    SYSAUX             21
    ________          291

IND TOOLS               0
    SYSTEM            370
    SYSAUX            283
    ________          654

ALL UNDOTBS            52
    TOOLS              23
    SYSTEM            990
    SYSAUX            508
    ________         1573

ORA_HASH : fonction de hachage

La fonction de hachage ORA_HASH introduite en 10G, permet de répartir des valeurs en entrée vers une sortie homogène. C'est cette même fonction qui est utilisée par Oracle pour répartir les valeurs d'une colonne dans une table partitionnée. (Voir exemple ci-dessous).

Cette fonction a deux ou trois arguments

select ORA_HASH(expr, max_bucket, seed_value) from DUAL;
 
expr : expression sur laquelle on applique le hachage
 
max_bucket : numéro maximal de sortie de fonction en partant de 0
 
seed_value (otionnelle) : valeurs prise en compte dans la fonction de hachage pour permettre une variation des résultats avec une même expression en entrée (par défaut seed_value = 0).

 
SQL> CREATE TABLE test PARTITION BY HASH (a) (
  2     PARTITION p1,
  3     PARTITION p2,
  4     PARTITION p3,
  5     PARTITION p4
  6  )
  7  AS
  8  SELECT ROWNUM AS a
  9  FROM ALL_OBJECTS
 10  WHERE ROWNUM < 1000
 11  /

Table created.

SQL> SELECT COUNT(*) FROM test PARTITION (p1)
  2     WHERE ORA_HASH(a,3)+1<>1;

  COUNT(*)
----------
         0

SQL> SELECT COUNT(*) FROM test PARTITION (p2)
  2     WHERE ORA_HASH(a,3)+1<>2;

  COUNT(*)
----------
         0

SQL> SELECT COUNT(*) FROM test PARTITION (p3)
  2     WHERE ORA_HASH(a,3)+1<>3;

  COUNT(*)
----------
         0

SQL> SELECT COUNT(*) FROM test PARTITION (p4)
  2     WHERE ORA_HASH(a,3)+1<>4;

  COUNT(*)
----------
         0


CASE : plusieurs COUNT en une seule lecture

Si possible regrouper les COUNT sur une même table dans la même requête via un CASE, ce qui vous évitera de lire plusieurs fois la table en FULL.
 
Exemple
 
SQL> CREATE TABLE t1 AS
  2  SELECT ora_hash(ROWNUM,1000) PRICE
  3  FROM ALL_OBJECTS
  4  WHERE ROWNUM < 1000;

Table created.

SQL> SELECT COUNT (*) FROM t1
  2     WHERE price < 100 ;

  COUNT(*)
----------
       104

SQL> SELECT COUNT (*) FROM t1
  2     WHERE price BETWEEN 100 AND 900 ;

  COUNT(*)
----------
       799

SQL> SELECT COUNT (*) FROM t1
  2     WHERE price > 900 ;

  COUNT(*)
----------
        96

SQL> SET AUTOTRACE ON EXPLAIN
SQL> SELECT
  2     COUNT (CASE WHEN price < 100
  3             THEN 1 ELSE null END) count1,
  4     COUNT (CASE WHEN price BETWEEN 100 AND 900
  5             THEN 1 ELSE null END) count2,
  6     COUNT (CASE WHEN price > 900
  7             THEN 1 ELSE null END) count3
  8  FROM t1;

    COUNT1     COUNT2     COUNT3
---------- ---------- ----------
       104        799         96


Execution Plan
----------------------------------------------------------
   0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=3 Card=1 Bytes=13)
   1    0   SORT (AGGREGATE)
   2    1     TABLE ACCESS (FULL) OF 'T1' (TABLE) (Cost=3 Card=999 Byt
          es=12987)


SQL> SET AUTOTRACE OFF

COUNT : passage par un index ou pas

Le COUNT(*) renvoie le nombre total de lignes de la table. Il ne peut donc utiliser qu'un index Bitmap. Par contre il peut utiliser n'importe quel index Bitmap (car pour chaque valeur du Bitmap, on a une image de toutes les lignes - donc des lignes a valeur nulle).
Si la colonne est NOT NULL, alors toutes les valeurs de cette colonne seront renseignées, donc un index B*Tree indexera lui aussi toutes les lignes de la table, il pourra donc être utilisé pour le COUNT(*) dans ce cas uniquement.

Le COUNT(colonne) renvoie le nombre de lignes ayant une valeur non nulle pour la colonne. Il peut donc passer par un index B*Tree (et Bitmap) pour évaluer le résultat.

Le COUNT(n) est équivalent au COUNT(*) mais la méthode recommandée est le COUNT(*).


Exemples

SQL> CREATE TABLE t1 (a NUMBER);

Table created.

SQL> INSERT INTO t1 VALUES (1);

1 row created.

SQL> INSERT INTO t1 VALUES (null);

1 row created.

SQL> INSERT INTO t1 VALUES (3);

1 row created.

SQL> SELECT * FROM T1;

         A
----------
         1

         3

 
Exemple d'utilisation d'index B*Tree avec des NULLs
 

SQL> CREATE INDEX i_t1 ON t1 (a);

Index created.

SQL> EXEC DBMS_STATS.GATHER_TABLE_STATS(USER,'t1',no_invalidate=>FALSE);

PL/SQL procedure successfully completed.

SQL> SET AUTOTRACE ON EXPLAIN
SQL> SELECT COUNT(*) FROM T1;

  COUNT(*)
----------
         3


Execution Plan
----------------------------------------------------------
   0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=3 Card=1)
   1    0   SORT (AGGREGATE)
   2    1     TABLE ACCESS (FULL) OF 'T1' (TABLE) (Cost=3 Card=3)



SQL> SELECT COUNT(1) FROM T1;

  COUNT(1)
----------
         3


Execution Plan
----------------------------------------------------------
   0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=3 Card=1)
   1    0   SORT (AGGREGATE)
   2    1     TABLE ACCESS (FULL) OF 'T1' (TABLE) (Cost=3 Card=3)


Dans les deux exemples précédents, l'index B*Tree ne peut être utilisé car il n'indexe pas la totalité des lignes de la table (valeurs nulles exclues).

SQL> SELECT COUNT(a) FROM T1;

  COUNT(A)
----------
         2


Execution Plan
----------------------------------------------------------
   0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=1 Card=1 Bytes=13)
   1    0   SORT (AGGREGATE)
   2    1     INDEX (FULL SCAN) OF 'I_T1' (INDEX) (Cost=1 Card=3 Bytes
          =39)

Le count(colonne) décompte toutes les lignes ayant une valeur non nulle de cette colonne, donc autorise le passage par un index B*Tree

 
Exemple d'utilisation d'index Bitmap avec des NULLs

SQL> DROP INDEX i_t1;

Index dropped.

SQL> CREATE BITMAP INDEX i_t1 ON t1 (a);

Index created.

SQL> EXEC DBMS_STATS.GATHER_TABLE_STATS(USER,'t1',no_invalidate=>FALSE);

PL/SQL procedure successfully completed.

SQL> SET AUTOTRACE ON EXPLAIN
SQL> SELECT COUNT(*) FROM t1;

  COUNT(*)
----------
         3


Execution Plan
----------------------------------------------------------
   0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=1 Card=1)
   1    0   SORT (AGGREGATE)
   2    1     BITMAP CONVERSION (COUNT) (Cost=1 Card=3)
   3    2       BITMAP INDEX (FAST FULL SCAN) OF 'I_T1' (INDEX (BITMAP
          ))


L'index Bitmap enregistre une image bitmap de toutes les lignes de la table pour une colonne donnée (si index mono colonne), et donc index les valeurs nulles. Par conséquent le count(*) qui décompte toutes les lignes de la table, peut utiliser cet index.

SQL> SELECT COUNT(a) FROM t1;

  COUNT(A)
----------
         2


Execution Plan
----------------------------------------------------------
   0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=1 Card=1 Bytes=2)
   1    0   SORT (AGGREGATE)
   2    1     BITMAP CONVERSION (TO ROWIDS) (Cost=1 Card=3 Bytes=6)
   3    2       BITMAP INDEX (FAST FULL SCAN) OF 'I_T1' (INDEX (BITMAP
          ))

SQL> SET AUTOTRACE OFF


Exemple d'utilisation d'un index B*Tree avec contrainte NOT NULL


SQL> DROP INDEX i_t1;

Index dropped.

SQL> CREATE INDEX i_t1 ON t1 (a);

Index created.

SQL> EXEC DBMS_STATS.GATHER_TABLE_STATS(USER,'t1',no_invalidate=>FALSE);

PL/SQL procedure successfully completed.

SQL> DELETE FROM t1 WHERE a IS NULL;

1 row deleted.

SQL> SET AUTOTRACE ON EXPLAIN
SQL> SELECT COUNT(*) FROM t1;

  COUNT(*)
----------
         2


Execution Plan
----------------------------------------------------------
   0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=3 Card=1)
   1    0   SORT (AGGREGATE)
   2    1     TABLE ACCESS (FULL) OF 'T1' (TABLE) (Cost=3 Card=3)



SQL> SET AUTOTRACE OFF
SQL> ALTER TABLE t1 MODIFY a NOT NULL;

Table altered.

SQL> SET AUTOTRACE ON EXPLAIN
SQL> SELECT COUNT(*) FROM t1;

  COUNT(*)
----------
         2


Execution Plan
----------------------------------------------------------
   0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=1 Card=1)
   1    0   SORT (AGGREGATE)
   2    1     INDEX (FULL SCAN) OF 'I_T1' (INDEX) (Cost=1 Card=3)



SQL> SET AUTOTRACE OFF

Cette fois l'index B*Tree est bien utilisé pour le count(*)

BOOLEAN : Simuler un champ de type bouléen

Le type bouléen n'existe pas sous Oracle, ainsi pour créer une table avec un champ qui ne peut prendre que 1 ou 2 comme valeur, il faut passer par la création d'une contrainte de type CHECK.
 
SQL> CREATE TABLE t(
  2     x NUMBER CONSTRAINT BOOLEAN CHECK (x=0 OR x=1)
  3  );

Table created.

SQL> INSERT INTO t VALUES (0);

1 row created.

SQL> INSERT INTO t VALUES (1);

1 row created.

SQL> INSERT INTO t VALUES (2);
INSERT INTO t VALUES (2)
*
ERROR at line 1:
ORA-02290: check constraint (LC.BOOLEAN) violated

SQL%ROWCOUNT : Curseur implicite qui donne le nombre de lignes impactées en PL/SQL

Le curseur implicite Oracle SQL%ROWCOUNT utilise en PL/SQL pour connaître le nombre de lignes impactées par la dernière requête de type DML (select, insert, update, delete, merge).

Exemple :

SQL> CREATE TABLE toto (a NUMBER, b NUMBER);

Table created.

SQL> SET SERVEROUTPUT ON SIZE 20000
SQL> DECLARE
  2
  3  v NUMBER;
  4
  5  BEGIN
  6
  7  INSERT INTO toto VALUES (1, null);
  8
  9  DBMS_OUTPUT.PUT_LINE('A - NB LIGNES : '||SQL%ROWCOUNT);
 10
 11  INSERT INTO toto VALUES (2, null);
 12  INSERT INTO toto VALUES (3, null);
 13  DBMS_OUTPUT.PUT_LINE('B - NB LIGNES : '||SQL%ROWCOUNT);
 14
 15  UPDATE toto SET b=2;
 16  DBMS_OUTPUT.PUT_LINE('C - NB LIGNES : '||SQL%ROWCOUNT);
 17
 18  SELECT COUNT(*) INTO V FROM toto;
 19  DBMS_OUTPUT.PUT_LINE('D - NB LIGNES : '||SQL%ROWCOUNT);
 20
 21  END;
 22  /
A - NB LIGNES : 1
B - NB LIGNES : 1
C - NB LIGNES : 3
D - NB LIGNES : 1

PL/SQL procedure successfully completed.


SQL> CREATE TABLE titi AS SELECT * FROM toto;

Table created.

SQL> BEGIN
  2
  3  MERGE INTO toto USING titi ON (toto.a=titi.a)
  4  WHEN MATCHED THEN UPDATE SET toto.b=titi.b;
  5  DBMS_OUTPUT.PUT_LINE('E - NB LIGNES : '||SQL%ROWCOUNT);
  6
  7  EXECUTE IMMEDIATE('INSERT INTO titi SELECT * FROM titi');
  8  DBMS_OUTPUT.PUT_LINE('F - NB LIGNES : '||SQL%ROWCOUNT);
  9
 10  DELETE FROM titi;
 11  DBMS_OUTPUT.PUT_LINE('G - NB LIGNES : '||SQL%ROWCOUNT);
 12
 13  EXECUTE IMMEDIATE('DROP TABLE titi');
 14  DBMS_OUTPUT.PUT_LINE('H - NB LIGNES : '||SQL%ROWCOUNT);
 15
 16  END;
 17  /
E - NB LIGNES : 3
F - NB LIGNES : 3
G - NB LIGNES : 6
H - NB LIGNES : 0

PL/SQL procedure successfully completed.

lundi 11 juillet 2011

DBMS_RANDOM, CONNECT BY LEVEL : Alimentation d'une table de test

Lorsque l'on doit effectuer un test, on doit rapidement créer une table de test avec quelques champs simples et des valeurs séquentielles ou aléatoires. Voici des méthodes qui permettent d'atteindre ce but.

1ère méthode

SQL> CREATE TABLE toto(
  2     id  NUMBER,
  3     str VARCHAR2(4000),
  4     num NUMBER
  5  );

Table created.

SQL> BEGIN
  2  FOR i IN 1..200
  3  LOOP
  4  INSERT INTO toto VALUES(i, dbms_random.string('A',12),
  5   trunc(dbms_random.value(1,1000)));
  6  END LOOP;
  7  END;
  8  /

PL/SQL procedure successfully completed.
 

2ème méthode

SQL> CREATE TABLE toto AS
  2  SELECT rownum id, dbms_random.string('A', 12) str,
  3  trunc(dbms_random.value(1,1000)) num
  4  FROM all_objects
  5  WHERE rownum <= 200;
 

Résultat (méthodes 1 et 2)
 
SQL> COL STR FOR A12
SQL> SET PAGES 300
SQL> SELECT * FROM toto;

        ID STR                 NUM
---------- ------------ ----------
         1 ebGePFqTEVdt        356
         2 tuFhNYHGyfCq        222
         3 atQzvjgiHrAa        560
         4 YlBAahgGNEeu        828
         5 moJUngYgFmDl         79
         6 kkSTXdSPYhof        734
         7 PupVlDoEEUPf        450
         8 FMgJpmltAEkf        377
         9 DcfSaZOdBJku        821
        10 HFTPIYfDnHBK        521
...
       198 oIsMiAsCUPXo        400
       199 VangiDQjfWHX         42
       200 dobTswbjYOsL        219

200 rows selected.
 
Problème : si l'on veut créer plus de lignes qu'il n'y en a dans la plus grande table du catalogue (dba_segments ou dba_extents ou dba_tables par exemple...) des millions de lignes ?

Une astuce consiste à utiliser un produit Cartésien (habituellement pourchassé par les DBAs à juste titre...)

SQL> CREATE TABLE toto AS
  2  SELECT rownum id, dbms_random.string('A', 12) str,
  3  trunc(dbms_random.value(1,1000)) num
  4  FROM
  5  (SELECT 1 FROM all_objects WHERE rownum <= 9),
  6  (SELECT 1 FROM all_objects WHERE rownum <= 9)
  7  ;

Table created.

SQL> SELECT COUNT(*) FROM toto;

  COUNT(*)
----------
        81

SQL> SELECT MIN(id), MAX(id), COUNT(*) FROM toto;

   MIN(ID)    MAX(ID)   COUNT(*)
---------- ---------- ----------
         1         81         81

  

Bon allez, tout ça c'était pour vous faire réfléchir, car en fait il existe une méthode encore plus triviale, mais si on ne la connait pas, c'est plus difficile de l'inventer (moi je l'ai trouvée un jour sur le net, comme ça par hasard ...). Cette ultime méthode permet de s'affranchir à la fois de trouver une table assez grande comme source de lignes et de faire une boucle PL.

SQL> CREATE TABLE toto AS
  2  SELECT rownum id, dbms_random.string('A', 12) str,
  3  trunc(dbms_random.value(1,1000)) num
  4  FROM dual
  5  CONNECT BY LEVEL <= 200
  6  ;

Table created.
 
SQL> SELECT COUNT(*) FROM toto;

  COUNT(*)
----------
       200

Enfin, une petite astuce pour obtenir une distribution non-uniforme de valeurs (skewed values), dans le cas par exemple de tests sur les histogrammes :
 
SQL> CREATE TABLE toto AS
  2  SELECT 1 id
  3  FROM dual
  4  CONNECT BY LEVEL <= 10000
  5  ;

Table created.

SQL> UPDATE toto
  2  SET id=2
  3  WHERE ROWNUM <= 10
  4  ;

10 rows updated.

SQL> SELECT id, COUNT(*)
  2  FROM toto
  3  GROUP BY id
  4  ;

        ID   COUNT(*)
---------- ----------
         2         10
         1       9990

IS_NUMBER : détection des NUMBER déguisés en CHAR

Une fonction bien utile à utiliser dans les programmes pour détecter si une chaîne est en fait un NUMBER déguisé...

Par exemple, on me fournit une table avec une colonne varchar2 qui contient beaucoup de valeurs. Je soupçonne une erreur de conception, ce champ aurait-il pu être défini en NUMBER ?

Je peux rechercher s'il existe des lignes dans la table où la fonction me retourne 0.


CREATE OR REPLACE FUNCTION is_number(string_in VARCHAR2)
RETURN NUMBER
IS
val number;
BEGIN
val := TO_NUMBER (string_in);
RETURN 1;
EXCEPTION
WHEN OTHERS THEN RETURN 0;
END;
/

SQL> CREATE TABLE toto(a VARCHAR2(100));

Table created.

SQL> SELECT value FROM v$nls_parameters WHERE parameter='NLS_NUMERIC_CHARACTERS';

VALUE
----------------------------------------------
,.

SQL> INSERT INTO toto VALUES ('1564');

1 row created.

SQL> INSERT INTO toto VALUES ('11E3');

1 row created.

SQL> INSERT INTO toto VALUES ('0,12');

1 row created.

SQL> SELECT CASE
  2     WHEN (is_number(a) = 0) then 'CHAR'
  3     WHEN (is_number(a) = 1) then 'NUMBER'
  4     END AS WHAT,
  5     COUNT(*) FROM toto GROUP BY is_number(a);

WHAT     COUNT(*)
------ ----------
NUMBER          3
 
SQL> INSERT INTO toto VALUES ('0.12');

1 row created.

SQL> INSERT INTO toto VALUES ('ABCD');

1 row created.

SQL> SELECT CASE
  2     WHEN (is_number(a) = 0) then 'CHAR'
  3     WHEN (is_number(a) = 1) then 'NUMBER'
  4     END AS WHAT,
  5     COUNT(*) FROM toto GROUP BY is_number(a);

WHAT     COUNT(*)
------ ----------
NUMBER          3
CHAR            2

CONNECT BY PRIOR : Requête hiérarchique

SQL> CREATE TABLE articles(
  2     categorie VARCHAR2(30),
  3     categorie_sup VARCHAR2(30)
  4  );

Table created.
 
SQL> INSERT INTO articles VALUES('articles',null);

1 row created.

SQL> INSERT INTO articles VALUES('véhicules','articles');

1 row created.

SQL> INSERT INTO articles VALUES('prêt-à-porter','articles');

1 row created.

SQL> INSERT INTO articles VALUES('moto','véhicules');

1 row created.

SQL> INSERT INTO articles VALUES('automobile','véhicules');

1 row created.

SQL> INSERT INTO articles VALUES('fazer fz6','moto');

1 row created.

SQL> INSERT INTO articles VALUES('honda hornet 600','moto');

1 row created.

SQL> INSERT INTO articles VALUES('kawazaki z1000','moto');

1 row created.

SQL> INSERT INTO articles VALUES('peugeot 306','automobile');

1 row created.

SQL> INSERT INTO articles VALUES('chaussures','prêt-à-porter');

1 row created.

SQL> SELECT lpad(' ',2*level)||categorie as "ARTICLES"
  2  FROM articles
  3  CONNECT BY PRIOR categorie=categorie_sup
  4  START WITH categorie = 'articles'
  5  ;

ARTICLES
----------------------------
  articles
    prêt-à-porter
      chaussures
    véhicules
      automobile
        peugeot 306
      moto
        fazer fz6
        honda hornet 600
        kawazaki z1000

10 rows selected.


La fonction Oracle SYS_CONNECT_BY_PATH permet d'afficher la concaténation du chemin hiérarchique pour chaque élément listé. Bien pratique pour visualiser le contenu de la table..
  
SQL> SELECT SYS_CONNECT_BY_PATH(categorie, ' * ')
  2  FROM articles
  3  CONNECT BY PRIOR categorie=categorie_sup
  4  START WITH categorie = 'articles'
  5  ;

SYS_CONNECT_BY_PATH(CATEGORIE,'*')
--------------------------------------------------
 * articles
 * articles * prêt-à-porter
 * articles * prêt-à-porter * chaussures
 * articles * véhicules
 * articles * véhicules * automobile
 * articles * véhicules * automobile * peugeot 306
 * articles * véhicules * moto
 * articles * véhicules * moto * fazer fz6
 * articles * véhicules * moto * honda hornet 600
 * articles * véhicules * moto * kawazaki z1000

10 rows selected.

ROWID : Détecter puis supprimer les doublons d'une table

Problème classique : détection puis suppression de doublons.


SQL> CREATE TABLE toto(code_pays CHAR(3), ville VARCHAR2(30));

Table created.

SQL> INSERT INTO toto VALUES ('FRA','PARIS');

1 row created.

SQL> INSERT INTO toto VALUES ('FRA','LYON');

1 row created.

SQL> INSERT INTO toto VALUES ('FRA','LYON');

1 row created.

SQL> INSERT INTO toto VALUES ('USA','DENVER');

1 row created.

SQL> SELECT code_pays, ville, COUNT(*) FROM toto GROUP BY code_pays, ville HAVING COUNT(*) > 1;

COD VILLE                            COUNT(*)
--- ------------------------------ ----------
FRA LYON                                    2

SQL> DELETE FROM toto a WHERE ROWID > (SELECT MIN(b.rowid) FROM toto b WHERE a.code_pays=b.code_pays AND a.ville=b.ville);

1 row deleted.

SQL> SELECT code_pays, ville, COUNT(*) FROM toto GROUP BY code_pays, ville HAVING COUNT(*) > 1;

no rows selected.

Cas où l'une des colonnes contient des valeurs nulles parmi les lignes en doublons

Exemple :

SQL> INSERT INTO toto VALUES (null,'PARIS');

1 row created.

SQL> INSERT INTO toto VALUES (null,'PARIS');

1 row created.

SQL> INSERT INTO toto VALUES (null,'PARIS');

1 row created.

SQL> SELECT code_pays, ville, COUNT(*) FROM toto GROUP BY code_pays, ville HAVING COUNT(*) > 1;

COD VILLE                            COUNT(*)
--- ------------------------------ ----------
    PARIS                                   3

SQL> DELETE FROM toto a WHERE ROWID > (
  2  SELECT MIN(b.rowid) FROM toto b
  3  WHERE a.code_pays=b.code_pays AND a.ville=b.ville
  4  );

0 rows deleted.

SQL> DELETE FROM toto a WHERE ROWID > (
  2  SELECT MIN(b.rowid) FROM toto b
  3  WHERE (a.code_pays=b.code_pays AND a.ville=b.ville)
  4  OR (
  5    a.code_pays IS NULL AND b.code_pays IS NULL
  6    AND a.ville=b.ville
  7  )
  8  );

2 rows deleted.

LONG et LOB : Recherche de texte dans le corps des vues

La recherche d'une chaine de caractère dans le corps des vues peut être utile par exemple pour déterminer toutes les vues qui utilisent un db_link.
 
Or, le corps de la vue étant codé en long, il est impossible de l'afficher avec un select. On passe donc par une conversion en LOB dans une table intermédiaire.


Exemple :

SQL> CREATE FORCE VIEW v_test AS SELECT * FROM all_users@dblink;

Warning: View created with compilation errors.

SQL> SELECT * FROM user_views WHERE text LIKE '%@%';
SELECT * FROM user_views WHERE text LIKE '%@%'
                               *
ERROR at line 1:
ORA-00932: inconsistent datatypes: expected NUMBER got LONG

SQL> CREATE TABLE toto AS SELECT view_name, TO_LOB(text) AS text FROM user_views;

Table created.

SQL> SELECT * FROM toto WHERE text LIKE '%@%';


VIEW_NAME                      TEXT
------------------------------ -------------------------------
V_TEST                         SELECT * FROM all_users@dblink
 
On parvient ainsi à identifier une vue ayant un db_link dans sa définition.

ROWNUM ou ROW_NUMBER() : intervalle de lignes triées

Lorsque l'on affiche les résultats d'une requête dans une interface de logiciel, on peut avoir besoin de gérer la pagination, et donc le découpage du résultat par lots de lignes (ou intervalles). Pour ce faire, il faut retrouver les lignes n° tant à tant la plupart du temps au sein d'un ensemble trié.


Exemple :

On créé une table et des lignes avec valeurs insérées dans le désordre :
 
SQL> CREATE TABLE toto(id CHAR(1));

Table created.
 
SQL> INSERT INTO toto VALUES ('e');

1 row created.

SQL> INSERT INTO toto VALUES ('a');

1 row created.

SQL> INSERT INTO toto VALUES ('d');

1 row created.

SQL> INSERT INTO toto VALUES ('c');

1 row created.

SQL> INSERT INTO toto VALUES ('b');

1 row created.
 
On sélectionne ces lignes que l'on commence par trier suivant la colonne id, pour ne récupérer que celles qui ressortent de ce tri qu'entre la 2ème et 4ème position.

SQL> SELECT * FROM (
  2      SELECT ROWNUM num, id FROM (
  3          SELECT id FROM toto ORDER BY id
  4      )
  5  )
  6  WHERE num BETWEEN 2 AND 4;

       NUM I
---------- -
         2 b
         3 c
         4 d
 
On retrouve bien les valeurs b, c et d (ordre et intervalle recherchés). Ce qu'il faut retenir c'est que le tri doit être déjà fait avant de pouvoir sélectionner un intervalle par ROWNUM.

On peut également utiliser la fonction analytique ROW_NUMBER() OVER pour obtenir le même résultat.

Exemple :

SQL> SELECT *  FROM (
  2      SELECT ROW_NUMBER() OVER (ORDER BY id) num, id FROM toto
  3  )
  4  WHERE num BETWEEN 2 AND 4;

       NUM I
---------- -
         2 b
         3 c
         4 d