Vous êtes sur la page 1sur 260

Chapitre 1

Base de données et Système de gestion


de base de données

1.1 Qu’est-ce qu’une base de données (BD)


Une base de données peut être vue comme le besoin de mémoriser de façon durable des données et de
pouvoir exprimer le plus précisément possible les relations qu’entretiennent ces données.

Une fois cette représentation faite il est nécessaire d’associer des fonctionnalités (programmes et des
requêtes) à cette base de données afin de pouvoir l’exploiter le plus facilement possible.

Toutes les personnes exploitant la même base de données n’ont pas la même fonction et n’ont donc
pas forcément besoin de voir les mêmes informations ou d’appliquer les mêmes actions à la base de
données. Les systèmes des privilèges, des vues et des programmes stockés permettent de délimiter
rigoureusement ces différentes visions d’une même base de données (chaque vision est nommée schéma
externe).

Enfin, plusieurs utilisateurs peuvent appliquer simultanément des modifications à la même base de
données, il est alors nécessaire d’utiliser des techniques d’isolation et de synchronisation afin de garantir
la cohérence de ces modifications.

1.2 Qu’est-ce qu’un système de gestion de base de données (SGBD)


Un SGBD est la structure d’accueil d’une ou plusieurs bases de données : il offre les outils nécessaires
à la mise en place d’une base de données. On pourrait comparer le SGBD au système d’exploitation
et la base de données à un programme d’application utilisant les services du système.

Voici quelques-unes des caractéristiques d’un SGBD :


– Capacité de gérer des données persistantes et structurées.
– Capacité à gérer, autant que possible, la sémantique des données et à garantir des propriétés (les
contraintes, assertions, domaines des attributs, triggers et procédures stockées)
– Pouvoir manipuler facilement et efficacement de très grand volumes de données.
– Permettre l’exécution de transactions concurrentes par un ou plusieurs utilisateurs tout en conser-
vant les propriétés de la BD.
– Assurer la sécurité des données :
– contrôler les accès en fonction de droits accordés aux différents utilisateurs.
– tolérer les pannes logicielles ou matérielles grâce à des procédures de reprise.
– Procurer l’indépendance physique : le SGBD permet de manipuler les données indépendemment
de leurs implantations matérielles.
– Procurer l’indépendance logique : chaque utilisateur ne voit de la base que les données qui lui
sont nécessaires (schéma externe).

1
2 CHAPITRE 1. BASE DE DONNÉES ET SYSTÈME DE GESTION DE BASE DE DONNÉES

– Le cœur d’un SGBD est le modèle de données qu’il supporte, c’est à dire la manière d’organiser
les données qu’il offre. Le modèle actuellement le plus utilisé est le relationnel inventé dans les années
1970 dont une belle qualité est probablement la symétrie naturelle qu’il offre dans les associations
inter-données. Il existe d’autres modèle de données : hiérarchique, réseau et objet, qui eux ne sont
pas franchement symétriques.
– Fournir un langage de haut niveau adapté au modèle : SQL pour le modèle relationnel, CODASYL
pour le modèle réseau, OQL pour le modèle objet.
– Exemples de SGBD relationnels : Oracle, PostgreSQL, MySQL, Access et plein d’autres !

1.3 Les modèles de données


Un modèle de données est un formalisme permettant de :
– décrire les données (organisation, typage, ...)
– manipuler ces données.
Les deux principaux modèles :
Modèles à accès purement associatif Ce sont :
Relationnel années 1970, SQL1 1987, SQL2 1992
Déductif année 1980-1990, calcul des prédicats logiques du premier ordre, par exemple DATA-
LOG (à la Prolog)
La manipulation des données est déclarative : le programmeur n’a pas à se soucier du comment
mais seulement du quoi, par exemple : je veux la liste des clients dont les soldes sont positifs, je
n’ai pas à dire comment faire pour obtenir cette liste, c’est le SGBD qui s’en charge.
Modèles à accès Navigationnel Ce sont :
Fichiers avec chaı̂nage programme APOLLO 1965,
Hiérarchique fin des années 1960, utilistation de pointeurs permettant la navigation
Réseaux fin des années 1960, COSET
Orienté Objet années 1980-1990 (O2)
La manipulation des données est procédurale : en plus du quoi, le programmeur doit se préoccuper
du comment, par exemple : tant qu’il reste au moins un client, mettre le prochain client dans la
liste si son solde est positif.
Modèles hybrides On trouve des modèles hybrides qui disposent d’accès associatif et navigationnel :
le relationnel-objet (SQL3 1999, Oracle, PostgreSQL).

1.4 Les niveaux d’abstraction


Pour assurer l’indépendance logique et l’indépendance physique, le groupe ANSI/X3/SPARC a défini
en 1975 trois niveaux de description d’une base de données :
– Des schémas externes donnent différentes vues d’un même schéma conceptuel, chacun étant ap-
proprié à un type d’utilisateur (SQL introduit la notion de vue et de privilège).
– le schéma conceptuel, à ce niveau on définit la structuration et le typage des données. C’est le
domaine du concepteur de la base.
– le schéma interne qui définit les paramètres de stockage, les index favorisant certains accès, . . .C’est
le domaine de l’administrateur/optimiseur.
Ce niveau est le dernier avant la représentation physique des données sur disque et en mémoire
centrale et qui est à la charge du SGBD.

1.5 Schéma et instances


Dans une BD, il y a un schéma et des données.
Le schéma d’une BD est le résultat de la conception (par exemple le MCD de Merise) qui décrit
l’organisation des données. Un schéma n’est pas destiné à être modifié (ou bien rarement).
1.6. LES DIFFÉRENTS LANGAGES CÔTÉ SERVEUR 3

Une instance d’un schéma correspond aux données stockées dans la base à un moment donné. Les
données d’une instance respectent évidemment l’organisation imposée par le schéma. Le contenu d’une
BD est éminemment variable : chaque modification de la BD produit une nouvelle instance du schéma
correspondant.
Exemple :
1. soit le schéma relationnel : Personne (Nom, Prénom), et deux instances possibles de ce schéma :
DURAND Gaston
LAGAFFE Gaston
DUPOND Jules et
PERSONNE Paul
LAGAFFE Gaston
2. le même schéma avec un modèle objet (ici ODL de l’ODMG) :
class Personne (extent lesPersonnes key Nom) {
attribute string Nom ;
attribute string Prénom ;
}

Le mot clef extent introduit le nom de la collection qui contiendra les objets Personne.
3. le même schéma en SQL :
create table Personne (
Nom Varchar2 (20) primary key,
Prenom Varchar2 (20)
) ;

Ici Personne représente à la fois le schéma de relation et la variable contenant l’instance.

1.6 Les différents langages côté serveur


1.6.1 DDL : Data Definition Language
Pour définir/modifier les schémas externes et le schéma conceptuel
– par exemple, pour le modèle relationnel, SQL propose
create table Diplome (
id Number (5),
mention Varchar (20),
constraint Diplome_PK primary key (id)
) ;

create table Etudiant (


id Number (5),
nom Varchar (20),
prenom Varchar (20),
constraint Etudiant_PK primary key (id)
) ;

Modification du schéma qui ajoute une colonne aux étudiants :


alter table Etudiant
add (mon_diplome Number (5))
add (constraint Etudiant_Diplome_FK
foreign key (mon_diplome) references Diplome (id)) ;

Enrichissement du schéma avec la vue Effectifs donnant le nombre d’étudiants par diplôme :
4 CHAPITRE 1. BASE DE DONNÉES ET SYSTÈME DE GESTION DE BASE DE DONNÉES

create view Nb_Homonymes (Nom, Nombre_D_Etudiants_Portant_Ce_Nom) as


select e.nom, count (*)
from Etudiant e
group by e.nom ;

create view Effectifs (id, mention, nb_etudiants) as


select d.id, d.mention, count (e.id)
from Diplome d
left outer join Etudiant e on e.mon_diplome = d.id
group by d.id, d.mention ;

Bien que recalculées à chaque sollicitations, certaines vues sont comme des tables (on peut y ajouter,
modifier et supprimer des lignes, ces modifications étant en fait reportées par le SGBD sur les tables
sous-jacentes, chapitre 10).
– par exemple, pour le modèle objet, la norme ODMG propose ODL (Object Definition Language).

1.6.2 DML : Data Manipulation Language


Permet de modifier le contenu de la base (insertion, mises à jour, suppression de données) et d’inter-
roger la base (langage de requête).
– par exemple, pour le modèle relationnel, SQL propose les instructions insert, update, delete et
la requête select.
– par exemple, pour le modèle objet, la norme ODMG propose OQL (Object Query Language) et
OML (Object Manipulation Language).

1.6.3 DCL : Data Control Language


Pour gérer les utilisateurs et leurs privilèges.
Par exemple en SQL Oracle :
CREATE USER ...
DROP USER ...
GRANT ...

1.7 L’Architecture Client/Serveur


Très souvent le SGBD tourne sur une machine serveur plus ou moins dédiée, par contre les applicatifs
client tournent sur d’autres machines et doivent se connecter au SGBD via le réseau.

Il faut donc distinguer clairement entre ce qui doit tourner sur le serveur et ce qui doit tourner sur le
client.

1.7.1 Le code exécuté par le SGBD (le serveur)


Les ordres SQL
Les triggers réflexes déclenchés lors d’une modification des données, pour vérifier des contraintes
complexes, ou pour rendre la base de données plus autonome (langage : PL/SQL d’Oracle, ou
PLPGSQL de Postgres qui ressemblent tous deux fortement à Ada).
Les procédures stockées pour écrire des traitements complexes n’ayant de sens que s’ils sont menés
jusqu’à leur terme, par exemple une opération de virement d’un compte à un autre qui nécessite
deux opérations de mise à jour successives (2 update) (langage : PL/SQL d’Oracle, ou PLPGSQL
de Postgres qui ressemblent tous deux fortement à Ada).
Les méthodes des objets pour un SGBD orienté objet ou relationnel-objet.
1.8. LE CODE APPLICATIF EXÉCUTÉ CÔTÉ SERVEUR ET/OU CLIENT 5

Les SGBD proposent souvent leur propre langage de programmation : PL/SQL pour Oracle, PL/pgSQL
pour PostgreSQL et le langage de MySQL.

1.8 Le code applicatif exécuté côté serveur et/ou client


Ce code est en général écrit dans un langage hôte : ce sont des langages classiques (Cobol, C, Ada,
Java, . . .) qui permettront d’écrire une application cliente complète, ou du code destiné à être exécuté
par le serveur.

Il y a deux possibilités pour utiliser le SGBD à partir d’un langage hôte :


API La première possibilité est de fournir une API plus ou moins spécifique au SGBD (ODBC, JDBC,
libpq pour C de Postgres, OCI pour Oracle, . . .), il suffit d’utiliser les primitives de l’API dans
un programme traditionnel.
SQL embarqué La seconde, de loin la plus agréable, repose sur une extension du langage hôte
permettant d’écrire et d’exploiter très naturellement des ordres du SGBD (des ordres SQL par
exemple, et on parle alors de SQL embarqué ou embedded SQL). Le programme obtenu doit être
traité par un préprocesseur, en général fourni par l’éditeur du SGBD, qui, entre autres choses,
remplace les ordres embarqués par des appels à une API spécifique. Le nouveau programme
obtenu est écrit dans le langage hôte d’origine et contient des appels à une API, on est alors
ramené à la première possibilité.
Exemples de préprocesseurs :
– Oracle : Pro*C/C++, Pro*COBOL, SQLJ,
– Postgres : ecpg,
– le projet GNADE : SQL embarqué dans du Ada 95, avec des API ODBC, PostgreSQL et
MySQL
Avec le développement de l’accès à des bases de données via le réseau Internet, de nombreux envi-
ronnements normalisés ou non existent. Par exemple l’environnement Hibernate qui tend à rendre
transparent au programmeur la persistance des objets stockés dans une base de données.
Première partie

Relationnel et SQL

6
Chapitre 2

Le modèle relationnel et SQL

Inventé par E.F. Codd en 1970, chez IBM.

Ce modèle est lié à la théorie des ensembles (unicité des éléments, sous-ensemble, produit cartésien, . . .)

Une de ses réalisations pratiques : SQL (Structured Query Language).

Historique
– 1970, Codd invente l’algèbre relationnelle,
– 1972 à 1975 IBM invente SEQUEL puis SEQUEL/2 en 1977 pour le prototype SYSTEM-R de
SGBD relationnel
– SEQUEL donne naissance à SQL
– Parallèlement, Ingres développe le langage QUEL en 1976
– Dès 1979, Oracle utilise SQL
– 1981, IBM sort SQL/DS
– 1983, IBM sort DB2 (héritier de SYSTEM-R) qui fournit SQL.
– 1982, l’ANSI (organisme de normalisation américain) commence la normalisation de SQL qui aboutit
en 1986 et donne la norme ISO en 1987
– Une nouvelle norme SQL-89
– Puis la norme SQL-92 (ou SQL2) qui est la plus utilisée,
– Puis la normalisation SQL-99 (ou SQL3) avec, entre-autres, les extensions relationnel-objet, qui
n’est pas encore terminée !

2.1 Qu’est-ce qu’un ensemble


Un ensemble est une collection d’éléments de même nature. Par exemple l’ensemble des entiers négatifs,
ensemble des caractères, des voyelles, des mots de la langue françaises.
Définition d’un ensemble :
– par extension (ou énumération) : on explicite chaque élément, par exemple l’ensemble des voyelles :
{a, e, i, o, u, y}.
L’ordre des éléments n’a aucune importance : {a, e, i} = {i, a, e}.
Unicité de chaque élément apparaissant dans un ensemble, contre-exemple : {a, e, i, a} n’est pas un
ensemble.
L’ensemble vide : {} = ∅
– par intention (ou caractérisation) : on définit la ou les propriétés vérifiées par chaque élément de
l’ensemble et seulement les éléments de l’ensemble. Par exemple l’ensemble des entiers naturels
pairs :{x|x = 2p, p ∈ N }
En SQL on parle plutôt de domaine que d’ensemble, par exemple Varchar (20) est l’ensemble de
toutes les chaı̂nes de caractères de longueurs inférieures ou égales à 20 et, en Oracle, Number (5, 2)
est l’ensemble des nombres positifs ou négatifs pouvant s’exprimer avec 5 chiffres décimaux dont 2
après la virgule.

7
8 CHAPITRE 2. LE MODÈLE RELATIONNEL ET SQL

2.2 Notion centrale : schéma et valeur d’une relation


Le schéma d’une relation exprime comment est constituée une relation : le nombre d’attributs par
n-uplet, un nom différent pour chaque attribut et, pour chaque attribut, le domaine dans lequel il
prend ses valeurs. Par exemple :
schéma : Etudiant (NumCarte : Entier ; Nom : Chaine ; Note : Entier)
Le nombre d’attributs du schéma s’appelle son arité, le schéma Etudiant a une arité de 3.
La valeur d’une relation est un sous-ensemble du produit cartésien des domaines de son schéma (un
domaine est un ensemble de valeurs, par exemple l’ensemble des chaı̂nes de caractères, l’ensemble des
couleurs primaires, l’ensemble des notes de 0 à 20, l’ensemble des mentions de diplômes délivrés par
l’USTL, . . .).
Voici un exemple de valeur d’une relation :
NumCarte Nom Note
(122.678.555, Toto, 12)
(123.678.555, Truc, 10)
(213.678.555, Bidule, 15)

qui est bien un sous-ensemble du produit cartésien : Entier × Chaı̂ne × Entier.


Chaque ligne de la relation est un n-uplet1 dont l’ordre des attributs est fixé par le schéma. Dans
l’exemple, la première valeur de chaque n-uplet est le numéro de carte d’un étudiant, la deuxième son
nom, la troisième sa note. Chaque n-uplet représente un étudiant.
Une variable relationnelle contient une valeur de relation, la variable et la valeur ont évidemment avoir
le même schéma de relation. L’exemple précédent pourrait être désigné par la variable relationnelle
p2006.
On pourrait faire une analogie avec les langages de programmation : un schéma relationnel ressemble
à un type de donnée (Natural en Ada, boolean en Java) une variable relationnelle ressemble à une
variable qui est d’un type fixé lors de sa déclaration (N : Natural en Ada, boolean found en Java).

2.2.1 Schéma ou intention d’une relation


Par exemple voici la relation Ville :
schéma : Ville (Id : Entier, Nom : Chaine, Departement : 1..100, Population : Naturel)

SQL
En Oracle 10 :
create table Ville (
id Number (5),
nom Varchar2 (50),
departement Number (3),
population Number (10),
constraint Ville_PK primary key (id),
constraint Ville_Dpt_Intervalle check (departement between 1 and 100),
constraint Ville_Pop_Val check (0 <= population)
) ;

Cet ordre create crée la table Ville dont le schéma, décrit entre les parenthèses, est composé de
quatre attributs et comporte aussi des contraintes permettant de garantir les propriétés :
– constraint Ville_PK primary key (id) garantit que deux lignes de Ville auront toujours une
valeur définie et différente pour la colonne id. De façon plus consise on dit que id est la clef primaire
de Ville. La tentative d’ajouter dans la table Ville une ville dont id existe déjà dans une ligne de
Ville échouera et la valeur de Ville sera inchangée.
1
Ici on a affaire à des 3-uplet.
2.2. NOTION CENTRALE : SCHÉMA ET VALEUR D’UNE RELATION 9

– constraint Ville_Dpt_Intervalle check (departement between 1 and 100) garantit que que
la colonne departement aura une valeur comprise entre 1 et 100 si elle est définie. La tentative
d’ajouter dans la table Ville une ville dont departement vaut 105 échouera et la valeur de Ville
sera inchangée.
– constraint Ville_Pop_Val check (0 <= population) garantit que que la colonne population
aura une valeur positive ou nulle quand elle est définie : la tentative d’ajouter dans la table Ville
une ville à population négative échouera et la valeur de Ville sera inchangée.
Une table SQL ressemble à une variable relationnelle mais avec quelques différences dont la première
est importante :
– la valeur d’une variable relationnelle ne peut pas comporter plusieurs fois le même n-uplet alors
qu’une table — sauf si on pose explicitement une contrainte de clef primaire — peut comporter
plusieurs lignes identiques,
– un élément d’une relation s’appelle un n-uplet, alors qu’un élement d’une table s’appelle une ligne
(ou row en anglais).
– il est possible en SQL qu’une colonne n’ait pas de valeur, on dit qu’elle est indéfinie et cela se teste
avec l’opérateur booléen is null. En revanche cela n’aurait pas de sens pour une relation car cela
correspondrait à un n-uplet auquel il manque un attribut, ce qui n’aurait pas de sens en théorie.

2.2.2 Contenu ou instance ou extension d’une relation


L’extension d’une relation est un sous-ensemble du produit cartésien D1 × D2 × . . . × Dk .
Les membres (ou éléments) d’une relation sont appelés nuplets (k-uplets).

SQL

Plusieurs façons d’ajouter une ville dans la table Ville en Oracle 10 :


– insert into Ville values (1, ’Lille’, 59, 222400) ;

Dans cette forme on doit donner une valeur à chaque colonne dans l’ordre dans lequel sont déclarées
les colonnes.
– insert into Ville (id, Departement, Nom , Population)
values ( 2, 75, ’Paris’, 2200000) ;

Ici on voit qu’en explicitant les noms des colonnes on peut utiliser un autre ordre.
– insert into Ville (Nom, id) values (’Paris-Texas’, 5) ;

Enfin, en explicitant les colonnes à initialiser on peut n’en donner qu’un sous-ensemble, les colonnes
non mentionnées seront indéfinies (is null).

2.2.3 Schéma et extension


Souvent on représente par un seul tableau à la fois le schéma et une instance possible de la relation :

Id Nom Departement Population


1 Lille 59 222.400
7 Dunkerque 59 222.400
2 Paris 75 2.200.000
5 Paris-Texas
12 Lyon 69 420.000

Les colonnes blanches ou vides de Paris-Texas correspondent à des colonnes indéfinies.


Q. 1 Combien d’éléments ou lignes contient le produit cartésien du tableau précédent avec lui-même ?
10 CHAPITRE 2. LE MODÈLE RELATIONNEL ET SQL

2.3 Clef d’une relation


Une clef candidate C d’une relation R est un sous-ensemble minimal d’attributs de R qui déterminent
les autres attributs de R, c’est à dire que pour une valeur donnée de C, les autres attributs ont exac-
tement une valeur.

Par exemple le numéro de carte d’étudiant détermine le nom de l’étudiant et certainement d’autres
informations.

Autrement, dit une valeur de C apparaı̂t au plus une fois dans toute extension de R.

Une relation peut posséder plusieurs clefs candidates, on en choisira une qu’on appellera clef primaire.

Par exemple : Etudiant (num_carte, num_insee, nom, prénom, datenaiss) pourrait posséder
deux clefs candidates : (num_carte) qui doit être différent pour chaque étudiant et (num_insee) qui
identifie la naissance d’une personne et est censée être unique pour chaque personne née en France.
On peut choisir (num_carte) comme clef primaire.
Q. 2 Quel problème se poserait si on choisissait (num carte, nom) comme clef primaire d’un étudiant ?

En SQL, la clef primaire fait l’objet d’une contrainte primary key, les autres clefs candidates peuvent
faire l’objet d’une contrainte d’unicité (unique).
En Oracle ainsi qu’en PostgreSQL, aucune des colonnes d’une clef primaire ne peut être indéfinie (is
null).

2.4 Clef étrangère


Une clef étrangère est constituée d’une ou plusieurs colonnes et permet de désigner au plus une ligne
d’une autre table ou de la même table.
Une clef étrangère peut être interprétée comme un pointeur associatif vers une ligne d’une autre table
ou de la même table. Les colonnes de l’autre table correspondant à celles de la clef étrangère doivent
être la clef primaire complète de cette table ou constituer complètement les colonnes d’une contrainte
d’unicité.
Associatif signifie que pour retrouver la ligne référencée on recherche dans l’autre table la ligne dont les
colonnes de la clef primaire ou de la contrainte d’unicité sont égales à celles de la ligne référençante (cela
peut heureusement se faire efficacement grâce aux index associées aux clefs primaires et contraintes
d’unicité).

Par exemple une fête référence la ville dans laquelle elle se passe en mentionnant en tant que clef
étrangère le numéro de département et le nom de la ville dans ce département (deux villes de deux
départements différents pouvant porter le même nom) :

create table Ville ( create table Fete (


departement Number (3), <--- departement Number (3),
nom Varchar (20), <--- nom Varchar (20),
primary key (departement, nom) id Number (10),
) jour Date,
primary key (id),
foreign key (departement, nom)
-- | |
-- V V
references Ville (departement, nom)
)

L’ordre des colonnes est bien entendu important dans la déclaration de la contrainte foreign key.
2.5. L’ALGÈBRE RELATIONNELLE ET LE LANGAGE DE REQUÊTE SQL 11

Une clef étrangère comportant une colonne indéfinie ne désigne aucune ligne, sinon le SGBD (Oracle,
PostgreSQL et MySQL avec InnoDB) garantit que la ligne désignée existe, sinon l’ordre échoue.
Par défaut, une ligne référencée par une clef étrangère ne peut pas être détruite, d’autres comporte-
ments peuvent être spécifiés grâce à des options de déclaration de clef étrangère, par exemple si une
ligne référencée est détruite on peut demander que les lignes référençantes le soient aussi.

2.5 L’algèbre relationnelle et le langage de requête SQL


2.5.1 Préliminaire : l’identité
En notation relationnelle, il suffit de mentionner le nom de la relation, par exemple R, et on a alors
accès implicitement à sa valeur (son extension), exactement comme lorsqu’on mentionne la variable x
dans une expression arithmétique.

En SQL il faut par contre écrire la requête suivante pour exprimer le contenu d’une table :
select * from Ville ;

Tous les nuplets de la table Ville sont alors affichés.


Ou, si on veut garantir l’unicité de chaque nuplet affiché :
select distinct * from Ville ;

Q. 3 Si Ville possède une clef primaire, le distinct est-il utile dans la requête précédente ?

2.5.2 Les opérateurs de base


La projection : SELECT

Pour ne conserver que certaines colonnes.

ΠAp1 ,...,Apk (R) = {(xp1 , . . . , xpk ) | ∃(y1 , . . . , yn ) ∈ R, xpi = ypi ∀i ∈ [1, k]}

Par exemple :
 
Id Nom Dpt Population
Dpt Population
1 Lille 59 222.400
 
59 222.400
 
21 Gruson 59 5.000
 
 
  59 5.000
ΠDpt,P opulation  7 Dunkerque 59 222.400 =
  75 2.200.000

 2 Paris 75 2.200.000 

5 Paris-Texas
 
69 420.000
 
12 Lyon 69 420.000

Remarquer l’unicité des n-uplets du résultat.


En SQL, c’est la clause select de la requête qui exprime la projection. Le qualificatif distinct permet
d’obtenir l’unicité des lignes du résultat (distinct porte sur toutes les colonnes de la projection) :
select distinct v.Departement, v.Population from Ville v ;

Si on ne met pas distinct, les doublons éventuels sont conservés :


select v.Departement, v.Population from Ville v ;
12 CHAPITRE 2. LE MODÈLE RELATIONNEL ET SQL

La restriction : WHERE
Pour ne conserver que les nuplets vérifiant le prédicat P .

σP (R) = {(x1 , . . . , xk ) | (x1 , . . . , xk ) ∈ R ∧ P (x1 , . . . , xk )}

Par exemple, on veut les villes du nord :

Id Nom Dpt Population


 
 1 Lille 59 222.400 
  Id Nom Dpt Population

7 Dunkerque 59 222.400 
σDpt=59  = 1 Lille 59 222.400
 
2 Paris 75 2.200.000
7 Dunkerque 59 222.400
 
 
 5 Paris-Texas 
12 Lyon 69 420.000

En SQL, c’est la clause where de la requête qui exprime la restriction :


select * from Ville v
where v.Departement = 59 ; -- prédicat de la restriction

Le symbole * indique qu’il n’y a pas de projection (on retient toutes les colonnes).

L’union : UNION
R et S sont deux relations de même schéma.

R ∪ S = {(x1 , . . . , xk ) | (x1 , . . . , xk ) ∈ R ∨ (x1 , . . . , xk ) ∈ S}

Une requête select peut être utilisée comme une table, on peut donc avoir des emboı̂tements de
requêtes.
1. La requête ensembliste (sans doublons) :
select nom, ’Etudiant’ as categorie from Etudiant
Union
select nom, ’Enseignant’ as categorie from Enseignant ;

2. ou, si on souhaite conserver les boublons :


select nom, ’Etudiant’ as categorie from Etudiant
Union All
select nom, ’Enseignant’ as categorie from Enseignant ;

Lors d’une instruction insert il est possible d’ajouter 0, 1 ou plusieurs lignes d’un coup à condition
de remplacer la clause values par une requête, par exemple :
create table Ville_Du_Nord (
id Number (5),
nom Varchar2 (50),
constraint Ville_Du_Nord_PK primary key (id)
) ;

insert into Ville_Du_Nord


select v.id, v.nom from Ville v where v.departement = 59 ;

Q. 4 Utiliser cette technique pour éviter d’utiliser l’opérateur d’union dans les requêtes 1 et 2.
2.5. L’ALGÈBRE RELATIONNELLE ET LE LANGAGE DE REQUÊTE SQL 13

La différence : MINUS
R et S sont deux relations de même schéma.

R − S = {(x1 , . . . , xk ) | (x1 , . . . , xk ) ∈ R ∧ (x1 , . . . , xk ) 6∈ S}

Les villes dont le département est connu :


select * from Ville MINUS select * from Ville where Departement is null ;

Q. 5 Écrire plus simplement la requête précédente.

Nouveau jeu de données (figure 2.1)

Fig. 2.1 – Un exemple de valeur de table avec deux clefs étrangères etu et mat dans la table Note.

Table Etudiant  Table Note


Etudiant.id←etu  note mat →Matiere.id
Table Matiere
nom id
id nom coeff
Alfred 1 1 12 1
1 BD 3
Marc 2 1 14 2
2 CL 5
Julie 3 3 15 2

Le produit cartésien : CROSS JOIN


Le produit cartésien est une fonction binaire dont les deux opérandes sont des ensembles quelconques
et la valeur est l’ensemble des couples formés d’un élément du premier opérande et d’un élément du
second opérande. Exemple : {b, f } × {e, i, o} = {(b, e), (b, i), (b, o), (f, e), (f, i), (f, o)}.
Dans un couple (ou 2-uplet) l’ordre des éléments est important : (b, e) 6= (e, b).
Autre exemple : le produit cartésien de l’ensemble des étudiants de licence GMI avec l’ensemble des
UE de licence GMI.

R × S = {(r1 , . . . , rkr , s1 , . . . , sks ) | (r1 , . . . , rkr ) ∈ R ∧ (s1 , . . . , sks ) ∈ S}

Tous les couples étudiant, matière (Oracle10, Postgres, SQL92) :


select *
from Etudiant etu
cross Join Matiere mat ;
on obtient 3 × 2 nuplets.
En Oracle 8 on devait écrire (et en général en SQL on peut écrire) le produit cartésien comme ceci :
-- Oracle8, Postgres, SQL92
select *
from Etudiant, Matiere ;

Si on ne veut afficher que la partie Etudiant de chaque élément du produit cartésien, on peut préfixer
* avec le nom de la table ou son alias :
select etu.*
from Etudiant etu
cross Join Matiere mat ;

Q. 6 Sous quelle condition les deux requêtes suivantes ont-elle la même valeur, sous quelle condition
ont-elle des valeurs différentes ?
14 CHAPITRE 2. LE MODÈLE RELATIONNEL ET SQL

select * from Etudiant ;

select distinct etu.*


from Etudiant etu cross join Matiere mat ;

2.5.3 Quelques opérateurs supplémentaires


Ils peuvent s’exprimer grâce aux opérateurs de base vus précédemment et ne sont donc théoriquement
pas insdispensables, mais ils sont tellement pratiques qu’à la fois le relationnel et SQL leur attribuent
une identité.

La jointure, produit cartésien et restriction : ... INNER JOIN ... ON <condition>


Elle permet de ne conserver que les éléments pertinents d’un produit cartésien.

R ⊲⊳P S = σP (R × S)

où P exprime la condition de conservation d’un élément du produit cartésien.


Par exemple les couples (étudiant, matière) si l’étudiant a une note dans cette matière en se basant
sur les contenu des tables de la figure 2.1 page 13 :
select e.nom as etudiant, m.nom as matiere
from Etudiant e
cross join Note n
cross join Matiere m
where e.id = n.etu
and n.mat = m.id ;

SQL2, PostgreSQL et Oracle 10 (et d’autres bien entendu) disposent d’un opérateur de jointure
spécifique <table> inner join <table> on <condition>. La requête précédente peut alors être
réécrite plus clairement en :
select e.nom as etudiant, m.nom as matiere
from Etudiant e
inner join Note n on e.id = n.etu
inner join Matiere m on n.mat = m.id ;

Alfred BD 12
Alfred CL 14
Julie CL 15

Q. 7 Pourquoi Marc n’apparaı̂t-il pas dans le résultat ?


Le mot clef inner permet de distinguer cette jointure de la jointure dite externe (voir la section 2.12
page 30) qui, elle, utilise le mot clef outer.
En Oracle 8 on devait écrire :
select e.nom as etudiant, m.nom as matiere
from Etudiant e, Note n, Matiere m -- 1) produit cartésien
where e.id = n.etu -- 2) condition de
and n.mat = m.id ; -- la jointure

Q. 8 Que vaut la requête suivante ? Marc apparaı̂t-il ?

select e.nom, n.note


from Etudiant e inner join Note n on e.id != n.etu ;
2.5. L’ALGÈBRE RELATIONNELLE ET LE LANGAGE DE REQUÊTE SQL 15

L’opérateur != signifie différent et peut aussi se noter <>.

On distingue plusieurs cas particuliers de jointures

Équi-jointure Égalité entre colonnes : c’est probablement la plus courante, très souvent on teste
l’égalité entre la clef étrangère d’une table et la clef primaire d’une autre table. L’exemple précédent
est une équi-jointure.

Jointure naturelle : attention danger Équi-jointure de R et S sur les colonnes de mêmes noms.
En SQL92 et PostgreSQL on ajoute le mot clef natural.
La jointure naturelle est particulièrement dangereuse : supposons une application qui utilise la jointure
naturelle entre deux tables T1 et T2 . Si, plus tard, on ajoute à T1 et à T2 une colonne homonyme et de
même type alors ces deux colonnes participeront automatiquement à cette jointure naturelle, ce qui
n’est pas forcément ce que souhaite celui qui ajoute ces colonnes.

Auto-jointure Jointure d’une relation avec elle-même. Par exemple, les employés qui sont chef d’au
moins un autre employé :
select distinct chef.*
from Employe emp
inner join Employe chef on chef.id = emp.mon_chef ; -- équi-jointure

Non équi-jointure Le prédicat de la clause on d’une jointure n’est pas forcément une égalité :
toute condition peut convenir.

Grâce à l’ordre alter, on ajoute l’attribut sexe aux étudiants :


alter table Etudiant
add sexe Varchar2 (1)
default ’M’ -- valeur par défaut (discutable !)
check (sexe in (’M’, ’F’)) -- les 2 valeurs possibles
not null ; -- ne peut ^
etre indéfini

update Etudiant
set sexe = ’F’
where id = 3 ;
Q. 9 Écrire la requête qui donne tous les binômes mixtes d’étudiant et sans redondance : si on obtient
le binôme (Alfred, Julie) on ne doit pas obtenir aussi le binôme (Julie, Alfred).
Un autre exemple : on a une table F contenant des couples (x, y) d’une fonction y = f (x) définie sur
les entiers. On veut une requête contenant 0 lignes si la fonction stockée dans F est croissante (pour
tout couple de lignes (x1 , y1 ), (x2 , y2 ) vérifiant x1 < x2 on a f (x1 ) > f (x2 )) et contenant au mois une
ligne si elle est décroissante.
Q. 10 Pourquoi est-il logique que x soit la clef primaire de F ?

Q. 11 Écrire cette requête.

Q. 12 En utilisant la fonction count (voir section 2.8 page 21) modifier la requête précédente pour
qu’elle valle une seule ligne d’une colonne contenant le nombre de couple de lignes décroissant.

L’intersection

R et S sont deux relations de même schéma.


R∩S = {(x1 , . . . , xk ) | (x1 , . . . , xk ) ∈ R∧(x1 , . . . , xk ) ∈ S}
Oracle ne propose pas d’opérateur d’intersec-
tion, mais on peut la réaliser grâce à l’égalité : R ∩ S = R − (R − S)
16 CHAPITRE 2. LE MODÈLE RELATIONNEL ET SQL

La division
Le schéma de R englobe strictement celui de S, c’est à dire que R comporte toutes les colonnes de S
(mêmes noms et domaines) et a au moins une colonne en plus.

Soit CR l’ensemble des colonnes de R n’apparaissant pas dans S. La division est la projection sur CR
des groupes de lignes de R ayant la même valeur en CR et comportant toutes les lignes de S dans les
colonnes S.

R ÷ S = {ΠCR (x)|x ∈ R ∧ S ⊆ ΠS (σΠC (x)=ΠCR (y∈R) (R))}


R

Autrement dit ΠCR (x) appartient à la division si les lignes de R ayant ces valeurs couvrent toutes les
lignes de S.

A B C D
a b c d
a b e f C D A B
Par exemple : b c e f ÷ c d = a b
e d c d e f e d
e d e f
a b d e
La division peut s’exprimer grâce aux autres opérateurs :

R ÷ S = ΠA,B (R) − ΠA,B (ΠA,B (R) × S − R) (2.1)

En effet, ΠA,B (ΠA,B R × S − R) sont les nuplets qui n’appartiennent pas à la division.

Par exemple l’ensemble des étudiants qui sont inscrits à toutes les UE peut être calculé en divisant
la jointure des étudiants avec leurs inscriptions par la table UE projetée sur sa colonne id. Mais
comme SQL ne dispose pas d’opérateur de division, on est obligé de s’y prendre autrement en utilisant
l’égalité (2.1).
select e.id, e.nom
from Etudiant e
Minus
select id_etu, nom_etu
from (-- La relation totale : Tous les couples étudiant, UE
select e.id as id_etu, e.nom as nom_etu, u.id as id_UE
from Etudiant e cross join UE u
Minus
-- La relation à diviser est obtenue par une jointure.
-- Les couples étudiant, UE si l’étudiant y a une note
select e.id as id_etu, e.nom as nom_etu, i.UE as id_UE
from Etudiant e
inner join Inscrit i on e.id = i.etu) ;

Il y a d’autres manières plus simples d’obtenir le même résultat, mais elles utilisent des fonctions
d’agrégation (ici la fonction count(), voir 2.8 page 21) et éventuellement la partition des nuplets en
groupes (group by, voir 2.11 page 27) qui ne font pas partie de l’algèbre relationnelle :
Utiliser count() pour compter le nombre d’UE et le nombre d’inscriptions d’un étudiant :
select e.id, e.nom
from Etudiant e
cross join (select count (*) as nb_UE from UE) m
where m.nb_UE = (select count (*) from Inscrit i where i.etu = e.id) ;
2.6. LE CAS DES VALEURS INDÉFINIES 17

On peut espérer que le calcul du nombre total d’UE nb_UE ne sera fait qu’une seule fois car la
sous-requête qui fait ce calcul ne dépend pas de la requête englobante, on dit que cette sous-
requête est close ou autonome.
Attention : cette technique ne marche que si les tables disposent des contraintes nécessaires :
create table UE (
create table Etudiant (
id Number (5) primary key,
id Number (5) primary key,
nom varchar2 (20),
nom varchar2 (20)
coeff Number (5)
) ;
) ;
create table Inscrit (
etu Number (5) references Etudiant (id),
UE Number (5) references UE (id),
primary key (etu, UE)
) ;
En particulier la contrainte primary key garantit que ses colonnes sont définies et donc les co-
lonnes clefs étrangères de Inscrit sont forcément définies.
Créer un groupe par étudiant et toujours compter le nombre total d’UE :
select e.id, e.nom
from Etudiant e
inner join Inscrit i on e.id = i.etu
cross join (select count (*) as nb_UE from UE) m
group by e.id, e.nom, m.nb_UE
having count (*) = m.nb_UE ;

La clause having représente une condition de conservation d’un groupe. Ici un groupe correspond
aux concaténations d’une ligne étudiant avec chaque ligne d’inscription le concernant ainsi que
le nombre total d’UE. Cette condition porte sur chaque groupe (ou étudiant) séparément, ainsi
l’expression count (*) représente le nombre d’inscriptions d’un même étudiant.
Pour résumer : la condition du where porte sur chaque ligne produite par la clause from et la
condition du having porte sur chaque groupe construit par le group by.

2.6 Le cas des valeurs indéfinies


Dans la pratique il est souhaitable de pouvoir mémoriser une nouvelle ligne dans une table, même si
certaines colonnes ne peuvent être renseignées du fait qu’on n’a pas forcément toute l’information.

Par exemple je veux quand même pouvoir enregistrer un nouveau client même si je ne connais pas son
numéro de téléphone. Par exemple voici deux ordres équivalents qui ne renseignent pas le téléphone
d’un nouveau client :
Insert into Client (id, nom, tel) values (13, ’Tartampion’, null) ;
Insert into Client (id, nom) values (13, ’Tartampion’) ;

Et une manière d’enregistrer le fait qu’on ne connaı̂t plus le nouveau numéro du client 15 :
update Client set tel = null where id = 15 ;

La colonne téléphone sera alors dite indéfinie : elle n’a pas de valeur. On pourra tester si une colonne
(etplus généralement une expression) est définie ou non avec le prédicat booléen is [not] null :

    
– <expr> is null vrai ssi <expr> est indéfinie, faux ssi <expr> est définie.

   
– <expr> is not null est équivalent à not (<expr> is null)
Par exemple, les villes dont on ne connaı̂t ni la population ni le département :
select v.nom
from Ville v
18 CHAPITRE 2. LE MODÈLE RELATIONNEL ET SQL

where v.population is null and v.departement is null ;

Paris-Texas

Q. 13 Quelle ambiguı̈té y a-t-il dans la question : les villes du nord du tableau page 9.

Q. 14 Lister les villes qui ne sont pas dans le département du Nord ou dont le département n’est pas
renseigné.
SQL permet qu’une colonne soit is null à condition qu’elle ne soit assujettie ni à la contrainte not
null ni à primary key.

2.6.1 Noter l’absence de valeur


Un opérande n’ayant pas de valeur peut se noter explicitement avec le mot clef null, par exemple
null + 5.

Attention : ne pas interpréter ce null comme le pointeur null des langages de programmation ni
comme le zéro des entiers !

2.6.2 Comportement des opérateurs et des fonctions à valeur non booléenne


La plupart des opérateurs et des fonctions à valeur autre que booléenne sont indéfinis si un de leurs
opérandes est indéfini. Par exemple :

a b a+b
1 2 3
is null 2 is null
0 0 0
0 is null is null
is null is null is null

 exemple :
Par  
(1 + n.note) is null ⇔ n.note is null 

2.6.3 Comportement des opérateurs relationnels


Les opérateurs relationnels (=, <, ... et x between a and b) sont à valeur booléenne. Quand un
de leurs opérandes est indéfini, il ont vraiment une valeur qui est appelée unknown (je ne sais pas).

a b a = b, a != b, a <= b, ...
is not null is not null vrai ou faux
Au moins un des deux is null unknown

Par exemple, quel que soit l’état de la colonne nom, les expressions null=null et nom!=null valent
nécessairement unknown.

2.6.4 Comportement des opérateurs logiques


Les opérateurs logiques (not, or et and) travaillent donc en logique tri-valuée, c’est à dire que leurs
opérandes ont des valeurs prises dans un ensemble de trois valeurs : {vrai, faux, unknown}.

Quand aucun des opérandes n’est unknown on a affaire à la logique binaire habituelle. Précisons ce
qui se passe quand un des opérandes vaut unknown :
not vaut évidemment unknown.
and vaut faux si l’autre opérande vaut faux, sinon unknown.
2.7. QUELQUES OPÉRATEURS ET FONCTIONS SCALAIRES DE SQL/ORACLE 19

or vaut vrai si l’autre opérande vaut vrai, sinon unknown.

a b not b a and b a or b
unknown unknown unknown unknown unknown
unknown faux vrai faux unknown
unknown vrai faux unknown vrai

Q. 15 Que donnerait le ou exclusif xor qui n’existe pas en Oracle et en PostgreSQL ?

Q. 16 Donner une définition du prédicat x between a and b en utilisant uniquement les opérateurs
<= et and.
Q. 17 Que donnerait l’opérateur a between b and c si un de ses opérandes est indéfini ?

Q. 18 Définir le comportement que devrait avoir l’opérateur ou exclusif (qui n’existe ni en Oracle ni
en PostgreSQL !).

2.6.5 Présomption d’innocence de la clause where


La clause where peut apparaı̂tre dans une requête (select) mais aussi dans une mise à jour de lignes
(update) ou une suppression de lignes (delete).
Si la condition d’une clause where s’évalue à false ou unknown alors le nuplet correspondant n’est
pas traité.
Par exemple, pour le delete, l’idée est qu’on ne veut pas détruire un nuplet si on ne sait pas s’il vérifie
la condition de suppression (présomption d’innocence).
Q. 19 La requête suivante, censée lister les clients dont le nom n’est pas défini, est incorrecte, pour-
quoi ? En donner une version correcte.

select * from Client c where c.nom = NULL ;

2.7 Quelques opérateurs et fonctions scalaires de SQL/Oracle


2.7.1 between a and b
Les expressions a et b peuvent être des nombres, des chaı̂nes, des dates, tout type disposant d’un
ordre.
v.population between 1000 and 15000
v.nom between ’b’ and ’e’

2.7.2 Expression conditionnelle : case


case
when <predicat1> then valeur1
[ when <predicat2> then valeur2
...
when <predicatN> then valeurN ]
[ else valeurDéfaut ]
end
Le premier prédicat qui vaut vrai donne sa valeur au case, si aucun prédicat ne vaut vrai c’est la
valeur par défaut du else et s’il n’y a pas de else la valeur est indéfinie (is null).
select v.nom as nom,
case
when v.population >= 100000 then ’Grande Ville’
when v.population < 100000 then ’Petite Ville’
else ’Je ne sais pas : la population est indéfinie !’
20 CHAPITRE 2. LE MODÈLE RELATIONNEL ET SQL

end as categorie
from Ville v ;

Q. 20 Donner une autre formulation équivalente au case précédent qui utilise le prédicat is null.

2.7.3 Les fonctions nvl et coalesce


La fonction nvl a deux paramètres et vaut la valeur du premier paramètre s’il est défini (is not null),
sinon elle vaut celle du second paramètre.
select ’Bonjour ’ || nvl (upper (e.nom), ’Anonyme’)
from Etudiant e ;
La fonction coalesce, à au moins un paramètre et vaut la première valeur définie en partant de la
gauche et est indéfinie si tous ses paramètres le sont2 .
Q. 21 Écrire l’équivalent de nvl (upper (e.nom), ’Anonyme’) en utilisant l’opérateur case.

Q. 22 Écrire l’équivalent de coalesce (a, b, c) en utilisant l’opérateur nvl.

2.7.4 Manipuler les chaı̂nes


Les fonctions de chaı̂ne (upper, lower)

Attention : Oracle confond les notions de chaı̂ne vide (de longueur nulle) et de chaı̂ne indéfinie (une
chaı̂ne indéfinie se comporte à peu près comme une chaı̂ne vide) ! Ce défaut devrait disparaı̂tre dans
les versions futures. PostgreSQL n’a pas ce défaut !

Concaténation : || et reconnaissance de modèle : like

Je ne dis bonjour qu’aux étudiants dont le nom contient un r qui n’est pas la dernière lettre :
select ’Bonjour ’ || e.nom from Etudiant e
where e.nom like ’%r_%’ ;
Bonjour Alfred
Bonjour Marc
Bonjour rené

Dans le modèle de like :


– % correspond à un nombre quelconque de n’importe quel caractères (éventuellement nul).
– _ correspond à exactement un caractère quelconque.
Par exemple ’Alfred’ like ’%r_%’ est vrai et ’mer’ like ’%r_%’ est faux.
Q. 23 Écrire le modèle qui reconnaı̂t toute chaı̂ne contenant un caractère x qui n’est ni le premier,
ni le dernier de la chaı̂ne.

Q. 24 Écrire le modèle qui reconnaı̂t toute chaı̂ne contenant deux caractères x séparés par au moins
deux caractères.

Q. 25 Comment reconnaı̂tre les chaı̂nes qui ont un caractère x en première et/ou en dernière position ?

Attention Note de la documentation Oracle 10 :


Oracle Database currently treats a character value with a length of zero as null. However,
this may not continue to be true in future releases, and Oracle recommends that you do
not treat empty strings the same as nulls.
2
Postgres propose aussi la fonction coalesce avec la même signification.
2.8. LES FONCTIONS D’AGRÉGATION COUNT, SUM, AVG, MIN, MAX 21

Mais comment distinguer entre la chaı̂ne vide et le fait qu’une expression de type chaı̂ne est indéfinie
puisqu’Oracle lui-même confond les deux ? ? ? ? ?
Toujours à propos des chaı̂nes vides (et non pas indéfinies) :
Although Oracle treats zero-length character strings as nulls, concatenating a zero-length
character string with another operand always results in the other operand, so null can result
only from the concatenation of two null strings. However, this may not continue to be true
in future versions of Oracle Database. To concatenate an expression that might be
null, use the NVL function to explicitly convert the expression to a zero-length
string.
Autrement, bien qu’actuellement (version Oracle 10) on ait les égalités suivantes :

mon commentaire
’’ is null = vrai n’importe quoi ! on devrait avoir faux
’’ || ’toto’ = ’toto’ c’est cohérent
null || ’toto’ = ’toto’ n’importe quoi ! on devrait avoir indéfini
null = ’’ = unknown c’est cohérent
’’ = ’’ = unknown n’importe quoi ! on devrait avoir vrai

Oracle annonce que bientôt il appliquera la norme, c’est à dire que la chaı̂ne vide sera considérée
comme définie. Pour garantir la portabilité du code il recommande d’utiliser systématiquement la
fonction nvl() lors des concaténations :
’Nom du client : ’ || nvl (client.nom, ’’).

En revanche PostgreSQL est parfaitement cohérent sur la notion de chaı̂ne vide qui est bien entendu
parfaitement définie.

2.8 Les fonctions d’agrégation count, sum, avg, min, max


Ces fonctions effectuent un calcul synthétique sur l’ensemble des nuplets fournis à la projection (clause
select).

Par exemple sum calcule la somme des valeurs définies que prend son expression pour chacun des
nuplets et min en calcule la plus petite.

Une requête dont la clause select comporte de telles fonctions dans ses expressions de projection
fournit exactement une ligne (sauf si la requête est munie d’une clause group by, voir la section 2.11).

sum, avg, min et max donnent un résultat indéfini si l’expression argument n’est jamais définie,
c’est en particulier le cas quand aucun nuplet n’est sélectionné.
En revanche count, qui compte le nombre de fois que son expression a une valeur définie, a toujours
une valeur définie (éventuellement la valeur zéro).
Par exemple count (e.id) donne le nombre de fois que l’attribut e.id est défini. Formes spéciales :
– count (*) renvoie le nombre total de nuplets fournis.
– count (distinct <expression>) nombre de valeurs différentes et définies que prend l’expression.
Q. 26 Donner d’autres formes de count (*) qui soient équivalentes.
Enfin, on ne peut pas demander à la clause select de fournir à la fois une information synthétique
(exactement un nuplet) et une information individuelle (0, 1 ou plusieurs nuplets). Donc, dès qu’une
fonction d’agrégation apparaı̂t dans la clause select, un nom de colonne ne peut apparaı̂tre que dans
une expression argument d’une fonction d’agrégation.
La requête suivante fournira toujours exactement une ligne.
select count (distinct n.mat) as nb_matieres,
avg (n.note) as moyenne,
sum (n.note) / count (n.note) as autre_moyenne,
22 CHAPITRE 2. LE MODÈLE RELATIONNEL ET SQL

n c n*c
n c n*c
Différentes n c n*c ? 1 ?
1 ? ?
valeurs de n c n*c ? ? ? 3 2 6
? 2 ?
la table T ? ? ? 4 3 12
4 ? ?
3 ? ?
<expr>
Sum (n) ? ? 5 10
Sum (2) ? 4 6 8
Sum (n*c) ? ? ? 18
Max (n) ? ? 4 4
Max (15) ? 15 15 15
Count (n) 0 0 2 3
Count (distinct n) 0 0 2 2
Count (*) 0 2 3 4

Fig. 2.2 – Un exemple où on évalue la requête select <expr> from T pour différentes
valeurs de la table T. Un ? signifie que la valeur est indéfinie (is null). La colonne n*c
montre que le produit d’un entier par un indéfini est indéfini. La première collection met
en évidence la spécificité de count par rapport aux autres fonctions d’agrégation.

max (n.note - 5 + 5) as meilleure_note


from Note n ;

2 13.66 13.66 15
Et voici un exemple incorrect car il mélange information synthétique et information individuelle :
select e.nom as nom
count (*) as nb_etudiants,
from Etudiant e ;
-- erreur Oracle

Le tableau suivant résume les différentes fonctions d’agrégation count, sum, avg, min, max

fonction valeur si expr est toujours


indéfinie ou que
aucune ligne ne
lui est fournie
sum (expr) somme des valeurs définies de expr is null
avg (expr) moyenne des valeurs définies de expr is null
min (expr) min des valeurs définies de expr is null
max (expr) max des valeurs définies de expr is null
count (expr) nombre de valeurs définies de expr 0
count (distinct expr) nombre de valeurs définies et différentes de expr 0
count (*) nombre de lignes 0 si aucune ligne
count (1+2) nombre de lignes 0 si aucune ligne
count (’abc’) nombre de lignes 0 si aucune ligne

Q. 27 Quel est le résultat de select count (distinct 1+5) from T pour chaque valeur de T de la
figure 2.2 page 22 ?
Q. 28 Évaluer l’expression Sum (n*c)/Sum (c) pour les valeurs de la figure 2.2 page 22. Si on
interprète n comme une note et c comme un coefficient, en quoi et pour quelle(s) collection(s) le
résultat est-il incorrect, corriger l’expression en conséquence.
2.8. LES FONCTIONS D’AGRÉGATION COUNT, SUM, AVG, MIN, MAX 23

Q. 29 Parmi les expressions de la figure 2.3 page 23, regrouper celles qui ont exactement le même
comportement (vous devriez obtenir 7 groupes).

count (*) count (e.nom) count (55 + 2*3.14)


sum (1) count (’coucou’) sum (e.note) / count (*)
count (e.id) count (upper (e.nom)) sum (case when e.nom is null then 0 else 1 end)
avg (e.note) count (e.nom is null) sum (e.note) / count (e.note)

Fig. 2.3 – Expressions à classer.

2.8.1 Évaluation d’une requête synthétique


Une requête synthétique produit toujours exactement une ligne (même si le from where ne produit
aucune ligne) en utilisant les fonctions d’agrégation dans sa clause select.
On veut calculer la moyenne pondérée par les coefficients de matière de l’étudiant Alfred. Voici la
requête et, conceptuellement, comment elle va être évaluée (il est très probable qu’un vrai moteur
SQL ne fera pas l’évaluation de cette manière) :
select Sum (n.note*m.coeff) /
Sum (case when n.note is null then 0 else m.coeff end) as moy_alfred
from Etudiant e
inner join Note n on e.id = n.etu
inner join Matiere m on n.mat = m.id
where e.nom = ’Alfred’ ;
--
-- 1) résultat de la jointure et de la restriction where :
--
NOTE| COEFF
--------|------
12| 3
14| 5
--
-- 2) calcul des expressions en argument des fonctions d’agrégation :
--
N.NOTE*M.COEFF| CASE ...
--------------|------
36| 3
70| 5
--
-- 3) Calcul les sommes de chacune des deux colonnes :
--
SUM(N.NOTE*M.COEFF)|SUM(CASEWHENN.NOTEISNULLTHEN0ELSEM.COEFFEND)
-------------------|----------------------------------------
106| 8
--
-- 4) Enfin calcul de la moyenne d’Alfred (la division) :
--
MOY_ALFRED
----------
13.25

Les expressions arguments des fonctions d’agrégation sont donc évaluées séparément pour chaque nu-
plet et les expressions externes aux fonctions d’agrégation sont calculées en dernier.
24 CHAPITRE 2. LE MODÈLE RELATIONNEL ET SQL

Pour avoir cette même moyenne pour chaque étudiant, il faudra utiliser la clause group by, voir la
section 2.11.

2.9 Les sous-requêtes


2.9.1 sous-requête dans la clause from
Dans la clause from on peut écrire un select entre parenthèses à la place du nom d’une table. Par
exemple : les villes dont la population est supérieure ou égale à la moyenne des populations :
select v.nom
from Ville v
cross join (select AVG (v.population) as moyenne from Ville v) population
where v.population >= population.moyenne ;
Ou encore, les villes dont la population est supérieure ou égale à la population moyenne par ville de
leur département :
select v.nom
from Ville v
inner join (select AVG (v.population) as moyenne,
v.departement as departement
from Ville v
group by v.departement) pop_par_dpt
on v.departement = pop_par_dpt.departement
where v.population >= pop_par_dpt.moyenne ;

Remarquer que la sous-requête calculant la moyenne de population par département est close (auto-
nome) : elle ne dépend en rien de la requête englobante.

Une clause on ne peut mentionner que des alias de tables déjà déclarés.

Une sous-requête dans la clause from ne peut pas mentionner des colonnes appartenant aux tables
cette clause from : elle doit être close ou autonome (idem en PostgreSQL). Autrement dit : une sous-
requête dans une clause from ne peut pas être corrélée (ou dépendante) avec une table ou une autre
sous-requête de la même clause from.
L’exemple suivant est refusé par Oracle car la sous-requête n’est pas close :
select v.nom
from Ville v
inner join (select AVG (vl.population) as moyenne,
max (vl.departement) as departement
from Ville vl
where vl.departement = v.departement) pop_par_dpt
on v.departement = pop_par_dpt.departement
where v.population >= pop_par_dpt.moyenne ;
C’est parti !
ORA-00904: "V"."DEPARTEMENT" : identificateur non valide

2.9.2 sous-requêtes dans les clauses where et select


En général un opérande dans une expression peut être une sous-requête entre parenthèses.
Si cette sous-requête produit :
– exactement une ligne d’une colonne, elle peut être employée avec un opérateur scalaire correspondant
au type de la valeur.
2.9. LES SOUS-REQUÊTES 25

– un nombre quelconque de nuplets, elle devra être utilisée avec un opérateur ensembliste approprié
(any, all, in, exists)
Dans where et select une sous-requête peut être corrélée si elle mentionne des colonnes appartenant
à des tables de la clause from de la requête englobante.

sous-requête close, autonome ou non corrélée


C’est une sous-requête qui ne dépend pas du nuplet courant de la requête englobante, une sous-
requête non corrélée donnera donc toujours le même résultat, l’optimiseur peut s’en rendre compte et
ne l’évaluer qu’une seule fois.
Par exemple : les villes dont la population est supérieure ou égale à la moyenne :
select v.nom from Ville v
where v.population >= (select AVG (v.population) from Ville v) ;

sous-requête corrélée
Le résultat d’une sous-requête corrélée dépend du nuplet courant de la requête principale car elle
mentionne des colonnes de ce nuplet.

Par exemple les villes dont la population est supérieure ou égale à la moyenne de leur département :
select v.nom from Ville v
where v.population >= (select AVG (vl.population) from Ville vl
where vl.departement = v.departement) ;

Q. 30 Lister les couples matière, nom d’un étudiant ayant la meilleure note dans cette matière avec
les deux techniques : sous-requête dans la clause from et sous-requête dans la condition. On a trois
tables : Etudiant, Note et Matiere.

2.9.3 Factorisation des sous-requêtes non corrélées


La clause with permet de factoriser une fois pour toutes les sous-requêtes non corrélées et de les
baptiser avant d’écrire la requête principale.

En voici la syntaxe :
with <query-name> as ( <subquery> ) { , <query-name> as ( <subquery> ) }
select ... ;

Une sous-requête factorisée peut mentionner les noms des sous-requêtes factorisées qui la précèdent.
La requête principale peut évidemment utiliser tous les noms des sous-requêtes factorisées.
Intérêt : simplifier des requêtes complexes contenant des sous-requêtes non corrélées.
Un seul with par instruction SQL.
Exemple :
with R1 as (select * from X where ...)
R2 as (select ... from R1 ...)
select ... R1 ... R2 ... ;

Exemple Oracle :
with
Dept_Costs as (
select d.department_name, sum (e.salary) dept_total
from Employees e
inner join Departments d on e.department_id = d.department_id
26 CHAPITRE 2. LE MODÈLE RELATIONNEL ET SQL

group by department_name),
Avg_Cost as (
select sum (dept_total)/count (*) avg
from Dept_Costs)
select * from Dept_Costs
where dept_total > (select avg from Avg_Cost)
order by department_name ;

Q. 31 Combien de fois la sous-requête Dept Costs est-elle utilisée ?

Q. 32 Que calcule cette requête ?

Q. 33 Réécrire la requête principale précédente en utilisant une jointure.


Il est parfois possible de décorréler une sous-requête puis d’utiliser une clause with. Par exemple,
soit :
select *
from Etudiant e
where not exists (
select *
from Matiere m
where not exists (
select * from Note n where n.etu = e.id
)
)

Q. 34 Quelle est la sous-requête corrélée et en quoi l’est-elle ?

Q. 35 Pourquoi un étudiant ne peut-il avoir plus d’une note pour une matière ? (voir la figure 2.1
page 13)

Q. 36 Réécrire cette requête en évitant que la sous-requête soit corrélée. Suggestion : déplacer les
sous-requêtes dans la clause from principale et utiliser un comptage.

Q. 37 En utilisant une clause with pour factoriser la sous-requête non corrélée, donner deux requêtes
différentes qui calculent la même chose.

2.9.4 Les opérateurs/fonctions ensemblistes sur résultat d’une requête emboı̂tée


 
 
– <expr> < ALL (select ...) vrai si <expr> est strictement inférieure à toutes les valeurs pro-
duites par le select.
  = != < <= >=
Valable aussi pour les opérateurs
 
– <expr> < ANY (select ...) vrai si <expr> est strictement inférieure à au moins une des valeurs
produites par le select.
  = != < <= >=
Valable aussi pour les opérateurs
– [NOT] EXISTS (select ...) vrai ssi le select produit au moins un (aucun si NOT) nuplet.
 
 
– <expr> [NOT] IN (select ...) vrai ssi <expr> est égale à au moins une (aucune si NOT) des
valeurs produites par le select.
Remarquer que les valeurs peuvent être constituées de plus d’une colonne :
mat in (select mat from ...) ou (mat, etu) in (select mat, etu from ...)
L’opérande droit de in peut aussi être une liste de constantes explicites, par exemple :
note in (2, 3, 5, 7, 11, 13, 17, 19)
Pour chaque matière, les étudiants qui ont la meilleure note :
2.10. ORDONNER LE LISTING DES NUPLETS : ORDER BY 27

select m.nom, e.nom


from Etudiant e
inner join Note n on e.id = n.etu
inner join Matiere m on n.mat = m.id
where n.note >= All (select n.note from Note n where n.mat = m.id) ;

BD Alfred
CL Julie
Q. 38 Récrire la requête précédente en utilisant not exists plutôt que >= All.

Q. 39 any vaut faux si la sous-requête renvoie un ensemble vide, que vaut all dans ce même cas ?

 
Q. 40 Pour chaque matière, lister les étudiants qui n’ont pas la plus mauvaise note.

 
Q. 41 Donner un opérateur ensembliste équivalent à expr IN (select ...)

2.10 Ordonner le listing des nuplets : order by


Cette clause order by permet d’indiquer dans quel ordre on souhaite obtenir les nuplets produits par
la clause select.
Obtenir les nuplets dans un certain ordre n’est utile que pour un lecteur humain (par exemple : lors
d’un jury on aime bien avoir la liste des étudiants par moyenne décroissante) ou pour un programme
dont l’algorithme a besoin de récupérer les nuplets dans un ordre bien précis (par exemple si on veut
vérifier par programme que les numéros d’étudiants sont uniques et contigüs le plus simple est d’ouvrir
un curseur sur les numéros croissants, voir le chapitre PL/SQL).
Cette clause d’ordre n’est donc utilisable que pour le select principal (elle était interdite dans les
sous-requêtes en Oracle < 10).

En Oracle 10 cette règle n’est plus vraie : il est possible d’utiliser la clause order by dans une
sous-requête.

Pour trier les villes par départements croissants, puis populations décroissantes, puis noms croissants :
select * from Ville v
order by v.Departement asc, v.Population desc, v.Nom ;
Par défaut l’ordre est asc (i.e. croissant), desc demande un ordre décroissant.
On n’est évidemment pas obligé d’ordonner sur toutes les colonnes et on peut trier sur le résultat
d’une expression :
select * from Ville v
order by upper (v.Nom) ;

-- On peut aussi ordonner sur une colonne de la projection :


select upper (v.Nom) as nom_MAJ from Ville v
order by nom_MAJ ;

La clause order by est toujours la dernière d’une requête.

2.11 La formation de groupes : group by


L’ensemble des nuplets produits par les clauses from et where peut être partitionné en sous-ensembles
ou groupes non vides et disjoints. La manière de partitionner est indiquée par les expressions pa-
ramètres de la clause group by qu’on appellera clef de groupe : les nuplets ayant la même valeur pour
la clef de groupe font partie du même groupe. Seules les expressions du group by peuvent figurer
en direct dans la projection du select, toute autre expression ou nom de colonne ne peut figurer
28 CHAPITRE 2. LE MODÈLE RELATIONNEL ET SQL

qu’en paramètre d’une fonction d’agrégation : cette fonction s’appliquera donc aux nuplets de chaque
groupes pris séparément. Par exemple pour calculer la moyenne de chaque étudiant on utilise la clef
de groupe e.id, e.nom :
select e.id, e.nom, avg (n.note) as moyenne
from Etudiant e
inner join Note n on e.id = n.etu
group by e.id, e.nom ;

1 Alfred 13
3 Julie 15

Une telle requête peut constituer un nombre quelconque de groupes (éventuellement aucun groupe si
aucun nuplet n’est retenu par le where) et elle produira autant de nuplets qu’il y a de groupes.
Une manière de visualiser ce regroupement est de remplacer la clause group by par une clause order
by dont la clef de tri est la clef de groupe :
select e.id as id, e.nom as nom, n.note as note
from Etudiant e
inner join Note n on e.id = n.etu
order by e.id, e.nom ;

Qui donne :
clef de groupe
id nom note
premier groupe 1 Alfred 12
1 Alfred 14
second groupe 3 Julie 15
Remarquer que dans ce cas on ne peut pas appliquer la fonction avg() sur les notes.

Le regroupement devient intéressant dès qu’on veut obtenir une information synthétique sur chaque
groupe grâce aux fonctions d’agrégation (sinon on peut se contenter du qualificatif distinct de la
clause select). Par exemple on souhaite connaı̂tre la moyenne de chaque étudiant :
select e.id, e.nom, AVG (n.note) as moyenne, count (*) as nb_notes
from Etudiant e
inner join Note n on e.id = n.etu

-- Résultat de l’équi-jointure ordonné sur la clef de groupe :


-- -----------------------
-- nom id | etu note mat
-- -----------------------
-- Alfred 1 | 1 12 1
-- Julie 3 | 3 15 2
-- Alfred 1 | 1 14 2

group by e.id, e.nom ;

-- Résultat du regroupement (ou group by) :


-- -----------------------
-- 2 groupes | individus du groupe
-- nom id | etu note mat
-- -----------------------
-- Alfred 1 | 1 12 1
-- | 1 14 2
2.11. LA FORMATION DE GROUPES : GROUP BY 29

-- -----------------------
-- Julie 3 | 3 15 2

id nom moyenne nb_notes


1 Alfred 13 2
3 Julie 15 1
Et encore une manière de lister, pour chaque matière, les étudiants qui ont la meilleure note. On
remplace, dans la clause from, la table Matiere par la table (virtuelle) des notes maxi de chaque
matière :
select m_max.nom, e.nom
from Etudiant e
inner join Note n on e.id = n.etu
inner join (select -- meilleure note de chaque matière
m.id as id,
m.nom as nom,
Max (n.note) as note_max
from Matiere m
inner join Note n on m.id = n.mat
group by m.id, m.nom) m_max on n.mat = m_max.id
where n.note = m_max.note_max ;

Q. 42 En supposant que chaque matière soit dotée d’un coefficient coeff, calculer la moyenne
pondérée de chaque étudiant. On supposera que toutes les notes et coefficients sont renseignés (is
not null).

Q. 43 Que se passe-t-il si le coefficient d’une matière est indéfini ?

Q. 44 Comment calculer une moyenne correcte pour l’étudiant si certaines notes ne sont pas ren-
seignées ? (si une note n’est pas renseignée, il faut ne pas la prendre en compte)

2.11.1 Sélectionner des groupes : la clause having


La sous-clause having de group by est l’équivalent pour un groupe de la clause where pour une
ligne. Elle permet de ne laisser passer que les groupes qui vérifient sa condition. En dehors des fonc-
tions d’agrégation, elle ne peut donc mentionner que des expressions de la clef du group by.

Par exemple la moyenne des étudiants ayant au moins deux notes :


select e.id, e.nom, AVG (n.note) as moyenne, count (*) as nb_notes
from Etudiant e
inner join Note n on e.id = n.etu
group by e.id, e.nom
having count (*) >= 2

-- Résultat du having :
-- -----------------------
-- 1 groupe | individus du groupe
-- nom id | etu note mat
-- -----------------------
-- Alfred 1 | 1 12 1
-- | 1 14 2
;

1 Alfred 13 2
30 CHAPITRE 2. LE MODÈLE RELATIONNEL ET SQL

Q. 45 Moyenne pondérée des étudiants ayant une note renseignée dans chaque matière.

Q. 46 Pour chaque étudiant, nombre de matières pour lesquelles il a une note définie.

Q. 47 Quelle sera la valeur systématique d’une requête ayant un group by muni de la clause having
count (*) = 0 ?
Q. 48 Donnez une nouvelle version de la requête listant les étudiants inscrits à toutes les UE (voir
section 2.5.3).

2.11.2 group by et informations indéfinies


Lors d’un group by sur une seule expression E, Oracle 10 considère que toutes les lignes pour les-
quelles E est indéfinie (is null) font partie du même groupe (ce qui n’est pas plus évident que de
considérer qu’elle forment autant de groupes différents). PostgreSQL 8.2.1 a la même attitude.

Le mieux serait cependant d’expliciter la valeur indéfnie :


select nvl (<expression>, ’inconnu’), ...
...
group by nvl (<expression>, ’inconnu’), ...
Attention : ’inconnu’ doit être du même type que <expression>.
Q. 49 Mettre en place une expérience pour savoir comment se comporte votre SGBD favori dans ce
cas.

2.12 Les jointures externes : outer join


Dans l’exercice précédent, le problème est qu’on ne voit pas Marc car n’ayant pas de notes il ne fait
pas partie de la jointure (figure 2.1 page 13).
On peut résoudre ce problème grâce à une jointure externe sur la table Etudiant : un étudiant n’ayant
aucune note fera alors partie de la jointure mais toutes les colonnes relatives à la partie Note seront
indéfinies (Oracle10, Postgres, SQL92) :
select e.id, e.nom, n.note
from Etudiant e
left outer join Note n on e.id = n.etu ;

1 Alfred 12
1 Alfred 14
2 Marc <-- nuplet supplémentaire gr^
ace à la jointure externe
3 Julie 15
Si un nuplet Etudiant n’a pas de note, le left outer join le concatène quand même avec un nuplet
Note dont toutes les colonnes sont indéfinies.
Le left désigne la table dont on veut conserver tous les nuplets : celle de gauche. Cette jointure externe
est signalée par left outer join.
Q. 50 Dans la requête précédente, qu’obtiendrait-on avec une jointure externe conservant les lignes
de la table de droite : right outer join ?
La jointure externe n’est pas une primitive car on peut l’exprimer grâce aux opérateurs précédents,
voici l’équivalent de la requête précédente :
select e.id, e.nom, n.note
from Etudiant e inner join Note n on e.id = n.etu
union
select e.id, e.nom, null
from Etudiant e
where e.id not in (select distinct n.etu from Note n) ;
2.13. CONTRAINTES SUR L’USAGE DES FONCTIONS D’AGRÉGATION 31

Il suffit de rajouter le group by pour obtenir des informations synthétiques par étudiant (Oracle10,
PostgreSQL, SQL92) :
select e.id, e.nom, count (n.etu) as nb_notes
from Etudiant e
left outer join Note n on e.id = n.etu
group by e.id, e.nom ;

1 Alfred 2
2 Marc 0 <-- car n.etu est indéfini pour Marc
3 Julie 1
La fonction count (expression) compte le nombre de fois que expression est définie. n.etu étant
indéfini pour Marc, son nombre de matières vaut zéro.

Les jointures sont (Oracle10, Postgres, SQL92) :


inner join : jointure classique (interne)
left outer join : jointure externe conservant les lignes de la table de gauche qui ne s’apparient avec
aucune ligne de la table de droite,
right outer join : comme ci-dessus mais ce sont les lignes de la table de droite qui sont conservées,
full outer join : pour une jointure externe complète (conservation des lignes de gauche et de droite)
Exemple : liste des couples étudiant, matière, même pour les étudiants n’ayant aucune note et matière
est alors indéfinie :
select e.nom, nvl (m.nom, ’aucune matière’) Alfred BD
from Etudiant e Alfred CL
donne
left outer join Note n on e.id = n.etu Julie CL
left outer join Matiere m on n.mat = m.id ; Marc aucune matière

2.13 Contraintes sur l’usage des fonctions d’agrégation


Une clause on ne peut mentionner aucune fonction d’agrégation, elle s’applique à la construction d’une
concaténation de lignes.

Une clause where ne peut mentionner aucune fonction d’agrégation car elle s’applique à exactement
une ligne de la clause from. Cependant elle peut contenir une sous-requête utilisant des fonctions
d’agrégation car une sous-requête est un nouveau monde et n’a donc pas d’impact sur la clause where,
par exemple pour avoir les notes des étudiants supérieures à leurs moyennes :
select e.id as id, e.nom as nom, n.note as note, n.ue as ue
from Etudiant e
inner join Note n on n.etudiant = e.id
where n.note > (select Avg (n.note) as moyenne
from Note n
where n.etudiant = e.id) ;
Une clause group by ne peut mentionner aucune fonction d’agrégation.

Une clause having peut mentionner des fonctions d’agrégation mais avec une profondeur d’au plus 1.
Les colonnes clef du group by peuvent apparaı̂tre en dehors ou à l’intérieur de fonctions d’agrégation,
les autres colonnes doivent absolument apparaı̂tre à l’intérieur de fonctions d’agrégation.

La clause select d’une requête R peut :


1. si R n’a pas de clause group by :
– si R n’est pas une requête synthétique aucune fonction d’agrégation n’apparaı̂t,
32 CHAPITRE 2. LE MODÈLE RELATIONNEL ET SQL

– si R est une requête synthétique, toute colonne provenant de sa clause from doit apparaı̂tre
dans une fonction d’agrégation dont la profondeur est exactement de 1.
En revanche des constantes ou des colonnes provenant d’une requête englobante peuvent ap-
paraı̂tre en dehors des fonctions d’agrégation, ou à l’intérieur, car elles ont une valeur constante
pour l’évaluation de R.
2. si R a une clause group by :
– si R n’est pas une requête synthétique alors toute colonne ne faisant pas partie de la clef de
groupe doit apparaı̂tre dans une fonction d’agrégation avec une profondeur de 1. Les colonnes
clef de groupe peuvent apparaı̂tre à l’extérieur ou à l’intérieur des fonctions d’agrégation.
– si R est une requête synthétique alors toute colonne ne faisant pas partie de la clef de groupe
doit apparaı̂tre dans un double emboı̂tement de fonctions d’agrégation (profondeur de 2).
Les colonnes clef de groupe doivent apparaı̂tre à une profondeur 1 ou 2 dans les fonctions
d’agrégation.
En revanche des constantes ou des colonnes provenant d’une requête englobante peuvent ap-
paraı̂tre en dehors des fonctions d’agrégation, ou à l’intérieur, car elles ont une valeur constante
pour l’évaluation de R. Par exemple :
select Avg (Sum (n.note*n.coeff) / Sum (n.coeff)) as moyenne_promo
from Note n where n.promotion = ’L3GMI’ and n.note is not null
group by n.etudiant ; -- Sum porte sur toutes les notes d’un m^
eme étudiant
Un autre exemple où on suppose qu’un étudiant est inscrit à exactement un groupe : on veut connaı̂tre
le nombre de groupes, l’effectif moyen des groupes et l’effectif maximum d’un (ou plusieurs) groupe :
select
Count (g.id_groupe) as nb_groupes,
Avg (Count (*)) as effectif_moyen_par_groupe,
Max (Count (*)) as effectif_maximum
from Etudiant e
inner join Groupe g on g.id_etu = e.id_etu
group by g.id_groupe ;
Les deux count (*) calculent le nombre de lignes de chaque groupe (autrement dit le nombre
d’étudiants inscrits par groupe).

2.14 Emplacement des fonctions d’agrégation


Une fonction d’agrégation ne peut être utilisée ni dans une clause on de jointure ni dans la clause
where.

Il est possible d’emboı̂ter des fonctions d’agrégation dans le select d’une requête munie d’une clause
group by, mais sans dépasser une profondeur d’emboı̂tement de deux. Dans ce cas la requête donne
une information synthétique des informations obtenues pour chaque groupe, par exemple la moyenne
des moyennes des étudiants :
select Avg (Avg (n.note)) as moyenne_promo
from Etudiant e
inner join Note n on e.id = n.etu
group by e.id, e.nom ;

Cette requête calcule la moyenne de chaque étudiant, puis la moyenne de ces moyennes.

Il est aussi possible d’utiliser des fonctions d’agrégation dans l’expression du having mais avec une
profondeur d’emboı̂tement de un : donc on ne peut y emboı̂ter deux fonctions d’agrégation. Par
exemple si on veut la moyenne des moyennes supérieures ou égales à 10 :
select Avg (Avg (n.note)) as moyenne_promo
from Etudiant e
2.15. POUR CONCLURE 33

inner join Note n on e.id = n.etu


group by e.id, e.nom
having AVG (n.note) >= 10 ;

2.15 Pour conclure


En conclusion, l’exécution d’une requête se fait conceptuellement dans cet ordre :
1. from produit les nuplets du produit cartésien (éventuellement la jointure pour ANSI SQL et
Oracle 9, Postgres),
2. where applique une restriction (et condition de jointure dans Oracle 8) aux nuplets de la clause
from,
3. group by construit des groupes avec sa clause optionnelle having de restriction,
4. select produit la projection de chaque groupe de nuplets provenant du group by ou de chaque
nuplet du where s’il n’y a pas de group by,
5. order by ordonne les nuplets provenant du select.
On peut remarquer que l’ordre syntaxique et l’ordre conceptuel n’ont pas grand chose à voir l’un avec
l’autre ! En particulier, la clause select n’est pas la première à être exécutée.
Chapitre 3

Dépendances fonctionnelles et
normalisation

Une relation universelle est l’unique relation formée de tous les attributs pertinents d’un problème.
A, B, C, D désignent des attributs.
R, T, X, Y, Z désignent des ensembles d’attributs (éventuellement vides).
F un ensemble de dépendances fonctionnelles (DF)
On notera indifféremment X ∪ Y ou XY .

3.1 Dépendances fonctionnelles


Une DF est notée X → Y et exprime que dans toute extension de X ∪ Y les valeurs des attributs de
X déterminent de façon unique celles des attributs de Y . Autrement dit : si on connaı̂t une valeur de
X alors on connaı̂t la valeur de Y lui correspondant.
Q. 51 Soit la table (numéro-de-carte-étudiant, nom), que peut-on faire de numéro-de-carte-étudiant ?

X → Y est élémentaire si X = {C1 C2 · · · Ck } et que pour tout 1 ≤ i ≤ k on n’a pas X − {Ci } → Y .


X → Y est triviale ssi Y ⊆ X, y compris pour Y vide.
Exemple de {numéro-insee} → {sexe, date-naissance} est élémentaire,
dépendances {numéro-insee, sexe} → {sexe, date-naissance} n’est pas élémentaire,
fonctionnelles : {date-naissance, sexe} → {sexe} est triviale.
Soit la relation universelle LDF qui décrit une ligne d’une facture : LDF= {num-facture, date, client,
produit, qté-produit, prix-produit}. Une facture (num-facture) est établie à une date pour un client.
Le prix d’un produit est constant. Une facture peut avoir plusieurs produits (i.e. plusieurs ligne). Un
produit apparaı̂t dans au plus une ligne d’une facture. Un client a au plus une facture par jour.
Q. 52 Donner l’ensemble des DF élémentaires de LDF.

Q. 53 Donner quelques DF triviales et quelques DF non triviales et non élémentaires de LDF.

Q. 54 Combien y a-t-il de dépendances triviales dont le déterminant est LDF ?

3.2 La nécessité de décomposer une relation en sous-relations


Motivation : éviter la répétition (redondance) d’information et l’impossibilité de représenter certaines
informations tout en essayant de conserver les dépendances fonctionnelles.

Q. 55 Sur l’exemple de la relation LDF, mettre en évidence plusieurs anomalies.

Q. 56 Quelles vérifications un programme doit-il faire préalablement à l’ajout d’un tuple LDF.

Q. 57 Que doit-on faire pour modifier le prix d’un produit.

34
3.3. AXIOMES DE ARMSTRONG 35

On a donc souvent besoin de décomposer (normaliser) une relation en plusieurs sous-relations afin
d’éviter ces anomalies.
Q. 58 Proposer une telle décomposition de la relation Ligne-de-Facture et indiquer les dépendances
fonctionelles qui sont conservées par les sous-relations.

3.3 Axiomes de Armstrong


Ils permettent de déduire de nouvelles Axiomes de Armstrong
dépendances fonctionnelles à partir d’un (1) trivialité Y ⊆X ⇒ X→Y
ensemble F de dépendances fonction- (2) augmentation X→Y ⇒ XZ → Y Z
nelles. (3) transitivité X →Y ∧Y →Z ⇒ X→Z
Q. 59 De R = {A, B, C, D, E, F } muni de F = {AB → CD, B → F }, déduire {DE → E, AB →
C, ABD → ADF }. (l’axiome d’augmentation est précieux, ainsi que le fait que XX = X)
F + est la clôture de l’ensemble de DF F obtenue par application des axiomes de Armstrong.
Q. 60 Calculer la clôture de F = {A → B} sur R = {A, B}.
L’intérêt d’une telle clôture est qu’elle permet de définir l’équivalence entre deux ensembles de DF F1
et F2 portant sur la même relation universelle : F1 est équivalent à F2 ssi F1 + = F2 +.
Q. 61 Sans passer par la clôture, on veut montrer que sur R = {A, B, C}, F1 = {A → B, B → C}
est équivalente à F2 = {A → BC, B → C}. Comment peut-on si prendre ? faites-le.

3 corollaires bien pratiques des axiomes de Armstrong


(4) union / décomposition X →Y ∧X →Z ⇔ X →YZ
(5) pseudo-transitivité X →Y ∧YZ →T ⇒ XZ → T
(6) augmentation bis X →Y ∧Z →T ⇒ XZ → Y T

Q. 62 Prouver ces corollaires à l’aide des axiomes et des corollaires déjà prouvés.
Soit R = {A, B, C, D, E, F } munie de : F = {{A, B} → {C}, {C, D} → {E, F }, {E} → {F, D}}

Q. 63 Montrer que si on supprime la DF {E} → {F } on perd une information.

Q. 64 En revanche si on supprime la DF {C, D} → {F } montrer qu’on ne perd rien.

3.4 Calculer les clés candidates d’une relation


Une clé candidate d’une relation R vis à vis d’un ensemble de dépendances fonctionnelles F , est un
sous-ensemble minimal d’attributs de R qui détermine tous les attributs de R.
Q. 65 Quelles sont les clés candidates de R munie de F = {} ?
Définition : tout ensemble d’attributs incluant strictement ceux d’une clé candidate est une super-clé.
Cet algorithme détermine l’ensemble des clés candidates d’une relation R munie d’un ensemble de
DF :
1. On construit le graphe des dépendances, y compris les attributs n’apparaissant dans aucune
dépendance et sont donc des sommets isolés dans le graphe.
2. Les sommets non cibles d’une flèche appartiennent à toutes les clés, on les note et les marque.
3. Tant qu’il existe un sommet S déterminé par des sommets marqués, marquer S.
4. Effacer tous les sommets marqués et les flèches qui en partent.
5. Tant qu’il existe un sommet S non source d’une flèche, effacer S qui n’appartient à aucune clé.
6. Les sommets restant sont forcément dans des cycles, considérer séparément chacun d’eux comme
appartenant à une des clés, le marquer puis recommencer en (3)
7. S’il ne reste pas de sommet, supprimer toutes les clés non minimales et c’est fini.
36 CHAPITRE 3. DÉPENDANCES FONCTIONNELLES ET NORMALISATION

Voici le graphe de C = {Ville, Rue, Zip, D} muni de F = {{Ville,


Rue } → Zip, Zip → Ville}.
D Ville
Une clé non minimale est : {Ville, Rue, Zip, D}. Zip
Les 2 clés candidates sont : {{Ville, Rue, D}, {Rue, Zip, D}} Rue

Q. 66 Dessiner le graphe des dépendances de Ligne-de-Facture (voir question Q.52).

Q. 67 Marquer les nœuds de ce graphe déterminés directement ou indirectement par (date, client,
produit) puis montrer qu’on obtient le même résultat en utilisant les DF et les axiomes et corollaires
de Armstrong.
Q. 68 Donner les clés candidates de Ligne-de-Facture.
la relation munie des dépendances fonctionnelles
R = {A, B, C, D, E, F, G, H, I} {A → BC, C → D, BDE → A, F → AG, G → H}
Q. 69 Donner
R = {A, B, C, D, E, F, G} {AC → B, B → C, C → DE, D → F, E → F, F → G}
les clés de :
R = {A, B, C, D, E} {A → DE, BC → A, E → B, D → C}
R = {A, B, C, D, E} {A → DE, B → AC → A, E → B, D → C}

Définitions des formes normales : BCNF ⇒ 3NF ⇒ 2NF ⇒ 1NF


Une forme normale permet de mesurer la qualité d’une relation munie de dépendances fonctionnelles.
Par exemple 2NF nous garantit que toutes les clés complètes sont nécessaires pour déterminer les
attributs n’appartenant à aucune clé : cela permettra d’éviter des redondances.
Par exemple M agasin = {P roduit, Date, P rix, P roducteur} muni de Regle = {{P roduit, Date} →
P rix, {P roduit} → P roducteur} a comme clés C = {{P roduit, Date}}. Elle n’est donc pas 2NF.
Q. 70 Pourquoi M agasin n’est pas 2NF ? Donner un exemple de redondance sur M agasin.

1NF Si tout attribut a une valeur atomique.


2NF Une relation est en 2NF si elle est 1NF et que tout attribut n’appartenant à aucune clé can-
didate est en dépendance élémentaire ou (irréductible) avec chacune des clés. (contre-exemple :
{A, B, C}, {B → C})
3NF Une relation est en 3NF si tout attribut A n’appartenant à aucune clé X dépend de chacune
des clés par une DF directe de F +, autrement dit 6 ∃Y |A 6∈ XY, X → Y, Y 6→ X, Y → A, ou
encore sans intermédiaire possible qui ne serait pas une clé. Une relation 3NF est aussi 2NF.
(contre-exemple : {A, B, C, D}, {AB → C, C → D}, 2NF ?)
BCNF : Boyce Codd Normal Form Une relation R est BCNF vis à vis d’un ensemble de DF F ,
si toute DF non triviale de F + a comme déterminant une clé ou une super-clé de R .

Q. 71 Par exemple R = {cru, pays,région, qualité} munie de {{cru, pays} → {région, qualité},
{région} → {pays}} n’est pas BCNF car {région} n’est pas une clé. Est-elle 2NF ? 3NF ?
Q. 72 Normalité de LDF (voir Q.52) ?

Q. 73 Normalité de R = {A, B, C, D} munie de F = {AB → CD, BC → D, CD → A} ?

Q. 74 Normalité de R = {A, B, C, D} munie de F = {A → BC, B → C, C → B} ?

3.5 Décomposer une relation sans perte d’information


Quand une relation ne satisfait pas la normalité souhaitée, on la décompose en deux sous-relations.
Si cette décomposition ne satisfait toujours pas la normalité souhaitée on pourra à nouveau les
décomposer : le processus de décomposition est itératif.
Cette technique presque mécanique de décomposition risque de donner un résultat similaire à celui
obtenu par une approche plus intuitive comme par exemple la conception du MCD de Merise.
3.5. DÉCOMPOSER UNE RELATION SANS PERTE D’INFORMATION 37

Soient la relation R munie de F et R1 , R2 une décomposition de R (i.e. R1 ∪ R2 = R et R1 ∩ R2 n’est


pas vide). Cette décomposition est sans perte d’information vis à vis de F si toute extension r de
R vérifiant F est égale à ΠR1 (r) ⊲⊳ ΠR2 (r) = r, cette jointure naturelle se faisant par égalité sur les
colonnes de R1 ∩ R2 .

Soit R = {A, B, C} munie de F = {A → C}. Pour l’exemple d’extension donné à droite,


A B C
montrer que les décompositions suivantes de R :
a1 b1 c1
Q. 75 R1 = {A, C}, R2 = {A, B} ne perd pas d’information. a1 b2 c1
Q. 76 R1 = {B, C}, R2 = {A, C} perd de l’information. a2 b2 c1
Le principe de non perte d’information est évidemment incontournable lors d’une décomposition ! D’où
l’importance du théorème suivant.

Théorème de décomposition sans perte d’information Soient R = {A1 , A2 , . . . , An } un schéma


relationnel, F un ensemble de dépendances fonctionnelles et X, Y, Z une partition de R telle que
X → Y ∈ F +. Alors R1 = X ∪ Y, R2 = X ∪ Z est une décomposition de R sans perte d’information1 .

X, Y, Z est une partition de R ⇔ (X ∪ Y ∪ Z = R) ∧ (X ∩ Y = ⊘) ∧ (X ∩ Z = ⊘) ∧ (Y ∩ Z = ⊘)

Démonstration : Soit r une valeur quelconque de R et r1 = ΠR1 (r), r2 = ΠR2 (r). On montre d’abord
que r1 ⊲⊳ r2 ⊆ r, pour cela on peut montrer que r1 ⊲⊳ r2 6⊆ r est une absurdité : supposons que
(xi , yi ) ∈ r1 et (xi , zi ) ∈ r2 et que (xi , yi , zi ) 6∈ r, puisque (xi , yi ) ∈ r1 et (xi , zi ) ∈ r2 ont été obtenus
par projection de r, c’est qu’il existe deux nuplets (xi , yi , zi′ ), (xi , yi′ , zi ) appartenant à r, or X → Y on
a donc yi = yi′ et donc (xi , yi , zi ) ∈ r. De la même manière on montre que r ⊆ r1 ⊲⊳ r2 .

Q. 77 Montrer que la condition du théorème est aussi nécessaire, c’est à dire que si une décomposition
est sans perte alors elle vérifie nécessairement la condition du théorème. Suggestion : montrer que si
on n’a ni R1 ∩ R2 → R1 ni R1 ∩ R2 → R2 alors la décomposition est avec perte, un exemple suffit.

Q. 78 En SQL, à quelles contraintes serait soumis X dans les tables R1 et R2 ?


L’ensemble des DF de Ri est la projection ΠRi (F +) = {X → Y ∈ F + |X ∪ Y ⊆ Ri }.
Une décomposition sans perte d’information ne préserve pas toujours les dépendances fonctionnelles.

R = {A, B, C, D} munie de {AB → C, C → D}


Exemple : X = {A, B} Y = {C} Z = {D}
R1 = {A, B, C} munie de {AB → C} R2 = {A, B, D} munie de {AB → D}
mais la dépendance {C → D} est perdue. On perd donc une contrainte d’intégrité facilement expri-
mable par une contrainte d’unicité ou de clé primaire. Il faudra programmer pour garantir que cette
dépendance est préservée lors des modifications de table.
Q. 79 Implanter R1 et R2 en SQL, comment garantir la dépendance perdue C → D ?

Q. 80 Donner une autre décomposition de R qui préserve à la fois l’information et les DF.

Q. 81 On décompose la relation R de la question Q.73 en R1 = {A, B, C}, R2 = {A, B, D}. Cette


décomposition est-elle sans perte ? Quelles sont les DF conservées par cette décomposition ?

Q. 82 Décomposer LDF (voir Q.52) en sous-relations qui sont toutes BCNF, cette décomposition
conserve-t-elle toutes les DF ?
Remarque : pour un même problème R muni de F il peut y avoir plusieurs décompositions différentes
permettant d’obtenir des sous-relations vérifiant une forme normale.
Attention : une décomposition BCNF sans perte d’information peut perdre des dépendances fonc-
tionnelles (ce n’est pas le cas de 3NF).

1
Autremrent dit : R1 , R2 est sans perte d’information ssi R = R1 ∪ R2 et (R1 ∩ R2 → R1 ou R1 ∩ R2 → R2 ).
38 CHAPITRE 3. DÉPENDANCES FONCTIONNELLES ET NORMALISATION

Application (emprunté au poly de Mireille Clerbout)


Soit la relation D = {dépôt, journal, titre, catégorie, tx com, prix, adr dépôt, jour, quantité} munie
des dépendances F :
{dépôt} → {adr dépôt} {catégorie} → {tx com} {titre} → {journal}
{dépôt, journal, jour} → {quantité} {journal} → {titre, prix, catégorie, tx com}
Utilisez des diminutifs pour faire les questions, par exemple D pour dépôt, Jl pour journal, Jr pour
jour . . ..
Q. 83 Déterminer les clés de D munie de F et montrer qu’elle n’est pas BCNF (section 3.4).

Q. 84 Décomposer D par étapes successives en sous-relations qui sont BCNF et qui conservent,
globalement, toutes les DF de F (section 3.5).
Q. 85 Dessiner le MCD de la décomposition obtenue.

Q. 86 Écrire les ordres SQL de création des tables BCNF et leurs garnissages à partir d’une table D
déjà peuplée.
Chapitre 4

SQL/DML les ordres de modification


des tables

SQL signifie Structured Query Language


SQL = {DDL, DML, DCL}
DML = Data Manipulation Language

4.1 insert : ajout de nouvelles lignes


Pour ajouter de nouvelles lignes.
insert into <nomTable> [(col1, ..., coln)] values (val1, ..., valn) ;
ou
insert into <nomTable> [(col1, ..., coln)] <requete> ;
Exemple :
– Insertion d’une ligne en explicitant la valeur de toutes les colonnes dans l’ordre de leurs déclarations :
insert into Client values (4, ’Durif’, ’Philippe’, 300) ;

On peut explicitement indiquer qu’une colonne n’est pas définie (is null) en mettant null pour
signifier l’absence de valeur.
– Insertion d’une ligne en explicitant les valeurs d’un sous-ensemble des colonnes de la table :
insert into Client (num_client, nom, prenom) values (5, ’Durif’, ’Pablo’) ;

Les colonnes non mentionnées seront indéfinies ou bien auront leur valeur par défaut éventuellement
indiquée lors de la création de la table (default).
– Insertion de toutes les lignes produites par une requête :
insert into Client (num_client, nom, prenom)
select ref, nom, prenom
from Employe
where salaire > 1000 ;
Le mot clef default peut être utilisé en tant que valeur d’une colonne et indique que la colonne doit
prendre sa valeur par défaut si elle en a une (voir create table section 5.1 page 41) ou être indéfinie
si elle n’en a pas.

4.2 update : la mise à jour de lignes existantes


Pour modifier des lignes existantes.
update <nomTable>
set affectation {, affectation}
[where condition] ;

39
40 CHAPITRE 4. SQL/DML LES ORDRES DE MODIFICATION DES TABLES

affectation ::= colonne = expression


| (col1, ..., colp) = (sous-requ^
ete-1-ligne-p-colonnes)
Attention : la sous-requête éventuelle ne doit pas porter sur la table en cours de modification sinon
on aura une erreur de table mutante.
Exemple, augmentation du solde des clients ayant un numéro inférieur à 4 :
update Client set solde = solde + 100 where num_client < 4 ;
Exemple avec une liste de colonnes :
create table Departement (
deptno Number (5) primary key,
prefecture Varchar2 (10) not null unique
) ;

create table Employe (


id Number (5) primary key,
salaire Number (10, 2),
commission Number (10, 2),
deptno references Departement (deptno) -- clef étrangère
) ;

On veut déplacer sur Paris les employés des départements de Lille et Lyon en doublant leurs salaires
et en leur accordant une commission de 500 :
update Employe
set (salaire, commission, deptno) =
(select 2 * Employe.salaire, 500.0, d.deptno
from Departement d
where d.prefecture = ’Paris’)
where deptno in (select deptno from Departement
where prefecture in (’Lille’, ’Lyon’)) ;

ou bien, de façon équivalente :


update Employe
set
salaire = 2 * Employe.salaire,
commission = 500.0,
deptno = (select d.deptno from Departement d where d.prefecture = ’Paris’)
where deptno in (select deptno from Departement
where prefecture in (’Lille’, ’Lyon’)) ;

4.3 delete : suppression de lignes existantes


delete from <nomTable> [where condition] ;
Exemple suppression des clients ayant un numéro égal à 2 ou 5 :
delete from Client where num_client in (2, 5) ;
Suppression de tous les clients :
delete from Client ; -- vide la table
Chapitre 5

Contraintes d’intégrité en SQL

DDL = Data Definition Language


Dès la déclaration d’une table on peut fixer un certain nombre de propriétés sur les valeurs que peuvent
prendre les attributs.

5.1 Création des tables


create [global temporary] table <nom-table>
( <liste-des-colonnes-et-contraintes-de-table> )
[on commit preserve rows | delete rows]
[ as <requ^
ete> ] ;
global temporary la table est temporaire et visible par toutes les sessions qui en ont le droit. Les
données d’une telle table ne sont visibles que par la session qui les a insérées. Les données insérées ne
survivent pas à la fin de :
– la transaction qui les a insérées si l’option on commit delete rows a été précisée, c’est l’option
par défaut.
– la session qui les a insérées si on commit preserve rows (une session est en général une séquence
de transactions par forcément contiguës dans le temps).
as permet d’initialiser le contenu de la table avec le résultat de la requête, dans ce cas il ne faut pas
préciser les types des colonnes de la table et on ne peut pas donner une contrainte de clef étrangère
(on pourra toujours ajouter cette dernière plus tard avec la commmande alter table add constraint
...).
create table Client (
id Number (3),
nom Varchar2 (20) constraint Client_Nom_Defini not null,
prenom Varchar2 (20),
solde Number (6, 2) default 0.0,
constraint Client_PK primary key (id)
) ;

La clause default n’est pas une contrainte, elle provoque simplement l’introduction de la valeur par
défaut lors d’un insert ne précisant pas de valeur explicite.

5.2 Les types de données


SQL2 ne définit pas le type booléen (pourquoi ? ? ?).

Le mot clé BOOLEAN n’apparaı̂t même pas dans l’index de l’ouvrage Oracle i SQL Reference Release
3 (8.1.7) qui compte quand même plus de mille pages !

41
42 CHAPITRE 5. CONTRAINTES D’INTÉGRITÉ EN SQL

En revanche, PostgreSQL dispose du type boolean, mais du coup PostgreSQL n’a pas la valeur unk-
nown d’Oracle ; en PostgreSQL c’est l’absence de valeur (is null) qui joue le rôle de unknown.

Les types définis par la norme ne sont malheureusement pas toujours respectés.

Numériques Caractères Binaires Dates, Intervalles

5.2.1 Types numériques


Pour Oracle
Number (p, s) nombres en virgule fixe à p chiffres décimaux avec une précision de 10−s . L’in-
tervalle de valeur est : [−(10p − 1)10−s , (10p − 1)10−s ]
p, qui doit être ∈ [1, 38], indique le nombre maximal de chiffres en base 10,
s ∈ [−84, 127], comme scale (échelle en français) qui indique la précision : 10−s
si s = 2, la précision est de un centième
si s = −2, la précision est de cent
Par exemple Number (5, 2) = [−999, 99, 999, 99], précision 0, 01
Un autre exemple :
create table Essai (
n Number (3, -2) -- de -99900 à 99900, précision 100
) ;
insert into Essai VALUES (-240) ;
select * from Essai ;
-200
update Essai set n = n + 25 ;
select * from Essai ;
-200
update Essai set n = n + 125 ;
select * from Essai ;
-100
drop table Essai ;

Number (p) nombre entier, qui signifie Number (p, 0)


Number nombre en virgule flottante avec 38 chiffres décimaux.

Pour norme ANSI/SQL (acceptés par Oracle)


NUMERIC (p, s) et DECIMAL (p, s) (Oracle Number (p, s))
INTEGER, INT et SMALLINT (Oracle Number (38))
FLOAT (b), DOUBLE PRECISION et REAL (Oracle Number)
La fonction prédéfinie mod :
select mod (24.66, 24) from dual ;

affiche 0.66.

5.2.2 Types caractères


Pour Oracle
CHAR (n) chaı̂nes de taille exactement égales à n (jusqu’à 2000 caractères)
Varchar2 (n) chaı̂nes de tailles variables inférieure ou égale à n (jusqu’à 4000 caractères).
NCHAR et NVarchar2 : Unicode
CLOB et NCLOB (SQL3)
5.2. LES TYPES DE DONNÉES 43

Pour norme ANSI/SQL


– CHARACTER (n) et CHAR (n) (Oracle CHAR (n))
– NATIONAL CHARACTER (n), NATIONAL CHAR (n) et NCHAR (n) (Oracle NCHAR (n))
– NATIONAL CHARACTER VARYING (n), NATIONAL CHAR VARYING (n) et NCHAR VA-
RYING (n) (Oracle NVarchar2 (n))

5.2.3 Types binaires


– RAW (size) (jusqu’à 2000 octets) obsolète (utiliser BLOB et BFILE)
– LONG RAW (jusqu’à 2 Goctets) obsolète (utiliser BLOB et BFILE)
– BFILE adresse d’un fichier binaire (BLOB en SQL3)

5.2.4 Types temporels


Pour Oracle
– DATE = siècle-année-mois-jour-heure-minutes-seconde On dispose des fonctions
– La fonction SYSDATE donne la date courante du système.
– arithmétique (l’unité est le jour) et relation d’ordre sur les dates.
– to_char pour passer de la représentation interne à la représentation externe
select SYSDATE from Dual ;
select SYSDATE from Dual ;
2003-02-12 11:41:42.0

select to_char (SYSDATE, ’dd mon yyyy’) from Dual;


12 fev 2003

select to_char (SYSDATE+21, ’dd/mm/yy hh:mi’) from Dual;


05/03/03 05:42
select ’Il est ’ || to_char (SYSDATE, ’hh:mi’) from Dual;
Il est 05:42

select to_char (SYSDATE,


’"Il est" hh24 "heures" mi "minutes" ss "secondes"’)
from Dual;
Il est 17 heures 42 minutes 27 secondes

– to_date pour passer de la représentation externe à la représentation interne.


select to_char(to_date(’7/2/04 21h15’,’dd/mm/yy hh24"h"mi’),’dd/mm/yy hh24"h"’)
from Dual ;
07/02/04 21h
select to_date (’19h27’, ’hh24"h"mi’) from Dual ;
2004-02-01 19:27:00.0

– La différence entre deux dates est exprimée en nombre de jours (nombre réel éventuellement négatif)
et on peut ajouter un nombre de jours à une date.
– Months_Between (Date1, Date2) en gros : Date1 - Date2 en nombre de mois, donc positif si
Date1 est postérieure à Date2. Le résultat est un réel, il n’est entier que si Date1 et Date2 sont le
même jour du mois (par exemple le 12/3/05 et le 12/11/03) ou le dernier jour du mois (par exemple
le 28/2/06 et le 31/12/01).
– Pour avoir des dates sans prendre en compte l’heure de la journée, Oracle propose la fonction
Trunc (D in Date) qui renvoit la date D dont la partie heure est à zéro. PostgreSQL propose la
fonction date_trunc.
44 CHAPITRE 5. CONTRAINTES D’INTÉGRITÉ EN SQL

Pour norme ANSI/SQL


– TIME et TIMESTAMP (Oracle DATE)

5.3 Les contraintes


Depuis SQL2.

Déclarées à la création de la table, puis vérifiées automatiquement par le SGBD :


– programmation allégée
– sécurité plus forte
Oracle ne vérifie les contraintes qu’une fois l’instruction DML complètement terminée (on peut éventuellement
lui demander de ne les vérifier qu’en fin de transaction, c’est à dire au moment où les modifications
faites par la transaction sont publiées par l’instruction commit).

Si une contrainte n’est pas vérifiée en fin d’instruction DML, il y a annulation de la mise à jour avec
message d’erreur. Plus précisément, la table est remise dans l’état dans lequel elle était avant le début
de l’instruction DML (fonctionnement en tout ou rien).

5.3.1 Baptisez vos contraintes !


Chaque contrainte peut être baptisée (et on a toujours intérêt à le faire), elle pourra ensuite être
manipulée facilement par certaines commandes simplement en donnant sont nom.
Le nom d’une contrainte est donné après le mot-clef constraint :
constraint <nom-de-la-contrainte> <définition-de-la-contrainte> [[not] deferrable]
Par défaut l’attribut est not deferrable : la contrainte sera alors vérifiée en fin de l’instruction
modifiant la table (insert, update ou delete).
Si la contrainte est deferrable alors il sera possible de demander qu’elle ne soit vérifiée qu’en fin de
transaction (lors du commit) avec la commande set constraint <nom-contrainte> deferred.
Postgres 8 dispose lui aussi de cette possibilité, mais uniquement pour les clef étrangères (references).

5.3.2 Aspects syntaxiques


SQL distingue deux syntaxes pour décrire les contraintes : les contraintes de colonnes et les contraintes
de table.

La seule contrainte qui ne peut être décrite qu’en tant que contrainte de colonne est not null car elle
qualifie toujours une seule colonne.

Une autre contrainte exprimable dans les deux syntaxe est unique pouvant s’applique à plusieurs
colonnes.
Les autres contraintes peuvent être décrites indifféremment en tant que contrainte de colonne ou
contrainte de table ce sont unique, primary key, foreign key et check.

5.3.3 Liste des contraintes


not null l’attribut doit toujours avoir une valeur définie, c’est la seule contrainte qui ne peut
s’écrire qu’en contrainte de colonne.
primary key Aucune des colonnes de la clef primaire ne peut être indéfinie (Oracle crée un
index unique pour cette contrainte).
5.3. LES CONTRAINTES 45

Fig. 5.1 – Les deux manières de déclarer des contraintes


Syntaxe contrainte de colonne Syntaxe contrainte de table
Une contrainte de table peut porter sur plusieurs co-
Une contrainte de colonne porte sur exactement lonnes, elle est indiquée comme un élément de la liste
une colonne (par exemple la contrainte not null) des colonnes de la table :
et est indiquée au moment de la déclaration de la
colonne et on peut en mettre plusieurs : create table Commande (
produit Number (5), client Number (5),
create table Produit ( quantite Number (5) default 0,
id Number (5) constraint Commande_PK
constraint Produit_PK primary key, primary key (produit, client),
nom Varchar2 (10), constraint Commande_Produit_FK
stock Number (5) default 0 foreign key(produit) references Produit(id),
constraint Produit_stock_defini constraint Commande_Client_FK
not null foreign key(client) references Client(id),
constraint Stock_Positif constraint Quantite_Positive
check (stock >= 0)) ; check (quantite >= 0)) ;

Deux contraintes portent sur la colonne stock. default n’est pas une contrainte.

unique sur un attribut ou un groupe d’attributs dont la valeur, quand elle est définie, doit être
unique dans la table (Oracle crée un index unique pour cette contrainte).

Restriction Oracle 10 : contrairement à la norme SQL, Oracle considère que, dans une contrainte
d’unicité définie, les valeurs indéfinies pour une même colonne sont égales si d’autres colonnes
sont définies. Par exemple si on pose la contrainte unique (formation, rang) les deux couples
(1, 23) et (1, 24) sont bien distincts, en revanche (1, null) et (1, null) seront considérés
par Oracle comme égaux et ne pourront donc pas coexister.
En revanche si deux lignes sont indéfinies sur toutes les colonnes d’unicité alors Oracle les
considère comme satisfaisant l’unicité, par exemple (null, null) et (null, null) sont considérés
comme différents.

PostgreSQL respecte la norme SQL, c’est à dire qu’il considère (1, null) et (1, null) comme
distincts.
check prédicat portant sur les colonnes d’un même nuplet
check (qte >= 0)

check (date_deb < date_fin)


check (couleur IN (’BLANC’, ’VERT’, ’ROUGE’))

En SQL2 la condition de check est presque équivalente à celle de where (y compris des sous-
requêtes)
Restrictions Oracle 10 et PostgreSQL 8.2 : le prédicat doit porter uniquement sur la valeur de la
ligne courante, pas de sous-requête, de séquence, on ne peut pas utiliser les fonctions SYSDATE,
UID, USER ou USERENV ni les pseudo-colonnes LEVEL ou ROWNUM.
Si la condition de check est vraie ou unknown (présomption d’innocence) la propriété est
considérée comme respectée et la mise à jour est acceptée.

Q. 87 À votre avis, le delete provoque-t-il la vérification des contraintes not null et check ?

Q. 88 Ce même delete a-t-il des vérifications à faire quand il y a des contraintes primary key et
unique, lesquelles ?
46 CHAPITRE 5. CONTRAINTES D’INTÉGRITÉ EN SQL

Présomption d’innocence pour la contrainte check


Si la condition d’un check s’évalue à UNKNOWN alors la contrainte est considérée comme satisfaite.

Par exemple :


check (salaire > 0 or (salaire = 0 and commission > 0))
Q. 89 Montrer que si salaire is null la mise à jour est acceptée quel que soit l’état de commission.

L’idée est qu’on ne peut pas empêcher la création d’un nuplet en l’absence d’information (présomption
d’innocence).
Q. 90 Si commission n’est pas définie, le salaire peut-il être négatif ?

Q. 91 Corriger la contrainte pour garantir que le salaire et la commission ne sont jamais négatifs
(une idée consiste à utiliser l’opérateur is null, une autre idée à mettre plusieurs check).

Définition de nouveaux domaines (seulement en PostgreSQL)


En SQL2 et PostgreSQL oui, mais pas en Oracle :
create domain Quantite Integer default 0 check (value >= 0)

create table ... (


qte_produit Quantite,
...
) ;

Un exemple de domaine en PostgreSQL :


create domain Couleurs_Additives
as Text
default ’bleu’
constraint Couleurs_Additives_CHK
check (upper (value) in (’ROUGE’, ’VERT’, ’BLEU’)) ;

5.3.4 Contraintes d’intégrité d’entité : clef primaire


Il s’agit des clefs primaires
create table Contient (
commande Number (3),
produit Number (3),
constraint Contient_PK primary key (commande, produit)
) ;

Les colonnes de la clef primaire doivent être définies et les clefs primaires forment un ensemble (unicité).
Sous Oracle (et d’autres), un index unique est automatiquement créé sur la clef primaire, il prend le
nom de la contrainte (Produit_PK dans l’exemple).

Relation sans clef


En théorie, une relation est un ensemble de nuplet, c’est à dire qu’un nuplet ne peut pas apparaı̂tre
plus d’une fois dans l’extension d’une relation.
De façon plus pratique, une relation a toujours une clef qui garantit l’unicité des nuplets.

En Oracle comme en PostgreSQL il est possible de définir une table sans clef :
5.3. LES CONTRAINTES 47

create table Sans_Clef (num Number (3)) ;

et on pourra y insérer plusieurs nuplets de même valeur.

5.3.5 Contraintes d’intégrité référentielle : clef étrangère


create table Etudiant (
id Number (5),
nom Varchar2 (20),
constraint Etudiant_PK primary key (id)
) ;

create table Note (


note Number (2),
etudiant Number (3),
constraint Note_Etudiant_FK foreign key (etudiant) references Etudiant (id)
) ;

Le fait que la colonne Note.etudiant est une clef étrangère implique que la table Note dépend de la
table Etudiant. Autrement dit Note ne peut être créée que quand Etudiant existe.
Considérons une ligne de la table Note :
– si sa colonne etudiant est définie, il doit exister exactement une ligne de Etudiant dont le id est
égal à etudiant.
L’unicité de Etudiant.id est garantie puisque c’est justement la clef primaire.
– si sa colonne etudiant est indéfinie (is null), c’est qu’elle ne référence aucune ligne de Etudiant.
La colonne Note.etudiant est alors appelée une clef étrangère, on peut aussi la comprendre comme
un pointeur associatif qui n’est pas une adresse mémoire mais une valeur permettant de retrouver la
ligne désignée de la table Etudiant.
Une conséquence du exactement une ligne de la table Etudiant est que la colonne id doit garantir
l’unicité des lignes de Etudiant : id doit soit être une clef primaire soit supporter une contrainte
d’unicité (unique).

Une clef étrangère peut-être constituée de plusieurs colonnes : ces colonnes ne référencent une ligne
que si elles toutes définies.

Une table peut se référencer elle-même :


create table Employe (
id Number (3),
nom Varchar2 (20) constraint nom_not_null not null,
superieur Number (3),
constraint Employe_PK primary key (id),
constraint Employe_Superieur_FK
foreign key (superieur) references Employe (id)
) ;

Très souvent une clef étrangère référence directement une clef primaire.
Il peut être souhaitable et même agréable de ne pas expliciter le type de la clef étrangère qui sera celui
de la colonne id de Etudiant. Cela est possible en Oracle 10 :
– en contrainte de colonne :
create table Note (
note Number (2),
etudiant constraint Note_Etudiant_FK
foreign key (etudiant) references Etudiant (id)
48 CHAPITRE 5. CONTRAINTES D’INTÉGRITÉ EN SQL

) ;

– en contrainte de table :
create table Note (
note Number (2),
etudiant,
constraint Note_Etudiant_FK
foreign key (etudiant) references Etudiant (id)
) ;

Depuis sûrement assez longtemps MySQL accepte la syntaxe de déclaration de clef étrangère, il n’en
assure la sémantique que depuis sa version 6 et uniquement dans InnoDB.

Suivent quelques manipulations dont certaines sont erronées.

On peut noter un étudiant non défini !


insert into Note (note) values (13) ;
-- OK ! une contrainte not null permettrait d’éviter ce DEFAUT !

On ne peut pas noter un étudiant qui n’existe pas


insert into Note (note, etudiant) values (13, 111) ;
ORA-02291: violation de contrainte (DURIF.NOTE_ETUDIANT_FK) d’intégrité
- touche parent introuvable

On ne peut pas modifier la clef cible d’un étudiant noté


update Etudiant set id = 666
where nom = ’dupont’ ;
-- OK car ’dupont’ n’a pas de note

update Etudiant set id = 444


where nom = ’durif’ ;
-- ’durif’ a au moins une note
ORA-02292: violation de contrainte (DURIF.NOTE_ETUDIANT_FK) d’intégrité
- enregistrement fils existant

Modification de contrainte pour propager la mise à jour


Impossible en Oracle 10, mais possible en Postgres 8.

On ne peut pas supprimer un étudiant noté


delete from etudiant where id = 666 ;
-- OK car 666 n’a pas de note

delete from etudiant where id = 333 ;


-- 333 a au moins une note
ORA-02292: violation de contrainte (DURIF.NOTE_ETUDIANT_FK) d’intégrité
- enregistrement fils existant
5.4. LE DILEMME DE LA DÉPENDANCE MUTUELLE 49

5.3.6 Clef étrangère et modifications de la table maı̂tre


SQL permet de maintenir automatiquement la cohérence des clefs étrangères lorsqu’on modifie la table
référencée (ou table maı̂tre).

Pour cela il propose un certain nombre de comportements, qui ne sont pas tous implémentés par
Oracle :

Oracle PostgreSQL
SQL Commentaire (10.2) (8.1.3)
on delete|update no action (par Modification interdite (échec de par défaut par défaut
défaut) l’instruction).
on delete cascade Suppression propagée : les nuplets oui oui
référençant sont supprimés
on update cascade Modification propagée. non oui
on delete|update set null La référence devient indéfinie. oui oui
on delete|update set default La référence est remise à sa valeur non oui
par défaut.

Un tel comportement est indiqué lors de la déclaration d’une clef étrangère, ainsi on peut avoir des
clefs étrangères ayant la même cible et n’ayant pas le même comportement. Ces comportements sont
des compléments optionnels à ajouter à la définition d’une clef étrangère.

Redéfinition de contrainte pour propager la suppression on delete cascade figure 5.1


alter table Note drop constraint Commande_Produit_FK ;
alter table Note add (constraint Commande_Produit_FK foreign key (produit)
references Produit (id) on delete cascade) ;
select n.note, Nvl (e.nom, ’anonyme’) as nom
from Note n
left outer join Etudiant e on n.etudiant = e.id ;
NOTE NOM
----------
13 durif
10 durif
13 anonyme

delete from etudiant where e.nom is null ;


-- OK

select n.note, Nvl (e.nom, ’inconnu’) as nom


from Note n
left outer join Etudiant e on n.etudiant = e.id ;
NOTE NOM
----------
13 durif
10 durif

5.4 Le dilemme de la dépendance mutuelle


Par défaut Oracle ne vérifie les contraintes qu’à la fin de l’exécution de chaque instruction de mise à
jour (insert, update et delete). Un update peut donc parfaitement faire passer la table modifiée
50 CHAPITRE 5. CONTRAINTES D’INTÉGRITÉ EN SQL

par des états intermédiaires incohérents.

Un problème apparaı̂t cependant quand la cohérence à maintenir couvre plusieurs tables : il est alors
possible de différer en fin de transaction (lors du commit) les vérifications de manière à pouvoir
modifier les différentes tables.

Tout conducteur a exactement une voiture et toute voiture a exactement un conducteur :


create table Conducteur (
id Number (5),
nom Varchar2 (10),
voiture Number (5)
constraint Conducteur_Voiture_NOT_NULL not null,

constraint Conducteur_PK primary key (id)


) ;

create table Voiture (


id Number (5),
marque Varchar2 (10),
conducteur Number (5)
constraint Voiture_Conducteur_NOT_NULL not null,

constraint Voiture_PK primary key (id),


constraint Voiture_Conducteur_FK
foreign key (conducteur) references Conducteur (id)
) ;

alter table Conducteur


add (constraint Conducteur_Voiture_FK
foreign key (voiture) references Voiture (id) deferrable) ;

Ainsi il est impossible d’insérer un conducteur ou une voiture !


insert into Conducteur values (1, ’toto’, 6) ;
ORA-02291: violation de contrainte
(DURIF.CONDUCTEUR_VOITURE_FK) d’intégrité - touche parent introuvable

insert into Conducteur (id, nom) values (1, ’toto’) ;


ORA-01400: impossible d’insérer NULL dans ("DURIF"."CONDUCTEUR"."VOITURE")

Remarquer qu’on a pris soin de dire, lors du alter table, que la contrainte Conducteur_Voiture_FK
est deferrable, car, par défaut, les contraintes ne sont pas différables. On peut alors demander à
différer la vérification de cette contrainte en fin de transaction :
set constraint Conducteur_Voiture_FK deferred ;

insert into Conducteur values (1, ’toto’, 121) ;


insert into Voiture values (121, ’citron’, 1) ;

commit ; -- vérification des contraintes différées


--
-- ici la contrainte Conducteur_Voiture_FK est de nouveau "immediate"
--

Les contraintes différées sont vérifiées soit lors :


5.5. MODIFICATION DU SCHÉMA 51

– d’un set constraint ... immediate,


– de la validation et terminaison de la transaction courante, grâce à l’instruction commit ou impli-
citement par une déconnexion normale.
Si elles ne sont pas vérifiées, le prochain commit (ou la fin de session) effectuera un rollback qui
annulera toutes les modifications faites depuis le début de la transaction.

Enfin pour détruire ces deux tables interdépendantes on peut commencer par supprimer les contraintes
ou bien faire tout simplement :
drop table Conducteur cascade constraint ;
-- Détruit la contrainte de clef étrangère Voiture_Conducteur_FK
-- puis détruit la table Conducteur.

drop table Voiture ;

5.5 Modification du schéma


alter table permet :
ajouter/supprimer/modifier la définition d’une colonne
ajouter/supprimer des contraintes
activer/désactiver des contraintes

5.5.1 alter table


alter table <nom> add (<colonne-ou-contrainte> {, <colonne-ou-contrainte>}) ;
alter table <nom> modify (<colonne> {, <colonne>}) ;
alter table <nom> drop <colonne-ou-contrainte> ;

Ajouter une ou plusieurs colonnes et contraintes : add (...)


create table Client (id Number (5)) ;
alter table Client add (nom Varchar2 (20) not null,
tel Varchar2 (10) constraint tel_unique unique,
loc Varchar2 (15) default ’Lille’,
solde Number (10, 2), constraint Client_PK primary key (id)) ;
S’il n’y a pas de valeur par défaut, la nouvelle colonne est indéfinie et cela peut entrer en conflit avec
d’autres contraintes (par exemple si une nouvelle colonne est not null et que la table modifiée n’est
pas vide).
alter table Client
add (constraint Client_PK primary key (id)) ;

Modifier la définition d’une colonne : modify (...)


On peut augmenter la taille d’une colonne, la diminuer si la table est vide, et, pour le type Varchar2
diminuer la taille uniquement si la nouvelle taille est suffisante pour les données déjà stockées,
On ne peut changer de type que si la table est vide.
alter table Client
add (constraint Client_Solde check (solde >= 0))
modify (nom Varchar2 (30)) ;
On peut ajouter ou supprimer des contraintes, mais pas les modifier.
52 CHAPITRE 5. CONTRAINTES D’INTÉGRITÉ EN SQL

Suppression de colonne et/ou contraintes : drop

Suppression d’une contrainte nommée Suppression d’une colonne


alter table Client drop
alter table Client drop column tel ;
constraint tel_unique ;
-- Suppression d’une contrainte anonyme
alter table Dept drop unique (dname, loc) ;

Activer/Désactiver les contraintes : enable/disable

Contrainte activée : elle est vérifiée et est stockée dans le dictionnaire.


Contrainte désactivée : elle n’est pas vérifiée, mais elle reste stockée dans le dictionnaire (évidemment !).
Pourquoi désactiver des contraintes : quand on veut faire des traitements qui peuvent, provisoirement,
les violer, par exemple charger les tables une par une.

La réactivation d’une contrainte échoue tant qu’elle n’est pas vérifiée, on est donc obligé de corriger
les données.
Par défaut les contraintes sont actives.
On peut les désactiver dès leur définition, ou bien plus tard :
create table Emp (
empno Number (5) primary key disable,
...
)

alter table Autre


add primary key (num_autre) disable ;

alter table Dept


enable primary key,
enable unique (dname, loc) ;

alter table Dept


disable constraint dname_PK ;

Remarque : les contraintes primary key et unique créent des index sur la table qui sont reconstruits
à chaque réactivation de la contrainte.

5.5.2 Suppression d’une relation


Elle échoue si la table est référencée par des clefs étrangères (même si elle est vide).

drop table <nom> ;

Effets :
– enlève la définition de la table du dictionnaire,
– tous les index et triggers associés sont détruits,
– les sous-programmes PL/SQL qui dépendent de cette table deviennent inutilisables (ils sont toujours
là !)
– les vues et les synonymes qui dépendent de cette table sont toujours là mais renvoient une erreur
quand on les utilise !
– la place occupée par la table est restituée.
5.6. GÉNÉRATEUR D’ENTIERS : LES SEQUENCE 53

5.5.3 drop table ... cascade constraints


Le problème des dépendances dues aux clef étrangères :
create table Maitre (
id Number (3) primary key) ;

create table Esclave (


id Number (3),
constraint Esclave_Vers_Maitre_FK foreign key (id) references Maitre (id)
) ;

La suppression
drop table Maitre ;
-- erreur oracle

ne marche pas : il faut d’abord supprimer les tables référençantes ou désactiver/supprimer certaines
contraintes ou encore, plus simplement :
drop table Maitre cascade constraints; -- ok (supprime les contraintes référençantes)
qui supprime la contrainte Esclave_Vers_Maitre_FK qui fait référence à la table Maitre.

5.5.4 Vider une table sans la détruire (Oracle, PostgreSQL)


Ceci est plus efficace qu’un drop suivi d’un create, mais cette cette opération ne sera pas annulée
lors d’une éventuelle annulation de la transaction (rollback).
truncate table <nom> ;

PostgreSQL permet la même chose sur une liste de tables.

5.6 Générateur d’entiers : les sequence


Cela peut être pratique pour fabriquer une valeur de clef primaire.
create sequence Id_Voiture ;

create table Voiture (


idv Number (5),
marque Varchar2 (20),
nbPlaces Number (1),
constraint Voiture_PK primary key (idv)
) ;

insert into Voiture values (Id_Voiture.nextval, ’Peugeot’, 5) ;


...
drop sequence Id_Voiture ;
1. Par défaut le premier entier d’une séquence produit par nextval sera 1, il y a moyen de modifier
ce comportement.
2. nextval renvoie la valeur courante puis fait passer la séquence à la valeur suivante.
3. currval renvoie la dernière valeur de nextval sans modifier l’état de la séquence, ne peut être
consultée qu’après le premier appel à nextval.

create sequence Id_Voiture ;


select Id_Voiture.currval from dual ;
select Id_Voiture.nextval from dual ;
54 CHAPITRE 5. CONTRAINTES D’INTÉGRITÉ EN SQL

drop sequence Id_Voiture ;


Chapitre 6

Compléments Oracle SQL

6.1 Expression conditionnelle decode


decode (<expr>,
<search>, <result>
{, <search>, <result>}
[, <default>])

Renvoie le premier <result> tel que <expr> = <search>, sinon renvoie <default>, et s’il n’y a pas
de <default> alors null. Attention : decode considère que deux valeurs indéfinies sont égales (ce qui
est en contradiction avec le reste de SQL !).
Q. 92 Que vaut decode (n, 1, ’Intro’, 4, ’Techno’, ’Conclusion’) si n = 4 et si n = 3 ?

6.2 Expression conditionnelle case


Il s’agit d’une expression conditionnelle pouvant avoir un nombre quelconque de branches when :
case
{when <cond> then <expr-r> }
[ else <expr-d> ]
end

Exemple donnant une mention :


case
when 0 <= note and note < 10 then ’Refusé’
when 10 <= note and note < 12 then ’Passable’
when 12 <= note and note < 14 then ’Assez bien’
when 14 <= note and note < 16 then ’Bien’
when 16 <= note and note <= 20 then ’Très Bien’
else ’Bizarre ! note incorrecte ?’
end

Q. 93 Pourquoi n’utilise-t-on pas un between and ?

6.3 group by cube et group by rollup


cube et RollUp permettent de constituer, en plus des groupes fournis pas la clause group by, des
super-groupes de ces groupes et d’en fournir pour eux aussi des informations synthétiques.

55
56 CHAPITRE 6. COMPLÉMENTS ORACLE SQL

6.3.1 Clef de groupe


Rappelons que les expressions de la clause group by forment ce qu’on appellera la clef de groupe :
tous les tuples qui ont la même valeur pour les expressions de la clef de groupe appartiennent au même
groupe. Par exemple dans :
select Matiere.nom as Matiere,
sum (Note.note) / count (*) as Moyenne
from Note, Matiere
where Note.matiere = Matiere.id
group by Matiere.nom ;

MATIERE MOYENNE
---------------
BD 13
CL 13
SSM 14

la clef de groupe est constituée par le nom de la matière.

6.3.2 Cube et RollUp


cube et RollUp sont des extensions de la clause group by : en plus de constituer les groupes
correspondant à la clef de groupe, elles produisent des ensembles de groupes correspondant à des
sous-ensembles de la clef de groupe initiale. Puisque ces derniers groupes sont identifiés par des clefs
plus petites, ils contiendront donc plus de tuples et on les appellera des super-groupes. Donc une
sous-clef de groupe génère des super-groupes.

En fait Cube et RollUp peuvent ne porter que sur une partie des clefs de groupre, par exemple les
deux exemples suivants sont corrects :
group by Cube (a,b,c) --
group by a, Cube (b,c) -- a apparaitra dans toutes les clefs de groupe
le premier va constuire les huit regroupements possibles : (), (a), (b), (c), (a, b), (a, c), (b, c) et (a, b, c),
le second les quatre contenant tous a : (a), (a, b), (a, c) et (a, b, c).
Cube explore tous les groupes pour tous les sous-ensembles des expressions mises entre parenthèses
après Cube, y compris l’ensemble vide : si la clef de groupe contient n expressions alors Cube
génèrera les 2n ensembles de groupes correspondant chacun à une des 2n sous-clef de groupe.
Si on se place dans le cas où la clef de groupe comporte trois expressions x, y et z, cube introduit
effectivement les huit sommets d’un cube en trois dimensions.

les 8 sous-clefs de groupe qu’on peut voir comme les huit som-
Pour la requête
sont : mets d’un cube :
{bc} {abc}

select Max (d) (a b c) {b} {ab}


from ... (a b) (a c) (b c)
group by (a) (b) (c) {c}
Cube (a,b,c); () {ac}

{} {a}
Q. 94 Si la clef de groupe comportait deux expressions entre parenthèses, à quoi correspondrait
cube ? et si elle en comportait quatre ?.

Q. 95 Combien de lignes produisent les requêtes suivantes (dual contient une seule ligne) :
6.3. GROUP BY CUBE ET GROUP BY ROLLUP 57

select 1 from dual group by ’a’, ’b’, ’c’ ;


select 1 from dual group by ’b’, cube (’a’, ’c’) ;
select 1 from dual group by cube (’a’, ’b’, ’c’) ;

Q. 96 Sur le modèle de la requête précédente, écrire une requête qui imprime toutes les combi-
naisons des trois lettres a, b et c (voir la fonction grouping section 6.3.3).

Q. 97 À quoi peut bien servir la possibilité de construire des cubes.

RollUp explore tous les préfixes de la clef de groupe entre parenthèses : si la clef de groupe contient
n expressions alors rollUp génèrera n + 1 sous-clefs de groupe.

Les 4 sous-clefs de groupe seront les quatre


Pour la requête
préfixes possibles de abc :
select Max (d)
from ... (x a b c) (x a b) (x a) (x)
group by x, RollUp (a, b, c) ;
rollUp correspond à une exploration purement hiérarchique des groupes en super-groupes et
permet donc d’afficher des super-totaux. Par exemple (a b c) regroupe les individus qui ont
la même valeur en (a b c) ; l’intersection de deux groupes (a b c) différents est évidemment
vide. (a b) regroupe les individus qui ont la même valeur en (a b), ainsi le groupe (a b) de
valeur (v w) sera l’exacte union des groupes (a b c) dont (a b) = (v w) et c est quelconque :
on voit bien se dessiner une hiérarchie.
On constate donc que rollUp produit un sous-ensemble des sous-clefs produites par Cube.

Q. 98 Dans quel cas unique, cube et rollUp sont-ils équivalents ?

Q. 99 Dessiner comment group by rollup (CDM, CDP) groupe les lignes suivantes :

ENVOI
CDC CDP CDM QTE
1 A1 B1 C1 2
2 A1 B1 C4 7
3 A2 B3 C1 4
4 A2 B3 C4 5
5 A2 B3 C5 6
6 A3 B3 C1 2
7 A3 B4 C2 5
8 A5 B1 C4 3
9 A5 B2 C2 2
10 A5 B2 C4 1
11 A5 B3 C4 7
12 A5 B6 C4 5

Q. 100 quelles sont les clefs de groupe construites par group by a, rollup (b, c) ?

Q. 101 Combien de lignes produisent les requêtes suivantes (dual contient une seule ligne) :

select 1 from dual group by ’a’, ’b’, ’c’ ;


select 1 from dual group by ’b’, rollup (’a’, ’c’) ;
select 1 from dual group by rollup (’a’, ’b’, ’c’) ;

6.3.3 La fonction grouping


Pour exploiter correctement les regroupements fournis par cube et rollup, il est nécessaire, dans
la clause select de savoir à quel sous-clef de groupe on a affaire. C’est à cela que sert la fonction
58 CHAPITRE 6. COMPLÉMENTS ORACLE SQL

grouping ().
 
 
Dans les clauses select et having la fonction de groupe grouping ( <expr> ) renvoie 0 si l’ex-
  fait partie de la sous-clef du groupe actuellement traité et renvoie 1 sinon.
pression en paramètre
 
L’expression <expr> doit évidemment faire partie de la clef complète de groupe.

6.3.4 Exemples de RollUp


Si, en plus de la moyenne par matière, on veut aussi voir la moyenne générale (c’est à dire la moyenne
de toutes les notes et non pas la moyenne des matières) on insère simplement un RollUp et dans
le select on teste la présence de l’expression Matiere.nom dans la sous-clef du groupe en cours de
traitement :
select decode (grouping (Matiere.nom),
0, Matiere.nom, -- sous-clef = (Matiere.nom)
’Moyenne générale’) -- sous-clef = ()
as Matiere,
sum (Note.note) / count (*) as Moyenne
from Note
inner join Matiere on Matiere.id = Note.matiere
group by rollup (Matiere.nom) ;

MATIERE MOYENNE
------------------------
BD 13
CL 13
SSM 14
Moyenne générale 13.2
Ici, on utilise la fonction grouping() pour choisir le bon libellé de la première colonne. En revanche,
la formule de calcul de la deuxième colonne est la même pour tous les super-groupes : c’est la moyenne
de toutes les notes des étudiants (ceci explique que la moyenne générale ne soit pas égale à la moyenne
des moyennes de matière).

Maintenant, on veut en plus voir les notes individuelles de chaque étudiant :


select
decode (grouping (Matiere.nom) + grouping (Etudiant.nom),
2, ’Moyenne générale’, -- sous-clef = ()
1, Matiere.nom, -- sous-clef = (Matiere.nom)
0, Matiere.nom || -- sous-clef = (Matiere.nom, Etudiant.nom)
’ et ’ || Etudiant.nom)
As libellé,
sum (Note.note) / count (*)
AS Note_ou_Moyenne
from Etudiant
inner join Note on Note.etudiant = Etudiant.id
inner join Matiere on Matiere.id = Note.matiere
group by rollup (Matiere.nom, Etudiant.nom) ;

LIBELLÉ NOTE_OU_MOYENNE
----------------------------
BD et Prévert 13
BD 13
CL et Prévert 12
CL et Sartre 15
CL et Vian 12
6.3. GROUP BY CUBE ET GROUP BY ROLLUP 59

CL 13
SSM et Prévert 14
SSM 14
Moyenne générale 13.2

Une impression plus fine qui malheureusement n’est pas possible :


select decode (grouping (Matiere.nom),
1, ’Toutes les matières’ || count (distinct Matiere.nom),
Matiere.nom)
AS Matiere,
decode (grouping (Etudiant.id),
1, ’Tous les étudiants’,
Etudiant.id)
AS etudiant,
count (*) as effectif
from Etudiant, Note, Matiere
where Etudiant.id = Note.etudiant AND
Note.matiere = Matiere.id
group by ROLLUP (Matiere.nom, Etudiant.id) ;

NON car ORA-30480 : L’option distinct n’est pas autorisée avec group by cube
ou rollup.

6.3.5 Exemples de Cube


Pour voir la différence entre Cube et RollUp, on reprend la dernière requête en remlaçant le rollup
par un cube et on modifie la clause select en conséquence :
select
decode (grouping (Matiere.nom) + 2 * grouping (Etudiant.nom),
3, ’Moyenne générale’, -- sous-clef = ()
2, Matiere.nom, -- sous-clef = (Matiere.nom)
1, Etudiant.nom, -- sous-clef = (Etudiant.nom)
0, Matiere.nom || -- sous-clef = (Matiere.nom, Etudiant.nom)
’ et ’ || Etudiant.nom)
As libellé,
sum (Note.note) / count (*)
AS Note_ou_Moyenne
from Etudiant, Note, Matiere
where Etudiant.id = Note.etudiant AND
Note.matiere = Matiere.id
group by cube (Matiere.nom, Etudiant.nom) ;

LIBELLÉ NOTE_OU_MOYENNE
----------------------------
BD et Prévert 13
BD 13
CL et Prévert 12
CL et Sartre 15
CL et Vian 12
CL 13
SSM et Prévert 14
SSM 14
Prévert 13
60 CHAPITRE 6. COMPLÉMENTS ORACLE SQL

Sartre 15
Vian 12
Moyenne générale 13.2

6.3.6 Sélectionner les super-groupes

La clause having et la fonction grouping() permettent d’éviter l’édition de certains super-groupes.


Par exemple si on veut que seuls les super-groupes correspondant à des sous-clefs contenant l’expression
Matiere.nom soient édités :

select decode (grouping (Matiere.nom),


0, Matiere.nom,
’Impossible !!!’) as Matiere,
decode (grouping (Etudiant.nom),
0, Etudiant.nom,
’Moyenne promotion’)
AS etudiant_ou_promotion,
sum (Note.note) / count (*) as Moyenne
from Etudiant
inner join Note on Note.etudiant = Etudiant.id
inner join Matiere on Matiere.id = Note.matiere
group by rollup (Matiere.nom, Etudiant.nom)
having grouping (Matiere.nom) = 0 ;

MATIERE ETUDIANT_OU_PROMOTION MOYENNE


-------------------------------------
BD Prévert 13
BD Moyenne promotion 13
CL Prévert 12
CL Sartre 15
CL Vian 12
CL Moyenne promotion 13
SSM Prévert 14
SSM Moyenne promotion 14

Les premières expressions du group by peuvent être en dehors du cube ou du rollUp, elles font
alors partie de toutes les sous-clefs de groupe. La requête suivante est plus simple et donne le même
résultat que la précédente :
– par matière et étudiant
– par matière et promotion

select Matiere.nom as Matiere,


decode (grouping (Etudiant.nom),
0, Etudiant.nom,
’Moyenne promotion’)
AS etudiant_ou_promotion,
sum (Note.note) / count (*) as Moyenne
from Etudiant
inner join Note on Note.etudiant = Etudiant.id
inner join Matiere on Matiere.id = Note.matiere
group by Matiere.nom, rollup (Etudiant.nom) ;
6.4. REQUÊTES HIÉRARCHIQUES, ORDRE PRÉFIXÉ ET MONO-TABLE 61

6.4 Requêtes hiérarchiques, ordre préfixé et mono-table


Quand les nuplets d’une table décrivent une structure de données hiérarchique (ou plus généralement
un graphe sans cycle), Oracle permet, grâce aux requêtes dites hiérarchiques, l’exploration préfixée
des nuplets de cette structure.

Voici une table décrivant une telle hiérarchie :


create table Employe (
id Number (5),
nom Varchar2 (20),
superieur Number (5),
constraint Employe_PK primary key (id),
constraint Mon_Superieur_PK foreign key (superieur) references Employe (id)
) ;
Une requête hiérarchique est caractérisée par les deux clauses start with et connect by dont voici
la syntaxe et la sémantique :
select e.id, e.nom from Employe e where <condition>
start with <condition identifiant la (les) ligne(s) jouant le r^ ole de
racine(s) de la (des) hiérarchie(s). Sous-requ^etes possibles>
connect by <condition établissant la parenté entre une ligne mère et ses
lignes enfants le mot clef prior identifie les colonnes de la
ligne mère. Pas de sous-requ^
ete>
La clause start with est optionnelle, la clause connect by est obligatoire. Si start with est absente
alors toutes les lignes de la table sont utilisée en tant que racine.

Les nuplets de la hiérarchie sont parcourus en ordre préfixé à partir du nuplet racine (la racine est
prise avant ses enfants).

La clause where ne fait que retenir ou non les lignes produites par la requête hiérarchique, mais
ne modifie pas l’ensemble des nuplets sélectionnés par la requête hiérarchique (where agit après la
production hiérarchique des nuplets).

Une requête hiérarchique ne peut pas fonctionner sur une jointure : il ne peut y avoir qu’une seule
table dans la clause from.

Suivent quelques exemples.


Exploration strictement hiérarchique
Ici on liste l’employé 1 ainsi que tous ses subordonnés directs ou indirects :
select e.nom, e.id, e.superieur from Employe e
start with e.id = 1
connect by prior e.id = e.superieur ;
-- !!!!! * * ces 2 ’e’ sont ceux de deux lignes DIFFERENTES
Attention, les différents e de start with et connect by désignent des nuplets différents :
– dans start with, e est le nuplet racine de la hiérarchie en cours d’exploration (c’est le premier
père),
– dans connect by, e.id est une colonne d’un nuplet de la hiérarchie dont on recherche les fils car
il est qualifié de prior, en revanche, e.superieur est une colonne d’un nuplet dont on cherche à
savoir s’il est un fils du nuplet prior.
Le nuplet prior est nécessairement un descendant d’une des racines déterminées par la clause start
with.
Mise en forme de l’affichage
On souhaite montrer le niveau hiérarchique de chaque employé en indentant son nom en fonction de
la profondeur de sa position dans la hiérarchie.
62 CHAPITRE 6. COMPLÉMENTS ORACLE SQL


Pour cela la valeur de la pseudo-colonne level  est la distance à la racine plus 1 (pour la racine,
level vaut 1). La requête précédente pourrait alor s’écrire :
select lpad (’ ’, level-1) || e.nom, e.id, e.superieur
from Employe e
start with e.id = 1
connect by prior e.id = e.superieur ;
Remonter une hiérarchie

6.4.1 Exploration d’un graphe sans cycle


Cet exemple est très artificiel !
select e.id, e.nom
from Employe e
start with e.id = 1
connect by prior e.id < e.id ;

Autrement dit un employé est le père ou l’ancêtre de tous les employés qui ont un id strictement
supérieur au sien.
Deuxième partie

Développement serveur

63
Chapitre 7

Introduction à PL/SQL

PL/SQL = Programming Language with SQL.

Langage de programmation procédural inspiré de Ada.

Langage propriétaire (Oracle), mais la norme SQL3 s’en inspire.

Permet d’inclure facilement des requêtes SQL.

Ce langage est utilisé :


Côté serveur pour définir des objets procéduraux éventuellement persistants :
– blocs d’instructions anonymes et non persistants
– procédures, fonctions et paquetages stockés (donc persistants),
– des paquetages (eux aussi persistants),
– triggers (réflexes, ou déclencheur : base de donnée actives)
Côté client pour développer le code des interfaces graphiques (Developper 2000 par exemple).
L’intérêt des sous-programmes stockés est qu’il sont exécutés sur le serveur de données et qu’il sont
donc proches de la base de données qu’ils exploitent : leurs traitements seront donc plus efficaces que
s’ils étaient exécutés côté client.

7.1 Accès aux données : uniquement les ordres DML


Les ordres DML (insert, update et delete) s’écrivent comme en SQL dans le source PL/SQL, on
peut même y faire figurer des variables et des paramètres du programme PL/SQL.

La seule exception concerne select qui, étant une expression, renvoie une valeur qu’il faudra affecter
à une variable PL/SQL avec la nouvelle clause obligatoire : into.

7.2 Les types de donnée disponibles en PL/SQL


On dispose des types :
SQL Number, Varchar, Date, types objets (tous les types SQL)
PL/SQL Boolean, Positive, Natural, PositiveN, NaturalN, . . .
définis par l’utilisateur (PositiveN et NaturalN ⇒ is not nul),
types composés (record, tableau)
Si on a besoin d’effectuer des calculs numériques, on a intérêt à utiliser les types numériques spécifiques
à PL/SQL car ils ont des représentations plus adaptées.

Le N des types PositiveN et NaturalN indique que les valeurs ne peuvent pas être indéfinies (is null).

64
7.3. FONCTION STOCKÉE 65

Tous les types SQL sont utilisables en PL/SQL, y compris ceux définis par le programmeur
dans le contexte du relationnel-objet.

7.2.1 Exemples de déclaration de variable


Num NUMBER (4) ; -- ’Num is null’ est une expression PL/SQL correcte
En_Stock Boolean := False ;
Limite constant Real := 5000.0 ;
Par défaut, les variables sont indéfinies. On peut leur appliquer l’opérateur is [not] null.

7.2.2 Le type Boolean


Il est muni des deux valeurs true et false.

Attention : le type Boolean n’existe pas dans le SQL d’Oracle, la conséquence est qu’une fonction
booléenne ne pourra être utilisée nulle part dans un ordre DML, même pas dans la clause where. Elle
pourra seulement être utilisée par un autre programme PL/SQL.

7.2.3 Les expressions


Les opérateurs SQL sont disponibles en PL/SQL, par exemple le prédicat is [not] null.

7.2.4 Les connecteurs logiques and et or


Contrairement à Ada, les connecteurs logiques and et or sont à court-circuit (il n’y a donc pas en
PL/SQL d’opérateur and then ou or else).

7.3 Fonction stockée


Une première fonction qui montre que PL/SQL est effectivement un langage de programmation :

create or replace function pgcd (a in PositiveN, b in PositiveN) return PositiveN is


-- On ne peut pas modifier les paramètres "in" (comme en Ada).
ia PositiveN := a ; ib PositiveN := b ;
-- PLS_Integer PositiveN Natural ... + efficaces que Number pour calculer
begin
while ia <> ib loop -- <>, !=, ~=, ^=
if ia < ib then ib := ib - ia ;
else ia := ia - ib ;
end if ;
end loop ;
return ia ;
end pgcd ;
/

Le / indique à SQL/PLUS la fin du texte du sous-programme (ou du paquetage) qui est compilé et
stocké immédiatement.

Un appel à une fonction est une expression ou un bout d’expression, il est donc possible, pour tester
la fonction, d’en faire figurer un appel dans la clause select d’une requête :
SQL> select pgcd (7, 21) from Dual ; -- Dual : table prédéfinie d’une ligne
66 CHAPITRE 7. INTRODUCTION À PL/SQL

7.4 Procédure stockée


create or replace procedure ajouterClient
(id in Client.id%type, Nom in Client.nom%type) is
begin
insert into Client (id, Nom) values (id, Nom) ;
end ajouterClient ;

La notation id in Client.id%type s’appelle un typage implicite : le paramètre id à le même type


que la colonne id de la table Client, cela garantit une bonne cohérence avec la table manipulée et
offre une meilleure lisibilité.

Le typage implicite est aussi utilisable pour les variables locales.

On voit aussi que les paramètres de la procédure s’utilisent tout naturellement dans le insert.

7.4.1 Exécuter une procédure stockée


Elle pourra être appelée dans tout autre sous-programme ou trigger ou dans un bloc anonyme :
SQL> begin ajouterClient (5, ’Tartempion’) ; end ; -- bloc anonyme
ou directement avec l’instruction call :
SQL> call ajouterClient (5, ’Tartempion’) ; -- marche sous JDBC et SQL*PLUS
ou encore avec l’ordre Execute de SQL/PLUS :
SQL> Execute ajouterClient (5, ’Tartempion’) -- marche sous SQL*PLUS

7.4.2 Une autre procédure : équilibrage des salaires


On peut vraiment se demander l’intérêt de la procédure ajouterClient précédente : elle ne réalise
pas vraiment un algorithme. L’intérêt d’une procédure est de réaliser un algorithme correspondant à
une opération plus ou moins complexe nécessitant en général plusieurs accès à la base de données.
Par exemple, on veut automatiser le traitement social suivant : tous les employés ayant un salaire
supérieur à un seuil passé en paramètre voient leurs salaires ramenés à ce seuil. Le total de salaire
ainsi retranché est ensuite réparti équitablement entre tous les employés :
create or replace procedure Repartir (Seuil in Employe.salaire%type) is
total_a_repartir Employe.salaire%type ;
nb_employes NaturalN ;
begin
select Sum (case
when e.salaire > Repartir.Seuil then e.salaire - Repartir.Seuil
else 0
end), count (*)
INTO total_a_repartir, nb_employes
from Employe e ;
if total_a_repartir is not null and total_a_repartir <> 0 then
update Employe
set salaire = Repartir.Seuil
where salaire > Repartir.Seuil ;
update Employe
set salaire = salaire + total_a_repartir / nb_employes ;
end if ;
end Repartir ;
La clause into de la requête est obligatoire, elle permet d’affecter aux variables PL/SQL les valeurs
des colonnes de l’unique ligne produite. Exception si 0 ou plus d’une ligne.
7.5. BLOC ANONYME 67

Q. 102 Pourquoi teste-t-on l’état de définition de total a repartir ?

Q. 103 Réécrire les deux update en un seul.

7.5 Bloc anonyme


Un bloc anonyme est compilé, exécuté immédiatement puis oublié.
La forme générale d’un bloc est :

bloc ::= [ declare


déclaration de variables, sous-programmes, . . .]
begin
séquence d’instruction
[ exception
traitements d’exception ]
end ;

Les blocs sont bien pratiques pour tester vite fait des sous-programmes, et ils n’ont probablement pas
d’autre utilité ! Par exemple, si on veut tester les deux sous-programmes précédents :
SQL> declare
P constant Positive Not Null := pgcd (33, 56) ;
begin
if P = 2 then ajouterClient (17, ’Tartempion’) ;
else ajouterClient (P, ’Bof’) ;
end if ;
end ;

7.6 Autres
PL/SQL autorise aussi la programmation récursive (éventuellement croisée) et l’emboı̂tement de sous-
programmes.

7.7 Modes des paramètres formels : in (par défaut), out, in out


procedure Solde_De (id in NUMBER, Solde out Natural) is

Ou plutôt :
procedure Solde_De (id in Client.id%type, Solde out Natural) is


Le type d’un paramètre  pas être contraint, par exemple on ne peut pas définir un
formel ne peut
paramètre par Nom in VARCHAR (20)  .
Les paramètres peuvent être de mode in, in out ou out et sont de mode in par défaut.

7.7.1 Passage sans copie : nocopy


Par défaut les paramètres out et in out sont passés par copie. Pour demander le passage par adresse
on utilise l’indication nocopy :
declare
type Platoon is Varray (200) of Soldier;
procedure reorganize (My_Unit in out nocopy Platoon) IS
68 CHAPITRE 7. INTRODUCTION À PL/SQL

Ceci n’est qu’une indication (hint) : le compilateur peut quand même choisir le passage par copie.

Suivant que les paramètres sont passés par copie ou par adresse, l’effet peut-être très différent quand le
sous-programme se termine par une exception non traitée. Lors d’un passage par copie, si la procédure
modificatrice (ici Incr_Copie) est abandonnée par une exception, les modifications des paramètres
formels ne sont pas reportées sur les paramètres effectifs, comme le montre l’exemple suivant :
create or replace package Global is
Mon_Exception exception ;
end Global ;

create or replace procedure Incr_Copie (i in out Natural) is


begin
i := i + 1 ;
raise Global.Mon_Exception ;
end Incr_Copie ;

create or replace function Test_Copie (i in Natural) return Natural is


vi Natural := i ;
begin
begin
Incr_Copie (vi) ;
exception
when Global.Mon_Exception then
null ;
end ;
return vi ;
end Test_Copie ;

select Test_Copie (3) from Dual ;


TEST_COPIE(3)
----------------------------
3

En revanche si le passage se fait par adresse (nocopy), alors les modifications des paramètres effectifs
seront effectives :
create or replace procedure Incr_Adresse (i in out nocopy Natural) is
begin
i := i + 1 ;
raise Global.Mon_Exception ;
end Incr_Adresse ;

create or replace function Test_Adresse (i in Natural) return Natural is


vi Natural := i ;
begin
begin
Incr_Adresse (vi) ;
exception
when Global.Mon_Exception then
null ;
end ;
return vi ;
end Test_Adresse ;

select Test_Adresse (3) from Dual ;


7.8. TYPES COMPOSÉS : LES RECORDS 69

TEST_ADRESSE(3)
----------------------------
4

7.8 Types composés : les records


type Duree is record (h SmallInt, m SmallInt) ;
type Reunion is record (debut Date, d Duree, lieu VarChar2 (20)) ;
Dans un record, il n’y a que des types simples s’il doit correspondre à un nuplet d’une table rela-
tionnelle, sinon on peut avoir des composants eux-mêmes composés (comme c’est le cas ici pour le
composant d de Reunion qui est lui-même un record).

Si on déclare la variable R Reunion ; on pourra accéder à ses champs par une notation pointée, par
exemple R.d.h pour manipuler le nombre d’heures de la durée.

7.9 Types composés : les collections


Les éléments d’une collections sont tous du même type et sont accessibles par leurs indices (entiers)

7.9.1 Tables à accès associatif (index-by)


En fait il s’agit de table de correspondance (les map de Java) pouvant être indicées par des nombres
ou des chaı̂nes de caractères.
type <type_name> is table of <element_type> [not null] index by Binary_Integer;

7.9.2 Tables (emboı̂tées nested) à trous


Elles sont indicées par des entiers et peuvent contenir un nombre quelconque d’éléments.
type <type_name> is table of <element_type> [not null] ;

Par exemple avec une initialisation littérale :


type Point is record (
X Number (5),
Y Number (5)
) ;
type Des_Points is table of Point ;
P Des_Points ;
P3 Des_Points := Des_Points((0, 0), (1, 0), (0, 1)) ;

nested tables (relationnel-objet) : indicées à partir de 1 et sa taille peut augmenter dynamiquement.


peut comporter des trous lorsqu’on en a supprimé des éléments avec la méthode delete : P.delete (3)
supprime l’élément d’indice 3 et crée un trou : P.exists (3) devient faux. (la méthode next permet
de sauter les trous). Une nested table peut correspondre à la valeur d’un attribut de tables (relationnel-
objet).
Quelques méthodes applicables a une table P :
– P.count nombre d’éléments contenus dans la collection : ne compte pas les trous (éléments détruits
par exemple).
– P.exists (i) vrai si le i-ième élément de la collection existe (pas un trou).
– P.first et P.last sont indéfinis si la collection est vide, sinon l’indice du premier/dernier élément.
– P.next (i) et P.prior (i) renvoient, à partir de la i-ième case, l’indice de la prochaine/précédente
case garnie ou null si cette case n’existe pas.
70 CHAPITRE 7. INTRODUCTION À PL/SQL

– consultation du X de l’élément d’indice 2 : P (2).X


– P.delete supprime tous les éléments de P
– P.delete (3) supprime le troisième élément de P
– P.extend allonge la table P d’un élément indéfini, P doit avoir été initialisée au préalable
– P.extend (15) allonge la table P de 15 éléments indéfinis
Exception : COLLECTION_IS_NULL, SUBSCRIPT_OUTSIDE_LIMIT.
On peut voir une utilisation intéressante de ces tables en section 7.21.1.

7.9.3 Tableaux dense : Varray


Vecteur de taille variable mais bornée lors de la déclaration du type. Le premier indice vaut 1 et le
dernier varie entre 0 et la taille maximum. Un VARRAY est toujours dense et conserve son indiçage
même après stockage dans une table (contrairement aux nested tables).
type <type_name> is {VARRAY | VARYING ARRAY} (<size_limit>)
OF <element_type> [not null];

En pratique on préfère les Varray pour les petites collections.

7.10 Les objets


Les types objets sont déclarés au niveau SQL, mais sont utilisables en PL/SQL. Nous les verrons plus
tard !

7.11 Typage implicite : %type et %rowtype


On peut demander que le type d’une variable ou d’un paramètre soit le même que celui d’une autre
variable, d’une colonne de table, de vue ou de curseur (attribut %type) ou du même type record
que le record correpondant à un nuplet d’une table, d’une vue ou d’un curseur (attribut %rowtype).
Curseurs : voir section 7.15 page 74.
un_client Client%rowtype ;
prenom Client.prenom%type ; -- Tuple de la table "Client"
nom un_client.nom%type ; -- Le record "un_client"

7.12 Structures de contrôle


if <predicat> then ... {elsif <predicat> then ...} [else ... ] end if ;

case <expr-ctr>
when <expr-choix> then <sequence-d-instructions>
{when <expr-choix> then <sequence-d-instructions>}
[else <sequence-d-instructions>]
end case ;
-- Le premier ’when’ dont <expr-choix> est égal à <expr-ctr> est pris,
-- si aucun ’when’ on prend le ’else’

loop ... exit [when <predicat>] ; ... end loop ;

while <predicat> loop ... end loop ;

for V in [reverse] Min..Max loop ... end loop ;


-- L’intervalle Min..Max est évalué avant de commencer le for avec les
-- valeurs courantes de Min et Max. L’intervalle ne change pas, m^ eme
-- si la boucle modifie Min ou Max, comme en Ada.
7.13. RÉSULTATS DE COMPILATION 71

Attention : l’ordre exit permet de continuer l’exécution après la boucle qui le contient (équivalent
du break de C et Java) : exit ne termine pas le sous-programme !

Quand un prédicat est indéfini (is null), l’aiguillage se fait comme si le prédicat était faux. Par
exemple, si la condition d’un exit when est indéfinie, on reste dans la boucle ( !).

7.13 Résultats de compilation


Pour voir les erreurs de compilation éventuelles sous SQL*PLUS :
show errors ; -- Commande SQL*PLUS : messages d’erreur de compilation
-- ou bien en accédant directement à la bonne vue du dictionnaire :
select * from user_errors ;

Pour voir les noms, types et états de validité des objets (tables, synonymes, contraintes, index, vues,
sous-programmes, paquetages, triggers, . . .) de l’utilisateur :
select Object_Type, Object_Name, Status
from user_objects
order by Object_Type, Object_Name ;

Et pour fabriquer les commandes permettant de faire le ménage :


-- Fabriquer les commandes pour faire le ménage
select ’drop ’ || Object_Type || ’ ’ || Object_Name || ’;’
from user_objects where Object_Type<>’INDEX’ ;

7.14 PL/SQL et le DML (select, insert, update, delete)


Il est très facile d’intégrer des ordres DML dans un programme PL/SQL : à chaque ordre DML cor-
respond une instruction PL/SQL ayant exactement la même syntaxe, (sauf pour l’instruction select,
voir plus loin)
create procedure Augmenter (Categorie in Employe.categorie%type,
Augmentation in PositiveN) is
begin
update Employe set salaire = salaire + Augmentation
where categorie = Augmenter.Categorie ;
end Augmenter ;
On voit que les valeurs des paramètres (ou des variables) PL/SQL s’utilisent très naturellement dans
l’écriture de l’ordre DML.

7.14.1 Select expressions into variables PL/SQL from . . .


L’instruction select introduit la clause obligatoire into permettant d’affecter le résultat de la
requête à des variables du programme.
create function Pourcentage (S in Employe.sexe%type) return Number is
total NaturalN ;
personnes NaturalN ;
begin
select count (*), count (case when sexe=Pourcentage.S then 1 else null end)
into total, personnes
from Employe ;
return (personnes / total) * 100 ;
end Pourcentage ;
72 CHAPITRE 7. INTRODUCTION À PL/SQL

7.14.2 Les exceptions de select into


La valeur d’une requête select into doit avoir exactement une ligne puisque la variable PL/SQL doit
recevoir exactement une valeur.sinon une exception prédéfinie sera déclenchée :

exception signification
No_Data_Found si la requête n’a aucune ligne.
Too_Many_Rows si la requête a plus d’une ligne.

Attention : No_Data_Found est gommée par un test fait dans la requête :

select <fonction-a-tester> from Dual ;

Si la fonction échoue avec l’exception No_Data_Found, celle-ci est récupérée par le select qui donne
alors un nuplet dont l’unique colonne est indéfinie ! En revanche No_Data_Found est bien visible quand
on teste avec un bloc anonyme.
L’exemple précédent (Nb_Employe) ne pose pas ce problème car un select count (*) sans group
by fournit toujours exactement un nuplet. On pourrait en revanche avoir une exception avec :
select * into Le_Client
from Client
where nom = ’toto’ ;
-- Exceptions :
-- No_Data_Found si aucun nuplet n’est sélectionné,
-- Too_Many_Rows si plus d’un nuplet est sélectionné.
...

si aucun ou plus d’un client s’appelle toto.


Donc, pour s’assurer qu’un seul client s’appelle ’toto’ avant de le traiter, on préférera écrire simple-
ment :
declare
nb Natural ; Le_Client Client%rowtype ;
begin
begin
select * into Le_Client from Client where Client.nom = ’toto’ ;
exception
when No_Data_Found then
raise_application_error (-20111, ’Aucun client ne s’’appelle toto’) ;
when Too_Many_Rows then
raise_application_error (-20111, ’Plus d’’un client s’’appelle toto’) ;
end ;
Traiter (Le_Client) ;
end ;
raise_application_error génère une erreur SQL et arrête l’exécution PL/SQL.
plutôt que d’écrire la chose coûteuse et compliquée suivante :
declare
nb Natural ;
Le_Client Client%rowtype ;
begin
select count (*) into nb from Client where Client.nom = ’toto’ ;
if nb = 0 then
raise_application_error (-20111, ’Aucun client ne s’’appelle toto’) ;
elsif nb > 1 then
raise_application_error (-20111, ’Plus d’’un client s’’appelle toto’) ;
end if ;
7.14. PL/SQL ET LE DML (SELECT, INSERT, UPDATE, DELETE) 73

select * into Le_Client from Client where Client.nom = ’toto’ ;


Traiter (Le_Client) ;
end ;

D’autant que le comportement de cette solution dépend du niveau d’isolation de la transaction


qui l’exécute : en isolation read committed, le second select pourrait échouer avec une excep-
tion No_Data_Found ou une exception Too_Many_Rows si, avant que cette requête ne commence son
exécution, une autre transaction a publié (par commit) une modification supprimant le client ’toto’
ou ajoutant de nouveaux clients ’toto’.

7.14.3 Les noms des colonnes des tables peuvent cacher les variables/paramètres
Réécrivons la fonction Nb_Employe en donnant au paramètre formel le même nom de la colonne :
create function Nb_Emp (Categorie in Employe.categorie%type) return Natural is
nb Natural ;
begin
select count (*) into nb
from Employe e
where e.categorie = Categorie ; -- Aı̈e !!!
return nb ;
end Nb_Emp ;

Le problème est alors dans la clause where de la requête la mention du paramètre Categorie est en
fait comprise comme la colonne Categorie de la table Employe1 ! Le test d’égalité vaudra toujours
vrai (sauf pour les employés dont la catégorie est indéfinie), et la fonction ne fait plus ce qu’elle est
censée faire.

Une solution consiste à donner aux variables et paramètres PL/SQL des noms différents des noms des
colonnes des tables manipulées comme cela est fait dans la première version de la fonction Nb_Employe.

Une autre solution, probablement plus fiable, consiste à préfixer le nom de variable ou de paramètre
par le nom de la structure qui le déclare, dans notre exemple il s’agit du nom de la fonction :
create function Nb_Emp (Categorie in Employe.categorie%type)
return Natural is
nb NaturalN ;
begin
select count (*) into nb
from Employe e where e.categorie = Nb_Emp.Categorie ; -- Ouf !!!
return nb ;
end Nb_Emp ;

7.14.4 Une fonction ne devrait pas tenter de modifier la base de données


En général cela provoque une erreur d’exécution.
Soit :
create table T (x Number(5)) ;

create or replace function F (x in Number) return Number is


begin
insert into t values (f.x) ;
return 2*x ;
1
Lors de la compilation de l’instruction DML, on cherche d’abord si une des tables du from possède une colonnne de
ce nom avant de s’intéresser aux variables locales et aux paramètres.
74 CHAPITRE 7. INTRODUCTION À PL/SQL

end F ;

Un select ne peut se servir de cette fonction car elle tente de modifier la base de données
select f (5) from dual ;
ORA-14551: impossible d’effectuer une opération DML dans une interrogation
ORA-06512: à "DURIF.F", ligne 3
ORA-06512: à ligne 1

En revanche, dans un bloc anonyme, tout se passe bien :


declare
b number (5) ;
begin
b := f (5) ;
end ;

pour autant qu’il soit raisonnable d’avoir des fonctions à effet de bord.

7.15 Requêtes à nombre inconnu de résultats : les curseurs


Toute instruction du DML (et seulement du DML) peut-être écrite directement à différents endroit
d’un programme PL/SQL.
L’accès aux informations relatives à l’exécution de ces instructions se fait soit par curseur implicite
soit par curseur explicite.

7.15.1 Curseurs explicites statiques : requête fixée à la déclaration du curseur


Il s’agit ici de récupérer les nuplets fournis par une requête pouvant renvoyer un nombre quelconque
de nuplets.
Un curseur explicite permet de balayer séquentiellement les nuplets obtenus par une requête. La requête
est fixée une fois pour toutes dès la déclaration du curseur, mais elle peut être paramétrée.
Déclaration :
cursor <cursor_name> [(parameter[, parameter]...)] [return <return_type>]
is <select_statement> ;

 
parameter ::= <parameter_name> [in] datatype [{:= | default} <expression>]

 
Le return_type doit être un record ou un %rowtype (un %rowtype est un record).

Deux exemples presque équivalents de curseurs sans paramètre


typé implicitement typé explicitement
type Nom_Prenom is record (
Nom Client.nom%type,
cursor Les_Nom_Prenom is
Prenom Client.prenom%type
select nom, prenom
) ;
from Client
cursor Les_Nom_Prenom return Nom_Prenom is
where id between 3 and 10;
select nom, prenom
from Client where id between 3 and 10;

Exemple de curseur paramétré avec Min et Max


cursor Les_Nom_Prenom_2 (Min in Number := 0, Max in Number := 100) is
select nom, prenom from Client where id between Min and Max ;
7.15. REQUÊTES À NOMBRE INCONNU DE RÉSULTATS : LES CURSEURS 75

C’est lors de l’ouverture du curseur (instruction open, voir 7.15.4, page 75) qu’on fixera les paramètres
effectifs.

7.15.2 Comment baptiser les curseurs


Comme pour toute entité d’un programme, bien choisir le nom d’un curseur est important pour la
lisibilité du programme — il n’y a rien de pire que d’appeler curseur un curseur.
Un curseur représente en fait un ensemble d’objets qu’il permet d’explorer séquentiellement et sans
possibilité de revenir sur un objet déjà exploré (on ne peut faire qu’avancer).

Une manière de nommer un curseur pourrait alors être Les_<nature des objets>, c’est exactement
ce qu’on a fait avec le curseur Les_Nom_Prenom.

7.15.3 Utiliser un curseur pour typer implicitement


Un curseur statique n’est pas une variable : on ne peut ni l’affecter ni le passer en paramètre de
sous-programme. Pour faire cela il faut plutôt utiliser des variables curseur, voir ?? page ??.

On peut utiliser l’attribut %ROWTYPE pour typer une variable à partir d’un curseur :
nom_prenom Les_Nom_Prenom%ROWTYPE ; -- nom_prenom.nom

7.15.4 Les opérations et attributs des curseurs


Il y a trois opérations :
open <curseur> [(parameter[, parameter]...)] ; c’est lors de l’ouverture qu’on fixe les va-
leurs effectives des éventuels paramètres formels du curseur. L’ouverture calcule immédiatement
le result set de la requête.
open Les_Nom_Prenom ; open Les_Nom_Prenom_2 (Max => 55) ;

fetch <curseur> into <variable> { , <variable> } ;

On peut récupérer le nuplet courant soit dans un record du même type que le curseur soit, dans
autant de variables scalaires que le curseur a de colonnes :
declare
np Nom_Prenom ; nom Client.nom%type ; prenom Client.prenom%type ;
begin
fetch Les_Nom_Prenom into np ;
fetch Les_Nom_Prenom into nom, prenom ;

close <curseur> ;
close Les_Nom_Prenom ;

On peut ensuite réouvrir le curseur.

7.15.5 Exceptions déjà attachées à des codes d’erreur Oracle


Chaque erreur Oracle possède un code d’erreur numérique.
Lorsqu’un ordre SQL embarqué dans du PL-SQL provoque une erreur Oracle, cette erreur se matérialise
par une exception soit anonyme, soit nommée si la configuration a associé un nom d’exception à cette
erreur Oracle.
Voici quelques-un des codes d’erreur Oracle qui sont déjà associés à des exceptions prédéfinies :
76 CHAPITRE 7. INTRODUCTION À PL/SQL

Exception Code d’erreur Explication


prédéfinie Oracle
Cursor_Already_Open -6511
Dup_Val_On_Index -1 duplication d’une clef existant déjà (insert ou update)
Invalid_Cursor -1001
No_Data_Found -1403 select ... into
Too_Many_Rows -1422 select ... into ou returning ... into
Zero_Divide -1476
Value_Error non respect des intervalles numériques, par exemple
affecter -1 dans une variable Natural
-02290 violation de contraintes check
-02291 Clef étrangère : ligne référencée inexistante
-02292 Tentative de suppression d’une ligne référencée
Un exemple récapitulatif où le pragma Exception_init associe une exception à une erreur SQL :
create table Client (nom Varchar2 (20) primary key,
solde Number (5) constraint Solde_Positif check (0 <= solde)) ;

create or replace procedure Debiter (C in Client.nom%type, M in Client.solde%type) is


Insolvable exception ;
pragma Exception_init (Insolvable, -02290) ; -- violation de contraintes
begin
update Client set solde = solde - M where nom = C ;
exception
when Insolvable then
raise_application_error (-20111, ’Client ’||nom||’ non solvable’) ;
end Debiter ;
– Lorsqu’un ordre SQL échoue, Oracle génère une erreur Oracle identifiée par un numéro négatif
et un message approprié. Si cet ordre SQL est embarqué dans du PL/SQL, alors il faut absolu-
ment que le programme PL/SQL soit informé de cet échec et c’est effectivement ce qui se passe :
l’erreur Oracle est automatiquement transformée en une exception PL/SQL qui pourra alors être
éventuellement traitée par le PL/SQL avec le mécanisme de traitement des exceptions.

Par exemple lors de la tentative d’insertion d’une clef dupliquée, Oracle génère l’erreur -1 , cette
erreur sera automatiquement traduite en l’exception prédéfinie Dup_Val_On_Index dans le code
PL/SQL.

Voici un code naı̈f qui exploite cette exception pour trouver une clef satisfaisante (cette méthode
marche mais il est clair qu’il serait déraisonnable de la mettre en exploitation !) :
SQL> create table T (id Number (5) primary key, nom Varchar2 (20)) ;

create procedure Ajouter (Le_Nom in T.nom%type) is


l_id T.id%type := 0 ;
begin
loop
begin
Insert into T (id, nom) values (l_id, Le_Nom) ;
exit ;
exception
when Dup_Val_On_Index then -- exception attachée à l’erreur SQL -1
l_id := l_id + 1 ;
end ;
end loop ;
7.15. REQUÊTES À NOMBRE INCONNU DE RÉSULTATS : LES CURSEURS 77

end Ajouter ;

Q. 104 Que fait la procédure Ajouter ? Est-ce une bonne idée ?

Q. 105 Que calcule la fonction F suivante :

create function F (A in NaturalN) return NaturalN is


I Natural := A ;
R Natural := 0 ;
begin
loop
begin
I := I - 1 ;
R := R + 2*I + 1 ;
exception
when Value_Error then
exit ;
end ;
end loop ;
return R ;
end F ;

Q. 106 Réécrire F pour éviter l’exception Value Error.


Tous les codes d’erreurs Oracle ne sont pas nécessairement pré-attachés à une exception PL/SQL.
Le programmeur PL/SQL peut alors réaliser cet association grâce au pragma Exception_Init (voir
la section 7.15.6).
– Inversement, lorsqu’un code PL/SQL échoue à cause d’une exception non traitée, il faut que le
moteur Oracle soit informé de cet échec : l’exception PL/SQL est alors transformée en une erreur
Oracle (voir la section 7.15.7). Cette erreur pourra porter un numéro et un message fixés par le
programmeur PL/SQL en utilisant la procédure raise_application_error :
SQL> declare
Fonds_Insuffisants exception ;
procedure Bof is ... -- peut déclencher Fonds_Insuffisants
begin
Bof ;
exception
when Fonds_Insuffisants then
raise_application_error (-20111, ’C’’est une erreur de comptabilité !’) ;
end ;
ORA-20111: C’est une erreur de comptabilité !

Si on n’utilise pas la procédure raise_application_error, c’est l’erreur Oracle -06510 qui sera
transmise au moteur SQL :
begin Bof ; end ;
ORA-06510: PL/SQL : exception définie par l’utilisateur non traitée

7.15.6 Récupérer les erreurs Oracle sous forme d’exception : pragma Excep-
tion Init
Il est donc possible, dans PL/SQL, de traiter les erreurs Oracle avec le mécanisme des exceptions.
De plus, avec le pragma Exception_Init, on peut associer explicitement une exception à un code
d’erreur Oracle, ce qui permet ensuite d’utiliser cette exception pour traiter l’erreur correspondante.
Par exemple, ici on s’arrange pour que l’exception Trop_De_Nuplets soit synonyme de Too_Many_Rows :
78 CHAPITRE 7. INTRODUCTION À PL/SQL

declare
Trop_De_Nuplets exception ;
pragma Exception_Init (Trop_De_Nuplets, -1422) ;-- erreur SQL de Too_Many_Rows
Le_Client Client%rowtype ;
begin
select * into Le_Client from Client where age=25 ;
exception
when Trop_De_Nuplets then -- idem : Too_Many_Rows
...
end ;

7.15.7 Les exceptions sont propagées vers SQL sous forme d’erreurs Oracle
Quand une exception n’est pas traitée par le code PL/SQL, elle est propagée vers oracle sous la forme
d’un code d’erreur accompagné d’un message :
declare
Mon_Exception exception ;
begin
raise Mon_Exception ;
end ;

ORA-06510: PL/SQL : exception définie par l’utilisateur non traitée


ORA-06512: à ligne 4

Propager un code d’erreur et un libellé explicites


Il est possible de choisir le code d’erreur et le message avec la procédure raise_application_error :
SQL> begin raise_application_error (-20101, ’employé encore à l’’essai’) ; end ;

ORA-20101: employé encore à l’essai


ORA-06512: à ligne 2

[−20999, −20000] est l’intervalle des numéros d’erreurs utilisables par le programmeur.

7.16 Les paquetages


Pour regrouper des types, des exceptions, des sous-programmes et des variables globaux.

La surcharge des noms de sous-programmes est possible.

Comme en Ada, on distingue la déclaration de paquetage qui définit des entités utilisables de l’extérieur
et le corps de paquetage qui implémente les sous-programmes annoncés dans la déclaration de paque-
tage et peut définir ses propres entités privées (non visibles de l’extérieur du paquetage).

create or replace package Gestion_Client is


function nombre (bout in VARCHAR2) return Number ;
procedure ajouter (nom in Client.nom%type, prenom in VARCHAR2 := ’--’) ;
end Gestion_Client ;

create or replace package body Gestion_Client is

courant number (4) ;


7.17. PL/SQL ET L’INTERACTION HOMME-MACHINE 79

function nombre (bout in VARCHAR2) return Number is


nb Number (6) ;
begin
select count (*) into nb
from Client where upper(nom) like modele ’%’ || upper(bout) || ’%’ ;
return nb ;
end nombre ;

procedure ajouter(nom in Client.nom%type, prenom in VARCHAR2 := ’--’) is


begin
insert into Client values (courant, nom, prenom) ;
courant := courant + 1 ;
end ajouter ;

begin -- Initialisation des variables du paquetage


-- Séquence exécutée une et une seule fois, à chaque début de session.
select nvl (max (id), 0) + 1 into courant from Client ;
end Gestion_Client ;

Q. 107 Aucune des exceptions No Data Found ou Too Many Rows ne peut être déclenchée par le select
de l’initialisation du paquetage, pourquoi ?
Remarques importantes : les variables globales ont une persistance limitée à la durée de la session :
à chaque début de session elles sont réinitialisées.
De même, la partie initialisation du corps de paquetage est exécutée une et une seule fois au début de
chaque session.

7.17 PL/SQL et l’interaction homme-machine


Un programme PL/SQL est fait pour travailler au cœur de la base de données, c’est à dire que sauf
peut-être en phase de test, il est destiné à être exécuté dans un environnement sans interaction homme-
machine (on pourrait dire qu’il est exécuté en batch ou encore off-line).

C’est probablement pourquoi il est tellement pénible de faire, en PL/SQL, de l’interaction homme-
machine, même de façon très primitive.
Le paquetage prédéfini DBMS_OUTPUT permet, sous SQL*Plus, d’écrire des messages dans une table
gérée par ce paquetage :
SQL> create procedure Imp_Pgcd (A in PositiveN, B in PositiveN) is
begin
Dbms_Output.Put_Line (’Pgcd = ’ || To_Char (pgcd (A, B))) ;
end Imp_Pgcd ;
Pour qu’en fin d’exécution les messages de la table soient affichés à l’écran il faut le demander à
SQL*Plus :
SQL> set serveroutput on
SQL> Execute Imp_Pgcd (45, 129) ;
Pgcd = 3
Tous les messages du programme sont donc affichés d’un seul coup lorsque ce dernier se termine.

7.18 Les tables mutantes ne peuvent être consultées ou modifiées


Pendant l’exécution d’un ordre la modifiant (insert, update ou delete), une table est dite mutante
c’est à dire que son état est instable. Oracle interdit alors de modifier ou consulter cette table par
80 CHAPITRE 7. INTRODUCTION À PL/SQL

l’exécution un ordre DML emboı̂té dans le premier.

Si on tente de le faire alors Oracle, logiquement, fait avorter l’ordre principal avec l’erreur ORA-04091.

Soit la fonction parfaitement correcte :


create table Etudiant (note Number (4, 2)) ;
insert into Etudiant values (15.0) ;

create function Moyenne return Etudiant.note%type is


M Etudiant.note%type ;
begin
select AVG (e.note) into M from Etudiant e ;
return M ;
end Moyenne ;
Si on tente d’exécuter :
delete from Etudiant where note < Moyenne ;
ORA-04091: la table ETUDIANT est en mutation ; la fonction ne peut la voir
ORA-06512: à "MOYENNE", ligne 4
La table Etudiant n’étant pas vide, Oracle déclenche une erreur de table mutante, et c’est tant mieux !
En effet ce delete n’aurait aucun sens s’il était effectivement exécuté puisqu’après la suppression d’un
étudiant la fonction Moyenne ne renverrait probablement pas la même valeur et ainsi ce delete ne
serait pas équitable pour tous les étudiants.

Q. 108 Réécrire correctement cette fonctionalité sous forme d’une procédure PL/SQL.

Q. 109 Pouvez-vous expliquer pourquoi le delete suivant fonctionne correctement ?

delete from Etudiant e


where e.note < (select AVG (note) from Etudiant) ;

7.19 Question de style, de sûreté et d’efficacité


Utiliser un traitement d’exception pour prendre en compte les erreurs de paramètre pour les sous-
programmes stockés donne un code souvent plus efficace et plus clair.

Par exemple, voici deux tables :


create table Mere (id_mere Number (5) primary key) ;

create table Fils (


id_fils Number (5) primary key,
id_mere Number (5) references Mere (id_mere)
) ;
La table Fils comporte une clef étrangère vers la table Mere.

Lors de l’insertion d’une ligne dans Fils, la pratique courante consiste souvent à vérifier d’abord,
grâce à une requête la présence de la clef dans Mere.

Or ceci est inutile, car lors de l’insertion, si la clef étrangère n’apparaı̂t pas dans Mere alors Oracle
déclenche l’erreur -02291 (touche parent introuvable) : il suffit de récupérer cette erreur sous forme
d’une exception.

Voici deux versions de la procédure d’ajout d’un fils qui ont, grosso modo, le même comportement
(sauf si on se place dans un contexte multi-transactionnel) :
7.20. RÉCUPÉRATION DES VALEURS PRODUITES PAS LE SGBD (DML RETURNING) 81

version 1 version 2
create procedure Ajouter_Fils ( create procedure Ajouter_Fils (
f in Fils.id_fils%type, f in Fils.id_fils%type,
m in Mere.id_mere%type) m in Mere.id_mere%type)
is is
begin Mere_inexistante exception ;
if m is not null then pragma Exception_init
-- Tester si la mère existe (Mere_inexistante, -02291) ;
declare begin
nb Natural ; insert into Fils values (f, m) ;
begin exception
select count (*) into nb when Mere_inexistante then
from Mere raise_application_error
where id_mere = m ; (-20100, ’Mère inexistante’);
if nb = 0 then end Ajouter_Fils ;
raise_application_error
(-20100, ’Mère inexistante’);
end if ;
end ;
end if ;
-- La mère existe ou est indéfinie
insert into Fils values (f, m) ;
end Ajouter_Fils ;

Q. 110 Quelle version préférez-vous ? pourquoi ?


La version 2 (avec traitement d’exception) est aussi moins complexe en terme d’ordres SQL (un seul
ordre au lieu de deux), elle sera certainement plus facile à prendre en compte dans un environnement
transactionnel. En particulier, si la transaction qui exécute la version 1 est en read committed, il est
possible que le insert échoue bien que la vérification préalable ait confirmé la présence de la mère :
en read committed une instruction SQL voit les modifications validées avant qu’elle ne commence :
il se pourrait qu’une autre transaction détruise la mère juste après la vérification de son existence et
qu’elle valide cette suppression juste avant la tentative d’insertion.
Q. 111 Écrire une procédure stockée qui tente d’ajouter une commande d’une quantité d’un produit
à un client à condition que le client ait un solde suffisant pour payer toutes ses commandes.

7.20 Récupération des valeurs produites pas le SGBD (DML retur-


ning)
Cette fonctionnalité est particulièrement précieuse lors du développement logiciel : elle simplifie le
programme et le rend plus efficace. PostgreSQL, depuis sa version 8.2, propose lui aussi une fonction-
nalité équivalente.

Lors d’une instruction DML les nouvelles valeurs d’une ligne insérée ou modifiée peuvent être produites
par le SGBD lui-même et donc inconnues de la procédure :
1. lors d’une insertion, la clef est obtenue grâce à une sequence Oracle :
insert into Employe (id, nom, salaire)
values (Generateur_De_Clef.nextval, ’Dupont’, 2000.0) ;
On ne peut pas retrouver l’id de ce nouvel employé, si un ensemble d’autres colonnes n’est pas
aussi une clef.
2. c’est le update qui augmente le salaire :
update Employe
82 CHAPITRE 7. INTRODUCTION À PL/SQL

set salaire = salaire * 1.1


where id = 299 ;
La procédure stockée peut avoir besoin de connaı̂tre la nouvelle valeur du salaire, elle peut aussi vouloir
connaı̂tre les anciennes valeurs d’une ligne détruite.
Cela peut se faire grâce à la clause returning disponible en fin de chacune des instructions DML
(insert, update et delete) :
returning <expression> {, <expression>} into <variable> {, <variable>}
– lors du insert
insert into Employe (id, nom, salaire)
values (Generateur_De_Clef.nextval, ’Dupont’, 2000.0)
returning id, nom, salaire into id, nom, salaire ;
– lors du update
update Employe
set salaire = salaire * 1.1
where id = 299
returning salaire into nouveau_salaire ;
– lors du delete d’une ligne, on souhaite récupérer le contenu de cette ligne dans des variables de la
procédure. On pourrait écrire :
select nom, prenom into nom, prenom
from Employe
where id = 299 ;

delete from Employe


from Employee
where id = 299 ;
mais cette solution est erronée si la transaction est read committed et que la modification suivante :
update Employe
set id = case when id=298 then 299 else 298 end
where id in (298, 299) ;
est validée par une autre transaction entre la requête et l’instruction delete puisque lors du delete,
299 n’est plus le même employé !
Q. 112 Comment corriger simplement ce problème (voir le chapitre sur les transactions) ?
L’écriture suivante est certainement bien plus élégante et fiable :
delete from Employee
where id = 299
returning nom, prenom into nom, prenom ;
Seule l’exception Too_Many_Rows sera déclenchée si plus d’une ligne est modifiée par l’ordre DML. Si
aucune ligne n’est modifiée, les variables de into seront indéfinies, l’expression SQL%rowcount s’avère
alors utile pour détecter ce problème.

7.21 Amélioration des performances du code PL/SQL


Un des aspects coûteux de PL/SQL est que l’exécution de chaque ordre SQL demandée par PL/SQL
requiert de passer du monde PL/SQL au monde SQL.
Oracle propose (au moins) deux outils pour diminuer le nombre de passages d’un monde à l’autre :
– la structure de contrôle Forall qui transforme une suite d’itérations sous forme d’un travail batch
qui ne nécessitera qu’un seul passage d’un monde à l’autre au lieu d’autant de passage qu’il y a
d’itérations dans une boucle normale,
– la clause bulk collect qui permet de récupérer d’un seul coup dans une ou plusieurs collections
PL/SQL, un nombre inconnu a priori de lignes (mais probablement pas trop élévé) qui sont soit la
valeur d’une requête soit les nouvelles valeurs des lignes modifiées par un ordre DML muni de la
clause returning ... into ....
7.21. AMÉLIORATION DES PERFORMANCES DU CODE PL/SQL 83

7.21.1 La clause bulk collect


Cette clause bulk collect permet de récupérer d’un seul coup dans une ou plusieurs collections
PL/SQL :
– tous les résultats produits par une requête,
– lors du fetch ... into ... d’un curseur,
– ou la clause returning ... into ... d’un ordre DML.
Elle s’écrit toujours immédiatement avant le mot clef into.
Bien entendu les variables PL/SQL figurant après into doivent alors être des collections, par exemple
des table (voir section 7.9.2).
Exemple avec select :
create or replace procedure Tranche (smin in Employe.salaire%type,
smax in Employe.salaire%type) is
type Des_Employes is table of Employe%rowtype ;
Les_Employes Des_Employes ;
begin
select * BULK COLLECT INTO Les_Employes
from Employe
where salaire between smin and smax ;
if Les_Employes.count != 0 then
for I in Les_Employes.First..Les_Employes.Last loop
Dbms_Output.Put_Line (Les_Employes (I).id || ’: ’ ||
Les_Employes (I).nom) ;
end loop ;
end if ;
end Tranche ;

On peut aussi mettre une table par colonne fabriquée par la requête ou le curseur ou la clause retur-
ning.

Exemple avec fetch, c’est à dire un curseur :


create or replace procedure Tranche (smin in Employe.salaire%type,
smax in Employe.salaire%type) is
cursor Employes return Employe%rowtype is
select *
from Employe
where salaire between smin and smax ;
type Des_Employes is table of Employe%rowtype ;
Les_Employes Des_Employes ;
begin
open Employes ;
fetch Employes BULK COLLECT INTO Les_Employes ;
close Employes ;
if Les_Employes.count != 0 then
for I in Les_Employes.First..Les_Employes.Last loop
Dbms_Output.Put_Line (Les_Employes (I).id || ’: ’ ||
Les_Employes (I).nom) ;
end loop ;
end if ;
end Tranche ;

Exemple avec update :


create or replace procedure Augmenter (smin in Employe.salaire%type,
84 CHAPITRE 7. INTRODUCTION À PL/SQL

smax in Employe.salaire%type,
augm in Employe.salaire%type) is
type Nouvel_Etat_Employe is record (
id Employe.id%type,
nom Employe.nom%type,
nouveau_salaire Employe.salaire%type
) ;
type Des_Employes is table of Nouvel_Etat_Employe ;
Les_Employes Des_Employes ;
begin
update Employe
set salaire = salaire + augm
where salaire between smin and smax
returning id, nom, salaire BULK COLLECT INTO Les_Employes ;
if Les_Employes.count != 0 then
for I in Les_Employes.First..Les_Employes.Last loop
Dbms_Output.Put_Line (Les_Employes (I).id || ’: ’ ||
Les_Employes (I).nom || ’ ’ ||
Les_Employes (I).nouveau_salaire) ;
end loop ;
end if ;
end Augmenter ;

Pour un insert, update ou delete on est obligé de préciser les colonnes (* ne convient pas).

7.21.2 Limiter le nombre de lignes récupérées par fetch ... bulk collect
Seulement avec un curseur (fetch) on peut spécifier un nombre maximum de lignes à récupérer à
chaque fois, l’utilisation du curseur doit alors se faire à nouveau dans une boucle. La limite est donnée
après le mot clef limit. Voici une reprise de l’exemple précédent avec limit :
create or replace procedure Tranche (smin in Employe.salaire%type,
smax in Employe.salaire%type) is
cursor Employes return Employe%rowtype is
select *
from Employe
where salaire between smin and smax ;
type Des_Employes is table of Employe%rowtype ;
Les_Employes Des_Employes ;
Max_Lignes Natural := 2 ;
begin
open Employes ;
loop
fetch Employes BULK COLLECT INTO Les_Employes LIMIT Max_Lignes ;
exit when Les_Employes.count = 0 ;
for I in Les_Employes.First..Les_Employes.Last loop
Dbms_Output.Put_Line (Les_Employes (I).id || ’: ’ ||
Les_Employes (I).nom || ’ ’ ||
Les_Employes (I).salaire) ;
end loop ;
end loop ;
close Employes ;
end Tranche ;
7.21. AMÉLIORATION DES PERFORMANCES DU CODE PL/SQL 85

Q. 113 Ici il ne faut surtout pas utiliser Employes%notfound pour sortir de la boucle. Pourquoi à
votre avis ?
Chapitre 8

Les triggers

DDL
Un trigger est un bout de code qui sera exécuté à chaque fois qu’un événement particulier se produira
sur une table particulière. Un événement correspond à la modification d’une table (insert, update
ou delete).
La programmation par trigger est donc une forme de programmation événementielle.
Un trigger est une procédure compilée (en pcode) et stockée dans le dictionnaire, qui s’exécute auto-
matiquement chaque fois que l’événement déclenchant se produit.
Les triggers existent dans la plupart des SGBD (par exemple Oracle, PostgreSQL, MySQL 5.1 qui ne
permet que les triggers ligne et pas plus d’un trigger before et d’un trigger after par table)
Sous Oracle, le corps du trigger s’écrit en PL/SQL (on peut aussi utiliser C ou Java depuis Oracle 8).

Les triggers peuvent être utilisés pour garantir des propriétés que les contraintes déclaratives (check)
ne peuvent garantir. Un trigger qui échoue par une exception fait échouer l’ordre DML qui a provoqué
sont exécution, la table est alors remise dans son état d’origine.

Ils peuvent aussi servir à rendre la base plus dynamique ; par exemple, on peut grâce au trigger, es-
pionner les opérations faites sur la table des salaires en enregistrant dans une autre table l’heure et
l’identité de celui qui a tenté la modification.

La programmation de triggers est une tâche délicate puisqu’elle insère du code dans le fonctionnement
normal du moteur SQL.

8.1 Deux utilisations possibles des triggers


– Pour garantir qu’une propriété est vérifiée, si on ne peut l’exprimer de façon déclarative.
L’algorithme du trigger teste la propriété, si elle est vérifiée il n’y a rien d’autre à faire, si elle n’est
pas vérifiée le trigger appelle la procédure raise_application_error pour déclencher une erreur
et faire ainsi avorter l’ordre DML : la table sera automatiquement remise dans son état initial.

Exemples :
– garantir que le nombre d’étudiants inscrits à une unité d’enseignement est toujours inférieur à sa
capacité d’accueil.
– garantir que le salaire d’un employé est inférieur à celui de son supérieur.
Attention : quand c’est possible, une contrainte déclarative est toujours préférable à l’introduction
d’un trigger.
– Pour automatiser des traitements lors de certains événements, ce type de trigger permet
de mettre en œuvre la notion de BD active.

Exemples :
– on veut conserver la trace de toutes les modifications appliquées à une table en enregistrant dans
une autre table le nom de l’auteur de la modification et la date de modification.

86
8.2. STRUCTURE D’UN TRIGGER 87

– créer une commande de produit à chaque fois que sa quantité en stock passe en dessous d’un
certain seuil.

8.2 Structure d’un trigger


create [or replace] trigger <Nom-du-Trigger>
<instant>
<liste-événements> on <Nom-Table>
[for each row [when ( <Condition> ) ]]
<bloc-anonyme> ;

drop trigger <Nom-du-Trigger> ;


Le drop d’une table détruit automatiquement les triggers qui lui sont attachés.
<instant> ::= before | after
<liste-événements> ::= <événement> { or <événement> }
<événement> ::= delete | insert | update [ of <liste-colonnes> ]
<liste-colonnes> ::= <nom-colonne> { , <nom-colonne> }

8.2.1 before et after


Le trigger sera déclenché avant ou après la modification :
– déterminer si modification autorisée : before ou after,
– si le trigger doit fabriquer une valeur à mettre dans la table : before,
– si la modification doit d’abord être terminée : after.

8.2.2 Les événements


La liste d’événement indique quels sont les ordres DML qui provoqueront le déclenchement du trigger.
On peut donner une liste de colonnes à l’événement update. Il suffira qu’au moins une de ces colonnes
soit modifiée par le update pour que le trigger soit déclenché.

8.2.3 Granularité du trigger


Un trigger peut-être destiné à être déclenché soit :
– exactement une fois avant (before) ou après (after) l’exécution complète de l’ordre DML l’ayant
provoqué : il s’agit d’un trigger instruction voyant la BD avant toute modification si before ou
après toutes les modifications si after. Un tel trigger voit donc la BD dans un état stable et peut
donc consulter toutes les tables y compris celle à laquelle il est attaché.
– exactement une fois avant (before) ou après (after) la modification de chaque ligne : il s’agit
d’un trigger ligne. Autrement dit il sera déclenché autant de fois qu’il y aura de lignes modifiées
(éventuellement zéro fois si aucune ligne n’est modifiée). Pour chaque ligne modifiée le trigger est
exécuté et dispose de l’ancienne (préfixe old) et nouvelle valeur (préfixe new) de la ligne. Un tel
trigger étant exécuté pendant l’exécution de l’instruction DML la table en cours de modification
est dans un état instable (mutating table), le trigger ne peut donc pas la consulter (Oracle déclenche
une erreur SQL si on tente de le faire), en revanche il peut consulter toutes les autres tables de la BD.

PostgreSQL permet, de façon cohérente, à un trigger ligne after de consulter la table en cours de
modification en fait cela est cohérent car les triggers ligne after ne sont déclenchés qu’après que la
table ait été complètement modifiée (voir la section 8.10).

trigger instruction : for each row absent


Si for each row est absente, c’est un trigger de niveau instruction DML : il sera appelé exactement
une fois, avant ou après l’exécution de l’instruction DML. Il n’y a alors pas de ligne courante (ni old
ni new).
88 CHAPITRE 8. LES TRIGGERS

PostgreSQL a le mérite de permettre de dire explicitement qu’il s’agit d’un trigger instruction avec le
qualificatif for each statement. Cependant, comme en Oracle, si aucun des deux qualificatifs n’est
donné, il s’agit d’un trigger instruction.

trigger ligne : for each row présent


for each row implique que le trigger est un trigger ligne, il sera déclenché pour chaque tuple modifié :
si on supprime 10 lignes, le trigger sera déclenché 10 fois, si on supprime 0 ligne le trigger sera déclenché
0 fois.
Dans un trigger ligne, la ligne qui fait l’objet de la modification peut être consultée sur ses anciennes
valeurs (:old pour toute la ligne, :old.col pour une colonne particulière) et sur ses nouvelles valeurs
(:new pour toute la ligne, :new.col pour une colonne particulière).
Suivant l’instruction déclenchante :old ou :new n’ont pas forcément de sens :

:old.col :new.col
insert is null valeur insérée
delete valeur originale is null
update valeur originale nouvelle valeur ou valeur originale si pas de nouvelle valeur

:new et :old ont les mêmes valeurs, que le trigger soit before ou after mais une modification de :new
n’aura d’effet que dans un trigger before.

:old et :new ne peuvent être utilisés que dans le bloc anonyme du trigger.

Pour insert et update, on peut réaffecter :new dans le trigger, mais seulement pour un trigger before.
Un autre trigger ligne after verra les modifications apportées à :new par un trigger ligne before.

La clause when (Condition sur la ligne courante)


Uniquement pour les triggers ligne : le bloc anonyme ne sera exécuté que si la condition est vraie. En
particulier si la condition du when est unknown le trigger n’est pas déclenché.
La condition ne peut utiliser de fonction PL/SQL ni contenir de sous-requête et on doit utiliser les
préfixes old. et new. pour accéder aux noms de colonnes de la ligne courante.
L’intérêt de when est d’éviter le plus possible l’exécution du bloc anonyme car cette exécution nécessite
de passer du monde SQL au monde PL/SQL ce qui est coûteux en temps CPU.

8.2.4 Le bloc anonyme


C’est du PL/SQL.

Pour les triggers ligne, utilisation obligatoire des préfixes :old. et :new. pour désigner les colonnes
en cours de modification.

8.2.5 Prédicats utilisables dans le code PL/SQL


Pour l’écriture du bloc anonyme, on dispose des prédicats :

inserting deleting updating [ ( <nom-colonne> ) ]

Cela permet d’écrire un seul trigger pour gérer plusieurs événements.

8.3 Instants de déclenchement des triggers instruction et ligne


Sur le fonctionnement décrit ci-après on voit que les triggers instruction sont exécutés avant ou après
l’instruction de mise à jour, c’est à dire quand la table est dans un état stable (non mutante).
8.4. EXEMPLES DE TRIGGERS GARANTISSANT LE RESPECT D’UNE PROPRIÉTÉ 89

En revanche, les triggers ligne sont exécutés pendant l’exécution de l’instruction de mise à jour, c’est
à dire à un moment où la table n’est pas dans un état stable (elle est dite mutating).

État stable de la table, elle


est observable et modifiable 1. Exécution des triggers instruction before
par les triggers instruction
2. Début de l’instruction DML
3. Pour chaque ligne sélectionnée par la clause where
(a) Si update : calcul des valeurs new par la clause set
État instable de la table
(mutante), elle n’est ni ob- (b) Exécution des triggers ligne before
servable ni modifiable par les (c) Si insert ou update, inscription de new dans la table, si delete,
triggers ligne suppression de la ligne.
(d) Exécution des triggers ligne after
4. Fin de l’instruction DML
État stable de la table, elle
est observable et modifiable 5. Exécution des triggers instruction after
par les triggers instruction
Vérification des contraintes déclaratives de la table (si non différées en
fin de transaction)
Quand plusieurs triggers sont déclenchés par le même événement, ils sont exécutés séquentiellement
dans un ordre quelconque.

On voit que les triggers ligne remettent en cause l’apparente atomicité des ordres DML en permet-
tant d’injecter du code (celui des triggers ligne) qui sera exécuté pendant l’exécution de l’ordre DML.

Si un trigger échoue en déclenchant une erreur, quelle qu’elle soit, alors Oracle garantit que la base
est remise dans l’état dans lequel elle était avant l’exécution de l’instruction ayant déclenché ce ou ces
triggers (l’effet des ces triggers est lui aussi gommé).

8.4 Exemples de triggers garantissant le respect d’une propriété


Garantir une propriété consiste à faire échouer toute modification qui casse la propriété à maintenir.
Si un trigger déclenche une erreur SQL, alors l’ordre DML est abandonné et la table est remise dans
son état d’origine.
Supposons que la base de données doive à tout moment vérifier une propriété P . Si P ne peut être
exprimée de façon déclarative (contrainte de table ou assertion), alors on peut mettre en place un
système de triggers qui feront échouer tout ordre DML (insert, update, delete) qui aurait pour
conséquence de casser la propriété.

8.4.1 Un trigger instruction de contrainte : contrôle d’horaire


On veut empêcher toute modification de la table Salaire en dehors des heures d’ouverture du service :
create table Salaire (nom VARCHAR (20), salaire Number (7, 2)) ;

create or replace trigger Controler


before insert or delete or update on Salaire
declare
h constant Natural := to_number (to_char (Sysdate,’HH24’)) ;
begin
if h < 8 or 17 <= h then
raise_application_error (-20111, ’modification interdite !’) ;
90 CHAPITRE 8. LES TRIGGERS

end if ;
end ;

Q. 114 Peut-on garantir cette propriété sans passer par un trigger ?

Q. 115 Pourquoi, syntaxiquement, Controler est-il un trigger instruction ?

Q. 116 Le trigger est-il toujours correct si on remplace before par after ?

update Salaire set salaire = 0 ; -- erreur détectée m^


eme sur une table vide

8.4.2 Un trigger ligne de contrainte : salaires croissants dans le temps


On veut garantir (1) que le salaire d’un employé ne décroı̂t jamais et (2) qu’un salaire défini ne peut
pas devenir indéfini :
create or replace trigger Salaire_Croissant
before update of salaire on Salaire
for each row
when (old.salaire is not null and
(new.salaire is null or new.salaire < old.salaire))
begin
raise_application_error (-20111, ’nouveau salaire indéfini ou décroissant !’) ;
end Salaire_Croissant ;

Q. 117 Peut-on garantir cette propriété sans passer par un trigger ?

Q. 118 Enrichir la condition de when pour n’exécuter le bloc anonyme qu’en cas d’erreur de salaire.

Q. 119 Écrire un trigger qui garantit qu’une fois défini le salaire est constant.

8.5 Exemples de triggers rendant active la base


8.5.1 Mettre à jour une table de synthèse : information redondante
Une entreprise se compose de services, un employé travaille dans exactement un service.
create table Employe (
create table Service ( id Number (5) primary key,
id Number (5) primary key, nom Varchar2 (20),
intitule Varchar2 (20) salaire Number (15),
) ; service references Service (id)
) ;
Il se trouve que l’équipe de direction consulte très souvent pour chaque service le nombre d’employés
et le salaire moyen. Pour rendre ces consultations plus efficaces il est possible de stocker les résultats
dans une table de synthèse qui sera mise à jour, par des triggers, lors de chaque modification d’une
des deux tables.
create table Synthese (
id Number (5) primary key,
intitule Varchar2 (20),
effectif Number (5) default 0, -- nombre d’employes
som_sal Number (25) default 0, -- somme des salaires definis de ce service
nb_sal_def Number (5) default 0 -- nombre d’employes ayant un salaire defini
) ;
Un trigger ligne pour chacune des deux tables est nécessaire :
8.5. EXEMPLES DE TRIGGERS RENDANT ACTIVE LA BASE 91

create or replace trigger Modif_Service


after insert or update or delete on Service
for each row
begin
if inserting then
insert into Synthese (id, intitule) values (:new.id, :new.intitule) ;
elsif updating then
update Synthese
set id = :new.id, intitule = :new.intitule
where id = :old.id ;
else -- deleting evidemment
delete from Synthese where id = :old.id ;
end if ;
end ;

create or replace trigger Modif_Employe


after insert or update or delete on Employe
for each row
declare
procedure Ajouter (Serv in Service.id%type, Sal in Employe.salaire%type) is
begin
if Serv is null then return ; end if ;
update Synthese
set effectif = effectif + 1,
som_sal = som_sal + nvl (Sal, 0),
nb_sal_def = nb_sal_def + case when Sal is null then 0 else 1 end
where id = Serv ;
end Ajouter ;

procedure Retirer (Serv in Service.id%type, Sal in Employe.salaire%type) is


begin
if Serv is null then return ; end if ;
update Synthese
set effectif = effectif - 1,
som_sal = som_sal - nvl (Sal, 0),
nb_sal_def = nb_sal_def + case when Sal is null then 0 else -1 end
where id = Serv ;
end Retirer ;

procedure Modifier (Serv in Service.id%type,


Old_Sal in Employe.salaire%type,
New_Sal in Employe.salaire%type) is
begin
update Synthese
set som_sal = som_sal + nvl (New_Sal, 0) - nvl (Old_Sal, 0),
nb_sal_def=nb_sal_def + case
when Old_Sal is null and New_Sal is null then 0
when New_Sal is null then -1
when Old_Sal is null then 1
else 0
end
where id = Serv ;
end Modifier ;
begin
92 CHAPITRE 8. LES TRIGGERS

if inserting and :new.service is not null then


Ajouter (:new.service, :new.salaire) ;
elsif updating then
if :old.service = :new.service then
Modifier (:new.service, :old.salaire, :new.salaire) ;
else -- 2 services differents ou 1 ou 2 indefinis
Retirer(:old.service, :old.salaire); Ajouter(:new.service, :new.salaire);
end if ;
else -- deleting evidemment
Retirer (:old.service, :old.salaire) ;
end if ;
end ;
Cette technique va ralentir les ordres DML (insert, update, delete), mais s’ils sont relativement
rares et que les requêtes de synthèse sont très fréquentes, cela peut être intéressant.
Les modifications faites par un trigger sont annulées si l’instruction qui l’a déclenché échoue.
Q. 120 Quel problème se poserait pour le maintien du salaire maximum d’un service dans la table
Synthese si le salaire d’un employé peut décroı̂tre ?

8.5.2 Un trigger instruction d’audit


On souhaite maintenant garder trace de toutes tentative de modification de la table Salaire en
enregistrant la date, l’utilisateur et le type de modification (insert, update ou delete) :
create table Audit (quand Date, qui Varchar2 (20), quoi Varchar2 (10)) ;

create or replace trigger Auditeur


after insert or update or delete on Salaire
begin
if inserting then insert into Audit values (sysdate, user, ’insert’) ;
elsif updating then insert into Audit values (sysdate, user, ’update’) ;
else -- deleting évidemment
insert into Audit values (sysdate, user, ’delete’) ;
end if ;
end Auditeur ;

Q. 121 Si le trigger Auditeur était before, cela changerait-il quelque chose ?


Les triggers instruction s’exécutent soit avant (before) soit après (after) l’instruction de mise à jour :
la table sur laquelle ils s’appliquent n’est donc pas considérée comme mutante et ils sont autorisés à
consulter ou modifier la table elle-même.

8.5.3 Un trigger ligne pour cadrer les notes entre 0 et 20


Soit :
create table Les_Notes (mat Number (2), note Number (2)) ;

On souhaite que lors de la modification d’une note celle-ci soit éventuellement recadrée entre 0 et 20 :
create or replace trigger Cadrer_Note
before insert or update of note on Les_Notes
for each row when (new.note < 0 or 20 < new.note)
begin
:new.note := case when :new.note < 0 then 0 else 20 end ;
end Cadrer_Note ;
8.5. EXEMPLES DE TRIGGERS RENDANT ACTIVE LA BASE 93

Q. 122 Que se passe-t-il si new.note est indéfinie ?

Q. 123 Peut-on remplacer impunément before par after ? voir la section 8.3 page 88
Voici alors ce qui se passe lors d’une augmentation de 1 point des notes de la matière 2 par la
commande :
update Les_Notes set note = note + 1 where mat = 2 ;

contenu après clause après exécution tuple


initial set du trigger inscrit
mat note :old.note :new.note :old.note :new.note mat note
1 13

2 7 7 8 trigger non déclenché car when non satisfait

2 20 20 21 20 20 2 20

1 14

2 8 8 9 trigger non déclenché car when non satisfait

1 9
Chronologiquement, voici ce qui se passe :
1 Début de la commande update
2 Sélection et lecture dans old du premier tuple
La clause set calcule new.note : 8
Le trigger Cadrer_Note s’arrête sur when
Écriture du tuple avec new.
3 Sélection et lecture dans old du deuxième tuple
La clause set calcule new.note : 21
Exécution du trigger Cadrer_Note
Écriture du tuple avec new.
4 Sélection et lecture dans old du troisième et dernier tuple
La clause set calcule new.note : 9
Le trigger Cadrer_Note s’arrête sur when
Écriture du tuple avec new.
5 Fin de la commande update
On voit que lorsque le trigger s’exécute la table Les_Notes est en cours de modification, on dit qu’elle
est mutante ou mutating.
Pour cette raison, un trigger ligne ne peut ni consulter ni modifier la table à laquelle il est attaché
sous peine d’un déclenchement d’erreur de table mutante.

8.5.4 Un trigger ligne pour une BD active : commande automatique


Un magasin veut maintenir la disponibilité de ses produits en créant automatiquement une commande
pour un produit dont la quantité en stock plus la quantité commandée devient inférieure à un seuil
spécifique au produit.
create table Produit (
create table Commande (
id Number (3) primary key,
produit Number (3)
q_stock Number (3),
primary key
q_seuil Number (3),
references Produit (id),
constraint QS_Naturel
quantite Number (3)
check (q_stock >= 0 and q_seuil >= 0)
) ;
) ;
94 CHAPITRE 8. LES TRIGGERS

create or replace trigger Commande_Automatique


after insert or update of q_stock
on Produit
for each row when (new.q_stock is not null and new.q_seuil is not null and
new.q_stock < new.q_seuil)
begin
-- Ici on a : new.q_stock is not null et new.q_seuil is not null
update Commande
set quantite = :new.q_seuil - :new.q_stock
where produit = :new.id ;
if SQL%rowcount = 0 then
-- Il n’y avait pas de commande pour ce produit
insert into Commande values (:new.id, :new.q_seuil - :new.q_stock) ;
end if ;
end Commande_Automatique ;

Q. 124 Peut-on se passer des deux premiers tests de la clause when ?

Q. 125 La modification de quelle colonne a-t-on oublié de surveiller ?


Revoir la section 5.3.6 page 49 avant de résoudre la question suivante.
Q. 126 Quand un produit est supprimé, on ne veut plus le commander. Implanter.

8.6 Table mutante (Mutating table)


La notion de table mutante n’a strictement rien à voir avec le fait que plusieurs transactions accèdent
simultanément à la même table. En effet Oracle garantit l’étanchéité entre les transactions grâce à des
verrous et à un protocole de gestion de versions multiples d’un même nuplet (voir le chapitre 13).

La notion de table mutante est strictement interne à une seule transaction : une table est mutante
pendant l’exécution d’une instruction insert, update ou delete.

Pendant qu’une table est mutante elle ne peut ni être consultée ni être modifiée de façon emboı̂tée.
Si on tente de le faire alors Oracle, logiquement, fait avorter l’ordre principal avec l’erreur ORA-04091.

Ce problème peut apparaı̂tre notamment avec l’utilisation des triggers ligne puisque ceux-ci sont
exécutés pendant l’exécution de l’instruction qui les déclenchent. Il peut aussi apparaı̂tre avec des
fonctions stockées, par exemples si elles sont appelées dans la clause where d’un update et qu’elle
tente de consulter la table modifiée par le update.

La raison de cette erreur est qu’une table mutante est dans un état intermédiaire probablement
incohérent et que cela n’aurait alors aucun sens de la consulter.
Voici un trigger très simple qui est erroné car il tente de consulter la table en cours de modification.

Soit la table :
__________________________
v |
Employe (id, salaire), Adresse (id_employe, ville, dpt)
-- ----------
On veut garantir la propriété Psalaires égaux :

Tous les salaires sont égaux et un salaire indéfini est considéré comme
Psalaires égaux ≡


égal à n’importe quelle autre valeur.
8.6. TABLE MUTANTE (MUTATING TABLE) 95

Tout d’abord on remarque que seule la table Employe est impliquée dans le maintien de Psalaires égaux .
Q. 127 L’ordre delete peut-il casser Psalaires égaux ?
Analyse des cas :
– delete : ne peut évidemment pas casser Psalaires égaux
– insert :
– new.id ne peut casser Psalaires égaux
– new.salaire s’il est indéfini ne casse pas Psalaires égaux
– new.salaire s’il est défini peut casser Psalaires égaux
– update
– new.id ne peut casser Psalaires égaux
– new.salaire s’il est indéfini ne casse pas Psalaires égaux
– new.salaire s’il est défini peut casser Psalaires égaux
On décide donc d’écrire un trigger ligne erroné qui fera la vérification pour chaque employé modifié :
create or replace trigger Salaire_Egaux
before insert or update of salaire on Employe
for each row when (new.salaire is not null)
declare
Cpt_Sal_Diff Natural ;
begin
select Count (*) into Cpt_Sal_Diff
from Employe e
where e.salaire is not null and e.salaire != :new.salaire ;

if Cpt_Sal_Diff != 0 then
raise_application_error (-20111, ’salaires non égaux !’) ;
end if ;
end ;
On remarque que la requête du trigger utilise la table Employe qui est cours de modification par
l’ordre insert ou update qui a déclenché le trigger. Par exemple, l’ordre suivant qui tente d’augmenter
les salaires de 10 unités conserve évidemment Psalaires égaux et pourtant il échouera à cause de la
consultation d’une table mutante :
update Employe set salaire=salaire+10; -- échec : table mutante dans le trigger
Si Oracle ne déclenchait pas cette erreur de table mutante, le comportement serait bien pire : avant
de modifier le salaire du premier employé, le trigger détecterait que le nouveau salaire est différent de
ceux présents dans la table et déclencherait à tort l’erreur de salaires inégaux.

En revanche PostgreSQL (version 7.3.4) ne connaı̂t pas la notion de table mutante, du coup, pour le
même exemple :
– avec un trigger ligne before il déclencherait incorrectement une erreur de salaires inégaux !
– en revanche cela marche bien pour les triggers ligne after car ces triggers sont exécutés quand
la modification de la table est complètement terminée. Les valeurs de :new sont celles présente
dans la table et les valeurs :old sont (très probablement) celles mémorisées par le multiversion
(ou l’historique) des valeurs de chaque ligne (voir la partie sur les transactions, section 13.9.1 et 14
pages 157 et 165).
En fin de compte, une erreur de table mutante signifie une erreur de programmation.

Pourquoi Oracle ne signale-t-il pas cette erreur dès la compilation ? La raison est que dans certains cas
un trigger peut légitimement consulter ou modifier la table sur laquelle l’événement déclenchant a eu
lieu. Le cas principal est celui où le trigger est du type instruction, en effet un trigger instruction
s’exécute avant ou après l’instruction déclenchante, il travaillera donc sur une table non mutante , voir
la section 8.3 page 88.
96 CHAPITRE 8. LES TRIGGERS

Une solution, pour garantir Psalaires égaux , consiste donc à confier la vérification de la propriété à
un trigger instruction after.
Q. 128 Pour résoudre le problème de table mutante, remplacer le trigger ligne Salaire Egaux par un
trigger instruction after qui lui peut consulter la table Employe après modification.
Attention : un problème de table mutante peut aussi se produire pour un trigger instruction dans le
cas d’une cascade de déclenchements.
Q. 129 Donner un exemple où un trigger instruction échoue pour cause de table mutante.

8.7 Conception d’un trigger garantissant une propriété


Préalablement à l’utilisation de triggers il faut s’assurer que la propriété ne peut vraiment pas être
exprimée de façon déclarative : les triggers introduisent en général une complexité qui peut rendre
délicate la maintenance de la base de données.

C’est pourquoi, si la technique des triggers semble incontournable, il est important de faire une analyse
structurée avant de les implanter.
Le problème est : en quoi une modification de la BD peut-elle casser la propriété.
1. faire l’inventaire des tables pour lesquelles une modification pourrait casser la propriété,
2. construire un tableau à deux entrées : en lignes les tables, en colonnes les événements (insert,
update, delete) et, pour chaque case, en quoi l’événement se produisant sur la table est sus-
ceptible ou non de casser la propriété. Il est aussi intéressant d’y faire figurer les colonnes de la
table intervenant dans le maintien de la propriété.
3. utiliser les informations précédentes pour savoir si fonctionnellement un ou des triggers ligne ou
instruction peuvent ou doivent être mis en place.
Le choix entre trigger ligne ou instruction n’est pas forcément évident :
– le trigger ligne vérifie que la modification de chaque ligne conserve la propriété, il peut être intéressant
si très peu de lignes sont modifiées à chaque mise à jour de la BD.
– l’avantage du trigger instruction est qu’il travaille toujours sur une BD stable (non mutante), ce-
pendant il peut être coûteux si à chaque modification d’une table il vérifie que ses 10 millions de
lignes vérifient toujours la propriété alors qu’une seule ligne a été modifiée !

8.8 Exemple de conception de trigger


Appliquons cette démarche sur un exemple non trivial :
____________________ _________________________
v | | v
Produit(id, prix >= 0) Achat(p, c, quantité >= 0) Client(id, solde)
-- ---- --

 
La propriété Psolde suffisant à garantir est :
 
le solde d’un client est soit indéfini soit supérieur ou égal au total de ses achats .

D’abord on ne peut garantir cette propriété Psolde suffisant de façon déclarative : vérifier Psolde suffisant
nécessite d’observer l’état global des trois tables Produit, Achat et Client grâce à une requête qui
calcule la somme des achats de chaque client. Or Oracle ne permet pas d’évaluer une requête dans une
contrainte check et ne dispose pas des assertions définies par la norme SQL.

Donc l’usage de triggers est inévitable !

L’inventaire nous donne les trois tables et on obtient le tableau :


8.8. EXEMPLE DE CONCEPTION DE TRIGGER 97

insert update delete


Produit ♥ Si le prix a augmenté ♥
Un changement de produit et/ou de client et/ou
Un nouvel achat peut
Achat une augmentation de la quantité peuvent casser ♥
casser Psolde suffisant
Psolde suffisant
Client ♥ Si le solde a décru ♥
Un cœur (♥) dans une case indique que l’événement sur la table ne peut pas casser la propriété.
Il va donc être nécessaire d’écrire au moins 3 triggers !
Le tableau ne dit pas comment s’y prendre pour vérifier la propriété, et ce n’est pas son rôle. La suite

considère 
indépendamment chaque case du tableau susceptible de casser la propriété :
– table Client : seul un ordre update sur Client peut casser la propriété. C’est le cas le plus
simple : il suffit de vérifier Psolde suffisant pour chaque client modifié dont le solde a décru ou vient
d’être défini. Cela peut se faire avec un trigger ligne car le calcul du montant des achats d’un client
n’a besoin d’explorer que les tables Produit et Achat et on n’aura donc pas de problème de table
mutante.
create or replace trigger Maj_Solde_Client
before update of solde
on Client
for each row when (new.solde is not null and
(old.solde is null or new.solde < old.solde))
declare
Somme_Des_Achats Natural ;
begin
select Sum (a.quantite * p.prix) into Somme_Des_Achats
from Achat a inner join Produit p on a.p = p.id
where a.c = :new.id ;
if Somme_Des_Achats > :new.solde then
raise_application_error (-20111, ’Solde client insuffisant’) ;
end if ;
end ;
Ici on a adopté l’approche du check : si le nouveau solde est indéfini on considère que Psolde suffisant
est vérifiée (présomption d’innocence).

 
Q. 130 Réécrire plus simplement la clause when en utilisant la fonction nvl.
– table Produit : un ordre update peut casser la propriété. Pour cet événement, un trigger ligne
n’est pas approprié car il a be