Sie sind auf Seite 1von 260

Chapitre 1

Base de données et Système de gestion


de base de données

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


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

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

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

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

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


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

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


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

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

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

1.3 Les modèles de données


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

1.4 Les niveaux d’abstraction


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

1.5 Schéma et instances


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

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

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

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

1.6 Les différents langages côté serveur


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

create table Etudiant (


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

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


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

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

create view Nb_Homonymes (Nom, Nombre_D_Etudiants_Portant_Ce_Nom) as


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

create view Effectifs (id, mention, nb_etudiants) as


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

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

1.6.2 DML : Data Manipulation Language


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

1.6.3 DCL : Data Control Language


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

1.7 L’Architecture Client/Serveur


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

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

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


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

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

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


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

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


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

Relationnel et SQL

6
Chapitre 2

Le modèle relationnel et SQL

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

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

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

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

2.1 Qu’est-ce qu’un ensemble


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

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

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


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

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


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

2.2.1 Schéma ou intention d’une relation


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

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

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

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

2.2.2 Contenu ou instance ou extension d’une relation


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

SQL

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


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

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

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

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

2.2.3 Schéma et extension


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

Id Nom Departement Population


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

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


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

2.3 Clef d’une relation


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

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

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

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

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

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

2.4 Clef étrangère


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

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

create table Ville ( create table Fete (


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

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

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

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


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

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

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


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

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

2.5.2 Les opérateurs de base


La projection : SELECT

Pour ne conserver que certaines colonnes.

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

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

 2 Paris 75 2.200.000 

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

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


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

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


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

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

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

Par exemple, on veut les villes du nord :

Id Nom Dpt Population


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

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

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


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

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

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

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

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

2. ou, si on souhaite conserver les boublons :


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

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

insert into Ville_Du_Nord


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

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

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

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

Les villes dont le département est connu :


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

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

Nouveau jeu de données (figure 2.1)

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

Table Etudiant  Table Note


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

Le produit cartésien : CROSS JOIN


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

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

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


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

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

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

select * from Etudiant ;

select distinct etu.*


from Etudiant etu cross join Matiere mat ;

2.5.3 Quelques opérateurs supplémentaires


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

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


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

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

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


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

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

Alfred BD 12
Alfred CL 14
Julie CL 15

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


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

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

select e.nom, n.note


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

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

On distingue plusieurs cas particuliers de jointures

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

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

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

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

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


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

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

Q. 11 Écrire cette requête.

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

L’intersection

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


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

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

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

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


R

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

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

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

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

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

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

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

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

2.6 Le cas des valeurs indéfinies


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

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

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

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

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

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

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

Paris-Texas

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

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

2.6.1 Noter l’absence de valeur


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

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

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


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

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

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

2.6.3 Comportement des opérateurs relationnels


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

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

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

2.6.4 Comportement des opérateurs logiques


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

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

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

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

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

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

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

2.6.5 Présomption d’innocence de la clause where


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

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

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


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

2.7.2 Expression conditionnelle : case


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

end as categorie
from Ville v ;

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

2.7.3 Les fonctions nvl et coalesce


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

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

2.7.4 Manipuler les chaı̂nes


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

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

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

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

Dans le modèle de like :


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

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

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

Attention Note de la documentation Oracle 10 :


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

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

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

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

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

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


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

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

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

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

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

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

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


from Note n ;

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

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

fonction valeur si expr est toujours


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

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

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

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


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

Fig. 2.3 – Expressions à classer.

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


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

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

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

2.9 Les sous-requêtes


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

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

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

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

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


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

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

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


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

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

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

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

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


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

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

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

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

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

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

Q. 32 Que calcule cette requête ?

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


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

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

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

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

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

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


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

select m.nom, e.nom


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

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

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

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

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

2.10 Ordonner le listing des nuplets : order by


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

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

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

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


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

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

2.11 La formation de groupes : group by


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

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

1 Alfred 13
3 Julie 15

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

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

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

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


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

group by e.id, e.nom ;

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


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

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

id nom moyenne nb_notes


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

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

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

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

2.11.1 Sélectionner des groupes : la clause having


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

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


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

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

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

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

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

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

2.11.2 group by et informations indéfinies


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

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


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

2.12 Les jointures externes : outer join


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

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

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

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

Les jointures sont (Oracle10, Postgres, SQL92) :


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

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


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

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

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

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


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

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

2.14 Emplacement des fonctions d’agrégation


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

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

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

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

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


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

2.15 Pour conclure


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

Dépendances fonctionnelles et
normalisation

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

3.1 Dépendances fonctionnelles


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

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


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

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

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

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


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

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

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

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

34
3.3. AXIOMES DE ARMSTRONG 35

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

3.3 Axiomes de Armstrong


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

3 corollaires bien pratiques des axiomes de Armstrong


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

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

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

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

3.4 Calculer les clés candidates d’une relation


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

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


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

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

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

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


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

1NF Si tout attribut a une valeur atomique.


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

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

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

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

3.5 Décomposer une relation sans perte d’information


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

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


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

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


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

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


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

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

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

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

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


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

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


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

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

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


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

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

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

Application (emprunté au poly de Mireille Clerbout)


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

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

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

SQL/DML les ordres de modification


des tables

SQL signifie Structured Query Language


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

4.1 insert : ajout de nouvelles lignes


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

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

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

4.2 update : la mise à jour de lignes existantes


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

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

affectation ::= colonne = expression


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

create table Employe (


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

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

ou bien, de façon équivalente :


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

4.3 delete : suppression de lignes existantes


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

Contraintes d’intégrité en SQL

DDL = Data Definition Language


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

5.1 Création des tables


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

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

5.2 Les types de données


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

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

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

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

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

Numériques Caractères Binaires Dates, Intervalles

5.2.1 Types numériques


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

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


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

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


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

affiche 0.66.

5.2.2 Types caractères


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

Pour norme ANSI/SQL


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

5.2.3 Types binaires


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

5.2.4 Types temporels


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

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


12 fev 2003

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


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

select to_char (SYSDATE,


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

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


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

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

Pour norme ANSI/SQL


– TIME et TIMESTAMP (Oracle DATE)

5.3 Les contraintes


Depuis SQL2.

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


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

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

5.3.1 Baptisez vos contraintes !


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

5.3.2 Aspects syntaxiques


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

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

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

5.3.3 Liste des contraintes


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

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


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

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

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

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

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

check (date_deb < date_fin)


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

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

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

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

Présomption d’innocence pour la contrainte check


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

Par exemple :


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

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

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

Définition de nouveaux domaines (seulement en PostgreSQL)


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

create table ... (


qte_produit Quantite,
...
) ;

Un exemple de domaine en PostgreSQL :


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

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


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

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

Relation sans clef


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

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

create table Sans_Clef (num Number (3)) ;

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

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


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

create table Note (


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

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

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

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


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

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

) ;

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

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

Suivent quelques manipulations dont certaines sont erronées.

On peut noter un étudiant non défini !


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

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


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

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


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

update Etudiant set id = 444


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

Modification de contrainte pour propager la mise à jour


Impossible en Oracle 10, mais possible en Postgres 8.

On ne peut pas supprimer un étudiant noté


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

delete from etudiant where id = 333 ;


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

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


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

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

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

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

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


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

delete from etudiant where e.nom is null ;


-- OK

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


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

5.4 Le dilemme de la dépendance mutuelle


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

par des états intermédiaires incohérents.

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

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


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

constraint Conducteur_PK primary key (id)


) ;

create table Voiture (


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

constraint Voiture_PK primary key (id),


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

alter table Conducteur


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

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


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

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


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

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

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


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

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


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

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


5.5. MODIFICATION DU SCHÉMA 51

– d’un set constraint ... immediate,


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

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

drop table Voiture ;

5.5 Modification du schéma


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

5.5.1 alter table


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

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


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

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


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

Suppression de colonne et/ou contraintes : drop

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


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

Activer/Désactiver les contraintes : enable/disable

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


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

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

alter table Autre


add primary key (num_autre) disable ;

alter table Dept


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

alter table Dept


disable constraint dname_PK ;

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

5.5.2 Suppression d’une relation


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

drop table <nom> ;

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

5.5.3 drop table ... cascade constraints


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

create table Esclave (


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

La suppression
drop table Maitre ;
-- erreur oracle

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

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


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

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

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


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

create table Voiture (


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

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


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

create sequence Id_Voiture ;


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

drop sequence Id_Voiture ;


Chapitre 6

Compléments Oracle SQL

6.1 Expression conditionnelle decode


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

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

6.2 Expression conditionnelle case


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

Exemple donnant une mention :


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

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

6.3 group by cube et group by rollup


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

55
56 CHAPITRE 6. COMPLÉMENTS ORACLE SQL

6.3.1 Clef de groupe


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

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

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

6.3.2 Cube et RollUp


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

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

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

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


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

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

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

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


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

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

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

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

Les 4 sous-clefs de groupe seront les quatre


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

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

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

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

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

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

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


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

6.3.3 La fonction grouping


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

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

6.3.4 Exemples de RollUp


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

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

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


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

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

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

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


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

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

6.3.5 Exemples de Cube


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

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

Sartre 15
Vian 12
Moyenne générale 13.2

6.3.6 Sélectionner les super-groupes

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


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

select decode (grouping (Matiere.nom),


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

MATIERE ETUDIANT_OU_PROMOTION MOYENNE


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

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

select Matiere.nom as Matiere,


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

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


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

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


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

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

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

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

Suivent quelques exemples.


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


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

6.4.1 Exploration d’un graphe sans cycle


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

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

Développement serveur

63
Chapitre 7

Introduction à PL/SQL

PL/SQL = Programming Language with SQL.

Langage de programmation procédural inspiré de Ada.

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

Permet d’inclure facilement des requêtes SQL.

Ce langage est utilisé :


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

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


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

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

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


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

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

64
7.3. FONCTION STOCKÉE 65

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

7.2.1 Exemples de déclaration de variable


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

7.2.2 Le type Boolean


Il est muni des deux valeurs true et false.

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

7.2.3 Les expressions


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

7.2.4 Les connecteurs logiques and et or


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

7.3 Fonction stockée


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

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


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

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

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

7.4 Procédure stockée


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

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


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

Le typage implicite est aussi utilisable pour les variables locales.

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

7.4.1 Exécuter une procédure stockée


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

7.4.2 Une autre procédure : équilibrage des salaires


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

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

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

7.5 Bloc anonyme


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

bloc ::= [ declare


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

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

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

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


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

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


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

7.7.1 Passage sans copie : nocopy


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

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

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

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


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

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


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

select Test_Copie (3) from Dual ;


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

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

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


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

select Test_Adresse (3) from Dual ;


7.8. TYPES COMPOSÉS : LES RECORDS 69

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

7.8 Types composés : les records


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

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

7.9 Types composés : les collections


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

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


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

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


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

Par exemple avec une initialisation littérale :


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

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


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

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


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

7.9.3 Tableaux dense : Varray


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

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

7.10 Les objets


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

7.11 Typage implicite : %type et %rowtype


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

7.12 Structures de contrôle


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

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

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

while <predicat> loop ... end loop ;

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


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

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

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

7.13 Résultats de compilation


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

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

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


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

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


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

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


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

7.14.2 Les exceptions de select into


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

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

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

select <fonction-a-tester> from Dual ;

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

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


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

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


Traiter (Le_Client) ;
end ;

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


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

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

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

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

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

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


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

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


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

end F ;

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

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


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

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

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


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

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


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

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

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

Deux exemples presque équivalents de curseurs sans paramètre


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

Exemple de curseur paramétré avec Min et Max


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

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

7.15.2 Comment baptiser les curseurs


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

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

7.15.3 Utiliser un curseur pour typer implicitement


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

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

7.15.4 Les opérations et attributs des curseurs


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

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

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

close <curseur> ;
close Les_Nom_Prenom ;

On peut ensuite réouvrir le curseur.

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


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

Exception Code d’erreur Explication


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

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


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

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

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

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


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

end Ajouter ;

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

Q. 105 Que calcule la fonction F suivante :

create function F (A in NaturalN) return NaturalN is


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

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


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

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

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

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

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

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


ORA-06512: à ligne 4

Propager un code d’erreur et un libellé explicites


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

ORA-20101: employé encore à l’essai


ORA-06512: à ligne 2

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

7.16 Les paquetages


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

La surcharge des noms de sous-programmes est possible.

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

create or replace package Gestion_Client is


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

create or replace package body Gestion_Client is

courant number (4) ;


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

function nombre (bout in VARCHAR2) return Number is


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

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


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

begin -- Initialisation des variables du paquetage


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

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

7.17 PL/SQL et l’interaction homme-machine


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

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

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


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

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

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

Soit la fonction parfaitement correcte :


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

create function Moyenne return Etudiant.note%type is


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

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

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

delete from Etudiant e


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

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


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

Par exemple, voici deux tables :


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

create table Fils (


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

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

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

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

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

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


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

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


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

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

set salaire = salaire * 1.1


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

delete from Employe


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

7.21 Amélioration des performances du code PL/SQL


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

7.21.1 La clause bulk collect


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

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

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


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

Exemple avec update :


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

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

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

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

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

Les triggers

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

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

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

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

8.1 Deux utilisations possibles des triggers


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

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

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

86
8.2. STRUCTURE D’UN TRIGGER 87

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

8.2 Structure d’un trigger


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

drop trigger <Nom-du-Trigger> ;


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

8.2.1 before et after


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

8.2.2 Les événements


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

8.2.3 Granularité du trigger


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

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

trigger instruction : for each row absent


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

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

trigger ligne : for each row présent


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

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

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

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

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

La clause when (Condition sur la ligne courante)


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

8.2.4 Le bloc anonyme


C’est du PL/SQL.

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

8.2.5 Prédicats utilisables dans le code PL/SQL


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

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

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

8.3 Instants de déclenchement des triggers instruction et ligne


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

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

État stable de la table, elle


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

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

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

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


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

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


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

create or replace trigger Controler


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

end if ;
end ;

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

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

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

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


eme sur une table vide

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


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

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

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

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

8.5 Exemples de triggers rendant active la base


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

create or replace trigger Modif_Service


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

create or replace trigger Modif_Employe


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

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


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

procedure Modifier (Serv in Service.id%type,


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

if inserting and :new.service is not null then


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

8.5.2 Un trigger instruction d’audit


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

create or replace trigger Auditeur


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

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


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

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


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

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

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

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

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


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

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

2 20 20 21 20 20 2 20

1 14

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

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

8.5.4 Un trigger ligne pour une BD active : commande automatique


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

create or replace trigger Commande_Automatique


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

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

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


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

8.6 Table mutante (Mutating table)


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

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

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

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

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

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

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


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

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

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

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

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

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

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


Préalablement à l’utilisation de triggers il faut s’assurer que la propriété ne peut vraiment pas être
exprimée de façon déclarative : les triggers introduisent en général une complexité qui peut rendre
délicate la maintenance de la base de données.

C’est pourquoi, si la technique des triggers semble incontournable, il est important de faire une analyse
structurée avant de les implanter.
Le problème est : en quoi une modification de la BD peut-elle casser la propriété.
1. faire l’inventaire des tables pour lesquelles une modification pourrait casser la propriété,
2. construire un tableau à deux entrées : en lignes les tables, en colonnes les événements (insert,
update, delete) et, pour chaque case, en quoi l’événement se produisant sur la table est sus-
ceptible ou non de casser la propriété. Il est aussi intéressant d’y faire figurer les colonnes de la
table intervenant dans le maintien de la propriété.
3. utiliser les informations précédentes pour savoir si fonctionnellement un ou des triggers ligne ou
instruction peuvent ou doivent être mis en place.
Le choix entre trigger ligne ou instruction n’est pas forcément évident :
– le trigger ligne vérifie que la modification de chaque ligne conserve la propriété, il peut être intéressant
si très peu de lignes sont modifiées à chaque mise à jour de la BD.
– l’avantage du trigger instruction est qu’il travaille toujours sur une BD stable (non mutante), ce-
pendant il peut être coûteux si à chaque modification d’une table il vérifie que ses 10 millions de
lignes vérifient toujours la propriété alors qu’une seule ligne a été modifiée !

8.8 Exemple de conception de trigger


Appliquons cette démarche sur un exemple non trivial :
____________________ _________________________
v | | v
Produit(id, prix >= 0) Achat(p, c, quantité >= 0) Client(id, solde)
-- ---- --

 
La propriété Psolde suffisant à garantir est :
 
le solde d’un client est soit indéfini soit supérieur ou égal au total de ses achats .

D’abord on ne peut garantir cette propriété Psolde suffisant de façon déclarative : vérifier Psolde suffisant
nécessite d’observer l’état global des trois tables Produit, Achat et Client grâce à une requête qui
calcule la somme des achats de chaque client. Or Oracle ne permet pas d’évaluer une requête dans une
contrainte check et ne dispose pas des assertions définies par la norme SQL.

Donc l’usage de triggers est inévitable !

L’inventaire nous donne les trois tables et on obtient le tableau :


8.8. EXEMPLE DE CONCEPTION DE TRIGGER 97

insert update delete


Produit ♥ Si le prix a augmenté ♥
Un changement de produit et/ou de client et/ou
Un nouvel achat peut
Achat une augmentation de la quantité peuvent casser ♥
casser Psolde suffisant
Psolde suffisant
Client ♥ Si le solde a décru ♥
Un cœur (♥) dans une case indique que l’événement sur la table ne peut pas casser la propriété.
Il va donc être nécessaire d’écrire au moins 3 triggers !
Le tableau ne dit pas comment s’y prendre pour vérifier la propriété, et ce n’est pas son rôle. La suite

considère 
indépendamment chaque case du tableau susceptible de casser la propriété :
– table Client : seul un ordre update sur Client peut casser la propriété. C’est le cas le plus
simple : il suffit de vérifier Psolde suffisant pour chaque client modifié dont le solde a décru ou vient
d’être défini. Cela peut se faire avec un trigger ligne car le calcul du montant des achats d’un client
n’a besoin d’explorer que les tables Produit et Achat et on n’aura donc pas de problème de table
mutante.
create or replace trigger Maj_Solde_Client
before update of solde
on Client
for each row when (new.solde is not null and
(old.solde is null or new.solde < old.solde))
declare
Somme_Des_Achats Natural ;
begin
select Sum (a.quantite * p.prix) into Somme_Des_Achats
from Achat a inner join Produit p on a.p = p.id
where a.c = :new.id ;
if Somme_Des_Achats > :new.solde then
raise_application_error (-20111, ’Solde client insuffisant’) ;
end if ;
end ;
Ici on a adopté l’approche du check : si le nouveau solde est indéfini on considère que Psolde suffisant
est vérifiée (présomption d’innocence).

 
Q. 130 Réécrire plus simplement la clause when en utilisant la fonction nvl.
– table Produit : un ordre update peut casser la propriété. Pour cet événement, un trigger ligne
n’est pas approprié car il a 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 :

create or replace trigger Maj_Prix_Produit


after update of prix
on Produit
begin
Verifier_Soldes_Suffisants ;
end ;

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 :

create or replace trigger Verifier_Achat


after insert or update
on Achat
begin
Verifier_Soldes_Suffisants ;
end ;

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 :

insert into Achat select ... ;

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

Cette figure illustre cette mise en place :


Vider_CAV 3 Verifier_CAV
1 Achat
Voici la table de travail : instruction before instruction after
vide la table CAV vérifie les clients
-- Clients a verifier
de CAV
create global temporary table CAV (
2
id Number (5) not null Garnir_CAV
) on commit preserve rows ;
ligne before
create unique index CAV_PK on CAV (id) ; mémorise dans CAV le
le client concerné par l’achat

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 ;

-- TRIGGER LIGNE execute pour chaque ligne inseree dans Achat


create or replace trigger Garnir_CAV_Insert
before insert on Achat
for each row when (nvl (new.quantite, 0) != 0)
begin
insert into CAV values (:new.c) ;
exception
when Dup_Val_On_Index then -- le client y etait deja !
null ;
end ;

-- TRIGGER INSTRUCTION execute apres la fin de insert


create or replace trigger Verifier_CAV
after insert on Achat
declare
Nb_Clients_Insolvables Natural ;
begin
select Count (Count (*)) into Nb_Clients_Insolvables
from CAV cv
inner join Client c on cv.id = c.id
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
100 CHAPITRE 8. LES TRIGGERS

raise_application_error (-20111, ’Solde client insuffisant’) ;


end if ;
end ;
Dans le trigger Garnir_CAV_Insert il est inutile de vérifier que new.c et new.p sont définis puisqu’ils
font partie de la clef primaire.
L’équijointure de Verifier_CAV débute par la table CAV, elle ne prend donc en compte que les
clients concernés par de nouveaux achats.

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.

La partie la plus critique est probablement de bien concevoir :


1. ce que doit mémoriser la table de travail,
2. le trigger ligne qui garnit la table de travail : il doit minimiser le nombre de données à vérifier
sans en oublier aucune !
Nous en sommes à 5 triggers !
Lors d’un update sur Achat on peut utiliser la même technique qu’au point précédent : recenser les
clients qu’il faut absolument vérifier puis les vérifier après l’update de Achat. Pour cela on modifie
les deux triggers instruction précédents pour qu’ils soient aussi déclenchés par update :
create or replace trigger Vider_CAV
before insert or update
on Achat
begin -- comme avant
end ;
create or replace trigger Verifier_CAV
after insert or update
on Achat
declare -- comme avant
end ;

C’est la sélection de ces clients qui constitue le cœur du travail :


– si la new.quantite est indéfinie : il n’y a rien à vérifier,
– sinon il faudra vérifier new.c si :
– on a changé de client et que new.quantite est strictement positive,
– sinon si on a changé de produit et que new.quantite est strictement positive,
– sinon si on a augmenté la quantité ou bien que l’ancienne était indéfinie et que la nouvelle est
strictement positive.
create or replace trigger Garnir_CAV_Update
before update on Achat
for each row when (new.quantite is not null and
(nvl (old.quantite, 0) < new.quantite or
(old.p != new.p and new.quantite != 0) or
(old.c != new.c and new.quantite != 0)
))
begin
insert into CAV values (:new.c) ;
exception
when Dup_Val_On_Index then -- le client y était déjà !
null ;
end ;

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).

Ne pas abuser des triggers :


– ils introduisent un coût non négligeable (la clause when des triggers ligne permet cependant de
minimiser ce coût).
– interdépendance entre triggers : un trigger qui insère, détruit ou modifie des lignes peut provoquer
le déclenchement d’autres triggers qui peuvent eux-mêmes en déclencher d’autres . . .Cela induit une
complexité qui peut devenir difficile à maı̂triser.
Les propriétés d’une base de données peuvent aussi être garanties en ne donnant accès qu’à des
procédures stockées programmées pour conserver ces propriétés.
Oracle 8 propose deux nouvelles sortes de triggers :
triggers instead of ce sont des triggers de vues qui permettent de programmer explicitement la
modification des tables sous-jacentes de la vue lorsqu’on demande à modifier la vue (section 10.5
page 114),
triggers systèmes pour l’administrateur...

8.10 Deux mots à propos de PostgreSQL


Voici ce que PostgreSQL propose :
– PostgreSQL ne connaı̂t pas le concept de table mutante ! il n’y a donc pas de garde fou en Post-
greSQL.
– Les triggers lignes before voient la table dans un état instable sans qu’aucune erreur ne soit
déclenchée (ce qui ne semble pas très sérieux !).
– en revanche, et là c’est beaucoup mieux, les triggers lignes after travaillent sur une table stable dont
la valeur est celle obtenue après exécution complète de l’instruction de mise à jour ; le fait que ces
triggers disposent à la fois de l’ancienne et de la nouvelle valeur de chacune des lignes modifiées (old
et new) repose probablement sur le fait que PostgreSQL (comme Oracle) gère plusieurs versions de
chaque ligne d’une table (protocole multi-versions, voir les chapitres 13 et 14).
Troisième partie

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.

9.1 Qu’est qu’un objet


Toute entité créée par l’ordre create est un objet : table, vue, procédure stockée, trigger, index, . . ..
Pour pouvoir manipuler un objet, il faut bien sûr qu’il ait un nom, la plupart du temps on utilise
simplement le nom local de l’objet (exactement comme en Unix un simple nom de fichier ou de
répertoire désigne un objet du répertoire courant).
Il est bien entendu possible de désigner de façon plus absolue un objet en donant son nom complet :
– en Oracle, le nom complet d’un objet est le nom de l’objet préfixé par le nom du schéma dans lequel
il a été créé, par exemple durif.Employe ou GMI13.Client. Le nom de schéma est homonyme du
compte utilisateur disposant de ce schéma.
– en PostgreSQL, le nom complet d’un objet est le nom de l’objet préfixé par le nom du schéma
contenant cet objet lui-même préfixé par le nom de la base de données contenant ce schéma, par
exemple annuaire.public.Date_Admin où annuaire est le nom de la base de données et public
est le schéma par défaut1 . Contrairement à Oracle, PostgreSQL distingue clairement les notions
d’utilisateur et de schéma.
Avec un nom complet il est donc possible de désigner un objet créé par un autre utilisateur ou se
trouvant dans un autre schéma. Par défaut un utilisateur n’a aucun droit sur les objets qu’il n’a pas
créés.

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

9.2 Les utilisateurs et les privilèges


Les privilèges sont de deux sortes :
– les privilèges, dit objet, qui gouvernent les opérations possibles sur le contenu des objets de la base,
– les privilèges, dit système, qui gouvernent les opérations concernant les contenants : création, mo-
dification de la description (alter) et destruction d’objet.

9.2.1 Les privilèges objet


Les privilèges objet permettent de consulter ou modifier l’état d’un objet particulier, ou de l’exécuter,
mais sans pouvoir le détruire ou en créer de nouveaux (ces opérations correspondent à des privilèges
système). Chacun de ces privilèges fait référence à un objet particulier de la base :

privilèges types d’objet


select table, view et sequence
update, delete, insert table et view
alter table et sequence
execute sous-programme et paquetage
references (possibilité de définir une clé étrangère) table
index (possibilité de définir un index) table commande create index
... ...

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.

Q. 131 Pourquoi les privilèges index et references ont-ils un sens ?


Les synonymes (create synonym) sont transparents autant pour déterminer les privilèges de l’utili-
sateur sur l’objet désigné par le synonyme que pour gérer les privilèges de ce même objet (i.e. on peut
indifféremment utiliser le synonyme ou le vrai nom de l’objet).

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.

Un sous-programme pour lequel on a le privilège execute peut exécuter d’autres sous-programmes


pour lesquel on n’a pas ce privilège et manipuler des tables et des vues pour lesquelles on n’a pas de
privilège. Par défaut, un sous-programme s’exécute avec les privilèges de celui qui l’a compilé. Ceci
permet de limiter très précisément ce qu’un utilisateur peut faire sur un ensemble de tables.

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).

9.3 Gestion des privilèges objet


9.3.1 Donner des privilèges objet : grant
L’utilisateur qui donne des privilèges objet à un autre utilisateur doit soit être propriétaire de l’objet
soit avoir lui-même obtenu ces privilèges d’un autre utilisateur avec l’option with grant option.
grant <liste-de-privilèges-objet> on <objet> to
<liste-utilisateurs-et-ou-r^oles> | PUBLIC
106 CHAPITRE 9. PRIVILÈGES ET RÔLES

[ with grant option ] ;


<liste-de-privilèges-objet> ::= <privilège> { , <privilège> }
| all [privileges] [ ( <liste-de-colonnes> ) ]
<privilège> ::= alter | delete | execute | index | select | read
| insert [ ( <liste-de-colonnes> ) ]
| references [ ( <liste-de-colonnes> ) ]
| update [ ( <liste-de-colonnes> ) ]
<liste-de-colonnes> ::= <colonne> { , <colonne> }
Le privilège objet references autorise à créer des clés étrangères qui référencent la table.
La table User_Tab_Privs (Owner, Grantor, Grantee, Table_Name, Privilege, Grantable) per-
met à Oracle de se souvenir comment ont été accordés les privilèges sur une table et qui les a donnés.
En particulier si un privilège a été accordé à Toto par plusieurs donateurs (grantor), on s’en souvient.

9.3.2 Révoquer des privilèges objet : revoke


Seul celui qui a donné un privilège objet peut le révoquer, avec une révocation en cascade si ce privilège
avait été donné avec grant option.
La révocation :
revoke <liste-de-privilèges-objet> on <objet>
from <liste-utilisateurs-et-ou-r^oles> [ cascade constraint ] ;

<privilèges> ::= <privilège> { , <privilège> }


| all [privileges]
Seul le donateur d’un privilège peut le révoquer. Si cet utilisateur (notons-le U ) avait obtenu ce
privilège with grant option, le privilège est aussi révoqué à tous les utilisateurs auxquels U l’avait
accordé et le même processus est répété récursivement à ces derniers s’ils avaient reçu ce privilège
with grant option.
On ne peut pas révoquer un privilège pour un sous-ensemble de colonnes : on supprime globalement
le privilège.

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 ?

9.4 Les rôles


Les rôles sont très utiles pour regrouper les privilèges nécessaires à l’exercice d’une fonction sur le
système d’information.
Un rôle est un ensemble nommé de privilèges et/ou d’autres rôles. La constitution d’un rôle en sous-
rôles forme un graphe orienté acyclique (DAG, sinon erreur Oracle ORA-01934).
Quand il est activé, un rôle accorde tous les privilèges présents dans le DAG dont il est la racine.
On peut ensuite :
– accorder ou révoquer ce rôle à un utilisateur, exactement comme on le fait avec un privilège,
9.4. LES RÔLES 107

gmi52 gmi51 gmi50


create table Livre ...;

User Tab Privs vue par gmi52 (phase 1)


OWNER GRANTOR GRANTEE TABLE NAME PRIVILEGE GRANTABLE
grant select on Livre
to gmi50, gmi51
with grant option;

Privs vue par gmi52 (phase 2)


User Tab
OWNER GRANTOR GRANTEE TABLE NAME PRIVILEGE GRANTABLE
gmi52 gmi52 gmi50 LIVRE SELECT YES
gmi52 gmi52 gmi51 LIVRE SELECT YES
grant select on gmi52.Livre grant select on gmi52.Livre
to gmi49; to gmi49;
User Tab Privs vue par gmi52 (phase 3)
OWNER GRANTOR GRANTEE TABLE NAME PRIVILEGE GRANTABLE
gmi52 gmi52 gmi50 LIVRE SELECT YES
gmi52 gmi52 gmi51 LIVRE SELECT YES
gmi52 gmi50 gmi49 LIVRE SELECT NO
gmi52 gmi51 gmi49 LIVRE SELECT NO
revoke select on Livre
from gmi51 ;

User Tab Privs vue par gmi52 (phase 4)


OWNER GRANTOR GRANTEE TABLE NAME PRIVILEGE GRANTABLE
gmi52 gmi52 gmi50 LIVRE SELECT YES
gmi52 gmi50 gmi49 LIVRE SELECT NO

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.

Un rôle est un objet :

create role <nom-de-r^


ole> [ identified by <mot-de-passe> ] ;
alter role <nom-de-r^ole> [ identified by <mot-de-passe> ] ;
drop role <nom-de-r^
ole> ;
108 CHAPITRE 9. PRIVILÈGES ET RÔLES

9.5 Gestion des privilèges systèmes et des rôles


9.5.1 Les privilèges système
Ils correspondent à des opérations permettant de modifier la structure de la base de données, par
exemple créer une table, détruire une vue, . . .. Aucun de ces privilèges ne fait référence à un objet
précis de la base.

index pour optimiser des requêtes


procedure
sequence
session connexion au SGBD
synonym
create
table
alter [any]
trigger
drop [any]
type
user
view
cluster, context, database, role
rollback segment, tablespace
...

Du point de vue de grant et revoke, les rôles se comportent comme des privilèges systèmes.

9.5.2 Donner des rôles et/ou des privilèges systèmes : grant


grant <privilèges-système|r^
oles> to
<liste-utilisateurs-et-ou-r^ oles> | public
[ with admin option ] ;

<privilèges-système|r^
oles> ::= <privilège|r^
ole> {, <privilège|r^
ole>}
| all privileges

<privilège> ::= create table|create view|create procedure|create role|...


| drop table | ...
Pas de mémorisation du donateur.

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.

9.5.3 Révoquer des rôles et/ou des privilèges systèmes : revoke


La révocation :
revoke <privilèges-système|r^
oles> from <liste-utilisateurs-ou-r^
oles> ;
Contrairement à ce qui se passe pour les privilèges objet, la révocation des privilèges système n’est
pas transitive.

9.5.4 Les privilèges juste après la connexion


Les privilèges disponibles dès la connexion sont ceux donnés explicitement ainsi que ceux des rôles par
défaut fixés par :
alter user <nom> default role <liste-de-r^
oles> | all | none ;
Pour voir les rôles actuellement activés par gmi52 :
9.5. GESTION DES PRIVILÈGES SYSTÈMES ET DES RÔLES 109

select * from Session_Roles ;


ROLE
----------------------------
GMI
CONNECT
RESOURCE

9.5.5 Gérer ses rôles


Un rôle correspond à une fonction dans l’entreprise : un rôle n’est nécessaire que quand l’utilisateur
assume cette fonction. Une règle de sécurité : ne disposer à tout moment que des privilèges strictement
nécessaires à la tâche en cours.
Dès sa connexion, un utilisateur dispose des privilèges correspondant à ses rôles par défaut.
L’utilisateur doit pouvoir activer un des rôles qui lui ont été attribués et choisir de le désactiver quand
il n’en a plus besoin :
set role <liste-de-r^
oles-identifiés> ;
| none ;
| all [ except <liste-de-r^oles> ] ;

<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) !

9.5.6 Particularités des rôles


Un privilège ne peut être attribué à un rôle avec with grant option.

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 ;

revoke Gerer_Client from Gerer_Tout ;


110 CHAPITRE 9. PRIVILÈGES ET RÔLES

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

La modification d’un rôle actif a un effet immédiat.

La révocation d’un rôle actif n’a pas un effet immédiat !

9.7 Privilèges et sous-programmes stockés


Le code compilé d’un sous-programme stocké ne vérifie plus les droits à la volée (cela certainement
pour des raisons d’efficacité). Donc la compilation, échouera si elle n’est pas capable de garantir que,
lors des futures exécutions, les accès à des objets de la base ou des appels à d’autres sous-programmes
définis par ailleurs seront toujours autorisés.

Dans le cas de l’appel à un sous-programme (par exemple Dbms_Lock.Sleep()) dont le compilateur


ne peut garantir qu’il sera toujours autorisé, le message d’erreur est assez déroutant : il annonce que
Dbms_Lock n’a pas été déclaré.

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.

Connexion de durif Connexion de gmi52


create function Un return Number is
begin
return 1 ;
end Un ;
create function Deux return Number is
begin
return durif.Un + durif.Un ;
end Deux ;

Cette compilation donne l’erreur :

l’identificateur ’DURIF.UN’
doit ^
etre déclaré

Mais la fonction Deux est bien compilée et on la


trouve dans user_objects dans l’état invalide.
grant execute on Un to gmi52 ;
La fonction Deux est toujours invalide, on n’a
cependant pas besoin de la recompiler, on peut
tout de suite l’évaluer :

select Deux from dual ;

qui nous donne bien la valeur 2 et main-


tenant Deux est notée comme valide dans
user_objects.
revoke execute on Un from gmi52 ;
La fonction Deux est maintenant invalide dans
user_objects.

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.

Quelques usages des vues :


– Pour obtenir simplement une information synthétique.
– Pour éviter de divulguer certaines informations (nominative par exemple) : une vue peut restreindre
le nombre de colonnes consultables, l’utilisateur concerné pourra consulter la vue mais pas la ou les
tables d’où elle tire sa valeur.
– Pour assurer l’indépendance du schéma externe vis à vis du schéma interne : on peut espérer qu’une
modification des tables qui implantent la base de données permettra de modifier les requêtes des
vues sans changer le sens des informations qu’elles fournissent.
Une vue est évaluée à chaque consultation.
Oracle en définit un grand nombre pour faciliter la consultation de son dictionnaire, par exemple :
tab, user_objects, . . .

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 Le LDD d’une vue


En Oracle une vue est potentiellement l’équivalent d’une table, c’est à dire que, si la requête de la vue
est assez simple, on pourra mettre à jour la vue (insert, update, delete) ce qui en fait mettra à jour
la table sous-jacente.
Si la requête de la vue est trop complexe (group by par exemple), il est quand même possible de
modifier la base via une vue en lui attachant un trigger instead of.
create [or replace] view <nom-de-vue>
[ ( <liste-alias> ) ]
as <requ^
ete-select>
[ <with-clause> ] ;

<with-clause> ::= with read only


| with check option [ constraint <nom-de-contrainte> ]

drop view <nom-de-vue> ;


with read only interdit toute tentative de modification de la vue (insert, update, delete) ainsi
que l’attachement de trigger instead of.
with check option pour garantir que les insert et update sur la vue ne seront acceptés que s’ils
produisent des lignes que la vue peut sélectionner. Cette option n’a pas de sens si :
– la requête de la vue ou de toute sous-vue utilisée pour construire cette vue contient une
sous-requête
– ou bien si les instructions insert, update, delete sont programmée grâce à un trigger instead
of associé à cette vue.

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) ;

create table Commande (


client references Client (id),
montant 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

10.2 Vues Oracle modifiables


Certaines vues peuvent être l’objet de mise à jour par les instructions insert, update, delete, mais
pour cela il faut que Oracle soit capable de déduire les modifications à faire sur les tables et ce n’est
pas toujours possible. Voici les restrictions imposées par Oracle sur la requête de la vue afin que celle-ci
soit modifiable :
– pas d’opérateurs ensemblistes
– pas de fonction d’agrégation
– pas de clause group by ou order by
– pas de sous-requête
– pas de collection dans un select (objet-relationnel).
Si la vue comporte une jointure, les instructions DML sont très restreintes et ne peuvent concerner
qu’une seule table de base.
Q. 134 Les vues Bon Client et Client Moyenne sont-elles modifiables ?

10.3 Vue modifiable avec with check option


Avec l’option with check option on ne peut insérer que des lignes sélectionnables par la vue :
Insert into Bon_Client values (15, ’martin’, 2000) ; -- OK
Insert into Bon_Client values (20, ’dupont’, 500) ; -- échec : solde > 1000

Q. 135 L’insertion suivante est-elle acceptée ? pourquoi ?

Insert into Bon_Client (id, nom) values (33, ’durant’) ;

10.4 Vue modifiable sans with check option


Sans l’option with check option toute insertion est possible, mais ne sera pas forcément visible via
la vue :
create view Mauvaise_Vue (id, nom, solde) as
select id, nom, solde
from Client
where solde > 1000 ;

insert into Mauvaise_Vue values (45, ’dupont’, 500) ; -- OK

select * from Mauvaise_Vue ; -- on ne voit pas ’dupont’

update Mauvaise_Vue
set solde = 300
where id = 45 ; -- aucune ligne mise à jour

delete from Mauvaise_Vue


where id = 45 ; -- aucune ligne supprimée

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.

10.5 Vue non modifiable : trigger instead of


Si la vue n’est pas modifiable à cause de la complexité de sa requête, on peut lui attacher des triggers
instead of qui s’exécuteront à la place de l’ordre DML.
10.5. VUE NON MODIFIABLE : TRIGGER INSTEAD OF 115

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.

create table Etudiant ( create table Note (


id Number (5) primary key, etudiant references Etudiant (id),
nom Varchar2 (20) note Number (5)
) ; ) ;
Soit la vue non modifiable à cause, entre autres, de l’utilisation de avg :
create view Moyenne (id, nom, moyenne) as
select e.id, e.nom, nvl (to_char (avg (n.note)), ’pas de note’)
from Etudiant e left outer join Note n on e.id = n.etudiant
group by e.id, e.nom ;
On veut qu’un ordre DML sur la vue Moyenne se traduise par un ordre DML similaire sur la table
Etudiant :
– un insert insère simplement le nouvel étudiant dans la table Etudiant,
– un update met à jour uniquement le nom de l’étudiant (on pourrait aussi tenter de mettre à jour
son id mais cela poserait des problèmes à cause de la clef étrangère de Note)
– un delete supprime les notes de l’étudiant old.id puis l’étudiant.
create trigger DML_sur_Moyenne
instead of insert or delete or update
on Moyenne
for each row
begin
if inserting then
insert into Etudiant values (:new.id, :new.nom) ;
elsif updating (’nom’) then
update Etudiant set nom = :new.nom where id = :old.id ;
elsif deleting then
delete from Note where etudiant = :old.id ;
delete from Etudiant where id = :old.id ;
end if ;
end ;
Soit la vue non modifiable :
create or replace view Tout (etudiant, nom, note) as
select e.id as etudiant,
e.nom as nom,
n.note as note
from Etudiant e
left outer join Note n on n.etudiant = e.id ;

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

un delete n’ait aucun effet.


Q. 137 Implanter le trigger qui fait ce travail.

10.6 Deux mots à propos de Postgres 8.2.1


En Postgres, on peut modifier les tables sous-jacentes aux vues en créant une règle (create rule). Une
règle permet d’exécuter des commandes supplémentaires lorsqu’une commande donnée est exécutée
sur une table ou une vue donnée (also) ou à la place de la commande (instead).
create rule Creer_Etudiant as
on insert to Moyenne
do instead Insert into Etudiant values (new.id, new.nom) ;

create rule Modifier_Etudiant as


on update to Moyenne
do instead update Etudiant set nom = new.nom where id = old.id ;

create rule Supprimer_Etudiant as


on delete to Moyenne
do instead (delete from Note where etudiant = old.id ;
delete from Etudiant where id = old.id ) ;
Quatrième partie

Optimisations

117
Chapitre 11

Optimisations

11.1 Organisation physique d’un SGBD


La durabilité d’une base de données est assurée par son enregistrement sur un disque magnétique
(c’est probablement actuellement la technique la plus utilisée).

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.

11.2 Optimisations algébriques


11.2.1 Introduction
On s’intéresse à réduire le plus possible le nombre d’entrées/sorties sur le disque, les mesures de per-
formances se feront en nombre de lectures ou écritures sur le disque.

Nous allons nous intéresser particulièrement aux transformations algébriques et à la recherche de


chemins d’accès (utilisation d’index par exemple). Les possibilités offertes par Oracle seront ensuite
examinées.
Soit par exemple :
create table Etudiant (
id Number (5),
nom Varchar2 (20),
constraint Etudiant_PK primary key (id)
) ;

118
11.2. OPTIMISATIONS ALGÉBRIQUES 119

create table Inscription (


etudiant Number (5) references Etudiant (id),
matiere Varchar2 (3),
constraint Inscription_PK primary key (matiere, etudiant)
);

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.

11.2.2 Optimiser par des manipulations algébriques


Principalement : (Gardarin [8] p.315)
1. σP (A × B) = A ⊲⊳P B
120 CHAPITRE 11. OPTIMISATIONS

2. Commutativité des jointures : R ⊲⊳P S = S ⊲⊳P R


3. Associativité des jointures : (R ⊲⊳P S) ⊲⊳Q T = R ⊲⊳P (S ⊲⊳Q T )
4. Fusion des projections : ΠA1 ,...,Ak (ΠB1 ,...,Bl (R)) = ΠA1 ,...,Ak (R)
5. Regroupement ou dégroupement et commutativité des restrictions : σP (σQ (R)) = σP ∧Q (R) =
σQ∧P (R) = σQ (σP (R))
6. Quasi-commutativité des restrictions et projections : ΠA1 ,...,Ak (σP (R)) = σP (ΠA1 ,...,Ak (R)) si les
attributs A1 , . . . , Ak de R forment un sur-ensemble de ceux utilisés dans le prédicat P
7. Quasi-commutativité des restrictions et jointures : σP (R ⊲⊳Q S) = (σP (R)) ⊲⊳Q S si le prédicat
P porte uniquement sur des attributs de R
8. Distributivité des restrictions sur les unions, intersections ou différences : σP (R1 ∪R2 ) = σP (R1 )∪
σP (R2 )
9. Quasi-commutativité des projections et jointures : ΠA1 ,...,Ak (R ⊲⊳P S) = ΠA1 ,...,Ak (R) ⊲⊳P S si les
attributs A1 , . . . , Ak de R forment un sur-ensemble de ceux utilisés dans le prédicat de jointure
P.
10. Commutativité des projections avec les unions.
Q. 138 Dessiner l’arbre relationnel de la requête suivante puis le transformer pour qu’il soit plus
efficace (p(x) ∧ (x = y)) ≡ (p(x) ∧ (x = y) ∧ p(y)) :

select e.nom, i.matiere


from Etudiant e
inner join Inscription i on e.id = i.etudiant
where e.id between 20 and 50
and e.nom like ’%rr%’
and i.matiere in (’BDD’, ’CL’, ’SYS’) ;

11.3 Accès aux données sans index


Rappel : la page est l’unité d’entrée/sortie sur disque, c’est à dire que, quelle que soit la taille de
la valeur qu’on souhaite lire ou écrire sur le dique, le système (si la page n’est pas déjà en mémoire
tampon) lira ou écrira complètement la page qui contient cette valeur.
Une page est en général constituée d’un certain nombre de secteurs disque. Par exemple en Oracle
une page fait 4 kilo-octets.
Soit la table :
create table Employe (
id Number (5) primary key,
nom Varchar2 (20),
salaire Number (10, 2),
dpt Number (5)
) ;

dont la clef primaire est constituée de la colonne id.


Suposons qu’un SGBD naı̈f implémente cette table par un simple fichier linéaire contenant la liste
des employés et le fait qu’id étant la clef primaire doit être unique, mais aucune autre information.
Supposons que Employe contienne n lignes et qu’une page contiennent en moyenne p employés (le
nombre de pages de la table Employe sera alors de ⌈n/p⌉).
Lors de la requête suivante :
select e.nom from Employe e where e.id = 16 ;

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.

11.4 Notions de base sur les B+-arbres à clefs uniques


Voir “Introduction à l’algorithmique” de T. Cormen, C. Leiserson et R. Rivest chez Dunod.

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.

C1 C2 C3 ........ Cn espace libre

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.

C1 C2 ........ Ck espace libre

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 ).

11.4.1 Propriétés invariantes d’un B+-arbre


1. L’ensemble des clefs est muni d’une relation d’ordre total qui sert à maintenir la structure du
B+-arbre.
2. La capacité T en nombre de clefs d’un nœud interne doit être impaire avec T = 2K + 1 et K ≥ 1.
Un nœud interne doit pouvoir accueillir au moins 3 clefs.
3. tout nœud interne (sauf la racine) pouvant accueillir 2 ∗ K + 1 clefs doit toujours contenir au
moins K clefs (et donc K + 1 sous-arbres).
4. la racine n’a pas cette contrainte dans la mesure où elle peut être l’unique feuille et contenir une
seule clef avec son rowid.
5. De plus l’équilibre du B+-arbre (toutes les feuilles sont à la même profondeur) est dû au fait
que le B+-arbre croı̂t ou décroı̂t en hauteur par sa racine.
6. Deux clefs successives Ci et Ci+1 d’un nœud interne ou d’une feuille vérifient Ci < Ci+1
7. Les clefs C du sous-arbre situé à gauche de Ci vérifient Ci−1 < C ≤ Ci .

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. 141 Nombres minimaux et maximaux de sous-arbres d’un aiguillage avec T = 51 clefs ?

Q. 142 Une clef Ci d’un aiguillage peut-elle apparaı̂tre dans les aiguillages de Bi ?

Q. 143 Que peut-on dire de Ci par rapport à son sous-arbre gauche Bi ?

11.4.2 Algorithme de recherche d’un élément connaissant sa clef C


Pour trouver la feuille susceptible de contenir une clef C donnée (ou se rendre compte que C n’existe
pas dans le B+-arbre), la recherche commence par la racine du B+-arbre. Soit x la variable contenant
la page disque correspondant au nœud courant :
1. x ← lire (racine)
2. tant que x est un aiguillage : x ← lire (Bi ) avec Ci−1 < C ≤ Ci ,
3. x est une feuille : soit C s’y trouve : il faut lire le bloc table pour y trouver la ligne, soit C n’est
pas dans le B+-arbre et est donc absent de la table.
Coût dans le pire des cas : au plus 1 + ⌈logK+1 (⌈n/2⌉)⌉ lectures de page.
Les propriétés 3 et 5 page 122 garantissent que, dans le pire des cas — i.e. la racine contient une
clef et a donc deux fils et chaque nœud interne contient K clefs et a donc K + 1 fils — l’accès à un
élément de clef donnée se fera en au plus 1+⌈logK+1 (⌈n/2⌉)⌉ lectures de page. Voici quelques exemples
pour différentes valeurs de K et n (le nombre d’éléments de la table), à comparer aux 5.000 lectures
nécessaires si on ne dispose pas d’index !

Nombre de pages lues ou profondeur du B+-arbre, n est le nombre de lignes


pire des cas meilleur des cas
nœud le moins plein possible nœud le plus plein possible
K n = 1.000 n = 1.0002 n = 1.0003 n = 1.000 n = 1.0002 n = 1.0003
1 10 20 30
3 6 11 16
7 4 8 11
31 3 5 7
63 3 5 6
127 3 4 6

Fig. 11.3 – n est le nombre de lignes de la table.

Quelques rappels sur les fonctions logarithme :


propriété exemple d’application
log(ab) = log(a) + log(b) log(500.000) = log(2) + log(250) + log(1000)
log(ap ) = p log(a)
loga (n) = logb (n)/ logb (a) logK (n) = log2 (n)/ log2 (K)
⌈log2 (1.000)⌉ = 10

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

11.4.3 Un algorithme simple d’insertion


Une technique assez simple, mais peut-être pas très optimale, pour insérer d’un nouvel élément (clef,
valeur) consiste, lors de la descente dans l’arbre pour trouver la feuille d’insertion, à exploser chaque
nœud plein (i.e. contenant donc T = 2K + 1 clefs) en deux nœuds à moitié pleins (contenant chacun
K clefs), la clef du milieu CK+1 remonte dans le nœud père :

père non plein


PI PI+1 PI CK+1 PI+1
explosion
fils plein
C1 ... CK CK+1 CK+2 ... CT C1 ... CK libre CK+2 ... CT libre

B1 B2 BK BK+1 BK+2 BK+3 BT BT+1 B1 B2 BK BK+1 BK+2 BK+3 BT BT+1

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 ?

11.4.4 Un exemple avec une clef atomique


Voici un exemple de B+-arbre associé à une table ayant des lignes du genre (34, "nom") où 34 est la
clef.

B+−ARBRE (ou INDEX) 30 44

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. 150 Les deux utilisations précédentes du B+-arbre paraissent-elles intéressantes ? ne vaudrait-il


pas mieux parcourir directement la table sans passer par son B+-arbre ?

Q. 151 À quelle condition l’utilisation du B+-arbre pourrait-elle devenir intéressante en particulier


pour la question Q.148 ?

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].

11.4.5 Un exemple avec une clef composée


Ici les lignes de la table sont de la forme (matière, enseignant, volumeHoraire) et la clef (matière,
enseignant) est composée des deux premiers attributs d’un élément. Par exemple (BD, 22) est la clef
de l’élément (BD, 22, 45).
Voici un B+-arbre contenant ces informations avec K = 1 :

BD, 7 IA, 3

BD, 3 CL, 3 CL, 22 CL, 30 PI, 1

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.

11.4.6 B+-arbre à clef non unique ou index catégoriel


Les index permettant de retrouver efficacement plusieurs éléments appartenant à une même catégorie
sont eux aussi très utiles.
On peut, par exemple, regrouper des employés par service, des disques par éditeur ou des étudiants
par année de naissance.
Q. 163 Donner les propriétés d’un B+-arbre acceptant la multiplicité des clefs en s’inspirant de celles
du B+-arbre à clef unique.

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. 166 Algorithme pour retrouver les éléments de clef C.

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 ?

11.4.7 Plusieurs index B+-arbre sur une même table


Il est bien entendu possible d’avoir plusieurs index B+-arbre associés à une table.

11.4.8 SQL et index


Les contraintes primary key et unique demandent implicitement au SGBD de créer les index uniques
correspondant.

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.

11.5 Oracle et les plans d’exécution : Explain plan


Pour chaque requête (ou instruction DML), Oracle va utiliser un certain nombre de techniques d’opti-
misation pour calculer un plan d’exécution qui soit le meilleur possible (mais pas forcément optimal).
Le résultat de cette optimisation dépend, entre autres, des index disponibles, des contraintes d’intégrité
(par exemple le fait qu’une colonne soit une clef étrangère autorise à utiliser l’index de clef lors d’une
équi-jointure se faisant sur cette colonne et la table référencée) et aussi des connaissances statistiques
(par exemple si une des tables d’une jointure est très petite on peut la charger une fois pour toutes en
mémoire centrale et lire une seule fois l’autre table) dont dispose le SGBD au moment où il calcule le
plan d’exécution.
Le but de la commande explain plan est principalement de permettre au programmeur de voir de
quelle manière le SGBD va exécuter un ordre DML et donc de voir les défauts éventuels de ce plan
d’exécution. Le programmeur peut améliorer les choses par une reconception des index, des suggestions
d’optimisation (hint) faites au SGBD, une réécriture des requêtes, . . .
11.5. ORACLE ET LES PLANS D’EXÉCUTION : EXPLAIN PLAN 127

11.5.1 Table sans index


Employe
id nom dpt
create table Employe (
id Number (5), 6 jules 2
nom Varchar2 (20), 4 sophie 1
salaire Number (10, 2), 2 paul 3
dpt Number (5) 1 marc 2
) ; 7 léa 3
5 marie 2

 
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 :

Id Operation Name Rows Bytes Cost (%CPU)


0 SELECT STATEMENT 1 25 2 (0)
*1 TABLE ACCESS FULL EMPLOYE 1 25 2 (0)
1 - filter("E"."ID"=4)

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.

Coût en nombre d’accès disque :


– Le coût est principalement lié aux accès disque
– Ne nombre de nuplets de la table Employe
– Ep nombre moyen de nuplets employé par page (bloc disque)
– ⌈Ne /Ep ⌉ nombre d’accès disque.
Si Ne = 100.000, Ep = 20 alors le nombre d’accès disque est de 5000.

11.5.2 Table avec index unique sur la clef primaire


Par défaut pour une clef, Oracle crée un index en B+arbre, la colonne la plus à gauche de la clef est
celle de poids fort, la plus à droite est celle de poids faible (comme dans les notations numériques).
Employe
id nom dpt
Employe_PK 6 jules 2
alter table Employe add 4 sophie 1
(constraint Employe_PK 2 paul 3
primary key (id)) ; 4
1 marc 2
7 léa 3
5 marie 2

 
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 :

Id Operation Name Rows Bytes Cost(%CPU)


0 SELECT STATEMENT 1 25 1 (0)
1 TABLE ACCESS BY INDEX ROWID EMPLOYE 1 25 1 (0)
*2 INDEX UNIQUE SCAN EMPLOYE_PK 1 1 (0)
2 - access("E"."ID"=4)

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

directement la ligne de l’employé 4 dans la table Employe.

Coût en nombre d’accès disque :


– Ne nombre de nuplets de la table Employe
– K nombre minimum de clefs par nœud du Barbre de l’index
– 1 + ⌈logK+1 (⌈Ne /2⌉)⌉ nombre de nœud d’index à lire pour obtenir le rowid de l’employé 4
– 1 nombre de page à lire pour obtenir l’employé dont on a obtenu le rowid.
– 2 + ⌈logK+1 (⌈Ne /2⌉)⌉ nombre d’accès disque.
Si Ne = 100.000, K = 50 alors le nombre maximum d’accès disque est de 5. Ce qui est nettement
meilleur que précédemment ! Notons que les performances sont identiques quel que soit l’employé re-
cherché (la valeur de e.id pourrait n’être connue qu’à l’exécution).

11.5.3 Table avec index non unique sur le département


 
 
Évidemment, la requête select * from Employe e where e.dpt = 2 ; doit explorer toute la table.
Voici son plan d’exécution :

Id Operation Name Rows Bytes Cost(%CPU)


0 SELECT STATEMENT 1 51 2 (0)
*1 TABLE ACCESS FULL EMPLOYE 1 51 2 (0)
1 - filter("E"."DPT"=2)

Création d’un index :


create [unique] index <nom-index> on <nom-table> (<liste-colonnes-ou-expression>)
Si unique n’est pas mentionné l’index acceptera des occurrences multiples de la même clef. Pour
chaque colonne on peut choisir l’ordre croissant (asc) ou décroissant (desc).
Pour introduire un index pour des raisons d’optimisation, Oracle recommande d’utiliser explicitement
create [unique] index plutôt que d’introduire une contrainte d’unicité.

PostgreSQL propose les mêmes fonctionnalités.

On ajoute l’index non unique Employe_Dpt_Index sur les départements.

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 :

Id Operation Name Rows Bytes Cost(%CPU)


0 SELECT STATEMENT 34 1734 4 (0)
1 TABLE ACCESS BY INDEX ROWID EMPLOYE 34 1734 4 (0)
*2 INDEX RANGE SCAN EMPLOYE_DPT_INDEX 34 1 (0)
2 - access("E"."DPT"=2)
11.5. ORACLE ET LES PLANS D’EXÉCUTION : EXPLAIN PLAN 129

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 !

11.5.4 Table avec index unique sur une clef candidate


Il s’agit de la contrainte Unique.

11.5.5 L’ordre des colonnes d’un index a son importance


Que l’index soit unique ou non, l’ordre dans lequel on écrit les colonnes constituant sa clef peu avoir
des conséquences sur les performances.
Soit par exemple :
create table X (
a1 Number (5),
a2 Number (5),
a3 Number (5),
a4 Number (5),
constraint X_PK primary key (a1, a2, a3, a4)
) ;

Pour toute clause where (ou on pour les jointures) :


– si la condition porte au moins sur la colonne a1, Oracle utilisera l’index pour retrouver efficacement
les tuples.
– si la condition ne porte pas au moins sur la colonne a1, Oracle ne pourra pas utiliser l’index.
Ceci s’explique par le fait que, pour Oracle (ainsi que pour Postgres), le poids des colonnes constituant
la clef d’accès de l’index décroı̂t de gauche à droite.

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

11.5.6 Relativiser l’importance des index


En simplifiant, on peut dire que les index sont particulièrement important pour les grosses tables.
En effet les petites tables susceptibles de tenir complètement en mémoire centrale n’ont peut-être pas
besoin d’index.

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.

11.6 Représentation graphique


L’optimisation d’une BD est un sujet extrêmement important, elle peut permettre d’accélerer considérablement
l’exécution de certaines instructions DML.

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)
--- -------- ---

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 ;

Id Operation Name Rows Bytes Cost(%CPU)


0 SELECT STATEMENT 1 37 4 (0)
1 NESTED LOOPS 1 37 4 (0)
2 NESTED LOOPS 1 19 3 (0)
3 TABLE ACCESS FULL ENVOI 1 8 2 (0)
4 TABLE ACCESS BY INDEX ROWID CLIENT 1 11 1 (0)
*5 INDEX UNIQUE SCAN CLIENT_PK 1 0 (0)
6 TABLE ACCESS BY INDEX ROWID PRODUIT 1 18 1 (0)
*7 INDEX UNIQUE SCAN PRODUIT_PK 1 0 (0)
5 - access("C"."CDC"="E"."CDC"), 7 - access("P"."CDP"="E"."CDP")

Q. 170 Quelles sont les contraintes qui peuvent expliquer le TABLE ACCESS FULL sur la table ENVOI ?

Q. 171 À quoi correspondent les lignes d’Id 3, 4 et 5 dans la requête ?

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.

11.6.1 Comment lire un tel plan d’exécution


Un plan d’exécution correspond à une hiérarchie de phases d’évaluation. La profondeur d’une ligne
dans cette hiérarchie est proportionnelle à son indentation.

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 :

Select statement 8 faire la projection du triplet (C, E, P)


Nested loops 7 concaténer (C, E) et P en (C, E, P)
Nested loops 4 concaténer C et E en (C, E)
Table access full : Envoi 1 prendre chaque envoi E
Table access by index rowid : Client 3 prendre les informations de C
Index unique scan : Client_PK 2 trouver le rowid du client C de E
Table access by index rowid : Produit 6 prendre les informations de P
Index unique scan : Produit_PK 5 trouver le rowid du produit P de E

Il est aussi possible de représenter graphiquement cette hiérarchie, voir la figure 11.4.

Fig. 11.4 – Représentation graphique du plan d’exécution

Nested Loops
(cdc,cdp, nom)

Nested Loops Table Access


(cdc,cdp) By Index RowId
Table Access 5 Produit
Table Access By Index RowId rowid
Full 3 Client Index
1 Envoi rowid Unique Scan
Index 4 Produit_PK
Unique Scan
2 Client_PK Utiliser ce ROWID pour retrouver
rapidement le client C dans la table Client et fournir la concaténation (E, C) à l’étage supérieur.

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

Id Operation Name Rows Bytes Cost(%CPU)


0 SELECT STATEMENT 1 37 3 (0)
1 NESTED LOOPS 1 37 3 (0)
2 NESTED LOOPS 1 19 2 (0)
3 TABLE ACCESS BY INDEX ROWID CLIENT 1 11 1 (0)
*4 INDEX UNIQUE SCAN CLIENT_PK 1 1 (0)
*5 INDEX RANGE SCAN ENVOI_PK 1 8 1 (0)
6 TABLE ACCESS BY INDEX ROWID PRODUIT 1 18 1 (0)
*7 INDEX UNIQUE SCAN PRODUIT_PK 1 0 (0)
4 - access("C"."CDC"=’A3’), 5 - access("E"."CDC"=’A3’)
7 - access("P"."CDP"="E"."CDP")

Q. 173 Pourquoi ce plan ne part-il plus pas de la table Envoi mais de la table Client ?

Q. 174 Pourquoi l’id 5 indique-t-il un INDEX RANGE SCAN sur ENVOI PK ?

Q. 175 Dessiner la hiérarchie d’opérations de ce plan d’exécution.

Q. 176 Quel serait le nouveau plan d’exécution si la projection devenait select p.libelle, p.couleur

11.6.2 Quelques opérations d’un plan d’exécution


11.7. EXEMPLES DE PLAN D’EXÉCUTION 133

opération option description


clause group by regrouper les éléments du même groupe
SORT GROUP BY par un tri sur les valeurs des expressions définissant le re-
groupement
Un tri des nuplets d’une relation préalable à une jointure
JOIN
par fusion : MERGE JOIN
ORDER BY clause order by
tri afin d’éliminer les doublons (clause distinct par
UNIQUE
exemple).
AGGREGATE application d’une fonction d’aggrégation
VIEW calcul d’une sous-requête
FILTER par exemple les where et having
clause ORDER BY sur un index : il suffit de balayer l’index
INDEX FULL SCAN
pour obtenir les nuplets dans l’ordre : peut éviter un tri.
n’accède pas à la table sous-jacente, rapide mais pas dans
FAST FULL SCAN
un ordre particulier
recherche sur un index non unique ou sur les
premières colonnes d’un index (pas toutes, sinon il
RANGE SCAN
s’agit de UNIQUE SCAN). Les clefs identiques sont triées
sur leurs ROWIDs croissants.
UNIQUE SCAN recherche sur toutes les colonnes d’un index à clef unique
jointure par fusion de listes ordonnées au préalable (quand
MERGE JOIN la condition de jointure ne porte pas sur des colonnes clef),
voir l’opération de type set SORT JOIN
Quand une des tables jointes n’a aucune condition de join-
CARTESIAN
ture
construction en mémoire d’un hachage sur les clefs de join-
HASH JOIN ture de la plus petite relation de la jointure (la première
fille), l’autre relation est ensuite balayée complètement.
calcul d’une jointure : la première table (boucle externe)
NESTED LOOPS est balayée intégralement, la seconde (interne) est accédée
efficacement par exemple par une de ses clefs.
PROJECTION sous produit de UNION, MINUS et INTERSECTION
on trouve le nuplet connaissant son adresse (ou ROWID) :
TABLE ACCESS BY INDEX ROWID
datafile, page, position
balayage complet de la table, peut être efficace si la table
FULL
est petite.
UNION opération UNION
UNION-ALL opération UNION ALL

11.7 Exemples de plan d’exécution


11.7.1 Une vue est intégrée dans la requête qui l’utilise
Soit la vue Bons_Clients qui calcule les clients ayant un envoi pour chaque produit :
create view Bons_Clients_1 as
select c.cdc, c.loc
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, c.loc, p.Nb_Produits
134 CHAPITRE 11. OPTIMISATIONS

having Count (distinct e.cdp) = p.Nb_Produits ;

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 ;

Id Operation Name Rows Bytes Cost (%CPU)


0 SELECT STATEMENT 10 380 9 (23)
*1 FILTER
2 SORT GROUP BY 10 380 9 (23)
*3 HASH JOIN 10 380 8 (13)
4 MERGE JOIN CARTESIAN 10 210 4 (0)
5 VIEW 1 13 2 (0)
6 SORT AGGREGATE 1
7 INDEX FAST FULL SCAN PRODUIT_PK 3 2 (0)
8 INDEX FAST FULL SCAN ENVOI_PK 10 80 2 (0)
9 TABLE ACCESS FULL CLIENT 4 68 3 (0)
1 - filter(”P”.”NB PRODUITS”=COUNT(DISTINCT ”E”.”CDP”))
3 - access(”C”.”CDC”=”E”.”CDC”)

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 ;

Le plan de la requête select * from Bons_Clients_2 ; est :


Id Operation Name Rows Bytes Cost (%CPU)
0 SELECT STATEMENT 10 250 5 (20)
*1 FILTER
2 SORT GROUP BY 10 250 5 (20)
3 NESTED LOOPS 10 250 4 (0)
4 MERGE JOIN CARTESIAN 10 210 4 (0)
5 VIEW 1 13 2 (0)
6 SORT AGGREGATE 1
7 INDEX FAST FULL SCAN PRODUIT_PK 3 2 (0)
8 INDEX FAST FULL SCAN ENVOI_PK 10 80 2 (0)
*9 INDEX UNIQUE SCAN CLIENT_PK 1 4 0 (0)
1 - filter("P"."NB PRODUITS"=COUNT(DISTINCT "E"."CDP"))
9 - access("C"."CDC"="E"."CDC")

Q. 179 Dessiner la hiérarchie d’opérations de ce plan d’exécution.

Q. 180 En quoi ce nouveau plan est-il meilleur que le précédent ?


Voici une requête plus complexe et son plan d’exécution :
11.7. EXEMPLES DE PLAN D’EXÉCUTION 135

select distinct l.ville


from Localite l
natural join Bons_Clients_1
where l.dpt = 59 ;

Id Operation Name Rows Bytes Cost (%CPU)


0 SELECT STATEMENT 9 702 12 (17)
1 SORT UNIQUE 9 702 12 (17)
*2 FILTER
3 SORT GROUP BY 9 702 12 (17)
*4 HASH JOIN 9 702 11 (10)
5 MERGE JOIN CARTESIAN 20 1220 7 (0)
6 MERGE JOIN CARTESIAN 2 106 5 (0)
7 VIEW 1 13 2 (0)
8 SORT AGGREGATE 1
9 INDEX FAST FULL SCAN PRODUIT_PK 3 2 (0)
*10 TABLE ACCESS FULL LOCALITE 2 80 3 (0)
11 BUFFER SORT 10 80 4 (0)
12 INDEX FAST FULL SCAN ENVOI_PK 10 80 1 (0)
13 TABLE ACCESS FULL CLIENT 4 68 3 (0)
2 - filter("P"."NB PRODUITS"=COUNT(DISTINCT "E"."CDP"))
4 - access("L"."LOC"="C"."LOC" AND "C"."CDC"="E"."CDC")
10 - filter("L"."DPT"=59)

Ici aussi la définition de la vue et la requête principale se mélangent.

11.7.2 Les instructions DML


Les instructions de mise à jour sont aussi l’objet d’un plan d’exécution. Par exemple la mise à jour
qui augmente le bonus des bons clients :
update Client c
set bonus = bonus + 10
where exists (select b.cdc from Bons_Clients_2 b where c.cdc = b.cdc) ;

Id Operation Name Rows Bytes Cost (%CPU)


0 UPDATE STATEMENT 4 84 9 (23)
1 UPDATE CLIENT
*2 HASH JOIN SEMI 4 84 9 (23)
3 TABLE ACCESS FULL CLIENT 4 68 3 (0)
4 VIEW VW_SQ_1 10 40 5 (20)
5 VIEW BONS_CLIENTS_2 10 40 5 (20)
*6 FILTER
7 SORT GROUP BY 10 250 5 (20)
8 NESTED LOOPS 10 250 4 (0)
9 MERGE JOIN CARTESIAN 10 210 4 (0)
10 VIEW 1 13 2 (0)
11 SORT AGGREGATE 1
12 INDEX FAST FULL SCAN PRODUIT_PK 3 2 (0)
13 INDEX FAST FULL SCAN ENVOI_PK 10 80 2 (0)
*14 INDEX UNIQUE SCAN CLIENT_PK 1 4 0 (0)
2 - access("C"."CDC"="CDC")
6 - filter("P"."NB PRODUITS"=COUNT(DISTINCT "E"."CDP"))
14 - access("C"."CDC"="E"."CDC")

Ici la vue Bons_Clients_2 est effectivement utilisée telle quelle.


136 CHAPITRE 11. OPTIMISATIONS

11.8 Prise en compte des statistiques


Soit la requête :
update Client c set bonus = bonus + 10
where c.cdc between ’A1’ and ’A10’ ;

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)
) ;

La requête suivante peut-elle utiliser l’index de la clef primaire e.id ?


select e.nom, e.salaire from Employe e where abs (e.id) = 7 ;
Non, car ne connaissant pas la sémantique de la fonction abs, l’optimiseur ne peut en déduire la
ou les valeurs que doit avoir e.id pour que le prédicat abs (e.id) = 7 soit vérifié. Il ne pourra
donc pas utiliser l’index de clef primaire et effectuera un parcours complet de la table Employe !

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 !

La requête suivante ne pourra donc pas utiliser l’index :


select * from T where B = 3 and C between 5 and 100 ;

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 !

11.10 Les commandes Oracle et Postgres


11.10.1 Oracle : Explain Plan for ...<ordre DML>
Permet de ranger dans une table le plan d’exécution adopté par le moteur SQL pour exécuter l’ordre
DML passé en paramètre.

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.

11.10.2 Postgres : Explain <ordre DML>


Ressemble à ce que propose Oracle.
138 CHAPITRE 11. OPTIMISATIONS

11.10.3 Les statistiques


Elles influencent les choix de l’optimiseur : les choix ne seront généralement pas les mêmes suivant
que l’on pense travailler avec de petites ou de grandes tables, voir 11.8 page 136. Il faut donc qu’elles
soient à jour.

La constitution des statistiques est faite explicitement par l’administrateur :


Oracle ANALYZE mais les statistiques utilisées par l’optimiseur sont produites par le paquetage
DBMS_STATS, par exemple :
begin DBMS_STATS.GATHER_TABLE_STATS (’durif’, ’essai’) ; end ;
begin DBMS_STATS.GATHER_Schema_STATS (’durif’) ; end ;

select TABLE_NAME, NUM_ROWS, BLOCKS, LAST_ANALYZED


from User_Tables ;
select INDEX_NAME, INDEX_TYPE, TABLE_NAME, LEAF_BLOCKS,DISTINCT_KEYS
from User_Indexes ;

en Postgres ANALYZE

11.10.4 Oracle : suggestions d’optimisation faites par le programmeur (hints)


Le programmeur peut donner des indications d’optimisation sous forme d’un commentaire suivant
immédiatement le nom de l’ordre DML.
Par exemple la suggestion FULL demande une exploration complète de la table mentionnée en (Table Access Full).

Voici une requête sans suggestion d’optimisation et son plan d’exécution :


select 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 ;

Id Operation Name Rows Bytes Cost (%CPU)


0 SELECT STATEMENT 10 520 13 (16)
*1 HASH JOIN 10 520 13 (16)
*2 HASH JOIN 10 410 9 (12)
*3 HASH JOIN 10 230 6 (17)
4 TABLE ACCESS FULL CAMION 2 22 3 (0)
5 INDEX FAST FULL SCAN ENVOI_PK 10 120 2 (0)
6 TABLE ACCESS FULL PRODUIT 3 54 3 (0)
7 TABLE ACCESS FULL CLIENT 4 44 3 (0)
1 - access("E"."CDC"="C"."CDC")
2 - access("P"."CDP"="E"."CDP")
3 - access("CM"."CDM"="E"."CDM")

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

Id Operation Name Rows Bytes Cost (%CPU)


0 SELECT STATEMENT 10 520 19 (0)
1 NESTED LOOPS 10 520 19 (0)
2 MERGE JOIN CARTESIAN 24 960 19 (0)
3 MERGE JOIN CARTESIAN 8 176 9 (0)
4 TABLE ACCESS FULL CLIENT 4 44 3 (0)
5 BUFFER SORT 2 22 6 (0)
6 TABLE ACCESS FULL CAMION 2 22 2 (0)
7 BUFFER SORT 3 54 18 (0)
8 TABLE ACCESS FULL PRODUIT 3 54 1 (0)
*9 INDEX UNIQUE SCAN ENVOI_PK 1 12 0 (0)
9 - access("E"."CDC"="C"."CDC" AND "P"."CDP"="E"."CDP" AND "CM"."CDM"="E"."CDM")

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.

Il y a beaucoup d’autres suggestions possibles, en voici quelques-unes :


– ALL_ROWS optimise la consommation globale de ressources : approprié pour des exécutions en arrière-
plan (batch).
– FIRST_ROWS (<entier n>) optimise le temps de réponse pour produire les n premiers nuplets :
convient plutôt à une utilisation interactive.
– FULL (<nom-de-table-ou-alias>) demande à ne pas utiliser l’index éventuel (d’où un parcours
complet de la table).
– INDEX (<nom-de-table-ou-alias>) demande à utiliser l’index de la table (s’il y a plusieurs index,
on peut préciser les noms de ceux qu’on souhaite utiliser).
– ORDERED demande à effectuer la jointure en respectant l’ordre d’apparition des tables dans la clause
from.

11.11 Les groupes (clusters) : groupement de tables pré-jointes


Pour regrouper des tables souvent inter-jointes et ayant des colonnes de même sémantique (des clefs
étrangères vers des clefs primaires par exemple).
Un cluster est principalement défini par sa clef, formée d’au moins une colonne :
create cluster <nom-cluster> ( <dcl-colonne> { , <dcl-colonne> } ) index ;

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> ;

cet index utilise toutes les colonnes de la clef du cluster.


Par exemple on sait qu’on va souvent faire des équi-jointures entre les tables Adherant et Livre :
140 CHAPITRE 11. OPTIMISATIONS

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) ;

create table Livre (


id Number (5),
titre varchar2 (20),
emprunteur Number (5) references Adherant (id),
constraint Livre_PK primary key (id)
) cluster Emprunt (emprunteur) ;
3. créer l’index du cluster :
create index Index_Emprunt on cluster Emprunt ;
une fois cet index créé, on peut maintenant manipuler le contenu des tables.
id nom id titre emprunteur
4. Par exemple si les tables contiennent : ... 3 Louis XI 11
11 toto 5 Galilée 11
ces lignes figureront dans un seul bloc disque : 11 toto 3 Louis XI 5 Galilée

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 ?

11.12 Enrichir un index avec des colonnes d’autonomie


Ajouter dans l’index toutes les colonnes référencées par les requêtes, ainsi on n’a plus besoin d’accéder
à la table !
Mais attention cela peut faire diminuer le nombre d’éléments par feuille et faire grossir l’index en
nombre de blocs.

11.13 Tables organisées en index (IOT) : lignes ordonnées


C’est l’approche extrême de celle définie en 11.12
create table Adherant (
id Number (5),
nom varchar2 (20),
constraint Adherant_PK primary key (id)
) organization index ;
L’organization par défaut est heap (tas) qui indique que les lignes sont stockées sans ordre particu-
lier.
L’organization index fait que les lignes sont dans un index défini sur la clef primaire. La contrainte
de clef primaire est donc obligatoire dans ce cas. Cela peut rendre les requêtes utilisant cet ordre bien
11.13. TABLES ORGANISÉES EN INDEX (IOT) : LIGNES ORDONNÉES 141

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

12.1 Notion de transaction


Une transaction est une (la plus petite) unité logique de travail qui fait passer la base d’un état correct
dans un nouvel état correct, par exemple le virement d’une somme d’un compte à un autre compte
dans la même banque ne doit pas changer la somme des comptes. En général une transaction prend
peu de temps pour s’exécuter (ajouter un achat, changer le nom d’un client, faire un virement ban-
caire de compte à compte), sauf dans le cas de transactions destinées à fabriquer une synthèse de l’état
complet de la base (décisionnel).

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.

12.2 Notion de session


On appellera session la période qui commence à la connexion d’un utilisateur ou d’un programme
et se termine lors de sa déconnexion. Pendant une session plusieurs transactions seront exécutées en
séquence, mais pas forcément de façon contiguë : cas d’une session interactive où l’utilisateur prend le
temps de réfléchir ou discuter entre deux actions sur la BD.
Les SGBD permettent en général plusieurs sessions simultanées et donc autorisent la concurrence des
transactions.
Une session peut-être très courte et donner lieu à l’exécution de peu d’ordres (par exemple si elle est
commandée par un programme) ou être très longue et donner lieu à l’exécution de nombreux ordres,
c’est le cas des séances de TP : 2 heures ou de l’employé qui se connecte le matin et se déconnecte le soir.

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.

12.3 Modèle d’exécution des transactions (figure 12.2)

12.4 Propriétés que doivent respecter les transactions : ACID ou


CADI
C comme correction Une transaction préserve la sémantique de la BD
Une transaction doit faire passer la base d’un état correct dans un nouvel état correct. Cette
notion de correction a donc beaucoup à voir avec le fait que le programme exécuté par la tran-
saction est correct. Ce point ne sera donc pas développé dans la suite.

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

T1 variables Base de données

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 :

Instant d’exécution Transaction 1 : T1 Transaction 2 : T2


Ici le solde de 537 vaut 100
1 s := lire_solde (537)
2 s := s + 20
3 s := lire_solde (537)
4 ecrire_solde (537, s)
5 s := s + 10
6 ecrire_solde (537, s)
Ici le solde de 537 vaut 110 alors qu’il devrait valoir 130

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 !).

Les transactions sont gérées par un processus du SGBD : le moniteur transactionnel.

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.

12.5 Transaction et atomicité : comment cela marche ?


12.5.1 Au niveau du programmeur : instructions de validation et d’annulation
En fonctionnement normal, une transaction se termine obligatoirement soit par :
une validation de ses effets : toutes les modifications qu’elle a faites sont rendues permanentes et,
au moins pour Oracle et Postgres, ne peuvent devenir accessibles aux autres transactions qu’à ce
moment là (suivant leurs niveaux d’isolation). Une validation est la plupart du temps demandée
explicitement grâce à l’instruction SQL commit.
une annulation de ses effets : les nuplets qu’elle a détruits sont réinsérés, les nuplets qu’elle a modifiés
reprennent leurs valeurs initiales, les nuplets qu’elle a insérés disparaissent. Une annulation est
la plupart du temps demandée explicitement grâce à l’instruction SQL rollback.
12.6. TRANSACTION ET DURABILITÉ : COMMENT CELA MARCHE ? 147

12.5.2 Au niveau du SGBD


Le problème principal est de pouvoir remettre la base dans son état initial si la transaction est annulée
(rollback) ou qu’elle ne peut se terminer du fait d’une panne du système.

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.

12.6 Transaction et durabilité : comment cela marche ?


La durabilité stipule que les effets des transactions validées (commit) doivent être permanents2 .

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

Gestion de la concurrence des


transactions

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.

Sachant qu’une modification de la base de données sous-entend en général la modification de plusieurs


tables faisant passer la base d’un état correct dans un nouvel état correct, on appelle transaction
l’exécution de toute suite d’opérations élémentaires permettant soit d’obtenir une information perti-
nente sur l’état de la base soit de faire passer la base d’un état correct à un nouvel état correct.

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.

13.1 Notion d’ordonnancement


Comme on l’a vu, le processeur partage son temps entre les différentes transactions en cours d’exécution :
il exécute quelques instructions d’une transaction, puis quelques instructions d’une autre transaction,
puis revient à la première, . . ..

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.

13.2 Des problèmes dûs à la concurrence


Pour illustrer les problèmes, on reprend l’exemple du virement de compte à compte en distingant les
opérations de lecture et d’écriture dans la base :
a := lire (A) ;
a := a - S ;
ecrire (A, a) ;
b := lire (B) ;
b := b + S ;
ecrire (B, b) ;

13.2.1 Perte de mise à jour


T1 virer (A, B, 100) T2 virer (A, B, 200)
a := lire (A) ;
a := lire (A) ; a := a - 200 ;
a := a - 100 ;
ecrire (A, a) ;
b := lire (B) ; b := b + 100 ;
ecrire (A, a) ;
ecrire (B, b) ;
b := lire (B) ; b := b + 200 ;
ecrire (B, b) ;
Perte de mise à jour : la première écriture de A par T1 est perdue, et pire : la base est devenue
incorrecte !
Remède : Si tout accès à un nuplet commençait par le verrouiller, alors le verrouillage de T1 obligerait
T2 à attendre que T1 déverrouille A. C’est, en gros, ce que font automatiquement Oracle et Postgres
dans le cas où la mise à jour du solde est faite par un ordre update (update verrouille les nuplets
concernés par la mise à jour).

13.2.2 Dépendance non validée


T1 virer (A, B, 100) T2 virer (A, C, 200)
a := lire (A) ; a := a - 100 ;
ecrire (A, a) ;
a := lire (A) ; a := a - 200 ;
b := lire (B) ; b := b + 100 ;
ecrire (B, b) ;
erreur : compte B saturé
rollback ;
ecrire (A, a) ;
b := lire (C) ; b := b + 200 ;
ecrire (C, b) ;
commit ;
Dépendance non validée : l’annulation de T1 est provoquée par l’échec de l’écriture du compte B,
mais T2 ne s’en rend pas compte et utilise la valeur de A modifiée par T1 . Au final A est décrémenté
de 300 au lieu de 200. Il aurait fallu que T2 attende la fin de T1 .
150 CHAPITRE 13. GESTION DE LA CONCURRENCE DES TRANSACTIONS

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.

13.2.3 Analyse incohérente


T1 analyser (A, B) T2 virer (A, B, 200)
a := lire (A) ; a := a - 200 ;
ecrire (A, a) ;
a := lire (A) ;
b := lire (B) ;
b := lire (B) ; b := b + 200 ;
ecrire (B, b) ;
afficher (a + b) ;

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).

13.2.4 Lectures non reproductibles


T1 T2
a := lire (A) ;
ecrire (A, 1000) ;
a := lire (A) ;

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.

13.3 Approche théorique : la sérialisabilité


L’idée est de caractériser formellement les ordonnancements corrects.

Si un ordonnancement entrelacé est équivalent à (au moins) un ordonnancement séquentiel alors il


est correct car tout ordonnancement séquentiel est correct du point de vue de la concurrence des
transactions.
Un ordonnancement entrelacé est sérialisable (autrement dit : correct) s’il est équivalent à au moins
un ordonnancement séquentiel des mêmes transactions.
 
Tout d’abord on notera :

 
 
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

Comment déterminer la sérialisabilité d’un ordonnancement ?

L’idée est que :


– si deux transactions T1 et T2 écrivent le même objet et que, dans l’ordonnancement considéré,
l’écriture de T1 précède celle de T2 , alors, dans l’ordonnancement séquentiel équivalent T1 doit
précéder T2 .
– de même si T1 lit un objet et que T2 écrit ce même objet :
– si la lecture par T1 précède l’écriture faite par T2 , alors, dans l’ordonnancement séquentiel équivalent
T1 doit précéder T2 .
– si l’écriture faite par T2 précède la lecture par T1 , alors, dans l’ordonnancement séquentiel équivalent
 
T2 doit précéder T1 .

 
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)).

On notera p → q si p et q induisent un ordre et que p précède q dans l’ordonnancement considéré.

Un ordonnancement est sérialisable ⇔ le graphe suivant ne comporte pas de cycle :


– les sommets sont les transactions de l’ordonnancement,
– un arc va de la transaction Ti à la transaction Tj ssi on a pi → pj dans l’ordonnancement.
Q. 189 En quoi l’ordonnacement suivant n’est-il pas sérialisable : l1 (a) l2 (a) e2 (a) e1 (a)
Par exemple l1 (o) l2 (o) e1 (o) e2 (o) n’est pas sérialisable car on a l1 (o) → e2 (o) qui implique que T1
doit précéder T2 et l2 (o) → e1 (o) qui implique que T2 doit précéder T1 .
Bien sûr, on peut avoir plus d’un ordonnancement séquentiel équivalent à un ordonnancement entre-
lacé :
– si l’ordonnancement ne contient pas de couples d’opérations induisant un ordre (par exemple les
transactions travaillent sur des objets différents)  
 
– soit l’ordonnancement entrelacé de 3 transactions T1 , T2 et T3 : l3 (a) e1 (b) e2 (a) l2 (b) e3 (c) . Les
couples d’opérations induisant un ordre sont : l3 (a) → e2 (a) et e1 (b) → l2 (b) qui indiquent que dans
les ordonnancements séquentiels équivalents T3 et T1 doivent précéder T2 mais n’impose aucun ordre
entre T3 et T2 . On a alors deux ordonnancements séquentiels équivalents : T1 , T3 , T2 et T3 , T1 , T2 .
Q. 190 Combien (e3 (c) l2 (a) e4 (b) e1 (d) e2 (a)) a-t-il d’ordonnancements séquentiels équivalents ?

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

T1 virer (A, B, 100) T2 virer (A, C, 200)


a := lire (A) ; a := a - 100 ;
ecrire (A, a) ;
a := lire (A) ; a := a - 200 ;
ecrire (A, a) ;
b := lire (C) ; b := b + 200 ;
ecrire (C, b) ;
b := lire (B) ; b := b + 100 ;
ecrire (B, b) ;

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.

13.4 Approche pratique : les techniques proposées par les SGBD


On verra principalement le verrouillage et la gestion de versions multiples d’un même objet qui sont
à la base des techniques proposées par Oracle et Postgres (entre autres très probablement).

13.5 La technique préventive du verrouillage


Le verrouillage d’objet est un outil fourni par le SGBD qui permet d’empêcher que deux transactions
puissent accéder simultanément au même objet en obligeant une des deux transactions à attendre
que l’autre déverrouille l’objet. On dit aussi que le verrouillage est un outil (de très bas niveau, en
fait le seul outil d’encore plus bas niveau est l’instruction test and set de certains microprocesseurs)
pour synchroniser les transactions. Les méthodes synchronized de Java relèvent de la technique du
verrouillage en empêchant que deux tâches puissent exécuter en même temps une des méthodes syn-
chronized d’un même objet, pas forcément la même (une des deux tâches est mise en attente jusqu’à
ce que l’autre ait terminé d’exécuter sa méthode).

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).

Plusieurs transactions peuvent utiliser ce mode de verrouillage simultanément sur la même


donnée, d’où son nom de partagé.

En revanche ce mode est incompatible avec le mode exclusif.

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.

verrouillage exclusif : X comme eXclusive si la transaction souhaite modifier la donnée. L’opération


de verrouillage est lockX (o), celle de déverrouillage unlockX (o).
Typiquement ce mode de verrouillage est effectué automatiquement par le SGBD sur chaque
nuplet faisant l’objet d’une mise à jour (update, insert, delete) ou d’une sélection pour mise
à jour ultérieure (select ... for update : permet au programmeur de verrouiller explicitement
des nuplets sans pour autant les modifier section 16.3).

Ce verrouillage est incompatible avec toute autre demande de verrouillage.

Oracle et Postgres verrouillent en X automatiquement les nuplets modifiés ou sélectionnés pour


modification.

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()

13.6 Un protocole naı̈f de verrouillage

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

T1 virer (A, B, 100) T2 virer (A, B, 200)


lockX (A) ;
a := lire (A) ; a := a - 100 ;
lockX (A) ;
attente . . .
ecrire (A, a) ;
unlockX (A) ;
a := lire (A) ;
lockX (B) ;
a := a - 200 ;
b := lire (B) ; b := b + 100 ;
ecrire (A, a) ;
unlockX (A) ;
lockX (B) ;
attente . . .
ecrire (B, b) ;
unlockX (B) ;
b := lire (B) ; b := b + 200 ;
ecrire (B, b) ;
unlockX (B) ;

13.6.1 Ce protocole naı̈f ne résout pas tous les problèmes


Exemple d’un verrouillage naı̈f qui peut aboutir à une analyse incohérente de l’état de la base :

T1 virer (A, B, 100) T2 afficher (A + B)


lockX (A) ;
lockS (A) ;
a := lire (A) ; a := a - 100 ; ecrire (A, a) ; attente . . .
unlockX (A) ;
a := lire (A) ;
unlockS (A) ;
lockS (B) ;
lockX (B) ;
attente . . . b := lire (B) ;
unlockS (B) ;
afficher (a + b) ;
b := lire (B) ; b := b + 100 ; ecrire (B, b) ;
unlockX (B) ;

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é.

13.7 Le verrouillage deux phases (V2P)


Dans la première phase la transaction ne peut que verrouiller les données, dans la seconde phase elle ne
peut que relâcher les verrouillages. Bien entendu ces opérations de verrouilage puis de déverrouillage
peuvent être mélangées dans les instructions de la transaction.

Q. 193 Vérifier que V2P résout le problème précédent (section 13.6.1).


13.8. LE VERROUILLAGE DEUX PHASES RIGOUREUX (V2PR) 155

13.7.1 V2P : problème des cascades d’annulations


Exemple d’un verrouillage deux phases où l’annulation de T1 oblige à annuler aussi T2 . Supposons que
les soldes de la base n’aient pas le droit d’être négatifs. Supposons aussi que le virement de A vers B
rende négatif le solde de A, lors de la tentive d’écriture du nouveau solde une erreur sera déclenchée
menant à l’annulation de la transaction :
T1 virer (A, B, 100) T2 créditer (B, 200)
lockX (A) ;
a := lire(A); a := a-100;
lockX (B) ;
b:=lire(B); b:=b+100; ecrire(B,b); lockX (B) ;
unlockX (B) ; attente . . .
b:=lire(B); b:=b+200; ecrire(B,b);
unlockX (B) ;
ecrire(A, a); ⇒ annulation car a<0 ⇒ annulation en cascade

Q. 194 Pourquoi l’annulation de T1 implique-t-elle celle de T2 ?


L’annulation en cascade pose un sérieux problème : si T2 est déjà validée au moment où T1 est annulée
alors on ne peut plus annuler T2 et la base de données passe dans un état incohérent puisque T2 a
utilisé un état de B qui a été annulé !
Le problème vient du fait que T2 utilise B sans être sûr que sa nouvelle valeur sera validée. La solution
consiste donc à faire attendre T2 jusqu’à ce que T1 soit validée ou annulée : les verrous ne doivent être
relâchés par T1 qu’après sa validation ou son annulation. C’est exactement ce que fait le protocole
de verrouillage deux phases rigoureux.

13.8 Le verrouillage deux phases rigoureux (V2PR)


Pour éviter des problèmes d’annulation en cascade, on interdit tout déverrouillage explicite (la primi-
tive unlockX n’existe tout simplement pas !) : les déverrouillages sont effectués automatiquement par
le SGBD en fin de transaction (après commit ou rollback). Quand une transaction se débloque elle
est sûre de trouver les données dans le bon état car la validation ou l’annulation de leurs modifications
a déjà été faite.

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.

13.8.1 Le problème du V2PR : l’interblocage


Malheureusement le V2PR peut donner lieu à des interblocages (encore nommés étreintes fatales ou
T1 T2
lockX (A) ;
En fin de tableau, T1 attend que T2 déverrou
lockX (B) ;
B et T2 attend que T1 déverrouille A ce qui
deadlock) entre transactions : lockX (B) ;
traduit par un interblocage entre T1 et T2 : i
bloqué par T2
a un cycle dans le graphe d’attente :
lockX (A) ;
bloqué par T1
attend

T1 attend T2
Q. 196 Donner un exemple d’interblocage mettant en jeu trois transactions.

Q. 197 Caractériser graphiquement un interblocage concernant n transactions T1 , . . . , Tn


156 CHAPITRE 13. GESTION DE LA CONCURRENCE DES 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.

Certaines techniques de prévention (comme le wait/die et le wound/wait) nécessitent l’annulation de


la transaction susceptible de participer à un 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).

Q. 198 Java prévient-il ou détecte-t-il les interblocages ? (Ada non plus)


Oracle détecte les interblocages en provocant une erreur dans une des transactions y participant.

13.8.2 Détection puis résolution de l’interblocage (cas de Oracle et Postgres)


Détection d’interblocage : un cycle dans le graphe d’attente entre les transactions. Les transactions
forment les sommets du graphe, un arc Ti → Tj indique que Ti est bloqué en attente d’une ressource
verrouillée par Tj .

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.

13.8.3 Prévention des interblocages


– Par norme de programmation : on verrouille les objets toujours dans le même ordre (si toutes les
transactions verrouillent les objets dans le même ordre, aucun interblocage n’est plus possible), ou
bien pose préalable d’un verrou global et unique (ceci étant évidemment pénalisant).
– Par verrouillage en tout ou rien : soit on arrive à verrouiller tous les objets nécessaires et l’exécution
peut continuer, soit on n’y arrive pas et la transaction attend que tous ces objets soient disponibles.
– Par un protocole ad hoc, par exemple les protocoles wait/die et wound/wait qui seront vus en TD.
Ces deux protocoles nécessitent l’annulation d’une transaction en cas de possibilité d’interblocage.
13.9. MULTI-VERSIONS ESTAMPILLÉES MVE, PROTOCOLE D’ISOLATION 157

Granularité des objets verrouillables en général deux grains : la table et le nuplet (Oracle et
Postgres ont ces deux grains).

13.8.4 Inconvénients du V2PR


Le V2PR est plus strict que la théorie de la sérialisabilité : à cause du verrouillage qui force l’attente
de certaines transactions, certains ordonnancements sérialisables ne peuvent plus se produire en V2PR.

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.

13.9 Multi-versions estampillées MVE, protocole d’isolation


Contrairement au V2PR, le protocole MVE ne bloque aucune transaction mais effectue une vérification
de la sérialisabilité a postériori (protocole curatif). Il assure aussi l’isolation des transactions.

Objectifs et avantages de MVE :


– assure l’isolation inter-transaction, c’est à dire qu’une transaction ne verra pas les modifications
faites par d’autres transactions plus récentes
– lecteurs jamais bloqués,
– les lecteurs ne bloquent pas les écrivains.
Inconvénients de MVE :
– seule une opération d’écriture peut donner lieu à annulation,
– il peut y avoir des cascades d’annulations.
Ici, il faut bien distinguer les notions d’objet, qui est en général une ligne de table, et de valeur. Une
valeur est une constante alors qu’un objet peut posséder des valeurs différentes au fil du temps.

13.9.1 Introduction informelle à MVE


L’idée est de conserver et exploiter l’historique des valeurs qui ont été affectées à chaque objet de
manière à ce qu’une transaction ne voit pas les modifications faites par d’autres transactions.

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 :

versions de la ligne T9 T10


566
démarrage
lire donne 566
596
écrit (699 )
596 699
démarrage
lire donne 699
596 610
9
écrit (710
10 )
596 610 10
9 710

Q. 199 Que lira T10 après avoir écrit 7 dans la ligne ?


Le même exemple où les choses se passent moins bien :

versions de la ligne T9 T10


566
lire donne 566
596
lire donne 596
510
6
écrit (699 ) ⇒ échec car lu par T10
510
6

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.

T3, T6, T7, T8, T9 T10, T12, T13 T14, T50

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 :

T3, T6, T7, T8, T9 T10, T12, T13 T14, T50

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 :

T3, T6, T7 T8, T9 T10, T12, T13 T14, T50

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 ?

13.9.2 Définition précise de MVE


Chaque transaction T est estampillée de façon unique avec l’instant auquel elle a commencé, par
exemple T9 . Donc Th1 est plus ancienne que Th2 si ⇔ h1 < h2.
Pour chaque donnée (nuplet) Q, on maintient plusieurs versions notées Qrc où c est l’estampille
constante de la transaction qui a créé cette version et r celle de la transaction la plus récente qui
a lu cette version.

Toutes les versions d’une ligne sont des constantes.

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)

Exemple, étudions l’évolution des versions du nuplet Q :


Le nuplet Q dispose initialement de deux versions : Q13
6 Q15
13
T14 lit Q : version Q1513 : Q13
6 Q15
13
T16 lit Q : version Q1513 dont r est mis à 16h : Q13
6 Q16
13
T17 écrit une nouvelle valeur de Q : création de Q17
17 : Q13
6 Q16
13 Q17
17
T17 écrit une nouvelle valeur de Q : mise à jour de Q17
17 : Q13
6 Q16
13 Q17
17
16
T16 écrit une nouvelle valeur de Q : création de Q16 : Q13 Q16 Q16 Q17
6 13 16 17

Q. 210 En fin de tableau, que se passe-t-il si T15 tente d’écrire Q ?

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

13.9.3 Ordonnancements non acceptés par le protocole Multi-versions estam-


pillées

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

Ordonnancement dont on ne sait pas (encore) s’il est ou non sérialisable

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.

Ordonnancement clairement non sérialisable détecté par MVE

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

Ici le protocole multi-versions colle bien à la théorie de la sériabilité.

Ordonnancement sérialisable rejeté par MVE !

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

Cet ordonnancement sérialisable est pourtant refusé par le protocole multi-versions.

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 .

13.9.5 Inconvénient du protocole multi-versions : Les cascades d’annulations


versions de Q versions de A T9 T10 T11
Q66 A66
lire (A) -> A6
Q66 A96
ecrire (A)
Q66 A96 A99
lire (A) -> A9
Q66 A96 A11
9
ecrire (A)
Q66 A96 A11
9 A11
11
lire (Q)
Q10
6 A96 A11
9 A11
11
lire (Q)
Q10
6 A96 A11
9 A11
11
ecrire (Q) ⇒ annulation
⇒ annulation
L’annulation de T9 oblige à annuler T11 car T11 a lu A9 qui a été produite par T9 , on a donc une
cascade d’annulation.

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.

13.9.6 Avantages du protocole multi-versions


Ce protocole n’est pas bloquant puisqu’il n’utilise pas de verrouillage, cette qualité sera conservée pour
les transactions en lecture seule malgré l’ajout du protocole V2PR permettant de corriger le problème
des cascades d’annulations.
Q. 215 Compléter les versions de Q et indiquer les valeurs des lire(Q) dans le tableau suivant :
13.9. MULTI-VERSIONS ESTAMPILLÉES MVE, PROTOCOLE D’ISOLATION 163

versions de Q T7 T9 T10 T11


Q66
————– lire(Q) →

————– 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.

13.9.7 Ce qui est troublant dans MVE

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

Les versions Q10 11 12


6 , Q11 et Q12 ne peuvent être vues et manipulées que par les transactions d’estampilles
inférieures à 18h et vivent leurs vies. Comment comprendre cela ?
Une réponse consiste à faire l’hypothèse que l’écriture d’une nouvelle version est forcément précédée
par une lecture, c’est effectivement ce qui se passe lors d’un update. Dans ce cas il est impossible de
créer une nouvelle version dont l’estampille de création serait inférieure à la plus grande estampille de
lecture et les anciennes versions ne peuvent alors plus vivre leur vie L’exemple précédent devient :
164 CHAPITRE 13. GESTION DE LA CONCURRENCE DES TRANSACTIONS

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

Éléments d’implantation des


transactions

14.1 Multi-versions et V2PR (Oracle, PostgreSQL et MySQL/InnoDB)


Oracle ne permet de modifier que la version la plus récente d’une ligne.
– Pas de cascades d’annulations grâce au verrouillage deux phases rigoureux.
– Les lectures ne sont jamais bloquées et ne bloquent pas les écrivains grâce aux versions multiples.

14.1.1 Verrouillage 2 phases rigoureux en Oracle


Oracle propose deux granularités de verrouilage : les nuplets et les tables. Pour les nuplets on ne
dispose que du verrouillage exclusif noté X (pas de verrou partagé noté S sur les nuplets). Pour les
tables on dispose des verrouillages effectifs X et S et des verrouillages d’intention RS, RX et SRX qui
permettent de savoir rapidement si des nuplets de la table sont susceptibles d’être en cours de lecture
pour modification ultérieure (RS posé par select...for update voir section 16.3), de mise à jour (RX
posé par insert, update, delete) ou de lecture par plusieurs transactions et mise à jour par une
seule transaction (celle qui a verrouillé la table en SRX, seuls les verrouillages RS faits par les autres
transactions ne sont pas bloqués par SRX).

Ici on ne s’intéresse qu’au verrouillage niveau nuplet introduit par Oracle 6.

Trois composants fondamentaux dans la mémoire globale d’Oracle :


– le SCN (System Change Number) de la prochaine transaction à démarrer. Le SCN est un entier
qui identifie de façon unique chaque transaction vivante ou terminée, les SCN sont alloués de façon
croissante.
– chaque nuplet de la table conserve les versions encore utiles aux transactions en cours. La version
la plus récente est dans la table, les autres sont dans le segment de recouvrement et chaque version
est étiquetée avec le SCN de la transaction qui l’a créée,
– la table des transactions actives (nommée TTA dans la suite) contient les SCN de toutes les tran-
sactions actives (ni validées ni annulées).
Un nuplet est verrouillé de façon exclusive si et seulement si l’étiquette SCN de sa version la plus
récente (celle qui est dans la table) est présente dans la TTA. En fin de transaction, le SCN de la
transaction est supprimé de la TTA, ce qui déverrouille implicitement tous les nuplets verrouillés par
cette transaction.
Le verouillage d’une ligne n’empêche pas une autre transaction de lire la version appropriée de cette
ligne.

14.1.2 Multi-versions en Oracle : principes généraux


Oracle implémente une version simplifiée de la gestion de multi-version : chaque version d’un nuplet
est une valeur qu’a prise ce n-uplet étiquetée avec le SCN de la transaction qui a produit cette version

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).

Un état possible du système


Mémoire Globale
La transaction concernée SCN = 5025
Les transactions actives lors de la création de 5025 : CTTA = (1455, 2033, 4001)
Les transactions actuellement actives : TTA = (5025, 1455, 4001)
table T segment de recouvrement
ligne scn valeur scn valeur scn valeur
1 4 (1, B)
2 3999 (2, B) → 17 (2, A)
3 17 (3, B)
4 4001 (4, E)
5 2033 (5, C) → 2000 (5, B) → 17 (5, A)

Q. 217 Que peut-on dire des lignes 4 et 5 ?

Q. 218 Quelles sont les transactions validées (commit) parmi celles qui ont manipulé la table T ?

Q. 219 Quelles sont les lignes verrouillées ?

14.1.3 Démarrage d’une transaction : read only et serializable


Les transactions read only et serializable voient pendant toute leur vie la base dans l’état de
validation dans lequel elle était quand elles ont commencé.
– Une transaction read only ne peut faire aucune mise à jour.
– Une transaction en isolation serializable peut faire des mises à jour et ne voit que les modifications
qu’elle a faites.
Soit T la transaction read only ou serializable. En début d’exécution T est identifiée par le prochain
SCN (plus grand que tous les autres déjà attribués à d’autres transactions) et elle prend une copie
CTTA de la TTA qu’elle ne modifiera pas. La CTTA permettra à T de connaı̂tre les transactions
qui étaient actives quand elle a démarré ce qui lui permettra d’éviter de voir les modifications faites
par ces transactions (même si elles sont commises). Elle ignorera aussi les modifications faites par les
transactions plus récentes qu’elle.
On peut essayer de voir grahiquement ce qui se passe à l’instant t : soit l’état suivant de la mémoire
globale du SGBD et les états locaux des deux transactions actives :

Mémoire Globale Mémoires locales des transactions


État 1 TTA = (15, 13) Transaction serializable 15 : CTTA = (14, 13, 12)
prochain SCN = 17 Transaction serializable 13 : CTTA = (12, 10)

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

T11 T14 session 2

T10 T15 session 3

T12 T16 session 4

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. 221 En utilisant la figure 14.1 donner l’état correspondant à l’instant T.

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.

Mémoire Globale Mémoires locales des transactions


État 2 TTA = (20, 17) Transaction serializable 17 : CTTA = (7, 2)
prochain SCN = 25 Transaction serializable 20 : CTTA = (18, 14)

14.1.4 La nouvelle transaction sérialisable 5025 Démarre


Voici les contenus de la mémoire globale et de la nouvelle transaction 5025 :

Mémoire Globale Mémoires locales des transactions


TTA = (1455, 2033, 4001) Transaction serializable 5025 : CTTA = (1455, 2033, 4001)
prochain SCN = 5026

 
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 ?

14.1.6 Mise à jour d’une ligne : transaction serializable


Si Ta est serializable et qu’elle tente un update ou un delete elle va travailler avec la version V
obtenue par une lecture ; plusieurs cas peuvent se produire :
168 CHAPITRE 14. ÉLÉMENTS D’IMPLANTATION DES TRANSACTIONS

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).

14.1.7 La transaction sérialisable 5025 est en cours

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 :

Mémoire Globale Mémoires locales des transactions


TTA = (1455, 4001, 5025, 5555) Transaction serializable 5025 : CTTA = (1455, 2033, 4001)
prochain SCN = 5556

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

table T segment de recouvrement


ligne scn valeur scn valeur scn valeur scn valeur
1 4 (1, B)
2 3999 (2, B) → 17 (2, A)
3 5026 (3, C) → 17 (3, B)
4 4001 (4, E)
5 5555 (5, D) → 2033 (5, C) → 2000 (5, B) → 17 (5, A)
6 5555 (6, A)

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. 228 Nouvel état de la ligne 1 si 5025 tente de la modifier avec (1, X) ?

Q. 229 Que se passe-t-il si 5025 tente de modifier le 3ième nuplet ?

Q. 230 Que se passe-t-il si 5025 tente de modifier le 5ième nuplet ?

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 ?

Q. 234 Que doit faire le système pour annuler (rollback) 5555 ?


Attention : les segments de rollback qui stockent les anciennes versions sont, comme toute ressource,
d’une capacité limitée. Il se peut qu’ils se saturent et alors les anciennes transactions ne disposeront
pas des versions dont elles ont besoin (erreur ORA-1555 snapshot too old (rollback segment too small)).
Deux solutions : augmenter la taille des segments de rollback ou utiliser un verrouillage explicite pour
éviter la multiplication des versions.

14.1.8 Autre niveau d’isolation : read committed


En isolation read committed, chaque démarrage d’une instruction SQL commence par recharger
la CTTA avec la TTA actuelle, cette instruction pourra donc voir les modifications validées par une
autre transaction avant son démarrage.

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

table T segment de recouvrement


ligne scn valeur scn valeur scn valeur
1 14 (2, B) → 17 (2, A) → 3 (2, X)
Chapitre 15

Les niveaux d’isolation des transactions

15.1 Délimitation des transactions sous SQL/Oracle


Suivant le type d’instruction exécutée, il y a deux cas de délimitation :
1. l’exécution d’une instruction DDL ou DCL constitue une transaction : un commit est fait,
puis l’instruction DDL ou DCL est exécutée, puis :
– si elle s’est bien passée un commit est fait qui valide et clôt cette transaction,
– sinon rien de plus n’est fait et on reste dans la transaction commencée pour cette instruction
DDL ou DCL.
2. en revanche une transaction peut être l’exécution d’autant d’instructions DML que l’on veut,
il faudra la terminer explicitement par une validation (commit) ou une annulation (rollback).
Dans les deux cas, la transaction commence en même temps que l’exécution de la première instruction
SQL. PostgreSQL ne connaı̂t que le second cas pour DDL et DCL.

Transaction Oracle Transaction Postgres


En Oracle, toute exécution fait partie d’une
transaction. Une transaction commence avec la
première instruction DML ou set transaction En PostgreSQL une transaction commence avec
qui suit : l’instruction start transaction ... (ou begin)
– la connexion (début de session) et se termine comme en Oracle.
– une instruction DDL réussie Si on n’utilise pas l’instruction start transac-
– une validation (ordre commit) tion ... (ou begin) alors, par défaut, chaque
– une annulation (ordre rollback) instruction DML est exécutée comme une tran-
Une transaction se termine juste après saction complète, on parle alors de fonctionne-
– une validation (ordre commit) ment en auto commit (un peu comme le mode
– une annulation (ordre rollback) par défaut de JDBC).
– déconnexion normale ⇒ validation
– déconnexion anormale ⇒ annulation

15.1.1 Niveaux d’isolation : set transaction SQL et PL/SQL


Cette instruction, la première de la transaction (appelons la T ), règle le niveau d’isolation de T par
rapport au reste du monde. Plus précisément, un niveau d’isolation indique dans quelle mesure T verra
les modifications validées par d’autres transactions. En revanche le niveau d’isolation ne permet pas
de restreindre la visibilité des modifications qui seront faites par T . En quelque sorte l’isolation n’est
pas symétrique : elle permet de dire ce qu’on veut qu’une transaction puisse voir du monde extérieur
mais elle ne permet pas d’empêcher les autres de voir les modifications qu’elle effectue, par exemple
une autre transaction en non isolation (read uncommitted de SQL2) verra toutes les modifications
même celles qui ne sont pas validées ! Ce niveau de non isolation n’est disponible ni en Oracle ni en
PostgreSQL même si PostgreSQL le reconnaı̂t syntaxiquement, voir le tableau un peu plus loin.

171
172 CHAPITRE 15. LES NIVEAUX D’ISOLATION DES TRANSACTIONS

set transaction <option> ;


<option> ::= read only | isolation level <niveau-d-isolation>
| read write | use rollback segment <rollback_segment>
<niveau-d-isolation> ::= serializable | read committed
Cette déclaration (optionnelle) doit être la première instruction de la transaction. La valeur d’isolation
par défaut est positionnable dans une variable qui est initialisée à read committed.
Le mot read committed est d’ordre technique et signifie qu’une instruction peut voir toute modifi-
cation validée avant qu’elle ne démarre.
PostgreSQL utilise l’instruction start transaction ....
options

(SQL92, Oracle, PostgreSQL) transaction sérialisable (isolation par défaut en


SQL92). Une transaction sérialisable voit la base telle qu’elle était validée quand
elle a commencé, autrement elle ne voit aucunes des modifications validées par

d’autres transactions après son démarrage. Bien entendu elle voit ses propres 
 
modifications. 2 erreurs possibles : non sérialisabilité, interblocage
Ces deux erreurs son dues à pas de chance et ne devraient donc pas être in-
serializable terprétées comme des bogues, mais plutôt comme des circonstances empêchant
l’aboutissement de la transaction. Si la transaction sérialisable T tente de modifier
un nuplet modifié par une autre transaction validée après le début de T , l’ins-
truction DML correspondante échoue : ORA-08177 : Can’t serialize access
for this transaction. Attention : cette erreur a lieu aussi si la transaction va-
lidée a simplement effectué un select for update, même si elle n’a pas modifié
les lignes ainsi verrouillées.

(SQL92) (isolation par défaut en Oracle et en PostgreSQL) : chaque instruc-


tion DML de la transaction read committed voit ce qui est validé au moment
où 
 l’instruction a commencé à s’exécuter ou bien quand ele est débloquée.
 
1 erreur possible : interblocage Si cette instruction DML tente de ver-
read
rouiller un nuplet déjà verrouillé par une autre transaction, elle est bloquée jus-
committed
qu’au déverrouillage ; quand elle se débloque elle réévalue complètement la
sélection des nuplets. Cette erreur est due à pas de chance et ne devrait donc pas
être interprétée comme un bogue, mais plutôt comme une circonstance empêchant
l’aboutissement de la transaction.

(SQL92) (absent de Oracle) : à ce niveau, la transaction voit toutes les modifi-


cations, même celles non validées, elle n’est donc aucunement isolée. Présent en
read
PostgreSQL8 mais la documentation dit clairement que c’est implanté par du
uncommitted
read committed : When you select the level Read Uncommitted you really get
Read Committed !

(Oracle) transaction-level read consistency. La transaction ne voit que les chan-


gements commis avant son début (commandes autorisées : select, manipulation
de curseurs, lock table, set role, alter session, alter system, commit et
read only rollback)
L’intérêt de read only par rapport à serializable est certainement de faire des
économies sur les ressources allouées à la transaction puisqu’on sait qu’elle ne
pourra pas modifier la base de données.

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.2 Positionner l’isolation par défaut :


Ce niveau d’isolation par défaut peut être redéfini pour la session en cours avec la commande alter
session.
alter session set isolation_level = {serializable | read committed}

15.3 Les commandes intra-transaction SQL et PL/SQL


savepoint <nom-de-point-de-sauvegarde> ] ;
commit [ work ] ;
rollback [ work ] [ to [ savepoint ] <nom-de-point-de-sauvegarde> ] ;

Attention, contrairement à l’instruction rollback, l’instruction rollback to savepoint

ne termine pas la transaction en cours.

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.

15.3.4 Exemple de rollback work to savepoint lors d’une erreur de notation


Forme rollback work to savepoint xxx ; annule le travail fait depuis le point de reprise mentionné
(qui appartient bien sûr à la transaction courante). Tous les points de reprise posés après le point de
reprise mentionné sont oubliés. Le point de reprise mentionné est conservé, les verrous obtenus depuis
le point de sauvegarde sont relâchés mais :
– les transactions déjà bloquées sur ces verrous restent bloquées jusqu’à la fin de cette transaction
– les autres transactions qui n’avaient pas encore demandé ces verrous peuvent les obtenir.
La transaction n’est évidemment pas terminée, voici un exemple :
1
D’autres SGBD permettent le verrouillage des nuplets en mode Share, Postgres par exemple.
174 CHAPITRE 15. LES NIVEAUX D’ISOLATION DES TRANSACTIONS

update Etudiant set note = 14 where nom = ’Durand’ ;


savepoint Durand_note ;

update Etudiant set note = 18 where nom = ’Dupont’ ;


savepoint Dupont_note ;
-- oups! ce n’est pas Dupont mais Dupire qui a 18 :
rollback to savepoint Durand_note ;
update Etudiant set note = 18 where nom = ’Dupire’ ;

commit ;
Un rollback to savepoint ne termine pas la transaction.

15.4 Échec d’une transaction


Une transaction doit échouer (rollback) si une erreur du SGBD s’est produite pendant son exécution.
Ce qu’il faut faire en réponse à cet échec varie en fonction de la nature de l’erreur :
sémantique l’erreur est due au fait que la transaction a tenté de casser la cohérence de la base de
données, cette transaction est donc inappropriée et on ne devrait pas tenter de relancer la même
modification,
pas de chance l’erreur est un interblocage ou le fait qu’Oracle ne peut garantir la sérialisabilité,
cette erreur ne remet pas en cause la pertinence de la modification qui a échoué : il serait
peut-être intéressant d’attendre un peu (DBMS_LOCK.Sleep) puis de relancer automatiquement
la modification dans une nouvelle transaction2 .

15.5 Le virement de compte à compte en PL/SQL


Cet exemple ne reprend pas tous les points du squelette de la section 16.5 page 181 :
create table Compte (
id Number (5) primary key,
solde Number (5) constraint Solde_Positif check (solde >= 0)
) ;
La procédure Virer doit laisser inchangée la somme des soldes quitte à ne rien faire si ce n’est pas
possible :
create procedure Virer(D in Compte.id%type,C in Compte.id%type,S in PositiveN) is
Interblocage exception ;
pragma Exception_Init (Interblocage, -00060) ; -- deadlock (interblocage)
Solde_Negatif exception ;
pragma Exception_Init (Solde_Negatif, -02290) ; -- Solde_Positif violé (check)
begin
set transaction isolation level read committed ;
update Compte set solde = solde - S where id = D ;
if SQL%rowcount = 0 then
rollback ; raise_application_error (-20111, ’Compte à débiter inexistant’) ;
end if ;
update Compte set solde = solde + S where id = C ;
if SQL%rowcount = 0 then
rollback ; raise_application_error (-20111, ’Compte à créditer inexistant’) ;
end if ;
commit ;
exception
2
Doc Oracle : To minimize the performance overhead of rolling back transactions and executing them again, try to
put DML statements that might conflict with other concurrent transactions near the beginning of your transaction.
15.6. TRANSACTIONS AUTONOMES : PRAGMA AUTONOMOUS TRANSACTION 175

when Interblocage then


rollback ; raise_application_error (-20111, ’Interblocage’) ;
when Solde_Negatif then
rollback ; raise ;
end Virer ;
Q. 238 Montrer que deux exécutions simultanées de Virer peuvent s’interbloquer.

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.

Q. 241 Trouver la bêtise dans le code suivant, puis la corriger.

create procedure Betise (D in Compte.id%type) is


Interblocage exception ;
pragma Exception_Init (Interblocage, -00060) ; -- deadlock
Serialisabilite_Non_Garantie exception ;
pragma Exception_Init (Serialisabilite_Non_Garantie, -08177) ;
begin
set transaction isolation level serializable ;
loop
begin
update Compte set solde = solde * 1.1 where id = D ;
commit ;
exit ;
exception
when Interblocage or Serialisabilite_Non_Garantie then
-- attendre un peu que les choses se calment :
DBMS_Lock.Sleep (3.14) ; -- en secondes
when others then
rollback ; raise ;
end ;
end loop ;
end Betise ;

15.6 Transactions autonomes : pragma AUTONOMOUS TRANSACTION


On a parfois envie qu’une transaction dite mère puisse provoquer l’exécution d’une autre transaction
dite fille. Certains SGBD permettent de faire cela mais avec des sémantiques très différentes.
En Oracle cette sémantique est très simple : les transactions mère et fille sont complètement indépendantes,
c’est pourquoi on parle de transaction autonome pour une transaction fille.
La seule relation entre mère et fille est que la fille est exécutée complètement avant que sa mère ne
reprenne son exécution : en fait la fille peut-être considérée comme l’exécution d’un sous-programme
devant se terminer par un commit ou un rollback.
– une fille ne voit pas les modifications faites par sa mère, pourquoi ?
– la fille ne bénéficie d’aucun des verrous posés par sa mère.
– le succès ou non de la fille n’a aucun effet sur celui de sa mère et inversement.
– les modifications d’une transaction autonome sont publiées dès son commit sans attendre la fin de
sa mère,
176 CHAPITRE 15. LES NIVEAUX D’ISOLATION DES TRANSACTIONS

– une transaction autonome peut lancer d’autres transactions autonomes


Pour disposer de transactions autonomes, il suffit d’utiliser le pragma AUTONOMOUS_TRANSACTION dans
la partie déclarative de la procédure ou du bloc anonyme réalisant cette transaction.

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)) ;

create table Salaire (


id Number (5) primary key,
sal Number (7, 2) constraint Sal_Pos check (sal >= 0)) ;

create procedure Auditer (U in Memoire.auteur%type, M in Memoire.message%type) is


begin
insert into Memoire values (U, M) ;
end Auditer ;

create trigger Auditeur


before update on Salaire
for each row
declare
pragma AUTONOMOUS_TRANSACTION ;
begin
Auditer (user, ’modif salaire ’ ||
’old =(’ || to_char(:old.id) || ’,’ || to_char(:old.sal) || ’),’ ||
’new =(’ || to_char(:new.id) || ’,’ || to_char(:new.sal) || ’)’) ;
commit ; -- interdit dans un trigger non autonome
end ;
insert into Salaire values (1, 7000) ;
insert into Salaire values (2, 5000) ;

update Salaire set id = id + 1, sal = sal + 50 ;

select * from Memoire ;


AUTEUR MESSAGE
----------------------------
DURIF modif salaire old = (1, 7000), new = (2, 7050)
DURIF modif salaire old = (2, 5000), new = (3, 5050)

select * from Salaire ;


ID SAL
----------------------------
2 7050
3 5050

update Salaire set id = id + 1, sal = sal - 6000 ;


ORA-02290: violation de contraintes (DURIF.SAL_POS) de vérification

select * from Memoire ;


AUTEUR MESSAGE
----------------------------
DURIF modif salaire old = (1, 7000), new = (2, 7050)
DURIF modif salaire old = (2, 5000), new = (3, 5050)
15.6. TRANSACTIONS AUTONOMES : PRAGMA AUTONOMOUS TRANSACTION 177

DURIF modif salaire old = (2, 7050), new = (3, 1050)


DURIF modif salaire old = (3, 5050), new = (4, -950)

select * from Salaire ;


ID SAL
----------------------------
2 7050
3 5050

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

Synchronisation des transactions

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 ...

16.1 Un exemple de non synchronisation de transaction


Voici un exemple de problème qui ne sera résolu qu’en utilisant une synchronisation explicite. On
dispose des tables :
create table Membre (
create table Equipe (
id Number (5) primary key,
id Number (5) primary key,
salaire Number (10),
budget_salarial Number (10)
equipe references Equipe (id) not null
) ;
) ;

la somme des salaires des membres d’une équipe doit être inférieure
La propriété P de la base de donnée est : .


ou égale au budget salarial de cette équipe
La procédure Augmenter augmente le salaire d’un membre en tentant de conserver P :
create procedure Augmenter (M in Membre.id%type, A in Membre.salaire%type) is
l_equipe Equipe.id%type ;
le_budget_salarial Equipe.budget_salarial%type ;
somme_salaires Equipe.budget_salarial%type ;
begin
set transaction isolation level read committed ;
select equipe into l_equipe from Membre where id = Augmenter.M ;
select e.budget_salarial, nvl (Sum (m.salaire), 0)
into le_budget_salarial, somme_salaires
from Equipe e inner join Membre m on m.equipe = e.id
where m.equipe = Augmenter.l_equipe
group by e.id, e.budget_salarial ;
if somme_salaires + A > le_budget_salarial then
rollback ; raise_application_error (-20111, ’Budget dépassé’) ;
end if ;
update Membre set salaire = salaire + Augmenter.A where id = Augmenter.M ;
commit ;

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

Augmenter (1, 100) Augmenter (2, 50)


set trans ... read committed
set trans ... read committed
select equipe into l_equipe ... → 51
select e.budget_salarial ... → 2000, 1900
update Membre ...
select equipe into l_equipe ... → 51
select e.budget_salarial ... → 2000, 1900
commit ;
update Membre ...
commit ;

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.

16.2 Comment assurer une synchronisation


L’outil de base est la possibilité de poser des verrous : lorsqu’un verrou est posé sur une donnée, les
autres transactions tentant de modifier cette donnée seront bloquées jusqu’à ce que la donnée soit
déverrouillée pas la transaction qui l’avait verrouillée. Ce verouillage correspond bien à la possibilité
de verrouiller les toilettes qu’on utilise afin d’être sûr que personne d’autre ne peut entrer.
Lors du déverrouillage de la donnée, une seule transaction sera débloquée et pourra modifier cette
donnée car elle prendra soin de poser elle aussi un verrou sur cette donnnée.

16.3 Verrouillage fin avec select ... for update


Cette instruction permet de verrouiller en mode exclusif toutes les lignes sélectionnées par la requête.
Cela peut être très pratique pour résoudre des problèmes de synchronisation ou mettre en place des ap-
180 CHAPITRE 16. SYNCHRONISATION DES TRANSACTIONS

– dans une sous-requête,


Attention : la clause for
– dans un curseur PL/SQL,
plications interactives. update ne peut pas
– si la clause select contient distinct ou une fonction d’agrégation (
être utilisée :
– si la clause group by est présente,
Si la requête comporte une jointure, on peut compléter for update avec of suivi de noms de colonnes
permettant de savoir les lignes de quelle(s) table(s) il faut verrouiller, par exemple :
select ...
from Client c inner join Commande m on m.client = c.id
where c.id between 20 and 56
FOR UPDATE ; -- verrouille les lignes sélectionnées de Client et Commande
verrouillera des lignes de Client et celles de Commande leur correspondant. En revanche :
select ...
from Client c inner join Commande m on m.client = c.id
where c.id between 20 and 56
FOR UPDATE OF c.id ; -- ne verrouille que les lignes sélectionnées de Client
ne verrouillera que des lignes de Client ayant au moins une commande.

16.4 Comportement des instructions DML


Une requête select ne pose pas de verrou, n’est jamais bloquée ni bloquante (grâce à Multi-versions).
Les seules instructions posant un verrou et pouvant être bloquées par un verrou sont insert, update,
delete et select ... for update.

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

Q. 245 Pourquoi Oracle déclenche-t-il cette erreur de sérialisabilité ?

16.5 Squelette de procédure PL/SQL réalisant une transaction


Supposons qu’une procédure P réalise complètement le traitement d’une transaction, dans ce cas voici
une possibilité de squelette de cette procédure :
procedure P (...) is
begin
set transaction isolation level ... ;
--------------------------------------------------------
-- Verrouillages éventuels permettant de garantir une bonne
-- synchronisation des différentes transactions :
--------------------------------------------------------
select id into x from T where ... for update ; -- No_Data_Found éventuel
lock table ... -- un peu brutal a priori
--------------------------------------------------------
-- Les traitements peuvent maintenant ^ etre effectués.
-- 1) Détection du non maintien de certaines propriétés :
--------------------------------------------------------
if propriété non maintenue then
rollback ; raise_application_error (-20111, ’propriété non maintenue’) ;
end if ;
--------------------------------------------------------
-- 2) Si les propriétés sont garanties :
--------------------------------------------------------
modification(s) de la base
update, delete -- SQL%rowcount pour conna^ ıtre le nombre de lignes manipulées
--------------------------------------------------------
-- Validation des modifications et fin de la transaction
--------------------------------------------------------
commit ;
exception
when No_Data_Found then rollback ; raise_application_error (-20111, ’...’) ;
when others then rollback ; raise ;
end P ;

16.6 Augmenter : solution fine avec select ... for update


Cette solution est fine car elle ne verrouillera qu’une ligne de la table Voiture, ainsi elle ne bloquera
pas d’autres transactions s’intéressant à une autre voiture.
create procedure Augmenter (M in Membre.id%type, A in Membre.salaire%type) is
l_equipe Equipe.id%type ;
le_budget_salarial Equipe.budget_salarial%type ;
somme_salaires Equipe.budget_salarial%type ;
begin
set transaction isolation level read committed ;
-- Verrouiller l’équipe concernée par l’augmentation. No_Data_Found si M n’existe pas
select m.equipe into l_equipe
from Membre m inner join Equipe e on e.id = m.equipe
where m.id = Augmenter.M FOR UPDATE OF e.id ;
select e.budget_salarial, nvl (Sum (m.salaire), 0)
into le_budget_salarial, somme_salaires
from Equipe e inner join Membre m on m.equipe = e.id
182 CHAPITRE 16. SYNCHRONISATION DES TRANSACTIONS

where m.equipe = Augmenter.l_equipe


group by e.id, e.budget_salarial ;
if somme_salaires + A > le_budget_salarial then
rollback ; raise_application_error (-20111, ’Budget dépassé’) ;
end if ;
update Membre set salaire = salaire + Augmenter.A where id = Augmenter.M ;
commit ;
exception
when No_Data_Found then rollback; raise_application_error(-20111,’Membre inexistant’);
when others then rollback ; raise ;
end Augmenter ;
En reprenant un ordonnancement similaire à celui de la section 16.1 voyons ce qui va se passer :

Augmenter (1, 100) Augmenter (2, 50)


set trans ... read committed
set trans ... read committed
select equipe into l_equipe ... → 51
l’équipe 51 est verrouillée
select e.budget_salarial ... → 2000, 1900
update Membre ...
select equipe into l_equipe ...
transaction bloquée
commit ;
l’équipe 51 est déverrouillée
déblocage et réévaluation de la requête précédante
select e.budget_salarial ... → 2000, 2000
rollback ;
l’équipe 51 est déverrouillée
raise_application_error

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 ?

16.7 Verrouillage de table en Oracle


Rappel : Oracle verrouille en mode eXclusive chaque nuplet modifié par une transaction (voir la sec-
tion 14.1.1 p.165) ou sélectionné par une requête munie de la clause for update. La gestion de ces
verrouillages respecte le protocole de verrouillage 2 phases rigoureux.

Les verrous présentés ci-dessous portent sur les tables.

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

nuplets de la table et donc de verrouiller ces nuplets, T1 va automatiquement verrouiller la table en


mode row exclusive. Maintenant quand T2 tente de verrouiller la table en mode exclusive, elle se
rend compte très rapidement que la table est déjà verrouillée en row exclusive et elle se bloque
immédiatement sans avoir à explorer les nuplets de la table.

 
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.

lock table <nom-de-table> in row share mode [ nowait ] ;


-- erreur Oracle du nowait : -54
-- erreur PostgreSQL du nowait : the transaction is aborted (?).
Un verrouillage de table n’empêche jamais une autre transaction d’effectuer des requêtes sur cette
table et une requête ne verrouille jamais de table.

 
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

select ne verrouille rien et n’est jamais bloquée (cela grâce au multi-versions).


select ... for update verrouille la table en row share (RS), puis verrouille en X tous les nuplets
sélectionnés par le select. La table peut encore être verrouillée en mode share car le select ... for
update n’a modifié aucun nuplet.
update, insert et delete verrouillent d’abord la table en row exclusive (RX), puis verrouillent en
exclusive (X) tous les nuplets touchés par la modification.

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.

L’instruction lock table est utilisable en SQL et PL/SQL.

16.7.1 Augmenter : solution brutale avec lock table ...


Cette solution est brutale car elle verrouille carrément la table Equipe en mode exclusive qui est le
seul à convenir. Sa brutalité est due au fait que si d’autres transactions s’intéressent à d’autres équipes,
elles seront quand même bloquées ! Une telle technique risque donc de faire baisser les performances
transactionnelles du système par rapport à la solution plus fine donnée en 16.6.
create procedure Augmenter (M in Membre.id%type, A in Membre.salaire%type) is
l_equipe Equipe.id%type ;
le_budget_salarial Equipe.budget_salarial%type ;
somme_salaires Equipe.budget_salarial%type ;
begin
set transaction isolation level read committed ;
-- Blocage éventuel puis verrouillage de la table Equipe
LOCK TABLE Equipe IN EXCLUSIVE MODE ;
select equipe into l_equipe from Membre where id = Augmenter.M ;
select e.budget_salarial, nvl (Sum (m.salaire), 0)
into le_budget_salarial, somme_salaires
from Equipe e inner join Membre m on m.equipe = e.id
where m.equipe = Augmenter.l_equipe
group by e.id, e.budget_salarial ;
if somme_salaires + A > le_budget_salarial then
rollback ; raise_application_error (-20111, ’Budget dépassé’) ;
end if ;
update Membre set salaire = salaire + Augmenter.A where id = Augmenter.M ;
commit ;
exception
when No_Data_Found then rollback; raise_application_error(-20111,’Membre inexistant’);
when others then rollback; raise ;
end Augmenter ;

Q. 247 Pourquoi le mode share de verrouillage de la table ne conviendrait-il pas ?

Q. 248 Quel autre mode de verrouillage de la table pourrait convenir ?

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

insert into TT values (1) ;


end ;

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

Développer une application BD

Si on y réfléchit, la plupart des logiciels nécessitent la mémorisation persistante d’informations. Un


simple éditeur de texte procure cette persistance en utilisant directement le système de fichiers.

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.

17.1 Client serveur


Il s’agit maintenant de mettre en place des applications permettant un accès aisé à une base de
données, soit dans un contexte multi-machines (client/serveur), soit dans un contexte mono-machine
(l’application réside sur le serveur lui-même) :

O interaction _____________ ordres/données SQL _____________


/|\ <-----------> | Application |<-------------------->| Application |
| | | | SGBD |
/ \ | CLIENTE | RESEAU | SERVEUR |

Les fonctionnalités principales de ces applications :


– Interrogation de la base (les trains au départ de telle heure à telle heure)
– Mise à jour de la base (réservation de billet, annulation de réservation, ajout de voitures à un train,
. . .)
– Édition de rapports (taux d’occupation des trains en fonction de l’heure de départ, . . .)
Pour cela les éditeurs de SGBD ou des éditeurs tiers proposent trois types d’outils, en allant du plus
rudimentaire au plus sophistiqué :
API les interfaces de programmation (API Application Programming Interface, ou CLI Call Level
Interface). On peut distinguer deux sortes d’API :
concrète : spécifique à un SGBD particulier : applications efficaces mais difficilement portables
sur un autre SGBD.
abstraite : indépendante de tout SGBD particulier : applications moins efficaces mais plus
portables. Une API abstraite, par exemple ODBC ou JDBC, nécessite un driver spécifique
au SGBD utilisé.

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, . . .

17.2 Les API concrètes


Une API concrète est une bibliothèque permettant d’accéder à un SGBD bien particulier (OCI pour
Oracle, libpq pour Postgres, voire PHP, . . .).

Sources langage hote + appels à la bibliothèque

non Compilateur standard


connecté

connecter déconnecter Bibliothèque Fichiers objets

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

17.2.3 La mise en œuvre


Compilateur du langage hôte et bibliothèque d’accès au SGBD.

17.2.4 OCI : l’API concrète de Oracle


(Oracle Call Interface) programmée en C, la bibliothèque OCILIB s’utilise par compilation puis édition
des liens. Les avantages : contrôle fin du fonctionnement, supporte le SQL dynamique, possibilité
d’exécution asynchrone d’ordre SQL (l’application n’a pas à attendre la fin de l’ordre SQL pour
continuer à travailler)
On peut écrire :

select e.nom from Employees e where e.id = :idDemande

ou :idDemande est une variable de liaison dont la valeur est fournie par le programme applicatif. On
peut aussi écrire du code PL/SQL.

17.2.5 libpq : l’API concrète de Postgres

17.3 Les API abstraites


Ces API se veulent indépendantes de tout SGBD. Les deux qui sont présentées correspondent princi-
palement au modèle relationnel.

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.

17.3.1 ODBC : l’API abstraite de Microsoft


appli(s) <--> gestionnaire ODBC <---> pilotes spécif des SGBD
190 CHAPITRE 17. DÉVELOPPER UNE APPLICATION BD

17.3.2 JDBC : l’API abstraite de JavaSoft


JDBC est basé sur le X/Open SQL Call Level Interface (SQL92 Entry Level).

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).

17.3.3 Architectures JDBC : voir figure 17.3


Le standard propose 4 types de pilotes JDBC :

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).

Les pilotes proposés par Oracle :


Type 4 Thin JDBC (100% pur Java, applet), implémente en Java le protocole Oracle SQL*Net au
dessus des sockets Java. On obtient alors un code 100% Java et la possibilité de réaliser des
applets à condition que le SGBD soit sur la même machine que le serveur WEB.
Type 2 OCI JDBC (Java + API cliente OCI), nécessite l’installation de la bibliothèque OCI, on ne
peut donc pas faire d’applet.
Type 2 JDBC Server driver : de Type 2, pour les applications s’exécutant sur le serveur

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)

17.3.4 Fonctionnalités JDBC : java.sql voir figure 17.4


Toutes les méthodes déclenchent java.sql.SQLException ?
Q. 249 Pourquoi la plupart des types de JDBC sont-il de simples interfaces ?
17.3. LES API ABSTRAITES 191

' $
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

Fig. 17.4 – Architecture générale des types JDBC.

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") ;

 principale méthode de DriverManager est statique :


La 
 
static Connection getConnection (String url, String user, String pwd)
Exemple d’URL (protocole, SGBD, sous-protocole, adresse serveur, port, nom de la base)
"jdbc:oracle:thin:@//ma-machine.fil.univ-lille1.fr:3333:mabase"
"jdbc:postgresql://localhost/fil"
– jdbc:oracle:thin identifie le pilote à utiliser,
– @//ma-machine.fil.univ-lille1.fr:3333 identifie le serveur et le port de communication
TCP/IP,
– mabase identifie la base de données ou service sur lequel ouvrir une session.
Dans l’URL Oracle, on peut remplacer thin par oci pour utiliser un pilote de type 2.
DataSource Depuis JDK 1.4, on dispose de l’interface javax.sql.DataSource dont chaque instance
est une fabrique de connexions à la source de données physiques qu’elle représente. L’intérêt
de DataSource est un certain nombre de fonctionnalités supplémentaires (pool de connexions,
transactions distribuées) ainsi que de découpler un peu plus le code applicatif des informations
de type URL.
L’interface DataSource est implémentée par un éditeur de pilote (driver)
Connection getConnection (String username, String password)
Q. 250 Implémenter un DataSource rudimentaire à l’aide du DriverManager et de l’URL
d’Oracle.
Connection correspond à une session (plusieurs transactions successives). Les instructions sont exécutées
et les résultats sont renvoyés dans le contexte d’une connexion.
Par défaut une connexion est en mode auto-commit c’est à dire que chaque instruction SQL est
terminée automatiquement par un commit. On peut changer cela avec une des trois méthodes
192 CHAPITRE 17. DÉVELOPPER UNE APPLICATION BD


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()

Nuplet courant deleteRow() updateRow()

moveToInsertRow() moveToCurrentRow()

Nuplet d’insertion insertRow()

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)

PreparedStatement Un objet représentant une instruction SQL éventuellement précompilée (⇒


efficacité). Un PreparedStatement peut être paramétré en in et exécuté autant de fois que l’on
veut tant que la connexion qui l’a produit n’est pas fermée. C’est donc plus efficace de le créer
une fois pour toutes quand on a besoin d’exécuter fréquemment un même ordre SQL, même si
cette instuction n’a pas de paramètres et même si chaque exécution diffère du fait qu’on modifie
les valeurs des paramètres. Si le SGBD est capable de précompiler (et d’optimiser) lui-même la
requête, alors cette instruction pourra être exécutée efficacement de nombreuses fois.
Exemple de Sun
PreparedStatement pstmt =
con.prepareStatement("update Employe set salaire = ? where id = ?") ;
pstmt.setBigDecimal (1, 153833.00) ; // c’est en euros ???
pstmt.setInt (2, 110592) ; // c’est s^
urement le boss...
pstmt.executeUpdate () ;

 
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.

CallableStatement Pour exécuter un sous-programmes stocké. Un CallableStatement peut être


paramétré en in, out et in out et exécuté autant de fois que l’on veut tant que la connexion
qui l’a produit n’est pas fermée. C’est donc plus efficace de le créer une fois pour toutes quand
on a besoin de l’exécuter fréquemment, même si chaque exécution diffère du fait qu’on modifie
les valeurs des paramètres in.
Voici deux exemples de chaı̂nes exprimant un appel de sous-programme paramétré :
17.3. LES API ABSTRAITES 195

Type de sous-programme Le CallableStatement


Appel de procédure : connexion.prepareCall ("{call emprunter (?,?)}")
Appel de fonction : connexion.prepareCall ("{?=call factorielle (?)}")
Dès que le CallableStatement est créé, il faut spécifier, une fois pour toutes, les types des
paramètres out ou in out ou du résultat de la fonction avec registerOutParameter().

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.

17.3.5 Un exemple d’objet persistant : l’Employe


Cet exemple illustre l’usage d’un PreparedStatement et d’un CallableStatement en proposant un
objet persistant, c’est à dire que son état en mémoire centrale est cohérent avec son état dans la base
de données.
Le chargement individuel de chaque employé n’est pas une approche très efficace, il vaudrait sûrement
mieux charger d’un seul coup tous les employés dont on a besoin.

Voici la table et la procédure stockée abritées par le serveur :

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 :

class Employe { // PARTIE STATIQUE


private static PreparedStatement charger ;
196 CHAPITRE 17. DÉVELOPPER UNE APPLICATION BD

private static CallableStatement augmenter ;


public static void initialiser (Connection cnx) throws SQLException {
charger = cnx.prepareStatement ("select id, nom, salaire from Employe where id = ?") ;
augmenter = cnx.prepareCall ("{call Augmenter (?, ?, ?)}") ;
augmenter.registerOutParameter ("nouveau_sal", Types.INTEGER) ;
}
public static void fermer() throws SQLException{charger.close (); augmenter.close ();}
// PARTIE INSTANCE
private int id ; private String nom ; private int salaire ;
public Employe (final int id) throws SQLException {
Employe.charger.setInt (1, id) ;
ResultSet r = Employe.charger.executeQuery () ;
if (r.next ()) {
this.id = r.getInt("id"); nom = r.getString("nom"); salaire = r.getInt("salaire");
Employe.charger.getConnection ().commit () ;
} else {
Employe.charger.getConnection ().rollback () ;
throw new SQLException ("Employé " + id + " inexistant") ;
}
}
public void augmenter (final int pourcentage) throws SQLException {
Employe.augmenter.setInt ("id", id) ;
Employe.augmenter.setInt ("pourcentage", pourcentage) ;
Employe.augmenter.execute () ;
salaire = Employe.augmenter.getInt ("nouveau_sal") ;
}
public String toString () { return id + " " + nom + " " + salaire ; }
}

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 () ;

17.3.6 Un exemple JDBC : bibliothèque


Voici le code d’une application JDBC qui imprime les titres des livres empruntés et leurs emprunteurs,
puis le nombre total de livres de la bibliothèque (voir l’exemple SQLJ section 17.4.8 page 208). Ce
code est rangé dans le fichier Emprunts.java :

public class Emprunts {


private static void requete (java.sql.Statement stmt, String R) throws SQLException {
ResultSet resultat = stmt.executeQuery (R) ;
final int N_COL = resultat.getMetaData ().getColumnCount () ;
try {
while (resultat.next ()) {
for (int i = 1 ; i <= N_COL ; i++) {
Object o = resultat.getObject (i) ;
System.out.print (resultat.wasNull () ? "*null*" : o.toString ()) ;
}
System.out.println () ;
}
17.4. SQL EMBARQUÉ (INTÉGRÉ) 197

} 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é.

17.3.7 Récupération des valeurs produites pas le SGBD (DML returning)


Voir 7.20.
Lorsque l’application client permet de créer un nouveau livre (insert) et que la clef primaire de ce
nouveau livre est générée par le SGBD, par exemple grâce à une sequence Oracle, il serait intéressant
que l’application puisse connaı̂tre cette clef sans avoir à effectuer ensuite une requête qui risquerait
d’ailleurs d’être plutôt douteuse.

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 SQL Embarqué (intégré)


Il s’agit d’étendre la syntaxe et la sémantique d’un langage classique (Cobol, C, Ada, Java, . . .) pour
permettre d’y intégrer directement du SQL (un peu comme en PL/SQL).

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

17.4.2 La mise en œuvre : voir figure 17.6


Précompilation, compilation puis édition de lien avec une bibliothèque.
198 CHAPITRE 17. DÉVELOPPER UNE APPLICATION BD

Sources langage étendu (SQL embarqué)

Précompilateur

Sources langage hote + appels à la bibliothèque

Compilateur standard

Bibliothèque Fichiers objets

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.

17.4.3 SQL embarqué dans du C


Le SQL embarqué est défini par le standard SQL et supporté par beaucoup d’éditeur de SGBD.
Avantage : facilité du portage d’un SGBD vers un autre. 

Chaque ordre embarqué commence par la chaı̂ne EXEC SQL et se termine par ; . En gros il y a trois
grandes catégories d’ordre embarqué :
Déclaration des variables de liaison , exemple si le langage hôte est le C :
EXEC SQL BEGIN DECLARE SECTION ;
char user [26] ;
VARCHAR nom [20] ;
int nbAnciens ;
EXEC SQL END DECLARE SECTION ;

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

l’occurrence, en cas d’erreur, on appellera la fonction erreur_sgbd ()).

Voici deux autres exemples :


EXEC SQL WHENEVER SQLERROR continue ;
/* en cas d’erreur, on continue l’exécution du programme */
EXEC SQL WHENEVER NOT FOUND DO break;
/* si le dernier FETCH n’a rien trouvé : terminer la boucle */

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 AT Ma_Connexion SELECT count(*) INTO :nbAnciens FROM Vue_Ancien ;

EXEC SQL CREATE TABLE Livre (ref int primary key, titre char (50)) ;

EXEC SQL Insert into Livre values (1, ’Retour à Brooklyn’) ;

EXEC SQL DELETE FROM emp WHERE deptno = :dept_number ;

EXEC SQL ROLLBACK ;


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.

Les curseurs : la déclaration :


EXEC SQL DECLARE Curseur_Tous CURSOR FOR
SELECT a.nom, a.nom_marital
FROM Vue_Ancien a ;

Si on a exploré tout le résultat de la requête, on veut sortir de la boucle d’exploration, il s’agit


d’une déclaration d’intention :
EXEC SQL WHENEVER NOT FOUND DO break ; /* c’est le break du C */

Les opérations sur le curseur : ouvrir le curseur :


EXEC SQL OPEN Curseur_Tous ;

Obtenir la prochaine ligne de la requête (ou sortir de la boucle si not found) :


EXEC SQL FETCH Curseur_Tous
INTO :nom, :nom_marital INDICATOR :nom_marital_ind ;

L’INDICATOR nom_marital_ind permettra de savoir si la colonne nom_marital est définie ou


non (-1 si indéfinie, is null).
Fermer le curseur :
EXEC SQL CLOSE Curseur_Tous ;
200 CHAPITRE 17. DÉVELOPPER UNE APPLICATION BD

17.4.4 Quelques produits


– Oracle : Pro*C/C++, Pro*COBOL, SQLJ,
– le projet GNADE : SQL embarqué dans du Ada 95, avec des API ODBC, PostgreSQL et MySQL
– Postgres : ECPG (Embedded SQL in C ou C++) qui ressemble pas mal au Pro*C/C++ d’Oracle
(c’est un peu normal pour un standard !).

17.4.5 Exemple Postgres : bibliothèque et ECPG (exemple de 17.3.6)


Il s’agit du même exemple que celui traité en JDBC section 17.3.6 page 196.
/* POSTGRES : fichier biblio.pgc, précompilateur : ecpg */
#include <stdio.h>
#include <string.h>

#define SECURE_COPY(DEST, SOURCE) \


if (sizeof (DEST) <= strlen (SOURCE)) { \
fprintf (stderr, "Cha^ıne trop longue : \"%s\"\n", SOURCE) ; \
exit (1) ; \
} \
strcpy (DEST, SOURCE) ;

/*
* 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 */
}

/* prise en compte des erreurs par erreur_postgres () */


EXEC SQL WHENEVER SQLERROR DO erreur_postgres () ;

int main (const int argc, const char * const argv []) {

int nbEmprunts = 0 ; /* pas une variable de liaison */

EXEC SQL BEGIN DECLARE SECTION ; /* variables de liaison */


char user [26] ; /* variable en entrée */
char pw_user [26] ; /* variable en entrée */
int nbLivres ; /* variable en sortie */
VARCHAR nom [5] ; /* variable en sortie, si cha^
ıne trop grande
* tronque et pas de ’\0’ en fin */
char prenom [50] ; /* variable en sortie, si cha^ıne trop grande
* tronque et pas de ’\0’ en fin */
int prenom_ind ; /* indicateur de non valeur (-1 si is null) */
VARCHAR titre [10] ;/* variable en sortie */
17.4. SQL EMBARQUÉ (INTÉGRÉ) 201

EXEC SQL END DECLARE SECTION ;

SECURE_COPY (user, argv[1]) ; SECURE_COPY (pw_user, argv[2]) ;


EXEC SQL CONNECT TO bib@saison.lifl.fr:5432
AS Ma_Connexion
USER :user IDENTIFIED BY :pw_user ;

EXEC SQL DECLARE Les_Emprunts CURSOR FOR


select l.titre, p.nom, p.prenom
from Livre l inner join Personne p on l.emprunteur = p.p_ref ;
EXEC SQL OPEN Les_Emprunts ;
EXEC SQL WHENEVER NOT FOUND DO break;
while (1) {
EXEC SQL FETCH Les_Emprunts INTO :titre, :nom, :prenom INDICATOR :prenom_ind ;
put (titre.len, titre.arr) ; printf (" emprunté par ") ;
put (nom.len, nom.arr) ; printf (" ") ;
if (prenom_ind == -1) printf ("*null*") ; else put (sizeof (prenom), prenom) ;
printf ("\n") ;
}
EXEC SQL CLOSE Les_Emprunts ;

EXEC SQL AT Ma_Connexion SELECT count(*) INTO :nbLivres FROM Livre ;


/* AT Ma_Connexion : utile si on a plusieurs connexions ouvertes. */
printf ("Nombre de livres = %d\n", nbLivres) ;

EXEC SQL DISCONNECT Ma_Connexion ;


exit (0) ;
}

Les commandes sont ensuite :


1. précompilation
ecpg biblio.pgc

2. compilation et édition des liens


gcc -o biblio biblio.c -lecpg -I‘pg_config --includedir‘

3. exécution
./biblio <utilisateur> <mot-de-passe>

17.4.6 Oracle : Pro*C, quelques spécificités


Instructions
– sous Oracle, on peut inclure du code PL/SQL :
EXEC SQL EXECUTE ... END-EXEC ;

– on peut faire du SQL dynamique


Connexion sur une base indiquée autre que celle par défaut :
EXEC SQL DECLARE BD_DU_FIL DATABASE ;
EXEC SQL CONNECT :username IDENTIFIED BY :password
AT BD_DU_FIL USING :db_string ;
202 CHAPITRE 17. DÉVELOPPER UNE APPLICATION BD

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.

Gestion des erreurs

EXEC SQL INCLUDE sqlca ;

SQLCA : SQL Communication Area, pour récupérer les codes et messages d’erreurs qui proviennent
du serveur.

EXEC SQL WHENEVER <condition> <action>;

<condition> ::= SQLERROR | SQLWARNING | NOTFOUND

<action> ::= DO <appel de fonction>


| CONTINUE (ignore l’erreur et continue la séquence)
| GOTO label
| STOP (arr^
et du prgr et rollback)

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.

17.4.7 SQLJ : SQL embarqué dans du Java


Agréé par plusieurs compagnies dont Oracle et Sun, permet le SQL statique et des vérifications
sémantiques par rapport au schéma de base de données dès le prétraitement. Sont nécessaires :
– le précompilateur sqlj (translator) produit un .java à partir d’un .sqlj ainsi qu’un ou plusieurs
profils pour la génération de code standard ISO SQLJ. L’option par défaut -codegen=oracle ne
génère pas de profils on peut avoir les profils avec -codegen=iso, les deux nécessitent un pilote
JDBC.
sqlj invoque ensuite le compilateur java pour produire les .class.
– une librairie runtime SQLJ (SQLJ run time”)
– un pilote JDBC 
SQLJ est capable de vérifier, dès la traduction du source SQLJ en source pur Java, la sémantique du
SQL embarqué par rapport au schéma de la base de données, pour cela il a bien entendu besoin de
 
pouvoir se connecter au SGBD.
Chaque instruction #sql {<instruction SQL>} peut être préfixée par un contexte de connexion et/ou
un contexte d’exécution :
Contexte de connexion : permet à un même programme de travailler avec plusieurs connexions à
une même base de données ou à plusieurs bases de données. S’il n’est pas mentionné, l’instruction
fonctionne sur le contexte de connexion par défaut. Un contexte de connexion dispose d’un
contexte d’exécution par défaut. Chaque thread utilisant le même contexte de connexion doit
disposer de son propre contexte d’exécution.
Contexte d’exécution : toute instruction embarquée est exécutée par rapport à un contexte d’exécution,
s’il n’est pas mentionné il s’agit du contexte d’exécution par défaut.

Les contextes de connexion

Création de la connexion par défaut :


– la méthode directe :
17.4. SQL EMBARQUÉ (INTÉGRÉ) 203

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") ;

Création explicite d’un contexte de connexion :


final sqlj.runtime.ref.DefaultContext
ma_connexion = oracle.sqlj.runtime.Oracle.getConnection(
"jdbc:oracle:thin:@localhost:1521:orcl",
"nom-d-utilisateur",
"mot-de-passe"
) ;

Chaque connexion correspond à une session sur le SGBD.

Deux méthodes statiques de DefaultContext :


setDefaultContext() positionne le contexte connexion par défaut avec le paramètre
getDefaultContext() renvoie le contexte connexion par défaut.
Fermeture d’une connexion explicite et de la connexion par défaut :
ma_connexion.close () ;

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).

Les contextes d’exécution


sqlj.runtime.ExecutionContext
Méthode getExecutionContext() définie sur les contextes de connexion.
Plusieurs méthodes permettent d’obtenir de l’information sur la dernière instruction exécutée dans un
contexte d’exécution :
getWarnings() renvoie un java.sql.SQLWarning contenant le premier avertissement généré
par la dernière instruction exécutée dans ce contexte d’exécution
getUpdateCount() nombre de lignes modifiées
getQueryTimeout()
setMaxRows(int)
getMaxRows()
Création d’un contexte d’exécution : new ExecutionContext(), ce nouveau contexte n’a pas besoin
d’être lié à un contexte de connexion, il peut être utilisé avec différents contextes de connexion.

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

Spécifications des contextes à utiliser


Chaque instruction embarquée peut être préfixée par un contexte de connexion et/ou un contexte
d’exécution :

#sql { instruction SQL }; // connexion et exécution par défaut


#sql [<contexte_connexion>] { instruction SQL }; // exécution par défaut
#sql [<contexte_execution>] { instruction SQL }; // connexion par défaut
#sql [<contexte_connexion>, <contexte_execution>] { instruction SQL };

Les valeurs SQL indéfinies


Les valeurs SQL indéfinies (is null) sont récupérées comme la valeur null de Java. Mais tenter de
récupérer une valeur SQL indéfinie dans une variable Java de type primitif déclenche l’exception
sqlj.runtime.SQLNullException. La solution consiste à récupérer la valeur SQL dans un wrapper
comme java.lang.Integer pour une valeur entière.

Quelques instructions embarquées exécutées immédiatement


Les instructions SQL embarquées en SQLJ sont toujours statiques, ainsi, si le pilote (driver) le permet,
elles pourront s’exécuter plus efficacement que des instruction générées dynamiquement (par exemple
les chaı̂nes de caractères fournies à JDBC).
#sql {
create table Employe (
id Number (5) primary key,
nom Varchar2 (20),
salaire Number (10, 2) check (salaire >= 0)
)
} ;

#sql { insert into Employe values (1, ’toto’, 1000.00) } ;


#sql { insert into Employe (id, nom) values (2, ’titi’) } ;

Une requête devant avoir exactement un résultat :


final int ID = 67890 ;
String nom ;

#sql {
select nom into :nom
from Employe
where id = :ID } ;

System.out.println ("Nom de " + ID + " : " + (nom==null ? "anonyme" : nom)) ;

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

void augmenter_salaire (final int id, final int augmentation)


throws java.sql.SQLException
{
final sqlj.runtime.ExecutionContext ctx_execution =
sqlj.runtime.ref.DefaultContext.getDefaultContext ().getExecutionContext () ;

#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 ;

// Initialisation de la variable itérateur


#sql monIterateur = {Select nom, id from Employe} ;

// 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

Un petit échantillon des types compatibles :

type primitif Java type Oracle


int NUMBER
long NUMBER
float NUMBER
type référence Java type Oracle
Integer INTEGER ou NUMBER
Float NUMBER
Double NUMBER
java.math.BigDecimal NUMBER
String VarChar2
java.sql.Date DATE

Appels de sous-programmes stockés

Appel d’une procédure stockée avec un paramètre en entrée-sortie, un en sortie et un en entrée :


int x = 5, ancien_x, delta = 3 ;

#sql { call Augmenter (:inout x, :out ancien_x, :in delta) ;

// Par défaut le mode est in, on peut donc aussi écrire :


#sql { call Augmenter (:inout x, :out ancien_x, delta) ;

Appel de la fonction stockée sans paramètre Plus_Grand_Salaire :


java.lang.Number salaireMax ;
// ou oracle.sql.NUMBER salaireMax ;
...
#sql salaireMax = { VALUES (Plus_Grand_Salaire) } ;

Embarquement de bloc PL/SQL

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

Prise en compte des exceptions


Rappel : une exception est une condition à traitement délocalisé, c’est à dire qu’elle ne peut pas être
traitée à l’endroit où elle a été détectée : la structure de contrôle if then else ne convient donc pas
pour prendre en compte ce genre de condition.

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 ;
}

Les applications SQLJ peuvent être stockées et exécutées sur le serveur.

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

Ligne de commande SQLJ


Les sources ont l’extension .sqlj.
sqlj <options-java> fichiers.sqlj

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.

17.4.8 Un exemple SQLJ : bibliothèque (exemple de 17.3.6)


Il s’agit du même exemple que celui traité en JDBC section 17.3.6 page 196 et en ECPG section 17.4.5
page 200.
Cette application SQLJ imprime les titres des livres empruntés et leurs emprunteurs, puis le nombre
total de livres de la bibliothèque. Ce code est rangé dans le fichier Emprunts.sqlj :
#sql iterator LivreEmprunteur (String titre, String emprunteur) ;

public class Emprunts {


private static void impLivreEmprunteur () throws java.sql.SQLException {
LivreEmprunteur iter ;
#sql iter = { select titre, nom as emprunteur
-- Erreur qui peut ^
etre détectée dès la compilation :
-- from Livre inner join Personne on emprunteur = p_reff
from Livre inner join Personne on emprunteur = p_ref } ;
while (iter.next()) {
System.out.println (iter.titre () + ", " + iter.emprunteur ()) ;
}
iter.close() ;
}
private static void nbLivres () throws java.sql.SQLException {
int nbLivres ;
#sql {select count (*) into :nbLivres from Livre} ;
System.out.println ("Nombre de livres " + nbLivres) ;
}
public static void main (String[] args) throws java.sql.SQLException {
oracle.sqlj.runtime.Oracle.connect (Emprunts.class, "Emprunts.properties") ;
try {
#sql {set transaction read only} ;
impLivreEmprunteur () ;
nbLivres () ;
} finally {
#sql { commit } ;
oracle.sqlj.runtime.Oracle.close() ;
}
}
}

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

# ou bien les options -user, -password, -url de la commande SQLJ


sqlj.url=jdbc:oracle:thin:@<machine>.<domaine>.fr:1521:<service-de-test>
sqlj.user=test
sqlj.password=<mot-de-passe-de-test>

# Rend compte des problèmes de portabilité vis à vis des extensions


# de SQLJ spécifiques à Oracle
sqlj.warn=portable

# Activation des messages d’information


sqlj.warn=verbose

et fournir ce fichier en argument de la commande de prétraitement sqlj :


sqlj -props=Emprunts-sqlj.properties Emprunts.sqlj

L’erreur sur l’orthographe de la colonne p_ref incorrectement écrite p_reff sera détectée dès le
prétraitement !

Lors de l’exécution le programme utilise le fichier Emprunts.properties pour se connecter à la base,


voici le contenu de Emprunts.properties :
# Informations pour l’exécution

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.

17.4.9 Le même exemple en explicitant le contexte d’exécution


import java.sql.SQLException ;

#sql context EmpruntsCtx ;


#sql iterator LivreEmprunteur (String titre, String emprunteur) ;

public class Emprunts {


private static void impLivreEmprunteur (EmpruntsCtx ctx)
throws SQLException
{
LivreEmprunteur iter ;
#sql [ctx] iter = {
select titre, nom as emprunteur
from Livre inner join Personne on emprunteur = p_ref
} ;
while (iter.next()) {
System.out.println (iter.titre () + ", " + iter.emprunteur ()) ;
}
210 CHAPITRE 17. DÉVELOPPER UNE APPLICATION BD

iter.close();
}

private static void nbLivres (EmpruntsCtx ctx) throws SQLException {


int nbLivres ;
#sql [ctx] {select count (*) into :nbLivres from Livre} ;
System.out.println ("Nombre de livres " + nbLivres) ;
}

public static void main(String[] args) throws SQLException {


oracle.sqlj.runtime.Oracle.connect(Emprunts.class, "Emprunts.properties") ;

try {
EmpruntsCtx ctx = new EmpruntsCtx
(sqlj.runtime.ref.DefaultContext
.getDefaultContext().getConnection()) ;

#sql [ctx] {set transaction read only} ;


impLivreEmprunteur (ctx) ;
nbLivres (ctx) ;

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.

17.5 Outils de développement : AGL


– Oracle : JDeveloper
– MicroSoft : VBA, Access
– Postgres : PGaccess
– Hibernate sur http://www.hibernate.org/
Chapitre 18

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.

Ce qui est abordé d’Hibernate :


– la persistance relativement transparente offerte par Hibernate aux objets Java.
– la gestion de transactions de très longue durée car elles implique des décisions d’un être humain. Ces
très longues transactions ne sont pas implantées par celles du SGBD : cela serait trop coûteux. C’est
Hibernate qui propose de garantir la cohérence des données manipulées par ce genre de transaction
en affectant un numéro (ou une date) de dernière version à certaines de ces données : une mise à
jour d’une telle donnée ne sera acceptée par Hibernate que si la nouvelle valeur a été produite à
partir de la version la plus récente présente en base de données.
– lors de chargement d’objets persitants depuis la base de données, Hibernate peut être amené à
créer plusieurs objets persistants correspondant à une même ligne de table. Si ces chargements se
font pendant la même transaction SGBD (ou session Hibernate), Hibernate garantit l’unicité de
l’objet persistant correspondant à un chargement multiple d’une même ligne.

18.1 Architecture d’utilisation


A priori Hibernate est une couche logicielle d’assez bas niveau puisque son rôle est principalement de
gérer la persistance d’objets Java. On peut penser qu’Hibernate sera principalement localisé à côté du
ou des SGBD avec lesquels il travaille, autrement dit côté serveur, mais pas forcément sur la même
machine puisqu’Hibernate peut utiliser JDBC.

18.2 Quelques principes généraux


Une application Hibernate est paramétrable par des propriétés, par exemple sur le choix du SGBD
utilisé, sans que le changement de ces paramètres nécessite une modification ou une recompilation du
source Java. En fait ces paramètres sont utilisés uniquement à l’exécution de l’application ce qui fait
qu’une erreur dans leur syntaxe ou sémantique ne sera détectée qu’à l’exécution. Ces paramètres sont
principalement indiqués dans les deux fichiers hibernate.properties et hibernate.cfg.xml (voir
section 18.10 page 221).

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).

18.3 Notion de session


Sous Hibernate, le chargement d’objet depuis la base de données ou la mise à jour de celle-ci avec le
nouvel état d’objets mappés ne peut se faire que via une session active.
L’activité d’une session est délimitée par :
– org.hibernate.Session session = sessionFactory.openSession ()
où sessionFactory est un objet créé assez tôt par l’application et qui couvre probablement la
connexion JDBC. Les informations de configuration sont dans le fichier hibernate.properties ou
dans hibernate.cfg.xml. Un de ces deux fichiers doit se trouver dans la racine des sources Java.
– session.close ()

18.4 États des objets mappés de l’application Java


Ces trois états ne concerne que les instances de classes mappées.
persistent (persistant) : cet état n’est possible que quand une session est ouverte, il correspond au
fait qu’Hibernate s’occupe complètement de la persistance de l’objet. L’état de l’objet devient
détaché dès que la session est fermée.
detached (détaché) : précédemment persistant, mais actuellement non associé à une Session, car la
session a été fermée (close()).
transient (éphémère) : associé avec aucune Session, c’est l’état d’un objet qui vient juste d’être créé
par new, que ce soit en dehors où dans une session.
Seul un objet dans l’état persistant peut faire l’objet d’une mise à jour automatique dans la base de
donnée lors d’un flush sur la session.
18.5. CLASSE MAPPÉE ET FICHIER XML (POJO : PLAIN OLD JAVA OBJECTS) 213

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)

Transient Persistant Détaché

new session.delete(o) session.get() session.close()


session.load()

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

18.6 La notion de proxy : procuration, délégation de pouvoir


Un proxy est une politique consistant à différer une action tant qu’on n’a pas besoin de son résultat,
on parle de politique paresseuse (lazy). Les proxies hibernate concernent, entre autre, le chargement
des objets depuis la base de données.
Par exemple, le chargement d’un objet mappé lors d’une requête ne charge a priori pas les autres
objets qu’il peut référencer par ses clefs étrangères. Cependant ces objets référencés seront chargés dès
lors qu’on tentera d’y accéder via l’objet qui les référence, mais ceci à condition de le faire au cours
d’une Session active.
On obtient ce comportement paresseux avec la propriété lazy="true" qui peut être spécifiée à
différents niveaux de précision de la configuration. Par exemple pour forcer le chargement de l’objet
référencé par une clef étrangère lors du chargement de l’objet référençant on peut ajouter le paramètre
lazy="false" à la colonne many-to-one du fichier map de la classe référençante, voir section 18.9.3
page 221.
Ici la paresse n’est pas un défaut : elle reconnaı̂t que le chargement d’objets peut être différé tant
qu’on n’en a pas besoin et permet donc de gagner du temps.

18.7 Architecture logicielle

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.

Classes Java Fichiers de configuration


/hibernate.properties et d’autres (i.e. log4j.properties)
SessionFactory(*) /hibernate.cfg.xml
Les <NomClasse>.hbm.xml des classes mappées
Session(*) Un fichier <NomClasse>.hbm.xml
IDEE : peut etre rangé dans le meme répertoire.
Transaction(1) que le source .java correspondant.
<code>

Les fichiers de configuration :


– La connexion à laquelle correspond une Session est par défaut une connexion JDBC, d’où pour
configurer l’application la nécessité d’indiquer de quel SGBD il s’agit.
Les fichiers Java de org.hibernate :
– SessionFactory mémorise de façon immuable les paramètres de la configuration de l’application.
En général une application ne dispose que d’une instance de SessionFactory.
– Comme le dit la documentation : Session est l’interface centrale de l’abstraction de la persistance.
Sa durée de vie est déterminée par le début et la fin de sa transaction logique.
– Le <code> exécuté pendant une Transaction utilise les méthodes (entre autres) de la Session
ayant fourni cette Transaction. Ces méthodes permettent principalement de gérer la persistance
des objets qu’elles manipulent. Par exemple, les objets persistants modifiés ou créés ou détruits,
grâce aux méthodes de Session, seront l’objet d’update, d’insert ou de delete lors d’un flush()
sur la session courante ou lors du commit() de la transaction.

18.8 Deux classes et beaucoup d’interfaces


Le nombre important d’interfaces a la même signification qu’en JDBC : les interfaces fixent les fonction-
nalités que le programmeur poura utiliser, en revanche chaque interface est probablement implantée
18.8. DEUX CLASSES ET BEAUCOUP D’INTERFACES 215

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.

18.8.1 Interface org.hibernate.SessionFactory


Le tout premier objet à créer, il est ensuite immuable. Il est obtenu par :
new org.hibernate.cfg.Configuration ().configure ().buildSessionFactory () ;
qui entre autre lit les fichiers <nom-classe>.hbm.xml pour mettre en place le cadre de persistance.
– Session openSession()
crée une connexion et ouvre une Session sur celle-ci.
– void close()
ferme cette SessionFactory en relâchant toutes les ressources : les caches, le jeu de connexions,
. . .. Toutes les Session doivent avoir été fermées au préalable.

18.8.2 Interface org.hibernate.Session


La classe centrale offrant la notion de persistance ! Le cycle de vie d’une Session est borné par le début
et la fin d’une transaction logique (une longue transaction logique peut être réalisée par plusieurs
transactions du SGBD).
C’est seulement lorsqu’une Session est ouverte, ainsi qu’une transaction que les objets des classes
persistantes peuvent profiter de cette persistance, en général la mise à jour de la base de données se
fait de façon optimisée lors de la validation de la transaction logique (méthode commit())

Méthodes pour gérer une Session


Le terme anglais flush signifiera ici synchroniser l’état de la base de données avec celui de la mémoire
centrale, c’est à dire que lors d’un flush, Hibernate prendra en compte toutes les modifications effectuées
sur les objets persistants pour les traduire en ordres SQL qu’il fait exécuter pas le SGBD. Ces ordres
SQL pourront être des insert, update ou delete.
– void setFlushMode (org.hibernate.FlushMode flushMode)
par exemple
org.hibernate.FlushMode.COMMIT
Le flush() de la Session aura lieu quand Transaction.commit() sera exécuté.
org.hibernate.FlushMode.MANUAL
Il faudra appeler explicitement la méthode flush() poru synchroniser l’état de la base de
données avec celui de la mémoire centrale (flush).
– void flush ()
Le flush1 consiste à exécuter les ordres SQL permettant de synchroniser l’état de la base de données
avec celui de la mémoire centrale.
1
flush = faire jaillir, nettoyer à grande eau, to flush the lavatory = tirer la chasse d’eau.
216 CHAPITRE 18. INTRODUCTION À HIBERNATE

– 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 :

session.flush () ; // Effectue toutes les modification en mémoire centrale dans la BD


session.clear () ; // Détruit toutes les instances mappées ainsi que save update delete

Démarrer une Transaction pour cette Session


– org.hibernate.Transaction t = session.beginTransaction()

Méthodes de Session gérant les objets persistants


– void delete (Object object)
Supprime l’object persistant, suppression en cascades si cascade="delete" dans le mapping.
– Object get (Class classeMappee, Serializable id)
classeMappee est une classe mappée, c’est à dire que dans la base de données lui correspond une
table. Renvoie soit :
– une nouvelle instance de la classe classeMappee initialisée avec les informations trouvées dans la
base de données pour la ligne identifiée par la valeur de id. Cette instance est créée persistante.
– null si aucune ligne de la table de la base de données n’a comme clef la valeur de id
Un exemple relatif à celui des sections 18.9.1 page 218 et 18.9.2 page 219 :
Responsable leader = session.get (personnel.Responsable.class, new Long (51)) ;

– Object load (Class theClass, Serializable id)


Comme get(), mais l’objet doit exister dans la base de données sinon c’est une erreur, exception
HibernateException. Dans la mesure où l’entité est censée exister dans la base de données, il est
aussi possible de charger dans une instance existante qui doit être transient :
personnel.Responsable r = new personnel.Responsable () ;
session.load (r, new Long (55)) ;
– void lock(Object object, org.hibernate.LockMode lockMode)
La classe LockMode définit des constantes, par exemple :
– LockMode.NONE qui ne demande pas de verrou, sauf éventuellement un verrouillage en lecture
(Oracle ne verrouille pas les lignes en lecture). Permet de réassocier object avec la session.
– LockMode.UPGRADE verrouillage de mise à jour matérialisé par un select ... for update
L’option cascade="lock" dans les fichiers de mapping pour appliquer le verrouillage aussi aux
entités référencées par des clefs étrangères.
18.8. DEUX CLASSES ET BEAUCOUP D’INTERFACES 217

– Object merge (Object object)


Copie l’état de l’objet donné sur l’objet persistant ayant le même identifiant, avec éventuellement
chargement de ce dernier. Renvoie l’instance mise à jour et persistante.
cascade="merge" pour appliquer la fusion aussi aux entités associées par des références de clef
étrangères.
– void persist (Object object)
L’objet transient devient persistant. L’objet n’est pas forcément identifié immédiatement, mais il
sera sauvé dans la base au moins lors du flush() de la session. Si persist() est appelé alors qu’il
n’y a pas de transaction en cours alors il est garanti qu’il ne fera pas de insert dans la BD.
cascade="persist" pour appliquer la persistance aussi aux entités associées par des références de
clef étrangères.
– Serializable save (Object object)
L’objet transient devient persistant. L’identifiant de object est généré automatiquement si la map
de la classe déclare un générateur pour cette clef primaire. On peut considérer l’appel à cette
méthode comme un préalable à une instruction insert de SQL. Renvoie l’identifiant (i.e. la valeur
Java de la clef primaire).
cascade="save-update" pour appliquer la même opération aux entités associées par des références
de clef étrangères.
– void saveOrUpdate (Object object)
Fait, suivant l’état de object :
– un save() si object est transient,
– un update() si object est détaché.
object passe donc dans l’état persistant.
cascade="save-update" pour appliquer la même opération aux entités associées par des références
de clef étrangères.
– void update (Object object)
L’instance détachée object devient persistante.
cascade="save-update" pour appliquer la même opération aux entités associées par des références
de clef étrangères.

Méthodes fabricant une requête de la Session

– org.hibernate.Query createQuery (String queryString)


crée une requête avec queryString en syntaxe HQL

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.

La création des transactions est liée à la propriété hibernate.transaction.factory_class qui in-


dique la fabrique (factory) à utiliser. Par défaut cette fabrique est JDBCTransactionFactory qui four-
nit des JDBCTransaction, ces deux classes se trouvant dans le paquetage org.hibernate.transaction.

On décrit ici le fonctionnement des méthodes de ces JDBCTransaction :


void commit() Fait le flush() de la Session associée. La transaction correspondante du SGBD
est elle aussi validée (commit) mais seulement si elle a été démarrée par cette transaction
logique.
void rollback() Force la transaction correspondante du SGBD à faire un rollback.

18.9 Correspondance entre les classes persistantes et les entités (tables)


correspondantes de la base de données
Pour qu’une instance soit potentiellement persistante, il est obligatoire que sa classe soit mappée (ce
mot existe dans le Petit Robert).

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.

18.9.1 Les classes qui seront mappées


package personnel ;
public class Responsable {
private Long id ; // type référence préférable car pointeur null
private String nom ;
private int nbRayons ;
private Responsable () { id = null ; nom = null ; nbRayons = 0 ; }
public Responsable (String nom) {
this.id = null ; // affecté par Hibernate, voir Responsable.hbm.xml
this.nom = nom ; this.nbRayons = 0 ;
}
void setId (Long id) { this.id = id ;}
public Long getId () {return id ;}

public void setNom (String nom) { this.nom = nom ;}


public String getNom () { return nom ;}
18.9. CORRESPONDANCE ENTRE LES CLASSES PERSISTANTES ET LES ENTITÉS (TABLES) CORRESPON

public void setNbRayons (int nbRayons) { this.nbRayons = nbRayons ;}


public int getNbRayons () { return nbRayons ;}
public void addNbRayons (int n) { this.nbRayons += n ;}
}

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 ;

private Rayon () { id = null ; nom = null ; responsable = null ; }


public Rayon (String nom, personnel.Responsable responsable) {
this.id = null ; this.nom = nom ; this.responsable = responsable ;
}
public setResponsable (personnel.Responsable resp) {
if (responsable != null) { responsable.addNbRayons (-1) ;}
responsable = resp ;
if (responsable != null) { responsable.addNbRayons (+1) ;}
} ...
}
Remarquer que les constructeurs sans paramètres peuvent être privés : ainsi l’application ne pourra
pas fabriquer d’objet non ou mal initialisés.

18.9.2 Les map de ces deux classes Responsable et Rayon


Les fichiers de mapping sont alors :
– Le mapping de la classe personnel.Responsable : personnel/Responsable.hbm.xml
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="personnel.Responsable" table="Responsable">

<id name="id" type="long" column="id">


<generator class="sequence">
<param name="sequence">Seq_Responsable</param>
</generator>
</id>

<property name="nom" column="nom" length="20"/>

</class>
</hibernate-mapping>

<class> indique qu’à la classe Java personnel.Responsable correspondra la table Responsable


dans la base de données.

<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.

– Le mapping de la classe magasin.Rayon : magasin/Rayon.hbm.xml


<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="magasin.Rayon" table="Rayon">

<id name="id" type="long" column="id">


<generator class="sequence">
<param name="sequence">Seq_Rayon</param>
</generator>
</id>

<property name="nom" column="nom" length="50"/>

<many-to-one lazy="true" name="responsable" column="responsable"


foreign-key="Rayon_Responsable_FK" class="personnel.Responsable"
not-null="false" cascade="none" />
</class>
</hibernate-mapping>
<class> indique qu’à la classe Java magasin.Rayon correspondra la table Rayon dans la base de
données.

<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

à une clef étrangère dans l’entité Rayon.

notation francaise notation anglaise


1 n n 1
Rayon dirige Responsable Rayon dirige Responsable
many to one

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>

Une autre solution est d’utiliser la méthode suivante de org.hibernate.Hibernate :


– public static boolean isInitialized (Object proxy)
Dit si le proxy est initialisé.
– public static void initialize (Object proxy)
Initialise le proxy.

18.10 Les deux fichiers principaux de configuration


Il s’agit des deux fichiers hibernate.properties et hibernate.cfg.xml qu’on peut maintenir dans
le répertoire racine des sources Java et qu’il faut copier dans le répertoire racine des classes car c’est
là qu’Hibernate les cherchera.
– src/hibernate.properties
C’est l’ancien fichier de configuration d’Hibernate dans un format texte classique, par exemple :
222 CHAPITRE 18. INTRODUCTION À HIBERNATE

## 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

## driver JDBC, URL de connexion


hibernate.connection.driver_class oracle.jdbc.driver.OracleDriver
hibernate.connection.url jdbc:oracle:thin:@//vlaskop.fil.univ-lille1.fr:1521/filora10

hibernate.connection.username <un-nom>
hibernate.connection.password <un-mot-de-passe>

## nombre minimum de connexions dans le pool (fond commun)


hibernate.c3p0.min_size 5
## nombre maximum de connexions dans le pool
hibernate.c3p0.max_size 20
...

## voir le source SQL généré et exécuté


hibernate.show_sql true
## ce source est proprement formaté !
hibernate.format_sql true

## hbm2ddl signifie Hibernate mapping vers DDL


## l’option create : détruit la base puis la recrée chaque fois
## que l’application est démarrée
hibernate.hbm2ddl.auto create

## Spécifier le niveau d’isolation JDBC défini par une


## des constantes de l’interface java.sql.Connection :
## public static final int TRANSACTION_NONE 0
## public static final int TRANSACTION_READ_COMMITTED 2
## public static final int TRANSACTION_READ_UNCOMMITTED 1
## public static final int TRANSACTION_REPEATABLE_READ 4
## public static final int TRANSACTION_SERIALIZABLE 8

## On choisit Read Committed :


hibernate.connection.isolation 2

– 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>

<!-- JDBC connection pool (use the built-in) -->


<property name="connection.pool_size">1</property>

<!-- Enable Hibernate’s automatic session context management -->


<property name="current_session_context_class">thread</property>

<!-- Disable the second-level cache -->


<property name="cache.provider_class">org.hibernate.cache.NoCacheProvider</property>

<!-- Supprime puis recrée la base de données à chaque démarrage de l’application


"hbm2ddl" signifie Hibernate mapping vers DDL, autrement dit Hibernate fabrique
les ordres SQL de création de la base de données (les tables avec leurs contraintes,
les générateurs d’entiers, ...) à partir des fichiers de mapping, par exemple
"modele/donnee/Livre.hbm.xml". C’est ce qu’on appelle de la conception descendante,
on part de la spécification de haut niveau pour créer une implantation.
1) construire le schéma SQL en fonction du dialecte, ... et des fichiers
de mapping des classes persistantes
2) supprimer, dans le le SGBD, toutes les tables du schéma et autres
objets comme les séquences destinées à produire des valeurs de clef,
3) re-crée dans le SGBD, toutes les tables et autres objets, lorsque la
SessionFactory est construite, autrement dit repartir de zéro à chaque exécution.
Mettre en commentaire si vous ne voulez pas. -->
<property name="hbm2ddl.auto">create</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>

<!-- Spécifier le niveau d’isolation JDBC défini par une


des constantes de l’interface java.sql.Connection :
TRANSACTION_NONE 0 | TRANSACTION_READ_UNCOMMITTED 1
TRANSACTION_READ_COMMITTED 2 | TRANSACTION_REPEATABLE_READ 4
TRANSACTION_SERIALIZABLE 8 --> <!-- Read committed : -->
<property name="connection.isolation">2</property>

<!-- Fonctionnement par défaut de JDBC : autocommit de chaque


instruction par défaut (déconseillé). -->
<property name="connection.autocommit">false</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>

<!-- Voir le SQL envoyé au SGBD--> <property name="show_sql">true</property>


224 CHAPITRE 18. INTRODUCTION À HIBERNATE

<!-- Et bien formaté --> <property name="format_sql">true</property>


<!-- Avec des commentaires --> <property name="use_sql_comments">true</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.

18.11 Création de la SessionFactory


Cette création doit se faire au tout début de l’exécution de l’application, par exemple de cette manière :
package persistance ;
import org.hibernate.cfg.Configuration ;
public class HibernateUtil {
private static final org.hibernate.SessionFactory sessionFactory ;
static {
try {
sessionFactory = new Configuration().configure().buildSessionFactory() ;
} catch (Throwable ex) {
System.err.println("SessionFactory non créée. " + ex.getMessage ()) ;
throw new ExceptionInInitializerError (ex) ;
}
}
public static org.hibernate.SessionFactory getSessionFactory () {
return sessionFactory ;
}
public static void shutdown () { // fin de l’application
sessionFactory.close () ;
}
}
L’initialisation de la SessionFactory faite par du code static est faite lors du chargement en mémoire
de la classe HibernateUtil. Cette initialisation prend en compte les fichiers de configuration dans
l’ordre suivant :
1. le contenu de classes/hibernate.properties
2. le contenu de classes/hibernate.cfg.xml
3. les contenus de tous les mapping mentionné dans classes/hibernate.cfg.xml dans notre
exemple, il s’agit de personnel/Responsable.hbm.xml et magasin/Rayon.hbm.xml

18.12 Un outil pour assurer plus simplement la persistance


Une instance d’une classe mappée n’est pas forcément persistante. Elle ne peut l’être que pendant
qu’une Session et une Transaction sont ouvertes et si elle a été passée en paramètre à une des
méthodes de la Session.
Un exemple typique de code rendant persistantes les modifications faites à un objet :
18.12. UN OUTIL POUR ASSURER PLUS SIMPLEMENT LA PERSISTANCE 225

void renommer (Responsable leader, String nouveauNom) throws Exception {


leader.setNom (nouveauNom) ;
final org.hibernate.Session session =
persistance.HibernateUtil.getSessionFactory ().openSession () ;
session.setFlushMode (org.hibernate.FlushMode.COMMIT) ;
org.hibernate.Transaction tx = null ;
try {
tx = session.beginTransaction () ;
//
// (1) leader est supposé détaché (autrement dit il a déjà été persistant)
//
session.update (leader) ;
//
// (2) leader est persistant
//
tx.commit () ;
} catch (Exception e) {
if (tx != null) tx.rollback () ;
throw e ;
} finally {
session.close () ; // fermeture garantie de la session
}
//
// Ici, leader est détaché
//
}

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 {

public final void executionPersistante () throws Throwable {


final org.hibernate.Session session =
persistance.HibernateUtil.getSessionFactory ().openSession () ;
org.hibernate.Transaction tx = null ;
try {
tx = session.beginTransaction () ;
faire (session) ;
tx.commit () ;
} catch (org.hibernate.HibernateException exp) {
if (tx != null) tx.rollback () ;
if (exp.getThrowableCount () == 1) throw exp ;
else throw exp.getCause () ;
} finally {
session.close () ;
}
}
226 CHAPITRE 18. INTRODUCTION À HIBERNATE

/** 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().

18.12.1 Un exemple d’utilisation de l’outil TraitementPersistant


Par exemple le renommage d’un responsable donné en 18.12 peut se faire comme suit avec une classe
interne et statique :

class Traitement { ...


private static class Renommer extends persistance.TraitementPersistant {
private Responsable leader ;
private String nouveauNom ;
public void set (Responsable leader, String nouveauNom) {
this.leader = leader ; this.nouveauNom = nouveauNom ;
}
protected void faire (org.hibernate.Session session) throws Exception {
leader.setNom (nouveauNom) ; session.update (leader) ;
}
}
private static final Renommer renommer = new Renommer () ;
void renommer (Responsable leader, String nouveauNom) throws Throwable {
renommer.set (leader, nouveauNom) ; renommer.executionPersistante () ;
}
}

Q. 255 Écrire la classe static qui supprime un Responsable et la méthode qui l’utilise.

18.13 Le langage HQL


Permet principalement d’écrire des requêtes pour createQuery().

18.14 Les transactions


Si Hibernate fonctionne au dessus de JDBC, alors il nécessite que le auto commit soit à faux.

La durée de vie d’une session correspond à exactement une transaction.

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.

Pour mettre cela en place Hibernate propose :


– une gestion automatique de versions qui permet de savoir si une modification concurrente a été
faite pendant la réflexion de l’utilisateur. Cette vérification se fait généralement en fin de dialogue.
Utiliser le tag <version> pour qualifier l’attribut Java et la colonne de la table contenant le numéro
de version le plus récent dans la map de la classe.
Par exemple, on veut pouvoir éditer des messages en garantissant qu’une mise à jour ne sera enregistrée
dans la base que si elle a été faite à partir de la version la plus récente du message :

<hibernate-mapping>
<class name="modele.Message" table="Message">

<id name="id" type="long" column="id">


<generator class="sequence">
<param name="sequence">Seq_Message</param>
</generator>
</id>

<version name="num_version" column="num_version"


type="long" generated="never" insert="true"/>

<property name="contenu" column="contenu" length="20"/>

</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

Utilisateur Fatigué Utilisateur EnForme


Charge le message d’id 57 (version 3)
Charge le message d’id 57 (version 3)
Fatigué et EnForme ont chacun un objet Java de type Message contenant
exactement les mêmes informations.
Réfléchit sur le contenu Réfléchit sur le contenu
... ...
s’endort ? ...
... modifie le contenu
Sauve dans la BD le message d’id 57 avec son
nouveau contenu. Cela a pour effet de faire pas-
ser la version à 4 dans la BD et dans l’objet Java
de EnForme.
Hibernate autorise la persistance de cette nou-
...
velle version car elle a bien été fabriquée à par-
tir de la version immédiatement précédente. En
fait, lors de cette modification, Hibernate com-
mence par lire le numéro de version actuellement
dans la BD.
Se réveille ! goodbye
Modifie le contenu de son objet Java puis tente
de sauver dans la BD. Hibernate se rend compte
que la BD contient la version 4 alors que Fa-
tigué tente de faire persister une nouvelle va-
leur fabriquée à partir de la version 3. Hiber-
nate déclenche donc une erreur pour demander
à annuler cette tentative.

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.

18.16 Exceptions Hibernate non récupérables


Par définition les exceptions Hibernate ne sont pas récupérables : on ne peut pas réparer le problème
qu’elles signalent. Suite à une telle exception il faut alors absolument faire un rollback() de la
transaction puis un close() de la session et enfin redéclencher cette exception.

18.17 Le verrouillage pessimiste


Hibernate utilise le système de verrouillage du SGBD.

18.18 Un outil pour assurer plus simplement la persistance


Il s’agit d’une classe abstraite dont la méthode executionPersistante() met en place la session et
la transaction nécessaires à une exécution persistante :
package persistance ;
18.18. UN OUTIL POUR ASSURER PLUS SIMPLEMENT LA PERSISTANCE 229

public abstract class TraitementDeSession {

public final void executionPersistante () throws Throwable {


final org.hibernate.Session session =
persistance.HibernateUtil.getSessionFactory ().openSession () ;
org.hibernate.Transaction tx = null ;
try {
tx = session.beginTransaction () ;
faire (session) ;
tx.commit () ;
} catch (org.hibernate.HibernateException exp) {
if (tx != null) tx.rollback () ;
if (exp.getThrowableCount () == 1) {
throw exp ;
} else {
throw exp.getCause () ;
}
} finally {
session.close () ;
}
}
/** Méthode exécutée avec une session active passée en paramètre. */
protected abstract void faire (org.hibernate.Session session) throws Exception ;
}
Septième partie

Bases de données objet et compromis


du relationnel-objet

230
Chapitre 19

Le modèle objet

Pourquoi un modèle objet


Applications qui nécessitent des SGBDO
Nouvelles applications des BD faisant intervenir des informations à structure complexe.

R.G.G. Cattell. Object Data Management

– Ateliers de Génie Logiciel (AGL) : conception, spécification, implémentation, analyse, debogage,


maintenance et évolution de programmes et de documents.
– Conception Mécanique Assistée par Ordinateur (MCAD) : véhicules spaciaux, bâtiments ...
– Conception Electronique Assistée par Ordinateur (ECAD) : conception logique et physique.
– Fabrication Assistée par Ordinateur (FAO) : voitures sur une chaine de montage, synthèse chimique
...
– Bureautique : gestion de l’information d’une entreprise (mail, documentation, ...)
– Publication assistée par ordinateur (PAO) et Hypertextes : manip de documents complexes, docu-
ments à comportement dynamique.
– Graphiques : représentations graphiques d’objets complexes (souvent en lien avec la CAO et PAO)
– Applications Scientifiques et Médicales : manipulation et analyse de représentations chimiques, bio-
logiques, physiques.
– Services systèmes
– Fabrication et contrôle temps réel
– Les bases de connaissances
Les avantages généraux des objets : union de donnée et de code, implémentation cachée.
Disposer d’une bonne intégration entre langage déclaratif (type SQL ou L4G) et langage impératif
(L3G). Un peu comme le fait PL/SQL dans Oracle.

19.1 Navigation : le retour


 
Dans le modèle relationnel, une instance d’entité (une ligne) est identifiée par son contenu (par exemple
sa clef primaire). Pour retrouver une ou des instances particulières on est alors amené à effectuer une
 
recherche associative (i.e. par le contenu).

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 :

select v.numéro, p.nom


from Voiture v inner join Personne p on v.proprietaire = p.id ;
232 CHAPITRE 19. LE MODÈLE OBJET

' $
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 ;

19.1.1 SGBD et OID


– Oracle : Chaque objet d’une table objet se voit attribué un OID système de 16 octets unique dans
la table.
Il est aussi possible d’utiliser un OID basé sur la clef primaire de l’objet : ceci évite de consommer
les 16 octets de l’OID système et l’index qui va avec, et les chargements de table seront plus rapides.
Les références d’objet (REF), qui permettent la navigation, peuvent n’être basées que sur cet OID
(système ou clef primaire) dans le cas de références à portée limitée à une table (scoped REF).
Elles peuvent aussi être capables de désigner un objet sans qu’on connaisse a priori la table qui le
contient. Bien entendu, ces dernières références occupent plus de place.
– Postgres : la colonne système oid pas forcément unique car seulement 4 octets mais on dispose aussi
de la colonne implicite tableoid, et ctid (couple numéro de block, indice du tuple dans le bloc)
pour Postgres).
Mais pratiquement, que peut-on faire de ces informations ? ? ?

19.1.2 Problématique des OID


– les OID doivent rester constants lors des mises à jour de tuple, en effet lors d’une mise à jour, un
tuple peut carrément être déplacé sur le disque. Ainsi le ctid de Postgres qui est l’adresse physique
du tuple dans sa table ne peut servir d’OID.
– Pour manipuler un objet persistant, il faut d’abord le charger en mémoire centrale depuis le disque.
L’OID doit permettre de désigner l’objet qu’il soit ou non chargé en mémoire.

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.

– Plusieurs transactions peuvent tenter d’accéder au même objet, . . .


– Qu’advient-il des OID des objets détruits ? (prédicat IS DANGLING en Oracle).

33 AAA 59 Transaction 1
33 AAA 59 11 VVV 75

Dupont Transaction 2
11 VVV 75

Fig. 19.1 – Quelques objets chargés en mémoire centrale


19.2. L’ORIENTÉ OBJET : LES DEUX APPROCHES 233

19.2 L’orienté Objet : les deux approches


– puriste : on fait de l’objet pur (SGBDO)
– pragmatique : on introduit la notion d’objet au-dessus du relationnel (SGBDRO)

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.

19.3.1 Bourse ODL


La figure 19.2 donne un diagramme UML modélisant un système d’informations de la bourse.

porte_feuilles
Valeur * * Negociateur
* *
nb_titres

1 Ordre 1

Offre Demande

Fig. 19.2 – Diagramme de classes UML du SI de la bourse

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).

class Valeur (extent Les_Valeurs) {


attribute String nom ;
attribute Float cours ;
}

// Pour le porte-feuilles d’un négociateur, c’est à


// dire l’association N:N attribuée par le nombre de
// titres entre un Négociateur et une valeur,
// on a besoin d’introduire une entité pour cet attribut
// Remarque : on fait quelque chose de très similaire en relationnel
// en créant une relation pour chaque association N:N
234 CHAPITRE 19. LE MODÈLE OBJET

class Element_De_Porte_Feuilles (extent Les_Elements) {


attribute Int nb_titres ;
attribute Valeur la_valeur ;
relationship Negociateur le_negociateur
inverse Negociateur::porte_feuilles ;
}

-- La clef ’id’ permettra un accès associatif


class Negociateur (extent Les_Negociateurs key id) {
//
// Déclaration des attributs (variables d’instance)
//
attribute Short id ;
attribute String nom ;
attribute Short solde ;
//
// Déclaration des relations où associations
//
relationship list<Ordre> les_ordres
inverse Ordre::le_negociateur ;
relationship set<Element_De_Porte_Feuilles> porte_feuilles
inverse Element_De_Porte_Feuilles::le_negociateur ;
//
// Déclaration des méthodes
//
short capital () ;
}

-- La relation ’la_valeur’ permettra un accès navigationnel


class Ordre (extent Les_Ordres) {
attribute Short nb_titres ;
attribute Date la_date ;

relationship Valeur la_valeur ;


relationship Negociateur le_negociateur
inverse Negociateur::les_ordres ;
}

class Offre extends Ordre {


attribute Short prix_min ;
}

class Demande extends Ordre {


attribute Short prix_max ;
}

La collection Les_Ordres va contenir à la fois les Offre et les Demande.

19.3.2 À quoi sert ODL ?


Toutes les déclarations ODL sont abstraites :
– les types de base comme Short n’ont pas d’implantation définie,
– les méthodes ne sont que déclarées.
Le schéma objet décrit en ODL sert à deux choses :
19.3. SGBDO 235

– 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.

19.3.3 Bourse OQL


Les noms des négociateurs qui ont un capital d’au moins 5000 :

select n.nom
from n in Les_Negociateurs
where n.capital() >= 5000 ;

À noter l’utilisation de la méthode d’instance capital().

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)
) ;

On ne peut pas leur adjoindre d’autres colonnes.


les tables relationnelles comme d’habitude dont certaines colonnes peuvent être d’un type
objet :
create table Relationnelle (
a Adresse,
loyer Number (5, 2),
constraint Relationnelle_PK primary key (a.numero),
constraint Relationnelle_Prix_Positif check (1 <= a.numero)
) ;

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.

20.2 Types objet et méthodes


La définition d’un type objet se fait comme suit :
create type Personne as object (
nom VARCHAR2 (20),
naissance Date,
member function Age(AujourDhui Date) return Natural -- méthode d’instance
) not final ; -- est héritable
member indique que la fonction Age est une méthode d’instance.
not final est en rapport avec la possibilité de déclarer des sous-types qui hériteront de Personne. Par
défaut un type est final.
L’implantation des méthodes se fait dans le body :
create or replace type body Personne as
member function Age (AujourDhui Date) return Natural is
begin
return floor (months_between (AujourDhui, self.naissance) / 12) ;
end Age ;
end ;
self est le this de Java et comme en Java, il n’est obligatoire qu’en cas d’ambiguı̈té.

20.2.1 Appel d’une méthode


Cela se fait en préfixant la méthode en préfixant la méthode par l’objet sur lequel on veut l’exécuter :
create or replace function Majeur (P in Personne) return Varchar2 is
begin
if P.Age (sysdate) < 18 then
return ’mineur’ ;
else
return ’majeur’ ;
end if ;
end Majeur ;

20.2.2 Créer un nouveau type par composition


La composition peut servir à définir :
– un autre type (élément d’une collection, attribut, d’un type objet ...),
– le type d’une colonne de table (table relationnelle),
– le type des lignes d’une table objet.
Le type Etudiant va être composé de deux attributs :
create type Etudiant as object (
p Personne,
a Adresse,
member function Age (AujourDhui Date) return Natural
) ;

Q. 256 Écrire le body de Etudiant.


Pour l’appel d’une méthode on remarque la notation préfixée par l’objet sur lequel la méthode doit
s’appliquer (comme en Smalltalk, Java, C++, Ada 2005).
238 CHAPITRE 20. LE RELATIONNEL-OBJET DE ORACLE

20.2.3 Surcharge possible des méthodes (overloading)


Plusieurs méthodes de même nom peuvent coexister dans le même type si elles n’ont pas la même
signature. La signature d’une méthode comporte son nom, le nombre, les types et l’ordre de ses
paramètres y compris le paramètre implicite self.

20.3 Oracle 8 ne propose pas l’héritage

20.4 Oracle 10 propose l’héritage simple


create type <nom-du-sous-type> under <super-type>
([overriding] member ..., ...) [[not] final]; -- final par défaut
Par défaut un type objet est final : il ne peut avoir de sous-types.
Un sous-type :
– hérite des attributs et méthodes de son super-type,
– peut définir de nouveaux attributs et de nouvelles méthodes,
– peut redéfinir des méthodes héritées (grâce au qualificatif overriding).
En principe un sous-type doit définir au moins un nouvel attribut ou un nouveau sous-programme
membre ou une redéfinition d’une méthode héritée.

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.

20.4.1 Créer un nouveau type par héritage


create type Employe under Personne (salaire Number (10, 2)) not final ;
create type Stresse under Employe (stress Number (1)) not final ;
create type Programmeur under Stresse (email Varchar2 (30)) final ;
create type Secretaire under Stresse (tel Varchar2 (20)) ;
Les attributs nom et naissance et la méthode Age sont hérités par Employe. Seuls les nouveaux
attributs sont déclarés, par exemple le salaire de Employe.
Les types Programmeur et Secretaire ne pourront pas avoir de sous-types car il sont final explicite-
ment ou par défaut.

20.4.2 Redéfinition de méthode : overriding


La redéfinition (override) n’a rien à voir avec la surcharge (overloading voir section 20.2.3 page 238).
Une redéfinition de méthode a nécessairement la même signature que la méthode héritée redéfinie.
Une redéfinition doit fournir les mêmes éventuelles valeurs par défaut à ses paramètres que la méthode
redéfinie.

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).

20.4.3 Compatibilité d’un type avec ses super types (substitutable)


Là où on peut mettre un objet d’un type T on peut aussi mettre tout objet d’un sous-type de T ,
même si le sous-type de T a été créé après la structure d’accueil.
20.4. ORACLE 10 PROPOSE L’HÉRITAGE SIMPLE 239

create table Les_Personnes of Personne (


constraint PK_Les_Personnes primary key (nom)
) ;

insert into Les_Personnes values (Personne (’toto’, null)) ;

create type Internaute under Personne (email Varchar2 (30)) not final ;

insert into Les_Personnes values (Internaute (’Dufour’, null, ’Dufour@b.fr’)) ;


Cela est vrai pour les références, les attributs objets d’un objet, les colonnes objet de tables relation-
nelles, les tables objets et les collections.
Cette compatibilité peut cependant être désactivée au niveau stockage pour des tables ou des colonnes
spécifiques. Voir l’option not substitutable de create table.

20.4.4 Valeur littérale d’objet : les constructeurs de valeur


Tout type objet définit implicitement un constructeur permettant d’exprimer littéralement la valeur
d’un objet. Le nom d’un constructeur est le nom du type objet et il possède exactement autant de
paramètres que l’objet a d’attribut. Si on ne veut pas renseigner certains attributs, il faudra donc
écrire explicitement null.

Exemples d’objets littéraux exprimés grâce aux constructeurs :


Personne (’Dupont’, ’21/12/1975’)
Employe (’Truc’, ’12/6/1986’, 2000.0)
Programmeur (’Stressé’, ’02/6/1980’, 2000.0, 9, ’aa@fr’)
Adresse (null, null, ’Lille’)
Etudiant (Personne (’Dupont’, null),
Adresse (12, ’Charcot’, ’Lille’))
Tester la méthode Age :
select Personne(’Boyle’, to_date (’23/9/1989’,’dd/mm/yyyy’)).Age(sysdate)
from dual ;

20.4.5 Créer une table objet : structure de stockage


Une table objet est constituée d’une seule colonne du type de l’objet. Les contraintes classiques peuvent

être exprimées sur les attributs de l’objet constituant la table, par exemple ici la clef primaire est le
 
nom de l’étudiant (remarquez la notation p.nom ).
create table Les_Etudiants of Etudiant (
constraint PK_Les_Etudiants primary key (p.nom)
) ;
Pour retrouver les étudiants de la table Les_Etudiants en tant qu’objets de type Etudiant il faut
utiliser la fonction Value, sinon on verra simplement une ligne d’une table relationnelle dont les
colonnes sont les attributs de l’objet (voir section 20.4.13 page 242).
La fonction Treat (voir section 20.4.18 page 244) et le prédicat is [not] of type permettront toujours
de prendre en compte le type précis de l’objet (voir section 20.4.17 page 244).

20.4.6 Garnir une table objet


Il est alors possible d’exprimer de nouvelles valeurs d’objet lors de l’insertion ou du update :
insert into Les_Etudiants values
(Etudiant (Personne (’Dupont’, ’21/12/1975’),
Adresse (12, ’Charcot’, ’Lille’))) ;
240 CHAPITRE 20. LE RELATIONNEL-OBJET DE ORACLE

insert into Les_Etudiants values


(Etudiant (Personne (’Boyle’, null), Adresse (null, null, null))) ;
On peut mettre à jour l’adresse ou la ville :
update Les_Etudiants e update Les_Etudiants e
set e.a = Adresse (12, ’Faidherbe’, ’Lille’) set e.a.ville = ’Paris’
where e.p.nom = ’Boyle’ ; where e.p.nom = ’Boyle’ ;

20.4.7 Une table objet ne peut contenir d’objet indéfini


Un objet indéfini (is null) n’existe pas alors qu’un objet défini existe même si tous ses attributs sont
indéfinis (is null).
Une table objet ne peut pas contenir d’objet indéfini, cela reviendrait à admettre qu’une table puisse
avoir des lignes qui n’existent pas. Autrement dit le insert dans une table objet crée nécessairement
un objet, même si tous ses attributs sont indéfinis et l’update qui suit donne lieu à une erreur si la
table est initialement non vide :
insert into Les_Etudiants values (Etudiant (null, null)) ; -- OK
insert into Les_Etudiants values (null) ; -- erreur ORA-22805 :
-- impossible d’insérer un objet NULL dans des tables objet ou embo^ ıtées
Pour la même raison on a :
update Les_Etudiants e
set e = null ; -- erreur ORA-22805 :
-- impossible d’insérer un objet NULL dans des tables objet ou embo^ ıtées

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.

Dans l’exemple suivant l’alias est l_etudiant :


select e.p.nom as nom, e.Age (sysdate) as age
from Les_Etudiants e ;
Bien que cela ne soit pas souhaitable, on peut aussi accéder directement à la méthode Age de l’objet
Personne :
select e.p.nom as nom, e.p.Age (sysdate) as Age
from Les_Etudiants e ;

20.4.9 Modifier l’implémentation d’un type objet

 que soient les conditions, il est


A l’instar des paquetages, quelles  toujours possible de modifier et
 
recompiler le body d’un type : create or replace type body .

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).

20.4.10 Modifier la définition d’un type objet : alter type


Pour la définition d’un type objet, on ne peut qu’ajouter de nouvelles méthodes membre, on ne peut
en aucun cas modifier ou ajouter d’attributs.


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

alter type Etudiant replace as object (


p Personne,
a Adresse,
member function Age (AujourDhui Date) return Natural,
member function Statut (AujourDhui Date) return Varchar2,
member function Nom return Varchar2
) ;

create or replace type body Etudiant as ...

member function Age (AujourDhui Date) return Natural is


begin
return p.Age (AujourDhui) ;
end Age ;
...
end ;

Q. 257 Écrire la fonction membre Statut qui renvoie ’majeur’ ou ’mineur’.

20.4.11 Sous-programmes membres sans paramètres


Une fonction membre sans paramètres est déclarée comme en Ada, c’est à dire sans les parenthèses
qui délimitent les paramètres formels.

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 ;

20.4.12 Le problème de la persistance des modifications


Les SGBDOO (purement objet) gèrent automatiquement la persistance des objets : si on modifie
un attribut d’un objet persistant par une simple affectation et que cet objet provient de la base de
données, le SGBDOO garantit la persistance de cette modification. Autrement dit, le programmeur
manipule ses objets exactement comme il le ferait dans un langage objet, sans avoir à se préoccuper
de savoir s’ils sont persistants ou non.

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.

20.4.13 Value pour récupérer la ligne en tant qu’objet


La fonction value permet de récupérer le tuple sélectionné en tant qu’objet, elle n’est donc applicable
qu’aux tuples d’une table ou une vue objet.
La fonction value prend en paramètre un alias d’une table ou d’une vue objet et renvoie une instance
du type d’objet déclaré statiquement pour les lignes de cette table (même si l’objet de la ligne est
d’un sous-type de ce type).
select Value (e) from Les_Etudiants e ;

En revanche, pour un update :


update Les_Etudiants e
set e = Etudiant (...)
where ... ;

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

20.4.14 Définir un ordre sur un type : méthode order


Les clauses order by, distinct, group by (entre autres) de SQL ont besoin d’une relation d’ordre sur
les valeurs qu’elles manipulent. Ces valeurs pouvant être des objets, le programmeur relationnel-objet
doit pouvoir définir des ordres sur ses types objets.

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 ?

20.4.15 Passage par adresse : nocopy


Par défaut, Oracle passe les objets par copie (quel que soit le mode in, out ou in out). Le qualificatif
nocopy demande un passage par adresse.
Dans les fonctions membre self est passé implicitement et par défaut en in.
Dans les procédures membre self est passé implicitement et par défaut en in out.
Ce qui suit peut aussi se faire pour le paramètre implicite self des fonctions member qui est en mode
in out.
order member function Compare
(self in nocopy Etudiant, Avec in nocopy Etudiant) return Number
Cela peut accélérer les choses en cas de gros paramètre.

20.4.16 Méthode de classe : static


Ces méthodes ne reçoivent pas de paramètre self, elles s’exécutent indépendamment de toute instance
du type (comme en Java).
alter type Etudiant replace as object (
...,
static function constructeur (...) return Etudiant
) ;

create or replace type body Etudiant as


244 CHAPITRE 20. LE RELATIONNEL-OBJET DE ORACLE

...
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.

20.4.17 Prédicat sur le type précis d’un objet : is of type


<expr-objet> is [not] of type ( [only] <type> {, [only] <type>} )
Ce prédicat est vrai si le type dynamique de <expr-objet> est un sous-type d’un des <type> de la
liste. Ce doit être exactement le même type si only.
Par exemple on ne veut voir que les employés programmeurs ou secrétaires :
create table Les_Employes of Employe ;

select e.nom, e.salaire


from Les_Employes e
where Value (e) is of type (only Programmeur, Secretaire) ;
Q. 259 Dans cet exemple, peut-on se passer de only ? pourquoi ? (voir 20.4.1 page 238)

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.

20.4.18 Projeter un objet sur un de ses super-types : Treat


Treat (<expr-objet> as [ref] <type>)
<type> peut être un super ou un sous-type du type statique de <expr-objet>. Si pour une ligne, le
type dynamique de <expr-objet> est sous-type de <type> alors la projection de <expr-objet> sur
<type> est renvoyée, sinon l’objet indéfini (is null) est renvoyé.

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 ;

est Personne alors que dans :


select Value (e) from Les_Employes e ;

le type statique de Value (e) est Employe.

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.19 Supprimer des types : drop type


Il faut évidemment le faire dans le bon ordre.
drop table Les_Etudiants ;
drop type Etudiant ;
drop type Personne ;
drop type Adresse ;

20.4.20 Limitation des types objet


Lors de la déclaration d’un type objet, on ne peut faire figurer aucune contrainte (comme check) sur
les attributs.

On pourra bien sûr le faire à la déclaration d’une table objet.

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 :

1. ajoute une UE,


2. inscrit un étudiant à une UE.

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. 269 Requêtes calculant :

1. le nombre d’UE par étudiant,


2. le nombre d’étudiants par UE

Q. 270 Comment faire pour qu’on puisse lister les étudiants par ordre croissant ou décroissant de
leurs volumes horaire ? le faire.

20.5 Les références : ref


Une référence (ref) est un pointeur logique vers un objet d’une table objet (une référence contient
l’OID de l’objet sur 16 octets, l’OID de la table ou de la vue sur 16 octets et le rowid hint sur 10
octets).
Comme pour les clefs étrangères, ce mécanisme repose sur l’utilisation d’index.

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

create table Les_Voitures of Voiture (


constraint PK_Les_Voitures primary key (immatriculation)
) ;
Dans cette implantation les Etudiant peuvent parfaitement se trouver dans plusieurs tables : avec
une référence comment savoir dans quelle table se trouve l’étudiant ?

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.

20.5.1 Obtenir la référence d’un objet : la fonction ref (objet)


Seule une table objet peut fournir des références sur ses objets (c’est à dire sur ses lignes), c’est le cas
de la table Les_Etudiants :
insert into Les_Voitures values (
Voiture (’34 WWW 59’,
(select ref (e) from Les_Etudiants e where e.p.nom=’Dupont’))) ;
insert into Les_Voitures values (
Voiture (’22 XYZ 62’,
(select ref (e) from Les_Etudiants e where e.p.nom = ’Dupont’))) ;
insert into Les_Voitures values (
Voiture (’55 ABC 59’,
(select ref (e) from Les_Etudiants e where e.p.nom = ’Boyle’))) ;

Donc Dupont possède deux voitures.


Voici une représentation graphique de l’exemple précédent :

Les_Voitures Les_Etudiants
immatriculation proprietaire p.nom p.naissance a.numéro a.rue a.ville

34 WWW 59 Dupont 21/12/1975 12 Charcot Lille


22 XYZ 62
Durif Lille
55 ABC 59

20.5.2 Obtenir l’objet référencé : la fonction deref (référence)


Si la référence est pendante (l’objet désigné n’existe plus), la valeur de deref est indéfinie.

20.5.3 Naviguer sur les références : plus de jointure !


Le grand intérêt des références est qu’on peut les utiliser pour faire de la navigation : bon nombre de
jointures qu’il faut écrire explicitement dans le modèle relationnel seront prises en compte implicite-
ment par Oracle, et même, parfois, Oracle pourra se passer de jointure. Donc, a priori, la navigation
a au moins deux avantages :
– Simplicité d’écriture des ordres SQL.
– Efficacité de leur exécution.
20.5. LES RÉFÉRENCES : REF 247


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 ?

select v.proprietaire.Age (to_date (’1/6/2009’,’dd/mm/yyyy’)) as Age,


count (*) / count (distinct v.proprietaire) as Nb_Voitures
from Les_Voitures v
group by v.proprietaire.Age (to_date (’1/6/2009’,’dd/mm/yyyy’)) ;

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 :

SousSection Section Chapitre


titre ma_section titre mon_chapitre titre
Verrous Les transactions
Share Multi−versions
Relationnel−objet
Exclusive Problématique
Attribut Types PL/SQL
Constructeurs

select s.titre, s.ma_section.titre, s.ma_section.mon_chapitre.titre ;


from SousSection s ;

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.

20.5.4 Limiter la portée des références : scope for


A priori, une ref peut référencer un objet se trouvant dans n’importe quelle table objet du bon type.
De telles références sont coûteuses en espace mémoire et en temps d’accès. De plus on peut imaginer
qu’une telle souplesse puisse être à l’origine de la question : “mais dans quelle table se trouve l’objet
que je référence ?”.

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

Equipement Les_Appartements Les_Maisons


libellé logement adresse adresse
Robinet Lille
Baignoire Lyon
Robinet Lille

Fig. 20.1 – Les Maisons et Les Appartements sont des tables objet de type Logement. Est-ce vraiment
une bonne idée ?

drop table Les_Voitures ;

create table Les_Voitures of Voiture (


constraint PK_Les_Voitures primary key (immatriculation),
scope for (proprietaire) is Les_Etudiants
) ;

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.

20.5.5 Intégrité référentielle des références ref ⇒ scope for


Depuis Oracle 10, on peut utiliser une syntaxe très proche de celle des clefs étrangères pour maintenir
l’intégrité des références ref :
constraint <nom> foreign key (<colonne-référençante>)
references <table> [on delete cascade | set null ]
autrement dit, lors de la suppression d’un objet référencé on peut demander à ce que les lignes qui
le référencent soient elles aussi supprimées ou bien que les ref soient rendues indéfinies. Rappel : la
table référencée est forcément une table objet.
Cette contrainte ajoute automatiquement une contrainte de portée (scoped ref). Par exemple on veut
qu’un équipement disparaisse quand son logement est détruit :
create type Logement as object (
adresse Varchar2 (20)
) not final ;

create table Les_Maisons of Logement ;


create table Les_Appartements of Logement ;

create table Equipement (


libelle Varchar (20),
logement ref Logement,
constraint logement_OK foreign key (logement) references Les_Maisons
on delete cascade
) ;

Les logements référencés seront forcément dans la table Les_Maisons.


Cette contrainte n’est pas possible pour les références se trouvant dans des tables emboı̂tées.
Q. 276 Reprendre les exercices de la section 20.4.21 à la page 245 en remplaçant toutes les clefs
étrangères par des références.
20.6. LES TABLES EMBOÎTÉES 249

20.5.6 Tester les références pendantes : is [not] dangling


Une référence est pendante si l’objet qu’elle référence n’existe plus. Si on ne garantit pas l’intégrité
référentielle des références, il est possible que l’objet référencé ait été détruit entre temps, on dit alors
que la référence est pendante.

Le prédicat is dangling permet de savoir si une référence est pendante.


delete from Les_Etudiants e where e.p.nom = ’Dupont’ ;

update Les_Voitures
set proprietaire = null
where proprietaire is dangling ;

20.5.7 Limitation des références


Une ref ne peut faire l’objet d’une contrainte d’unicité ou de clef primaire.

Dans les expressions de contrainte (unique, check, . . .) on ne peut pas naviguer sur les ref.

20.6 Les tables emboı̂tées


Une colonne de table va maintenant pouvoir contenir un nombre (presque) quelconque de valeurs.

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.

20.6.1 Déclarer un type de table emboı̂table


On réimplémente l’association propriétaire en affectant à chaque étudiant la table (emboı̂tée) de ses
voitures :
create type Voiture as object (immatriculation VARCHAR2 (10)) ;
create type Des_Voitures as table of Voiture ;

20.6.2 Utiliser le type table emboı̂table pour typer une colonne


create table Les_Conducteurs_Relationnelle (
p Personne,
a Adresse,
v Des_Voitures,
constraint PK_Les_Conducteurs_Relationnelle primary key (p.nom)
) nested table v store as Tab_Voitures ;

Attention : Les_Conducteurs_Relationnelle n’est pas une table objet.


Grâce à la clause nested table v store as Tab_Voitures, le contenu des tables emboı̂tées de cha-
cun des conducteurs sera stocké dans l’unique table Tab_Voitures. Pour mémoriser l’appartenance
d’une voiture de la table Tab_Voitures à la table emboı̂tée v d’un conducteur particulier, Oracle ajoute
une colonne dans chacune des deux tables (16 octets). La table Tab_Voitures n’est pas manipulable
directement.
250 CHAPITRE 20. LE RELATIONNEL-OBJET DE ORACLE

20.6.3 Utiliser le type table emboı̂table pour typer un attribut d’objet


create type Conducteur under Personne (a Adresse, v Des_Voitures) ;

create table Les_Conducteurs of Conducteur (


constraint PK_Les_Conducteurs primary key (nom)
) nested table v store as Tab_Conducteurs ;

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.4 Constructeurs de valeur de table emboı̂tée


On peut exprimer la valeur d’une table emboı̂table grâce à son constructeur qui porte le nom du type
table :
– exprimer une table vide : Des_Voitures ()
– exprimer une table avec un contenu de départ :
Des_Voitures (Voiture (’34 WWW 59’), Voiture (’22 XYZ 62’))

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’)))) ;

insert into Les_Conducteurs values


(Conducteur (’Boyle’, null,
Adresse (null, null, ’Lille’),
null)) ;
insert into Les_Conducteurs values
(Conducteur (’Selby’, null, null, null)) ;

Les attributs v de Boyle et Selby sont indéfinis, ce qui est différent d’une table vide.

L’update suivant permet de fixer une table vide pour Boyle :


update Les_Conducteurs c
set v = Des_Voitures ()
where c.nom = ’Boyle’ ;

À ce stade, voici une représentation conceptuelle graphique du contenu de Les_Conducteurs :

Les_Conducteurs
nom naissance a.numéro a.rue a.ville v

Dupont 21/12/1975 12 Charcot Lille immatriculation


34 WWW 59
22 XYZ 62

Boyle Lille immatriculation


Selby
20.6. LES TABLES EMBOÎTÉES 251

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 ?

20.6.6 Mises à jour de la table emboı̂tée : table ( requête )


Pour cela, il faut faire travailler l’ordre de mise à jour  sur une requête qui renvoie la table emboı̂tée.
Le résultat de la requête doit être qualifié par le mot table (the dans les anciennes versions Oracle).
Supprimons une voiture à Dupont :
delete from
table (select c.v from Les_Conducteurs c where c.nom = ’Dupont’) v
where v.immatriculation = ’34 WWW 59’ ;
 
Attention : le select de la fonction table doit produire au plus une ligne :
– s’il produit plus d’une ligne, une erreur Oracle est déclenchée,
– s’il ne produit aucune ligne ou bien que la table emboı̂tée n’est pas définie (is null) alors un select
 
considérera qu’il s’agit d’une table vide, alors qu’une mise à jour provoquera une erreur Oracle.
Ajouter une voiture à Boyle :
insert into
table (select c.v from Les_Conducteurs c where c.nom = ’Boyle’)
values (Voiture (’55 ABC 59’)) ;

20.6.7 Consultation de tables emboı̂tées


Les voitures de Boyle :
select v.immatriculation -- voitures de Boyle
from table (select c.v from Les_Conducteurs c where c.nom = ’Boyle’) v ;

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

20.6.8 Méthodes PL/SQL de manipulation des tables emboı̂tées


Ces méthodes sont utilisables uniquement en PL/SQL.

En PL/SQL, l’accès à une table emboı̂tée se fait en l’indiçant à partir de 1.


Voici quelques-unes des méthodes applicables aux tables emboı̂tées :
EXISTS (i) Dit si le i ième élément existe, car il peut y avoir des trous dûs aux suppressions.
COUNT Nombre d’éléments effectifs de la table emboı̂tée (les trous ne sont pas comptés)
FIRST et LAST renvoient le plus petit (plus grand) indice d’un élément de la table emboı̂tée. Il
peut y avoir des ’trous’ entre FIRST et LAST (dûs à des suppressions dans la table emboı̂tée).
En général on a donc COUNT <= LAST - FIRST + 1. Les trous peuvent être détectés avec la
méthode EXISTS (i). Attention FIRST et LAST sont indéfinis si la table est indéfinie ou vide.
PRIOR (i) et NEXT (i) renvoient respectivement l’indice du non-trou précédant ou suivant i. Le
résultat est indéfini s’il n’y a pas de non-trou.
DELETE (i) supprime le ième élément et crée donc un trou à l’indice i.
Le nombre de voitures par conducteur ne peut pas s’écrire comme suit car la méthode count n’est
utilisable qu’en PL/SQL :
select c.nom, c.v.count -- Instruction erronée
from Les_Conducteurs c ;

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 ;

2. Avec une boucle pour en utilisant First et Last :


create or replace function Nb_Voitures (
nom in Les_Conducteurs.nom%type
) return Natural is
v Des_Voitures ;
n Natural := 0 ;
begin
select c.v into v
from Les_Conducteurs c
where c.nom = Nb_Voitures.nom ;
if v is not null and v.First is not null then
for I in v.First..v.Last loop
20.6. LES TABLES EMBOÎTÉES 253

if v.exists (I) then


n := n + 1 ;
end if ;
end loop ;
end if ;
return n ;
end Nb_Voitures ;

3. Avec une boucle tant que : First et Next


create or replace function Nb_Voitures (
nom in Les_Conducteurs.nom%type
) return Natural is
v Des_Voitures ;
n Natural := 0 ;
i Positive ;
begin
select c.v into v
from Les_Conducteurs c
where c.nom = Nb_Voitures.nom ;
if v is not null then
i := v.First ;
while i is not null loop
n := n + 1 ;
i := v.Next (i) ;
end loop ;
end if ;
return n ;
end Nb_Voitures ;

Q. 279 Écrire une autre version de Nb Voitures avec next.

20.6.9 Emboı̂tement des tables emboı̂tées


Depuis Oracle 10 (ou 9 ?) on peut emboı̂ter des tables sur un nombre quelconque de niveaux (en Oracle
8 on ne pouvait avoir qu’un seul niveau d’emboı̂tement).

create type Rangee as table of Voiture ;


create type Etage as table of Rangee ;
create type Parking as table of Etage ;

create table Les_Parkings (


id Number (5),
p Parking
) nested table p store as Tab_Etages
(nested table Column_Value store as Tab_Rangees
(nested table Column_Value store as Tab_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. 280 Requête donnant le nombre de voitures par parking.

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.

20.6.10 Exercices basiques sur Les Conducteurs, section 20.6.3

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).

Q. 287 Requête donnant les immatriculations des voitures du Pas-de-Calais.

Q. 288 Requête donnant le nombre de voitures par département, fonction Substr(cha^


ıne, dép,
lg), par exemple Substr(’abcdefg’, 6, 2) = ’fg’ et Substr(’abcdefg’, -3, 2) = ’ef’, le -3
part de la fin.

20.6.11 Exercices de synthèse


Reprendre les exercices de la section 20.4.21 à la page 245 en procédant de la manière suivante : chaque
étudiant dispose d’une table emboı̂tée contenant les références de ses UE. En revanche, on ne change
rien à la représentation des UE.

20.7 Tables emboı̂tées et REF


Supposons maintenant qu’un conducteur puisse conduire un nombre quelconque de voitures et qu’une
voiture puisse être conduite par un nombre quelconque de conducteurs. On va donner autonomie aux
voitures en ne les mettant plus dans une table emboı̂tée.

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.

Cette symétrie a deux conséquences :


– elle provoque une dépendance circulaire : on verra qu’il est possible de donner des définitions de
type incomplètes permettant de déclarer les références et qui seront complétées par la suite.

– 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.

20.7.1 Définitions incomplètes


create type Voiture ;
create type Conducteur ;
20.7. TABLES EMBOÎTÉES ET REF 255

20.7.2 Définitions des tables emboı̂tées


On a d’abord besoin d’introduire un type intermédiaire qui permettra de typer l’unique colonne des
tables emboı̂tables.
create type Ref_Voiture as object (r ref Voiture) ;
create type Ref_Conducteur as object (r ref Conducteur) ;

create type Ens_Voitures as table of Ref_Voiture ;


create type Ens_Conducteurs as table of Ref_Conducteur ;

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

et avec la seconde il faudrait écrire :


la_voiture.Column_Value.immatriculation

La suite de l’exemple s’appuie sur la première solution.

20.7.3 Définitions complètes de types


create type Voiture as object (
immatriculation VARCHAR2 (10),
conducteurs Ens_Conducteurs
) ;

create type Conducteur under Personne (


voitures Ens_Voitures
) ;

On peut vérifier que chacun des huits types précédents dépend indirectement de lui-même.

20.7.4 Déclarations des tables objets


create table Les_Voitures of Voiture (
constraint PK_Les_Voitures primary key (immatriculation)
) nested table conducteurs store as Tab_Ref_Conducteurs ;

create table Les_Conducteurs of Conducteur (


constraint PK_Les_Conducteurs primary key (nom)
) nested table voitures store as Tab_Ref_Voitures;

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

20.7.5 Maintenir la symétrie de l’association conducteur/voiture


Pour créer une association entre un conducteur et une voiture, on a tout intérêt à utiliser une procédure
stockée.
insert into Les_Voitures values (Voiture (’33 ABC 59’, Ens_Conducteurs ())) ;
insert into Les_Conducteurs values
(Conducteur (’Toto’, ’1/1/80’, Ens_Voitures ())) ;

Q. 289 Donner deux requêtes permettant de voir que l’assocition est bien symétrique.

Q. 290 Écrire la procédure Conduire qui associe un conducteur et une voiture.


Les deux requêtes suivantes devraient toujours donner le même résultat :
select c.nom, v.r.immatriculation
from Les_Conducteurs c, table (c.voitures) v ;

select c.r.nom, v.immatriculation


from Les_Voitures v, table (v.conducteurs) c ;

Et voici la procédure Conduire :


create or replace procedure Conduire (
nom in Les_Conducteurs.nom%type,
i in Les_Voitures.immatriculation%type) is
n natural ;
Pas_De_Table_Emboitee exception ;
pragma Exception_init (Pas_De_Table_Emboitee, -22908) ;
begin
-- tester si l’association existe déjà
select count (*) into n
from Les_Conducteurs c, table (c.voitures) v
where c.nom = Conduire.nom
and v.r.immatriculation = i ;
if n = 1 then
raise_application_error (-20111, ’association existe déjà’) ;
end if ;
-- mettre les deux à jour
savepoint debut ;
insert into
table (select c.voitures from Les_Conducteurs c where c.nom = Conduire.nom)
values ((select ref (v) from Les_Voitures v where v.immatriculation = i)) ;
insert into
table (select v.conducteurs from Les_Voitures v where v.immatriculation = i)
values ((select ref (c) from Les_Conducteurs c where c.nom = Conduire.nom)) ;
exception
when Pas_De_Table_Emboitee then
rollback work to savepoint debut ;
raise_application_error (-20111, ’conducteur ou voiture inexistant’) ;
end Conduire ;

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

table (select c2.voitures from Les_Conducteurs c2 where c2.p.nom = c.nom) v


where c.nom = Conduire.nom
and v.r.immatriculation = i ;

20.7.6 Suppression en cas de dépendance circulaire : drop ... force


En règle générale, Oracle interdit de détruire un objet si d’autres objets en dépendent. Par exemple il
n’est pas possible de détruire une table référencée par des clefs étrangères d’autres tables.
En cas de dépendance circulaire, on est alors très embêté ! heureusement Oracle fournit la clause force
qui permet de forcer la suppression d’un type, par exemple :
drop type Voiture force ;

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.

20.8 Niveaux de pureté des méthodes d’objet


Lors de la déclaration d’un type objet, il est possible de spécifier si une méthode peut ou non lire ou
avoir des effets de bord sur l’environnement.

Ceci a un intérêt en termes de génie logiciel.


PRAGMA RESTRICT_REFERENCES ( W/R Write/Read
<nom_méthode> | default, N No
{WNDS | WNPS | RNDS | RNPS | TRUST}+ D/P Database/Package
) S State
DEFAULT le pragma est appliqué à toutes les méthodes qui n’ont pas un pragma explicite.
WNDS aucune écriture dans la base de données.
WNPS aucune écriture dans un paquetage (modification de variables globales).
RNDS aucune lecture dans la base de données.
RNPS aucune lecture ou consultation d’une variable globale de paquetage.
TRUST aucune vérification des restrictions précédentes ne sera faite : on fait confiance au code.
create type Personne as object (
...,
member function Age (AujourDhui Date) return Natural,
member function Heures_Supplementaires () return Natural,

pragma restrict_references (Age, RNDS, RNPS, WNPS, WNDS),


pragma restrict_references (Heures_Supplementaires, RNPS, WNPS, WNDS)
) ;

Q. 291 En provocant sciemment des erreurs, vérifier que ces niveaux de pureté sont bien un garde-fou.

20.9 Conception d’un schéma relationnel-objet


Comme on vient de le voir, à un MCD donné, il est possible de donner un grand nombre d’implanta-
tions relationnelle-objet. Cette diversité de solutions pose un problème si on n’est pas capable de les
258 CHAPITRE 20. LE RELATIONNEL-OBJET DE ORACLE

décrire simplement et ainsi de les comparer.

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 ?).

20.9.1 Notations graphiques


Une flèche simple correspond à exactement une référence.

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.

20.9.2 Association 1-N

Chaque voiture a une référence sur son propriétaire

Etudiant Voiture

Chaque étudiant possède la table emboitée de ses voitures

Etudiant

v Voiture

Chaque étudiant possède une table emboitée de références sur ses voitures

Etudiant Voiture

qui est équivalent à

Etudiant

v ref Voiture Voiture

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).

20.9.3 Association N-N


Cela risque de donner encore plus de possibilités différentes, bonjour la maintenance, à moins peut-être
de travailler avec un outil de haut niveau qui cache cette complexité.

Le relationnel pur a toujours l’énorme avantage de la simplicité !


Bibliographie

[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

Das könnte Ihnen auch gefallen