Beruflich Dokumente
Kultur Dokumente
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 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 !
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)
) ;
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
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).
Il faut donc distinguer clairement entre ce qui doit tourner sur le serveur et ce qui doit tourner sur le
client.
Les SGBD proposent souvent leur propre langage de programmation : PL/SQL pour Oracle, PL/pgSQL
pour PostgreSQL et le langage de MySQL.
Relationnel et SQL
6
Chapitre 2
Ce modèle est lié à la théorie des ensembles (unicité des éléments, sous-ensemble, produit cartésien, . . .)
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 !
7
8 CHAPITRE 2. LE MODÈLE RELATIONNEL ET SQL
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.
SQL
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).
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).
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) :
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.
En SQL il faut par contre écrire la requête suivante pour exprimer le contenu d’une table :
select * from Ville ;
Q. 3 Si Ville possède une clef primaire, le distinct est-il utile dans la requête précédente ?
Π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
La restriction : WHERE
Pour ne conserver que les nuplets vérifiant le prédicat P .
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.
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 ;
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)
) ;
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.
Fig. 2.1 – Un exemple de valeur de table avec deux clefs étrangères etu et mat dans la table Note.
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
R ⊲⊳P S = σP (R × S)
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
É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.
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. 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
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.
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 :
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.
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
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.
Attention : ne pas interpréter ce null comme le pointeur null des langages de programmation ni
comme le zéro des entiers !
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
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.
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
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. 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 !).
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.
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 !
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é
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 ?
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.
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.
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
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).
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.
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
– 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 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.
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. 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.
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 ...)
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) ;
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
-- -----------------------
-- Julie 3 | 3 15 2
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. 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)
-- 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).
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.
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.
– 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).
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
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 .
Q. 56 Quelles vérifications un programme doit-il faire préalablement à l’ajout d’un tuple LDF.
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.
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. 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}
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) ?
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. 80 Donner une autre décomposition de R qui préserve à la fois l’information et les DF.
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
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
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.
39
40 CHAPITRE 4. SQL/DML LES ORDRES DE MODIFICATION DES TABLES
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’)) ;
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.
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.
affiche 0.66.
– 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
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).
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.
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)
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
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).
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).
En Oracle comme en PostgreSQL il est possible de définir une table sans clef :
5.3. LES CONTRAINTES 47
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.
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.
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.
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.
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 ;
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.
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,
...
)
Remarque : les contraintes primary key et unique créent des index sur la table qui sont reconstruits
à chaque réactivation de la contrainte.
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
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.
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 ?
55
56 CHAPITRE 6. COMPLÉMENTS ORACLE SQL
MATIERE MOYENNE
---------------
BD 13
CL 13
SSM 14
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}
{} {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
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).
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.
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) :
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.
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).
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
NON car ORA-30480 : L’option distinct n’est pas autorisée avec group by cube
ou rollup.
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
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
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.
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
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
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.
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.
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.
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
On voit aussi que les paramètres de la procédure s’utilisent tout naturellement dans le insert.
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.
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.
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 ;
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 ;
TEST_ADRESSE(3)
----------------------------
4
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.
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’
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 ( !).
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 ;
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.
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é.
...
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 ;
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
pour autant qu’il soit raisonnable d’avoir des fonctions à effet de bord.
parameter ::= <parameter_name> [in] datatype [{:= | default} <expression>]
Le return_type doit être un record ou un %rowtype (un %rowtype est un record).
C’est lors de l’ouverture du curseur (instruction open, voir 7.15.4, page 75) qu’on fixera les paramètres
effectifs.
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.
On peut utiliser l’attribut %ROWTYPE pour typer une variable à partir d’un curseur :
nom_prenom Les_Nom_Prenom%ROWTYPE ; -- nom_prenom.nom
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 ;
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)) ;
end Ajouter ;
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 ;
[−20999, −20000] est l’intervalle des numéros d’erreurs utilisables par le programmeur.
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).
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.
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.
Si on tente de le faire alors Oracle, logiquement, fait avorter l’ordre principal avec l’erreur ORA-04091.
Q. 108 Réécrire correctement cette fonctionalité sous forme d’une procédure PL/SQL.
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 ;
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
On peut aussi mettre une table par colonne fabriquée par la requête ou le curseur ou la clause retur-
ning.
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.
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.
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).
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.
: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.
Pour les triggers ligne, utilisation obligatoire des préfixes :old. et :new. pour désigner les colonnes
en cours de modification.
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).
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é).
end if ;
end ;
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.
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. 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 ;
2 20 20 21 20 20 2 20
1 14
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.
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.
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 !
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.
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 besoin de la table Produit pour calculer la somme des achats d’un client
et on aurait donc un problème de table mutante. Le plus simple est probablement de mettre en
place un trigger instruction after qui déclenche une erreur s’il existe au moins un client pour lequel
Psolde suffisant n’est plus vraie. La procédure suivante déclenche une erreur si la propriété n’est
pas vérifiée :
create or replace procedure Verifier_Soldes_Suffisants is
Nb_Clients_Insolvables Natural ;
begin
select Count (Count (*)) into Nb_Clients_Insolvables
from Client c
inner join Achat a on c.id = a.c
inner join Produit p on a.p = p.id
group by c.id, c.solde
having Sum (a.quantite * p.prix) > c.solde ;
if Nb_Clients_Insolvables != 0 then
raise_application_error (-20111, ’Solde client insuffisant’) ;
end if ;
end Verifier_Soldes_Suffisants ;
98 CHAPITRE 8. LES TRIGGERS
Remarquer Count (Count (*)) afin de compter le nombre de groupes, chaque groupe correspond
à un client insolvable à cause de la clause having.
À nouveau, un client dont le solde est indéfini n’est pas considéré comme un mauvais client.
Cette procédure qui vérifie tous les clients est la chose à faire après un update :
Cette solution n’est pas terrible car même si la modification consiste à diminuer les prix des produits
concernés (ce qui implique que Psolde suffisant ne peut pas être cassée) on va quand même vérifier
toute la base
!
– table Achat : les deux ordres insert et update peuvent casser Psolde suffisant. À nouveau un
trigger ligne provoquerait un problème de table mutante car il aurait besoin de consulter la table
Achat à la fois pour insert et update.
On va donc de nouveau utiliser un trigger instruction after :
Cette solution a le même inconvénient que précédemment : elle revérifie tous les clients, même
ceux qui ne sont pas concernés par les nouveaux achats ou les achats modifiés ! Par exemple, si la
table Achat contient 1 million d’achats, alors le trigger va traiter effectivement 1 million d’achats.
Supposons que le insert n’ait créé qu’un seul nouvel achat pour un client disposant déjà de 100
achats, alors, idéalement, il suffirait de faire la somme des prix de seulement 101 achats au lieu
du million d’achats traités par le trigger instruction Verifier_Achat. La vérification serait en gros
1000 à 10.000 fois plus rapide !
Une meilleure solution consiste donc à ne vérifier que les clients concernés par les nouveaux achats
créés par le insert. Souvenons-nous qu’un insert peut insérer plus d’une ligne avec la forme sui-
vante :
Pour cela, il est nécessaire de mémoriser les clients à vérifier pendant l’exécution du insert, on
va donc introduire la table de travail CAV destinée à mémoriser ces clients. Cette table sera garnie,
pendant le insert, grâce à un trigger ligne (Garnir_CAV_Insert). CAV qui joue le rôle d’une variable
globale, doit bien entendu être initialisée à vide avant le début de chaque ordre insert, ce sera le rôle
du trigger instruction before Vider_CAV. Enfin, comme précédemment, on a besoin d’un trigger
instruction after pour vérifier la propriété pour chacun des clients mémorisés dans CAV, c’est le rôle
du trigger instruction Verifier_CAV.
8.8. EXEMPLE DE CONCEPTION DE TRIGGER 99
CAV
Le global temporary fait qu’une session (connexion) ne voit que les modifications qu’elle a faites
sur CAV, elle ne voit pas les modifications faites sur CAV par d’autres sessions. Le on commit
preserve rows signifie que les modifications faites sur CAV lors de la session disparaissent quand
la session se termine. Toute nouvelle session voit la table CAV vide. Une telle table ne peut disposer
d’une clef primaire.
Il est aussi possible que le contenu de CAV disparaisse à la fin de la transaction courante avec la
clause on commit delete rows qui est l’option par défaut.
et les trois triggers attachés à la table Achat :
-- TRIGGER INSTRUCTION execute avant le debut de insert
create or replace trigger Vider_CAV
before insert on Achat
begin
delete from CAV ;
end ;
Cette architecture de solution est probablement utilisable dans pas mal de cas où on a des problèmes
de table mutante avec les triggers ligne.
Ici c’est la clause when de Garnir_CAV_Update qui est critique puisque c’est elle qui choisit les
8.9. CONCLUSION 101
clients à vérifier : il ne faut pas qu’elle en oublie et il serait souhaitable qu’elle ne prenne pas ceux
pour lesquels une vérification est inutile.
Finalement on s’en sort avec six triggers.
8.9 Conclusion
Losqu’un trigger échoue par une exception ou une erreur Oracle, il est abandonné, ainsi que l’instruc-
tion qui l’avait déclenché : tout se passe comme si l’instruction n’avait pas été exécutée (principe du
tout ou rien sur les instructions DML).
Aucune instruction DDL (create table par exemple) ou relative au contrôle de transaction (com-
mit, rollback, savepoint) ne peut être exécutée par un trigger, que ce soit directement dans les
instructions du trigger ou indirectement en appelant une procédure PL/SQL (réfléchissez et vous
comprendrez pourquoi !).
Si on a plusieurs triggers associés à une table et susceptibles d’être déclenchés par un même événement,
on sait juste que Oracle exécute les triggers d’un même type avant d’exécuter ceux d’un autre type
(autrement dit on ne sait pas grand chose sur l’ordre dans lequel seront exécutés les triggers, cela est
assez classique en programmation événementielle).
Schéma externe
102
103
La notion de schéma externe exprime le fait que plusieurs utilisateurs ont des fonctions différentes sur
une même base de données, autrement dit chaque fonction aura besoin de son schéma externe de cette
base de données. Pour chacune de ces fonctions il faudra ne lui permettre que les consultations et mo-
difications qui correspondent à ses besoins et pour lesquelles cette fonction assume ses responsabilités.
Bien entendu les deux chapitres qui suivent, privilèges et vues, ne sont pas les seuls outils permettant
de matérialiser un schéma externe. Les procédures stockées ainsi que le développement d’applications
clientes peuvent y participer.
Chapitre 9
Privilèges et rôles
Objectif : pouvoir limiter au strict nécessaire ce qu’un utilisateur peut faire sur la base de données.
Celui qui crée un objet (table, vue, procédure stockée, . . .) en est propriétaire et initialement seul lui
peut le manipuler.
Afin qu’un autre utilisateur puisse manipuler ces objets il faut que le propriétaire lui accorde directe-
ment (ou indirectement avec l’option grant option) des privilèges. Il y a deux sortes de privilèges :
– les privilèges objet (table, vue, sous-programme, . . .) permettent de manipuler des objets existant :
consultation et modification d’une table ou vue, exécution d’un sous-programme stocké,
– les privilèges système permettent de modifier la structure de la base en créant ou détruisant des
objets,
Un rôle est un assemblage de privilèges nécessaires pour assumer une fonction. On pourrait com-
prendre la notion de rôle comme une casquette que l’on porte pour accomplir une fonction particulière.
Comme il est possible de porter plusieurs casquettes, on peut assumer plusieurs rôles simultanément,
et on peut aussi abandonner un rôle comme on enlève une casquette. Un rôle correspond donc à des
privilèges temporaires.
La commande create schema d’Oracle est une facilité fonctionnelle, mais elle ne crée pas de nou-
veau schéma (le nom du schéma doit être celui de l’utilisateur exécutant cette commande) : c’est
certainement pourquoi la commande symétrique drop schema n’existe pas.
1
Au moins pour l’instant (PostgreSQL 8.2) le préfixe nom de la base de données doit être le nom de la base sur
laquelle on est connecté, autrement dit ce préfixe ne permet pas d’accéder à un objet d’une autre base de données.
104
9.2. LES UTILISATEURS ET LES PRIVILÈGES 105
Certains types d’objets n’ont pas de privilèges associés, par exemple primary key, unique et les
triggers, car ils seront toujours actifs.
Tout objet est la propriété de l’utilisateur qui l’a créé. Le propriétaire a tout pouvoir sur ses objets,
y compris celui de donner à d’autres utilisateurs des privilèges sur ses objets, puis de les révoquer.
Il en va de même pour les vues : inutile d’avoir des privilèges sur les tables ou vues sous-jacentes.
La destruction d’un objet supprime les privilèges associés, même si l’objet est ensuite recréé.
Un utilisateur peut manipuler des objets dans la mesure où il dispose des privilèges objet correspon-
dant. Des privilèges peuvent être accordés soit par le propriétaire de l’objet soit par un utilisateur
les ayant reçus avec l’option grant option, tous les deux peuvent ensuite les révoquer (commande
revoke).
Un utilisateur ne peut se révoquer des droits à lui-même (sauf indirectement par un cycle de with
grant option !).
L’option cascade constraint est nécessaire pour révoquer le privilège references car il faut suppri-
mer les contraintes de clé étrangère référençant la table.
Si un utilisateur a obtenu le même privilège depuis plusieurs donateurs, il se peut qu’il le conserve
même si le privilège est révoqué par un des donateurs. La figure 9.1 page 107 en donne un exemple.
Q. 132 En accord avec la figure 9.1, dessiner le graphe qui explicite comment, en phase 3, les privilèges
ont été obtenus, puis révoqués.
Q. 133 D’après la figure 9.1 et en partant de la phase 3, comment gmi51 peut-il s’y prendre pour
révoquer à tout le monde, sauf évidemment à gmi52 qui est propriétaire, le privilège select sur Livre ?
Fig. 9.1 – Exemple où gmi49 a obtenu le même privilège depuis plusieurs donateurs. gmi49 conserve
le privilège bien qu’il ait été révoqué à gmi51. Ceci est une bonne chose dans la mesure où le chef de
service gmi50 dispose toujours de ce privilège et qu’il souhaite que son collaborateur gmi49 continue
d’en disposer.
– un même utilisateur peut disposer de plusieurs rôles (plusieurs fonctions) qu’il n’est pas obligé
d’assumer tout le temps (set role),
– ajouter ou supprimer des privilèges à un rôle même si ce rôle est déjà accordé à des utilisateurs.
Un utilisateur se voit accordé un certain nombre de rôles, parmi ceux-ci il y a les rôles dit par défaut
et ceux qui ne le sont pas :
– les rôles par défaut d’un utilisateur et les privilèges qui lui sont directement accordés sont actifs dès
la connexion.
– pour bénéficier des privilèges associés aux rôles qui ne sont pas par défaut, l’utilisateur doit les
endosser explicitement (commande set role) et pourra ensuite les désactiver.
Du point de vue de grant et revoke, les rôles se comportent comme des privilèges systèmes.
<privilèges-système|r^
oles> ::= <privilège|r^
ole> {, <privilège|r^
ole>}
| all privileges
L’option with admin option autorise le bénéficiaire à transmettre le privilège à n’importe qui d’autre.
Si le privilège est un rôle, il pourra aussi le révoquer à un autre utilisateur, le modifier et le supprimer.
<liste-de-r^
oles-identifiés> ::= <r^
ole-identifié> { , <r^
ole-identifié> }
<r^
ole-identifié> ::= <nom-de-r^
ole> [ identified by <mot-de-passe> ]
<liste-de-r^
oles> ::= <nom-de-r^ole> { , <nom-de-r^ole> }
none désactive tous les rôles, y compris ceux par défaut.
Attention : set role n’est pas cumulatif (ou différentiel), il réinitialise l’ensemble des rôles actifs avec
uniquement ceux qui sont mentionnés.
set role ne peut être embarqué dans du PL/SQL, dommage (ni même de façon dynamique) !
Dans la mesure où un rôle n’est pas toujours actif pour l’utilisateur qui en bénéficie, un rôle ne devrait
pas comporter de privilèges qui n’ont de sens que s’il sont toujours actifs. Par exemple les privilèges
references et execute sont dans ce cas et ne devraient jamais être attribués via un rôle.
Par exemple, le privilège execute donné via un rôle ne permet pas de compiler une procédure appelant
la procédure sur laquelle porte ce privilège car, lors de son exécution, le code compilé ne vérifie pas si
on a le privilège d’exécuter la procédure.
9.5.7 Exemple
Sur la BD des clients, produits et achats.
create role G_Client ; grant update (solde) on Client to G_Client ;
create role G_Produit ; grant update (prix) on Produit to G_Produit ;
create role G_Achat ; grant insert, update (quantite) on Achat to G_Achat ;
-- Un super-r^
ole :
create role Gerer_Tout ;grant G_Client, G_Produit, G_Achat to Gerer_Tout ;
Plus tard on peut modifier le contenu d’un des rôles :
revoke update on Achat from Gerer_Achat ;
9.6 Exemple
administrateur durif utilisateur gmi25 effet
create table Salaire;
select * from durif.Salaire ; Table ou vue inexistante
create role X ;
grant select
on Salaire to X ;
grant X to gmi25 ;
select * from durif.Salaire ; Table ou vue inexistante
set role X ; active le rôle X
select * from durif.Salaire ; succès
set role NONE ; désactive le rôle X
select * from durif.Salaire ; Table ou vue inexistante
set role X ; active le rôle X
select * from durif.Salaire ; succès
delete from durif.Salaire ; privilèges insuffisants
grant delete
on Salaire to X ;
delete from durif.Salaire ; succès
revoke X from gmi25 ;
select * from durif.Salaire ; succès : le rôle reste actif
soit set role NONE;
set role X ; le rôle ’X’ n’est pas
accordé ou n’existe pas
soit
drop role x;
select * from durif.Salaire ; Table ou vue inexistante
Par défaut, un sous-programme stocké s’exécute avec les droits de celui qui a compilé le sous-programme
(on peut aussi le dire explicitement avec authid definer dans l’ordre create).
Les droits nécessaires pour que la compilation se passe bien doivent donc être garantis toujours actifs
pour l’utilisateur qui effectue la compilation. Autrement dit, ces droits ne doivent pas être octroyés
via des rôles, car un utilisateur peut à tout moment endosser ou abandonner un de ses rôles. Les droits
nécessaires doivent donc être attribués directement à l’utilisateur.
9.7. PRIVILÈGES ET SOUS-PROGRAMMES STOCKÉS 111
Bien entendu, si un de ces droits est ensuite révoqué le résultat de compilation deviendra invalide,
car Oracle se souvient (indépendamment du code compilé) des droits nécessaires à l’exécution de tout
sous-programme.
l’identificateur ’DURIF.UN’
doit ^
etre déclaré
On peut voir qu’Oracle adopte une attitude paresseuse2 quant à la validation d’un objet : c’est seule-
ment quand on tente d’utiliser un objet invalidé pour cause de droits manquants qu’Oracle va tenter
de le revalider en fonction de l’état actuel des droits de l’utilisateur. En l’occurrence c’est lors du
select Deux from dual ; qu’Oracle, voyant que Deux est invalide, va la remettre dans l’état valide
car gmi52 a maintenant le droit d’exécuter durif.Un.
La même expérience, mais en utilisant un rôle pour transmettre le droit d’exécution à gmi52 ne marche
pas.
2
L’adjectif paresseux n’est pas à prendre dans son sens péjoratif, il signifie ici qu’on ne fait les choses que quand cela
est nécessaire ! L’attitude paresseuse d’Oracle ou de certains logiciels peut s’avérer tout à fait efficace.
Chapitre 10
Les vues
En première approche, une vue est un objet qui associe un nom à une requête. Une fois créée, on
pourra consulter cette vue comme si c’était une table :
create view Bon_Client (id, nom, solde) as
select id, nom, solde
from Client
where solde > 1000
with check option ;
select *
from Bon_Client ;
select *
from Bon_Client
where lower (nom) like ’%gold%’ ;
En général, une utilisation particulière d’une base de données ne nécessite pas de voir toutes les données
de la base de données, ceci pour des raisons de confidentialité mais aussi tout simplement pour ne pas
polluer l’utilisateur avec des informations qui ne le concernent pas.
Par exemple les étudiants qui conçoivent l’annuaire des anciens GMI ne peuvent pas voir le salaire in-
dividuel que certains anciens renseignent, mais il peuvent en obtenir une moyenne. Ainsi ces étudiants
n’auront aucun droit sur la table Ancien mais disposeront d’une vue correspondant à la table Ancien
amputée de la colonne salaire et d’une vue calculant le salaire moyen.
Pour mettre en place une vision limitée et appropriée à la mission de l’utilisateur de la base de données,
les vues sont un des outils majeurs (le système de privilèges intervient lui aussi).
Les vues constituant le cadre juste nécessaire à une utilisation particulière de la base de données sont
un des outils permettant de réaliser un schéma externe.
112
10.1. LE LDD D’UNE VUE 113
Si le schéma externe d’une utilisation n’est constitué que de vues, on aurait tendance à penser que
cette utilisation est incapable de modifier la base, ce qui serait parfois très embêtant !
En fait, comme on le verra, Oracle et PostgreSQL disposent de moyens permettant de modifier la base
de données via les vues d’un schéma externe.
10.1.1 Un exemple
Soit la base de données :
create table Client (
id Number (5) primary key,
nom Varchar2 (20),
solde Number (6, 2) default 0.0) ;
La vue qui donne la liste des clients avec le montant moyen des commandes qu’il a effectuées
create view Client_Moyenne (id, nom, montant_moyen) as
select Cl.id as id, Cl.Nom as nom, Avg (Co.montant)
from Client Cl
inner join Commande Co on Co.client = Cl.id
group by Cl.nom
with read only ;
Comme une table, une vue peut être mentionnée dans la clause from d’une requête.
Si une des tables utilisées par la vue est détruite, cette dernière devient inutilisable.
114 CHAPITRE 10. LES VUES
update Mauvaise_Vue
set solde = 300
where id = 45 ; -- aucune ligne mise à jour
Les procédures stockées permettent aussi de résoudre ce problème en permettant d’exprimer les trai-
tements à mettre en place sur les tables pour mettre à jour la vue.
Les procédures stockées permettent aussi de résoudre ce problème en permettant d’exprimer les trai-
tements à mettre en place sur les tables pour mettre à jour la vue.
Les triggers instead of sont forcément des triggers ligne, c’est à dire que lors d’un update et d’un
delete ils disposent du contenu d’origine du nuplet courant de la vue (old) et du nouveau contenu de
ce nuplet (new) mais celui-ci n’est pas modifiable par le trigger. C’est cela qui permet de comprendre
pourquoi l’exemple suivant fonctionne.
before et after n’ont pas de sens pour les triggers instead of.
On ne peut pas attacher un trigger instead of sur une vue with read only.
Q. 136 Dans la clause select, pourquoi a-t-on pris soin d’écrire e.id as etudiant et non pas
n.etudiant as etudiant ?
On veut que :
un insert ajoute si nécessaire l’étudiant et systématiquement la note,
un update mette à jour uniquement le nom de l’étudiant,
116 CHAPITRE 10. LES VUES
Optimisations
117
Chapitre 11
Optimisations
L’unité atomique de lecture/écriture sur un disque est le secteur ou le bloc (plusieurs secteurs conti-
gus). La taille d’un secteur peut être de 512 ou 1024 octets voire 4096.
Écrire ou lire un secteur prend un temps énorme par rapport à la même opération en mémoire centrale.
Cela est dû principalement à l’aspect mécanique de l’accès au secteur :
1. le bras supportant la tête de lecture/écriture doit d’abord être déplacé radialement sur la piste
du secteur
2. il faut ensuite attendre que le secteur se présente sous le bras grâce à la rotation du disque,
3. enfin il faut lire ou écrire le secteur, la durée de cette opération dépend elle aussi de la vitesse
de rotation du disque.
Oracle organise ses accès au disque de la façon suivante :
– le bloc est la plus petite unité de l’écriture/écriture dont la taille est fixée par la constante DB_BLOCK_SIZE,
par exemple 2 kilo-octets.
– l’extent est l’unité suivante. Un extent est constitué d’un certain nombre de blocs contigus, ce qui
garantit un accès physique efficace.
– le segment est une collection d’extents qui constitue en général un seul objet de la base, par exemple
le segment de donnée d’une table ou le segment d’un index.
118
11.2. OPTIMISATIONS ALGÉBRIQUES 119
Pour connaı̂tre le nom des étudiants inscrit en ’BDD’ on peut écrire la requête :
select e.nom
from Etudiant e
inner join Inscription i on e.id = i.etudiant
where i.matiere = ’BDD’ ;
Supposons qu’il y a 1.000 étudiants (100 par bloc), 10.000 inscriptions (200 par bloc) et 100 étudiants
inscrits en ’BDD’.
Voici quelques manières de calculer cette requête.
Approche naı̈ve On effectue d’abord l’équi-jointure (sans se servir des index), puis la restriction et
enfin la projection.
1. Construire sur disque le résultat de la jointure : lire chacun des étudiants (1.000 lectures)
et pour chacun retrouver toutes ses inscriptions (1.000 × 10.000 lectures), on obtient 10.000
éléments dans la jointure qu’on écrit sur le disque (10.000 écritures).
2. lire les 10.000 lignes de la jointure pour ne conserver que celles de ’BDD’ et en faire la
projection.
Le nombre total d’entrées sorties est donc de 10.021.000.
Utiliser la semi commutativité de la restriction sur la jointure On se rend compte que la res-
triction sur la matière BDD pourrait être faite avant la jointure.
1. Calculer la restriction de Inscription sur ’BDD’ : 10.000 lectures et 100 écritures.
2. Calculer l’équi-jointure entre Etudiant et la restriction déjà calculée : 1.000 lectures d’étudiant
et pour chacun 100 lectures d’inscription et faire la projection.
Le nombre total d’entrées sorties est donc de 111.100. On a gagné un facteur de 90 !
Exploiter les index À chaque clef primaire ou contrainte d’unicité est associé un index. Un index
implanté par une structure ordonnée (Barbre par exemple) permet de retrouver une clef et sa
ligne en logm (n) avec m ≥ 2.
1. matiere étant le poids fort de la clef primaire de Inscription, il est possible, grâce à l’index
Inscription_PK, de retrouver les 100 inscriptions en ’BDD’ en au plus log2 (10.000) + 2 ×
100 = 214 lectures si les aiguillages du Barbre menant en feuille peuvent être conservés en
mémoire (voir 11.4.5 page 125) puis de les stocker avec 100 écritures.
2. Plutôt que faire la jointure par rapport aux étudiants, on peut la faire par rapport aux
inscriptions (la jointure est commutative) : on lit chacune des 100 inscriptions et, pour
chacune on retrouve l’étudiant grâce à l’index Etudiant_PK en au plus log2 (1.000) = 10
lectures.
Le nombre total d’entrées sorties est donc de 1.414. On gagne un facteur d’environ 7.000 par
rapport à l’approche naı̈ve !
Remarquer que ces améliorations sont le fruit de propriétés de l’algèbre relationnelle appliquées
en connaissant la taille des tables.
il faudre lire séquentiellement le fichier jusqu’à trouver l’employé d’id 16. Dès qu’on l’a trouvé on peut
arrêter l’exploration puisqu’on sait qu’id est unique.
11.4. NOTIONS DE BASE SUR LES B+-ARBRES À CLEFS UNIQUES 121
Par exemple, pour retrouver l’employé 16 il faudra lire en moyenne ⌈n/2p⌉ pages car cet employé peut
se trouver, de façon équiprobable, n’importe où dans la table (ce qui donne 5.000 lectures de page si
n = 1.000.000 et p = 100).
Pire : lors d’une insertion d’un nouvel employé, il faudra d’abord vérifier que son id n’apparaı̂t pas
déjà dans la table et donc faire une exploration exhaustive de celle-ci, c’est à dire lire les ⌈n/p⌉ pages
(10.000 lectures de page si n = 1.000.000 et p = 100).
Cela sera un peu plus compliqué suite à un update de la colonne id qui a pu modifier un nombre
quelconque de lignes.
Q. 139 Comment pourrait-on s’y prendre pour vérifier qu’un update conserve l’unicité de la clef
primaire ?
D’où l’intérêt de gérer une structure supplémentaire permettant de trouver rapidement un employé
grâce à sa clef et de garantir efficacement l’unicité des clefs. Cette structure s’appelle un index. Il y
a au moins deux sortes d’index : les B-arbres et les tables de hachage, nous n’envisagerons que les
B-arbres.
La fonctionnalité principale d’un B+-arbre est celle d’une table (ou map en anglais) permettant de
trouver rapidement l’adresse de la ligne1 d’une table ayant une valeur particulière de certaines colonnes.
On appellera clef du B+-arbre ces colonnes. L’intérêt du B+-arbre est qu’il est bien adapté à la gestion
sur disque où en fait un nœud correspond à un bloc disque dont la taille va de 512 octets à 4 Koctets,
le nombre maximum de clefs stockables par nœud dépend évidemment du nombre d’octets nécessaire
au stockage d’une clef.
Un autre intérêt est qu’il est parfaitement équilibré : toutes ses feuilles sont à la même profondeur.
La structure d’un B+-arbre est basée sur le fait que les clefs qui y définissent des aiguillages disposent
d’un ordre complet. On peut voir une clef comme un nombre ayant autant de chiffres que la clef a de
colonnes, les colonnes de gauche étant celles de poids fort, comme dans notre notation des nombres
en base 10.
Par exemple la clef (34, ’jaune’) est strictement plus petite que la clef (34, ’vert’) à cause de la
colonne de poids faible indiquant la couleur et de l’ordre lexicographique.
L’exemple le plus classique est celui où la clef du B+-arbre est la clef primaire de la table.
Ses caractéristiques principales sont :
– en terme de stockage : le B+-arbre est stocké sur disque, il est donc persistant et dispose d’une
grande capacité.
– en terme d’organisation : c’est une généralisation de l’Arbre Binaire de Recherche (ABR) : c’est un
arbre m-aire avec m ≥ 2 (tout nœud interne a au moins deux sous-arbres non vides) équilibré qui
permet donc des recherches par clef efficaces (en logm (n) accès disque, où n est le nombre d’éléments
du B+-arbre).
Une petite différence des B+-arbres tels que présentés ici avec les ABR : les couples (clef, adresse
de ligne) sont stockés dans les feuilles et les nœuds internes ne contiennent que des clefs (ce sont de
purs aiguillages)2 .
1
Oracle utilise le mot rowid pour désigner une adresse de ligne.
2
D’autres imlémentations des Barbres ressemblent plus aux ABR, soit en stockant les éléments complets plutôt que
simplement leurs clefs dans les nœuds internes, soit, si ces éléments sont de trop grande taille en associant à chaque clef
un pointeur permettant de retrouver l’élément possédant cette clef. Ces deux solutions permettent lors de la recherche
122 CHAPITRE 11. OPTIMISATIONS
– chaque nœud du B+-arbre occupe une page du système de fichiers (une page correspond en général
à un, deux ou quatre secteurs disque), l’idée est que la page (on dit parfois aussi bloc) est l’unité
atomique de lecture/écriture.
Un nœud interne (ou aiguillage) ne contient que des clefs et des adresses d’autres nœuds du B+-arbre.
Une adresse est en fait le numéro de page du nœud ou de la feuille pointé. Chaque nœud interne
constitue un aiguillage permettant de trouver le chemin menant à la feuille contenant la clef cherchée
et sa valeur, voir la figure 11.1.
B1 B2 B3 Bn Bn+1
Fig. 11.1 – Nœud interne (page disque) constituant un aiguillage : on a C1 < C2 < . . . < Cn ,
l’élément de clef C telle que Ci−1 < C ≤ Ci ne peut se trouver que dans le sous-arbre Bi . Si C ≤ C1 ,
C doit se trouver dans B1 . Si Cn < C, C doit se trouver dans Bn+1 . On remarque que ce nœud interne
n’est pas saturé et qu’il pourrait donc accueillir d’autres clefs et sous-arbres.
Un nœud feuille contient des éléments (clef, adresse de ligne), voir la figure 11.2.
Fig. 11.2 – Feuille : Ci−1 < Ci . L’adresse associée à Ci est celle du tuple dans la table ayant la valeur
Ci dans ses colonnes (on parle aussi de rowid plutôt que d’adresse).
La taille des nœuds internes et des feuilles étant fixée par le système, l’arité des nœuds internes et le
nombre d’éléments stockables dans une feuille dépendront des tailles physiques maximales nécessaires
à l’écriture sur disque de toute valeur de clef (taille Sclef ) et d’élément (taille Sélém ).
d’un élément par sa clef de trouver l’élément sans descendre forcément jusqu’aux feuilles du Barbre (comme c’est le cas
avec un ABR), ce qui semble être un avantage, mais on verra qu’en général il vaut mieux ne stocker qu’un minimum
d’information dans les nœuds internes, c’est à dire uniquement la clef, de manière à ce que l’arité, ou la largeur, des
aiguillages soit la plus grande possible ce qui a pour conséquence de diminuer la profondeur de l’arbre et donc le nombre
de pages à lire pour accéder à un élément.
11.4. NOTIONS DE BASE SUR LES B+-ARBRES À CLEFS UNIQUES 123
Si T = 101 on a au moins K = 50 clefs par aiguillage. (il en va de même pour les feuilles qui doivent
être au moins à moitié remplies mais pas forcément avec la même valeur de T .)
Q. 140 Supposons que la page fasse 4 kilo-octets, qu’un pointeur de page nécessite 16 octets et que le
SGBD utilise 4 octets de chaque aiguillage pour en gérer le contenu. Donner les valeurs de T = 2K + 1
lorsque la taille maximale d’une valeur de clef vaut respectivement 10 octets, 100 octets et 1000 octets.
Q. 142 Une clef Ci d’un aiguillage peut-elle apparaı̂tre dans les aiguillages de Bi ?
Q. 144 Donner la formule donnant le nombre de lectures de page dans le meilleur des cas — i.e. tous
les aiguillages sont pleins et ont donc chacun 2K + 2 fils.
Q. 145 Compléter le tableau de la figure 11.3.
124 CHAPITRE 11. OPTIMISATIONS
Si le nœud explosé n’est pas la racine, alors il a bien un père qui peut accueillir CK+1 (le père ne peut
pas être plein, sinon il aurait été explosé lors de la descente).
Si le nœud explosé est la racine alors on alloue un nouvel aiguillage vide qui va accueillir CK+1 et
devenir la nouvelle racine du B+-arbre.
Donc :
1. cette insertion conserve bien au moins K clefs par aiguillage,
2. la hauteur du B+-arbre n’augmente qu’à chaque fois qu’on explose la racine, car il est alors
nécessaire de créer une nouvelle racine au dessus des deux nœuds obtenus par explosion. Puisque
le B+-arbre croı̂t par la racine, toutes les feuilles restent à égale distance de la racine : l’équilibre
du B+-arbre est bien conservé (voir la propriété 5 page 122).
Q. 146 Lors d’une explosion, combien de nouvelles pages faut-il allouer dans les deux cas possibles ?
4 10 30 31 44 55 66
TABLE
(31, rr) (30, toto) (10, oo) (4, bof) (55, ii) (66, ii) (44, oo)
Un autre algorithme plus efficace consiste à ne faire exploser un nœud que quand c’est indispensable :
dans ce cas les explosions se font en remontant le chemin vers la racine : la pile des nœuds pères saturés
est alors nécessaire dont le fond est le dernier nœud père non saturé rencontré s’il en existe un. Si tous
les nœuds de la pile sont saturés alors le nœud en fond de pile est forcément la racine, c’est le cas où
le B+-arbre verra sa profondeur augmenter de 1.
Q. 147 Quelle est la valeur de K ? Est-ce bien un B+-arbre ?
Q. 148 Comment retrouver la feuille contenant la clef 44 ? étiqueter les blocs lus avec une *
11.4. NOTIONS DE BASE SUR LES B+-ARBRES À CLEFS UNIQUES 125
Q. 149 Comment retrouver les feuilles contenant toutes les clefs ∈ [25, 44] ? étiqueter les blocs lus
avec un +
Q. 152 Dessiner le nouvel état après insertion dans la table de (45, ”truc”) puis (7, ”truc”).
Q. 153 Dessiner l’état qu’on aurait obtenu si on avait permuté les deux insertions précédentes.
Q. 154 Donner un algorithme efficace pour retrouver toutes les feuilles pouvant contenir des clefs
∈ [a, b].
BD, 7 IA, 3
AI BD BD BD BD CL CL CL CL CL IA IA PI PI
32 3 5 7 22 3 5 22 30 33 3 5 1 3
Pour ordonner deux clefs multi-colonnes, plus une colonnes est à gauche plus elle est de poids fort.
Par exemple, pour (m1 , e1 ) et (m2 , e2 ) on compare d’abord les colonnes m1 et m2 et, seulement si
elles sont égales on compare les colonnes e1 et e2 .
Q. 155 D’après la figure, a-t-on (CL, 22) < (PI, 3) ? Quel est l’attribut de poids fort de la clef.
Q. 156 Donner un algorithme efficace pour retrouver tous les éléments dont le poids fort de la clef
est égal à une valeur donnée, par exemple BD.
On appellera sous-clef une clef incomplète constituée d’au moins une des colonnes de poids fort.
Q. 157 Même question pour retrouver tous les éléments dont la matière ∈ [MInf , MSup ].
Q. 158 Y a-t-il un algorithme aussi efficace dans le cas où on cherche les éléments dont le poids faible
de la clef est égal à une valeur donnée, par exemple les éléments d’enseignant 3 ? Expliquer.
Q. 159 Dessiner un B+-arbre contenant les mêmes éléments que dans l’exemple mais dont la clef a
l’enseignant en poids fort.
Q. 160 Soit des éléments de la forme (a, b, c, d, e) dont la clef est constituée des attributs {a, d, e}
et que l’on sache qu’on fera des accès uniquement sur les sous-clefs {d}, {a, d} et {a, d, e}, dans quel
ordre a-t-on intérêt à déclarer les colonnes de la clef du B+-arbre ? (Oracle et PostgreSQL exploitent
effectivement cet ordre)
Q. 161 Dans le cas précédent, comment pourrait-on faire une recherche relativement efficace sur la
sous-clef {d, e} ?
126 CHAPITRE 11. OPTIMISATIONS
Q. 162 Quelle caractéristique intéressante ont les feuilles d’un B+-arbre ? en déduire un ajout d’in-
formation permettant d’éviter de trier pour certaines clauses order by.
Q. 164 L’algorithme d’insertion par explosion a priori des nœuds pleins (voir la section 11.4.3
page 124) est-il toujours applicable et conserve-t-il les propriétés de ce B+-arbre ?
Q. 165 Reprendre les éléments du B+-arbre précédent pour les indexer par la catégorie matière.
Q. 167 Soit un index multiple sur les couleurs, que peut-on dire du sous-arbre compris entre la clef
jaune à gauche et jaune à droite ?
Par ailleurs la commande SQL create index permet de créer explicitement des index uniques ou non.
Par défaut, en Oracle et en Postgres, les index sont implémentés par des B+-arbres.
Dans ses index implantés en B+-arbres, Oracle chaı̂ne les feuilles dans l’ordre croissant de la clef,
dans les deux sens. Par ailleurs les feuilles contiennent des couples (clef, rowids), un rowid est l’adresse
d’une ligne de table.
3 pierre 2
La requête select * from Employe e where e.id = 4 ; doit explorer complètement la table pour
retrouver tous les employés dont l’id vaut 4. Voici son plan d’exécution :
L’opération TABLE ACCESS FULL signifie que l’exécution consiste à balayer toutes les lignes de la
table Employe. En effet id n’est pas une clef de Employe, plusieurs, voire tous les employés peuvent
avoir le même id.
3 pierre 2
La requête select * from Employe e where e.id = 4 ; utilise maintenant l’index de clef pri-
maire pour accéder rapidement à l’employé d’id 4. Voici son plan d’exécution :
Les opérations les plus décalées vers la droite sont celles qui sont exécutées en premier. On voit donc
que le plan consiste d’abord à utiliser l’index de clef primaire (EMPLOYE_PK) pour retrouver l’adresse
(ou rowid) de la ligne contenant l’employé d’id égal à 4 ; ce rowid est ensuite utilisé pour retrouver
128 CHAPITRE 11. OPTIMISATIONS
Employe
Employe_PK id nom dpt Employe_Dpt_Index
6 jules 2
create index Employe_Dpt_Index 4 sophie 1 1
on Employe (dpt) ; 4 2 paul 3
1 marc 2 2
7 léa 3
5 marie 2 3
3 pierre 2
Les deux requêtes select * from Employe e where e.dpt = 2 ; et
select * from Employe e where e.dpt between 2 and 10 ; exploitent l’index non unique sur
la colonne dpt. Elles ont le même plan d’exécution :
Le plan consiste maintenant à retrouver efficacement les rowid des employés du département 2, puis
à faire des accès direct dans la table Employe.
Pour obtenir ce plan d’exécution, il a fallut insérer 10000 lignes dans Employe.
Q. 168 Quel est le plan d’exécution de : select * from Employe e where e.dpt between 2 and
7?
Q. 169 Quel est le plan d’exécution de : select * from Employe e where e.dpt in (2, 7, 11) ?
On peut aussi créer un index dont la clef est formée d’expressions portant sur les colonnes de la table
indexée, par exemple pour ne pas distinguer les minuscules des majuscules :
create index Emp_Nom on Employe (upper (nom)) ;
où nom est bien sûr une colonne de la table Employe.
Attention pour que cet index Emp_Nom soit utilisé par l’optimiseur il faudra, dans les requêtes, utiliser
les mêmes expressions, par exemple :
select *
from Employe
where upper (nom) between ’C’ and ’H’ ;
La création d’un index utilise la table triée par rapport à la clef d’indexation : on obtient donc un
Barbre particulièrement compact et efficace.
Attention, si un index non unique existe déjà sur les mêmes colonnes que celles utilisées dans une
contrainte de clef primaire créée ensuite, alors la contrainte de clef primaire utilisera cet index multiple !
Moralité : lorsqu’on déclare les contraintes de clef ou d’unicité et les index on a intérêt à savoir
comment seront utilisées les colonnes y participant.
130 CHAPITRE 11. OPTIMISATIONS
Ne pas oublier qu’un index coûte en temps de mise à jour et en place mémoire. À chaque modification
d’une table, il faut aussi mettre à jour tous ses index.
L’algorithme utilisé pour exécuter une instruction DML s’appelle un plan d’exécution. Un plan est
une décomposition hiérarchique d’une instruction DML en opérations plus élémentaires, les plans sont
produits par l’optimiseur SQL.
Voici une requête et son plan (toutes les clefs primaires ont été déclarées dans les tables) :
-------------------- ---------------
v | | v
Client (cdc, nom) Envoi (cdc, cdp) Produit (cdp, couleur)
--- -------- ---
Q. 170 Quelles sont les contraintes qui peuvent expliquer le TABLE ACCESS FULL sur la table ENVOI ?
Q. 172 Pourriez-vous donner une approche plus efficace si on suppose que les index de ENVOI et
CLIENT sont des B+arbre et en supposant que la colonne cdc de la clef primaire de ENVOI est celle de
poids fort.
Tout d’abord, une ligne du plan d’exécution est précédée de l’évaluation des lignes plus indentées qui
la suivent jusqu’à la prochaine ligne indentée de la même manière.
Voici un exemple d’ordre d’évaluation fonction de cette indentation :
11.6. REPRÉSENTATION GRAPHIQUE 131
6
3
1
2
5
4
On remarque que pour deux lignes filles d’une même ligne c’est la première qui est évaluée en premier
et la seconde qui est évaluée ensuite, enfin c’est la ligne mère qui est évaluée.
On peut alors mieux comprendre le plan précédent et lui associer de la sémantique :
Il est aussi possible de représenter graphiquement cette hiérarchie, voir la figure 11.4.
Nested Loops
(cdc,cdp, nom)
On voit que les index des clefs sont utilisés, à chaque fois que c’est possible, pour constituer la jointure.
La seule table parcourue complètement est Envoi, pour les autres le plan utilise l’index de clé primaire
de la table.
En reprenant la requête précédente mais en précisant qu’on s’intéresse au client A3 on obtient un plan
d’exécution différent :
select c.nom, p.libelle, p.couleur
from Client c inner join Envoi e on c.cdc = e.cdc
inner join Produit p on p.cdp = e.cdp where e.cdc = ’A3’ ;
132 CHAPITRE 11. OPTIMISATIONS
Q. 173 Pourquoi ce plan ne part-il plus pas de la table Envoi mais de la table Client ?
Q. 176 Quel serait le nouveau plan d’exécution si la projection devenait select p.libelle, p.couleur
Voici une requête qui reproduit le contenu de la vue Bons_Clients_1 et son plan d’exécution quand
la table Client contient 4 clients :
select * from Bons_Clients_1 ;
On remarque qu’effectivement la définition de la vue est intégrée dans la requête (le plan d’exécution
n’utilise pas l’objet Bons_Clients_1).
Pour compter le nombre de produits, Oracle utilise l’index de clef primaire Produit_PK plutôt que la
table Produit.
Q. 177 Dessiner la hiérarchie d’opérations de ce plan d’exécution.
Q. 178 Pourquoi ce plan n’utilise-t-il pas la table Envoi mais seulement son index Envoi PK ?
Modifions légèrement la vue Bons_Clients_1 en remplaçant le select c.cdc, c.loc par select c.cdc
et en simplifiant le group by en conséquence :
create or replace view Bons_Clients_2 as
select c.cdc
from Client c
inner join Envoi e on c.cdc = e.cdc
cross join (select Count (*) as Nb_Produits from Produit) p
group by c.cdc, p.Nb_Produits
having Count (distinct e.cdp) = p.Nb_Produits ;
nombre de lignes
de Client plan d’exécution
Opération + Options Objet Type
4 UPDATE CLIENT
TABLE ACCESS FULL CLIENT TABLE
Opération + Options Objet Type
1003 UPDATE CLIENT
INDEX RANGE SCAN CLIENT_PK INDEX UNIQUE
11.9 Astuces
Ces astuces sont principalement liées à Oracle, certaines sont cependant assez générales.
Éviter de cacher les clefs dans des expressions : L’utilisation des index peut-être conditionnée
par la manière d’écrire les expressions de la clause where :
Soit la table :
create table Employe (
id Number (5) primary key,
nom Varchar(50),
salaire Number (7, 2)
) ;
Q. 181 Réécrire la requête afin que l’accès par clef puisse être effectué.
Attention aux conversions implicites dans les clauses where, on : l’expression colChar = 27
est comprise comme TO_NUMBER(colChar) = 27 et si colonneChar est une clef primaire, son
index ne sera pas utilisé !
Introduire un index peut accélérer les choses . . .MAIS Soit la requête :
select e.nom, e.salaire
from Employe e
where e.salaire between 1000.0 and 2000.0 ;
En l’état l’optimiseur n’a pas d’autre choix que de parcourir complètement la table Employe.
Si cette requête est (très) fréquente on a intérêt à introduire un index non unique sur la colonne
salaire :
create index Employe_Salaire_Index on Employe (salaire) ;
Attention quand même : l’index doit être mis à jour à chaque fois que la table est mise à jour,
ce qui introduit un coût supplémentaire lors des modifications. Si on multiplie inutilement les
index on consomme inutilement de la place mémoire et du temps CPU lors des modifications
11.10. LES COMMANDES ORACLE ET POSTGRES 137
(Oracle : la mise à jour d’un index prend en moyenne trois fois plus de temps que la mise à jour
dans la table. Une mise à jour d’une table munie de trois index sera environ dix fois plus longue
que s’il n’y avait pas d’index). La conception des index suppose au préalable une connaissance
précise des requêtes qui seront exécutées sur la base de données.
Attention à l’ordre des colonnes d’un index :
Quand un index — unique ou non — comporte plus d’une colonne :
create table T (A Number (5), B Number (5), C Number (5), D Number (5),
constraint T_PK primary key (A, B, C)) ;
La première colonne joue le rôle de poids fort et la dernière celle de poids faible. En l’occurrence,
A est le poids fort, B est le poids intermédiaire et C est le poids faible de la clef qui va servir
à ordonner l’index. C’est à dire que toutes les clefs ayant la même valeur en A sont rangées de
façon contiguë dans l’index et on pourra donc les retrouver efficacement ; il en va de même pour
les clefs ayant les mêmes valeurs en A et en B. En revanche les clefs ayant la même valeur en C
sont dispersées dans l’index et il faudra faire une exploration exhaustive de la table (plutôt que
de l’index) pour retrouver les lignes ayant une certaine valeur en C !
Q. 182 Redéfinir la clef primaire afin que l’index puisse être utilisé.
Q. 183 Cela changerait-il quelque chose si B était comparé à la valeur d’une variable ?
Q. 184 Pourquoi l’ordre (C, B, A) serait-il moins bon pour cette requête ?
Éviter les requêtes et les vues à tout faire Il vaut mieux écrire plusieurs requêtes ou vues cha-
cune adaptée à un usage particulier que de mettre en place peu de requêtes ou vues à tout faire
qui risquent de s’avérer inefficaces pour certains usages.
Un index peut éviter de devoir trier Si une requête a une clause order by et qu’il existe un
index de type B-arbre sur la table à trier dont les colonnes sont les mêmes et qu’elles sont
données dans le même ordre que dans la clause order by alors le tri est déjà fait !
Éviter les connexions/déconnnexions trop fréquentes.
Utiliser les curseurs et les variables de liaisons cela évite des compilations répétées de la même
requête.
Charger les données dans les tables avant de créer les index .
Les triggers peuvent coûter cher !
Le programmeur peut ensuite étudier à loisir ce plan d’exécution et tenter de l’améliorer par le biais
d’index, de suggestions explicites d’optimisation (hint voir 11.10.4 page 138) ou encore en modifiant
l’écriture de ses ordres DML.
Sous SQL+, la commande set autotrace on explain fait que le plan d’exécution sera affiché après
chaque ordre DML. La commande set autotrace off explain permet d’arrêter cet affichage.
en Postgres ANALYZE
et la même avec une suggestion qui demande à effectuer la jointure en respectant l’ordre d’apparition
des tables dans la clause from (hint ORDERED), le plan d’exécution est alors différent de celui obtenu
précédemment et il est plus cher :
select /*+ ORDERED */ -- conserve l’ordre de jointure
c.nom, cm.nom, p.libelle, p.couleur
from Client c cross join Camion cm
cross join Produit p
inner join Envoi e
on e.cdc = c.cdc and cm.cdm = e.cdm and p.cdp = e.cdp ;
11.11. LES GROUPES (CLUSTERS) : GROUPEMENT DE TABLES PRÉ-JOINTES 139
On voit qu’on effectue d’abord le produit cartésien des clients, camions et produits avant de faire
un accès par clé à l’index de Envoi, on remarque aussi qu’on n’accède pas à la table Envoi car la
projection n’a besoin d’aucune de ses colonnes.
Chaque déclaration de table du cluster doit indiquer quelles sont ses colonnes qui correspondent aux
colonnes clef du cluster :
create table <nom-table> (...)
cluster <nom-cluster> (<colonne> { , <colonne> }) ;
La correspondance entre les colonnes de la table et celles de la clef du cluster se fait par position : les
noms de colonnes n’ont pas besoin d’être identiques, en revanche leurs types et dimensions doivent
l’être.
Les lignes des tables qui auront la même valeur pour les colonnes clef du cluster seront stockées dans
les mêmes blocs disques, sachant que la valeur de la clef cluster n’est stockée qu’une seule fois dans
un bloc.
Ainsi on gagne en place et les équi-jointures faites sur la clef du cluster risquent d’être très efficaces.
Il faut ensuite, avant de pouvoir manipuler les tables, créer l’index du cluster :
create index <nom-index-cluster> on cluster <nom-cluster> ;
1. créer le cluster :
create cluster Emprunt (adherant Number (5)) index ;
2. créer les tables dans le cluster Emprunt :
create table Adherant (
id Number (5),
nom varchar2 (20),
constraint Adherant_PK primary key (id)
) cluster Emprunt (id) ;
On ne peut détruire un cluster qu’après avoir détruit toutes les tables qu’il contient :
drop table Livre ;
drop table Adherant ;
drop cluster Emprunt ;
Q. 185 En quoi cet exemple n’est peut-être pas très approprié pour illustrer les clusters ?
plus efficace, mais on peut imaginer qu’en revanche les modifications de la table coûteront plus qu’avec
une organisation en heap
Q. 186 Vérifier qu’une table organisée en index est ordonnée sur sa clef primaire, contrairement à
une table qui ne l’est pas. Cela se voit bien en supprimant une ligne puis en la recréant.
Postgres propose l’instruction cluster qui consiste à trier une table dans l’ordre de l’un de ses index.
Mais cette commande n’a pas d’effet sur les futures évolutions de la table : il faudra la relancer
régulièrement.
Cinquième partie
Les transactions
142
Chapitre 12
Les transactions
Une transaction a un début et une fin : le début correspond souvent au début de l’exécution de la
première instruction SQL, la fin correspond à une validation du travail qui rend publiques les mo-
difications faites (instruction commit ou lors d’une déconnexion normale) ou à une annulation qui
annule toutes les modifications faites (instruction rollback ou lors d’une déconnexion anormale) en
cas de problème.
Une transaction est l’exécution d’un code (en général une procédure) et non pas le code lui-même :
plusieurs transactions peuvent exécuter un seul et même code. On retrouve ici quelque chose de très
similaire à la distinction entre les notions de processus et de programmes : un processus (ou une tâche)
est une exécution d’un programme.
Une transaction est aussi l’unité de reprise (en cas de panne : il faut pouvoir lors du redémarrage du
SGBD annuler les effets partiels des transactions non terminées au moment de la panne et retrouver
les effets des transactions terminées au moment de la panne) et l’unité de concurrence (si le SGBD
autorise l’exécution simultanée ou quasi-simultanée de plusieurs transactions).
Une bonne pratique consiste à mettre en place des transactions courtes : peu d’ordres DML et une
exécution rapide. La gestion des reprises consomme alors moins d’espace et les problèmes dûs à la
concurrence (blocage, non sérialisabilité) sont moins probables.
Exemple de code faisant passer la base d’un état correct à un nouvel état correct et pouvant faire
l’objet d’une transaction : le virement de compte à compte qui a la propriété de conserver la somme
des soldes :
update Compte
set solde = solde - :somme
where id_compte = :debite ;
update Compte
set solde = solde + :somme
where id_compte = :credite ;
Ce programme est paramétré par les deux numéros de compte (debite et credite) et la somme à virer
143
144 CHAPITRE 12. LES TRANSACTIONS
(somme). Une transaction exécutant ce code disposera évidemment d’une valeur précise pour chacun
de ces paramètres.
Dans ce dernier cas il ne serait pas raisonnable qu’une session corresponde à une seule transaction (car
alors, comme on le verra dans la suite avec le niveau d’isolation sérialisable, un employé effectuant des
réservations de trains ne pourrait pas voir les réservations faites ses collègues et, pire, le travail d’une
journée pourrait alors se voir annulé lors de la déconnexion).
En fait une session est la période d’existence d’une connexion au SGBD permettant de lancer des
transactions successives.
Enfin, dans le cas d’une session interactive, l’utilisateur passe certainement plus de temps à ne pas
faire travailler le SGBD qu’à le faire travailler : la durée de sa session est certainement bien supérieure
à la somme des durées des transactions dont il a demandé l’exécution au cours de cette session.
Session 1 C T2 T5 T7 T9 D
Session 2 C T1 T3 T4 T6 T8 D
temps
Fig. 12.1 – On a deux sessions qui se recouvrent partiellement dans le temps. L’identification des
transactions se fait dans l’ordre chronologique de leurs démarrages, indépendamment de la session
pour le compte de laquelle elles s’exécutent. La dernière transaction de la session 1 (T9 ) se termine à
cause de la déconnexion de l’utilisateur (fin de session). Toutes les autres transactions se terminent
par un des deux ordres spécifiques commit ou rollback.
Dans l’exemple la correction pourait consister à garantir que la somme des soldes des comptes
est invariante lors d’un virement.
A comme atomicité Exécution en tout (commit) ou rien (rollback) des transactions
12.4. PROPRIÉTÉS QUE DOIVENT RESPECTER LES TRANSACTIONS : ACID OU CADI 145
Mémoire Centrale
a b CO:
11
00
code
00
11
00
11 Unique A B
00
11 Processeur
a b CO:
T2 variables
Fig. 12.2 – Modèle d’exécution des transactions. Chacune des deux transactions T1 et T2 dispose
de son propre espace de travail en mémoire centrale (CO : compteur ordinal, des variables a et b
et probablement une pile d’exécution). Le processeur n’exécute qu’une seule instruction à la fois,
ici il travaille pour le compte de T1 . Une transaction qui veut modifier un objet (un nuplet par
exemple) de la base doit (1) en lire une copie dans sa mémoire de travail, (2) modifier cette copie,
(3) réécrire dans la base cette copie comme nouvelle valeur de l’objet. À tout moment de l’exécution
de cette séquence, la transaction peut être temporairement suspendue par le système pour laisser
travailler une autre transaction. A et B sont les objets de la base susceptibles d’être modifiés par
les transactions. En tant qu’objets de la base ils sont accessibles par n’importe quelle transaction,
c’est donc l’état de ces objets qui risque de devenir incohérent si des protocoles de synchronisation
et de coopération inter-transactions ne sont pas mis en place. Un dernier point : on voit que deux
transactions concurrentes peuvent parfaitement exécuter le même code, la même procédure stockée par
exemple, d’où la distinction entre programme qui correspond à du code et transaction qui correspond
à l’exécution d’un code.
une transaction doit s’exécuter en tout ou rien : soit elle réussit et la base se trouve dans un
nouvel état correct, soit elle échoue et la base doit être remise dans son état correct de départ,
c’est à dire que tout se passe comme si la transaction n’avait jamais eu lieu.
Dans l’exemple de virement, entre les deux update la base est dans un état incorrect, si le
second update échoue (par exemple parce que le compte 572 n’existe pas ou bien que le SGBD
se plante) alors il faut annuler l’effet du premier update.
D comme durabilité Même en cas de panne logicielle voire matérielle
Les effets sur la BD d’une transaction réussie doivent être conservés durablement, même si le
SGBD se plante avant d’avoir eu le temps d’écrire sur disque le nouvel état de la BD (ce qui
est tout à fait possible puisque le SGBD utilise un système de cache mémoire lui permettant
d’optimiser les accès à la mémoire secondaire : les effets de la transaction sont inscrits dans le
cache, l’écriture du cache sur le disque ne se faisant qu’à un moment ultérieur que la transaction
ne maı̂trise pas).
Pour rendre durables ces effets, on valide la transaction (commit). En Oracle, on peut aussi
comprendre commit comme la publication des modifications faites par la transaction, car c’est
seulement à partir de ce moment que les autres transactions pourront éventuellement1 voir ces
modifications, sauf dans le cas de l’isolation read uncommitted défini par la norme SQL, mais
Oracle ne propose pas ce niveau de non isolation.
Les SGBD disposent en général de deux mécanismes pour garantir cette durabilité : 1) des
fichiers journaux (ou log) permettent de prendre en compte des pannes logicielles ou matérielles
n’affectant pas les supports de stockage du SGBD, 2) des sauvegardes complètes de la base pour
le cas où un disque est détruit.
I comme isolation Deux transactions concurrentes n’interfèrent pas sur les données qu’elles lisent
1
Éventuellement car une transaction sérialisable démarrée avant cette publication ne verra pas ces modifications.
146 CHAPITRE 12. LES TRANSACTIONS
ou modifient
La plupart des SGBD permettent à plusieurs utilisateurs de travailler simultanément sur la
base. Chaque utilisateur interagit avec la base par le biais de transactions. Deux transactions
simultanées (ou quasi-simultanées) peuvent potentiellement chercher à modifier le même objet
(nuplet) de la base de données, les interférences qui en découlent peuvent mettre la base dans
un état incorrect.
Exemple de deux transactions qui interfèrent sur le compte 537 :
Chaque transaction à son propre jeu de variables locales pendant son exécution.
Q. 187 A quel ordre SQL correspond ce qui est fait par T1 et T2 ? L’interférence entre T1 et T2
a pour conséquence que tout se passe comme si T1 n’avait pas eu lieu !
Dans cet exemple, il faut bien comprendre que chacune des deux transactions T1 et T2 dispose de
sa propre variable de travail s : le modèle d’exécution repose sur un processeur unique exécutant
en temps partagé chacune des transactions, chaque transaction disposant de son propre espace
pour stocker ses paramètres et ses variables comme l’illustre la figure 12.2 page 145.
L’idée est alors que le SGBD doit fournir des outils permettant de garantir les transactions contre
ce genre de problème. SQL propose deux outils : 1) le verrouillage d’objet qui permet d’obliger
une autre transaction à attendre que l’objet soit déverrouillé avant de pouvoir y accéder, et 2) le
niveau d’isolation d’une transaction T qui dit dans quelle mesure elle pourra voir les modifications
faites par les autres transactions, par exemple le niveau d’isolation SQL serializable fait que la
transaction T ne verra aucune des modifications faites par les transactions non terminées quand
T a commencé : elle aura l’impression d’être la seule à utiliser la base de données (ce qui n’est
pas nécessairement la solution à tous les problèmes !).
La procédure de virement de compte à compte qui évite les interférences entre transactions peut
s’écrire en PL/SQL Oracle comme indiqué à la section 15.5, page 174.
Pour gérer l’annulation d’une transaction (instruction rollback), le SGBD mémorise dans des seg-
ments d’annulation (rollback segments), les valeurs initiales des nuplets modifiés. En cas d’annulation
de la transaction il est alors possible, grâce aux segments d’annulation, de remettre les objets modifiés
par la transaction dans leurs états initiaux.
Pour permettre, lors de la reprise après une panne système ou une coupure électrique, d’annuler les
transactions non terminées lors d’une panne, le SGBD mémorise dans un journal (fichier log) les états
avant et après des objets modifiés par les transactions.
Le problème est que les modifications de la base ne sont pas écrites immédiatement sur disque : le
SGBD utilise un système de cache en mémoire centrale permettant d’éviter de trop nombreuses et
coûteuses entrées-sorties sur le disque. Cette mémoire cache contient des copies de blocs du disque
et c’est sur ces copies qu’ont lieu les mises à jour. Plus tard, au moment opportun, ces copies seront
copiées sur le disque pour mettre la base à jour.
Que se passe-t-il alors si une panne de courant vient effacer le contenu de la mémoire cache ?
– d’une part des modifications faites par des transactions validées n’ont probablement pas été écrites
sur le disque, une partie des effets de ces transactions est donc définitivement perdu
– encore pire, la base a toutes les chances d’être dans un état incorrect. En effet, pour les transactions
validées au moment de la coupure, seule une partie de leurs modifications ont pu être écrites sur le
disque.
Pour éviter ce genre de problème (dû à une coupure de courant ou à un écroulement du système),
les SGBD mettent en place des mécanismes de reprise qui repose sur un journal de reprise (fichier
log sur disque dont on a déjà parlé à la section 12.5.2 page 147) contenant les informations sur les
mises à jours effectuées par les transactions. Ce journal est mis à jour physiquement lors de plusieurs
événements :
– avant toute mise à jour physique de la base de données avec les caches mémoire, les informations
de reprise de ces mises à jour sont écrites physiquement dans le journal,
– lors d’une validation (commit) toutes les mises à jour de la transaction qui n’ont pas été écrites
physiquement dans le journal doivent l’être ainsi qu’une information indiquant la validation de la
transaction. Seulement quand ces informations ont été physiquement écrites, l’opération de valida-
tion (commit) peut se terminer et le programme reprendre son cours.
– cycliquement le SGBD effectue un point de contrôle qui consiste à :
1. écrire physiquement des caches mémoire dans la base de données (ce qui implique une écriture
préalable des informations de reprise dans le journal de reprise),
2. écrire physiquement dans le journal de reprise un point de contrôle contenant l’identitification
de toutes les transactions en cours d’exécution.
Principe de restauration : lors de la reprise le SGBD effectue :
– d’abord un parcours du journal de reprise en arrière en annulant les effets des transactions non
validées au moment de la défaillance et ce jusqu’au dernier point de contrôle enregistré,
– puis ils effectue, à partir de ce dernier point de contrôle, un parcours en avant du journal en rejouant
les modifications des transactions validées au moment de la défaillance.
2
Cette section s’inspire fortement de [5].
Chapitre 13
Les SGBD sont pour la plupart multi-utilisateurs, c’est à dire que plusieurs utilisateurs doivent pou-
voir simultanément consulter et modifier une même base de données.
Le postulat de base est donc qu’un utilisateur n’interagit avec la base que via des transactions, ce qui
est d’ailleurs vrai en Oracle et Postgres (on parle de bases de données transactionnelles).
Le but du jeu est que chaque transaction ait l’impression d’être la seule à utiliser la base (et non pas
chaque utilisateur : l’employé qui fait des réservations de train — chaque exécution d’une réservation
représentant une transaction — doit voir les réservations faites par les autres employés !), c’est ce
qu’on appelle l’isolation. Le SGBD est, dans certains cas, incapable de garantir cette isolation des
transactions (pour des raisons conjoncturelles tout à fait valables et qui ne remettent pas forcément en
cause la correction des programmes exécutés par les transactions, on le comprendra mieux plus tard) ;
en revanche il est toujours capable de se rendre compte de cette incapacité. Quand il se rend compte
de cette incapacité, il le signale à la transaction (en Oracle sous forme de l’erreur Oracle ORA-08177).
Celle-ci peut (doit) alors décider de se terminer en annulant toutes les modifications qu’elle a effectuées
grâce à l’instruction SQL d’annulation rollback, et elle peut tenter de mener à bien le travail qu’elle
était censée faire en se relançant (grâce à une boucle).
Les SGBD fournissent en général deux outils pour gérer les interactions entre transactions : d’une part
on peut spécifier pour chaque transaction un niveau d’isolation plus ou moins étanche, d’autre part on
peut effectuer des verrouillages explicites d’objets de la base (en général on peut verrouiller les tables
et les nuplets) pour synchroniser les transactions qui accèdent à des objets communs.
Un ordonnancement est la trace chronologique des instructions exécutées par les transactions.
Un ordonnancement est dit entrelacé si les instructions (li comme lecture et ei comme écriture faites
par la transaction i) des transactions sont mélangées : (l2 l1 l1 e2 l2 e1 ) est un ordonnancement entrelacé
des deux transactions T1 = (l1 l1 e1 ), T2 = (l2 e2 l2 ).
Quand le processeur est peu chargé, il se peut que chaque transaction soit exécutée de bout en bout
148
13.2. DES PROBLÈMES DÛS À LA CONCURRENCE 149
sans être interrompue par une autre transaction, on parle alors d’ordonnancement séquentiel.
Intérêt d’une exécution quasi-parallèle par rapport à une exécution purement séquentielle :
– transaction longue/transaction courte : même si la transaction courte commence après le début de
la transaction longue, elle pourra se terminer avant,
– transaction en attente d’entrée/sortie sur disque : une autre transaction peut alors prendre la main.
Remède : comme dans l’exemple précédent, mais en plus il faut que T1 ne déverrouille A qu’après son
annulation, ainsi T2 verra la valeur originale de A. Ce protocole de déverrouillage s’appelle verrouillage
deux phases rigoureux et est automatiquement garanti par Oracle et Postgres.
Analyse incohérente : la somme affichée n’est pas correcte car A est lu après modification et B l’est
avant d’être modifié. Si initialement A vaut 500 et B vaut 1000 alors la transaction T1 affiche 1300 au
lieu d’afficher 1500.
Remède : il suffirait de se souvenir de la valeur originale du nuplet A : T1 utiliserait alors des valeurs de
A et B qui sont en phase. C’est ce que propose le protocole multi-versions mis en place automatiquement
par Oracle et Postgres. (On pourrait aussi s’en sortir en utilisant des verrouillages).
Lectures non reproductibles. : la même transaction T1 voit deux valeurs différentes pour le même
objet A.
Remède : exactement le même que dans l’exemple précédent.
Q. 188 Donner un ordonnancement entrelacé correct de l’exemple 13.2.1 page 149.
13.2.5 Moralité
On remarque que les différents problèmes vus précédemment sont toujours dûs aux opérations de lec-
ture et d’écriture qui peuvent provoquer des interférences entre transactions quand elles s’appliquent
aux mêmes objets de la base.
C’est pourquoi, dans la suite, on ne s’intéressera plus qu’à ces opérations de lecture et d’écriture.
ei (o) l’opération d’écriture de l’objet o par la transaction Ti .
i
l (o) l’opération de lecture de l’objet o par la transaction Ti .
13.3. APPROCHE THÉORIQUE : LA SÉRIALISABILITÉ 151
Par exemple, voici un cas d’ordonnancement non sérialisable . . . e1 (o) . . . e2 (o) . . . e1 (o) . . . . On
voit qu’on ne peut trouver aucun ordonnancement séquentiel de T1 et T2 . En effet il faudrait que, dans
la séquence, T1 précède T2 et que T2 précède T1 , ce qui est évidemment impossible !
Dans un ordonnancement, deux opérations exécutées sur le même objet par deux transactions différentes
induisent un ordre des deux transactions si l’une est une écriture et l’autre une écriture ou une
lecture1 . Par exemple l’ordonnancement (e2 (o, 501) l1 (o)) implique que T2 doit précéder T1 dans un
ordonnancement séquentiel équivalent.
Inversement, deux opérations de lecture d’un même objet par deux transactions n’induisent pas d’ordre
des deux transactions.
Autrement dit, deux opérations induisent un ordre si leurs effets sur l’objet ou sur le calcul effectué
risquent de ne pas être les mêmes suivant l’ordre dans lequel on les exécute, en voici un exemple très
simple : (e1 (o, 501) e2 (o, −61)) et (e2 (o, −61) e1 (o, 501)).
Q. 191 Appliquer aux exemples précédents : 13.2.1, 13.2.2, 13.2.3, 13.2.4 et à l’ordonnancement
correct
Q. 192 L’ordonnancement suivant est-il correct ? est-il sérialisable ? qu’en conclure sur la théorie de
la sérialisabilité ?
1
Certains auteurs parlent, dans ce cas, d’opérations incompatibles.
152 CHAPITRE 13. GESTION DE LA CONCURRENCE DES TRANSACTIONS
Remarques
– Il existe d’autres manières d’induire un ordre qui sont plus sophistiquées et reconnaissent plus
d’ordonnancements comme étant sérialisables que celle présentée ici (qui a le mérite de la simplicité).
Par exemple, si on considère des opérations de plus haut niveau que de simples lectures et écritures :
x1 (o, 10) et x2 (o, 20) n’induisent pas d’ordre si l’opération x consiste à (1) lire la valeur de o, (2)
ajouter à cette valeur celle du deuxième paramètre et (3) écrire cette valeur comme nouvelle valeur de
o. Notre définition de l’induction d’ordre ferait que l’ordonnancement (x1 (o, 10) x2 (o, 20) x1 (o, 30))
serait déclaré non sérialisable alors qu’avec cette nouvelle définition on se rend compte qu’il est
parfaitement sérialisable.
– Cette étude théorique de la sérialisabilité suppose que toutes les transactions participant à l’or-
donnancement sont terminées. En pratique il se peut que, sur un SGBD chargé, les transactions
s’entrelacent de façon permanente, les ordonnancements à étudier pourraient donc être de lon-
gueurs illimitées et une telle approche n’est donc pas applicable pratiquement. Les SGBD mettent
donc en œuvre des protocoles de prévention (par verrouillage des données) et de détection de non
sérialisabilité (par estampillage des données et des transactions) qui sont plus contraignantes que
la théorie (elles empêcheront certains ordonnancements bien qu’il soient sérialisables) mais qui sont
réalisables techniquement.
On verra qu’Oracle prend le meilleur de ces deux types de protocole (prévention et détection) pour
corriger les défauts de l’une avec les qualités de l’autre.
Chaque donnée (nuplet) de la base peut-être verrouillée, utilisée puis déverrouillée par une transaction
(le déverrouillage est fait automatiquement en fin de transaction : voir le protocole V2PR section 13.8
page 155).
Une transaction ne manipule une donnée que si elle l’a préalablement verrouillée dans le mode appro-
prié (ceci est garanti par le SGBD puisque c’est lui qui implicitement verrouille les données). Quand
une transaction demande à verrouiller une donnée déjà verrouillée par d’autres transactions dans un
13.6. UN PROTOCOLE NAÏF DE VERROUILLAGE 153
mode incompatible, elle est mise en attente jusqu’à ce que tous les verrouillages incompatibles
soient levés. Il y a deux modes de verrouillage :
verrouillage partagé : S comme Shared si la transaction ne souhaite que lire la donnée. L’opération
de verrouillage est lockS (o), celle de déverrouillage unlockS (o).
Typiquement ce mode de verrouillage est effectué automatiquement par le SGBD sur chaque
nuplet sélectionné par une requête (select).
Attention : Oracle et Postgres (depuis la version 6.5) ne disposent pas de ce mode de verrouillage,
ils préfèrent utiliser un système de multi-versions des nuplets permettant de ne jamais bloquer
les transactions en lecture seule.
Le tableau suivant résume les compatibilités entre les deux modes de verrouillage :
état de verrouillage
non verrouillé S X primitives
demande Shared : lecture + + - lockS (), unlockS ()
de verrou eXclusive : modification + - - lockX(), unlockX()
Ce protocole consiste à verrouiller l’objet que l’on souhaite mettre à jour, puis à le déverrouiller dès
la fin de cette mise à jour.
Ce protocole résout le problème de perte de mise à jour de la section 13.2.1 page 149 :
154 CHAPITRE 13. GESTION DE LA CONCURRENCE DES TRANSACTIONS
Ici, il faudrait que T2 attende la fin de T1 pour commencer son exécution. Ceci est parfaitement possible
si on s’arrange pour que le déverrouillage de A par T1 se fasse après le verrouillage de B (verrouillage
deux phases).
C’est pourquoi on introduit le protocoles de verrouillage deux phases dont l’objectif est de
garantir la sérialisabilité.
Q. 195 Réécrire l’exemple précédent (section 13.7.1) avec le V2PR : l’annulation de T2 est-elle
nécessaire ?
Ce protocole garantit la sérialisabilité et l’absence de cascades d’annulations. Il est automatiquement
mis en œuvre en Oracle et PostgreSQL.
T1 attend T2
Q. 196 Donner un exemple d’interblocage mettant en jeu trois transactions.
Dans la question précédente, n ne peut pas être égal à 1, c’est à dire qu’une même transaction ne
peut pas s’interbloquer avec elle-même : les SGBD (et les systèmes de synchronisation en général)
sont assez malins pour permettre à une même transaction de verrouiller plusieurs fois le même objet
(par exemple, en Java, une méthode synchronized peut parfaitement être récursive !).
Comme en médecine pour les maladies, il y a deux manières de gérer les interblocages : la prévention qui
consiste à s’arranger pour qu’il n’y ait jamais d’interblocage, la détection qui laisse les interblocages se
produire puis les détecte et les corrige en annulant une des instructions qui participe à cet interblocage.
Pour la technique de détection on n’annule aucune transaction mais plutôt l’instruction d’une des
transactions qui participe à l’interblocage (c’est ce que fait Oracle en provocant une erreur pour cette
instruction).
Ainsi le code d’une transaction doit envisager l’échec de ses intructions pour cause d’interblocage.
Le plus simple est d’effectuer un rollback brutal, d’attendre un peu que les choses se calment et de
relancer une nouvelle transaction sur le même code pour tenter de faire quand même le travail, car en
général un interblocage ne correspond pas à une erreur de programmation mais plutôt à un manque
de chance.
Une solution plus subtile consiste à rester dans la même transaction en effectuant éventuellement un
rollback partiel (rollback to savepoint).
Les SGBD détectent périodiquement les interblocages et les dénouent en faisant échouer l’instruction
en cours d’une des transactions participant à l’interblocage (en Oracle c’est l’erreur -00060). Comment
choisir cette transaction :
– celle qui est la plus proche de sa fin (comment le savoir ?)
– celle qui a fait le moins de modifications
– la plus jeune (elle vieillira et deviendra de moins en moins sujette à avortement)
– en Oracle, il semble qu’il n’y ait pas de critère particulier.
Le fait que les verrous ne soient relâchés qu’en fin de transaction (commit ou rollback) garantit que
lors d’un tel échec aucune autre transaction n’a pu lire une donnée produite par la transaction choisie,
ainsi la résolution d’un interblocage ne produira jamais d’avortements en cascade.
Granularité des objets verrouillables en général deux grains : la table et le nuplet (Oracle et
Postgres ont ces deux grains).
Le verrouillage deux phases rigoureux bloque toute transaction qui tente de lire une donnée en cours
de modification et inversement. Le protocole multi-versions permet d’assouplir cela en permettant
que les lectures ne soient jamais bloquées et qu’elles ne bloquent jamais les écrivains : le mode de
verrouillage partagé (SHARE) n’est plus nécessaire. C’est ce que proposent Oracle et Postgres.
On parle d’objet, sachant que dans une base de données l’objet est une ligne de table (ou nuplet).
Chaque objet aura plusieurs versions : chaque changement de valeur de l’objet produit une nouvelle
version.
Pour distinguer les versions d’un même objet, chaque version V d’une ligne sera estampillée comme
ceci Vcr où :
1. c est l’estampille de création de cette version, c’est la date de démarrage de la transaction ayant
produit cette version. Cette estampille est constante.
2. r est l’estampille de lecture de cette version, c’est la date de démarrage de la transaction la plus
récente ayant lu cette version. Cette estampille est variable (elle ne peut que croı̂tre).
3. A la création d’une nouvelle version on a Vcc où c est l’identifiant de la transaction productrice.
On supposera que deux transactions ne pourront jamais avoir la même date de démarrage (c’est facile
si on utilise la valeur d’un compteur pour dater les transactions, le compteur étant incrémenté après
chaque démarrage d’une transaction).
Une transaction Th ne pourra lire que la version Vcr telle que c est la plus grande estampille ≤ h.
max(r,h)
L’effet de cette lecture modifiera l’estampille de lecture comme ceci :Vc . Cette mise à jour de
l’estampille de lecture permettra de faire échouer une tentative d’écriture par la transaction Tk telle
158 CHAPITRE 13. GESTION DE LA CONCURRENCE DES TRANSACTIONS
que c ≤ k < max(r, h) car cela ôterait toute signification à la lecture faite par Th .
Par exemple si la ligne lue par la transaction T11 a les versions V46 V79 V12 15 c’est la version V 9 qui sera
7
15 . Si T
lue et la liste de versions deviendra V46 V711 V12 6 11 11
11 créée une version on obtient : V4 V7 V11 V12 .
15
Voici un exemple où deux transactions T9 et T10 incrémentent l’entier d’une même ligne et où tout se
passe bien :
La tentative d’écriture de T9 échoue car on se rend compte que cette version a été lue par une
transaction plus récente puisqu’elle est dans l’état 510 6 : le 10 indique qu’une transaction plus récente
a lu cette valeur et il ne faut pas que T9 puisse changer ce passé.
Cet échec de T9 montre que T10 est bien isolée des modifications faites par d’autres transactions.
On en déduit que :
′
– la tentative par Th de créer une nouvelle version dans . . . Vcr Vcr′ . . . avec c ≤ h < c′ doit vérifier
′
r ≤ h (sinon échec), et on obtient . . . Vcr Vhh Vcr′ . . .
– une lecture peut allonger l’intervalle [c, r] de la version qu’elle lit mais sans chevaucher l’intervalle
de la version suivante dont l’estampille de création est forcément strictement supérieure à celle de
la transaction
Du coup, pour les versions Vcr11 Vcr22 Vcr33 Vcr44 d’une ligne on est sûr d’avoir la propriété : c1 ≤ r1 ≤
c2 ≤ r2 ≤ c3 ≤ r3 ≤ c4 ≤ r4 .
10 ? cela pourrait-il
Q. 200 La transaction T10 peut-elle donner une nouvelle valeur à la version V10
déranger une autre transaction qui aurait lu cette version ?
15 ?
Q. 201 La transaction T10 peut-elle donner une nouvelle valeur à la version V10
La figure 13.1 page 159 donne une approche graphique de MVE.
Q. 205 Montrer que sur l’historique Q75 de Q, MVE n’accepte que l’ordonnancement [l9 (Q) e9 (Q) l10 (Q) e10 (Q)].
Q. 206 Montrer que sur l’historique Q75 de Q, [l10 (Q) l9 (Q) e10 (Q) l9 (Q)] est accepté et surtout
correct : T9 lit-elle toujours la même valeur ?
13.9. MULTI-VERSIONS ESTAMPILLÉES MVE, PROTOCOLE D’ISOLATION 159
Fig. 13.1 – Voici une représentation plus graphique des versions 1473 1011 16
10 2114 d’une ligne contenant
un entier et quelles versions vont voir les transactions représentées. Sur cette figure les instants ont
une largeur non nulle de manière à pouvoir montrer la valeur de la ligne.
14 10 21
3 7 10 11 14 16
Q. 202 Donner les transactions dont on est sûr qu’elles ont écrit une valeur sur cette ligne et celles
dont on est sûr qu’elles ont lu cette ligne.
Q. 203 Donner les transactions dont on est sûr qu’elles n’ont jamais lu la ligne.
Si T8 lit cette ligne, elle obtient la version 1473 et l’état des versions devient :
14 10 21
3 8 10 11 14 16
Si T8 augmente cette valeur de 5 puis l’écrit dans la même ligne, le nouvel état des versions sera :
14 19 10 21
3 8 10 11 14 16
Q. 204 Qu’est-ce qui explique que l’écriture faite pat T8 ne gêne pas T9 ?
Le protocole est alors le suivant : lors d’une tentative de lecture ou d’écriture de Q par la transaction
Th , on choisit la version Qrc de plus grand c avec c ≤ h. Puis s’il s’agit d’une :
– lecture : on met à jour le r de Qrc avec max(r, h) et sa valeur est utilisée.
– écriture, il y a trois cas :
– si c = r = h : aucune autre transaction n’a encore lu cette version produite par Th : la valeur de
Qrc est remplacée par la valeur écrite sans qu’il y ait création d’une nouvelle version.
– si c = r < h ou c < r ≤ h : la nouvelle version Qhh est créée.
– si c ≤ h < r : la transaction Tr plus récente que Th , a déjà lu la donnée : Th ne doit pas modifier
Qrc et être annulée afin de garantir l’isolation de Tr .
Cela a pour conséquence que Th verra les écritures qu’elle a faites.
Un grand intérêt de ce protocole est de garantir aux transactions, même si certaines sont très longues,
qu’elles liront toujours la même valeur d’une ligne de table.
160 CHAPITRE 13. GESTION DE LA CONCURRENCE DES TRANSACTIONS
Q. 207 Supposons que T6 ait produit avec succès la nouvelle version P66 de l’objet P , puis que la
transaction T8 ait lu P66 qui devient P68 , puis que T6 échoue en tentant d’écrire une nouvelle version
de Q74 . T6 est donc annulée, mais que devrait-il se passer en plus de cette annulation ? Cela paraı̂t-il
raisonnable ? (c’est le problème de ce protocole qui sera résolu par l’utilisation du verrouillage)
Pratiquement une opération de lecture isolée correspond à une requête (select) tandis qu’une lecture
suivie d’une écriture d’un même objet correspond à une mise à jour d’un nuplet (update). Dans les
deux cas l’objet préexiste à l’opération.
Le cas de la création d’un nouveau nuplet (insert) par T6 peut être pris en compte par la création
d’un nouvel objet O dont l’unique version est O66 .
Q. 208 Supposons que T4 tente une lecture de Q ayant les versions Q11 15
7 Q11 . Comment interpréter ce
cas ?
L’interprétation est exactement la même dans le cas d’une mise à jour (update).
Q. 209 Montrer qu’une transaction qui ne fait que des lectures de Q lira toujours la même valeur.
(on peut montrer qu’il n’est pas possible qu’elle obtienne deux valeurs différentes)
Q. 211 Que se passe-t-il si T13 est annulée ? Annuler une transaction revient à supprimer les versions
qu’elle a créées.
Q. 212 Reprendre le tableau précédent en remarquant que l’attribution d’une nouvelle version à un
objet correspond à un update, or un update commence toujours par lire la version correspondant à
la transaction pour pouvoir calculer la nouvelle version. Par exemple augmentation de 10% du salaire
d’un employé.
Suppression des versions inutiles On peut montrer que des versions anciennes ne seront plus
jamais utilisées par aucune transaction présente ou future. Soient h l’estampille de la plus ancienne
′
transaction encore active et Qrc et Qrc′ deux versions de Q, telles que c < c′ ≤ h. La version Qrc peut
être supprimée. Exemple :
Soit : Q13
6 Q18
13 Q21
18
La plus ancienne transaction active est T14 : suppression de Q11
6 : Q18
13 Q21
18
On représente chaque version d’un objet Q par Qrc , et on supposera qu’on dispose initialement de
l’unique version Q66 .
13.9. MULTI-VERSIONS ESTAMPILLÉES MVE, PROTOCOLE D’ISOLATION 161
versions de Q T9 T10
Q66
lire (Q) donne Q6
Q96
lire (Q) donne Q6
Q10
6
ecrire (Q) ⇒ annulation
Si, après l’écriture de Q par T9 , T10 ne tente pas de lire ou d’écrire Q l’ordonnancement est sérialisable.
Mais au moment de l’annulation de T9 on ne le sait pas encore et cette annulation est peut-être abusive,
mais nécessaire du point de vue du protocole multi-versions.
versions de Q T9 T10
Q66
lire (Q) donne Q6
Q96
lire (Q) donne Q6
Q10
6
ecrire (Q)
Q10
6 Q10
10
ecrire (Q) ⇒ annulation
versions de Q T9 T10
Q66
lire (Q) donne Q6
Q10
6
ecrire (Q)
Q10
6 Q10
10
lire (Q) donne Q6
Q10
6 Q10
10
ecrire (Q) ⇒ annulation
Q. 213 Montrer que cet ordonnancement est pourtant sérialisable (13.3 page 150).
13.9.4 Ordonnancement non sérialisable mais accepté, à juste titre, par MVE
Un ordonnancement non sérialisable peut être accepté, à juste titre, par ce protocole, ceci grâce aux
versions multiples d’un même objet. Un exemple où T9 lit toujours la même valeur :
162 CHAPITRE 13. GESTION DE LA CONCURRENCE DES TRANSACTIONS
versions de Q T9 T10
Q66
lire (Q) donne Q6
Q10
6
lire (Q) donne Q6
Q10
6
ecrire (Q)
Q10
6 Q10
10
lire (Q) donne Q6
Q10
6 Q10
10
lire (Q) donne Q10
Q10
6 Q10
10
Q. 214 Montrer que cet ordonnancement n’est pas sérialisable (13.3 page 150).
Cela montre simplement que la définition de la sérialisabilité que nous utilisons (car il y en a d’autres)
n’est pas absolue et ne prend donc pas en compte les possibilités de MVE : la théorie de la sérialisabilité
est une simplification du monde.
En conclusion : soit l’ensemble Os des ordonnancements sérialisables et Omve celui des ordonnance-
ments acceptés par le protocole multi-versions, on a : Os ∩ Omve 6= ∅, Os 6⊆ Omve et Omve 6⊆ Os .
Si par malheur T11 est déjà validée alors c’est foutu : on peut mettre la base de données à la poubelle !
Solution : faire attendre, grâce à un verrou, l’écriture de T11 jusqu’à ce que T9 soit validée ou annulée
(⇒ V2PR).
Le protocole multi-versions ne peut donc se passer d’un protocole garantissant l’absence de cascades
d’annulations : le V2PR paraı̂t bien convenir.
————– lire(Q) →
————– lire(Q) →
————– ecrire(Q)
————– lire(Q) →
————– ecrire(Q)
————– lire(Q) →
————– ecrire(Q)
————– lire(Q) →
————– lire(Q) →
Q. 216 En fin de l’ordonnancement précédant, on suppose que toutes les transactions d’estampille
< 10 sont terminées, donner les versions que l’on peut supprimer.
Chaque version peut vivre sa vie : de nouvelles versions peuvent être produites par des transactions
anciennes et ces versions ne seront jamais vues par les futures transactions. Par exemple :
versions de Q
Q10
6
T12 écrit Q
Q10 12
6 Q12
T11 écrit Q
Q10
6 Q11
11 Q12
12
T18 écrit Q
Q10 11 12 18
6 Q11 Q12 Q18
T21 lit Q
Q10 11 12 21
6 Q11 Q12 Q18
versions de Q
Q10
6
T12 lit Q
Q12
6
T12 écrit Q
Q12 12
6 Q12
T11 lit Q
Q12
6 Q12
12
T11 échoue en tentant d’écrire Q
Q12
6 Q12
12
T18 lit Q
Q12 18
6 Q12
T18 écrit Q
Q12 18 18
6 Q12 Q18
T21 lit Q
Q12
6 Q18
12 Q21
18
On voit qu’il n’y a plus de trous entre les versions successives et les anciennes transactions disposent
toujours des versions qui les concernent. C’est exactement ce que font Oracle et Postgres.
Chapitre 14
165
166 CHAPITRE 14. ÉLÉMENTS D’IMPLANTATION DES TRANSACTIONS
(il n’y a pas d’estampille de lecture). La version la plus récemment produite est dans la table alors
que les versions plus anciennes sont mémorisées dans le segment de recouvrement (rollback segment).
Les versions sont dans une liste ordonnée de la version la plus récemment créée à la plus ancienne. Pour
créer une nouvelle version V ′ , un update doit modifier la version la plus récente V (celle en table) et
V ′ sera insérée en tête de la liste des versions poussant ainsi V dans le segment de recouvrement, sauf
si c’est la même transaction qui produit V et V ′ , dans ce cas V ′ se substitue à V .
Si une transaction tente de modifier une version de ligne qui n’est pas la plus récente, une erreur sera
produite.
Les versions du segment de recouvrement ne peuvent qu’être lues, elle ne peuvent pas servir à calculer
une nouvelle version (update) ou à supprimer la ligne (delete).
Q. 218 Quelles sont les transactions validées (commit) parmi celles qui ont manipulé la table T ?
Q. 220 Le diagramme temporel de la figure 14.1 donne un historique cohérent avec les données
ci-dessus (État 1), trouvez la petite erreur du diagramme !
14.1. MULTI-VERSIONS ET V2PR (ORACLE, POSTGRESQL ET MYSQL/INNODB) 167
T9 T13 session 1
10 11 12 13 14 T 15 16 Etat 1 temps
Fig. 14.1 – Diagramme temporel possible des transactions de 4 sessions (erreur par rapport à État
1). Les boı̂tes en pointillés correspondent aux transactions terminées à l’instant t. On voit par exemple
que T10 existait encore quand T13 a démarré mais était terminée au démarrage de T15 . Quant à T16
elle est déjà terminée (le prochain SCN global est 17). On aurait aussi pu représenter les transactions
de SCN inférieurs à 10, mais il est sûr qu’elles sont toutes terminées quand T13 démarre.
Q. 222 La CTTA d’une transaction read only ou serializable peut-elle contenir un SCN supérieur
au sien ?
Q. 223 L’État 2 suivant est incorrect. Un diagramme temporel pourrait aider à trouver l’incohérence.
14.1.5 Lecture d’une ligne : transaction read only et serializable
L’objectif est le suivant : pour une ligne, Ta obtient la version qu’elle a produite elle-même ou,
sinon, la version la plus récente produite par une transaction Tb déjà validée quand Ta a démarré
(principe d’isolation de read only et serializable), autrement dit b < a ∧ b 6∈ CTTA de Ta .
Q. 224 Montrer que, si la version obtenue par T est la plus récente (celle qui est dans la table), alors,
si la ligne est verrouillée c’est forcément par T (montrer que les autres cas sont impossibles).
L’algorithme est alors le suivant : T obtient la version la plus récente (c’est à dire en parcourant la
liste des versions en partant de celle qui est dans la table) dont le SCN est inférieur ou égal à son SCN
et n’est pas dans sa CTTA.
Si aucune version acceptable n’existe c’est que le nuplet (la ligne) n’existait pas au démarrage de T
et T ignore donc ce nuplet.
Q. 225 Que lit la transaction 5025 si elle explore la table donnée en 14.1.2 p.166 ?
1. V est dans la table (donc version la plus récente) : le nuplet est verrouillé par Ta puis la mise
à jour est faite dans la table avec mise de V dans le segment de recouvrement si son SCN n’est
pas égal à celui de Ta .
2. V est dans le segment de recouvrement et le nuplet n’est pas verrouillé : on en déduit que la
version la plus récente (celle qui est dans la table) a été fabriquée par une transaction Tb déjà
validée et soit Tb est plus jeune que Ta soit le SCN de Tb est dans la CTTA de Ta . Dans les
deux cas Ta cherche à modifier une version trop ancienne : une erreur de non sérialisabilité est
déclenchée (ORA-08177), provoquant l’abandon du update ou du delete1 .
3. V est dans le segment de recouvrement et le nuplet est verrouillé par une autre transaction Tb :
Ta va être bloquée jusqu’à la fin de Tb . Tb peut se terminer de deux façons :
– Tb est validée (commit), le verrou est relâché et on se trouve dans la situation 2 précédente :
une erreur de sérialisabilité est déclenchée pour le update de Ta .
– Tb est annulée (rollback), le verrou est relâché et Ta se retrouve soit dans le premier cas avec
un succès, soit dans le deuxième avec un échec.
Deux exemples du troisième cas ou T10 serializable tente de modifier l’unique ligne de T. La version
vue par T10 est entourée :
– D’abord un succès :
Mémoire Globale Mémoires locales des transactions
TTA = (5, 10) Transaction serializable 10 : CTTA = (2, 4, 5) T10 tente de mettre à jour la
prochain SCN = 11 Transaction serializable 5 : CTTA = (4) ligne 1 : elle est bloquée par la
modification de T5 . Si T5 effec-
table T segment de recouvrement
valeur
tue un rollback, la table re-
ligne scn valeur scn scn valeur
prend son état initial :
1 5 (1, B) → 3 (1, C)
table T segment de recouvrement du coup T10 est débloquée et réussit sa
ligne scn
valeur scn valeur scn valeur mise à jour car T3 était terminée quand
1 3 (1, C) T10 à commencé.
– Maintenant un échec : au début T10 est bloquée par la modification de T5 :
table T segment de recouvrement
valeur
ligne scn valeur scn valeur scn À nouveau T5 effectue un rollback,
la table reprend son état initial :
1 5 (1, B) → 4 (1, C) → 3 (1, D)
table T segment de recouvrement du coup T10 est débloquée, mais la version
ligne scn valeur scn valeur qu’elle tente de modifier n’est pas la plus
récente : l’erreur de non sérialisabilité est
1 4 (1, C) → 3 (1, D)
déclenchée (ORA-08177).
Q. 226 Donner un ordonnancement qui fasse que la version lue par T30 ne soit pas la plus récente
du segment de recouvrement (par exemple c’est la troisième de la liste des versions).
Plus tard, la transaction 5025 commence à lire la table, l’état de la TTA et de la table ont pu changer,
mais pas celui de la CTTA :
1
En fait il ne s’agit pas à proprement parler d’une erreur de sérialisabilité, mais plutôt d’une indication disant
qu’Oracle ne peut garantir que cette exécution est sérialisable.
14.1. MULTI-VERSIONS ET V2PR (ORACLE, POSTGRESQL ET MYSQL/INNODB) 169
Q. 227 Que lit la transaction 5025 si elle explore la table ci-dessus ? L’insensibilté de la transac-
tion sérialisable aux modifications faites par les autres transactions est-elle effective ? (voir la ques-
tion Q.225)
Q. 231 Pour quelle raison peut-on être sûr que 2033 a été validée (commit) ?
Q. 232 Comment prendre en compte la suppression du 2ième nuplet par 5025 ? conserver les versions !
Q. 233 Que doit faire le système pour valider (commit) 5555 ? conclusion ?
sur cet aspect uniquement (car une validation doit aussi mettre à jour les fichiers journaux) que peut-
on en conclure sur l’efficacité de l’instruction commit en Oracle ?
Si une instruction read committed est bloquée par un verrou, lors de son déblocage elle rechargera
sa CTTA avec la TTA actuelle. Elle pourra donc voir les modifications faites par la transaction qui la
bloquait.
Dans une transaction en isolation read committed, chaque instruction DML voit les versions publiées
(commit) par d’autres transactions avant qu’elle ne commence.
Lorsqu’une instruction read committed est débloquée d’un verrou elle recommence depuis le début
son traitement en voyant toutes les modifications publiées par d’autres transactions avant son redémarrage.
Q. 235 Proposer un protocole pour l’isolation read committed.
Q. 236 L’état suivant peut-il être atteint si 14 était serializable ? peut-il l’être si 14 était read
committed.
Mémoire Globale
TTA = (25, 26), prochain SCN = 27
170 CHAPITRE 14. ÉLÉMENTS D’IMPLANTATION DES TRANSACTIONS
171
172 CHAPITRE 15. LES NIVEAUX D’ISOLATION DES TRANSACTIONS
Q. 237 Oracle permet-il à une transaction d’observer des modifications non validées ?
15.2. POSITIONNER L’ISOLATION PAR DÉFAUT : 173
En Oracle ou Postgres, tant qu’elle est vivante, une transaction est la seule à voir les modifications
qu’elle a effectuées.
Oracle ne dispose que du verrouillage exclusif (X) des nuplets, il ne dispose pas de verrouillage partagé
(S)1 . C’est grâce à la gestion des multiples versions des nuplets qu’Oracle peut se passer de ces verrous
S tout en garantissant que les lectures ne sont jamais bloquées ni bloquantes par/pour les écritures
faites par d’autres transactions.
15.3.1 savepoint
Pose un point de reprise intermédiaire dans la transaction courante, ce qui permettra de faire un
rollback partiel de la transaction mais sans terminer cette transaction, on peut ensuite retenter le
traitement annulé sans devoir créer une nouvelle transaction.
15.3.2 commit
Termine la transaction et tous les changements effectués par la transaction deviennent permanents.
Les éventuels points de reprise intermédiaires posés depuis le début de la transaction sont oubliés et
tous les verrous posés par la transaction sont relâchés.
15.3.3 rollback
Forme rollback work, qui utilise le segment de rollback, annule le travail fait depuis le début de la
transaction, relâche tous les verrous et oublie tous les points de reprise. La transaction est terminée.
commit ;
Un rollback to savepoint ne termine pas la transaction.
Q. 239 Réécrire la procédure Virer pour qu’elle reprenne le traitement en cas d’interblocage.
Ici l’isolation read committed est adaptée :
Q. 240 Montrer que l’isolation serializable pourrait provoquer inutilement des erreurs de sérialisabilité.
Règle : les transactions qui modifient la base ne doivent pas être trop longues, par exemple, au lieu
de faire une seule transaction qui effectue N virements, il vaut probablement mieux en faire N qui
effectuent chacune un virement.
Par exemple, un trigger d’audit doit inscrire des informations dans une table de façon persistante,
même si l’instruction DML qui a déclenché le trigger échoue :
create table Memoire (auteur Varchar2 (20), message Varchar2 (50)) ;
Malgré l’erreur pendant le second update, toutes les inscriptions faites par le trigger sont là !
Le pragma peut aussi être mis dans la procédure, mais cela la spécialise et n’est probablement pas
une bonne idée.
Q. 242 Que se passe-t-il si une transaction autonome se bloque sur une des lignes verrouillées par sa
transaction mère ?
Chapitre 16
La synchronisation est une technique permettant à une transaction de bloquer d’autres transactions
tant qu’elle n’a pas terminé son travail par un commit ou annulé sont travail par un rollback.
Les niveaux d’isolation ainsi que le verrouillage automatique des lignes modifiées par une transaction
ne suffisent pas toujours à garantir la cohérence de la base de données.
Il est parfois nécessaire de synchroniser explicitement les accès des transactions aux données. Pour
cela la technique classique est celle d’un verrouillage explicite qui en Oracle ainsi qu’en PostgreSQL
peut se faire à deux niveaux :
– verrouillage explicite de lignes d’une table avec la commande select ... for update
– verrouillage explicite de table avec la commande lock table ...
178
16.2. COMMENT ASSURER UNE SYNCHRONISATION 179
exception
when No_Data_Found then
rollback ; raise_application_error(-20111,’Membre inexistant’);
when others then
rollback ; raise ;
end Augmenter ;
On peut alors montrer que la propriété P peut être Equipe Membre
cassée lors de l’exécution de la procédure Augmenter equipe id salaire
budget_salarial id
par deux transactions concurrentes. État initial des 51 1 900
2000 51
tables : 51 2 1000
Equipe Membre
equipe id salaire
nouvel état des tables, P est cassée ! budget_salarial id
51 1 1000
2000 51
51 2 1050
Q. 243 Cela se passerait-il mieux si le niveau d’isolation était serializable ?
On peut imaginer d’autres ordonnancements qui casseraient P .
Q. 244 Donner le nombre d’ordonnancements pouvant casser P .
Une solution consiste à bloquer une des deux transactions jusqu’au commit de l’autre de façon à ce
qu’elle soit obligée de voir la modification faite par l’autre transaction. Ici l’isolation read committed
est la seule appropriée car elle permettra à la transaction bloquée de voir, lorsqu’elle sera débloquée,
les modifications validées par l’autre.
Après une présentation des outils de verrouillage les sections 16.6 et 16.7.1 proposent de les utiliser
pour résoudre le problème de synchronisation de la procédure Acquerir. Ces outils devraient se trouver
dans la plupart des SGBD, en tous les cas dans Oracle et PostgreSQL.
Chaque instruction DML est atomique (en tout ou rien) mais ce n’est pas une transaction (pas de
commit).
16.4.1 Déblocage
Un déblocage correspond forcément au fait que la transaction bloquante vient d’être validée ou annulée
(protocole V2PR).
Lors d’un déblocage en isolation read committed, les instructions insert, update, delete et select
... for update réévaluent complètement la sélection des nuplets.
En revanche, en isolation serializable, une erreur de sérialisabilité est déclenchée (ORA-08177), sauf
si la transaction bloquante est annulée. Attention : cette erreur a lieu aussi si la transaction bloquante
a simplement effectué un select for update, même si elle n’a pas modifiée les lignes ainsi verrouillées.
Exemple d’erreur -08177 (sériabilité non garantie) causée par un select for update :
Tbloquante Tbloquée
set transaction isolation
level read committed
set transaction isolation
level serializable
select * from Client
where id = 4
for update
update Client
set solde = solde - 12
where id = 4
bloquée
commit
ORA-08177 :
Impossible de sérialiser
16.5. SQUELETTE DE PROCÉDURE PL/SQL RÉALISANT UNE TRANSACTION 181
Un select ... for update doit être simple : pas de group by de fonction d’agrégation, . . .(section 16.3)
Q. 246 Pour le même début d’ordonnancement, que se passerait-il si le niveau d’isolation était
serializable ?
Il y a deux sortes de verrouillage de table : d’une part le verrouillage effectif dont les modes sont
share et exclusive, d’autre part le verrouillage intentionnel dont les modes sont row share, row
exclusive ; share row exclusive est à la fois effectif et intentionnel.
L’intérêt du verrouillage intentionnel est le suivant : supposons qu’une transaction T1 non terminée
ait modifié des nuplets d’une table, chacun de ces nuplets est donc verrouillé en mode exclusif par
cette transaction. Une autre transaction T2 souhaite verrouiller en mode exclusif cette même table afin
de s’assurer d’être la seule à modifier cette table. T2 doit évidemment être bloquée à cause des ver-
rouillages de nupplet effectués par T1 . Le problème est que pour se rendre compte que T1 a verrouillé
des nuplets il faut explorer les nuplets de la table ce qui risque d’être très coûteux. C’est ce problème
de coût que résout le verrouillage intentionnel de table : avant même de commencer à modifier les
16.7. VERROUILLAGE DE TABLE EN ORACLE 183
Le verrouillage intentionnel permet simplement d’améliorer les performances d’exécution
des transactions, fonctionnellement il n’apporte rien et, en théorie, on pourrait parfaite-
ment s’en passer.
Certains verrouillages comme share peuvent être posés simultanément pas plusieurs transactions.
D’autres comme exclusive ne peuvent être posés que par une transaction à la fois.
Certains verrouillages comme row share et row exclusive ne bloquent aucune opération de mise à
jour, ils bloquent seulement la pose d’autres verrous de table.
Les opérations de mise à jour posent automatiquement, en plus des verrous nuplet, un verrou row
exclusive sur la table à modifier (même pour un update qui ne modifiera rien !).
Tous les verrouillages (table et nuplets) posés par une transaction sont relâchés lors du prochain com-
mit ou rollback : Oracle respecte le verrouillage deux phases rigoureux.
Voici les modes de verrouillage dans l’ordre croissant d’exigence :
row share (RS) : bloque uniquement la pose de verrou exclusive pour les autres transactions,
il est posé automatiquement par un select ... for update.
row exclusive (RX) : ce verrouillage est posé automatiquement par un ordre DML de modi-
fication de la table (update, delete, insert) même si aucun nuplet de la table n’est modifié. Il
permet de bloquer la pose de verrou exclusive, share row exclusive et share qui ne peuvent
pas être posés tant qu’une transaction modificatrice n’est pas validée.
share (S) la table est en lecture seule : la transaction qui a posé ce verrou ne peut pas tenter
de modifier la table ou de faire un select...for update. Plusieurs transactions peuvent bien
sûr positionner ce verrou. Les transactions tentant de poser des verrous exclusive, SRX ou RX
(insert, update et delete) sur la table sont bloquées.
share row exclusive (SRX) la somme de share et row exclusive. La transaction diposant
de ce verrou et la seule à pouvoir modifier la table. D’autres transactions peuvent lire la table
et ne sont pas bloquées. D’autres transactions peuvent aussi exécuter un select for update.
exclusive (X) la transaction possédant ce verrou peut tout faire sur la table, les autres tran-
sactions ne peuvent que la lire (mises à jour et verrouillages bloqués).
Table d’incompatibilité (un - indique que les deux verrous sont exclusifs, un + indique que les deux
verrous peuvent être posés en même temps). Remarquez que cette matrice est symétrique.
Transaction demandeuse
X SRX S RX RS mot clef verrou intentionnel
Transaction X - - - - - exclusive non
disposant SRX - - - - + share row exclusive
du verrou S - - + - + share non
RX - - - + + row exclusive oui update, delete, insert
RS - + + + + row share oui select ... for update
184 CHAPITRE 16. SYNCHRONISATION DES TRANSACTIONS
Inversement : si une table est verrouillée en mode share alors un update, un insert ou un delete
seront bloqués jusqu’au déverrouillage.
16.8 PL/SQL
Un bloc anonyme est exécuté en tout ou rien, par exemple à l’issue du code suivant, la table TT est
vide :
create table TT (id Number (5) primary key) ;
begin
insert into TT values (1) ;
insert into TT values (2) ;
16.8. PL/SQL 185
mais la transaction commencée par un bloc anonyme qui échoue n’est pas terminée !
Utiliser for update (pour les programmes interactifs, par exemple)
declare
Employe_Bloque exception ;
pragma Exception_Init (Employe_Bloque, -54) ;
begin
select salaire into le_salaire
from Employe
where id = l_id and emploi = ’vendeur’ and 1000 > salaire
for update nowait;
exception
when Employe_Bloque then
-- faire autre chose ?
when No_Data_Found then
rollback ;
raise_application_error (-20111, ’Cet employé gagne >= 1000.’) ;
end ;
Lors du open, détermine les nuplets sélectionnés et les verrouille. nowait est optionnel :
– si absent : transaction bloquée jusqu’à ce que tous les nuplets puissent être verrouillés
– si présent : si des nuplets sont déjà verrouillés par ailleurs, le contrôle est rendu au programme (via
l’erreur Oracle -54) qui peut faire autre chose avant de recommencer.
Les nuplets seront déverrouillés lors du prochain commit ou rollback . Un curseur for update ne
peut donc plus être utilisé après un commit.
Si le curseur utilise une jointure, il faut utiliser la forme for update of <colonne> pour ne verrouiller
que les nuplets de la (des ?) table(s) possédant la (les ?) colonne(s)
Sixième partie
Développement client/serveur
186
Chapitre 17
Mais pour peu que le volume de données soit important, et surtout que les données entretiennent entre
elles des relations complexes, on a alors tout intérêt à utiliser un SGBD pour faire persister ces données.
De plus, on en tire plusieurs avantages liés aux fonctionnalités classiques des SGBD :
– facilité de description des contraintes sur les données
– facilité d’interrogation et de manipulation complexe des données (DML),
– facilité de partage cohérent des données entre plusieurs activités concurrentes (transaction)
– facilité de restauration des données lors de pannes logicielles ou matérielle
– facilité de gestion des droits d’accès aux données (DCL)
– ...
Pour conclure : pour la plupart des logiciels, il ne serait pas très pertinent de se refuser à utiliser un
SGBD.
187
188 CHAPITRE 17. DÉVELOPPER UNE APPLICATION BD
le SQL embarqué dans des langages classiques comme Cobol, C, Ada, Java, . . ., l’outil majeur est
alors un précompilateur.
AGL des environnements de développement d’applications graphiques, d’édition de rapports
Les problèmes sont :
1. de faire coexister l’aspect déclaratif de SQL et procédural des langages d’accueil (PL/SQL en
donne une bonne idée),
2. l’adéquation entre les types de donnée SQL et ceux du langage d’accueil.
3. la répartition du code entre le poste client et le SGBD.
4. garantir l’indépendance du code client par rapport au SGBD effectivement utilisé (passer par
des standard). Par exemple un même code client utilisant JDBC ou ODBC ne dépend quasiment
pas du SGBD effectivement utilisé que ce soit Oracle, PostgreSQL, MySQL, . . .
connecté
Editeur de liens
ordres
SQL Exécutable
Fig. 17.1 – API concrète : le programme effectue explicitement des appels aux primitives d’accès
au SGBD proposées par une bibliothèque (API). La nouveauté par rapport aux procédures stockées
est la nécessité de se connecter au SGBD pour pouvoir l’utiliser, puis de s’en déconnecter.
Une application utilisant une API concrète est prévue pour un SGBD particulier, il sera très pénible
de la modifier pour la porter sur un autre SGBD.
17.2.1 Principe
Le développeur utilise le langage de son choix et utilise une bibliothèque d’accès au SGBD fournie par
l’éditeur ou un tiers.
Bibliothèque :
ouvrir, fermer une connexion au SGBD
demander l’exécution d’un ordre SQL (statement)
récupérer les résultats d’une requête (resultset)
gérer les transactions (commit, rollback)
17.2.2 Avantages
– l’application a un contrôle très fin de la manipulation de la base
– l’application peut construire dynamiquement les instructions SQL
17.3. LES API ABSTRAITES 189
ou :idDemande est une variable de liaison dont la valeur est fournie par le programme applicatif. On
peut aussi écrire du code PL/SQL.
reposent sur l’utilisation cachée de pilotes (driver) qui sont spécifiques aux SGBD.
Elles
Une application utilisant une API abstraite doit théoriquement pouvoir fonctionner avec n’importe
quel SGBD, pour peu qu’on dispose du pilote approprié.
Ainsi il est relativement facile, sans rien modifier (ou presque) à une application Access prévue initia-
lement pour fonctionner avec la base Access de la faire fonctionner avec une base Postgres : il suffit
d’installer le pilote ODBC de Postgres et de remplacer toutes les liaisons aux tables Access par des
liaisons réseau aux tables Postgres.
Gestionnaire
de Pilote Oracle
1 Pilotes
Pilote Postgres
donne
2 Pilote MySQL
Application Connexion
Interface Pilote DB2
Standard
Fig. 17.2 – Principe général des API abstraites. Ici, l’application a demandé au gestionnaire de
pilotes une connexion utilisant le pilote Postgres. Le gestionnaire de pilotes possède deux visages :
côté application, il offre une interface standard quel que soit le SGBD utilisé ; côté SGBD, il gère les
différents pilotes permettant l’accès à autant de SGBD différents. Le seul moment ou l’application
a conscience du SGBD particulier qu’elle souhaite utiliser est celui de la connexion : elle doit, par
exemple en JDBC, fournir une url permettant d’identifier entre-autres le driver à utiliser pour cette
connexion. Et encore, cette url pourrait n’être connue qu’à l’exécution car fournie par l’utilisateur.
JDBC permet principalement le SQL dynamique (Statement) souple mais coûteux, il autorise aussi
la préparation d’instructions paramétrables (PreparedStatement et CallableStatement) intéressant
dans le cas où elles sont exécutées de nombreuses fois car elles peuvent être précompilées et exécutées
plus efficacement (ceci en fonction des capacités du pilote utilisé).
On verra que le préprocesseur SQLJ lui est complémentaire en permettant le SQL statique (et donc
efficace).
pont JDBC-ODBC, l’application doit être installée sur le client (pas d’applet), cette ar-
Type 1 chitecture considère ODBC comme un pilote. L’intérêt est de pouvoir porter sans aucune
difficulté toute application JDBC (Sun) sur la technologie ODBC (Microsoft).
le pilote utilise des méthodes natives d’une bibliothèque propriétaire écrite dans un autre
Type 2 langage (en C par exemple). Cette bibliothèque doit être installée sur le poste client. On
obtient des applications moins portables, mais plus performantes.
Type 3 pur Java en utilisant une API réseau générique et un middleware ( ?)
Type 4 pur Java en utilisant le protocole réseau du SGBD (application ou applet).
Client
Serveur
Tout Java : application et applet
pilote IV
Application Thin Application
JDBC
Client JDBC Server Driver
Java C à installer
seulement des applications sur le client
pilote II SGBD
Application OCI OCI lib
JDBC
Fig. 17.3 – Architectures possibles Oracle JDBC : les pilotes de type 4 (thin) et 2 (OCI)
' $
java.sql DriverManager OracleDataSource javax.sql.DataSource
interface
Connection Connection Connection
classe concrète
donne un
Statement ResultSet ResultSetMetaData
implémente
PreparedStatement
hérite
SQLException
& %
CallableStatement
SQLException
public String getMessage()
public int getErrorCode()
public String getSQLState()
DriverManager gère les différents pilotes (driver) JDBC connus.
Il faut tout d’abord charger ces pilotes Le driver correspondant à l’URL doit être chargé au
préalable, par exemple avec :
Class.forName("oracle.jdbc.driver.OracleDriver") ;
Class.forName("org.postgresql.Driver") ;
suivantes qui doivent être appelées avant le début d’une transaction : void setAutoCommit (boolean autoCom
par défaut : chaque ordre SQL est commis.
vrai
void setReadOnly (boolean readOnly) faux par défaut
void setTransactionIsolation (int level) level défini dans java.sql.Connection
permet, en début de transaction, de spécifier un des 4 niveaux d’isolation de SQL2 :
TRANSACTION_READ_UNCOMMITTED
TRANSACTION_READ_COMMITTED
TRANSACTION_REPEATABLE_READ
TRANSACTION_SERIALIZABLE
Statement createStatement(int RSType, int RSConcurrency, int RSHoldability) On
peut fixer 3 comportements possibles pour les result set (RS) produits par ce statement :
– RSType : TYPE_FORWARD_ONLY (par défaut) on ne peut qu’avancer dans le result set (next()),
TYPE_SCROLL_INSENSITIVE on peut avancer ou reculer (previous()) dans le result set qui
est insensible aux modifications faites par d’autres, transactions) ou TYPE_SCROLL_SENSITIVE
comme le précédent et le result set peut être sensible aux modifications faites par d’autres.
– RSConcurrency : CONCUR_READ_ONLY (par défaut) en lecture seule, CONCUR_UPDATABLE on
peut mettre à jour la base de données via le result set, ces mises à jour seront effectives avec
insertRow(), deleteRow() et updateRow().
– RSHoldability : CLOSE_CURSORS_AT_COMMIT le result set sera fermé lors du prochain commit()
sur la connexion, HOLD_CURSORS_OVER_COMMIT le result set n’est pas fermé lors d’un commit().
Statement createStatement()
les result set seront TYPE_FORWARD_ONLY et CONCUR_READ_ONLY.
PreparedStatement prepareStatement(String sql) dans la chaı̂ne sql les ’?’ indiquent
paramètres in
les
CallableStatement prepareCall (String sql) Pour appeler une procédure stockée. Il fau-
dra fixer une fois pour toutes les types des paramètres out et in out ou du résultat de la fonction
avec registerOutParameter(), puis avant un appel on positionnera les valeurs des paramètres
in et in out et après l’appel on peut récupérer les valeurs des paramètres out et in out. Dans
la chaı̂ne sql, les ’?’ indiquent les paramètres.
void commit()
void rollback()
void close()
Statement objet utilisé pour exécuter une instruction SQL et récupérer son résultat sous la forme
d’un ResultSet. Un seul ResultSet par Statement peut être ouvert à la fois.
boolean execute (String sql) Pour exécuter n’importe quel ordre SQL ou une procédure
stockée qui peut renvoyer plus d’un résultat. Renvoie vrai si le premier résultat est un result
set et faux s’il s’agit d’un nombre de mises à jour ou qu’il n’y a pas de résultat. Les trois
méthodes suivantes permettent de récupérer ces résultats.
ResultSet getResultSet() suite à un execute () qui vaut vrai.
int getUpdateCount() suite à un execute () qui vaut faux.
boolean getMoreResults() pour obtenir les résultats suivants d’un execute ().
ResultSet executeQuery(String sql)
Pour un select, renvoie un seul result set.
int executeUpdate(String sql)
Pour une instruction insert, update ou delete ou une instruction SQL qui ne renvoie rien
(DDL par exemple). La valeur renvoyée est le nombre de lignes affectées ou zéro pour les
instructions SQL qui ne renvoient rien.
void close()
17.3. LES API ABSTRAITES 193
ResultSet le résultat d’une requête. Un curseur, initialement avant la première ligne. On peut avoir
des ResultSet balayables dans les deux sens, insensibles aux modifications faites par d’autres
transactions, et même modifiables (voir Connection.createStatement())
boolean next() une fois pour
la 1ière ligne, faux s’il n’y a plus de lignes
XXX getXXX (int/String) par exemple : int id = resultset.getInt ("id") Accès
par indice de colonne (à partir de 1) ou par nom de colonne (sans distinction minus-
cules/majuscules). XXX peut être : Boolean, Date, Float, Int, String et même Object
quand on ne connaı̂t pas précisément le type de la colonne désignée.
Pour préserver la portabilité de l’application, on a intérêt à lire les colonnes dans l’ordre
croissant des indices de colonne.
boolean wasNull() à faire juste après le getXXX() quand la dernière colonne lue par
getXXX() est indéfinie (is null).
ResultSetMetaData getMetaData() Pour connaı̂tre le schéma des lignes.
void close()
Si le result set est TYPE_SCROLL_[IN]SENSITIVE il dispose aussi des méthodes de repositionne-
ment suivantes du curseur :
boolean previous()
boolean first()
boolean last()
boolean absolute(int row)
boolean relative(int rows)
Il est aussi possible de modifier la base via un ResultSet à condition que celui-ci ait été obtenu
par un Statement créé avec le type CONCUR_UPDATABLE comme dans l’exemple suivant :
java.sql.Statement stmt =
con.createStatement(ResultSet.TYPE_FORWARD_ONLY,
ResultSet.CONCUR_UPDATABLE) ;
! ! ! Attention ! ! !, pour Oracle, la requête qui fabrique le result set doit
nommer chaque colonne du résultat, ce ne doit pas être un select * ...,
sinon on ne pourra pas modifier la base via le result set obtenu !
Les modifications possibles sont : l’insertion d’une ligne en utilisant la ligne d’insertion du
ResultSet, la modification d’une ligne et la suppression d’une ligne, voir la figure 17.5.
void deleteRow(), void updateRow()
le curseur doit se trouver sur la ligne courante du result set qui doit être celle qu’on veut
modifier. La mise à jour est rendue effective dans la base et dans le result set,
void updateXXX (int/String, XXX)
mise à jour de la colonne de la ligne courante ou de la ligne d’insertion sans modifier la
base de données. XXX peut être : Boolean, Date, Float, Int, String . . .et même Object Il
y a aussi void updateNull (int/String) qui rend indéfinie la colonne mentionnée.
void moveToInsertRow()
déplace le curseur sur l’insert row, ligne spéciale permettant de construire les nouvelles
lignes à insérer,
void insertRow()
le curseur doit se trouver sur l’insert row qui est insérée dans la base et dans le result set,
void moveToCurrentRow()
retour à la ligne courante.
En cas d’erreur, la sémantique de void deleteRow(), void updateRow() et void insertRow()
n’est pas claire.
194 CHAPITRE 17. DÉVELOPPER UNE APPLICATION BD
ResultSet.CONCUR_UPDATABLE
updateXXX()
moveToInsertRow() moveToCurrentRow()
updateXXX()
Fig. 17.5 – Un updatable result set avec sa ligne d’insertion. Les méthodes soulignées mettent à jour
la base de données.
ResultSetMetaData objet décrivant la constitution d’une ligne du result set qui l’a produit.
int getColumnCount()
String getColumnClassName(int column)
column à partir de 1.
int getColumnDisplaySize(int column)
String getColumnName(int column)
L’instruction peut comporter des paramètres indiqués par le caractère ?.
void setXXX (int parameterIndex, XXX x) fixe la valeur d’un paramètre avant l’exécution.
Les paramètres sont indexés à partir de 1.
ResultSet executeQuery() pour select, int executeUpdate() pour un insert, up-
date ou delete.
Avant chaque exécution on fixe les valeurs des paramètres in et in out avec les méthodes setXxx
(int index, Xxx valeur) ou setXxx (String nomParametreFormel, Xxx valeur) qui uti-
lise le nom du paramètre formel du sous-programme, mais il faut que le pilote soit capable de le
faire.
Après une exécution on retrouve la valeur de la fonction ou celles des paramètres out et in out
avec les méthodes getXxx (index/String).
SQLData une interface pour faire la correspondance entre les objets SQL définis par l’utilisateur
(UDT) et leurs équivalents en Java.
Un objet SQLData et le nom du type SQL correspondant doivent être fournis à la table de
correspondance de la Connection concernée. Il faudra utiliser la méthode ResultSet.getObject
et dans l’autre sens : PreparedStatement.setObject.
String getSQLTypeName() throws SQLException
void readSQL(SQLInput stream, String typeName) throws SQLException
Il faut lire les attributs dans leur ordre de définition dans le type objet,
writeSQL(SQLOutput stream) throws SQLException
Il faut écrire les attributs dans leur ordre de définition dans le type objet.
create table Employe (id Number (5) primary key, nom Varchar2(20), salaire Number(10));
create or replace procedure Augmenter
(id in Employe.id%type, pourcentage in Natural, nouveau_sal out Employe.salaire%type) is
begin
set transaction isolation level read committed ;
update Employe
set salaire = (salaire * (100 + pourcentage))/100
where id = Augmenter.id
RETURNING salaire INTO nouveau_sal ; -- range le nouveau salaire dans ce paramètre
if SQL%rowcount = 0 then
rollback; raise_application_error(-20111, ’Employé inexistant : ’||to_char(id));
end if ;
commit ;
end Augmenter ;
Un code client utilisant ces services et rendant persistantes les modifications faites aux objets Employe :
Remarquer que les variables statiques charger et augmenter n’ont besoin d’être initialisées qu’une
seule fois. Un petit exemple d’utilisation :
Employe.initialiser (connexion) ;
{ Employe e1 = new Employe (1) ; Employe e2 = new Employe (2) ;
e1.augmenter (10) ; e2.augmenter (20) ;
}
Employe.fermer () ; connexion.close () ;
} finally { resultat.close() ; }
}
private static void livreEmprunteur (Statement stmt) throws SQLException {
requete (stmt,"select titre, nom as emprunteur" +
" from Livre inner join Personne on emprunteur=p_ref") ;
}
private static void nbLivres (java.sql.Statement stmt) throws SQLException {
requete (stmt, "select count (*) from Livre") ;
}
public static void main(String args[]) throws SQLException {
Class.forName("oracle.jdbc.driver.OracleDriver") ;
final Connection connect = DriverManager.getConnection
("jdbc:oracle:thin:@//<machine>.<domaine>:<port>/filora10", "toto", "psswrd") ;
try {
connect.setAutoCommit (false) ;
Statement stmt = connect.createStatement() ;
try { stmt.execute ("set transaction read only") ;
livreEmprunteur (stmt) ;
nbLivres (stmt) ;
} finally { stmt.close() ; }
} finally { connect.close () ; }
}
}
Remarquez que la méthode requete() est applicable à toute requête, même si son affichage n’est pas
très sophistiqué.
C’est ce que permet le DML returning lors d’une instruction DML insert ou update. Il est disponible
avec les pilotes Oracle sauf le pilote interne côté serveur mais ces fonctionnalités ne sont pas standard.
17.4.1 Avantages
– Le code est plus concis et de plus haut niveau qu’avec une API, en particulier au niveau de la liaison
des variables du langage avec les instructions SQL (utilisation du : ). (En JDBC, il faut utiliser
péniblement les méthodes getXXX() et updateXXX())
– Vérification de la syntaxe et des types SQL dès la compilation
– un source SQLJ peut parfaitement utiliser directement l’API JDBC
Précompilateur
Compilateur standard
Editeur de liens
Exécutable
Fig. 17.6 – SQL embarqué et API concrète : au plus haut niveau on utilise un sur-langage
du langage hôte qui autorise l’écriture d’ordres SQL, au niveau intermédiaire le programme effectue
explicitement des appels aux primitives d’accès au SGBD proposées par une API concrète ou abstraite.
Par exemple le préprocesseur SQL C de Postgres utilise l’API concrète libecpg, et le préprocesseur
SQLJ utilise l’API abstraite JDBC.
Ce sont des variables du langage qui seront utilisées pour transférer de l’information vers/depuis
la base de données.
VARCHAR nom [20] sera remplacé (Oracle et Postgres) par le précompilateur par :
struct {
unsigned short len ;
unsigned char arr [20] ;
} nom ;
Attention, le tableau arr ne se termine pas forcément par un ’\0’ si le tableau est plein.
Déclarations d’intention , exemple si le langage hôte est le C :
EXEC SQL WHENEVER SQLERROR DO erreur_sgbd () ;
Cet ordre n’a aucun effet immédiat, il a par contre un effet sur la manière dont les erreurs pro-
voquées par les ordres SQL ultérieurs dans le source du programme seront prises en compte (en
17.4. SQL EMBARQUÉ (INTÉGRÉ) 199
Bien entendu, c’est la dernière déclaration d’intention rencontrée qui est effective.
Les instructions à proprement parler dont voici quelques exemples :
EXEC SQL CONNECT TO annuaire@saison.lifl.fr:5432
AS Ma_Connexion USER :user IDENTIFIED BY :pw_user ;
EXEC SQL CREATE TABLE Livre (ref int primary key, titre char (50)) ;
On voit que la mention des variables de liaison doit être précédée de : .
Le AT Ma_Connexion , qui est optionnel, permet au programme de travailler simultanément
avec plusieurs connexions.
/*
* Ecrit la cha^ıne "s" en s’arr^
etant après "lg" caractères ou
* dès la rencontre du fameux ’\0’.
*/
void put (const int lg, const char * const s) {
int i = 0 ;
for ( ; i < lg && s [i] != ’\0’ ; i++) printf ("%c", s [i]) ;
}
void erreur_postgres () {
fprintf (stderr, "Erreur: %s\n", sqlca.sqlerrm.sqlerrmc) ;
/* Pour éviter de boucler en cas d’erreur de déconnexion */
EXEC SQL WHENEVER SQLERROR continue ; /* continuer l’exécution */
EXEC SQL DISCONNECT Ma_Connexion ; /* se déconnecter */
exit (1) ; /* quitter */
}
int main (const int argc, const char * const argv []) {
3. exécution
./biblio <utilisateur> <mot-de-passe>
db_string chaı̂ne de caractère en syntaxe Net8 (réseau, machine et base de connées) pour se connecter
à une base de donnée distante. Les instructions SQL utilisant la clause AT BD_DU_FIL seront alors
dirigées vers cette base de données.
SQLCA : SQL Communication Area, pour récupérer les codes et messages d’erreurs qui proviennent
du serveur.
EXEC SQL WHENEVER <condition> est une déclaration d’intention, elle définit comment les ordres SQL
qui la suivent prendront en charge les erreurs correspondant à la condition indiquée, et ce, jusqu’au
prochain EXEC SQL WHENEVER portant sur la même condition.
oracle.sqlj.runtime.Oracle.connect
("jdbc:oracle:thin:@localhost:1521:orcl", "dupond", "passe-tigre") ;
– la méthode avec la classe Mon_Application_SQLJ qui contient la méthode statique main() qui sera
le programme principal à exécuter, et le fichier connexion_a_la_BDD.infos qui contient l’URL, le
nom et le mot de passe de l’utilisateur :
oracle.sqlj.runtime.Oracle.connect
(Mon_Application_SQLJ.class, "connexion_a_la_BDD.infos") ;
oracle.sqlj.runtime.Oracle.close () ;
Bien entendu, en travaillant avec plusieurs connexions sur la même base de données, depuis la connexion
C2 on ne verra pas les modifications faites par C1 tant que C1 ne les aura pas validées (commit).
En SQLJ, le auto-commit est à faux par défaut (contrairement à JDBC).
Chaque instruction s’exécutant dans un contexte d’exécution écrase les informations d’état des ins-
tructions précédentes.
En cas d’une application multi-tâches, chaque tâche (thread) doit utiliser un contexte d’exécution
différent.
204 CHAPITRE 17. DÉVELOPPER UNE APPLICATION BD
#sql {
select nom into :nom
from Employe
where id = :ID } ;
Si la colonne nom est indéfinie, alors la variable de liaison nom reçoit la valeur null (on a une exception
si la variable de liaison est d’un type primitif).
Comme on le voit, le nom d’une variable du programme figurant dans une instruction #sql {}
doit être préfixé par le caractère :. En fait le : peut préfixer une expression Java, par exemple :
where nom = :(nom.toUpper ()) ou encore, en précisant par in que le mode de passage de l’expres-
sion est en entrée :where nom = :in(nom.toUpper ())
On peut bien sûr utiliser aussi les autres instructions DML, par exemple :
17.4. SQL EMBARQUÉ (INTÉGRÉ) 205
#sql [ctx_execution] {
update Employe
set salaire = salaire + :augmentation
where id = :id } ;
if (ctx_execution.getUpdateCount () == 0) {
#sql { rollback } ;
throw new Error ("Pas d’employé d’id = " + id) ;
} else {
#sql { commit } ;
}
}
Remarquer qu’ici tous les ordres SQL embarqués sont exécutés dans le même contexte d’exécution.
Les itérateurs
Définition de la classe itérateur NomNumero avec les noms et types des colonnes :
#sql iterator NomId (String nom, int id) ;
Puisque NomId est une classe, sa déclaration peut se faire de façon autonome en dehors de celle d’une
autre classe. Une classe itérateur déclarée dans une autre classe doit être public static :
class X {
#sql public static iterator NomId (String nom, int id) ;
...
}
Une instance de NomId disposera alors de deux méthodes de type accesseur : String nom () et
int id () :
{
// Déclaration d’une variable itérateur
NomId monIterateur ;
// Exploration de la requ^ete
try {
while (monIterateur.next()) {
int id = monIterateur.id () ;
String nom = monIterateur.nom () ;
if (nom == null) {// c’est que : << Employe.nom is null >>
...
}
} finally {
// Fermeture * garantie * de l’itérateur
monIterateur.close() ;
206 CHAPITRE 17. DÉVELOPPER UNE APPLICATION BD
}
}
Le nom et le type d’une des colonnes d’un itérateur doit correspondre à la colonne de même nom (à la
casse près) du select et doit être d’un type compatible. En revanche l’ordre des colonnes de l’itérateur
peut être différent de celui des colonnes homonymes dans le select.
Types compatibles
Peut permettre de faire un maximum de traitements sur le serveur et limiter les communications
réseau.
#sql {
declare
...
begin
...
end } ;
17.4. SQL EMBARQUÉ (INTÉGRÉ) 207
Ici on intercepte l’exception puis on la redéclenche car on ne résout pas la condition à laquelle elle
correspond :
try {
#sql {
select bureau into :bureau
from Employe
where id = :id } ;
System.out.println ("Bureau de " + id + " : " + bureau) ;
} catch (java.sql.SQLException excp) {
switch (excp.getErrorCode ()) {
case 2000:
System.err.println ("Erreur sur le select : aucune ligne sélectionnée") ;
break ;
case 21000:
System.err.println ("Erreur sur le select : plus d’une ligne sélectionnée") ;
break ;
default:
System.err.println (excp.getMessage ()) ;
}
throw excp ;
}
Architecture
Pour JDK 1.4 et génération de code spécifique Oracle :
Positionner la variable d’environnement ORACLE_HOME sur le répertoire contenant les outils JDBC
et SQLJ.
Ajouter à la variable d’environnement CLASSPATH le pilote JDBC, le traducteur et le runtime
appropriés :
$ORACLE_HOME/jdbc/lib/classes12.zip !! la version 9.0.1 pas la 10.2.0.1
$ORACLE_HOME/sqlj/lib/translator.jar
$ORACLE_HOME/sqlj/lib/runtime12.jar
Vérifications sémantiques :
online grâce à l’option -props qui indique au précompilateur comment se connecter au SGBD
afin de vérifier l’adéquation de la sémantique du programme SQLJ avec celle du schéma de la
base de données.
offline sinon, les erreurs éventuelles ne seront vues qu’à l’exécution de l’application.
208 CHAPITRE 17. DÉVELOPPER UNE APPLICATION BD
Génération d’un fichier .java et de fichiers sérialisés de profil .ser ou .class. Un profil contient des
informations à propos des instructions SQL embarquées.
Tous ces fichiers (.java et .ser) sont ensuite compilés par Java pour obtenir les .class.
Q. 251 Donner une implantation de la classe produite par le type itérateur LivreEmprunteur. Quelle
est la chose qu’on a du mal à garantir dans cette implantation ?
On peut apprécier la briéveté du code, cependant, SQLJ étant conçu par Oracle il est très lié à ce
SGBD.
Lors du prétraitement, SQLJ peut vérifier que la sémantique du programme correspond bien à celle
de la base de données (existance des tables, des colonnes, typage cohérent des variables Java et des
expressions SQL, . . .). Pour cela il faut indiquer à SQLJ comment il peut se connecter à la base grâce
au fichier Emprunts-sqlj.properties :
# Informations pour que SQLJ puisse faire des
# vérifications sémantiques vis à vis du shéma de
# la base de données dès le prétraitement.
17.4. SQL EMBARQUÉ (INTÉGRÉ) 209
#
# option SQLJ : -props=Emprunts-sqlj.properties
L’erreur sur l’orthographe de la colonne p_ref incorrectement écrite p_reff sera détectée dès le
prétraitement !
sqlj.url=jdbc:oracle:thin:@<machine>.<domaine>.fr:1521:<service>
sqlj.user=biblio
sqlj.password=<mot-de-passe-de-biblio>
Exécuter l’application :
java Emprunts
On remarque que la base utilisée pour le prétraitement et celle utilisée pour l’exécution peuvent ne
pas être les mêmes.
iter.close();
}
try {
EmpruntsCtx ctx = new EmpruntsCtx
(sqlj.runtime.ref.DefaultContext
.getDefaultContext().getConnection()) ;
ctx.close(ctx.KEEP_CONNECTION) ;
} finally {
#sql [ctx] { commit } ;
oracle.sqlj.runtime.Oracle.close() ;
}
}
}
17.4.10 JPublisher
Un outil fourni par Oracle qui exploite la définition des types objet SQL (voir le chapitre sur le
relationnel-objet de Oracle) pour en donner un équivalent en objets Java ou en structures C. Cela
permet ensuite d’écrire en Java des applications clientes qui utilisent ces objets.
Introduction à Hibernate
Cette présentation d’Hibernate essaie d’être une introduction pas trop compliquée pour faire com-
prendre quelques éléments de base de cet environnement. Elle ne prétend certainement pas faire du
lecteur un spécialiste. Elle se limite à une application simple de type client/serveur, alors qu’Hibernate
est fait pour du développement WEB.
Probablement qu’un des objectifs principaux d’Hibernate est de faciliter la persistance, dans une base
de données, des objets manipulés par le programmme.
Hibernate propose beaucoup d’outils qu’il fournit ou qu’il emprunte à d’autres éditeurs (Apache, Sun,
. . .) chacun synthétisant des besoins dans le domaine du génie logiciel.
211
212 CHAPITRE 18. INTRODUCTION À HIBERNATE
Un objet Java ne peut être persistant, c’est à dire avoir sa place dans une table de la base de
données gérée de façon plus ou moins transparente par Hibernate, que s’il est instance d’une classe
Java mappée. La map d’une classe permet, entre autre, de savoir comment la table correspondante
devra être implantée dans le SGBD d’accueil.
Techniquement, une classe peut être mappée de deux manières :
– en créant un fichier XML contenant la map de la classe, ainsi la map de personnel/Responsable.java
sera dans le fichier personnel/Responsable.hbm.xml, (voir section 18.9.2 page 219)
– en ajoutant des annotations directement dans le source de la classe Java. Les annotations sont une
nouveauté de Java5.
Hibernate donne un aspect partiellement déclaratif à la persistance des informations de la base de
données qui sont gérées en mémoire centrale (il reste cependant un peu de travail explicite à faire pour
garantir la persistance).
Hibernate s’adapte de façon transparente à environ 16 SGBD pour ce qui est des spécificités syn-
taxiques et sémantiques : les ordres de création du schéma, les requêtes et les ordres DML (insert (y
compris avec l’utilisation d’une sequence Oracle pour produire la valeur de la clef primaire), update
et delete) sont fabriqués par Hibernate en fonction des modifications faites par le programme en
mémoire centrale et du dialecte du SGBD sous-jacent.
Sans avoir à modifier l’application, on peut obtenir des services différents simplement en modifiant
des fichiers de propriétés et configuration. Par exemple :
– gérer la richesse des messages de trace imprimés dans la console de lancement (log4j)
– demander la suppression de la base de données puis sa création lorsque l’application démarre, option
create de l’application hbm2ddl.
– obtenir le source des ordres DDL créant la base de données
– obtenir l’impression des ordres SQL produits et exécutés par Hibernate via JDBC
– pouvoir changer de SGBD en modifiant simplement le fichier de propriétés Hibernate et éventuellement
certains fichiers mappant des classes (fichier <nom-classe>.hbm.xml).
Un objet ne peut être dans l’état persistant que pendant qu’une session est ouverte. Dès que la session
sera fermée, cet objet passera dans l’état détaché.
Les changements d’état d’une instance de classe mappée se font aussi au sein d’une session :
– un objet instancié directement avec new est transient : il n’est associé à aucune session. Pendant une
session il devient persistent suite aux opérations : session.save(obj), session.persist(obj) ou
session.saveOrUpdate(obj).
– un nouvel objet obtenu, pendant une session, depuis la base de données par session.get() ou
session.load() est persistent. Il devient transient avec session.delete(obj). Il devient detached
lors de la fermeture de la session (session.close())
– Un ancien objet persistent pendant une session précédente, est initialement detached lors d’une
nouvelle session. Il devient persistent avec session.update(obj), session.saveOrUpdate(obj),
session.lock(obj) ou comme nouvelle instance persitante avec session.merge(obj).
Fig. 18.1 – Cette figure résume une partie des transitions d’états possibles pendant une session. Les
deux seuls états initiaux possibles (transient et persistant) sont indiqués par ⇑. Remarquez qu’il n’y
a que des méthodes de Session, la session devant être ouverte.
session.persist(o)
session.saveOrUpdate(o) session.saveOrUpdate(o)
session.save(o) session.update(o)
Les objets persistants modifiés sont détectés lors d’un flush() de la Session et des ordres SQL
(insert, update ou delete) sont alors exécutés pour garantir leur persistance.
18.5 Classe mappée et fichier XML (POJO : Plain Old Java Ob-
jects)
Une classe X est dite mappée si un fichier X.hbm.xml où X est le nom de la classe lui est associé. Ce
fichier décrit, en XML, l’aspect relationnel des objets de cette classe ainsi que des associations qu’ils
entretiennent avec d’autres classes mappées (clef primaire, clef étrangère, . . .).
Le code Java d’une classe mappée X ressemble à un BEAN, c’est à dire que la classe doit disposer
de méthodes getXxx() et setXxx() où xxx est une variable d’instance. Par ailleurs, X doit proposer
un constructeur sans paramètres qui doit être visible dans le paquetage (ni public ni protected ni
private).
Clef primaire composée de plus d’une colonne : le programmeur décrit cette clef par une nouvelle
classe. Recommandation : utiliser un type référence pour le type de la clef car alors on dispose du null
de Java pour représenter l’absence de valeur.
Définir equals() et hashCode() pour ces classes peut-être utile dans certains cas.
214 CHAPITRE 18. INTRODUCTION À HIBERNATE
Fig. 18.2 – Une (*) signifie qu’on peut avoir plusieurs instances simultanées, le (1) de Transaction
signifie qu’on a à un moment donnée au plus une transaction produite par une même Session.
par autant de classes qu’il y a de SGBD auquels Hibernate est capable de s’adresser.
Les classes d’implantation seront choisies lorsque les fichiers de propriétés et de configuration auront
été chargés par le programme.
Classe org.hibernate.HibernateException
C’est une java.lang.RuntimeException : pas besoin de la documenter avec une clause throws. Qua-
siment toutes les méthodes Hibernate sont susceptibles de déclencher cette exception.
Classe org.hibernate.cfg.Configuration
Le constructeur de Configuration utilise le fichier hibernate.properties ou plutôt hibernate.cfg.xml.
– Configuration configure ()
lit les mapping et les propriétés dans hibernate.cfg.xml
– SessionFactory buildSessionFactory ()
crée une SessionFactory correspondant à la configuration.
– void clear ()
Pour vider le cache (gestion des ressources).
– org.hibernate.Session close ()
fin de la session
Par défaut un flush est effectué aux instants suivants :
– avant l’évaluation d’une requête,
– lors du commit() de la Transaction
– lors d’un appel explicite à flush() (ouf !)
Q. 252 Pourquoi la documentation dit-elle qu’un flush doit être exécuté avant l’évaluation d’une
requête ?
Voici deux possibilités pour éviter la saturation du cache de second niveau :
– Désactiver le cache de second niveau :
hibernate.cache.use_second_level_cache false
hibernate.jdbc.batch_size 20
taille du paquet JDBC.
– ou bien en appelant successivement, éventuellement plusieurs fois dans une même transaction lo-
gique, les deux méthodes suivantes :
Interface org.hibernate.Query
Représentation objet d’une requête Hibernate. L’ordre peut comporter des paramètres nommés, par
exemple :nom Un même paramètre peut apparaı̂tre plusieurs fois dans la requête. On peut aussi utiliser
le ? pour un paramètre comme en JDBC, attention ils sont numérotés à partir de 0 contrairement à
JDBC. On ne peut pas mélanger les deux notations de paramètre. La durée de vie d’une requête est
limitée à celle de la Session qui l’a créé.
– executeUpdate()
exécute l’instruction update ou delete
– List list()
renvoie le résultat d’une requête comme une liste, si plusieurs entités par ligne l’élément de liste est
un Object[]
– Object uniqueResult() pour récupérer l’unique résultat d’une requête ; null si aucun résultat et
exception NonUniqueResultException si plus d’un résultat.
– Query setInteger(int position, int val)
– Query setInteger(String name, int val)
– Query setString(int position, String val)
– Query setString(String name, String val)
218 CHAPITRE 18. INTRODUCTION À HIBERNATE
Interface org.hibernate.SQLQuery
Interface org.hibernate.Transaction
Il s’agit ici de transactions dites logiques qui ne correspondent pas directement aux transactions du
SGBD sous-jacent. Une transaction logique correspond à un dialogue avec l’utilisateur qui peut être
très long par rapport à la durée souhaitable d’une transaction SGBD. C’est ce qui explique (entre
autres) qu’en général une transaction logique recouvre plusieurs transactions du SGBD.
Le mapping d’une classe dit comment une instance de la classe doit être stockée dans une table rela-
tionnelle du SGBD.
Le mapping d’une classe s’effectue avec un fichier xml séparé du fichier Java décrivant la classe. Avec
la couche annotation introduite par Java5, ce mapping peut aussi être spécifié directement dans le
source Java grâce à des annotations, mais nous ne verrons pas cette possibilité.
Un objet ayant une clef étrangère sur un autre, est déclaré en Java comme désignant cet objet alors
que dans le mapping on spécifiera cet attribut comme une clef étrangère.
Dans l’exemple suivant un rayon de magasin à au plus un responsable et un responsable peut l’être
de plusieurs rayons.
Le rayon Java n’a pas à stocker la valeur de clef étrangère de son responsable : un rayon connaı̂t
directement son responsable !, mais attention, par défaut le mode de chargement des entités
associées est paresseux et le responsable ne sera par chargé, voir 18.9.3 page 221 :
package magasin ;
public class Rayon {
private Long id ; private String nom ; private personnel.Responsable responsable ;
</class>
</hibernate-mapping>
<id> permet de décrire la clef primaire de cette entité. Le <generator> de <id> permet de dire que
les clefs primaires seront générées automatiquement par le SGBD lors du save() d’une nouvelle
instance. Ici le générateur de clef est une séquence qui convient à Oracle et Postgres dans la mesure
220 CHAPITRE 18. INTRODUCTION À HIBERNATE
où la clef primaire est un entier. Cet objet séquence s’appelle Seq_Responsable dans le schéma de
la base de données.
<property> introduit une colonne normale pour le nom du responsable avec une longueur maximale
de 20 caractères.
Dans chaque paragraphe décrivant un attribut, le paramètre name donne le nom de l’attribut dans
la classe Java et column sera le nom de la colonne correspondante dans la table.
Enfin on voit que l’attribut nbRayons de la classe Responsable n’est pas persistant : on ne
lui fait correspondre aucune colonne de table dans le mapping.
Question : comment faire pour que l’attribut nbRayons ait toujours une valeur cohérente avec le
nombre de rayons effectivement sous sa responsabilité ? Il faudrait qu’Hibernate, pour une ligne
d’une table ne fabrique pas plus d’une instance (cela est possible au sein d’une session) et sache la
retrouver quand c’est nécessaire.
<id> permet de décrire la clef primaire de cette entité. Le <generator> de <id> permet de dire que les
clefs primaires seront générées automatiquement par le SGBD lors du save() d’une nouvelle instance.
Ici le générateur de clef est une séquence qui convient à Oracle et Postgres dans la mesure où la clef
primaire est un entier. Cet objet séquence s’appelle Seq_Rayon dans le schéma de la base de données.
<property> introduit une colonne normale pour le nom du rayon avec une longueur maximale de 50
caractères.
Dans chaque paragraphe décrivant un attribut, le paramètre name donne le nom de l’attribut dans la
classe Java et column sera le nom de la colonne correspondante dans la table.
<many-to-one> peut être interprété comme suit : plusieurs rayons peuvent être dirigés par un même
responsable, autement dit plusieurs rayons peuvent référencer le même responsable. Cela correspond
18.10. LES DEUX FICHIERS PRINCIPAUX DE CONFIGURATION 221
Le paramètre cascade peut avoir la simple valeur "all" pour dire que toutes les opérations doivent
être cascadées sur l’objet associé ou "none", qui est la valeur par défaut, pour dire qu’aucune ne doit
l’être. La notion de cascade n’a en général pas grand sens pour les associations <many-to-one> et
<many-to-many>, mais peut en avoir pour <one-to-one> et <one-to-many>
18.9.3 Chargement paresseux par défaut de l’objet désigné par une clef étrangère
Lors du chargement d’un rayon, le mode de chargement du responsable de ce rayon est paresseux par
défaut (lazy) : il ne se fera que quand ce sera nécessaire. C’est à dire que lors du get() ou load()
d’un rayon, son attribut responsable sera initialisé sur un proxy non initialisé avec les informations du
responsable.
Ce responsable (s’il est défini) sera chargé automatiquement (mais seulement pendant une session)
quand on tentera d’y accéder, par exemple en lui demandant son nom : (rayon.getResponsable().getNom()).
Mais ceci ne pourra se faire que lorsqu’une Session et peut-être aussi une Transaction sont ouvertes.
Si ce n’est pas le cas, on aura une erreur d’exécution.
Cette politique paresseuse par défaut a le mérite de ne faire le travail que quand il est nécessaire et
donc d’améliorer les performances du programme.
Une autre approche, qui peut s’avérer plus coûteuse, consiste à dire que lors du chargement d’un rayon
on veut que son responsable soit lui aussi systématiquement chargé. Pour cela on peut mettre à faux
la politique paresseuse dans le paragraphe <many-to-one .../> de magasin/Rayon.hbm.xml avec le
paramètre lazy="false" :
<hibernate-mapping>
<class name="magasin.Rayon" table="Rayon">
...
<many-to-one ... lazy="false" .../>
...
</class>
</hibernate-mapping>
## dialecte à utiliser
hibernate.dialect org.hibernate.dialect.Oracle9Dialect
La documentation API de org.hibernate.cfg.Environment fournit des informations sur ce fichier.
Voici un petit sous-ensemble des propriétés du fichier src/hibernate.properties :
## SGBD Oracle 10
## dialecte à utiliser
hibernate.dialect org.hibernate.dialect.Oracle9Dialect
hibernate.connection.username <un-nom>
hibernate.connection.password <un-mot-de-passe>
– src/hibernate.cfg.xml
est le nouveau fichier de configuration d’Hibernate écrit en XML, on y retrouve les mêmes paramètes
que dans hibernate.properties :
<?xml version=’1.0’ encoding=’utf-8’?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<!-- SQL dialect org.hibernate.dialect.Oracle10gDialect -->
<property name="dialect">org.hibernate.dialect.Oracle10gDialect</property>
18.10. LES DEUX FICHIERS PRINCIPAUX DE CONFIGURATION 223
<property name="connection.driver_class">oracle.jdbc.driver.OracleDriver</property>
<property name="connection.url"> URL d’accès au SGBD </property>
<property name="connection.username">toto</property>
<property name="connection.password"></property>
<!-- C3P0 Connection Pool : nombre minimum de connexions dans le pool -->
<property name="c3p0.min_size">2</property>
<!-- nombre maximum de connexions dans le pool -->
<property name="c3p0.max_size">3</property>
<property name="c3p0.timeout">300</property>
<property name="c3p0.max_statements">50</property>
<property name="c3p0.idle_test_period">3000</property>
<!-- Rel^
achement des connexions
auto (valeur par défaut conseillée) | on_close (déconseillé)
after_transaction (JDBC) | after_statement (JTA) -->
<property name="connection.release_mode">auto</property>
<!-- Liste des fichiers mappant les classes dont les instances peuvent persister -->
<mapping resource="personnel/Responsable.hbm.xml"/>
<mapping resource="magasin/Rayon.hbm.xml"/>
</session-factory>
</hibernate-configuration>
On peut ou non préfixer les noms des attributs (name) avec hibernate. .
Lors de la compilation ces deux fichiers ainsi que les maps de classes doivent être copiés dans le
répertoire classes.
Attention : toute erreur dans ces fichiers de configuration ne sera pas détectée lors de la compilation
mais lors de l’exécution du programme.
C’est lors de tx.commit () que Hibernate se rendra compte que l’objet désigné par leader est devenu
persistant (grâce à l’exécution de la méthode update()) et qu’il a été modifié, il fabriquera et exécutera
donc l’ordre SQL adapté au dialecte du SGBD qui rendra persistante cette modification.
Q. 253 Que se passerait-il si au point (1) de l’exemple ci-dessus l’objet désigné par leader était en
fait dans l’état transient ? voir la figure 18.1 page 213.
Q. 254 Comment peut-on résoudre facilement le problème de la question précédente ?
Une autre solution pour unifier le code :
package persistance ;
public abstract class TraitementPersistant {
/** Méthode forcément exécutée avec une session active passée en paramètre. */
protected abstract void faire (org.hibernate.Session session) throws Exception ;
}
Il suffit d’hériter de cette classe pour implanter la méthode faire() puis demander son exécution avec
executionPersistante().
Q. 255 Écrire la classe static qui supprime un Responsable et la méthode qui l’utilise.
Une idée est que pendant une transaction (et donc sa session) aucun dialogue interactif avec l’utili-
sateur ne doit avoir lieu. Si ce n’est pas le cas, la durée de la transaction risque d’être très longue et
de dégrader les performances transactionnelles, par exemple si un verrou est posé en début de tran-
saction sur une table pendant une heure parce que l’utilisateur a dû discuter avec ses collaborateurs
pour prendre une décision.
L’idée consiste alors à distribuer sur plusieurs transactions un traitement nécessitant un dialogue avec
l’utilisateur, par exemple :
1. une première transaction SGBD charge les informations nécessaires au dialogue puis elle se
termine,
2. le dialogue a lieu en dehors de toute transaction SGBD : l’utilisateur consulte et modifie locale-
ment les données récupérées par la première transaction,
18.14. LES TRANSACTIONS 227
3. une seconde et dernière transaction SGBD à lieu pour rendre persistantes les modifications
demandées par l’utilisateur ou bien elle devrait échouer si le nouvel état de la base de données
n’est plus cohérent avec ce que demande l’utilisateur.
<hibernate-mapping>
<class name="modele.Message" table="Message">
</class>
</hibernate-mapping>
Pour gérer les versions on doit ajouter un attribut/colonne qui s’appelle ici num_version et est un
entier. Lors de la création d’un nouveau message (save()), cette num_version est initialisée à 0 par
Hibernate, puis à chaque mise à jour valide du message num_version est incrémentée. Une mise à
jour n’est valide que si le num_version stocké dans l’objet Java est égal au num_version stocké dans
la base de données, sinon une erreur Hibernate arrêtera cette mise à jour. En effet si le num_version
stocké dans la base de données est différent de celui de l’objet Java c’est que quelqu’un d’autre a
entre-temps modifié cette ligne de la base de données.
– <generated="never"> signifie que la valeur de num_version n’est pas gérée par la base de données
et donc l’est certainement par Hibernate.
– <insert="true"> dit que lorsque le save() est validé, la colonne num_version apparaı̂tra dans
l’ordre insert correspondant (sinon elle n’apparaı̂t pas et alors la base de données doit garantir une
valeur par défaut pour cette colonne num_version).
Quand une application tente de mettre à jour un message, Hibernate charge la version de ce message
depuis la base de données, si cette version est plus récente que celle de l’objet mappé de l’application,
une erreur est déclenchée.
Ceci doit permettre d’empêcher un utilisateur de sauver une modification faite à partir d’un état ob-
solète car il a déjà été modifié par un autre utilisateur, autrement dit de garantir la sérialisabilité de
ces modifications. Le rollback() à faire après une telle erreur devrait garantir que les autres mises à
jour sont elles aussi annulées.
Voici un exemple :
228 CHAPITRE 18. INTRODUCTION À HIBERNATE
18.15 Unicité des objets Java mappés chargés lors d’une même ses-
sion/transaction
Hibernate gère automatiquement l’unicité de représentation en mémoire centrale. C’est à dire que
si pendant une transaction on charge plusieurs fois la même ligne d’une table ou à cause d’un
<lazy="false"> sur une clef étrangère, on obtiendra pour cette ligne un seul objet persistant en
mémoire centrale.
Mais attention cette unicité n’est assurée que pour les chargements multiples de la même ligne qui
ont lieu pendant la même session/transaction.
230
Chapitre 19
Le modèle objet
Par exemple, en supposant qu’une voiture possède exactement un propriétaire, on peut retrouver les
couples voiture/propriétaire par l’équi-jointure :
' $
Dans le modèle objet, une instance d’entité (un objet) peut aussi être identifiée par son contenu et il
est donc possible de faire des recherches associatives, mais un objet est de toute façon identifié par
son identifiant unique, ou OID (Object IDentifier). Cet identifiant permet de localiser plus ou moins
directement l’objet et il est alors possible de remplacer les opérations d’équijointure sur clé étrangère
par des accès direct à l’objet qui sont a priori plus efficaces. Cet accès direct à un objet grâce à son
& %
identifiant s’appelle la navigation.
Par exemple, en supposant qu’une voiture conserve non pas la clef de son propriétaire mais son OID, on
pourra alors utiliser la navigation et ainsi simplifier la requête et éviter une équijointure. La navigation
s’exprime comme en Java ou en Ada par une notation pointée :
select v.numéro, v.proprietaire.nom
from v in Voiture ;
Par contre, une fois chargé, pour que l’accès soit efficace, il faut utiliser le pointeur mémoire. Lors
de différentes exécutions, l’objet sera chargé à des adresses différentes, on ne peut donc pas utiliser
son adresse mémoire comme OID. Voir la figure 19.1.
33 AAA 59 Transaction 1
33 AAA 59 11 VVV 75
Dupont Transaction 2
11 VVV 75
19.3 SGBDO
À partir de 1988 sont apparus les premiers SGBDO comme O2 (INRIA), ObjectStore,
En septembre 1991, création de l’ODMG (Object Database Management Group), groupe de réflexion
pour l’élaboration d’un standard de SGBD0.
3 langages (dernière version en 1997) :
ODL (Équivalent de DDL)
OQL (En gros le SELECT à la mode objet)
OML langages de manipulation destiné à être intégré dans C++, Smalltalk et Java.
porte_feuilles
Valeur * * Negociateur
* *
nb_titres
1 Ordre 1
Offre Demande
On définit des types d’objet par des interfaces, et des classes qui implémentent des interfaces (très
similaire à Java : héritage simple, implémentation de plus d’une interface possible)
Une classe peut aussi indiquer, grâce au mot clef extent , le nom de la collection (ou des collections)
destinée à héberger ses instances).
– guider l’implémentation (en terme de représentation des types abstraits d’ODL, et de codage des
méthodes) qui sera faite grâce à OML qui décrit l’intégration de ces objets dans au moins trois
langages (C++, Smalltalk et Java).
– permettre l’écriture de requêtes en langage OQL sans avoir à connaı̂tre l’implémentation.
select n.nom
from n in Les_Negociateurs
where n.capital() >= 5000 ;
Les noms des négociateurs qui ont un capital d’au moins 5000 euros pour au moins une des valeurs
dont ils ont des titres en porte-feuilles :
select n.nom
from n in Les_Negociateurs, e in n.porte_feuilles
where e.la_valeur.cours * e.nb_titres >= 5000 ;
À noter :
– l’itération cachée : pour chaque négociateur, on teste chacun de ses éléments de porte-feuille avecc
la clause where,
– la navigation e.la_valeur.cours qui permet d’éviter une jointure.
Chapitre 20
Le relationnel-objet de Oracle
Avec le relationnel-objet Oracle permet la définition de nouveaux types de données. Parmi ceux-ci
nous verrons les types objets, les types références et les types tables emboı̂tables. Nous ne verrons pas
les types varray.
20.1 Péliminaire
Oracle introduit deux niveaux dans son approche du relationnel-objet :
le niveau conceptuel des types introduits par l’instruction create type :
– types objet (attributs + méthodes)
– types tables emboı̂tables
À ce niveau on décrit des objets sans pouvoir exprimer aucune contrainte d’intégrité et on ne
fait (ou ne devrait faire) aucune hypothèse sur la manière dont seront stockés physiquement les
objets. Exemple :
create type Adresse as object (
numero Number (5), rue VARCHAR2 (20), ville VARCHAR2 (20)
) ;
le niveau stockage des tables introduites par l’instruction create table : ce sont les structures
d’accueil — ou de stockage — des valeurs des objets et des tables emboı̂tables. C’est seulement
à ce niveau qu’on peut exprimer des contraintes d’intégrité et des triggers.
On a maintenant deux sortes de tables :
les tables objets formées d’une seule colonne de type objet déclarées comme suit :
create table Les_Adresses of Adresse (
constraint Les_Adresses_PK primary key (numero, rue, ville),
constraint Les_Adresses_Prix_Positif check (1 <= numero)
) ;
create table Les_Adresses_2 of Adresse (
constraint Les_Adresses_2_PK primary key (numero, rue, ville)
) ;
236
20.2. TYPES OBJET ET MÉTHODES 237
Remarquer la notation pointée a.numero pour exprimer une contrainte portant sur un
attribut d’une colonne objet.
Contrairement à Java et SmallTalk qui ne disposent que d’une seule hiérarchie d’héritage, et comme
Ada et C++, en Oracle on peut avoir plusieurs hiérarchies d’héritage. Par exemple le type Personne
n’hérite d’aucun autre type et pourrait être la racine d’une hiérarchie d’héritage.
On peut cacher une méthode membre héritée en la redéfinissant par une méthode statique ( !) et ceci
sans le mot clef overriding ( ! ! !).
En PL/SQL, une redéfinition ne peut appeler la méthode redéfinie originale (comme on le fait en Java
avec super). On peut quand même factoriser le code en utilisant des méthodes statiques mais c’est
un peu scabreux !
À cause de la possibilité de redéfinition, l’appel de méthode donne lieu à une liaison dynamique qui
choisit la bonne méthode à exécuter en fonction du type précis de self et non pas bien sûr en fonction
du type statique de l’expression qui calcule self (c’est exactement la même chose qu’en Java).
create type Internaute under Personne (email Varchar2 (30)) not final ;
En revanche, une table relationnelle peut parfaitement avoir des objets indéfinis dans ses colonnes de
type objets, car cela ne remet pas en cause l’existance des lignes contenant ces objets indéfinis.
20.4.8 Accès aux composants par notation pointée
Pour les tables objet il faut toujours déclarer un alias de table et l’utiliser pour préfixer les
attributs de l’objet. Ceci est valable pour toutes les instructions du DML.
Cette propriété particulièrement agréable résulte de la séparation claire entre spécification (la définition
du type) et implantation (le body du type).
Par exemple, bien que la table Les_Etudiants le type Etudiant, il est possible d’ajouter la
utilise
méthode Statut grâce à la commande alter type :
20.4. ORACLE 10 PROPOSE L’HÉRITAGE SIMPLE 241
En revanche lors de l’appel d’une fonction membre sans paramètres, il faut quand même mettre les
parenthèses (comme en Java ou en C).
select e.Nom () as nom from Les_Etudiants e ;
Malheureusement, Oracle n’est que relationnel-objet (SGBDRO) et ne gère pas la persistance des ob-
jets : c’est au programmeur de coder, si nécessaire, l’ordre update qui garantira la persistance d’une
modification d’un objet.
Prenons l’exemple d’une méthode permettant de modifier le numéro dans la partie adresse d’un
étudiant.
alter type Etudiant replace as object (
...,
member procedure Changer_Numero (nouveau_numero in Positive)
) ;
Si cette modification n’est pas destinée à être persistante il suffit de modifier l’objet en mémoire
centrale :
create or replace type body Etudiant as
...
member procedure Changer_Numero (nouveau_numero in Positive) is
begin
242 CHAPITRE 20. LE RELATIONNEL-OBJET DE ORACLE
self.a.numero := nouveau_numero ;
end Changer_Numero ;
end ;
En revanche si cette modification doit être persistante, on peut être tenté d’émettre un ordre de mise
à jour dans la méthode :
create or replace type body Etudiant as
...
member procedure Changer_Numero (nouveau_numero in Positive) is
begin
self.a.numero := nouveau_numero ;
update Les_Etudiants
set a.numero = nouveau_numero
where p.nom = self.nom ;
end Changer_Numero ;
end ;
Plusieurs inconvénients :
– choix précoce sur le fait que la méthode a un effet persistant ou non – on pourrait par exemple
fournir systématiquement deux versions de chaque procédure, une persistante et l’autre non. ce
choix n’est pas à faire avec un SGBDOO
– en cas de persistance il faut connaı̂tre les structures de stockage (les tables). Un même type d’objet
peut être stocké dans plusieurs tables : comment choisir la bonne table pour garantir la persistance ?
Cela pose aussi des problèmes de maintenance si on choisit de modifier les noms des tables de
stockage.
Une solution : gérer la persitance à l’extérieur des méthodes.
Elle est par exemple utile en PL/SQL pour récupérer une ligne comme un objet :
declare
Toto Etudiant ;
begin
select Value (e) into Toto
from Les_Etudiants e where e.p.nom = ’Toto’ ;
Toto.Changer_Numero (20) ;
update Les_Etudiants e
set e = Toto where e.p.nom = ’Toto’ ;
end ;
20.4. ORACLE 10 PROPOSE L’HÉRITAGE SIMPLE 243
Une méthode d’instance qualifiée de order définit l’ordre des valeurs du type :
alter type Etudiant replace as object (
...,
order member function Compare (Avec in Etudiant) return Number
) ;
Une fonction d’ordre doit renvoyer (comme en C et en Java) :
– un entier négatif pour signifier que self est strictement plus petit que Avec.
– zéro pour signifier que self est égal à Avec.
– un entier positif pour signifier que self est strictement plus grand que Avec.
Ici on décide d’ordonner les étudiants par dates de naissance croissantes :
create or replace type body Etudiant as
...
order member function Compare (Avec in Etudiant) return Number is
begin
return self.p.Naissance - Avec.p.Naissance ;
end Compare ;
end ;
Cette fonction sera par exemple utilisée lors d’un order by, à condition d’utiliser la fonction Value()
comme ici :
select e.p.nom from Les_Etudiants e order by Value (e) ;
ou lors d’un group by.
Attention : seul un type racine d’héritage peut définir une fonction order.
Q. 258 Le fait que seul le type racine puisse définir un ordre peut-il s’expliquer ?
...
static function constructeur (...) return Etudiant is...
end ;
Le nom du type est utilisé comme préfixe lors de l’appel d’un sous-programme statique.
Q. 260 Quel est le type statique de e ? la clause select peut-elle consulter e.stress ?
Q. 261 Donner deux is of type qui seront toujours vrais sur Les Employes.
Treat nous permet de voir les Programmeur et les Secretaire comme des Stresse :
select e.nom, e.salaire, Treat (Value (e) as Stresse).stress
from Les_Employes e
where Value (e) is of type (only Programmeur, Secretaire) ;
Q. 262 Simplifier la requête pour qu’on puisse voir tous les stressés quel que soit leur type précis.
L’expression type statique correspond au type d’objet le plus précis qu’on puisse associer à l’expression
dès sa compilation. Par exemple le type statique de l’expression Value (p) dans :
select Value (p) from Les_Personnes p ;
Cependant on sait que les objets de la table Les_Employes peuvent être du type Employe ou de n’im-
porte lequel de ses sous-types, Programmeur par exemple. Donc, à l’exécution, la valeur de Value (e)
pourra être d’un type plus spécifique que Employe.
Par exemple, si on ne veut voir que les programmeurs avec tous leurs attributs de programmeur :
create view Vue_Programmeurs of Programmeur as
select Treat (Value (e) as Programmeur)
from Les_Employes e
where Value (e) is of type (only Programmeur) ;
20.5. LES RÉFÉRENCES : REF 245
20.4.21 Exercices
Q. 263 Introduire le type UE (Unité d’Enseignement) qui a comme attributs un nom (unique pour
toutes les UE) un certain nombre de crédits ECTS et un volume horaire.
Q. 264 Chaque étudiant peut suivre plusieurs UE et chaque UE peut être suivie par plusieurs
étudiants : implémenter la table des UE et les associations qu’elles entretiennent avec les étudiants.
Q. 265 Écrire les ordres SQL qui :
Q. 266 Ajouter aux étudiants la fonction membre volume qui renvoie la somme des volumes horaires
des UE auxquelles est inscrit l’étudiant. Quel défaut y a-t-il dans l’implantation de volume ?
Q. 267 Lister les étudiants qui sont inscrits à moins de 200h.
Q. 268 Comment empêcher qu’un étudiant s’inscrive pour un volume de plus de 300 heures ?
Q. 270 Comment faire pour qu’on puisse lister les étudiants par ordre croissant ou décroissant de
leurs volumes horaire ? le faire.
Par exemple, soit une association 1 vers n de l’objet Voiture vers l’objet Etudiant :
create type Voiture as object (
immatriculation VARCHAR2 (10),
proprietaire ref Etudiant
) ;
246 CHAPITRE 20. LE RELATIONNEL-OBJET DE ORACLE
Il vaudrait sûrement mieux imposer que tous les Etudiant référencés soient dans une seule table :
create table Les_Voitures of Voiture (
constraint PK_Les_Voitures primary key (immatriculation),
scope for (proprietaire) is Les_Etudiants
) ;
Ce qui fait aussi que les références prennent moins de place puisqu’on sait dans quelle table se trouvent
les objets référencés.
Les_Voitures Les_Etudiants
immatriculation proprietaire p.nom p.naissance a.numéro a.rue a.ville
Attention, la navigation sur les ref n’est possible que dans le monde SQL. Ainsi en PL/SQL seul
les ordres SQL embarqués pourront utiliser la navigation.
Par exemple les couples immatriculation, nom du propriétaire :
select v.immatriculation, v.proprietaire.p.nom
from Les_Voitures v ;
La jointure n’est pas explicite : on accède directement à l’objet Etudiant en navigant sur la référence
v.proprietaire.
Q. 271 Que calcule la requête suivante ?
Q. 272 Comment faire pour prendre aussi en compte les étudiants ne possédant pas de voiture ?
comme quoi la navigation n’est pas une baguette magique.
Un autre exemple de navigation sur la base de données suivante :
Q. 273 Implanter et garnir l’exemple précédent, est-il possible d’utiliser le même type objet pour
définir les tables Section et SousSection ?
Q. 274 Requête qui donne les nombres minimum, maximum et moyen de sous-sections par chapitre.
L’implémentation de la figure 20.1 est satisfaisante tant qu’on cherche le logement d’un équipement.
Q. 275 Requête listant les équipements des maisons ?
Pour résoudre ces deux problèmes Oracle propose la contrainte obligeant à ce que seuls les objets
d’une table particulière puissent être référencés : en scoped ref les références seront moins coûteuses en
espace (pas plus de 16 octets) et on saura toujours dans quelle table se trouvent les objets référencés.
alter table Les_Voitures
add (scope for (proprietaire) is Les_Etudiants) ;
Ou bien :
248 CHAPITRE 20. LE RELATIONNEL-OBJET DE ORACLE
Fig. 20.1 – Les Maisons et Les Appartements sont des tables objet de type Logement. Est-ce vraiment
une bonne idée ?
Les références à portée limitée peuvent aussi être exploitées par l’optimiseur.
Conclusion : la clause scoped ref ne peut être que fortement recommandée pour des raisons d’efficacité
et de lisibilité de la base de données.
update Les_Voitures
set proprietaire = null
where proprietaire is dangling ;
Dans les expressions de contrainte (unique, check, . . .) on ne peut pas naviguer sur les ref.
Pour cela on peut définir des tables de taille maximale fixée (les Varray) ou bien des tables emboı̂tées
sans limite de taille.
Varray est plus efficace que table emboı̂tée.
Dans la suite on ne verra que les tables emboı̂tées qui sont fonctionnellement plus riches.
Q. 277 Pourquoi n’est-il plus nécessaire qu’une voiture connaisse son propriétaire ?
C’est cette deuxième version que nous utiliserons.
20.6.5 Insertions
insert into Les_Conducteurs values
(Conducteur(’Dupont’, ’21/12/1975’,
Adresse (12, ’Charcot’, ’Lille’),
Des_Voitures (Voiture(’34 WWW 59’), Voiture(’22 XYZ 62’)))) ;
Les attributs v de Boyle et Selby sont indéfinis, ce qui est différent d’une table vide.
Les_Conducteurs
nom naissance a.numéro a.rue a.ville v
On remarque que les attributs a et v de Selby sont indéfinis, alors que ceux de Boyle sont définis : la
table emboı̂tée de Boyle est simplement vide.
Et une représentation qui tend plus vers l’implantation physique :
Les_Conducteurs Tab_Voitures
nom naissance a.numéro a.rue a.ville v xxx immatriculation
1 34 WWW 59
Dupont 21/12/1975 12 Charcot Lille xxx : 1
1 22 XYZ 62
Boyle Lille xxx : 2
3 11 CVS 75
Selby
3 41 SVN 94
Céline xxx : 3
On a ajouté le conducteur Céline pour mieux montrer que Tab_Voitures contient toutes les voitures.
Q. 278 Quel problème se pose si on veut qu’une voiture puisse avoir plusieurs conducteurs ? Comment
le résoudre en conservant les tables emboı̂tées ?
Et pour voir les couples nom du conducteur, voiture, on écrit tout simplement :
select c.nom, v.immatriculation -- couples (conducteur, voiture)
from Les_Conducteurs c, table (c.v) v ;
Ici chaque conducteur n’est joint qu’avec les tuples de sa table emboı̂tée.
Donc, si un conducteur ne conduit aucune voiture, il n’apparaı̂tra pas dans la liste. Si on veut qu’il
apparaisse quand même, on peut utiliser une jointure externe ((+) après la table emboı̂tée) :
select c.nom, NVL (v.immatriculation, ’Pas de voiture’)
from Les_Conducteurs c, table (c.v) (+) v ;
252 CHAPITRE 20. LE RELATIONNEL-OBJET DE ORACLE
on écrira plutôt :
select c.nom, count (v.immatriculation)
from Les_Conducteurs c, table (c.v) (+) v
group by c.nom ;
Pour illustrer l’utilisation d’une table, voici quelques manières de calculer le nombre de voitures du
conducteur dont le nom est passé en paramètre (il s’agit de versions inutilement compliquées) :
1. Le plus simple en utilisant la méthode Count :
create or replace function Nb_Voitures (
nom in Les_Conducteurs.nom%type
) return Natural is
v Des_Voitures ;
begin
select c.v into v
from Les_Conducteurs c
where c.nom = Nb_Voitures.nom ;
return case when v is null then 0 else v.count end ;
end Nb_Voitures ;
Column_Value est le nom par défaut de l’unique colonne anonyme d’une table.
On ne peut décrire aucune contrainte lors de la définition d’un type de table emboı̂table.
Si elle n’est pas nommée explicitement, la colonne d’une table emboı̂tée s’appelle Column_Value.
254 CHAPITRE 20. LE RELATIONNEL-OBJET DE ORACLE
Q. 281 Insérer un parking disposant d’un étage composé d’une rangée vide.
Q. 282 À ce parking, ajouter un étage composé d’une rangée accueillant deux voitures.
Q. 283 Comment introduire un nombre maximal de voitures pouvant être garées dans une rangée ?
Q. 284 Représenter la même information de façon purement relationnelle, puis écrire la même requête
et initialiser la table de la même manière.
Q. 285 Requête calculant les noms des conducteurs conduisant au moins 2 voitures.
Q. 286 Requête calculant les noms des conducteurs possédant au moins une voiture immatriculée
dans le Pas-de-Calais (62).
Un conducteur dispose d’une table emboı̂tée contenant les références des voitures conduites et une
voiture d’une table emboı̂tée contenant les références de ses conducteurs. L’association doit rester
symétrique contrairement à la solution suggérée pour l’exercice 20.6.11 page 254.
– Oracle nous permet la symétrie de structure, par contre on ne peut lui demander de garantir la
symétrie de contenu — c’est à dire le fait qu’un conducteur conduit une voiture si et seulement si
cette voiture est conduite par ce conducteur. C’est donc au programmeur qu’il incombe de garantir
cette symétrie de contenu.
Une autre solution pas tout à fait équivalente permet de se passer du type intermédiaire :
create type Ens_Voitures as table of ref Voiture ;
create type Ens_Conducteurs as table of ref Conducteur ;
mais alors la table Ens_Voitures contient une unique colonne anonyme et il faudra utiliser le pseudo-
identificateur Column_Value pour désigner cette colonne. Par exemple, si la_voiture désigne une
valeur de Ens_Voitures, avec la première solution on écrirait :
la_voiture.r.immatriculation
On peut vérifier que chacun des huits types précédents dépend indirectement de lui-même.
Il semble qu’il ne soit pas possible de limiter la portée des références contenues dans une table emboı̂tée
(aucun moyen d’utiliser la clause Scope For).
256 CHAPITRE 20. LE RELATIONNEL-OBJET DE ORACLE
Q. 289 Donner deux requêtes permettant de voir que l’assocition est bien symétrique.
Attention cette procédure PL/SQL n’est valide qu’à partir de Oracle10, en Oracle 8 il aurait fallu
écrire :
-- tester si l’association existe déjà
select count (*) into n
from Les_Conducteurs c,
20.8. NIVEAUX DE PURETÉ DES MÉTHODES D’OBJET 257
20.7.7 Exercices
Reprendre l’exercice de la section 20.6.11 à la page 254 en proposant cette fois une implantation
symétrique de l’association entre les étudiants et les UE.
Q. 291 En provocant sciemment des erreurs, vérifier que ces niveaux de pureté sont bien un garde-fou.
Le but ici est de fournir une notation graphique permettant d’exprimer clairement et sans la lourdeur
de la syntaxe Oracle les choix fait pour l’implantation. Cette notation s’appelle le schéma navigationnel.
Ce schéma navigationnel peut aussi permettre de se faire une idée a priori de l’adéquation de l’im-
plantation qu’il décrit avec les opérations ou requêtes qu’il devra supporter (par exemple : ce schéma
navigationnel permet-il une exécution efficace de telle requête très fréquente ?).
Une flèche double correspond à plusieurs références (éventuellement zéro). Cet ensemble de références
peut-être implanté de diverses manières : table emboı̂tée de références, table (clef de l’objet référençant,
référence).
Si les objets référencés par une flèche double ne sont pas partageables on peut aussi se passer de
références en les stockant dans une table emboı̂tée dans l’objet référençant.
Etudiant Voiture
Etudiant
v Voiture
Chaque étudiant possède une table emboitée de références sur ses voitures
Etudiant Voiture
Etudiant
Dans le premier cas on a simplement remplacé une clef étrangère par une référence. Sauf si on a posé
une contrainte d’intégrité référentielle sur la colonne référençante, Oracle autorise la suppression d’un
objet référencé.
Dans le second cas les voitures n’existent pas de façon autonome : une voiture ne peut être mémorisée
que si elle appartient à un étudiant. Une contrainte d’unicité sur les voitures d’un étudiant ne peut
être exprimée simplement : il faut la programmer.
20.9. CONCEPTION D’UN SCHÉMA RELATIONNEL-OBJET 259
Dans le troisième cas il n’est pas possible de garantir que les références de voiture sont forcément prise
dans la table des voitures (pas de clause scope for possible).
[1] PostgreSQL 8.2.1 Documentation. 2006. Documentation plutôt bien lisible du SGBD PostgreSQL
(on y apprend des choses), le site : http://www.postgresql.org.
[2] S. Sudarshan Abraham Silberschatz, Henry F. Korth. Database System Concepts. Mc Graw Hill,
1997. Fondamental. Un classique et assez gros bouquin général, qui parle de quasiment tous les
aspects. Un de ceux que je préfère.
[3] ACSIOME. Modélisation dans la conception des systèmes d’information. Masson, 1990. Un bon
bouquin sur la modélisation avec plein d’exemples et d’exercices très complets.
[4] Nacer Boudjlida. Bases de données et systèmes d’informations. Dunod, ISBN 2-10-004309-9,
1999. Assez proche des objectifs de ce poly. Les généralités sont exposées clairement, en revanche,
techniquement il y a relativement peu d’informations sur un SGBD particulier, les SGBD donnés
en exemple sont Sybase et Oracle.
[5] Chris J. Date. Introduction aux bases de données, 8ième édition. Vuibert, ISBN 2-7117-4838-3,
2004. Fondamental. Un (très bon, le meilleur de ce que j’ai pu lire) classique. Une introduction
intuitive aux fondements des BDD relationnelles.
[6] Steven Feuerstein. Oracle PL/SQL, Guide du programmeur, 3ième édition. O’Reilly, ISBN 2-
84177-238-1, 2002. Technique. Un gros bouquin spécialisé, pour se perfectionner en PL/SQL. Le
chapitre sur les dates est particulièrement limpide. Malheureusement cet ouvrage ne donne aucun
retour d’expérience pour la mise en place de transactions (voir mon poly :-).
[7] A Sayah G. Padiou. Techniques de synchronisation pour les applications parallèles. Cepadues
Editions, 1990. Un livre pas très épais qui introduit clairement les problèmes des applications
parallèles, leur compréhension, et les techniques permettant de les résoudre.
[8] Georges Gardarin. Bases de Données, objet et relationnel. Eyrolles, 1999. Fondamental. Un gros
bouquin général, qui parle de quasiment tous les aspects. Enormément de références bibliogra-
phiques, mais, dans le genre, je préfère [2] et surtout [5].
[9] Jennifer Widom Hector Garcia-Molina, Jeffrey D. Ullman. Database Systems, the complete book.
Prentice Hall, 2002. Un super livre aussi.
[10] Christian Marée / Guy Ledant. SQL2 Initiation Programmation. Armand Colin, 1994. Technique.
Très pratique sur SQL2.
[11] Philippe Mathieu. Des bases de données à l’internet. Vuibert, 2000. Un bon bouquin assez
général sur les bases de données et qui aborde la construction d’applications Web.
[12] Jason Price. Java Programming with Oracle SQLJ. O’Reilly, 2001. Je n’en ai lu qu’un chapitre
dont je déduis que cet ouvrage sur SQLJ a l’air très pédagogique. Le chapitre est celui qu’on peut
trouver à partir de la page http://www.oreilly.com/catalog/orasqlj.
[13] Raghu Ramakrishnan and Johannes Gehrke. Database Management Systems. McGraw-Hill, ISBN
0072465638, 2002. Je ne l’ai pas lu (c’est Cédric qui me l’a indiqué : C’est en anglais, il est très
cher, mais je le trouve très bien et très clair).
[14] Günther Stürner. Oracle7. Thomson Computer Press, 1995. Technique. Génial pour savoir
comment marche Oracle.
260