Sunteți pe pagina 1din 352

Base de données relationnelle

Une base de données relationnelle est une base de données structurée suivant les
principes de l’algèbre relationnelle.

La théorie des bases de données relationnelles est due à Edgar Frank Codd.

Remarque : l'adjectif relationnel ne fait pas référence ici aux relations entre
les tables mais aux tables elles-mêmes.
Elle est mise en œuvre au moyen d’un Système de Gestion de Bases de Données
Relationnelles (SGBDR).

Le concept permet de stocker et d’organiser une grande quantité d’informations.


Les SGBD (Système de Gestion de Base de Données) permettent de naviguer dans ces
données et d’extraire (ou de mettre à jour) les informations voulues au moyen
d'une requête.
Les données apparaissent comme stockées dans des tables qu'on peut mettre en
relation.
Une table elle-même est une relation, mais entre les différents champs
qui la composent.
Ce système se démarque donc totalement en termes d’interface des bases de
données de type hiérarchique, même si au plan de l'implémentation et, en
fonction des statistiques d’accès à la base, un modèle hiérarchique sera
utilisé, qui n’aura jamais besoin d’être pris en compte par l’utilisateur.

De plus les données d'une table sont souvent subordonnées à un des champs (une
clé).
Ce modèle relationnel conduit à :
 Une grande simplicité d’usage
 Une transparence pour l’utilisateur de toute réorganisation technique de la
base (la seule différence pour l’utilisateur se situera, si l’opération est
réussie, dans les temps de réponse).
 Une facilité de combinaison du contenu de plusieurs tables (opération join ou
jointure).
Les tables possèdent un certain nombre de colonnes ou champs permettant de
décrire des n-uplets (lignes ou enregistrements).

La non duplication (absence de redondance) des n-uplets est théoriquement assurée


par le SGBDR.
Dans les relations,
il est possible de définir deux types de clés :
clé primaire permet d’identifier un et un seul n-uplet (par exemple le numéro de
sécurité
sociale).

clé externe
c’est un attribut d’une relation qui est clé primaire dans une autre relation.
Elle permet donc de lier deux tables entre elles.

Pour accéder aux données, on utilise différents opérateurs logiques, notamment


la sélection (ou projection), mais aussi les jointures (dont il existe
différents types).
Les opérations sont communiquées sous forme de requêtes aux SGBDR (Système de
Gestion de Base de Données Relationnelle). Le SGBDR convertit les requêts SQL en
expressions relationnelles pour pouvoir effectuer les opérations sur les tables
La plupart utilisent le langage normalisé SQL ou le TCL pour les SGBDR
Multivalué.

Dans une base de données relationnelle, le but est de séparer les informations
au maximum pour éviter les doublons et la redondance, et d'empêcher la perte de
qualité d’information (par exemple, l'adresse d'un fournisseur n'est mise à jour
qu'une et une seule fois : la modification sera alors prise en compte sur
l'ensemble des courriers).

Dans la table PERSONNE ci-dessous, l’ensemble {PersID, nom, prénom, date_naiss,


ville_naiss} est un ensemble d'attributs.

Chaque attribut définit une information élémentaire à l’intérieur d’une ligne (aussi
appelée tuple) de la table. Il ne peut exister deux fois le même tuple dans une relation.
Les
attributs sont parfois aussi appelés colonnes.

On peut définir des clés, qui sont des contraintes d’intégrité portant sur une
relation. Elles consistent à imposer qu’il ne puisse exister deux tuples ayant
même valeur pour un sous-groupe d’attributs (la clé) de la relation. Si on
reprend l’exemple de la table PERSONNE, la clé pourrait être PersID, donc deux
tuples différents ne pourraient pas avoir une même valeur pour l’attribut PersID
(mais les valeurs des autres attributs peuvent être identiques).
Certaines clés sont dites clés étrangères ; ce sont des contraintes d’intégrité
portant sur une relation R1, consistant à imposer que la valeur d’un groupe
d’attributs apparaisse comme valeur de clé dans une autre relation R2.

Si l’on reprend l’exemple des deux tables PERSONNE et VILLE, la clé étrangère de la
table PERSONNE pourrait être ville_naiss, qui pointe sur la table VILLE. Il est
impératif que le nombre d’attributs formant la clé étrangère de la table R1
corresponde au nombre d’attributs formant la clé primaire de la table R2.
Ces clés étrangères sont issues du processus de normalisation du modèle des
données.
Lors de l’implémentation d’une base de données, il faut penser à certains
aspects :
Personne ne doit pouvoir mettre à jour des données dans une table pendant
qu’une autre personne les modifie déjà, car cela pourrait aboutir à des
incohérences. Un système comme Paradox l’autorise cependant grâce à un
mécanisme ingénieux mettant à jour automatiquement tous les affichages en
cours au même instant.
Les transactions sont atomiques, c’est-à-dire qu’en cas de panne majeure du
système informatique au milieu d’une modification, un mécanisme doit permettre
d’annuler les transactions en cours si elle n’ont pas pu être exécutées
totalement (mécanisme dit du COMMIT).
Des vérifications d’intégrité doivent assurer que chaque valeur inscrite dans
un tuple soit une valeur permise (par exemple, on peut interdire de mettre une
valeur supérieure à 12 dans un attribut « mois »).
Exemple :
On a une table « personne » contenant le nom, le prénom, la date de
naissance et la ville de naissance pour chaque personne. Une ligne de la
table contiendra donc les informations relatives à une
personne.PERSONNE
PersIDnomprénomdate_naissville_naiss
1Dupontbob01-01-19501
2yyyymeurise29-04-19992
3zzzzcodd26-12-20001

note : ici ville_naiss est une clé étrangère (table VILLE)


De même, on a une table « ville » contenant la population et la superficie
de chaque ville.VILLE
VilleIDnompopulationsuperficieregion
1Paris12345612345612
2Lyon123451234522
3Grenoble1234123422

note : ici region est une clé étrangère (table REGION)

Si on veut pouvoir connaître, pour chaque personne, la population et la


superficie de sa ville de naissance, il est utile, au lieu de stocker le nom de
la ville de naissance dans la table « personne », de stocker un identifiant (clé
étrangère) se référant à un numéro unique pour chaque ville (clé primaire).
Ainsi, les informations concernant chaque ville sont stockées unitairement.

Un des langages utilisés pour construire des requêtes permettant d’interroger et


de manipuler les données des bases de données relationnelles est le langage SQL.
Pour reprendre notre exemple, SQL sert à formaliser des questions (requêtes) du
type : « Quelles sont toutes les personnes nées dans la ville X » ou « Dans
quelle ville est né Dupont ».
- SQL n’étant pas exactement proche de la formulation intuitive d’une requête
contrairement au TCL (voir système d'exploitation Pick), deux approches sont
utilisées pour s’en affranchir :
 création de langages frontaux traduisant en SQL des phrases simples du genre :
« Lister par région le chiffre d’affaires moyen de chaque produit »
 création de requêtes en remplissant un formulaire avec les conditions qu’on
souhaite voir vérifiées et en laissant vierges les autres champs (Query by
example)
SQL n'est cependant pas incontournable à ce jour pour effectuer des requêtes
générales très complexes. Plusieurs systèmes affichent dans un premier temps en
réponse à une requête complexe son coût prévisible (en temps ou en ressources
d’accès), en demandant à l’utilisateur confirmation ou invalidation
préalablement à toute exécution. On peut imaginer en effet sur des bases
courantes des requêtes SQL dont l’exécution demanderait des centaines d’heures
ou des milliers d’euros. Une réécriture simple suffit souvent à obtenir le même
résultat, ou un analogue fonctionnel, de façon bien plus économique. Malgré le
succès du langage SQL qui a suivi, Edgar F. Codd dénoncera cet outil qu'il
considère comme une interprétation incorrecte de ses théories.

Les SGBDR sont livrés avec des APIs propriétaire (c’est-à-dire propres à chaque
SGBDR) pour communiquer avec eux.
Pour permettre d'utiliser une ou plusieurs bases de données avec un logiciel
sans devoir récrire le code source, des APIs standardisées pour accéder au SGBDR
ont été crées :
ODBC dans l'univers Microsoft
JDBC dans l'univers Java
Comme il existe une différence conceptuelle entre le monde objet (C++, Java,
.Net, etc.) et la représentation relationnelle, il est apparu plusieurs
solutions pour les réconcilier. Une solution est le mapping objet relationnel
dont le framework open source Hibernate est un des meilleurs exemples pour
l'univers Java.[réf. nécessaire]
Ainsi, dans l'univers Java, de nouveaux standards pour l'accès aux SGBDR sont
apparus :
les EJB entité
JDO

Les bases de données relationnelles étaient pressenties dans les années 1970
comme remplaçants des fichiers classiques dans les systèmes d’exploitation (voir
projet FS). Cela fut implémenté dans les ordinateurs du type IBM Système 38 ou
AS/400, ainsi que dans un système d’exploitation nommé Pick, mais sans se
généraliser. Les brevets de cette époque étant maintenant dans le domaine
public, l’idée redevient d’actualité dans les années 2000 avec le système WinFS
(voir Microsoft Windows).

INTRODUCTION AU LANQUAGE SQL!

Préambule
1. SQL un langage ?
2. SQL une histoire...
2.1. SQL une norme
2.2. SQL un standard
3. Remarques préliminaires sur les SGBDR

4. Les subdivisions du SQL


4.1. DDL : " Data Definition Language "
4.2. DML : " Data Manipulation Language "
4.3. DCL : " Data Control Language "
4.4. TCL : " Transaction Control Language "
4.5. SQL intégré : " Embedded SQL "

5. Implémentation physique des SGBDR


5.1. SGBDR "fichier"
5.2. SGBDR "Client/Serveur"
6. Type de données
6.1. Types alphanumériques
6.2. Types numériques
6.3. Types temporels
6.4. Types " BLOBS " (hors du standard SQL 2)
6.5. Autres types courants, hors norme SQL 92
6.6. Les domaines, ou la création de types spécifiques
7. Contraintes de données
8. Triggers et procédures stockées
9. Résumé
10. Conclusion

. Et d’abord, SQL est-il un vrai langage ?

Si l’on doit accepter ce mot au sens informatique du


terme, il semble difficile de dire oui tant SQL est loin
de la structure et des possibilités d’un langage de
programmation courant.
Point de variable, point de procédure ou de fonction
… Pourtant il s’agit bien de former des phrases qui seront compilées
afin d’exécuter des traitements.
Même si SQL s’est doté au fil du temps d’extensions,
comme la possibilité de paramétrer les requêtes, il y a
des lacunes importantes qui sont autant de frein à sa
pénétration. Par exemple SQL ne sait être récursif [1]
alors que ce mode d’exécution est nécessaire pour
résoudre une frange importante de problèmes, notamment
ceux pour traiter les arbres ou les graphes.

En fait SQL est un langage de type " déclaratif ". On


spécifie ce que l’on veut obtenir ou faire et c’est la
machine qui décide comment elle doit l’exécuter. Pour
certains, SQL est perçu comme un pis-aller tandis que
d’autres préfèrent l’éviter ou retarder au plus
l’inéluctable moment où son apparition sera
incontournable.

La différence fondamentale entre les langages courants


comme C ou Pascal, qui sont des langages procéduraux,
réside dans le fait qu’avec ces derniers, vous indiquez
l’ensemble des instructions nécessaires à traiter un
problème. Vous gardez ainsi une certaine maîtrise sur le
cycle d’exécution et l’enchaînement des différentes
tâches d’un programme vous est parfaitement connu. En
revanche, dans SQL vous n’avez, et d’ailleurs ne devez,
pas avoir la moindre idée de comment la machine exécute
votre demande, ni même dans quel ordre elle décompose le
traitement de la requête en différentes tâches, ni
d’ailleurs même comment elle les synchronise.
Lors d’une extraction concernant plusieurs tables, le
moteur relationnel pourrait parfaitement lancer
plusieurs traitements en parallèles pour extraire les
données de chacune des tables, puis effectuer les
jointures des différentes tables résultantes en fonction
de l’ordre de terminaison des extractions...

C’est pourquoi, les moteurs relationnels incluent des


modules d’optimisation logique et parfois statistique.

Ainsi un optimiseur logique préférera traiter en premier


les clauses excluant de la réponse un maximum de
données, tandis qu'un optimiseur statistique commencera
par traiter les tables de plus faible volume.
Exemple :

Soit une table contenant le personnel de la société " VIVE LE SQL " et une autre
contenant
les salaires mensuels desdits employés.

" VIVE LE SQL " compte 1000 employés soit autant de lignes dans la table du personnel,
et
la moyenne de durée d’emploi étant de 6 ans, la table des salaires compte donc environ
72 000 lignes.

Soit la requête suivante : rechercher les employés dont le nom commence par Z ou qui
sont
domiciliés à Trifouilly et qui ont eu au moins un salaire supérieur à 50 000 Francs.

* Un optimiseur logique commencera immanquablement par traiter la table des


salaires s celle des employés.
* Un optimiseur statistique commencera par traiter les employés puis les salaires.

Nous pauvres humains, aurions immédiatement traité la table des salaires, car à ce niveau
d’émolument il ne devrait pas y avoir grand monde dans la table des salariés…

Eh bien, croyez moi ou pas… certains optimiseurs statistiques auraient eût la même
appréciation !
En effet les plus performants ne se contentent pas seulement de conserver le nombre de
lignes ou
le volume des données d’une table, mais aussi les moyennes, médianes, maximums et
minimums de
certaines colonnes, voir même le nombre de valeurs distinctes (indice de dispersion) ainsi
que
le nombre d’occurrences des valeurs les plus fréquentes, notamment pour les colonnes
pourvues
d’index... C'est le cas en particulier du SGBDR INGRES.

Bref, pour conclure cette brève présentation de SQL,


nous pouvons affirmer que, malgré ses défauts, et en
attendant un futur langage objet pour l’interrogation
des données, incluant tous les mécanismes dynamiques et
bien entendu la récursivité [1], il nous faut continuer
à utiliser sagement SQL, du mieux que nous pouvons, en
rendant ainsi hommage a quelques-uns de ses créateurs...

2. SQL une histoire...


Nous sommes en 1970. Le docteur Codd, un chercheur d’IBM
à San José, propose une nouvelle manière d’aborder le
traitement automatique de l’information, se basant sur
la théorie de l’algèbre relationnel (théorie des
ensembles et logique des prédicats). Cette proposition
est faite afin de garantir une plus grande indépendance
entre la théorie et l’implémentation physique des
données au sein des machines. C’est ainsi que naîtrons,
vers la fin des années 70, les premières applications
basées sur la proposition de Ted Codd, connues de nos
jours sous l’acronyme SGBDR (signifiant Système de
Gestion de Bases de Données Relationnelles).

Dans cette même période, Peter CHEN tente une approche


pragmatique et conceptuelle du traitement automatique
des données en proposant le modèle Entité-Association
comme outil de modélisation. Nous sommes en 1976.

En parallèle, différents chercheurs travaillent à


réaliser ce que seront les SGBDR d’aujourd’hui : citons
entre autres l’équipe de Gene Wong à l’université de
Berkeley qui entame le projet INGRES en 1972, et en 1975
propose le langage QUEL comme outil d’interrogation des
données (aucun ordre de mise à jour des données n’y
figure).
De même à Noël 1974 démarre chez IBM le projet qui
portera le nom de System R, et donnera comme langage
d’interrogation SEQUEL (Structured English QUEry
Langage) en 1976.

Lors d’une conférence internationale à Stockolm (IFIP


Congres), Larry Ellison, dirigeant d’une petite
entreprise appelée Software Development Laboratories
entend parler du projet et du langage d’interrogation et
de manipulation des données. Il rentre en contact avec
les chercheurs d’IBM. Quelques années après, Larry
Ellison sort le premier SGBDR commercialisé et change le
nom de sa société. Elle s’appellera désormais Oracle...

IBM, pour sa part, sortira une version commerciale bien


après celle d’Oracle, ce qui fera dire à l’un des
responsables du projet que " cela vous montre combien de
temps il faut à IBM pour faire n’importe quoi... ".
Finalement la première installation de System R est
réalisée en 1977 chez Pratt & Whitney, et déjà les
commandes du langage d’interrogation se sont étoffées de
quelques judicieuses techniques préconisées par Oracle,
comme la gestion des curseurs.

Au même moment apparaissent d’autres langages


d’interrogation comme QBE de Zloof (IBM 1977) repris
pour Paradox par Ansa Software, ou REQUEST de Fred
Damerau (basé sur le langage naturel) ou encore RENDEZ
VOUS (1974) de l’équipe de Ted Codd ainsi que SQUARE
(Boyce 1975).

Mais le point critique du langage SEQUEL porte sur


l’implémentation du ... rien ! Ou plutôt devrais-je dire
du nul !!!

Finalement la première réalisation connue sous le nom de


SQL (82) voit le jour après l’arrivée de DB2, une
avancée significative du System R d’IBM datant de mars
1979.

On peut donc dire que le langage SQL est né en 1979,


mais baptisé SQL en 1982.
2.1. SQL une norme
Notons que SQL sera normalisé à quatre reprises : 1986
(SQL 86 - ANSI), 1989 (ISO et ANSI) et 1992 (SQL 2 - ISO
et ANSI) et enfin 1999 (SQL:1999 - ISO) souvent apellé à
tort (y compris par moi même !) SQL 3. A ce jour, aucun
SGBDR n'a implémenté la totalité des spécifications de
la norme actuellement en vigueur. Mais je dois dire que
le simple (?) SELECT est argumenté de quelques 300 pages
de spécifications syntaxiques dans le dernier document
normatif...

Néanmoins, la version SQL 2 (1992) est la version vers


laquelle toutes les implémentations tendent. C'est
pourquoi nous nous baserons sur SQL 2.

A noter, la future norme SQL:2003, sortira en juillet


2003.

2.2. SQL un standard


Même si SQL est considéré comme le standard de toutes
les bases de données relationnelles et commercialisées,
il n'en reste pas moins vrai que chaque éditeur tend à
développer son propre dialecte, c'est à dire à rajouter
des éléments hors de la norme. Soit fonctionnellement
identiques mais de syntaxe différentes (le LIKE d'Access
en est l'exemple le plus stupide !) soit
fonctionnellement nouveau (le CONNECT BY d'Oracle pour
la récursivité par exemple). Il est alors très difficile
de porter une base de données SQL d'un serveur à
l'autre. C'est un moindre mal si l'on a respecté au
maximum la norme, mais il est de notoriété absolue que
lorsque l'élément normatif est présent dans le SGBDR
avec un élément spécifique ce sera toujours l'élément
spécifique qui sera proposé et documenté au détriment de
la norme !

Exemple, la fonction CURRENT_TIMESTAMP, bien présente


dans MS SQL Server, n'est pas spécifié dans la liste des
fonctions temporelle de l'aide en ligne !!!

De plus, le plus petit dénominateur commun entres les 4


poids lourds de l'édition que sont Oracle, Sybase (ASE),
Microsoft (SQL Server) et IBM (DB2) est tel qu'il est
franchement impossible de réaliser la moindre base de
données compatible entre ces différentes
implémentations, sauf à n'y stocker que des données
numériques !!!

Nous avons aussi démontré qu'une même table créée sur


différents SGBDR avec les mêmes données n'avait par
forcément le même comportement au regard du simple
SELECT du fait des jeux de caractères et collations par
défaut. Or tous les SGBDR ne sont pas paramétrables à ce
niveau et ceux qui le sont, ne présentent pas en général
les mêmes offres en cette matière.

Enfin, pour tous ceux qui veulent connaître la norme


comparée au dialecte de leur SGBDR favori, il suffit de
lire le tableau de comparaison des fonctions de SQL :
Toutes les fonctions de SQL

3. Remarques préliminaires sur les SGBDR


Avant tout, nous supposerons connues les notions de
"bases de données", "table", "colonne", "ligne", "clef",
"index", "intégrité référentielle" et "transaction",
même si ces termes, et les mécanismes qu'ils induisent
seront plus amplement décrits au fur et à mesure de
votre lecture.

Voici quelques points qu'il convient d'avoir à l'esprit


lorsque l'on travaille sur des bases de données :

Il existe une grande indépendance entre la couche


abstraite que constitue le SGBDR et la couche physique
que sont le ou les fichiers constituant une base de
données. En l'occurrence il est déraisonnable de
croire que les fichiers sont arrangés dans l'ordre des
colonnes lors de la création des tables et que les
données sont rangées dans l'ordre de leur insertion ou
de la valeur de la clef.
Il n'existe pas d'ordre spécifique pour les tables
dans une base ou pour les colonnes dans une table,
même si le SGBDR en donne l'apparence en renvoyant
assez généralement l'ordre établi lors de la création
(notamment pour les colonnes). Par conséquent les
tables, colonnes, index... doivent être repérés par
leur nom et uniquement par cet identifiant.
Les données sont systématiquement présentées sous
forme de tables, et cela quel que soit le résultat
attendu. Pour autant la table constituée par la
réponse n'est pas forcément une table persistante, ce
qui signifie que si vous voulez conserver les données
d'une requête pour en faire usage ultérieurement, il
faudra créer un objet dans la base (table ou vue) afin
d'y placer les données extraites.
La logique sous-jacente aux bases de données repose
sur l'algèbre relationnel lui-même basé sur la théorie
des ensembles. Il convient donc de penser en terme
d'ensemble et de logique et non en terme d'opération
de nature atomique. A ce sujet, il est bon de se
rappeler l'usage des patates (ou diagrammes de Wen)
lorsque l'on "sèche" sur une requête.
Du fait de l'existence d'optimiseurs, la manière
d'écrire une requête à peu d'influence en général sur
la qualité de son exécution. Dans un premier temps il
vaut mieux se consacrer à la résolution du problème
que d'essayer de savoir si la clause "bidule" est plus
gourmande en ressource lors de son exécution que la
clause "truc". Néanmoins l’optimisation de l’écriture
des requêtes sera abordée dans un article de la série.

4. Les subdivisions du SQL


Le SQL comporte 5 grandes parties, qui permettent : la
définition des éléments d'une base de données (tables,
colonnes, clefs, index, contraintes...), la manipulation
des données (insertion, suppression, modification,
extraction…), la gestion des droits d'accès aux données
(acquisition et révocation des droits), la gestion des
transactions et enfin le SQL intégré. La plupart du
temps, dans les bases de données "fichier" (dBase,
Paradox...) le SQL n'existe qu'au niveau de la
manipulation des données, et ce sont d'autres ordres
spécifiques qu'il faudra utiliser pour créer des bases,
des tables, des index ou gérer des droits d'accès.
Certains auteurs ne considèrent que 3 subdivisions
incluant la gestion des transactions au sein de la
manipulation des données... Cependant ce serait
restreindre les fonctionnalités de certains SGBDR
capable de gérer des transactions comprenant aussi des
ordres SQL de type définition des données ou encore
gestion des droits d'accès !
4.1. DDL : " Data Definition Language "
C'est la partie du SQL qui permet de créer des bases de
données, des tables, des index, des contraintes…
Elle possède les commandes de base suivante : CREATE, ALTER,
DROP
Qui permettent respectivement de créer, modifier, supprimer un élément
de la base.

4.2. DML : " Data Manipulation Language "


C'est la partie du SQL qui s'occupe de traiter les
données. Elle comporte les commandes de base suivantes :
INSERT, UPDATE, DELETE, SELECT Qui permettent respectivement d'insérer, de
modifier de supprimer et d'extraire des données.

4.3. DCL : " Data Control Language "


C'est la partie du SQL qui s'occupe de gérer les droits
d'accès aux tables.
Elle comporte les commandes de base suivantes :GRANT, REVOKE
Qui permettent respectivement d'attribuer et de révoquer
des droits.

4.4. TCL : " Transaction Control Language "


C'est la partie du SQL chargé de contrôler la bonne
exécution des transactions.
Elle comporte les commandes de base suivantes :
SET TRANSACTION, COMMIT, ROLLBACK

Qui permettent de gérer les propriétés ACID des


transactions.

4.5. SQL intégré : " Embedded SQL "


Il s'agit d'éléments procéduraux que l'on intégre à un
langage hôte :SET, DECLARE CURSOR, OPEN, FETCH...

Le terme "ACID", ne fait pas référence, loin s'en faut


au LSD, mais plus basiquement aux termes suivants :

A - Atomicité : une transaction s'effectue ou pas (tout


ou rien), il n'y a pas de demi-mesure. Par exemple
l'augmentation des prix de 10% de tous les articles
d'une table des produits ne saurait être effectuée
partiellement, même si le système connaît une panne en
cours d'exécution de la requête.

C - Cohérence : le résultat ou les changements induits


par une transaction doivent impérativement préserver la
cohérence de la base de données. Par exemple lors d'une
fusion de société, la concaténation des tables des
clients des différentes entités ne peut entraîner la
présence de plusieurs client ayant le même identifiant.
Il faudra résoudre les conflits portant sur le numéro de
client avant d'opérer l'union des deux tables.

I - Isolation : les transactions sont isolées les unes


des autres. Par exemple la mise à jour des prix des
articles ne sera visible pour d'autres transactions que
si ces dernières ont démarré après la validation de la
transaction de mise à jour des données. Il n'y aura donc
pas de vue partielle des données pendant toute la durée
de la transaction de mise à jour.

D - Durabilité : une fois validée, une transaction doit


perdurer, c'est à dire que les données sont persistantes
même s'il s'ensuit une défaillance dans le système. Par
exemple, dès lors qu'une transaction a été validée,
comme la mise à jour des prix, les données modifiées
doivent être physiquement stockées pour qu'en cas de
panne, ces données soient conservées dans l'état où
elles ont été spécifiées à la fin de la transaction.

les propriétés ACID d'un SGBDR :


Atomicité : si tout se passe correctement, les actions de la transaction sont toutes
validées, sinon on retourne à l’état initial.

L’unité de travail est indivisible. Une transaction ne peut être partiellement effectuée.

Cohérence : le passage de l’état initial à l’état final respecte la cohérence de la base.

Isolation : les effets de la transaction ne sont pas perceptibles tant que celle-ci n’est pas
terminée.
Une transaction n’est pas affectée par le traitement des autres transactions.

Durabilité : les effets de la transaction sont durable.

Rendre transparentes la complexité et la localisation des traitements et des données, tout


en assurant
un bon niveau de performance, telles sont aujourd’hui les demandes des entreprises et les
nouveaux
critères sur lesquels s’arrêtent leurs choix.

Pour assurer l'ensemble de ces fonctions de base, les


SGBDR utilisent le principe de la journalisation : un
fichier dit "journal", historise toutes les transactions
que les utilisateurs effectuent et surtout leur état :
en cours, validé ou annulée. Une mise à jour n'est
réellement effectuée que si la transaction aboutit.
Ainsi en cas de panne du système, une relecture du
journal permet de resynchroniser la base de données pour
assurer sa cohérence.
De même en cas de "RollBack", les instructions de la transaction
sont lues "à l'envers" afin de rétablir les données telles qu'elles devaient être à
l'origine de la transaction.

5. Implémentation physique des SGBDR


Il existe à ce jour, deux types courant d'implémentation
physique des SGBD relationnels. Ceux qui utilisent un
service de fichiers associés à un protocole de réseau
afin d'accéder aux données et ceux qui utilisent une
application centralisée dite serveur de données. Nous
les appellerons SGBDR "fichier" et SGBDR client/serveur
(ou C/S en abrégé).

5.1. SGBDR "fichier"


Le service est très simple à réaliser : il s'agit de
placer dans une unité de stockage partagée (en général
un disque d'un serveur de réseau) un ou plusieurs
fichiers partageables. Un programme présent sur chaque
poste de travail assure l'interface pour traiter les
ordres SQL ainsi que le va et vient des fichiers de
données sur le réseau.
Il convient de préférer des SGBDR à forte granularité au
niveau des fichiers. En effet plus il y a de fichiers
pour une même base de données et moins la requête
encombrera le réseau, puisque seuls les fichiers
nécessaires à la requête seront véhiculés sur le réseau.

Ces SGBDR "fichier" ne proposent en général pas le


contrôle des transactions, et peu fréquemment le DDL et
le DCL. Ils sont, par conséquent, généralement peu ACID
!

Les plus connus sont ceux qui se reposent sur le modèle


XBase. Citons parmi les principaux SGBDR "fichier" :
dBase, Paradox, Foxpro, BTrieve, MySQL, … Généralement
basés sur le modèle ISAM de fichiers séquentiels
indexés.

Avantage : simplicité du fonctionnement, coût peu élevé


voire gratuit, format des fichiers ouverts,
administration quasi inexistante.

Inconvénient : faible capacité de stockage (quoique


certains, comme Paradox, acceptent 2 Go de données par
table !!!), encombrement du réseau, rarement de gestion
des transactions, faible nombre d’utilisateurs, faible
robustesse, cohérence des données moindre.

Access se distingue du lot en étant assez proche d’un


serveur SQL : pour une base de données, un seul fichier
et un TCL. Mais cela présente plus d’inconvénients que
d’avantages : en effet pour interroger une petite table
de quelques enregistrements au sein de base de données
de 500 Mo, il faut rapporter sur le poste client, la
totalité du fichier de la base de données... Un non sens
absolu, que Microsoft pali en intimant à ses
utilisateurs de passer à SQL Server dès que le nombre
d’utilisateurs dépasse 10 !

5.2. SGBDR "Client/Serveur"


Le service consiste à faire tourner sur un serveur
physique, un moteur qui assure une relative indépendance
entre les données et les demandes de traitement de
l'information venant des différentes applications : un
poste client envoie à l'aide d'un protocole de réseau,
un ordre SQL (une série de trames réseau), qui est
exécuté, le moteur renvoie les données. De plus le SGBDR
assure des fonctions de gestions d'utilisateurs de
manière indépendante aux droits gérés par l'OS.

A ce niveau il convient de préférer des SGBDR C/S qui


pratiquent : le verrouillage d'enregistrement plutôt que
le verrouillage de page (évitez donc SQL Server...), et
ceux qui tournent sur de nombreuses plates-formes
système (Oracle, Sybase...). Enfin certains SGBDR sont
livrés avec des outils de sauvegarde et restauration.

Les SGBDR "C/S" proposent en général la totalité des


services du SQL (contrôle des transactions, DDL et DCL).
Ils sont, par conséquent, pratiquement tous ACID. Enfin
de plus en plus de SGBD orientés objets voient le jour.
Dans ce dernier cas, ils intègrent la plupart du temps
le SQL en plus d'un langage spécifique d'interrogation
basé sur le concept objet (O², ObjectStore, Objectivity,
Ontos, Poet, Versant, ORION,GEMSTONE...)

Les serveurs SQL C/S les plus connus sont : Oracle,


Sybase, Informix, DB2, SQL Server, Ingres, InterBase,
SQL Base...

Avantage : grande capacité de stockage, gestion de la


concurrence dans un SI à grand nombre d’utilisateurs,
haut niveau de paramétrage, meilleure répartition de la
charge du système, indépendance vis à vis de l'OS,
gestion des transactions, robustesse, cohérence des
données importante. Possibilité de montée en charge très
importante en fonction des types de plateformes
supportées.

Inconvénient : lourdeur dans le cas de solution


"monoposte", complexité du fonctionnement, coût élevé
des licences, administration importante, nécessité de
machines puissantes.

NOTA : Pour en savoir plus sur le sujet, lire l'étude


comparative sur les SGBDR à base de fichier et ceux
utilisant un moteur relationnel, intitulée Quand faut-il
investir sur le client/serveur ? On y discute aussi des
différents modes de verrouillage ...

6. Type de données
Dernier point que nous allons aborder dans ce premier
article, les différents types de données spécifiés par
SQL et leur disponibilité sur les 5 systèmes que nous
avons retenus pour notre étude.

Selon la norme ISO de SQL 92

6.1. Types alphanumériques


CHARACTER (ou CHAR) : valeurs alpha de longueur fixe
CHARACTER VARYING (ou VARCHAR ou CHAR VARYING) : valeur
alpha de longueur maximale fixée
Ces types de données sont codés sur 2 octets (EBCDIC ou
ASCII) et on doit spécifier la longueur de la chaîne.

Exemple :
NOM_CLIENT CHAR(32)
OBSERVATIONS VARCHAR(32000)

NATIONAL CHARACTER (ou NCHAR ou NATIONAL CHAR) :


valeurs
alpha de longueur fixe
NATIONAL CHARACTER VARYING (ou NCHAR VARYING ou
NATIONAL
CHAR VARYING) : valeur alpha de longueur maximale fixée
sur le jeu de caractère du pays
Ces types de données sont codés sur 4 octets (UNICODE)
et on doit spécifier la longueur de la chaîne.

Exemple :
NOM_CLIENT NCHAR(32)
OBSERVATIONS NCHAR VARYING(32000)
Nota : la valeur maximale de la longueur est fonction du
SGBDR.

6.2. Types numériques


NUMERIC (ou DECIMAL ou DEC) : nombre décimal à
représentation exacte à échelle et précision facultatives
INTEGER (ou INT): entier long
SMALLINT : entier court
FLOAT : réel à virgule flottante dont la représentation
est binaire à échelle et précision obligatoire
REAL : réel à virgule flottante dont la représentation
est binaire, de faible précision
DOUBLE PRECISION : réel à virgule flottante dont la
représentation est binaire, de grande précision
BIT : chaîne de bit de longueur fixe
BIT VARYING : chaîne de bit de longueur maximale

Pour les types réels NUMERIC, DECIMAL, DEC et FLOAT, on


doit spécifier le nombre de chiffres significatifs et la
précision des décimales après la virgule.

Exemple :
NUMERIC (15,2)

signifie que le nombre comportera au plus 15 chiffres


significatifs dont deux décimales.

ATTENTION : le choix entre le type DECIMAL


(représentation exacte) et le type FLOAT ou REAL
(représentation binaire) doit être dicté par des
considérations fonctionnelles. En effet, pour des
calculs comptables il est indispensable d'utiliser le
type DECIMAL exempt, dans les calculs de toute fraction
parasite capable d'entraîner des erreurs d'arrondis. En
fait le type DECIMAL se comporte comme un entier dans
lequel la virgule n'est qu'une représentation
positionnelle. En revanche pour du calcul scientifique
on préférera utiliser le type FLOAT, plus rapide dans
les calculs.

Exemple :
SELECT CAST(3.14159 AS FLOAT (16,4)) AS PI_FLT, CAST(3.14159 AS DECIMAL
(16,4)) AS PI_DEC

PI_FLT PI_DEC
----------------------------------------------------- ------------------
3.1415899999999999 3.1416
NOTA : on peut utiliser une notation particulière pour
forcer le typage implicite. Il s'agit d'une lettre
précédent la chaîne à transtyper. Les lettres
autorisées, sont : N (pour Unicode), B pour binary
(chaine de 0 et 1) et X pour binary (chaine hexadécimale
constituées de caractères allant de 0 à F).

Exemple :
SELECT N'toto' AS MY_NCHAR, B'01010111' AS MY_BINARY_BIT, X'F0A1' AS
MY_BINARY_HEX

6.3. Types temporels


DATE : date du calendrier grégorien
TIME : temps sur 24 heures
TIMESTAMP : combiné date temps
INTERVAL : intervalle de date / temps
Rappelons que les valeurs stockées doivent avoir pour
base le calendrier grégorien [2] qui est en usage depuis
1582, date à laquelle il a remplacé le calendrier
julien. En matière de temps, la synchronisation
s’effectue par rapport au TU ou temps universel (UTC :
Universal Time Coodinated) anciennement GMT (Greenwich
Mean Time) l’ensemble ayant été mis en place, lors la
conférence de Washington DC en 1884, pour éviter que les
chemins de chemins ne se télescopent.

ATTENTION : Le standard ISO adopté pour le SQL repose


sur le format AAAA-MM-JJ. Il est ainsi valable jusqu’en
l’an 9999… ou AAAA est l’année sur 4 chiffres, MM le
mois sur deux chiffres, et JJ le jour. Pour l'heure le
format ISO est hh:mm:ss.nnn (n étant le nombre de
millisecondes)

Exemple :
1999-03-26 22:54:28.123 est le 26 mars 1999 à 22h 54m, 28s et 123
millisecondes.

Mais peu de moteurs de requêtes l’implémente de manière


aussi formelle...

Le type INTERVAL est très particulier. Il est


malheureusement rarement présent dans les SGBDR.
Sa définition se fait à l'aide de la syntaxe suivante :
INTERVAL précision_min TO [précision_max]

où précision_min et précision_max peuvent prendre les


valeurs :
{YEAR | MONTH | DAY | HOUR | MINUTE | SECOND}

avec la condition supplémentaire suivante :


précision_max ne peut être qu'une mesure temporelle plus
fine que précision_min.

Exemple :
JOURS INTERVAL DAY
TRIMESTRE INTERVAL MONTH TO DAY
TACHE INTERVAL HOUR TO SECOND
DUREE_FILM INTERVAL MINUTE

ATTENTION
Veuillez noter que les réceptacles de valeurs
temporelles ainsi créés par le type INTERVAL sont des
entiers, et que leur valeur est contrainte lorsqu'ils
sont définis avec une précision maximum sur tous les
éléments les composant sauf le premier. Ainsi, dans une
colonne définie par le type INTERVAL MONTH TO DAY, on
pourra stocker une valeur de 48 mois et 31 jours, mais
pas une valeur de 48 mois et 32 jours. De même dans un
INTERVAL HOUR TO SECOND la valeur en heure est
illimitée, mais celle en minute et en seconde ne peut
dépasser 59.

6.4. Types " BLOBS " (hors du standard SQL 2)


Longueur maximale prédéterminée, donnée de type binaire,
texte long voire formaté, structure interprétable
directement par le SGBDR ou indirectement par add-on
externes (image, son, vidéo...). Attention : ne sont pas
normalisés !

On trouve souvent les éléments suivants :


TEXT : suite longue de caractères de longueur
indéterminé
IMAGE : stockage d'image dans un format déterminé
OLE : stockage d'objet OLE (Windows)

6.5. Autres types courants, hors norme SQL 92


BOOLEAN (ou LOGICAL) : curieusement le type logique (ou
encore booléen) est absent de la norme. On peut en
comprendre aisément les raisons… La pure logique
booléenne ne saurait être respectée à cause de la
possibilité offerte par SQL de gérer les valeurs nulles.
On aurait donc affaire à une logique dite " 3 états "
qui n’aurait plus rien de l’algèbre booléenne. La norme
passe donc sous silence, et à bon escient ce problème et
laisse à chaque éditeur de SGBDR le soin de concevoir ou
non un booléen " à sa manière ".
On peut par exemple implémenter un tel type de données,
en utilisant une colonne de type caractère longueur 1,
non nul et restreint à deux valeurs (V / F ou encore T /
F).
MONEY : est un sous type du type NUMERIC avec une
échelle maximale et une précision de deux chiffres après
la virgule.
BYTES (ou BINARY) : Type binaire (octets) de longueur
devant être précisée. Permet par exemple le stockage
d’un code barre.
AUTOINC : entier à incrément automatique par trigger.

6.6. Les domaines, ou la création de types spécifiques

Il est possible de créer de nouveau types de données à


partir de types pré existants en utilisant la notion de
DOMAINE.

Dans ce cas, avant d'utiliser un domaine, il faut le


recenser dans la base à l'aide d'un ordre CREATE :
CREATE DOMAIN nom_du_domaine AS type_de_donnée

Exemple :
CREATE DOMAIN DOM_CODE_POSTAL AS CHAR(5)

Dès lors il ne suffira plus que d'utiliser ce type à la


place de CHAR(5).

L'utilisation des domaines possède de nombreux avantages


:

ils peuvent faire l'objet de contraintes globales


ils peuvent être modifiés (ce qui modifie le type de
toutes les colonnes de table utilisant ce domaine d'un
seul coup !)
Exemple :
ALTER DOMAINE DOM_CODE_POSTAL
ADD CONSTRAINT MIN_CP CHECK (VALUE >= '01000')

Dans ce cas le code postal saisie devra au minimum


s'écrire 01000. On pourrait y ajouter une contrainte
maximum de forme (VALUE <= '99999').

NOTA : certains SGBDR n'ont pas implémenté l'ordre


CREATE DOMAIN. C'est le cas par exemple de SQL Server.
Ainsi, il faut aller "trifouiller" les tables systèmes
pour insérer un nouveau type de donnée à l'aide de
commandes "barbares" propre au SGBDR.

Exemple (SQL Server) :


sp_addtype DOM_CODE_POSTAL, 'CHAR(5)', 'null'

Un des immenses avantages de passer par des définitions


de domaines plutôt que d'utiliser directement des types
de donnés est que les domaines induisent une bonne
normalisation de la conception du schéma des données.

Pour ma part j'utilise souvent le jeu de domaine suivant


:
D_KEY_INTEGER entier
D_TRIGRAMME char(3)
D_CODE char(8)
D_LIBELLE_COURT varchar(16)
D_LIBELLE varchar(32)
D_LIBELLE_LONG varchar(64)
D_TITRE varchar(128)
D_DATE date
D_TEMPS dateTime
D_TEXTE text
D_ADRESSE varchar(32) /* spécifique aux adresses */
D_BOOLEEN smallint(1) /* contraint à 0 ou 1, valeur par défaut 0 */
D_MONNAIE
D_ENTIER_COURT
D_ENTIER_LONG
D_REEL
...

7. Contraintes de données
Dans la plupart des SGBDR il est possible de contraindre
le formatage des données à l'aide de différents
mécanismes.
Parmi les contraintes les plus courantes au sein des
données de la table on trouve :

valeur minimum
valeur maximum
valeur par défaut
valeur obligatoire
valeur unique
clef primaire
index secondaire
format ou modèle (par exemple 3 caractères majuscules
suivi de 2 caractères numériques)
table de référence (recopie d'une valeur d'une table
dans un champ d'une autre table en sélectionnant par
la clef) aussi appelé CHECK en SQL
liste de choix
Enfin entre deux tables liées, il est souvent nécessaire
de définir une contrainte de référence qui oblige un
enregistrement référencé par sa clef à être présent ou
détruit en même temps que l'enregistrement visé est
modifié, inséré ou supprimé. Ce mécanisme est appelé
INTÉGRITÉ RÉFÉRENTIELLE.

Exemple : Soit une base de données contenant deux tables


: CLIENT et COMMANDE dotées des structures suivantes ...

CLIENT :
NO_CLIENTINTEGER
NOM_CLIENTCHAR(32)

COMMANDE :

REF_COMMANDECHAR(16)
DATE_COMMANDEDATE
MONTANT_COMMANDEMONEY
NO_CLIENTINTEGER

Et dans lesquelles on trouve les valeurs suivantes :

CLIENT :

143DUPONT
212MARTIN
823DUBOIS

COMMANDE :

1999-1111/7/19991235.52212
1999-1217/7/199945234.63823
1999-1318/7/19995485.23142
1999-1421/7/199911542.23212

Si l'on détruit la ligne de la table CLIENT concernant


le n°212 (MARTIN) alors les factures 1999-11 et 1999-14
deviennent orphelines. Il faut donc interdire la
suppression de ce client tant que des références de ce
client persistent dans la table COMMANDE.
De même, le changement de la valeur de la clef NO_CLIENT
ferait perdre la valeur de référence du lien entre les
deux tables, à moins que la modification ne soit
répercutée dans la table fille.

NOTA : on parle alors de tables en relation mère / fille


ou encore maître / esclave.
8. Triggers et procédures stockées
En ce qui concerne les SGBDR en architecture client /
serveur, il est courant de trouver des mécanismes de
triggers (permettant d'exécuter du code en fonction d'un
événement survenant dans une table) ainsi que des
procédures stockées (du code pouvant être déclenché à
tout moment). Dans les deux cas, c'est sur le serveur,
et non dans le poste client que la procédure ou le
trigger s'effectue.

L'avantage réside dans une plus grande intégrité du


maniement des données et souvent d'un traitement plus
rapide que si le même code trournait sur le poste client.

Ainsi le calcul d'un tarif dans une table de prestation


peut dépendre de conditions parfois complexes ne pouvant
être facilement exécutée à l'aide de requêtes SQL. Dans
ce cas on aura recours au langage hôte du SGBDR, pour
lequel on écrira une procédure permettant de calculer ce
tarif à partir de différents paramètres.

Mais la plupart du temps, triggers et procédures


stockées s'écrivent dans un langage propre au SGBDR
(Transact SQL pour SQL Server et Sybase, PL/SQL pour
Oracle, etc...). Pour un aperçu du langage Transact SQL,
veuillez lire l'article "Un aperçu du langage Transact
SQL".

Exemple : on désire calculer le tarif d'adhésion à une


mutuelle santé pour une famille composée d'un homme né
le 11/5/1950, d'une compagne née le 21/6/1965, d'un fils
né le 16/3/1992, d'une fille né le 11/1/1981 et d'une
grand mère à charge (ascendant) née le 21/12/1922, le
futur adhérent désirant payer sa cotisation au mois.
Les bases tarifaires établies sont les suivantes :

Table "TARIF_BASE" :

TYPESEXEAGE_MINTARIF
ADHERENTHOMME161500
ADHERENTHOMME651800
ADHERENTFEMME161400
ADHERENTFEMME651700
CONJOINTHOMME161200
CONJOINTHOMME651500
CONJOINTFEMME161100
CONJOINTHOMME651300
ENFANTHOMME0400
ENFANTHOMME8600
ENFANTHOMME14800
ENFANTHOMME181000
ENFANTFEMME0300
ENFANTFEMME8500
ENFANTFEMME14700
ENFANTFEMME18850
ASCENDANTHOMME351200
ASCENDANTHOMME651400
ASCENDANTFEMME351100
ASCENDANTHOMME651300

Table "TARIF_MAJO"

PAIEMENTMAJORATION
MOIS12%
TRIMESTRE8%
SEMESTRE4%
ANNEE0%

Table "TARIF_MINO"

Nb_ENFANT_MAXMINORATION
110%
225%
350%
4100%

Il est très difficile d'établir une requête permettant


de trouver le bon tarif dans un tel cas. En revanche, en
passant la table de paramètre suivant à une procédure :

Table "PARAMS" :
TYPESEXEDATE_NAISSANCE
ADHERENTHOMME11/5/1950
CONJOINTFEMME21/6/1965
ENFANTHOMME16/3/1992
ENFANTFEMME11/1/1981
ASCENDANTFEMME21/12/1922

Il n'est pas très compliquée d'écrire dans un langage


donné une procédure permettant de calculer ce tarif.
Une telle, procédure pourrait s'écrire dans un pseudo
code proche du Pascal :
Procedure CalcTarif(Params Array, modePaiement string) : money

var
unTarif : mney
leTarif : money
i,j : smallint ; indice de boucle
n : samllint ; nombre d'enfants
; tableau multicellulaire représentant les données des tables
BaseTarif : Array ; table TARIF_BASE
MajoTarif : Array ; table TARIF_MAJO
Minotarif : Array ; table TARIF_MINO
endVar

leTarif := 0

for i from 1 to Params.size()


; recherche du tarif pour les enfant avec comptage du nombre d'enfants
if Params.TYPE = "ENFANT"
then
for j from 1 to BaseTarif.size()
if (BaseTarif[j].TYPE = Params[i].TYPE) and (BaseTarif[j].SEXE =
Params[i].SEXE)
then
if BaseTarif[j].AGE_MIN >= CalcAge(Params[i].DATE_NAISSANCE)
then
unTarif := BaseTarif[j].TARIF
break
endif
endif
endFor
; cumul des différents tarifs enfant trouvés
leTarif := leTarif + unTarif
; dénombrement des enfants
n = n+1
; recherche de la minoration pour le nombre d'enfants
for j from MinoTarif.size() downTo 1
if MinoTarif[j].NB_ENFANT_MAX <= n
then
; minoration du tarif cumulé des enfants
leTarif := leTarif * (1 - MinoTarif[j].MINORATION / 100)
break
endif
endFor
endif
endFor
for i from 1 to Params.size()
; recherche du tarif pour les autres types excepté les enfants
if Params.TYPE = "ENFANT"
then
continue
endif
for j from 1 to BaseTarif.size()
if (BaseTarif[j].TYPE = Params[i].TYPE) and (BaseTarif[j].SEXE =
Params[i].SEXE)
then
if BaseTarif[j].AGE_MIN >= CalcAge(Params[i].DATE_NAISSANCE)
then
unTarif := BaseTarif[j].TARIF
break
endif
endif
endFor
; cumul des différents autres tarifs trouvés
leTarif := leTarif + unTarif
endFor
; calcul de la majoration pour le mode de paiement :
for i from 1 to MajoTarif.size()
if modePaiement = MajoTarif[i].PAIEMENT
then
leTarif := leTarif * (1 + MajoTarif[j].MAJORATION / 100)
break
endif
endFor
return leTarif

endProcedure

9. Résumé
Voici les différentes implémentations du SQL sur
quelques uns des différents moteurs relationnels que
nous avons choisi d’analyser.

SGBDRParadox 7Access 97Sybase adaptive 11SQL


Server 7Oracle 8
NatureService de fichierService de fichierServeur
de donnéesServeur de donnéesServeur de données
Nb utilisateur (max / en pratique)255 / 50300 / 10

Taille max de la baseillimitée1 Go


Taille max d'une table2 Go (hors BLOBS)1 Go
NormalisationSQL 92SQL 89 ?SQL 92SQL 92SQL 89
DDLOui (a)OuiOuiOuiOui
DMLOuiOuiOuiOuiOui
DCLNonOuiOuiOuiOui
TCLOui (b,c)NonOuiOuiOui
DCLNonOuiOuiOuiOui
TCLOui (b,c)NonOuiOuiOui

CHARlimité à 255 car.limité à 255 car.limité à 255


car.limité à 8000 car.limité à 2000 car.

VARCHARNonNonOuiOuilimité à 4000 car.

NUMERICOui, avec 15 chiffres significatifssous


types comprenant des entiers et des réelsOuiOuiOui

INTEGEROuiNonOuiOuiOui

SMALLINTOuiNonOuiOuiOui

FLOATOui, en fait NUMERICVoir NUMERICOuiOuiOui

DATEOuiNonNonNonOui

TIMEOuiNonNonNonNon

TIMESTAMPOuiOuiOuiOuiNon

INTERVALNonNonNonNonNon

BITNonOuiOui
BOOLEANOui (LOGICAL)OuiNonNonNon
MONEYOuiOuiOuiOuiNon
BYTESOuiNonNonNonOui (RAW)
AUTOINCOuiOuiNon (d)Non (d)Non (d)
BLOBOui (4 types différents : MEMO, MEMO FORMATE
en RTF, IMAGE et BINARY) limités à 2 GoOui (2
types différents : MEMO, HYPERLIEN limité à 64
ko)Oui (2 types différents TEXT IMAGE)Oui (2 types
différents IMAGE, TEXT) Oui (7 types différents :
LONG, LONG RAW, LONG VARCHAR, BFILE, BLOB, CLOB,
NCLOB)
Autres typesOCTET (1 à 255), OLEOLE, liste de
choixBIT, BINARYBIT, BINARY, CURSOR, TINYINT, GUID
ROWID (N° d'enregistrement)
INTEGRITÉ RÉFÉRENTIELLEOui, stricte ou cascade
(suppression et modif.)Oui, stricte ou cascade
(suppression et modif.)OuiOui, pas en cascadeOui
TRIGGERSNonNonOuiOui, limitésBEFORE INSERT, BEFORE
UPDATE, BEFORE DELETE, AFTER INSERT, AFTER UPDATE,
AFTER DELETE
PROCÉDURES STOCKÉESNonNonOui, langage propriétaire
Transact SQLOui, langage propriétaire Transact
SQLOui, langage propriétaire PL/SQL

(a) Avec quelques limitations (par exemple les


contraintes d’intégrité référentielles ne peuvent être
crées par le SQL)
(b) Limité en rollback à 255 enregistrements
(c) Pas dans le SQL, mais en code du langage hôte
(d) Mais possible à l'aide de triggers ou de commandes
spécifiques

10. Conclusion
En matière de SGBDR " fichier ", Paradox se révèle plus
pauvre au niveau du DDL et du TCL, mais plus riche en
matière de DML qu’Access. Quant aux types de données,
Paradox se révèle bien plus complet qu’Access qui
n’intègre même pas de champ de type " image "... Pensez
que dans Access le type entier n’est même pas défini !
Enfin en matière de BLOB la plupart des SGBDR acceptent
jusqu’à 2Go de données, sauf Access qui est limité à 64
Ko...
En ce qui concerne la capacité de stockage Access révèle
très rapidement de nombreuses limites, comme en nombre
d'utilisateurs en réseau.
En matière de contrôle des transactions Paradox est
limité à 255 enregistrements en RollBack. Mais la
présence de tables auxiliaires permet de dépasser ces
limites sans encombre, à condition de prévoir le code à
mettre en œuvre.
Différence fondamentale pour Paradox, pas de DCL. Mais
cela est largement compensé par un niveau de sécurité a
forte granularité qui n’est pas compatible avec le SQL
normalisé. Ainsi dans Paradox on peut placer des droits
au niveau des tables mais aussi de chaque champ et le
moteur crypte les données dès qu’un mot de passe est
défini (SQL Base de Centura permet aussi de crypter les
données à l’aide des plus récents algorithmes de
chiffrage)
Point très négatif pour Access dans sa catégorie : il
pratique le verrouillage de pages !
Enfin les vues n’existent pas dans Access mais elles
sont présentent dans Paradox sous une forme non SQL
appelée " vue de requête reliés " (QBE).

En matière de serveur SQL C/S, le SGBDR Sybase se révèle


très proche de SQL Server ce qui n'est pas absurde
puisqu'ils sont parents. Oracle possède une bonne
diversité de types mais sa conformité à la norme laisse
à désirer (pas de JOIN par exemple, pauvreté des
fonctions temporelles). En revanche Oracle possède un
type de champ bien utile et intégré à toutes les tables,
le ROWID qui donne le n° de la ligne dans la table et
qui est toujours unique, même si la ligne a été
supprimée. On retrouve des mécanismes similaires dans
SQL Server sous le nom de GUID ou bien avec l'auto
incrémentation via "identity".
1. Le modèle conceptuel de données
Voici un MCD établi à partir de l'outil AMC (AMC*Designor ou encore Power AMC) :

Pour ceux qui ne seraient pas familiarisé avec cet outil et la méthode MERISE, voir
l'article consacré à la méthode MERISE sur ce même site.

Quelques explications
La plupart des clefs sont des entiers (I) qui pourront être auto générés par exemple
par un type AUTOINCREMENT (Paradox, Access) ou encore via un trigger (identity de
SQL Server...). Pour certaines entités, notamment celles servant de références à la
saisie (MODE_PAIEMENT, TYPE, CODE) la clef est un code. Enfin pour les entités
TARIF et PLANNING, nous avons choisi une date comme clef.
Chaque entité est repérée à l'aide d'un trigramme (code de 3 lettres) qui sert de
préfixe pour chaque attribut. Exemple : CHB pour CHAMBRE, LIF pour
LIGNE_FACTURE, etc...
Les booléens seront représentés par des valeurs numériques 0 (faux) et 1 (vrai),
chaque attribut ayant obligatoirement une valeur par défaut.

Voici les codes des différents types de données :

I Integer (entier long)


N Number (réel)
SI Short Integer (entier court)
BL Boolean (booléen)
A Char (caractères alpha de longueur fixe)
VarChar (caractères alpha longueur variable
VA
avec un maximum)
D Date
MN Money (monnaie)
L'association "occupée" permet de connaître la réservation ou l'occupation d'une
chambre (une chambre peut avoir été réservée mais pas occupée), c'est pourquoi
cette association possède les attributs NB_PERS (nombre de personnes : entier)
RESERVE (réservée : booléen) et OCCUPE (occupe : booléen). Une chambre à une
date donnée, ne peut être occupée que par un seul client. Mais un client peut
occuper plusieurs chambres à la même date ou la même chambre à différentes
dates, voire même plusieurs chambres à plusieurs dates...
Entité CLIENT : Un client peut avoir plusieurs adresses, plusieurs numéros de
téléphone et plusieurs e-mail. Pour le téléphone, comme pour l'e-mail, l'attribut
'localisation' permet de savoir si le téléphone est situé au domicile, à l'entreprise,
etc...
L'entité TITRE permet de donner un titre à une personne, parmi les valeurs 'M.'
(monsieur), 'Mme.' (madame) et 'Melle.' (mademoiselle).
L'entité TYPE permet de connaître le type de téléphone, parmi les valeurs 'TEL'
(téléphone), 'FAX' (télécopie) et 'GSM' (portable).
L'entité MODE_PAIEMENT permet de connaître le genre de paiement, parmi les
valeurs 'ESP' (espèces), 'CHQ' (chèque), 'CB' (carte bancaire).
L'association "payée" intègre la date du paiement d'une facture.

NOTA : ce modèle est incomplet. Si l'on devait faire figurer l'adresse sur la facture il
faudrait choisir une adresse du client. La meilleure façon de régler le problème est de
faire glisser la clef du client dans la table des adresses et d'ajouter dans la table
facture l'ID de l'adresse choisie pour la facture. C'est ce que l'on apelle un "lien
identifiant" qui se positionne au niveau du lien entre l'association "domicilié" et
l'entité "adresse". On rajoute alors une association entre la facture et l'adresse de
cardinalité 0,1.

2. Le modèle physique
Nous avons demandé à générer un modèle basé sur le SQL ANSI de manière à
pouvoir être compatible avec la plupart des SGBDR :

Vous constaterez que toutes les tables ont été préfixées avec la lettre T lorsquelles
proviennent d'entités, et de TJ lorsqu'elles proviennent d'associations. Dans ce
dernier cas, leur nom a été constitué des trigrammes des tables en jeu dans la
jointure (TJ_TRF_LIF et TJ_CHB_PLN_CLI).
3. Script de création de la base de données
La génération de la base de données au format SQL standard donne le code suivant :

-- ============================================================
-- Nom de la base : MCD_HOTEL
-- Nom de SGBD : ANSI Niveau 2
-- Date de création : 16/01/2001 22:24
-- Copyright : Frédéric BROUARD
-- ============================================================

-- ============================================================
-- Table : T_CHAMBRE
-- ============================================================
create table T_CHAMBRE
(
CHB_ID INTEGER not null,
CHB_NUMERO SMALLINT not null,
CHB_ETAGE CHAR(3) ,
CHB_BAIN NUMERIC(1) not null default
0,
CHB_DOUCHE NUMERIC(1) not null default
1,
CHB_WC NUMERIC(1) not null default
1,
CHB_COUCHAGE SMALLINT not null,
CHB_POSTE_TEL CHAR(3) ,
primary key (CHB_ID)
);

-- ============================================================
-- Index : T_CHAMBRE_PK
-- ============================================================
create unique index T_CHAMBRE_PK on T_CHAMBRE (CHB_ID asc);

-- ============================================================
-- Table : T_TARIF
-- ============================================================
create table T_TARIF
(
TRF_DATE_DEBUT DATE not null,
TRF_TAUX_TAXES NUMERIC not null,
TRF_PETIT_DEJEUNE NUMERIC(8,2) not null,
primary key (TRF_DATE_DEBUT)
);

-- ============================================================
-- Index : T_TARIF_PK
-- ============================================================
create unique index T_TARIF_PK on T_TARIF (TRF_DATE_DEBUT asc);

-- ============================================================
-- Table : T_PLANNING
-- ============================================================
create table T_PLANNING
(
PLN_JOUR DATE not null,
primary key (PLN_JOUR)
);

-- ============================================================
-- Index : T_PLANNING_PK
-- ============================================================
create unique index T_PLANNING_PK on T_PLANNING (PLN_JOUR asc);

-- ============================================================
-- Table : T_TITRE
-- ============================================================
create table T_TITRE
(
TIT_CODE CHAR(8) not null,
TIT_LIBELLE VARCHAR(32) not null,
primary key (TIT_CODE)
);
-- ============================================================
-- Index : T_TITRE_PK
-- ============================================================
create unique index T_TITRE_PK on T_TITRE (TIT_CODE asc);

-- ============================================================
-- Table : T_TYPE
-- ============================================================
create table T_TYPE
(
TYP_CODE CHAR(8) not null,
TYP_LIBELLE VARCHAR(32) not null,
primary key (TYP_CODE)
);

-- ============================================================
-- Index : T_TYPE_PK
-- ============================================================
create unique index T_TYPE_PK on T_TYPE (TYP_CODE asc);

-- ============================================================
-- Table : T_MODE_PAIEMENT
-- ============================================================
create table T_MODE_PAIEMENT
(
PMT_CODE CHAR(8) not null,
PMT_LIBELLE VARCHAR(64) not null,
primary key (PMT_CODE)
);

-- ============================================================
-- Index : T_MODE_PAIEMENT_PK
-- ============================================================
create unique index T_MODE_PAIEMENT_PK on T_MODE_PAIEMENT (PMT_CODE
asc);

-- ============================================================
-- Table : T_CLIENT
-- ============================================================
create table T_CLIENT
(
CLI_ID INTEGER not null,
TIT_CODE CHAR(8) ,
CLI_NOM CHAR(32) not null,
CLI_PRENOM VARCHAR(25) ,
CLI_ENSEIGNE VARCHAR(100) ,
primary key (CLI_ID)
);

-- ============================================================
-- Index : T_CLIENT_PK
-- ============================================================
create unique index T_CLIENT_PK on T_CLIENT (CLI_ID asc);

-- ============================================================
-- Index : L_CLI_TIT_FK
-- ============================================================
create index L_CLI_TIT_FK on T_CLIENT (TIT_CODE asc);

-- ============================================================
-- Table : T_FACTURE
-- ============================================================
create table T_FACTURE
(
FAC_ID INTEGER not null,
CLI_ID INTEGER not null,
PMT_CODE CHAR(8) ,
FAC_DATE DATE not null,
FAC_PMT_DATE DATE ,
primary key (FAC_ID)
);

-- ============================================================
-- Index : T_FACTURE_PK
-- ============================================================
create unique index T_FACTURE_PK on T_FACTURE (FAC_ID asc);

-- ============================================================
-- Index : L_FAC_CLI_FK
-- ============================================================
create index L_FAC_CLI_FK on T_FACTURE (CLI_ID asc);

-- ============================================================
-- Index : TJ_FAC_PMT_FK
-- ============================================================
create index TJ_FAC_PMT_FK on T_FACTURE (PMT_CODE asc);

-- ============================================================
-- Table : T_ADRESSE
-- ============================================================
create table T_ADRESSE
(
ADR_ID INTEGER not null,
CLI_ID INTEGER not null,
ADR_LIGNE1 VARCHAR(32) not null,
ADR_LIGNE2 VARCHAR(32) ,
ADR_LIGNE3 VARCHAR(32) ,
ADR_LIGNE4 VARCHAR(32) ,
ADR_CP CHAR(5) not null,
ADR_VILLE CHAR(32) not null,
primary key (ADR_ID)
);

-- ============================================================
-- Index : T_ADRESSE_PK
-- ============================================================
create unique index T_ADRESSE_PK on T_ADRESSE (ADR_ID asc);

-- ============================================================
-- Index : L_ADR_CLI_FK
-- ============================================================
create index L_ADR_CLI_FK on T_ADRESSE (CLI_ID asc);

-- ============================================================
-- Table : T_TELEPHONE
-- ============================================================
create table T_TELEPHONE
(
TEL_ID INTEGER not null,
CLI_ID INTEGER not null,
TYP_CODE CHAR(8) not null,
TEL_NUMERO CHAR(20) not null,
TEL_LOCALISATION VARCHAR(64) ,
primary key (TEL_ID)
);

-- ============================================================
-- Index : T_TELEPHONE_PK
-- ============================================================
create unique index T_TELEPHONE_PK on T_TELEPHONE (TEL_ID asc);

-- ============================================================
-- Index : L_TEL_CLI_FK
-- ============================================================
create index L_TEL_CLI_FK on T_TELEPHONE (CLI_ID asc);

-- ============================================================
-- Index : L_TEL_TYP_FK
-- ============================================================
create index L_TEL_TYP_FK on T_TELEPHONE (TYP_CODE asc);

-- ============================================================
-- Table : T_EMAIL
-- ============================================================
create table T_EMAIL
(
EML_ID INTEGER not null,
CLI_ID INTEGER not null,
EML_ADRESSE VARCHAR(100) not null,
EML_LOCALISATION VARCHAR(64) ,
primary key (EML_ID)
);

-- ============================================================
-- Index : T_EMAIL_PK
-- ============================================================
create unique index T_EMAIL_PK on T_EMAIL (EML_ID asc);

-- ============================================================
-- Index : L_EML_CLI_FK
-- ============================================================
create index L_EML_CLI_FK on T_EMAIL (CLI_ID asc);

-- ============================================================
-- Table : T_LIGNE_FACTURE
-- ============================================================
create table T_LIGNE_FACTURE
(
LIF_ID INTEGER not null,
FAC_ID INTEGER not null,
LIF_QTE NUMERIC not null,
LIF_REMISE_POURCENT NUMERIC ,
LIF_REMISE_MONTANT NUMERIC(8,2) ,
LIF_MONTANT NUMERIC(8,2) not null,
LIF_TAUX_TVA NUMERIC(8,2) not null,
primary key (LIF_ID)
);

-- ============================================================
-- Index : T_LIGNE_FACTURE_PK
-- ============================================================
create unique index T_LIGNE_FACTURE_PK on T_LIGNE_FACTURE (LIF_ID asc);

-- ============================================================
-- Index : L_LIF_FAC_FK
-- ============================================================
create index L_LIF_FAC_FK on T_LIGNE_FACTURE (FAC_ID asc);

-- ============================================================
-- Table : TJ_TRF_CHB
-- ============================================================
create table TJ_TRF_CHB
(
CHB_ID INTEGER not null,
TRF_DATE_DEBUT DATE not null,
TRF_CHB_PRIX NUMERIC(8,2) not null,
primary key (CHB_ID, TRF_DATE_DEBUT)
);

-- ============================================================
-- Index : TJ_TRF_CHB_PK
-- ============================================================
create unique index TJ_TRF_CHB_PK on TJ_TRF_CHB (CHB_ID asc,
TRF_DATE_DEBUT asc);

-- ============================================================
-- Index : L_CHB_TRF_FK
-- ============================================================
create index L_CHB_TRF_FK on TJ_TRF_CHB (CHB_ID asc);

-- ============================================================
-- Index : L_TRF_CHB_FK
-- ============================================================
create index L_TRF_CHB_FK on TJ_TRF_CHB (TRF_DATE_DEBUT asc);

-- ============================================================
-- Table : TJ_CHB_PLN_CLI
-- ============================================================
create table TJ_CHB_PLN_CLI
(
CHB_ID INTEGER not null,
PLN_JOUR DATE not null,
CLI_ID INTEGER not null,
CHB_PLN_CLI_NB_PERS SMALLINT not null,
CHB_PLN_CLI_RESERVE NUMERIC(1) not null default
0,
CHB_PLN_CLI_OCCUPE NUMERIC(1) not null default
1,
primary key (CHB_ID, PLN_JOUR)
);

-- ============================================================
-- Index : TJ_CHB_PLN_CLI_PK
-- ============================================================
create unique index TJ_CHB_PLN_CLI_PK on TJ_CHB_PLN_CLI (CHB_ID asc,
PLN_JOUR asc, CLI_ID asc);

-- ============================================================
-- Index : L_CHB_PLN_CLI_FK
-- ============================================================
create index L_CHB_PLN_CLI_FK on TJ_CHB_PLN_CLI (CHB_ID asc);

-- ============================================================
-- Index : L_PLN_CHB_CLI_FK
-- ============================================================
create index L_PLN_CHB_CLI_FK on TJ_CHB_PLN_CLI (PLN_JOUR asc);

-- ============================================================
-- Index : L_CLI_CHB_PLN_FK
-- ============================================================
create index L_CLI_CHB_PLN_FK on TJ_CHB_PLN_CLI (CLI_ID asc);
Vous noterez que nous avons volontairement omis les intégrités référentielles de
manière à alléger le code mais aussi pour le rendre le plus compatible possible.

Si vous voulez la version complète du code de génération de cette base de données,


voici un tableau des différentes versions que j'ai fait générer par AMC :

ACCESS 95/97
DB2 C/S 2
DBASE 5.0 Windows
FOXPRO 5 Windows
INFORMIX SQL 7.1
INGRES 6.4
INTERBASE 4.0
ORACLE 7
PARADOX 7 Windows
SQL SERVER 6
SYBASE SQL SERVER 10
NOTA : nous n'avons pas introduit de colonne de type auto incrémenté dans les
scripts de création de base de données, mais vous pouvez les modifier en y
introduisant un trigger. Ne le faites pas si vous voulez pouvoir exploiter le jeu de
données nécessaire aux exercices qui se trouvent dans les chapitres qui suivent.
Exemples : pour le SGBDR InterBase de Borland / Inprise, vous pouvez utiliser un
générateur de nombre séquentiel utilisable par tous. Il faut donc créer autant de
générateur qu'il existe dans la base de colonnes nécessitant une auto
incrémentation, puis dans chacun des triggers de type BEFORE INSERT, appeler ce
générateur.
Pour SQL Server de Microsoft, vous pouvez utiliser le type 'identity', mais vous
devrez certainement modifier le type des colonnes des clefs étrangères dans le script
de création de la base.
Pour Paradox il suffit de remplacer le type "I" par le type "+" dans les colonnes où
cela s'avère nécessaire.

Exemple de trigger pour InterBase :

A la création de la base :

CREATE GENERATOR CLI_ID_GEN TO 2301;


Qui indique de réserver un espace pour stocker la valeur de l'auto incrément de nom
CLI_ID_GEN et commençant par la valeur 2301.
Dans le trigger BEFORE INSERT de la table CLIENT, on utilise ce générateur pour
alimenter le champs NUM_CLI :

CREATE TRIGGER AUTOINC_CLI FOR T_CLIENT


BEFORE INSERT AS
BEGIN
NEW.CLI_ID = GEN_ID(CLI_ID_GEN,1);
END
NEW.CLI_ID est la valeur de la colonne après passage dans le trigger et
AUTOINC_CLI une fonction appelant le générateur.

4. Création des données


Comme il nous faut des données pour travailler, vous trouverez ci dessous un script
SQL dans lequel chaque ligne constitue une instruction d'insertion de données.
Nous avons utilisé les caractères /* et */ pour y insérer des commentaires. Si votre
interpréteur SQL ne comprend pas ces instructions, vous pouvez réaliser un petit
programme qui lit ce fichier ligne par ligne et lance les requêtes d'insertion sauf dans
les deux cas suivants :

 la ligne est vide


 la ligne débute par /*

Téléchargez le script SQL de création du jeu d'essai :

Formats d'insertion Exemple (23 février 2001)


Date ISO (AAAA-MM-JJ) insert into table (colonne_date) values ('2001-02-23')
Date US ('MM/JJ/AAAA') insert into table (colonne_date) values ('02/23/2001')
Date FR ('JJ/MM/AAAA') insert into table (colonne_date) values ('23/02/2001')
Pour le SGBDR Paradox, vous pouvez :

1. mettre le fichier des ordres SQL d'insertion dans une colonne de table via un
import de données. Par exemple dans une table Paradox de nom
INSERT_EXEMPLE possédant une colonne de nom SQL_ORDER.
2. jouer le script ci dessus afin d'insérer les données :

var
tc TCursor
svar String
sqlVar SQL
db Database
endvar

errorTrapOnWarnings(True)
db.open(...) => chemin de la base de données cible
tc.open("INSERT_EXEMPLE.db")

scan tc :
svar = TC.SQL_ORDER
try
sqlVar.readFromString(svar)
sqlVar.executeSQL(db)
onFail
errorShow()
msgInfo("ORDRE SQL",sVar)
quitLoop
endTry
endscan

errorTrapOnWarnings(False)
endMethod

Dans le précédent article nous avons commencer à décortiquer


le simple SELECT. Dans le présent, nous allons nous consacrer
aux jointures entre tables. Toutes les jointures sous toutes
les coutûres !
Autrement dit comment faire des requêtes portant sur plusieurs
tables.

I. Préambule
II. Les jointures ou comment faire des requêtes sur plusieurs
tables
II-A. Premiers essais de jointure
II-B. Différents type de jointures (naturelles, équi, non
equi, auto, externes, hétérogènes, croisée et union)
III. Syntaxe normalisée des jointures
III-A. Opérateur de jointure naturelle
III-B. Les jointures internes
III-C. Les jointures externes
III-D. Différence entre jointure externe et jointure interne
III-D-1. L'hypothèse du monde clos
III-D-2. Mécanisme en jeu
III-D-3. Discussion sur la jointure externe
III-E. La jointure croisée
III-F. La jointure d'union
IV. Nature des conditions de jointures
IV-A. Équi-jointure
IV-B. Non équi-jointure
IV-C. Auto-jointure
IV-D. La jointure hétérogène
V. Récapitulatif des jointures normalisées
V-A. Terminologie et syntaxe des jointures
V-B. Arbre de jointure
VI. Note importante
VII. Résumé

Les jointures permettent d'exploiter pleinement le modèle


relationnel des tables d'une base de données.
Elle sont faites pour mettre en relation deux (ou plus) tables
concourant à rechercher la réponse à des interrogations. Une
jointure permet donc de combiner les colonnes de plusieurs
tables.

Il existe en fait différentes natures de jointures que nous


expliciterons plus en détail.

Retenez cependant que la plupart des jointures entre tables s'effectuent en imposant
l'égalité
des valeurs d'une colonne d'une table à une colonne d'une autre table. On parle alors
de jointure naturelle ou équi-jointure. Mais on trouve aussi des jointures d'une
table sur elle-même. On parle alors d'auto-jointure. De même, il arrive que l'on
doive procéder à des jointures externe, c'est-à-dire joindre une table à une autre, même si
la valeur de liaison est absente dans une table ou l'autre. Enfin, dans quelques cas, on
peut procéder à des jointures hétérogènes, c'est-à-dire que l'on remplace le critère
d'égalité par un critère d'inégalité ou de différence.
Nous verrons au moins un cas de cette espèce.

Une jointure entre tables peut être mise en oeuvre, soit à


l'aide des éléments de syntaxe SQL que nous avons déjà vu,
soit à l'aide d'une clause spécifique du SQL, la clause JOIN.
Nous allons commencer par voir comment à l'aide du SQL de base
nous pouvons exprimer une jointure.
II-A. Premiers essais de jointure
Rappel de la syntaxe du SELECT :

SELECT [DISTINCT ou ALL] * ou liste_de_colonnes FROM


nom_des_tables_ou_des_vues

C'est ici le pluriel de la partie FROM qui change tout!

Tâchons donc de récupérer les n° des téléphones associés aux


clients.

Exemple 1 :

SELECT CLI_NOM, TEL_NUMERO


FROM T_CLIENT, T_TELEPHONE

CLI_NOM TEL_NUMERO
------- --------------
DUPONT 01-45-42-56-63
DUPONT 01-44-28-52-52
DUPONT 01-44-28-52-50
DUPONT 06-11-86-78-89
DUPONT 02-41-58-89-52
DUPONT 01-51-58-52-50
DUPONT 01-54-11-43-21
DUPONT 06-55-41-42-95
DUPONT 01-48-98-92-21
DUPONT 01-44-22-56-21
...

Cette requête ne possède pas de critère de jointure entre une


table et l'autre. Dans ce cas, le compilateur SQL calcule le
produit cartésien des deux ensembles, c'est-à-dire qu'à chaque
ligne de la première table, il accole l'ensemble des lignes de
la seconde à la manière d'une "multiplication des petits
pains" !
Nous verrons qu'il existe une autre manière, normalisée cette
fois, de générer ce produit cartésien. Mais cette requête est
à proscrire.
Dans notre exemple elle génère 17 400 lignes!

Il faut donc définir absolument un critère de jointure.

Dans le cas présent, ce critère est la correspondance entre


les colonnes contenant la référence de l'identifiant du client
(CLI_ID).

Exemple 2 :

SELECT CLI_NOM, TEL_NUMERO


FROM T_CLIENT, T_TELEPHONE
WHERE CLI_ID = CLI_ID

CLI_NOM TEL_NUMERO
------- --------------
DUPONT 01-45-42-56-63
DUPONT 01-44-28-52-52
DUPONT 01-44-28-52-50
DUPONT 06-11-86-78-89
DUPONT 02-41-58-89-52
DUPONT 01-51-58-52-50
DUPONT 01-54-11-43-21
DUPONT 06-55-41-42-95
DUPONT 01-48-98-92-21
DUPONT 01-44-22-56-21
...

Nous n'avons pas fait mieux, car nous avons créé une clause
toujours vraie, un peu à la manière de 1 = 1 !
En fait il nous manque une précision : il s'agit de déterminer
de quelles tables proviennent les colonnes CLI_ID de droite et
de gauche. Cela se précise à l'aide d'une notation pointée en
donnant le nom de la table.

Il est donc nécessaire d'indiquer au compilateur la provenance


de chacune des colonnes CLI_ID et donc d'opérer une
distinction entre l'une et l'autre colonne.
Ainsi, chaque colonne devra être précédée du nom de la table,
suivi d'un point.

Exemple 3 :

SELECT CLI_NOM, TEL_NUMERO


FROM T_CLIENT, T_TELEPHONE
WHERE T_CLIENT.CLI_ID = T_TELEPHONE.CLI_ID

CLI_NOM TEL_NUMERO
------- --------------
DUPONT 01-45-42-56-63
DUPONT 01-44-28-52-52
DUPONT 01-44-28-52-50
BOUVIER 06-11-86-78-89
DUBOIS 02-41-58-89-52
DREYFUS 01-51-58-52-50
DUHAMEL 01-54-11-43-21
BOUVIER 06-55-41-42-95
MARTIN 01-48-98-92-21
MARTIN 01-44-22-56-21
...

On tombe ici à 174 enregistrements dans la table !

Mais il existe une autre façon de faire, plus simple encore.


On utilise la technique du "surnommage", c'est-à-dire que l'on
attribue un surnom à chacune des tables présente dans la
partie FROM du SELECT :
Exemple 4 :

SELECT CLI_NOM, TEL_NUMERO


FROM T_CLIENT C, T_TELEPHONE T
WHERE C.CLI_ID = T.CLI_ID

CLI_NOM TEL_NUMERO
------- --------------
DUPONT 01-45-42-56-63
DUPONT 01-44-28-52-52
DUPONT 01-44-28-52-50
BOUVIER 06-11-86-78-89
DUBOIS 02-41-58-89-52
DREYFUS 01-51-58-52-50
DUHAMEL 01-54-11-43-21
BOUVIER 06-55-41-42-95
MARTIN 01-48-98-92-21
MARTIN 01-44-22-56-21
...

Ici, la table T_CLIENT a été surnommée "C" et la table


T_TELEPHONE "T".

Bien entendu, et comme dans les requêtes monotabulaires on


peut poser des conditions supplémentaires de filtrage dans la
clause WHERE. Cherchons par exemple les clients dont les
numéros de téléphone correspondent à un fax :

Exemple 5 :

SELECT CLI_NOM, TEL_NUMERO


FROM T_CLIENT C, T_TELEPHONE T
WHERE C.CLI_ID = T.CLI_ID
AND TYP_CODE = 'FAX'
CLI_NOM TEL_NUMERO
------- --------------
DUPONT 01-44-28-52-50
MARTIN 01-44-22-56-21
DUHAMEL 01-54-11-43-89
DUPONT 05-59-45-72-42
MARTIN 01-47-66-29-55
DUBOIS 04-66-62-95-64
DREYFUS 04-92-19-18-58
DUHAMEL 01-55-60-93-81
PHILIPPE 01-48-44-86-19
DAUMIER 01-48-28-17-95
...

Le fait de placer comme critère de jointure entre les tables,


l'opérateur logique "égal" donne ce que l'on apelle une
"équi-jointure".

Comme vous pouvez le constater, le nom du client


"BOUVIER" n'apparaît pas. Il n'a pas été "oublié" par le
traitement de la requête, mais le numéro de fax de ce
client n'est pas présent dans la table T_TELEPHONE. Or
le moteur SQL recherche les valeurs de la jointure par
égalité. Comme l'ID_CLI de "BOUVIER" n'est pas présent
dans la table T_TELEPHONE, il ne peut effectuer la
jointure et ignore donc cette ligne. Nous verrons
comment réparer cette lacune lorsque nous parlerons des
jointures externes.
On peut aussi utiliser les surnoms dans la partie qui
suit immédiatement le mot clef SELECT. Ainsi l'exemple
4, peut aussi s'écrire :

Exemple 6 :

SELECT C.CLI_ID, C.CLI_NOM, T.TEL_NUMERO


FROM T_CLIENT C, T_TELEPHONE T
WHERE C.CLI_ID = T.CLI_ID
AND T.TYP_CODE = 'FAX'

CLI_ID CLI_NOM TEL_NUMERO


------ ------- --------------
1 DUPONT 01-44-28-52-50
10 MARTIN 01-44-22-56-21
8 DUHAMEL 01-54-11-43-89
1 DUPONT 05-59-45-72-42
2 MARTIN 01-47-66-29-55
4 DUBOIS 04-66-62-95-64
5 DREYFUS 04-92-19-18-58
8 DUHAMEL 01-55-60-93-81
13 PHILIPPE 01-48-44-86-19
15 DAUMIER 01-48-28-17-95
...

C'est particulièrement pratique lorsque l'on veut récupérer


une colonne qui se retrouve dans les deux tables, ce qui est
souvent le cas de la (ou les) colonne(s) de clef étrangère qui
permet justement d'assurer la jointure.

Pour joindre plusieurs tables, on peut utiliser le même


processus de manière répétitive...

Exemple 7 :

SELECT C.CLI_ID, C.CLI_NOM, T.TEL_NUMERO, E.EML_ADRESSE,


A.ADR_VILLE
FROM T_CLIENT C, T_TELEPHONE T, T_ADRESSE A, T_EMAIL E
WHERE C.CLI_ID = T.CLI_ID
AND C.CLI_ID = A.CLI_ID
AND C.CLI_ID = E.CLI_ID

CLI_ID CLI_NOM TEL_NUMERO EML_ADRESSE ADR_VILLE


------ -------- -------------- ----------------------- ---------
1 DUPONT 01-45-42-56-63 alain.dupont@wanadoo.fr VERSAILLES
1 DUPONT 05-59-45-72-42 alain.dupont@wanadoo.fr VERSAILLES
1 DUPONT 05-59-45-72-24 alain.dupont@wanadoo.fr VERSAILLES
1 DUPONT 01-44-28-52-50 alain.dupont@wanadoo.fr VERSAILLES
1 DUPONT 01-44-28-52-52 alain.dupont@wanadoo.fr VERSAILLES
2 MARTIN 01-47-66-29-29 mmartin@transports_martin_fils.fr VERGNOLLES
CEDEX 452
2 MARTIN 01-47-66-29-55 mmartin@transports_martin_fils.fr VERGNOLLES
CEDEX 452
2 MARTIN 01-47-66-29-29 plongeur@aol.com VERGNOLLES
CEDEX 452
2 MARTIN 01-47-66-29-55 plongeur@aol.com VERGNOLLES
CEDEX 452
5 DREYFUS 04-92-19-18-58 pdreyfus@club-internet.fr PARIS
...

De même que nous l'avons vu dans l'exemple 2.4, ne sont


visible ici que les lignes clients ayant à la fois, au
moins une adresse, un e-mail et au moins un numéro de
téléphone. Si nous avions voulu une liste complète des
clients avec toutes les coordonnées disponibles, nous
aurions du faire une requête externe sur les tables.

II-B. Différents type de jointures (naturelles, équi, non


equi, auto, externes, hétérogènes, croisée et union)
Lorsque nous étudions le modèle relationnel de notre base de
données exemple nous avons vu que le modèle physique des
données, répercute les clefs des tables maîtres en tant que
clefs étrangères des tables pour lesquelles une jointure est
nécessaire. En utilisant la jointure entre clefs primaires et
clefs secondaires basée sur l'égalité des valeurs des colonnes
nous exécutons ce que les professionnels du SQL appelle une
jointure naturelle.
Il est aussi possible de faire des équi-jointures qui ne sont
pas naturelles, soit par accident (une erreur !), soit par
nécessité.
Il est aussi possible de faire des non équi-jointures,
c'est-à-dire des jointures basée sur un critère différent de
l'égalité, mais aussi des auto-jointures, c'est-à-dire de
joindre la table sur elle-même. Le cas le plus délicat à
comprendre est celui des jointures externes, c'est-à-dire
exiger que le résultat comprenne toutes les lignes des tables
(ou d'au moins une des tables de la jointure), même s'il n'y a
pas correspondance des lignes entre les différentes tables
mise en oeuvre dans la jointure.
La jointure d'union consiste à ajouter toutes les données des
deux tables à condition qu'elles soient compatibles dans leurs
structures.
La jointure croisée permet de faire le produit cartésien des
tables.
Enfin on peut effectuer des requêtes hétérogènes, c'est-à-dire
de joindre une table d'une base de données, à une ou plusieurs
autres base de données éventuellement même sur des serveurs
différents, voire même sur des serveurs de différents types
(par exemple joindre une table T_CLIENT de la base BD_COMMANDE
d'un serveur Oracle à la table T_PROSPECT de la base
BD_COMMERCIAL d'un serveur Sybase !).

Dans la mesure du possible, utilisez toujours un


opérateur de jointure normalisé Sql2 (mot clef JOIN).

En effet :
Les jointures faites dans la clause WHERE (ancienne syntaxe
de 1986 !) ne permettent pas de faire la distinction de
prime abord entre ce qui relève du filtrage et ce qui relève
de la jointure.
Il est à priori absurde de vouloir filtrer dans le WHERE (ce
qui restreint les données du résultat) et de vouloir
"élargir" ce résultat par une jointure dans la même clause
WHERE de filtrage.
La lisibilité des requêtes est plus grande en utilisant la
syntaxe à base de JOIN, en isolant ce qui est du filtrage et
de la jointure, mais aussi en isolant avec clarté chaque
condition de jointures entre chaque couples de table.
L'optimisation d'exécution de la requête est souvent plus
pointue du fait de l'utilisation du JOIN.
Lorsque l'on utilise l'ancienne syntaxe et que l'on supprime
la clause WHERE à des fins de tests, le moteur SQL réalise
le produit cartésiens des tables ce qui revient la plupart
du temps à mettre à genoux le serveur !

III. Syntaxe normalisée des jointures

Les jointures normalisées s'expriment à l'aide du mot clef


JOIN dans la clause FROM. Suivant la nature de la jointure, on
devra préciser sur quels critères se base la jointure.

Voici un tableau résumant les différents types de jointures


normalisées :
Jointure interne
SELECT ...
FROM <table gauche>
[INNER]JOIN <table droite>
ON <condition de jointure>
Jointure externe
SELECT ...
FROM <table gauche>
LEFT | RIGHT | FULL OUTER JOIN <table droite>
ON condition de jointure
Jointure naturelle
SELECT ...
FROM <table gauche>
NATURAL JOIN <table droite>
[USING <noms de colonnes>]

Jointure croisée
SELECT ...
FROM <table gauche>
CROSS JOIN <table droite>

Jointure d'union
SELECT ...
FROM <table gauche>
UNION JOIN <table droite>

Nous allons décrire en détail toutes ces jointures.

III-A. Opérateur de jointure naturelle

Il existe un opérateur normalisé pour effectué en SQL la


jointure naturelle des tables :

SELECT [DISTINCT ou ALL] * ou liste de colonnes


FROM table1 NATURAL JOIN table2 [USING (colonne1 [, colonne2 ...])]

L'opérateur NATURAL JOIN permet d'éviter de préciser les


colonnes concernées par la jointure.
Dans ce cas, le compilateur SQL va rechercher dans les 2
tables, les colonnes dont le nom est identique. Bien entendu,
le type de données doit être le même !

NOTA : on veillera au niveau de la modélisation et notamment


au niveau du MPD (Modèle Physique de Données) que les noms des
colonnes de clefs en relation avec d'autres tables par
l'intermédiaires des clefs étrangères soient strictement
identiques.

Exemple 8 :

SELECT CLI_NOM, TEL_NUMERO


FROM T_CLIENT NATURAL JOIN T_TELEPHONE

CLI_NOM TEL_NUMERO
------- --------------
DUPONT 01-45-42-56-63
DUPONT 01-44-28-52-52
DUPONT 01-44-28-52-50
BOUVIER 06-11-86-78-89
DUBOIS 02-41-58-89-52
DREYFUS 01-51-58-52-50
DUHAMEL 01-54-11-43-21
BOUVIER 06-55-41-42-95
MARTIN 01-48-98-92-21
MARTIN 01-44-22-56-21
...

Mais cette syntaxe est rarement acceptée par les moteurs SQL
actuels !
La partie optionnelle USING permet de restreindre les colonnes
concernées, lorsque plusieurs colonnes servent à définir la
jointure naturelle. Ainsi la commande SQL :

SELECT CLI_NOM, TEL_NUMERO


FROM T_CLIENT
NATURAL JOIN T_TELEPHONE
USING (CLI_ID)

Revient au même que la commande SQL de l'exemple 8.

III-B. Les jointures internes

Comme il s'agit de la plus commune des jointures c'est celle


qui s'exerce par défaut si on ne précise pas le type de
jointure. Après le mot clef ON on doit préciser le critère de
jointure.

Reprenons notre exemple de départ :

Exemple 9 :

SELECT CLI_NOM, TEL_NUMERO


FROM T_CLIENT
INNER JOIN T_TELEPHONE
ON T_CLIENT.CLI_ID = T_TELEPHONE.CLI_ID

CLI_NOM TEL_NUMERO
------- --------------
DUPONT 01-45-42-56-63
DUPONT 01-44-28-52-52
DUPONT 01-44-28-52-50
BOUVIER 06-11-86-78-89
DUBOIS 02-41-58-89-52
DREYFUS 01-51-58-52-50
DUHAMEL 01-54-11-43-21
BOUVIER 06-55-41-42-95
MARTIN 01-48-98-92-21
MARTIN 01-44-22-56-21
...

Ou en utilisant le surnommage :

Exemple 10 :

SELECT CLI_NOM, TEL_NUMERO


FROM T_CLIENT C
INNER JOIN T_TELEPHONE T
ON C.CLI_ID = T.CLI_ID

CLI_NOM TEL_NUMERO
------- --------------
DUPONT 01-45-42-56-63
DUPONT 01-44-28-52-52
DUPONT 01-44-28-52-50
BOUVIER 06-11-86-78-89
DUBOIS 02-41-58-89-52
DREYFUS 01-51-58-52-50
DUHAMEL 01-54-11-43-21
BOUVIER 06-55-41-42-95
MARTIN 01-48-98-92-21
MARTIN 01-44-22-56-21
...

Plus pratique à écrire et aussi lisible sinon plus !

NOTA : le mot clef INNER est facultatif. Par défaut l'absence


de précision de la nature de la jointure la fait s'exécuter en
jointure interne.
Ainsi on peut reformuler le requête ci-dessus en :
Exemple 11 :

SELECT CLI_NOM, TEL_NUMERO


FROM T_CLIENT C
JOIN T_TELEPHONE T
ON C.CLI_ID = T.CLI_ID

CLI_NOM TEL_NUMERO
------- --------------
DUPONT 01-45-42-56-63
DUPONT 01-44-28-52-52
DUPONT 01-44-28-52-50
BOUVIER 06-11-86-78-89
DUBOIS 02-41-58-89-52
DREYFUS 01-51-58-52-50
DUHAMEL 01-54-11-43-21
BOYER 06-55-41-42-95
MARTIN 01-48-98-92-21
MARTIN 01-44-22-56-21
...

III-C. Les jointures externes

Les jointures externes sont extrêmement pratiques pour


rapatrier le maximum d'informations disponible, même si des
lignes de table ne sont pas renseignées entre les différentes
tables jointes.

Procédons à l'aide d'un exemple pour mieux comprendre la


différence entre une jointure interne et une jointure externe.
Nous avons vu à l'exemple 9 que seul les clients dotés d'un
numéro de téléphone étaient répertoriés dans la réponse.
Ainsi, le client "BOUVIER" était absent.

Exemple 12 :
SELECT CLI_NOM, TEL_NUMERO
FROM T_CLIENT C
INNER JOIN T_TELEPHONE T
ON C.CLI_ID = T.CLI_ID
WHERE TYP_CODE = 'FAX'

CLI_NOM TEL_NUMERO
------- --------------
DUPONT 01-44-28-52-50
MARTIN 01-44-22-56-21
DUHAMEL 01-54-11-43-89
DUPONT 05-59-45-72-42
MARTIN 01-47-66-29-55
DUBOIS 04-66-62-95-64
DREYFUS 04-92-19-18-58
DUHAMEL 01-55-60-93-81
PHILIPPE 01-48-44-86-19
DAUMIER 01-48-28-17-95
...

Que faut-il modifier dans la requête pour obtenir une ligne


"BOUVIER" avec aucune référence de téléphone associée dans la
réponse ?
Il suffit en fait d'opérer à l'aide d'une jointure externe :

Exemple 13 :

SELECT CLI_NOM, TEL_NUMERO


FROM T_CLIENT C
LEFT OUTER JOIN T_TELEPHONE T
ON C.CLI_ID = T.CLI_ID
WHERE TYP_CODE = 'FAX' OR TYP_CODE IS NULL

CLI_NOM TEL_NUMERO
------- --------------
DUPONT 01-44-28-52-50
DUPONT 05-59-45-72-42
MARTIN 01-47-66-29-55
BOUVIER NULL
DUBOIS 04-66-62-95-64
DREYFUS 04-92-19-18-58
FAURE NULL
LACOMBE NULL
DUHAMEL 01-54-11-43-89
DUHAMEL 01-55-60-93-81
...

ou encore :

SELECT CLI_NOM, TEL_NUMERO


FROM T_CLIENT C
LEFT OUTER JOIN T_TELEPHONE T
ON C.CLI_ID = T.CLI_ID AND TYP_CODE IS NULL

La syntaxe de la jointure externe est la suivante :

SELECT ...
FROM <table gauche>
LEFT | RIGHT | FULL OUTER JOIN <table droite 1>
ON <condition de jointure>
[LEFT | RIGHT | FULL OUTER JOIN <table droite 2>
ON <condition de jointure 2>]
...

Les mots clefs LEFT, RIGHT et FULL indiquent la manière dont


le moteur de requête doit effectuer la jointure externe. Il
font référence à la table située à gauche (LEFT) du mot clef
JOIN ou à la table située à droite (RIGHT) de ce même mot
clef. Le mot FULL indique que la jointure externe est
bilatérale.

SELECT colonnes
FROM TGauche LEFT OUTER JOIN TDroite ON condition de jointure
On recherche toutes les valeurs satisfaisant la
condition de jointure précisée dans prédicat, puis on
rajoute toutes les lignes de la table TGauche qui n'ont
pas été prises en compte au titre de la satisfaction du
critère.

SELECT colonnes
FROM TGauche RIGHT OUTER JOIN TDroite ON condition de jointure

On recherche toutes les valeurs satisfaisant la


condition de jointure précisée dans prédicat, puis on
rajoute toutes les lignes de la table TDroite qui n'ont
pas été prises en compte au titre de la satisfaction du
critère.

SELECT colonnes
FROM TGauche FULL OUTER JOIN TDroite ON condition de jointure

On recherche toutes les valeurs satisfaisant la


condition de jointure précisée dans prédicat, puis on
rajoute toutes les lignes de la table TGauche et TDroite
qui n'ont pas été prises en compte au titre de la
satisfaction du critère.

NOTA : il existe des équivalences entre différentes


expressions logiques à base de jointures externes. Les
principales sont :

la jointure externe droite peut être obtenue par une


jointure externe gauche dans laquelle on inverse l'ordre des
tables.
la jointure externe bilatérale peut être obtenue par la
combinaison de deux jointures externes unilatérales avec
l'opérateur ensemblistes UNION.
Remplacement d'une jointure externe droite par une jointure
externe gauche.
L'équivalent logique de :
Exemple 14 :

SELECT CLI_NOM, TEL_NUMERO


FROM T_CLIENT C
RIGHT OUTER JOIN T_TELEPHONE T
ON C.CLI_ID = T.CLI_ID
WHERE TYP_CODE = 'FAX'

est :

SELECT CLI_NOM, TEL_NUMERO


FROM T_TELEPHONE T
LEFT OUTER JOIN T_CLIENT C
ON C.CLI_ID = T.CLI_ID
WHERE TYP_CODE = 'FAX'

Remplacement d'un FULL OUTER JOIN avec jointures externes


gauche et droite :
L'équivalent logique de ...

Exemple 15 :

SELECT CLI_NOM, TEL_NUMERO


FROM T_CLIENT C
FULL OUTER JOIN T_TELEPHONE T
ON C.CLI_ID = T.CLI_ID
WHERE TYP_CODE = 'FAX'

est :

SELECT CLI_NOM, TEL_NUMERO


FROM T_CLIENT C
LEFT OUTER JOIN T_TELEPHONE T
ON C.CLI_ID = T.CLI_ID
WHERE TYP_CODE = 'FAX'
UNION
SELECT CLI_NOM, TEL_NUMERO
FROM T_CLIENT C
RIGTH OUTER JOIN T_TELEPHONE T
ON C.CLI_ID = T.CLI_ID
WHERE TYP_CODE = 'FAX'

Remplacement d'un FULL OUTER JOIN avec jointures externes


gauche uniquement :
L'équivalent logique de ...

Exemple 16 :

SELECT CLI_NOM, TEL_NUMERO


FROM T_CLIENT C
FULL OUTER JOIN T_TELEPHONE T
ON C.CLI_ID = T.CLI_ID
WHERE TYP_CODE = 'FAX'

est :

SELECT CLI_NOM, TEL_NUMERO


FROM T_CLIENT C
LEFT OUTER JOIN T_TELEPHONE T
ON C.CLI_ID = T.CLI_ID
WHERE TYP_CODE = 'FAX'
UNION
SELECT CLI_NOM, TEL_NUMERO
FROM T_TELEPHONE T
LEFT OUTER JOIN T_CLIENT C
ON C.CLI_ID = T.CLI_ID
WHERE TYP_CODE = 'FAX'

III-D. Différence entre jointure externe et jointure interne


Pour bien comprendre la distinction entre les jointures
internes et externes, nous devons consacrer quelques instants
à aborder des problèmes de logique ensembliste sous un oeil
pragmatique.

III-D-1. L'hypothèse du monde clos


Les jointures externes sont extrêmement pratiques pour
rapatrier le maximum d'informations disponible, même si des
lignes de table ne sont pas renseignées entre les différentes
tables jointes.

Sans le savoir, nous faisons assez systématiquement


l'hypothèse du monde clos. c'est-à-dire que nous considérons
que l'absence d'information, n'est pas une information. Si
vous demandez à une secrétaire de vous communiquer les
coordonnées des clients qui sont domiciliés à Paris, elle vous
donnera une liste où figurera autant de fois le nom "Paris"
qu'il y a de clients dans la liste, et ceci paraît bien normal
! Sauf que, comme l'aurait fait tout un chacun, votre
secrétaire a fait l'hypothèse du monde clos sans le savoir en
présumant que les clients pour lesquels l'adresse n'est pas
renseignée ne sont pas domiciliés à PARIS !
C'est cela l'hypothèse du monde clos : considérer que
l'absence d'information doit être synonyme de critère de
discrimination... La jointure externe permet de contrer
l'hypothèse du monde clos en considérant qu'en cas d'absence
de jointure entre une table et l'autre, on ne supprime par
pour autant l'information.

III-D-2. Mécanisme en jeu


Lorsqu'une ligne d'une table figurant dans une jointure n'a
pas de correspondance dans les autres tables, le critère
d'équi-jointure n'est pas satisfait et la ligne est rejetée.
C'est la jointure interne. Au contraire, la jointure externe
permet de faire figurer dans le résultat les lignes
satisfaisant la condition d'équi-jointure ainsi que celles
n'ayant pas de correspondances.

Ainsi, si je veux contacter tous mes clients, quelque soit le


mode de contact que je veux utiliser dans le cadre d'une
campagne publicitaire, j'ai intérêt à obtenir une réponse
contenant tous les clients, même ceux qui n'ont pas de
téléphone, d'e-mail ou d'adresse.

Exemple 17 :
SELECT CLI_ID, CLI_NOM, TYP_CODE || ' : ' || TEL_NUMERO AS
TEL_CONTACT, EML_ADRESSE, ADR_VILLE
FROM T_CLIENT C
LEFT OUTER JOIN T_TELEPHONE T
ON C.CLI_ID = T.CLI_ID
LEFT OUTER JOIN T_EMAIL E
ON C.CLI_ID = E.CLI_ID
LEFT OUTER JOIN T_ADRESSE A
ON C.CLI_ID = A.CLI_ID

CLI_ID CLI_NOM TEL_CONTACT EML_ADRESSE


ADR_VILLE
------ -------- -------------- ----------------------- ---------
1 DUPONT 01-45-42-56-63 alain.dupont@wanadoo.fr VERSAILLES
1 DUPONT 05-59-45-72-42 alain.dupont@wanadoo.fr VERSAILLES
1 DUPONT 05-59-45-72-24 alain.dupont@wanadoo.fr VERSAILLES
1 DUPONT 01-44-28-52-50 alain.dupont@wanadoo.fr VERSAILLES
1 DUPONT 01-44-28-52-52 alain.dupont@wanadoo.fr VERSAILLES
2 MARTIN 01-47-66-29-29 mmartin@transports_martin_fils.fr
VERGNOLLES CEDEX 452
2 MARTIN 01-47-66-29-55 mmartin@transports_martin_fils.fr
VERGNOLLES CEDEX 452
2 MARTIN 01-47-66-29-29 plongeur@aol.com VERGNOLLES
CEDEX 452
2 MARTIN 01-47-66-29-55 plongeur@aol.com VERGNOLLES
CEDEX 452
3 BOUVIER GSM : 06-11-86-78-89 NULL MONTMAIZIN
...

NOTA : Sur certains moteurs SQL la jointure bilatérale externe


(FULL OUTER JOIN) s'exprime :

SELECT colonnes
FROM TGauche FULL JOIN TDroite ON condition de jointure

D'anciennes syntaxes permettent de faire des jointures


externes unilatérale. Par exemple, il n'est pas rare de
rencontrer les syntaxes suivantes :
SELECT colonnes
FROM table_1 t1, table_2 t2
WHERE t1.id1 *= t2.id2

ou encore :

SELECT colonnes
FROM table_1 t1, table_2 t2
WHERE t1.id1 (+)= t2.id2

Elles sont bien évidemment à proscrire si la syntaxe SQL 2 est


disponible !

III-D-3. Discussion sur la jointure externe


La jointure externe est rarement bien comprise du premier
coup. Si je vous propose de lire cette discussion qui a eût
lieu sur un forum Internet, c'est parce quelle permet de mieux
la comprendre.

Objet : Optimisation Jointure

Je me pose une petite question de syntaxe SQL sur les jointures.


Et comme Frédéric va surement me répondre, je prends un exemple de son site.
Y-a t-il une syntaxe meilleure que l'autre (si oui pourquoi) ?

SELECT CLI_NOM, TEL_NUMERO


FROM T_CLIENT C
INNER JOIN T_TELEPHONE T
ON C.CLI_ID = T.CLI_ID
WHERE TYP_CODE = 'FAX'

SELECT CLI_NOM, TEL_NUMERO


FROM T_CLIENT C
INNER JOIN T_TELEPHONE T
ON (C.CLI_ID = T.CLI_ID AND T.TYP_CODE = 'FAX' )

Au niveau performance, il n'y a aucune différence, l'optimiseur de


SQL/Server construit le même plan
Par contre, au niveau conceptuel il est interessant de différencier ce qui
appartient à la jointure et ce qui appartient à la limitation du jeu de
résultat
Cela facilite la maintenance et la lecture du source
[...]

Pour comprende la différence entre le prédicat de la clause de jointure


et le prédicat du filtre WHERE, je vous propose le petit test suivant :

-- test de jointure

CREATE TABLE TEST_JOIN1


(COL1 INT,
COL2 CHAR(2))

CREATE TABLE TEST_JOIN2


(COL1 INT,
COL2 CHAR(2))

INSERT INTO TEST_JOIN1 VALUES (101, 'AA')


INSERT INTO TEST_JOIN1 VALUES (102, 'AA')
INSERT INTO TEST_JOIN1 VALUES (103, 'BB')

INSERT INTO TEST_JOIN2 VALUES (101, 'AA')


INSERT INTO TEST_JOIN2 VALUES (102, 'AA')
INSERT INTO TEST_JOIN2 VALUES (201, 'BB')

-- équivalente ??? En apparence seulement !

SELECT TJ1.COL1, TJ1.COL2


FROM TEST_JOIN1 TJ1
JOIN TEST_JOIN2 TJ2
ON TJ1.COL1 = TJ2.COL1
WHERE TJ1.COL2 = 'AA'

SELECT TJ1.COL1, TJ1.COL2


FROM TEST_JOIN1 TJ1
JOIN TEST_JOIN2 TJ2
ON TJ1.COL1 = TJ2.COL1 AND TJ1.COL2 = 'AA'

-- d'accord... même résultat !!!

--changeons le lien interne en lien externe...


SELECT TJ1.COL1, TJ1.COL2
FROM TEST_JOIN1 TJ1
LEFT OUTER JOIN TEST_JOIN2 TJ2
ON TJ1.COL1 = TJ2.COL1
WHERE TJ1.COL2 = 'AA'

SELECT TJ1.COL1, TJ1.COL2


FROM TEST_JOIN1 TJ1
LEFT OUTER JOIN TEST_JOIN2 TJ2
ON TJ1.COL1 = TJ2.COL1 AND TJ1.COL2 = 'AA'

-- pas d'acord !!! résultat différend...

Tu peux alors m'expliquer pourquoi cette requête renvoie le même résultat que ta
dernière ?

SELECT TJ1.COL1, TJ1.COL2


FROM TEST_JOIN1 TJ1
LEFT OUTER JOIN TEST_JOIN2 TJ2
ON TJ1.COL1 = TJ2.COL1 AND TJ1.COL2 = 'taratata' and 1=0

La clause WHERE est un filtre d'élimination. Il suppose que les


différentes tables sont déjà jointes en une seule et parcoure
l'ensemble des résultats à la recherche des lignes correspondant aux
critères donnés.

En revanche la clause JOIN se comporte différemment. Elle agit AVANT


que la jointure soit effective.

Dans le cadre d'une jointure INTERNE le comportement est similaire à


celui d'une clause WHERE.

Dans le cas d'une jointure externe il faut décomposer la logique en


plusieurs étapes :
1) évaluation des éléments du prédicat
2) application de la jointure : lignes jointe en 1)
+ lignes sans correspondance

Reprennons la clause de jointure de la requête donnée par Laurent :


TJ1.COL1 = TJ2.COL1 AND TJ1.COL2 = 'taratata' and 1=0
prédicat de jointure prédicats hors jointure

1° 1=0 => aucune ligne n'est récupérée


donc aucune ligne de TEST_JOIN1 n'a de correspondance avec TEST_JOIN2
conclusion : toutes les lignes de TEST_JOIN1 seront reprises du fait
de la jointure externe

2° TJ1.COL2 = 'taratata' => encore une fois aucune ligne n'est récupérée
donc aucune ligne de TEST_JOIN1 n'a de correspondance avec TEST_JOIN2
conclusion : toutes les lignes de TEST_JOIN1 seront reprises du fait
de la jointure externe

3° TJ1.COL1 = TJ2.COL1 => 2 lignes sont en correspondance avec TEST_JOIN2


conclusion, ces deux lignes seront récupérées du fait de la jointure
externe et la 3eme ligne aussi puisqu'elle n'a aucune correspondance

???

Pour comprendre la différence, il suffit de demander à voir les colonnes


de la table TEST_JOIN2 dans les différents cas :

SELECT TJ1.COL1, TJ1.COL2, TJ2.COL1, TJ2.COL2


FROM TEST_JOIN1 TJ1
LEFT OUTER JOIN TEST_JOIN2 TJ2
ON TJ1.COL1 = TJ2.COL1 AND TJ1.COL2 = 'AA'

COL1 COL2 COL1 COL2


----------- ---- ----------- ----
101 AA 101 AA
102 AA 102 AA
103 BB NULL NULL

SELECT TJ1.COL1, TJ1.COL2, TJ2.COL1, TJ2.COL2


FROM TEST_JOIN1 TJ1
LEFT OUTER JOIN TEST_JOIN2 TJ2
ON TJ1.COL1 = TJ2.COL1
WHERE TJ1.COL2 = 'AA'

COL1 COL2 COL1 COL2


----------- ---- ----------- ----
101 AA 101 AA
102 AA 102 AA

SELECT TJ1.COL1, TJ1.COL2, TJ2.COL1, TJ2.COL2


FROM TEST_JOIN1 TJ1
LEFT OUTER JOIN TEST_JOIN2 TJ2
ON TJ1.COL1 = TJ2.COL1 AND TJ1.COL2 = 'taratata' and 1=0

COL1 COL2 COL1 COL2


----------- ---- ----------- ----
101 AA NULL NULL
102 AA NULL NULL
103 BB NULL NULL

et voilà !

III-E. La jointure croisée

La jointure croisée n'est autre que le produit cartésien de


deux tables. Rappelons que le produit cartésien de deux
ensembles n'est autre que la multiplication généralisée. Dans
le cas des tables, c'est le fait d'associer à chacune des
lignes de la première table, toutes les lignes de la seconde.
Ainsi, si la première table compte 267 lignes et la seconde
1214, on se retrouve avec un résultat contenant 324 138
lignes. Bien entendu, s'il n'y a aucun doublon dans les
tables, toutes les lignes du résultat seront aussi uniques.

La jointure croisée peut s'écrire de deux manières différentes


:

à l'aide de l'opérateur normalisé :

SELECT colonnes
FROM table_1 CROSS JOIN table_2

ou à l'aide d'une clause FROM simplifiée :

SELECT colonnes
FROM table_1, table_2

Ce qui est certainement l'expression la plus minimaliste de


tous les ordres SELECT du SQL !
Nous voudrions savoir si notre table des tarifs de chambre
(TJ_TRF_CHB) est complète, c'est-à-dire si l'on a bien toutes
les chambres (T_CHAMBRE) pour toutes les dates de début de
tarif (T_TARIF) :

Exemple 18 :

SELECT CHB_ID, TRF_DATE_DEBUT, 0 AS TRF_CHB_PRIX


FROM T_TARIF, T_CHAMBRE
ORDER BY CHB_ID, TRF_DATE_DEBUT

CHB_ID TRF_DATE_DEBUT TRF_CHB_PRIX


------ -------------- ------------
1 1999-01-01 0,00
1 1999-09-01 0,00
1 2000-01-01 0,00
1 2000-09-01 0,00
1 2001-01-01 0,00
2 1999-01-01 0,00
2 1999-09-01 0,00
2 2000-01-01 0,00
2 2000-09-01 0,00
2 2001-01-01 0,00
...

En comparant rapidement le contenu des deux tables on peut


s'assurer que tous les tarifs ont bien été renseignés.

Avec la syntaxe normalisée, cette requête s'écrit :

Exemple 19 :

SELECT CHB_ID, TRF_DATE_DEBUT, 0 AS TRF_CHB_PRIX


FROM T_TARIF CROSS JOIN T_CHAMBRE
ORDER BY CHB_ID, TRF_DATE_DEBUT

CHB_ID TRF_DATE_DEBUT TRF_CHB_PRIX


------ -------------- ------------
1 1999-01-01 0,00
1 1999-09-01 0,00
1 2000-01-01 0,00
1 2000-09-01 0,00
1 2001-01-01 0,00
2 1999-01-01 0,00
2 1999-09-01 0,00
2 2000-01-01 0,00
2 2000-09-01 0,00
2 2001-01-01 0,00
...

et donne le même résultat !

III-F. La jointure d'union

La jointure d'union, permet de faire l'union de deux tables de


structures quelconque. Elle s'exprime qu'à l'aide de
l'opérateur normalisé SQL2 suivant :

SELECT colonnes
FROM table_1 UNION JOIN table_2

Effectuons par exemple la jointure d'union entre les tables


T_TITRE ("M.", "Mme.", "Melle." ) et T_TYPE ("FAX", "GSM",
"TEL") :

Exemple 20 :

SELECT *
FROM T_TITRE UNION JOIN T_TYPE
TIT_CODE TIT_LIBELLE TYP_CODE TYP_LIBELLE
-------- -------------------------------- -------- -------------------
M. Monsieur NULL NULL
Melle. Mademoiselle NULL NULL
Mme. Madame NULL NULL
NULL NULL FAX Télécopie
NULL NULL GSM Téléphone portable
NULL NULL TEL Téléphone fixe

En fait c'est comme si l'on avait listé la première table,


puis la seconde en évitant toute colonne commune et compléter
les espaces vides des valeurs NULL.

Mais cette jointure est très rarement implantée dans les


moteurs SQL.

NOTA : si l'opérateur UNION JOIN n'est pas présent dans votre


moteur, vous pouvez le fabriquer comme suit, car elle
(l'union) peut être facilement remplacée par un autre ordre
SQL presque aussi simple :

SELECT *
FROM <table gauche>
FULL OUTER JOIN <table droite>
ON <critère>

Où la condition <critère> est n'importe quel prédicat valant


toujours faux comme "1=2".

En dehors du fait de linéariser des tables hétérogènes, il est


franchement permis de douter de l'utilité d'un tel opérateur.

IV. Nature des conditions de jointures


Nous allons maintenant analyser les différentes jointures
basées sur la nature des conditions pour bien les distinguer.

IV-A. Équi-jointure
L'équi-jointure consiste à opérer une jointure avec une
condition d'égalité. Cette condition d'égalité dans la
jointure peut ne pas porter nécessairement sur les clefs
(primaires et étrangères).
Recherchons par exemple les clients dont le nom est celui
d'une ville contenu dans la table des adresses :

Exemple 21 :

SELECT DISTINCT C.CLI_ID, C.CLI_NOM, A.ADR_VILLE


FROM T_CLIENT C
JOIN T_ADRESSE A
ON C.CLI_NOM = A.ADR_VILLE

CLI_ID CLI_NOM ADR_VILLE


------ ------- ---------
92 PARIS PARIS
...

Nous avons donc bien réalisé une équi-jointure, mais elle


n'est pas naturelle parce qu'elle ne repose pas sur les clefs
des tables.

Bien entendu, il existe un opérateur normalisé SQL 2


permettant de traiter le cas de l'équi-jointure :

SELECT [DISTINCT ou ALL] * ou liste de colonnes


FROM table1 [INNER] JOIN table2 ON condition de jointure

Le mot clef INNER n'étant pas obligatoire, mais voulant


s'opposer aux mot clefs OUTER, UNION et CROSS.
Ainsi, la requête précédente, s'écrit à l'aide de cette
syntaxe :

Exemple 22 :

SELECT DISTINCT C.CLI_ID, C.CLI_NOM, A.ADR_VILLE


FROM T_CLIENT C
INNER JOIN T_ADRESSE A
ON C.CLI_NOM = A.ADR_VILLE

CLI_ID CLI_NOM ADR_VILLE


------ ------- ---------
92 PARIS PARIS
...

IV-B. Non équi-jointure


Il s'agit là d'utiliser n'importe quelle condition de jointure
entre deux tables, exceptée la stricte égalité. Ce peuvent
être les conditions suivantes :

> supérieur
>= supérieur ou égal
< inférieur
<= inférieur ou égal
<> différent de
IN dans un ensemble
LIKE correspondance partielle
BETWEEN ... AND ... entre deux valeurs
EXISTS dans une table

En règle générale ou trouve des non équi-jointures dans le


cadre de comparaisons temporelles ou de mesures physiques. Par
exemple on pourrait rechercher une pièce mécanique dans un
stock qui soit de même nature ou de même fonction qu'une pièce
donnée, mais plus légère.
Nous voulons obtenir les factures qui ont été émises avant que
le prix des petits déjeuners n'atteigne 6 €.

Exemple 23 :

SELECT F.*
FROM T_FACTURE F
INNER JOIN T_TARIF T
ON F.FAC_DATE < T.TRF_DATE_DEBUT
WHERE TRF_PETIT_DEJEUNE >= 6

FAC_ID CLI_ID PMT_CODE FAC_DATE FAC_PMT_DATE


------ ------ -------- ---------- ------------
1 1 CB 1999-01-31 1999-02-14
3 1 CHQ 1999-02-28 1999-03-12
5 1 CB 1999-03-31 1999-04-23
7 1 CHQ 1999-04-30 1999-05-14
9 1 CHQ 1999-05-31 1999-06-14
11 1 CB 1999-06-30 1999-07-14
13 1 CHQ 1999-07-31 1999-08-12
15 1 CB 1999-08-31 1999-09-23
25 2 CB 1999-01-31 1999-02-23
27 2 CHQ 1999-02-28 1999-03-18
...

NOTA : pour récupérer toutes les colonnes d'une table, on peut


utiliser l'opérateur * suffixé par le nom de table, comme nous
l'avons fait ici pour la table des factures.

Si notre table des tarifs avait été organisée par tranches,


comme ceci :

TRF_DATE_DEBUT TRF_DATE_FIN TRF_TAUX_TAXES TRF_PETIT_DEJEUNE


-------------- ------------ -------------- -----------------
1999-01-01 1999-08-31 18,60 6,00 E
1999-09-01 1999-12-31 20,60 7,00 E
2000-01-01 2000-08-31 20,60 8,00 E
2000-09-01 2000-12-31 20,60 9,00 E
2001-01-01 2001-12-31 20,60 10,00 E

alors,récupérer le tarif des chambres pour chacune des dates


du planning devient un exercice très simple :

Exemple 24 :

SELECT CPC.CHB_ID, CPC.PLN_JOUR, TC.TRF_CHB_PRIX


FROM TJ_CHB_PLN_CLI CPC
INNER JOIN T_TARIF T
ON CPC.PLN_JOUR BETWEEN T.TRF_DATE_DEBUT AND
T.TRF_DATE_FIN
INNER JOIN TJ_TRF_CHB TC
ON T.TRF_DATE_DEBUT = TC.TRF_DATE_DEBUT
AND CPC.CHB_ID = TC.CHB_ID

CHB_ID PLN_JOUR TRF_CHB_PRIX


------ ---------- ------------
1 2000-01-22 40,00 E
1 2000-01-28 40,00 E
1 1999-01-02 33,50 E
1 1999-01-03 33,50 E
1 1999-01-05 33,50 E
1 1999-01-07 33,50 E
1 1999-01-09 33,50 E
1 1999-01-10 33,50 E
1 1999-01-11 33,50 E
1 1999-01-13 33,50 E
...

Nous avons donc à nouveau un exemple remarquable de non


equi-jointure.

Constatons que la colonne TRF_DATE_FIN de cette nouvelle


version de la table des tarifs implique une redondance
de l'information. En effet, cette date de fin est
déductible de la date de début de la ligne qui contient
la date immédiatement postérieure avec un jour de moins.
De plus le problème induit par cette organisation des
données fait qu'il faut obligatoirement définir une date
de fin des tarifs, même dans le futur, sinon certaines
tarifications ne pourront être établies par cette
requête.
Il ne s'agit donc pas d'une modélisation correcte !

IV-C. Auto-jointure
Le problème consiste à joindre une table à elle-même. Il est
assez fréquent que l'on ait besoin de telles auto-jointures
car elle permettent notamment de modéliser des structures de
données complexes comme des arbres. Voici quelques exemples de
relation nécessitant une auto-jointure de tables :

dans une table des employées, connaître le supérieur


hiérarchique de tout employé
dans une table de nomenclature savoir quels sont les
composants nécessaires à la réalisation d'un module, ou les
modules nécessaires à la réalisation d'un appareil
dans une table de personnes, retrouver l'autre moitié d'un
couple marié.
La représentation d'une telle jointure dans le modèle de
données, se fait par le rajout d'une colonne contenant une
pseudo clef étrangère basée sur le clef de la table.
Dans ce cas, une syntaxe possible pour l'auto-jointure est la
suvante :

SELECT [DISTINCT ou ALL] * ou liste de colonnes


FROM laTable t1
INNER JOIN laTable t2
ON t1.laClef = t2.laPseudoClefEtrangère

C'est l'exemple typique ou l'utilisation de surnoms pour les


tables est obligatoire, sinon il y a risque de confusion pour
le moteur SQL.

Pour donner un exemple concret à nos propos nous allons


modéliser le fait qu'une chambre puisse communiquer avec une
autre (par une porte). Dès lors, le challenge est de trouver
quelles sont les chambres qui communiquent entre elles par
exemple pour réaliser une sorte de suite. Pour ce faire, nous
allons ajouter à notre table des chambres une colonne de clef
étrangère basée sur la clef de la table.
Dans ce cas, cette colonne doit obligatoirement accepter des
valeurs nulles !

Voici l'ordre SQL pour rajouter la colonne CHB_COMMUNIQUE dans


la table T_CHAMBRE :

ALTER TABLE T_CHAMBRE ADD CHB_COMMUNIQUE INTEGER

Alimentons là de quelques valeurs exemples en considérant que


la 7 communique avec la 9 et la 12 avec la 14 :

UPDATE T_CHAMBRE SET CHB_COMMUNIQUE = 9 WHERE CHB_ID = 7


UPDATE T_CHAMBRE SET CHB_COMMUNIQUE = 7 WHERE CHB_ID = 9
UPDATE T_CHAMBRE SET CHB_COMMUNIQUE = 12 WHERE CHB_ID = 14
UPDATE T_CHAMBRE SET CHB_COMMUNIQUE = 14 WHERE CHB_ID = 12

Pour formuler la recherche de chambres communiquantes, il


suffit de faire la requête suivante :

Exemple 25 :

SELECT DISTINCT c1.CHB_ID, c1.CHB_COMMUNIQUE


FROM T_CHAMBRE c1
INNER JOIN T_CHAMBRE c2
ON c1.CHB_ID = c2.CHB_COMMUNIQUE

CHB_ID CHB_COMMUNIQUE
------ --------------
7 9
9 7
12 14
14 12
où la table T_CHAMBRE figure deux fois par l'entremise de deux
surnommages différents c1 et c2.

Notons que cette présentation n'est pas pratique car elle


dédouble le nombre de couples de chambres communiquantes,
donnant l'impression qu'il y a 4 couples de chambres
communiquantes ce qui est faux. Il manque juste un petit
quelque chose pour que cette requête soit parfaite. Il suffit
en effet de ne retenir les solutions que pour des identifiants
inférieurs ou supérieur d'une table par rapport à l'autre :

Exemple 26 :

SELECT DISTINCT c1.CHB_ID, c1.CHB_COMMUNIQUE


FROM T_CHAMBRE c1
INNER JOIN T_CHAMBRE c2
ON c1.CHB_ID = c2.CHB_COMMUNIQUE
AND c1.CHB_ID <= c2.CHB_ID

CHB_ID CHB_COMMUNIQUE
------ --------------
7 9
12 14

IV-D. La jointure hétérogène


Une jointure hétérogène consiste à joindre dans une même
requête, des tables provenant de bases de données différentes,
voire de serveurs de données différents. Un tel type de
jointure n'est possible que :

entre deux bases d'un même serveur : que si le SQL du


serveur le permet (par exemple deux bases Oracle, deux bases
SQL Server, etc.)
entre deux bases de deux serveurs différents : qu'en passant
par un outil tiers de type "middleware" (par exemple une
entre une base Oracle et une base Sybase). L'un des rares
middleware a faire ce type de jointure est le BDE (Borland
Database Engine) d'Inprise.
Dans ces deux cas, il faut bien vérifier la compatibilité des
types car il se peut que des résultats surprenants
apparaissent notamment dans le traitement des nombre réels.

Pour effectuer de telles requêtes, la syntaxe est toujours


spécifique au moteur employé. Avec le BDE le niveau de SQL est
SQL 2, cantonné aux fonctions de base.

Voici un exemple de requête entre deux serveur à l'aide du BDE


:

Exemple 27 :

SELECT C.CLI_NOM, T.TEL_NUMERO


FROM ":ORACLE_CLIBD:T_CLIENT" C, ":SYBASE_TELBD:T_TELEPHONE" T
WHERE C.CLI_ID = T.CLI_ID

NOM_CLI TEL_NUMERO
------- --------------
DUPONT 01-45-42-56-63
DUPONT 01-44-28-52-52
DUPONT 01-44-28-52-50
BOUVIER 06-11-86-78-89
DUBOIS 02-41-58-89-52
DREYFUS 01-51-58-52-50
DUHAMEL 01-54-11-43-21
BOYER 06-55-41-42-95
MARTIN 01-48-98-92-21
MARTIN 01-44-22-56-21
...

En fait le BDE se sert d'alias permettant de définir à la fois


le serveur et la base concerné. Ici les alias sont :
":ORACLE_CLIBD:" et ":SYBASE_TELBD:".

V. Récapitulatif des jointures normalisées

V-A. Terminologie et syntaxe des jointures

Jointure naturelle : la jointure s'effectue sur les colonnes


communes, c'est-à-dire celles de même nom et type :

SELECT colonnes
FROM table1 NATURAL JOIN table2 [USING col1, col2 ... ]
[WHERE prédicat] ...

Le mot clef USING permet de restreindre les colonnes


communes à prendre en considération.
Jointure interne : la jointure s'effectue entre les tables
sur les colonnes précisées dans la condition de jointure :

SELECT colonnes
FROM table1 t1 [INNER ] JOIN table2 t2 ON condition
[WHERE prédicat] ...

Jointure externe : la jointure permet de récupérer les


lignes des tables correspondant au critère de jointure, mais
aussi celle pour lesquelles il n'existe pas de
correspondances.

SELECT colonnes
FROM table1 t1 [RIGHT OUTER | LEFT OUTER | FULL OUTER ] JOIN table2 t2 ON
condition
[WHERE prédicat] ...

RIGHT OUTER : la table à droite de l'expression clef


"RIGHT OUTER" renvoie des lignes sans correspondance avec
la table à gauche.
LEFT OUTER : la table à gauche de l'expression clef "LEFT
OUTER" renvoie des lignes sans correspondance avec la
table à droite.
FULL OUTER : les deux tables renvoient des lignes sans
correspondance entre elles.
Jointure croisée : la jointure effectue le produit cartésien
(la "multiplication") des deux tables. Il n'y a pas de
condition.

SELECT colonnes
FROM table1 t1 CROSS JOIN table2 t2
[WHERE prédicat] ...

Jointure d'union : la jointure concatène les tables sans


aucune correspondances de colonnes.

SELECT colonnes
FROM table1 UNION JOIN table2

Il n'y a pas de critère de jointure.


Si votre SGBDR n'implémente pas la jointure externe
droite, inversez l'ordre des tables et faire une
jointure externe gauche lorsque cela est possible.

V-B. Arbre de jointure


La jointure de multiple tables peut se représenter sous la
forme d'un arbre. Cet arbre possède donc une racine, c'est la
table principale, celle d'où l'on veut que l'information
parte. Elle possède aussi des feuilles, c'est-à-dire des
tables d'entités. Les tables situées entre la racine et les
feuilles, sont souvent des tables de jointure, possédant en
principe deux clef étrangères. Dans le principe toute table de
jointure devrait être un noeud de l'arbre.
La représentation arborescente d'une jointure est un excellent
moyen pour visualiser si la clause de jointure de votre
requête est à priori correcte. En effet, une référence
circulaire dans la clause de jointure ne peut pas être
représentée sous la forme d'un arbre et il y a fort à parier
que la requête soit incorrecte.

Voici par exemple la requête qui "met à plat", la base hotel :

SELECT *
FROM T_CLIENT CLI -- le client (racine de l'arbre)
JOIN T_ADRESSE ADR -- adresse, table d'entité (feuille de l'arbre)
ON CLI.CLI_ID = ADR.CLI_ID
JOIN T_TITRE TIT -- titre, table d'entité (feuille de l'arbre)
ON CLI.TIT_CODE = TIT.TIT_CODE
JOIN T_EMAIL EML -- mail, table d'entité (feuille de l'arbre)
ON CLI.CLI_ID = EML.CLI_ID
JOIN T_TELEPHONE TEL -- téléphone, table d'entité servant de jointure
(noeud dans l'arbre)
ON CLI.CLI_ID = TEL.CLI_ID
JOIN T_TYPE TYP -- type de téléphone, table d'entité (feuille de l'arbre)
ON TEL.TYP_CODE = TYP.TYP_CODE
JOIN TJ_CHB_PLN_CLI CPC -- table de jointure (noeud dans l'arbre)
ON CLI.CLI_ID = CPC.CLI_ID
JOIN T_PLANNING PLN -- date du planning, table d'entité (feuille de
l'arbre)
ON CPC.PLN_JOUR = PLN.PLN_JOUR
JOIN T_CHAMBRE CHB -- chambre, table d'entité servant de jointure
(noeud dans l'arbre)
ON CPC.CHB_ID = CHB.CHB_ID
JOIN TJ_TRF_CHB TC -- table de jointure (noeud dans l'arbre)
ON CHB.CHB_ID = TC.CHB_ID
JOIN T_TARIF TRF -- tarif, table d'entité (feuille de l'arbre)
ON TC.TRF_DATE_DEBUT = TRF.TRF_DATE_DEBUT
JOIN T_FACTURE FAC -- facture, table d'entité servant de jointure
ON CLI.CLI_ID = FAC.CLI_ID
JOIN T_LIGNE_FACTURE LIF -- ligne de facture, table d'entité (feuille de
l'arbre)
ON FAC.FAC_ID = LIF.FAC_ID
JOIN T_MODE_PAIEMENT PMT -- mode de paiement, table d'entité (feuille
de l'arbre)
ON FAC.PMT_CODE = PMT.PMT_CODE

Correctement indenté on distingue déjà la structure


arborescente. On peut la mettre en évidence en supprimant tout
ce qui n'est pas un nom de table :

T_CLIENT CLI -- le client (racine de l'arbre)


T_ADRESSE ADR -- adresse, table d'entité (feuille de l'arbre)
T_TITRE TIT -- titre, table d'entité (feuille de l'arbre)
T_EMAIL EML -- mail, table d'entité (feuille de l'arbre)
T_TELEPHONE TEL -- téléphone, table d'entité servant de jointure
(noeud dans l'arbre)
T_TYPE TYP -- type de téléphone, table d'entité (feuille de l'arbre)
TJ_CHB_PLN_CLI CPC -- table de jointure (noeud dans l'arbre)
T_PLANNING PLN -- date du planning, table d'entité (feuille de l'arbre)
T_CHAMBRE CHB -- chambre, table d'entité servant de jointure
(noeud dans l'arbre)
TJ_TRF_CHB TC -- table de jointure (noeud dans l'arbre)
T_TARIF TRF -- tarif, table d'entité (feuille de l'arbre)
T_FACTURE FAC -- facture, table d'entité servant de jointure
T_LIGNE_FACTURE LIF -- ligne de facture, table d'entité (feuille de
l'arbre)
T_MODE_PAIEMENT PMT -- mode de paiement, table d'entité (feuille de
l'arbre)

Par cette indentation, il est facile de repérer les jointures


entre les tables.

VI. Note importante


Les jointures ne sont pas la seule manière de mettre en
relation différentes tables au sein d'une même requête SQL. On
peut aussi joindre plusieurs tables à l'aide des sous-requêtes
ainsi qu'à l'aide des opérateurs ensemblistes que nous allons
voir aux deux prochains chapitres.

VII. Résumé
Voici les différences entre les moteurs des bases de données :

Paradox Access PostGreSQL Sybase SQL Server 7 Oracle 8


DB2 (400)
INNER JOIN Oui Oui Oui Oui Oui Non (1) Oui
OUTER JOIN LEFT, RIGHT, FULL LEFT, RIGHT LEFT, RIGHT,
FULL LEFT, RIGHT, FULL LEFT, RIGHT, FULL Non (1) LEFT
(4)
UNION JOIN Non (2) Non Non Non (2) Non (2) Non (1) (2)
Non
CROSS JOIN Non (3) Non (3) Oui Non (3) Oui Non (1) (3)
Oui

(1) Oracle ne connaît toujours pas le JOIN (ça fait quand même
plus de dix ans de retard pour cet éditeur pionnier qui semble
s'endormir sur ses lauriers). Il faut donc utiliser une
syntaxe propriétaire. Exception : la version 9 supporte enfin
les jointures normalisées.

(2) Possible avec un FULL OUTER JOIN

(3) Possible avec l'ancienne syntaxe sans précision de critère


WHERE

(4) de plus IBM DB2 (400) dispose d'un très intéressant


"exception join" equivalent à :

SELECT *
FROM tablegauche
LEFT OUTER JOIN tabledroite
ON références de jointure
WHERE tabledroite.clef IS NULL

L'optimisation de l'exécution des requêtes SQL ne passe


pas que par une simple récriture de ces dernières. De
nombreux points tels que l'infrastructure du réseau,
l'O.S. utilisé comme l'architecture de la base peuvent
être la cause de pertes significatives de temps
d'exécution.

Voici un petit guide de l'optimisation dans le cadre de


l'exploitation de serveurs de bases de données
relationnelles.

Préambule
1. L'environnement
2. Le serveur physique ou "la machine"
3. Le Serveur logique ou SGBDR
4. La base de données
5. Le modèle de données
6. En développement
7. En exploitation
8. Optimiseurs et plan de requêtes
9. Transformations usuelles
10. Quelques trucs
11. CONCLUSION

Préambule
NOTA : La structure de la base de données exemple, ainsi
qu'une version des principales bases utilisées sont
disponibles dans la page "La base de données exemple"

Si les SGBDR sont dotés d'un optimiseur, cela n'empêche


pas ce dernier de se tromper ou d'être limité par le
carcan de votre expression de requête. De plus
l'optimiseur étant interne au SGBDR, il n'a aucune
influence sur le SGBDR lui même, la machine ni
l'infrastructure du réseau, élément décisif en matière
de rapidité de traitement des flux de données.
Voici donc, point par point, les éléments qu'il faut
prendre en considération pour "booster" votre SGBDR et
l'exécution des requêtes SQL !

1. L'environnement
L'environnement informatique, c'est à dire
l'architecture globale du SI, a une influence
déterminante sur les performances du SGBDR et donc sur
la vitesse à laquelle vos utilisateurs vont accéder aux
données.

L'indispensable est un serveur dédié pour le SGBDR


surtout s'il est en Client/Serveur. L'utile est de
paramétrer l'OS du serveur sur lequel travaille le SGBDR
de façon à ce que le serveur dispose de ressources
adéquates.

On veillera à mettre des dispositifs d'accès adéquats en


fonction du nombre d'utilisateurs simultanées et de la
charge qu'ils induisent. Le mieux étant un réseau
déterministe, ce qu'hélas Ethernet n'est pas. Mais on
pourra par exemple employer plusieurs cartes réseau pour
un même serveur pour des groupes d'utilisateurs ou sous
réseaux distincts.
On peut aussi employer des dispositifs accélérant le
débit comme de la connexion fibre optique, ou des switch
plutôt que des hubs. Dans ce dernier cas, des switch
administrables présentent un intérêt majeur, celui de
pouvoir répartir la charge...
Voici un tableau dont on peut s'inspirer :

utilisateurs simultanés15155050300300
débitfaiblefortfaiblefortfaiblefort
architecture10 Mo/s, 1 carte réseau100 Mo/s, 1
carte réseau100 Mo/s, 1 à 2 cartes réseau100 Mo/s,
1 à 4 cartes réseau + switch frontal1 Go/s, 1
carte réseau FO + switch frontal1 Go/s, 2 à 4
cartes réseau en FO + switch frontal cascadé

FO : fibre optique

Au dela, la répartition de charge passe par des


techniques de clustering. Dans ce cas entre en compte
les phénomènes de réplication et il vaut mieux prévoir
un développement en mode "client serveur déconnecté".

Des adresses fixes des ressources du réseau sont


préférables.

CE QUI FAIT GAGNER DU TEMPS...

Une infrastructure réseau adaptée à la charge, où le


nombre de collision va être minime, voir nul... C'est à
dire :

Des interfaces réseau rapide (fibre, switch...)


Des sous réseaux parrallélisés
Un protocole réseau déterministe (100 VG anylan,
SPX/IPX...)
La répartition de la charge par les dispositifs
d'interface administrables et/ou un ensemble de
serveurs

2. Le serveur physique ou "la machine"


Un SGBDR ne fonctionne correctement que sur une machine
qui lui est dédiée. Tout autre solution d'exploitation
est à proscrire.
La machine est donc un PC de type serveur.

ATTENTION : un PC de base ne peut en aucun cas être


reconvertit en serveur car l'architecture interne d'un
serveur n'a rien à voir avec un PC destiné à une
utilisation personnelle. Ainsi un serveur est taillé
pour paralléliser des flux de données et accèder
rapidement aux ressources disque, tandis qu'un PC
personnel est plutôt conçu pour traiter rapidement des
routines graphiques...

On vérifiera que le serveur choisit possède au moins les


caractéristiques et équipements suivants :

Alimentation redondante, répartissant la charge et


extractible à chaud (hot swap et hot plug)
Mémoire RAM autocorrective (ECC)
Cartes réseau redondantes (IPSEC)
Ondulation électrique (UPS)
Disques durs redondants (RAID)
Système de sauvegarde des données sur support physique
externe
L'alimentation redondante permet de continuer
l'exploitation du serveur malgré une panne de ce sous
sytème, ainsi que son remplacement à chaud. Il est
particulièrement recommandé de disposer d'une
alimentation de secours de dépannage.

La mémoire RAM autocorrective ECC (Error Checking and


Correction) est une mémoire qui isole les parties abimée
de l'espace mémoire et en interdit l'accès
dynamiquement, ce qui empêche la survenance des erreurs.
Elle doit être en quantité suffisante par rapport à la
taille de la base. Il n'est pas rare de définir une
taille de RAM d'au moins 256 Mo + taille de la base pour
le serveur. Si par exemple votre base de données fait
800 Mo, une RAM de plus de 1 Go est souhaitable. Ainsi
la quasi totalité des données servies pourront être
montées en mémoire afin d'assurer un temps d'accès bien
inférieur à la lecture du disque.
On a tout intérêt à prévoir au moins une carte réseau
redondante et lui affecter une adresse logique
supplémentaire. C'est plus facile lorsque la carte tombe
en panne de dévier le traffic vers une carte déjà
installée que d'éteindre le serveur pour remplacer la
carte !

Un onduleur est un dispositif électronique permettant de


pallier à un défaut d'alimentation électrique pendant un
temps restreint. Contrairement à une idée reçue, les
onduleurs ne sont pas destinés à filtrer le courant
électrique et en principe ne protègent pas de la
surtension ou des parasites. Pour cela il faut un
dispositif complémentaire à l'onduleur, et c'est
pourquoi il existe des onduleurs "on line" et "off
line". Pour un serveur il faut toujours utiliser un
onduleur "on line" doté d'une sortie logique afin
d'informer le serveur d'une panne de courant, pour que
l'extinction du serveur se fasse dans de bonnes
conditions, ou bien qu'un groupe électrogène, qui doit
prendre le relai, soit démarré.

REMARQUE : Le talon d'achille d'un serveur sont les


disques et sur ces disques se trouvent le capital
"savoir" de l'entreprise, c'est à dire les données. La
plupart des défaillances matérielles des systèmes
informatiques viennent des alimentations et des disques
durs des PC qui fonctionnent 24heures/24... C'est donc
sur ces éléments qu'il convient d'être le plus exigeant.

En ce qui concerne les disques, tous les éditeurs de


SGBDR un tant soit peu sérieux, recommandent une
technologie de contrôleur disque de type RAID en SCSI.
Rapellons la définition du système RAID (Redundant Array
of Inexpensive Disks) : un ensemble de disques dans
lequelle une partie de la capacité physique est utilisée
pour y stocker de l'information redondante. Cette
information redondante permet la régénération des
données perdues. Les systèmes RAID se caractérisent par
différents niveaux dont les plus connus sont numérotés
de 0 à 10 (en fait, 0, 1, 3, 4, 5, 6 7 et 10). Le niveau
RAID 5 constitue une bonne sécurité combinée à une bonne
tolérance à la panne, puisqu'en principe, un disque
endommagé peut être remplacé "à chaud", c'est à dire
sans interruption du fonctionnement du serveur.
Le SCSI (Small Computer System Interface ) est
particulièrement intéressant parce qu'il s'agit d'un
système "intelligent" de gestion de l'espace disque et
de ses anté-mémoires qui sollicite notablement moins le
processeur que les technologies classiques comme l'IDE.
Le débit des périphériques SCSI est plus important et il
est multitâche alors que l'IDE est mono tâche.

Un dispositif physique de sauvegarde des données : une


mauvaise habitude consite à croire qu'il suffit de se
doter de la technologie RAID pour d'être à l'abri de
tout problème et donc de faire l'impasse sur un
dispositif de sauvegarde. C'est une hérésie!
Une sauvegarde implique une réelle délocalisation des
données pour parer à tout problème grave (dégâts des
eaux, incendie, malveillance...) mais aussi tout
incident d'exploitation comme la suppression
malencontreuse ou malintentionnée des données.

NOTA : il ne faut pas confondre la sauvegarde logique


des données que la plupart des éditeurs de SGBDR
proposent dans leur package et une sauvegarde physique.
En effet les SGBDR fonctionnant en permanence il n'est
bien souvent ,pas possible de "fermer" le ou les
fichiers constituant la base de données à des fins de
copie lors d'une sauvegarde. Pour y pallier, les
éditeurs proposent un mécanisme qui écrit un jeu de
données intègre de la base dans un fichier externe à des
fins de stockage et d'archivage (sauvegarde logique). Il
convient toujours d'utiliser ce principe associée à la
sauvegarde physique.

ATTENTION : il ne faut jamais dépasser un taux


d'occupation de l'ordre de 67% de l'espace disque. En
effet, en exploitation, votre base de données "grossie",
et occupe donc une place de plus en plus grande sur le
disque. Plus le disque se rempli et plus les temps
d'accès sont long. Le phénomène n'est pas linéaire. S'il
est insensible lorsque le disque est faiblement rempli,
il peut devenir sensible lors des lectures lorsque le
disque arrive à saturation. Il peut même bloquer
totalement le SGBDR en cas de modification des données.
Certains OS réseau permettent de définir des alertes
administratives en cas de dépassement de quotas d'espace
disque. Elles sont absolument nécessaires pour une
exploitation sereine et performante du SGBDR.

CE QUI FAIT GAGNER DU TEMPS...

Un PC qui soit un véritable serveur


De la RAM en quantité suffisante
Des disques SCSI
Un taux d'occupation d'espace disque toujours
inférieur à 67%

3. Le Serveur logique ou SGBDR


Nous avons déjà dit que le SGBDR doit être installé sur
une machine dédiée. Il est même conseillé de n'installer
qu'une seule base en exploitation par machine. Ainsi
dans le cadre de l'organisation de votre système
informatique, si vous disposez d'une base de données
"front office" et d'une autre "back office" il vous
faudra opter pour deux machines indépendantes (pensez
simplement à prendre exactement les mêmes, car en cas de
panne de l'une il est plus facile de redémarrer
l'ensemble des services sur l'autre...).

Les paramètres de l'installation du serveur peuvent


avoir une grande influence sur les temps de réponse mais
aussi sur le comportement de vos requetes. Par exemple
le choix d'un jeu de caractères combiné à une collation
compatible dont le tri est binaire sera bien plus rapide
et efficace qu'une collation insensible à la casse et
aux caractères diacritiques. Lire à ce sujet "Une
question de caractères".

Mais l'ensemble de ces paramètres étant spécifique à


chaque éditeur il est difficile d'en dire plus...
CE QUI FAIT GAGNER DU TEMPS...

Une base exploitée, sur une machine dédiée


Des paramètres SGBDR adaptées (collation binaire,
taille des buffers...)

4. La base de données
Lors de la création d'une base il est possible de
demander la création d'un fichier doté d'une taille
précise. Il convient de toujours créer le fichier de la
base de données avec la taille qu'aura la base de
données au cours de son exploitation. Ainsi si votre
base de données doit faire à terme 3 Go, lors de la
création de la base, donnez cette valeur comme taille du
fichier. Les dispositifs d'auto adaptation de la taille
de la base font généralement perdre du temps par le fait
qu'ils fragmentent le fichier constituant la base.

Placez le fichier des données de la base sur un disque à


part en évitant le disque système. Placez le fichier du
journal sur un disque à part en évitant le disque
système et celui contenant les données de la base.
Ceci retarde la saturation des disques et fait gagner du
temps du fait de la parrallélisation des tâches
d'écriture du système SCSI sur deux disque physiques
différents.

Si vous le pouvez, répartissez les données de votre base


sur plusieurs disques par exemple en plaçant les données
les plus lourdes (BLOB) sur les disques les moins
performants.

Choisissez bien la taille de la page de données (c'est


la granule de base pour le stockage des données de votre
SGBDR). Cela se calcule en fonction de la taille de la
base adulte, de la taille des disques et de la taille de
la RAM...
On peut donner à titre indicatif l'échelle suivante
(mieux vaut consulter la documentation de votre SGBDR) :
si la taille préconisée par votre SGBDR est 8 Ko pour
une base de quelques Go avec 1 Go de RAM sur des disques
de 9 Go avec un OS 32 bits... alors :

RAM256 Mo512 Mo1 Go2 Go4 Go


Disque4Go4Go9 Go18Go36Go
Base600 Mo1 Go3 Go8 Go25 Go
Page2 Ko4 Ko8 Ko16 Ko32 Ko

Réglez les paramètres des structures de stockage en


fonction de la place disque et du pourcentage de mise à
jour.
Par exemple, les structures d'arbres pour stocker les
index utilisent le plus souvent un paramètre, le "fill
factor" (facteur de remplissage) qui peut être ajusté.
La plupart du temps ce "fill factor" est de l'ordre de
92 à 98 %. Plus il est faible, plus l'accès à l'index
sera rapide, mais plus le volume de données occupé par
cet index sera important et donc couteux en terme de
mise à jour. A vous de décider si vous voulez optimiser
les lectures ou les écritures sur la base de données.

CE QUI FAIT GAGNER DU TEMPS...

Créez la base de données avec une taille de fichier


calculé sur le volume à l'age adulte de la base
Répartissez les fichiers de données et du journal sur
des disques différents, en évitant le disque système
Répartissez les données sur plusieurs disque,
notamment les BLOBS sur les disques les moins rapide
Précisez une taille de page de données adaptée à la
structure physique du système et au volume des données

Réglez les paramètres des structures de stockage des


index

5. Le modèle de données
Normalisez au maximum... Créez des tables les plus
petites possible en externalisant dans des tables de
références toutes les informations susceptible d'être
utilisées plusieurs fois.

Exemple :
Standardisez vos types de données et leur format en
utilisant des domaines. Voir définir les domaines et les
utiliser... Vous n'aurez donc pas d'effort à à demander
au SGBDR lors de comparaisons sur des colonnes de
contenu similaire (trantypage implicite). En effet si
vous voulez comparer le nom d'un client définit comme
VARCHAR(32) au nom d'un prospect défini comme NCHAR(25),
le SGBDR devra fournir un effort supplémentaire pour
homogénéiser les types de colonnes avant d'opérer la
comparaison.

Choisisez des clefs dont la taille soit exactement la


taille du mot du processeur (par exemple CHAR(4),
INTEGER dans un OS 32 bits comme Win95, 98, NT,
2000....). Préférez des clefs purement informatiques
pour vos jointures plutôt que les clefs naturelles qui
découlent de votre analyse.

MAUVAISBON

Évitez les clefs composites. Préférez encore une fois


une clef purement informatique !

Évitez les collonnes "nullables" (c'est à dire pouvant


possédez une valeur nulle) lorsque ces dernières doivent
être calculées. En particulier les données comptables et
les colonnes représentant des données booléennes.
Préférez mettre la valeur par défaut 0, sinon il vous
faudra utiliser un opérateur COALESCE ou CASE dans les
requêtes effectuant une opération arithmétique afin que
les marqueurs NULL soient pris en compte en tant que 0.

Préférez les types fixes (CHAR, NCHAR...) plutôt que les


types variables (VARCHAR...) chaque fois que la colonne
sera sollicité en recherche et jointure.

Utilisez au maximum les contraintes prévues dans la


clause CREATE avant de passer à une programmation de
triggers. Par exemple préférez le ON DELETE SET NULL et
un batch de nuit pour une suppression en cascade plutôt
que d'utiliser un trigger.

Modélisez vos relations de manière performante. Ainsi


une hiérarchie sera avantageusement modélisée par un
arbre représenté par intervalle plutôt que par une auto
référence. Évitez les jointures trop complexes en
particulier sur plus de deux tables.

Indexez l'essentiel, c'est à dire, les clefs primaires


et étrangères, les colonnes les plus sollicités en
recherche et jointure, etc... Jamais les colonnes de
type BLOB ou texte libre !

Ajoutez à votre base une table des dates, plutôt que


d'utiliser des fonctions de calculs temporellles.

Dénormalisez à bon escient si tout le reste à échoué


dans votre recherche de gain de temps.

CE QUI FAIT GAGNER DU TEMPS...

Normalisez vos données, entités et relations


Standardisez le format et le type de vos colonnes
Utiliser des clefs composée d'une colonne unique d'un
type 32 bits et purement informatique (un entier c'est
parfait)
Évitez les colonnes nullables surtout si elle doivent
être calculées
Préférez les types fixe (CHAR au lieu de VARCHAR) pour
les colonnes fréquemment sollicitées en recherche et
jointure
Utilisez des modèles performants pour vos relations
complexes (héritage, arbres....)
Indexez l'essentiel, pas le superflu!
Ajoutez une table des dates plutôt que d'utiliser des
fonctions de calcul temporel
Dénormalisez vos relations lorsque tout le reste à
échoué !
6. En développement
Prévoyez le formatage de vos données avant toute
insertion ou mise à jour. Par exemple assurez vous qu'un
nom de personne soit toujours en majuscule sans blancs
parasite ni début ni en fin (TRIM + UPPER).

Exemple - pour ma part j'utilise systématiquement une


routine de formattage comprenant les modèles suivants :

FormatRègles
DateFrretire tous les caractères autres que
chiffres et pose des barres JJ/MM/AAAA
Téléphoneformate une chaine de chiffre en
téléphone :
33 1 45 78 45 78 (11 chiffres) ou
01 45 78 45 78 (10 chiffres)
dans tous les autres cas :
groupes de 2 et si impair, alors commence par 1
chiffre isolé

Prénomne retient que les caractères a à z


(minuscule) accent, et lettres diacritiques et les
caractères [' ', '-', '''] (espace, tiret,
apostrophe) avec capitalisation des initiales
Mailformatage d'une adresse mail :
minuscule, acceptation des caractères '_' , '-' ,
et '.' ; présence obligatoire d'un seul @

IDENTIFIANTne retient que les caractères A à Z


(majuscule) et le caractère '_' (blanc souligné)
transforme les accents et lettres diacritiques.
Tous les autres caractères sont rejetés
09ne retient que les caractères 0 à 9 sans espace
AZ_majne retient que les caractères A à Z
(majuscule), transforme les accents et lettres
diacritiques. Tous les autres caractères sont
rejetés
AZ09_majne retient que les caractères A à Z
(majuscule) et 0 à 9, transforme les accents et
lettres diacritiques. Tous les autres caractères
sont rejetés
az_minne retient que les caractères a à z
(minuscule), transforme les accents et lettres
diacritiques. Tous les autres caractères sont
rejetés
az09_minne retient que les caractères a à z
(minuscule) et 0 à 9, transforme les accents et
lettres diacritiques. Tous les autres caractères
sont rejetés
AZ_maj_plusne retient que les caractères A à Z
(majuscule) et les caractères [' ', '-', ''']
(espace, tiret, apostrophe) sans aucun autre
caractères (ni accent, ni lettres diacritiques)
AZ09_maj_plusne retient que les caractères A à Z
(majuscule) et 0 à 9 et les caractères [' ', '-',
'''] (espace, tiret, apostrophe) sans aucun autre
caractères (ni accent, ni lettres diacritiques)
az_min_plusne retient que les caractères a à z
(minuscule) et les caractères [' ', '-', ''']
(espace, tiret, apostrophe) sans aucun autre
caractères (ni accent, ni lettres diacritiques)
az09_min_plusne retient que les caractères a à z
(minuscule) et 0 à 9 t les caractères [' ', '-',
'''] (espace, tiret, apostrophe) sans aucun autre
caractères (ni accent, ni lettres diacritiques)
Az09plusplusconvertis les accents et diacritique
en lettres non accentuées non diacritées

Dès lors les recherches et comparaisons n'ont pas besoin


d'être effectuées avec un paramétrage de type
"insensible à la casse" + "insensible aux accents", car
les données sont toujours formatées de la même façon. Ce
qui accélère notablement les recherches.

Prévenez les doublons en cherchant avant insertion si la


ligne n'a pas déjà été saisie. Pour des noms propres,
utiliser les recherches phonétiques comme le Soundex.
Voir L'art des "Soundex".

Interdisez les orphelins... Utiliser l'intégrité


référentielle et les triggers pour l'étendre afin de
prévenir toute ligne orpheline.

Indexer les mots de vos textes longs s'ils doivent faire


l'objet d'une recherche interne. Voir L'indexation
textuelle.
CE QUI FAIT GAGNER DU TEMPS...

Un bon formatage des données saisies et mise à jour


permet des recherches plus rapides
L'absence de doublons évite le double comptage et
l'emploi du mot clef DISTINCT
Une indexation textuelle évite les recherches longues
et fastidieuses

7. En exploitation
Surveillez le volume des données : la base est-elle trop
grosse ? Pourquoi ? Le fichier du journal trop grand ?
Pouvez vous le tronquer ? La place libre du disque
est-elle assez conséquente (au moins 33%) ?...

Si votre SGBDR est doté d'un optimiseur statistiques,


mettez à jour régulièrement les statistiques.

Reindexez de temps à autres la base, surtout après


d'importantes mises à jour par lot.

Réorganisez votre base, afin d'optimiser le nombre de


pages réllément utiles.

Surveillez la charge en accès et le volume du traffic :


identifiez les gros consommateurs et optimisez leurs les
"tuyaux".

CE QUI FAIT GAGNER DU TEMPS...

Ne descendez jamais en dessous de 33% de place vide


sur les disques
Mettez à jour les statistiques
Réorganisez votre base régulièrement et réindexez si
besoin est
Surveillez la charge et adpatez les tuyaux
8. Optimiseurs et plan de requêtes
La plupart des SGBDR sont dotés d'un optimiseur qui
analyse de façon logique ou statistique la meilleure
façon d'exécuter la requête. Il n'est pas rare que vous
puissiez intervenir sur la façon dont le SGBDR et son
optimiseur prétende opérer.
La façon dont il va opérer s'apelle le "plan de requête"
et montre les opérations simplistes qu'il va réaliser
pour répondre à votre demande. Des SGBDR comme Oracle,
MS SQL Server ou encore DB2 possèdent un outil
permettant de visualiser ce plan.

Voici l'exemple de l'outil de visualisation des plans de


requêtes de MS SQL Server :

Il est certes très beau, mais la principale information


manque : le temps d'exécution de chaque étape ainsi que
le nombre d'entrée/sortie effectuées sur le disque...
Pour cela il faut faire un clic droit sur chaque
emblème, ce qui ne s'avère pas très pratique...

Suivant les éditeurs, vous pouvez intervenir directement


sur le plan de requête ou bien préciser les index à
utiliser ou encore vous pouvez n'être autorisé qu'à ré
écrire votre requête différement afin de trouver
l'optimisation la plus économe en temps et en
ressources...

CE QUI FAIT GAGNER DU TEMPS...

Choisissez le bon plan !

9. Transformations usuelles
Voici quelques transformations usuelles qu'il convient
d'avoir à l'esprit afin d'optimiser vos requêtes.

ATTENTION : vérifiez bien que la transformation opère


une réduction du temps de traitement car cela n'est pas
toujours le cas et peut dépendre de votre indexation, du
type de données, des paramètres de votre SGBDR et des
ressources de votre machine... Il n'y a pas de miracle,
seul des tests peuvent vous convaincre de l'efficacité
d'écrire votre requête de telle ou telle manière.

N°ÉVITEZPRÉFÉREZ
1évitez d'employer l'étoile dans la clause
SELECT...

SELECT *
FROM T_CLIENT...préférez nommer les colonnes une
à une

SELECT CLI_ID, TIT_CODE, CLI_NOM,


CLI_PRENOM, CLI_ENSEIGNE
FROM T_CLIENT
2évitez d'employer DISTINCT dans la clause
SELECT...

SELECT DISTINCT CHB_NUMERO, CHB_ETAGE


FROM T_CHAMBRE...lorsque cela n'est pas nécessaire

SELECT CHB_NUMERO, CHB_ETAGE


FROM T_CHAMBRE
3n'employez pas de colonne dans la clause
SELECT...
de la sous requête EXISTS...

SELECT CHB_ID
FROM T_CHAMBRE T1
WHERE NOT EXISTS (SELECT CHB_ID
FROM TJ_CHB_PLN_CLI T2
WHERE PLN_JOUR = '2000-11-11'
AND T2.CHB_ID = T1.CHB_ID)...utilisez
l'étoile ou une constante

SELECT CHB_ID
FROM T_CHAMBRE T1
WHERE NOT EXISTS (SELECT *
FROM TJ_CHB_PLN_CLI T2
WHERE PLN_JOUR = '2000-11-11'
AND T2.CHB_ID = T1.CHB_ID)
4évitez de compter une colonne...

SELECT COUNT (CHB_ID)


FROM T_CHAMBRE...quand-il suffit de compter les
lignes

SELECT COUNT (*)


FROM T_CHAMBRE
5évitez d'utiliser le LIKE...

SELECT *
FROM T_CLIENT
WHERE CLI_NOM LIKE 'D%'...si une fourchette de
recherche le permet

SELECT *
FROM T_CLIENT
WHERE CLI_NOM BETWEEN 'D' AND 'E '
6évitez les jointures dans le WHERE...

SELECT *
FROM T_CLIENT C, T_FACTURE F
WHERE EXTRACT(YEAR FROM F.FAC_DATE) = 2000
AND F.CLI_ID = C.CLI_ID...préférez l'opérateur
normalisé JOIN

SELECT *
FROM T_CLIENT C
JOIN T_FACTURE F
ON F.CLI_ID = C.CLI_ID
WHERE EXTRACT(YEAR FROM F.FAC_DATE) = 2000
7évitez les fourchettes < et > pour des valeurs
discrètes...

SELECT *
FROM T_FACTURE
WHERE FAC_DATE > '2000-06-18'
AND FAC_DATE < '2000-07-15'...préférez le
BETWEEN

SELECT *
FROM T_FACTURE
WHERE FAC_DATE BETWEEN '2000-06-18'
AND '2000-07-14'
8évitez le IN avec des valeurs discrètes
recouvrantes...

SELECT *
FROM T_CHAMBRE
WHERE CHB_NUMERO IN (11, 12, 13, 14)...préférez
le BETWEEN

SELECT *
FROM T_CHAMBRE
WHERE CHB_NUMERO BETWEEN 11 AND 14
9évitez d'employer le DISTINCT...

SELECT DISTINCT CLI_NOM, CLI_PRENOM


FROM T_CLIENT C
JOIN TJ_CHB_PLN_CLI J
ON C.CLI_ID = J.CLI_ID
WHERE PLN_JOUR = '2000-11-11'...si une sous
requête EXISTS vous offre le dédoublonnage

SELECT CLI_NOM, CLI_PRENOM


FROM T_CLIENT C
WHERE EXISTS (SELECT *
FROM TJ_CHB_PLN_CLI J
WHERE C.CLI_ID = J.CLI_ID
AND PLN_JOUR = '2000-11-11')
10évitez les sous requêtes...

SELECT CHB_ID
FROM T_CHAMBRE
WHERE CHB_ID NOT IN (SELECT CHB_ID
FROM TJ_CHB_PLN_CLI
WHERE PLN_JOUR = '2000-11-11')...quand
vous pouvez utiliser les jointures

SELECT DISTINCT C.CHB_ID


FROM T_CHAMBRE C
LEFT OUTER JOIN TJ_CHB_PLN_CLI P
ON C.CHB_ID = P.CHB_ID
AND PLN_JOUR = '2000-11-11'
WHERE P.CHB_ID IS NULL
11évitez les sous requêtes avec IN...

SELECT CHB_ID
FROM T_CHAMBRE
WHERE CHB_ID NOT IN (SELECT CHB_ID
FROM TJ_CHB_PLN_CLI
WHERE PLN_JOUR = '2000-11-11')...lorsque
vous pouvez utiliser EXISTS

SELECT CHB_ID
FROM T_CHAMBRE T1
WHERE NOT EXISTS (SELECT *
FROM TJ_CHB_PLN_CLI T2
WHERE PLN_JOUR = '2000-11-11'
AND T2.CHB_ID = T1.CHB_ID)
12transformez les COALESCE...

SELECT LIF_ID,
(LIF_QTE * LIF_MONTANT)
* (1 - COALESCE(LIF_REMISE_POURCENT, 0)/100)
- COALESCE(LIF_REMISE_MONTANT, 0) AS TOTAL_LIGNE
FROM T_LIGNE_FACTURE...en UNION

SELECT LIF_ID, (LIF_QTE * LIF_MONTANT)


FROM T_LIGNE_FACTURE
WHERE LIF_REMISE_POURCENT IS NULL
AND LIF_REMISE_MONTANT IS NULL
UNION
SELECT LIF_ID, (LIF_QTE * LIF_MONTANT)
- LIF_REMISE_MONTANT
FROM T_LIGNE_FACTURE
WHERE LIF_REMISE_POURCENT IS NULL
AND LIF_REMISE_MONTANT IS NOT NULL
UNION
SELECT LIF_ID, (LIF_QTE * LIF_MONTANT)
* (1 - LIF_REMISE_POURCENT/100)
FROM T_LIGNE_FACTURE
WHERE LIF_REMISE_POURCENT IS NOT NULL
AND LIF_REMISE_MONTANT IS NULL
UNION
SELECT LIF_ID, (LIF_QTE * LIF_MONTANT)
* (1 - LIF_REMISE_POURCENT/100)
- LIF_REMISE_MONTANT
FROM T_LIGNE_FACTURE
WHERE LIF_REMISE_POURCENT IS NOT NULL
AND LIF_REMISE_MONTANT IS NOT NULL
13transformez les CASE...

ELECT CHB_NUMERO, CASE CHB_ETAGE


WHEN 'RDC' THEN 0
WHEN '1er' THEN 1
WHEN '2e' THEN 2
END AS ETAGE, CHB_COUCHAGE
FROM T_CHAMBRE
ORDER BY ETAGE, CHB_COUCHAGE...en UNION

SELECT CHB_NUMERO, 0 AS ETAGE, CHB_COUCHAGE


FROM T_CHAMBRE
WHERE CHB_ETAGE = 'RDC'
UNION
SELECT CHB_NUMERO, 1 AS ETAGE, CHB_COUCHAGE
FROM T_CHAMBRE
WHERE CHB_ETAGE = '1er'
UNION
SELECT CHB_NUMERO, 2 AS ETAGE, CHB_COUCHAGE
FROM T_CHAMBRE
WHERE CHB_ETAGE = '2e'
ORDER BY ETAGE, CHB_COUCHAGE
14transformez les EXCEPT...

SELECT CHB_ID
FROM T_CHAMBRE
EXCEPT
SELECT CHB_ID
FROM TJ_CHB_PLN_CLI
WHERE PLN_JOUR = '2000-11-11'...en jointures

SELECT DISTINCT C.CHB_ID


FROM T_CHAMBRE C
LEFT OUTER JOIN TJ_CHB_PLN_CLI P
ON C.CHB_ID = P.CHB_ID
AND PLN_JOUR = '2000-11-11'
WHERE P.CHB_ID IS NULL
15transformez les INTERSECT...

SELECT CHB_ID
FROM T_CHAMBRE
INTERSECT
SELECT CHB_ID
FROM TJ_CHB_PLN_CLI
WHERE PLN_JOUR = '2000-11-11'...en jointure

SELECT DISTINCT C.CHB_ID


FROM T_CHAMBRE C
INNER JOIN TJ_CHB_PLN_CLI P
ON C.CHB_ID = P.CHB_ID
WHERE PLN_JOUR = '2000-11-11'
16transformez les UNION...

SELECT OBJ_NOM AS NOM, OBJ_PRIX AS PRIX


FROM T_OBJET
UNION
SELECT MAC_NOM AS NOM, MAC_PRIX AS PRIX
FROM T_MACHINE
ORDER BY NOM, PRIX(l'exemple complet se trouve dans : les techniques
des SGBDR)
...en jointure

SELECT COALESCE(OBJ_NOM, MAC_NOM) AS NOM,


COALESCE(OBJ_PRIX, MAC_PRIX) AS PRIX
FROM T_OBJET O
FULL OUTER JOIN T_MACHINE M
ON O.OBJ_NOM = M.MAC_NOM
AND O.OBJ_PRIX = M.MAC_PRIX
ORDER BY NOM, PRIX
17transformez les sous requêtes <> ALL ...

SELECT CHB_ID, CHB_COUCHAGE


FROM T_CHAMBRE
WHERE CHB_COUCHAGE <> ALL (SELECT CHB_COUCHAGE
FROM T_CHAMBRE
WHERE CHB_ETAGE ='RDC')...
en NOT IN

SELECT CHB_ID, CHB_COUCHAGE


FROM T_CHAMBRE
WHERE CHB_COUCHAGE
NOT IN (SELECT CHB_COUCHAGE
FROM T_CHAMBRE
WHERE CHB_ETAGE ='RDC')
18transformez les sous requêtes = ANY ...

SELECT CHB_ID, CHB_COUCHAGE


FROM T_CHAMBRE
WHERE CHB_COUCHAGE = ANY (SELECT CHB_COUCHAGE
FROM T_CHAMBRE
WHERE CHB_ETAGE ='RDC')...
en IN

SELECT CHB_ID, CHB_COUCHAGE


FROM T_CHAMBRE
WHERE CHB_COUCHAGE IN (SELECT CHB_COUCHAGE
FROM T_CHAMBRE
WHERE CHB_ETAGE ='RDC')
19transformez les sous requêtes ANY / ALL ...

SELECT CHB_ID, CHB_COUCHAGE


FROM T_CHAMBRE
WHERE CHB_COUCHAGE > ALL (SELECT CHB_COUCHAGE
FROM T_CHAMBRE
WHERE CHB_ETAGE ='RDC')...en
combinant sous requêtes et aggrégat

SELECT CHB_ID, CHB_COUCHAGE


FROM T_CHAMBRE
WHERE CHB_COUCHAGE > (SELECT MAX(CHB_COUCHAGE)
FROM T_CHAMBRE
WHERE CHB_ETAGE ='RDC')
20évitez les sous requêtes corrélées...

SELECT DISTINCT VILLE_ETP


FROM T_ENTREPOT AS ETP1
WHERE NOT EXISTS
(SELECT *
FROM T_RAYON RYN
WHERE NOT EXISTS
(SELECT *
FROM T_ENTREPOT AS ETP2
WHERE ETP1.VILLE_ETP = ETP2.VILLE_ETP
AND (ETP2.RAYON_RYN = RYN.RAYON_RYN)))(l'exemple complet se
trouve dans : la division
relationnelle...)
...préférez des sous requêtes sans corrélation

SELECT DISTINCT VILLE_ETP


FROM T_ENTREPOT
WHERE RAYON_RYN IN
(SELECT RAYON_RYN
FROM T_ENTREPOT
WHERE RAYON_RYN NOT IN
(SELECT RAYON_RYN
FROM T_ENTREPOT
WHERE RAYON_RYN NOT IN
(SELECT RAYON_RYN
FROM T_RAYON)))
GROUP BY VILLE_ETP
HAVING COUNT (*) =
(SELECT COUNT(DISTINCT RAYON_RYN)
FROM T_RAYON)
21évitez les sous requêtes corrélées...

SELECT FAC_ID, (SELECT MAX(LIF_QTE * LIF_MONTANT)


FROM T_LIGNE_FACTURE L
WHERE F.FAC_ID = L.FAC_ID)
FROM T_FACTURE F
ORDER BY FAC_ID...préférez des jointures
SELECT F.FAC_ID, MAX(LIF_QTE * LIF_MONTANT)
FROM T_FACTURE F
JOIN T_LIGNE_FACTURE L
ON F.FAC_ID = L.FAC_ID
GROUP BY F.FAC_ID
ORDER BY F.FAC_ID
22n'utilisez pas de nombre dans la clause ORDER
BY...

SELECT LIF_ID, (LIF_QTE * LIF_MONTANT)


FROM T_LIGNE_FACTURE
ORDER BY 1, 2...spécifiez de préférence les noms
des colonnes, y compris dans la clause SELECT

SELECT LIF_ID,
(LIF_QTE * LIF_MONTANT) AS LIF_MONTANT
FROM T_LIGNE_FACTURE
ORDER BY LIF_ID, LIF_MONTANT

REMARQUE : toutes les transformations ne répondent pas


de la même manière à la charge. Autrement dit, en
fonction du volume des données telle ou telle
transformation peut s'avérer gagner du temps puis en
faire perdre lorsque le volume des données s'accroit.

CE QUI FAIT GAGNER DU TEMPS...

Transformez vos requêtes et choisissez après tests


Ré évaluez le coût à la charge

10. Quelques trucs


Filtrez au maximum en utilisant la clause WHERE, moins
il y a de lignes retournées, mieux le SGBDR traitera
vite les données.

Si votre SGBDR le supporte, ajoutez une clause limitant


le nombre de lignes renvoyées (TOP, LIMIT...), surtout
lorsque vous testez ou mettez au point votre code et que
la base est exploitée. Moins le SGBDR à de données à
servir, plus il sera véloce.
Projetez au minimum en utilisant la clause SELECT et des
colonnes nommées. Moins il ya de colonnes retournées,
mieux le SGBDR traitera rapidement votre requête.

Surnommez vos tables avec des alias les plus courts


possible. Évitez de préfixer les colonnes non ambigües.

Exemple :

-- surnom trops longs, préfixes inutiles


SELECT CLIENT.CLI_ID, CLIENT.CLI_NOM, CLIENT.CLI_PRENOM,
TELEPHONE.TEL_NUMERO
FROM T_CLIENT CLIENT
JOIN T_TELEPHONE TELEPHONE
ON CLIENT.CLI_ID = TELEPHONE.CLI_ID
-- le flux de caractères comporte 55 caractères de moins et est tout aussi compréhensible.
SELECT C.CLI_ID, CLI_NOM, CLI_PRENOM, TEL_NUMERO
FROM T_CLIENT C
JOIN T_TELEPHONE T
ON C.CLI_ID = T.CLI_ID

Dans un batch répètant cette requête 2 000 fois, le


volume de caractères inutiles aurait été de 100 Ko...

N'utilisez pas de jointure inutiles pour un filtrage.

Exemple :

-- une table de référence sert à la saisie interactive de la référence


-- elle est redondante dans un filtrage puisque le code figure dans
-- la table mère (T_TELEPHONE)
SELECT C.CLI_ID, CLI_NOM, CLI_PRENOM, TEL_NUMERO
FROM T_CLIENT C
JOIN T_TELEPHONE T
ON C.CLI_ID = T.CLI_ID
JOIN T_TYPE TT
ON T.TYP_CODE = TT.TYP_CODE
WHERE TYP_LIBELLE = 'Téléphone fixe'
-- la table mère (T_TELEPHONE) est filtré directement
-- sur le code correspondant au téléphone de type 'fixe'
SELECT C.CLI_ID, CLI_NOM, CLI_PRENOM, TEL_NUMERO
FROM T_CLIENT C
JOIN T_TELEPHONE T
ON C.CLI_ID = T.CLI_ID
WHERE TYP_CODE = 'TEL'

Dans une expression filtrée, placez toujours une colonne


seule d'un côté de l'opérateur de comparaison.

Exemple :

-- l'index sur la colonne LIF_QTE ou LIF_MONTANT ne peut être activé

SELECT *
FROM T_LIGNE_FACTURE
WHERE LIF_QTE + 10 = LIF_MONTANT / 5
-- l'index sur la colonne LIF_QTE peut être activé

SELECT *
FROM T_LIGNE_FACTURE
WHERE LIF_QTE = LIF_MONTANT / 5 - 10

N'utilisez pas de joker en debut de mot dans le cadre


d'une recherche LIKE. Si le besoin est impératif,
ajoutez une colonne contenant là chaîne de caractères à
l'envers.

Exemple :

CREATE TABLE T_MOT


(MOT VARCHAR(25))

INSERT INTO T_MOT (MOT) VALUES ('marchand')


INSERT INTO T_MOT (MOT) VALUES ('marcher')
INSERT INTO T_MOT (MOT) VALUES ('flamand')

SELECT *
FROM T_MOT
WHERE MOT LIKE '%and'

MOT
-------------------------
marchand
flamand
-- l'index sur la colonne MOT ne peut être activé
ALTER TABLE T_MOT
ADD TOM VARCHAR(25)

UPDATE T_MOT
SET TOM = REVERSE(MOT) -- REVERSE renvoie la chaine en inversant l'ordre des
lettres

SELECT *
FROM T_MOT
WHERE TOM LIKE 'dna%'

MOT TOM
------------------------- -------------------------
marchand dnahcram
flamand dnamalf

-- l'index sur la colonne TOM peut être activé

Evitez la recherche de négation (NOT) ou de différence


(<>), préférez la recherche positive.

Exemple :

SELECT *
FROM T_FACTURE
WHERE FAC_PMT_DATE NOT BETWEEN FAC_DATE AND FAC_DATE +
INTEVAL 30 DAY
SELECT *
FROM T_FACTURE
WHERE FAC_PMT_DATE < FAC_DATE
OR FAC_PMT_DATE > FAC_DATE + NTEVAL 30 DAY

Créez des vues pour simplifier vos requêtes.

Exemple :

-- recherche du tarif des chambres 1, 3 et 5 à la date du 25/12/2000

SELECT CHB_ID, TRF_CHB_PRIX


FROM TJ_TRF_CHB
WHERE TRF_DATE_DEBUT = (SELECT MAX(TRF_DATE_DEBUT)
FROM TJ_TRF_CHB
WHERE TRF_DATE_DEBUT < '2000-12-25')
AND CHB_ID IN (1, 3, 5)
-- vue simplifiant la présentation des intervalles de validité des tarifs

CREATE VIEW V_TARIF_CHAMBRE


AS
SELECT CHB_ID, TRF_DATE_DEBUT,
COALESCE((SELECT MIN(TRF_DATE_DEBUT)
FROM TJ_TRF_CHB T2
WHERE T2.TRF_DATE_DEBUT > T1.TRF_DATE_DEBUT), '2099-12-31')
- INTERVAL 1 DAY AS TRF_DATE_FIN,
TRF_CHB_PRIX
FROM TJ_TRF_CHB T1

SELECT CHB_ID, TRF_CHB_PRIX


FROM V_TARIF_CHAMBRE
WHERE '2000-12-25' BETWEEN TRF_DATE_DEBUT AND TRF_DATE_FIN
AND CHB_ID IN (1, 3, 5)

Essayez de ne jamais utiliser de BLOB (TEXT, BLOB,


CLOB...) stockez vos images, vos long textes et vos
fichiers de ressources associés directement dans les
fichiers de l'OS, cela désemcombre la base de données
qui traitera les données les plus utiles plus vite. Voir
par exemple l'article "Des images dans ma base".

Essayez de ne jamais utiliser le CASE dans les requêtes.


Utilisez les transformations en UNION ou jointures au
pire faites cette cuisine dans le code de l'interface
cliente (voir ci dessus n°13).

N'utilisez pas de type UNICODE pour vos chaînes de


caractères si votre application n'a pas d'intérêt à être
internayionalisée. En effet des colonnes UNICODE (NCHAR,
NVARCHAR) sont deux fois plus longues et coutent donc le
double en temps de traitement par rapport à des colonnes
de type CHAR ou VARCHAR...

Précisez toujours la liste de colonnes dans un ordre


INSERT.

Exemple :
-- mauvais
INSERT INTO T_CLIENT VALUES (198, 'M.', 'DUCORNET', 'Archibald', NULL)
-- bon
INSERT INTO T_CLIENT (CLI_ID, TIT_CODE, CLI_NOM, CLI_PRENOM,
CLI_ENSEIGNE)
VALUES (198, 'M.', 'DUCORNET', 'Archibald', NULL)
-- excellent (insertion implicite des NULL)
INSERT INTO T_CLIENT (CLI_ID, TIT_CODE, CLI_NOM, CLI_PRENOM)
VALUES (198, 'M.', 'DUCORNET', 'Archibald')

Remaniez vos requête de façon à lire les tables toujours


dans le même ordre (par exemple l'ordre alphabétique des
nom des tables) de façon à prévenir les verrous mortels
et les temps d'attente trop long dus à des
interblocages.

Déchargez le serveur des tâches basique que vous pouvez


aisément faire sur le client. Par exemple si vous devez
présenter l'initiale d'un nom, inutile de la demander au
serveur, rappatriez les noms et sélectionnez la première
lettre dans le code de l'interface cliente.

Evitez l'emploi systématique d'une clause ORDER BY.


Si l'ordre que vous voulez activer est complexe, ajouter
une colonne ORDRE et spécifiez le manuellement.

Exemple :

-- vous voulez présenter une liste de noms de pays ayant en premier la France
-- en second les états de l'union européenne en ordre alphabétique
-- et en troisième tous les autres pays en orde alphabétique.

CREATE TABLE T_PAYS


(PAYS VARCHAR(16),
UNION_EUROPEENE BIT(1))

INSERT INTO T_PAYS VALUES ('Allemagne', 1)


INSERT INTO T_PAYS VALUES ('Autriche', 1)
INSERT INTO T_PAYS VALUES ('Espagne', 1)
INSERT INTO T_PAYS VALUES ('France', 1)
INSERT INTO T_PAYS VALUES ('Irlande', 1)
INSERT INTO T_PAYS VALUES ('Chili', 0)
INSERT INTO T_PAYS VALUES ('Chine', 0)
INSERT INTO T_PAYS VALUES ('Japon', 0)

SELECT PAYS, 1 AS N
FROM T_PAYS
WHERE PAYS = 'France'
UNION
SELECT PAYS, 2 AS N
FROM T_PAYS
WHERE UNION_EUROPEENE = 1
AND PAYS <> 'France'
UNION
SELECT PAYS, 3 AS N
FROM T_PAYS
WHERE UNION_EUROPEENE = 0
ORDER BY N, PAYS
-- mauvais : requête lourde !
-- ajout d'une colonne d'ordre
ALTER TABLE T_PAYS ADD ORDRE INTEGER

-- ajout de l'ordre manuel


UPDATE T_PAYS SET ORDRE = 1 WHERE PAYS = 'France'
UPDATE T_PAYS SET ORDRE = 2 WHERE PAYS = 'Allemagne'
UPDATE T_PAYS SET ORDRE = 3 WHERE PAYS = 'Autriche'
UPDATE T_PAYS SET ORDRE = 4 WHERE PAYS = 'Espagne'
UPDATE T_PAYS SET ORDRE = 5 WHERE PAYS = 'Irlande'
UPDATE T_PAYS SET ORDRE = 6 WHERE PAYS = 'Chili'
UPDATE T_PAYS SET ORDRE = 7 WHERE PAYS = 'Chine'
UPDATE T_PAYS SET ORDRE = 8 WHERE PAYS = 'Japon'

SELECT PAYS, ORDRE


FROM T_PAYS
ORDER ORDRE

Utilisez UNION ALL si vous ne souhaitez pas le


dédoublonnage dans le résultat, cela pénalise moins le
serveur.

Essayez de vous affranchir de la clause HAVING par


exemple en imbriquant une sous requête dans la clause
FROM.

Un index créé sur deux colonnes ne peut pas être utilisé


pour filtrer la seconde colonne car il stocke en
principe les données sur la concaténation des deux
colonnes. Restructurez les index en évitant les index
composites.

CREATE INDEX NDX_CLI_NOM_PRENOM ON T_CLIENT (CLI_NOM,


CLI_PRENOM)

SELECT *
FROM T_CLIENT
ORDRE BY PRENOM
-- l'index ne peut être activé sur la seule colonne CLI_PRENOM car il contient :
AIACHAlexandre
ALBERTChristian
AUZENATMichel
BACQUEMichel
BAILLYJean-François
...Activez le calculs des statistiques après des mises à
jour massives.

Faites des transactions les plus courtes possible,


validez vos transactions dès que possible, mettez des
points de sauvegarde partiels.

Lorsqu'une requête devient trop importante, pensez à en


faire une procédure stockée paramétrée, surtout si votre
SGBDR prépare les requêtes.

Évitez autant que faire ce peut d'utiliser des CURSOR.


Préférez des requêtes mêmes complexes. Bannissez les
CURSOR pacourus en arrière (une clause ORDER BY ... DESC
le remplace aisément). Interdisez-vous les CURSOR
parcourus par bond de plus d'une ligne.

CE QUI FAIT GAGNER DU TEMPS...

Évitez les gros pièges


Ajoutez de l'information : des tables, des vues, des
index pour faciliter le requêtage
11. CONCLUSION
Désolé, il n'y a pas de recettes miracle pour
l'optimisation des requêtes. Simplement quelques grosses
fautes à éviter. L'essentiel est un bon paramétrage de
la machine et son environnement, du serveur et de l'OS.
Le reste est spécifique à chaque SGBDR et nécessite un
peu d'huile de coude et beaucoup de prudence.
Mais n'oubliez surtout pas que des tests en charge sont
indispensables afin de trancher entre telle ou telle
expression de requête avec le volume probable de votre
base en exploitation.

LES CLAUSES GROUP BY ET LES JOINTURES SQL

1. Numéroter des lignes et toutes les requêtes qui en


découlent
Soit la table T_CLIENT_CLI (CLI_ID, CLI_NOM) comme suit
:

CLI_ID CLI_NOM
------- ------------
17 DURAND
192 DUPONT
44 DUVAL
11 DUMOULIN
741 DULIN
82 DUPOND
177 DURANDCréation du jeu d'essai :

CREATE TABLE T_CLIENT_CLI


(CLI_ID INTEGER,
CLI_NOM VARCHAR(10))

INSERT INTO T_CLIENT_CLI (CLI_ID, CLI_NOM)


VALUES (17, 'DURAND')
INSERT INTO T_CLIENT_CLI (CLI_ID, CLI_NOM)
VALUES (192, 'DUPONT')
INSERT INTO T_CLIENT_CLI (CLI_ID, CLI_NOM)
VALUES (44, 'DUVAL')
INSERT INTO T_CLIENT_CLI (CLI_ID, CLI_NOM)
VALUES (11, 'DUMOULIN')
INSERT INTO T_CLIENT_CLI (CLI_ID, CLI_NOM)
VALUES (741, 'DULIN')
INSERT INTO T_CLIENT_CLI (CLI_ID, CLI_NOM)
VALUES (82, 'DUPOND')
INSERT INTO T_CLIENT_CLI (CLI_ID, CLI_NOM)
VALUES (177, 'DURAND')La question est : comment obtenir en réponse les noms de
nos clients par ordre alphabétique avec leur rang
(depuis 1, jusqu'à n) ?

1.1. RÉPONSE 1
Si l'on applique strictement cette question, alors la
réponse est :

CLI_NOM RANG
------------- -----
DULIN 1
DUMOULIN 2
DUPOND 3
DUPONT 4
DURAND 5
DURAND 5
DUVAL 7En effet, les deux DURAND se trouvant ex æquo occupent
le 5eme rang, tandis que DUVAL occupe non pas le 6eme,
mais le 7eme rang !

1.2. RÉPONSE 2
Une autre solution possible est :

CLI_NOM RANG
------------- -----
DULIN 1
DUMOULIN 2
DUPOND 3
DUPONT 4
DURAND 5
DURAND 6
DUVAL 7C'est à dire une numérotation franche et directe sans
tenir compte des doublons ou de l'ambiguïté des
informations sélectionnées. C'est un peu ce que font les
colonnes d'auto incrémentation de certains SGBDR.

1.3. RÉPONSE 3
Enfin on peut raffiner cette dernière solution en
introduisant un comptage pour faire disparaître les
doublons :

CLI_NOM RANG NOMBRE


------------- ----- ------
DULIN 1 1
DUMOULIN 2 1
DUPOND 3 1
DUPONT 4 1
DURAND 5 2
DUVAL 7 1Qui a le mérite d'être plus propre sans correspondre
toutefois à la demande initiale !

1.4. RÉPONSE 4
En poussant les choses à l'extrême, on peut exiger que
la numérotation de rang soit stricte et sans "trou",
comme ceci :

CLI_NOM RANG NOMBRE


------------- ----- ------
DULIN 1 1
DUMOULIN 2 1
DUPOND 3 1
DUPONT 4 1
DURAND 5 2
DUVAL 6 1Mais quelle est sont les requêtes nécessaires pour
parvenir aux différentes solutions proposées ?

1.5. Requêtes associées


Dans le principe, les requêtes pour répondre à ce genre
de demande nécessitent une auto non équi jointure afin
de faire le dénombrement de tuples dont les valeurs
précèdent le tuple en cours.

Requête de la réponse 1 :

SELECT T1.CLI_NOM, COUNT(T2.CLI_ID) + 1 AS RANG


FROM T_CLIENT_CLI T1
LEFT OUTER JOIN T_CLIENT_CLI T2
ON T1.CLI_NOM > T2.CLI_NOM
GROUP BY T1.CLI_ID, T1.CLI_NOM
ORDER BY RANGRequête de la réponse 2 :

SELECT T1.CLI_NOM, COUNT(T2.CLI_ID) + 1 AS RANG


FROM T_CLIENT_CLI T1
LEFT OUTER JOIN T_CLIENT_CLI T2
ON T1.CLI_NOM || CAST(T1.CLI_ID AS CHAR(16))
> T2.CLI_NOM || CAST(T2.CLI_ID AS CHAR(16))
GROUP BY T1.CLI_ID, T1.CLI_NOM
ORDER BY RANGRequête de la réponse 3 :

SELECT T1.CLI_NOM, RANG, NOMBRE


FROM (SELECT DISTINCT T1.CLI_NOM, COUNT(T2.CLI_ID) + 1 AS RANG
FROM T_CLIENT_CLI T1
LEFT OUTER JOIN T_CLIENT_CLI T2
ON T1.CLI_NOM > T2.CLI_NOM
GROUP BY T1.CLI_ID, T1.CLI_NOM)
T1
INNER JOIN (SELECT CLI_NOM, COUNT(CLI_ID) AS NOMBRE
FROM T_CLIENT_CLI T1
GROUP BY CLI_NOM) T2
ON T1.CLI_NOM = T2.CLI_NOM
ORDER BY RANGRequête de la réponse 4 :

SELECT T2.CLI_NOM, COUNT(T1.CLI_NOM) AS RANG, NOMBRE


FROM (SELECT DISTINCT CLI_NOM
FROM T_CLIENT_CLI) T1
LEFT OUTER JOIN (SELECT DISTINCT CLI_NOM
FROM T_CLIENT_CLI) T2
ON T1.CLI_NOM <= T2.CLI_NOM
INNER JOIN (SELECT CLI_NOM, COUNT(CLI_ID) AS NOMBRE
FROM T_CLIENT_CLI
GROUP BY CLI_NOM) T3
ON T2.CLI_NOM = T3.CLI_NOM
GROUP BY T2.CLI_NOM, T3.NOMBRE
ORDER BY RANGLe moins que l'on puisse dire, c'est que ce genre de
requêtes ne vient pas à l'esprit du débutant. Que dire
alors du développeur confronté à ce problème dans un
contexte de tables bien plus étoffées que celle de notre
exemple ? Il y a fort à parier que ce dernier passe la
main et se fend d'une jolie procédure au mieux stockée,
au pire sur le poste client !

2. Affecter des lignes à des places


La deuxième famille de problèmes qui mérite notre
attention dans ce cadre, concerne les problèmes
d'affectation, problèmes chers à tous les enseignants en
début d'année scolaire par exemple. La question est :
partant d'une table constituant une population et d'une
autre constituée de place (chaque place étant destinée à
recevoir un élève, un spectateur…) comment assigner une
place à chaque élément de la population ?

Reprenons notre table des clients et ajoutons une table


des fauteuils modélisant les places d'un théâtre
T_PLACE_PLC (PLC_REF). Les places de théâtre étant
numérotées comme vous le savez par des lettres (le rang)
et des chiffres (ordre dans le rang). Pour notre
démonstration nous nous limiterons à trois rangs et 5
places par rang, c'est à dire un théâtre de poche !

PLC_REF
-------
A01
A02
A03
A04
A05
B01
B02
B03
B04
B05
C01
C02
C03
C04
C05Création du jeu d'essai :

CREATE TABLE T_PLACE_PLC


(PLC_REF CHAR(3))

INSERT INTO T_PLACE_PLC (PLC_REF) VALUES ('A01')


INSERT INTO T_PLACE_PLC (PLC_REF) VALUES ('A02')
INSERT INTO T_PLACE_PLC (PLC_REF) VALUES ('A03')
INSERT INTO T_PLACE_PLC (PLC_REF) VALUES ('A04')
INSERT INTO T_PLACE_PLC (PLC_REF) VALUES ('A05')
INSERT INTO T_PLACE_PLC (PLC_REF) VALUES ('B01')
INSERT INTO T_PLACE_PLC (PLC_REF) VALUES ('B02')
INSERT INTO T_PLACE_PLC (PLC_REF) VALUES ('B03')
INSERT INTO T_PLACE_PLC (PLC_REF) VALUES ('B04')
INSERT INTO T_PLACE_PLC (PLC_REF) VALUES ('B05')
INSERT INTO T_PLACE_PLC (PLC_REF) VALUES ('C01')
INSERT INTO T_PLACE_PLC (PLC_REF) VALUES ('C02')
INSERT INTO T_PLACE_PLC (PLC_REF) VALUES ('C03')
INSERT INTO T_PLACE_PLC (PLC_REF) VALUES ('C04')
INSERT INTO T_PLACE_PLC (PLC_REF) VALUES ('C05')Le problème est on ne peut
plus simple : comment
affecter les clients aux premiers sièges ?

Il semble à l'évidence que le plus simple serait de


numéroter les lignes des clients puis les lignes des
sièges et d'effectuer une jointure avec cette
numérotation.
Quelque chose comme :

CLI_ID CLI_NOM CLI_NUM


------- ------------ -------
17 DURAND 1
192 DUPONT 2
44 DUVAL 3
11 DUMOULIN 4
741 DULIN 5
82 DUPOND 6
177 DURAND 7PLC_REF PLC_NUM
-------- -------
A01 1
A02 2
A03 3
A04 4
A05 5
B01 6
B02 7
B03 8
B04 9
B05 10
C01 11
C02 12
C03 13
C04 14
C05 15

Dès lors, la solution saute aux yeux :

SELECT CLI_NOM, PLC_REF


FROM T_CLIENT_CLI
JOIN T_PLACE_PLC
ON CLI_NUM = PLC_NUMQui donne :

CLI_NOM PLC_REF
------------ -------
DURAND A01
DUPONT A02
DUVAL A03
DUMOULIN A04
DULIN A05
DUPOND B01
DURAND B02Néanmoins nous n'avons pas ces colonnes à disposition…
Comment faire ?
Il suffit d'appliquer ce que nous venons de voir dans
l'exemple précédent, à la fois pour les clients, mais
aussi pour les fauteuils et de joindre le tout sur les
colonnes de numérotation ainsi générées.

Je vous livre la requête telle quelle, sa mise au point


étant assez joyeuse !!!

SELECT CLI_NOM, PLC_REF


FROM (SELECT T1.CLI_NOM, COUNT(T2.CLI_ID) + 1 AS RANG
FROM T_CLIENT_CLI T1
LEFT OUTER JOIN T_CLIENT_CLI T2
ON T1.CLI_NOM || CAST(T1.CLI_ID AS CHAR(16))
> T2.CLI_NOM || CAST(T2.CLI_ID AS CHAR(16))
GROUP BY T1.CLI_ID, T1.CLI_NOM) C
INNER JOIN (SELECT T3.PLC_REF, COUNT(T4.PLC_REF) + 1 AS RANG
FROM T_PLACE_PLC T3
LEFT OUTER JOIN T_PLACE_PLC T4
ON T3.PLC_REF > T4.PLC_REF
GROUP BY T3.PLC_REF) P
ON C.RANG = P.RANGEt encore avons nous tenu compte que la colonne
PLC_REF
est une clef candidate de la table T_PLACE_PLC...

3. La solution, la jointure linéaire !


La condition primale est de disposer d'une table très
simple dotée d'une seule colonne et remplie avec la
suite des nombres entiers : T_I_ENT (ENT_I). Bien
entendu on se limitera par exemple à une plage allant de
0 à 1000 voire plus selon ses besoins :

ENT_I
-------
0
1
2
3
4
5
6
7
8
9
10
...CREATE TABLE T_I_ENT
(ENT_I INTEGER)

INSERT INTO T_I_ENT (ENT_I) VALUES (0)


INSERT INTO T_I_ENT (ENT_I) VALUES (1)
INSERT INTO T_I_ENT (ENT_I) VALUES (2)
INSERT INTO T_I_ENT (ENT_I) VALUES (3)
INSERT INTO T_I_ENT (ENT_I) VALUES (4)
INSERT INTO T_I_ENT (ENT_I) VALUES (5)
INSERT INTO T_I_ENT (ENT_I) VALUES (6)
INSERT INTO T_I_ENT (ENT_I) VALUES (7)
INSERT INTO T_I_ENT (ENT_I) VALUES (8)
INSERT INTO T_I_ENT (ENT_I) VALUES (9)
INSERT INTO T_I_ENT (ENT_I) VALUES (10)
...
INSERT INTO T_I_ENT (ENT_I) VALUES (1000)Notons au passage qu'il est inutile de
saisir tous les
nombres de 1 à 1000, les dix premiers suffisent et une
simple requête d'insertion jouera le rôle d'insertion
complémentaire :

INSERT INTO T_I_ENT (ENT_I)


SELECT DISTINCT I1.ENT_I + (I2.ENT_I * 10) + (I3.ENT_I * 100)
FROM T_I_ENT I1
CROSS JOIN T_I_ENT I2
CROSS JOIN T_I_ENT I3
CROSS JOIN T_I_ENT I4
WHERE I1.ENT_I + (I2.ENT_I * 10) + (I3.ENT_I * 100) BETWEEN 0 AND 1000Dès
lors la juxtaposition de la projection du nom de la
table client ordonné par client avec la projection de la
table des entiers ordonnés répond à notre attente :

CLI_NOM T_I_ENT
---------- ------------
DULIN 1
DUMOULIN 2
DUPOND 3
DUPONT 4
DURAND 5
DURAND 6
DUVAL 7C'est pourquoi je propose le nouvel opérateur de
jointure linéaire : LINEAR JOIN permettant de faire
correspondre à la ligne de rang un de la table de
gauche, la ligne de rang un + offset de la table de
droite et ainsi de suite.

4. Syntaxe et règles de la jointure linéaire


La jointure linéaire répond à la syntaxe suivante :

SELECT <liste de sélection>


FROM <table gauche>
[LEFT | RIGHT] LINEAR JOIN <table droite> [OFFSET <offset value>]Dans notre
précédent exemple, il suffit donc de faire :

SELECT CLI_NOM, T_I_ENT


FROM T_CLIENT_CLI
LINEAR JOIN T_I_ENT OFFSET 1 /* élimination de la première ligne, un zéro
*/
ORDER BY CLI_NOM, T_I_ENTQuelques explications :

le mot clef OFFSET permet d'indiquer à partir de


quelle ligne prendre en compte la première ligne de la
table de droite associée à la première ligne de la
table de gauche
la clause ORDER BY opère séparément avant la jointure
linéaire pour les tables jointes
la jointure peut être externe à gauche (LEFT LINEAR
JOIN) ou a droite (RIGHT LINEAR JOIN), mais pas des
deux côtés. Par défaut elle est interne
Exemple de jointure linéaire externe droite :

SELECT CLI_NOM, T_I_ENT


FROM T_CLIENT_CLI
RIGHT LINEAR JOIN T_I_ENT
ORDER BY CLI_NOM, T_I_ENTqui donne :

CLI_NOM T_I_ENT
---------- ------------
DULIN 0
DUMOULIN 1
DUPOND 2
DUPONT 3
DURAND 4
DURAND 5
DUVAL 6
NULL 7
NULL 8
...
NULL 1000Pour résoudre notre problème d'affectation des places de
théâtre, il suffit de faire :

SELECT CLI_NOM, PLC_REF


FROM T_CLIENT_CLI
LINEAR JOIN T_I_ENT
LINEAR JOIN T_PLACE_PLC
ORDER BY CLI_NOM, T_I_ENT, PLC_REFJe ne sais pas ce que vous en pensez mais
je trouve
cette écriture plus simple et facile à comprendre !

5. CONCLUSION
Ces requêtes s'apparentent aux T-JOIN (théta jointures)
du Docteur Codd en vue d'obtenir une correspondance
optimale des inégalités (typiquement le problème
d'affectation des élèves dans des salles de capacités
données).
Je laisse à votre sagacité la représentation d'une telle
jointure en algèbre relationnelle !

Comment limiter le nombre de lignes retounées par


une commande SELECT ?[haut]

Exemple:

--Retouner 100 lignes d'une table T_Client


SELECT TOP 100 *
FROM t_client

OU

SET ROWCOUNT 100


GO
SELECT * FROM t_client
GO
SET ROWCOUNT 0
GO

Comment implémenter la fonction LIMIT de MySQL en


SQLServer ?[haut]

auteur : Wolo Laurent


Le SGBD MySQL fournit une fonctionalité intéressante
dans les SELECT : LIMIT.

SELECT * FROM MATABLE LIMIT 10, 30

Affiche 30 lignes à partir de l'enregistrement 10. Voici


une solution pour implémenter cette fonctionalité en SQL
:

SELECT * FROM (
SELECT TOP 10 Field1, Field2 FROM (
SELECT TOP 30 Field1, Field2
FROM matable
ORDER BY monchamp asc
) AS tbl1 ORDER BY monchamp desc
) AS tbl2 ORDER BY monchamp asc

et une petite fonction php pour généraliser, a améliorer


selon vos besoins :

function getLimitMSSQL($start, $nbrows, $fields, $table, $where, $orderfield, $sort =


'asc') {
$top = $start + $nbrows ;
if ( $sort == 'asc' ) {
$asc = 'asc' ;
$desc = 'desc' ;
} else {
$asc = 'desc' ;
$desc = 'asc' ;
}
$sql = '' ;
$sql = "SELECT * FROM (
SELECT TOP $nbrows $fields from (
SELECT TOP $top $fields
FROM $table
$where
ORDER BY $orderfield $asc
) tbl1 ORDER BY $orderfield $desc
) as tbl2 order by $orderfield $asc
";
return $sql ;
}
Comment connaître le nombre d'enregistrements
retournés par une requête SELECT, UPDATE, INSERT ,
DELETE ?[haut]

auteur : Fabien Celaia


En interrogeant la variable globale @@rowcount,
directement après l'appel de la requête

Comment supprimer la ligne informationnelle du


total de lignes impactées ?[haut]

auteur : Fabien Celaia

set nocount on

Comment réactiver la ligne informationnelle du


total de lignes impactées ?[haut]

auteur : Fabien Celaia

set nocount off

Comment ne traiter que les n premières lignes d'un


buffer ?[haut]

auteur : Fabien Celaia

set rowcount n

Comment retraiter toutes les lignes après un set


rowcount n ?[haut]

auteur : Fabien Celaia

Set rowcount 0
Comment récupérer le résultat d'une requete
dynamique ?[haut]

auteur : HULK
Si la requête dynamique ne retourne qu'une seule valeur

CREATE PROCEDURE RecupVariable AS


DECLARE @VAR AS VARCHAR(10)
SET @VAR = (SELECT champ FROM Table WHERE ..)
PRINT @VAR

Si elle retourne plusieurs valeurs ou plusiqeurs lignes,


passer par une table de travail

CREATE PROCEDURE RecupVariable @PARAM INT


AS
DECLARE @VAR AS VARCHAR(10), @QUERY AS VARCHAR(150)
SET @QUERY = 'SELECT champ1 INTO TableTempo FROM Table WHERE champ2 =
' + CAST(@PARAM AS VARCHAR(5))
EXEC(@QUERY)
SET @VAR = (SELECT Champ1 FROM TableTempo)
DROP TABLE TableTempo
PRINT @VAR

Comment puis je récupérer une liste / un tableau


en sql[haut]

auteur : Rudi Bruchez


deux solutions :

un grand varchar avec un délimiteur, que l'on parse


dans une procédure stockée, par exemple à l'aide d'une
fonction qui retourne une table.
une structure XML que l'on envoie dans un grand
varchar, ou un TEXT, et que l'on convertit en table
dans une procédure stockée avec OPENXML.
Pour un XML du genre:

<Elements>
<Element>
<Key/>
<Value/>
</Element>
</Elements>

Coder comme ceci

DECLARE @idoc int


DECLARE @tField TABLE (FieldName varchar(100), Value varchar(100))

EXEC master.dbo.sp_xml_preparedocument @idoc OUTPUT, @KeyValue

INSERT INTO @tField (FieldName, Value)


SELECT FieldName, Value
FROM OPENXML(@idoc, '/Elements/Element', 2) WITH (FieldName
varchar(100) 'Key', Value varchar(100))

.. ou sans passer par une variable table, directement


dans un JOIN.

En étant conscient que les performances du OPENXML


peuvent poser problème sur un serveur très sollicité. A
tester.

lien : Exemple

Comment générer un fichier image (.JPG, .GIF) à


partir d'un champ varbinary ?[haut]

auteur : davidou2001
Voici le code pour réaliser ceci

-- Le fichier binaire
DECLARE @s varbinary(2048)
SET @s = 0x47494...code hexa du fichier

DECLARE @o int
DECLARE @r int
EXEC sp_oacreate 'adodb.stream', @o output
EXEC sp_oasetproperty @o, 'type', 2
EXEC sp_oamethod @o, 'open'
EXEC sp_oamethod @o, 'writetext', NULL, @s

EXEC sp_oacreate 'adodb.stream', @r output


EXEC sp_oasetproperty @r, 'type', 1
EXEC sp_oamethod @r, 'open'
EXEC sp_oasetproperty @o, 'position', 2
EXEC sp_oamethod @o, 'copyto', NULL, @r
EXEC sp_oamethod @r, 'savetofile', NULL, @basedir, 2

On peut utiliser la primitive READTEXT pour obtenir le


code hexa d'une colonne image, par exemple.

Les jointures ANSI sont-elle similaires aux


jointures faites via conditions ?[haut]

SQL Server n'est pas compatible avec la norme SQL sur la


jointure externe avec *=. Il ne donne pas les mêmes
résultats...

CREATE TABLE T1 (C1 INT)

INSERT INTO T1 VALUES (1)


INSERT INTO T1 VALUES (0)
INSERT INTO T1 VALUES (1)

CREATE TABLE T2 (C2 INT)

INSERT INTO T2 VALUES (2)


INSERT INTO T2 VALUES (0)
INSERT INTO T2 VALUES (2)

CREATE TABLE T3 (C3 INT)

INSERT INTO T3 VALUES (3)


INSERT INTO T3 VALUES (0)
INSERT INTO T3 VALUES (3)

SELECT *
FROM T1
LEFT OUTER JOIN T2 ON T1.C1 = T2.C2
LEFT OUTER JOIN T3 ON T1.C1 = T3.C3

C1 C2 C3
----------- ----------- -----------
1 NULL NULL
0 0 0
1 NULL NULL

SELECT *
FROM T1, T2, T3
WHERE C1 *= C2 AND C1 *= C3

C1 C2 C3
----------- ----------- -----------
1 NULL NULL
0 0 0
1 NULL NULL

-- exact !

SELECT *
FROM T1
LEFT OUTER JOIN T2 ON T1.C1 = T2.C2
LEFT OUTER JOIN T3 ON T2.C2 = T3.C3

C1 C2 C3
----------- ----------- -----------
1 NULL NULL
0 0 0
1 NULL NULL

SELECT *
FROM T1, T2, T3
WHERE C1 *= C2
AND C2 *= C3

-- Serveur : Msg 301, Niveau 16, État 1, Ligne 1


-- La requête comporte une requête de jointure externe qui n'est pas autorisée.
-- SQL Server ne sait pas faire des jointures externes en cascade...

SELECT *
FROM T1
INNER JOIN (SELECT *
FROM T2) AS T
ON T1.C1 = T.C2
RIGHT OUTER JOIN T3 ON T.C2 = T3.C3

C1 C2 C3
----------- ----------- -----------
NULL NULL 3
0 0 0
NULL NULL 3

SELECT *
FROM T1, (SELECT * FROM T2) AS T, T3
WHERE C1 = C2
AND C2 =* C3

-- Serveur : Msg 303, Niveau 16, État 1, Ligne 1


-- La table 'T2' est un membre interne d'une clause de jointure externe. Cela n'est pas
autorisé si la table participe aussi à une clause JOIN régulière.
-- SQL Server ne sait pas faire des jointures externes en cascade...

SELECT *
FROM T1
RIGHT OUTER JOIN T2 ON T1.C1 = T2.C2
RIGHT OUTER JOIN T3 ON T1.C1 = T3.C3

C1 C2 C3
----------- ----------- -----------
NULL NULL 3
0 0 0
NULL NULL 3

SELECT *
FROM T1, T2, T3
WHERE C1 =* C2
AND C1 =* C3

C1 C2 C3
----------- ----------- -----------
NULL 2 3
NULL 2 0
NULL 2 3
NULL 0 3
0 0 0
NULL 0 3
NULL 2 3
NULL 2 0
NULL 2 3

-- résultat totalement faux !!!


Sommaire > Administration de la base de données > Connexions
au serveur
Comment fixer la durée d'attente de libération d'un
verrou sur un object de la base de données ?
Je n'arrive pas à me connecter à une base de données
de mon serveur depuis un programme client
Comment se connecter à un serveur SQL qui se trouve
derrière un proxy ?
Comment se connecter à un serveur se trouvant derrière
un pare-feu ?
Quelle procédure stockée permet de limiter le nombre
de connexions simultanées ?
Mon serveur ne démarre pas à cause du méssage : Echec
d'ouverture d'une session a empêché le démarrage d'un service.
Comment connaître le type d'authentification installée
sur le serveur ?
Comment resoudre le problème 'Délai d'attente expiré'
lorsqu'on tente de se connecter au serveur ?
Déconnectez les utilisateurs d'une base de données
Comment démarrer un serveur si la base tempdb est
corrompue

Comment fixer la durée d'attente de libération


d'un verrou sur un object de la base de données
?[haut]

auteur : Wolo Laurent


SET LOCK_TIMEOUT permet à une application de définir le
délai maximal pendant lequel
une instruction doit attendre en cas de ressource
bloquée. Si l'attente d'une instruction
dépasse la valeur du paramètre LOCK_TIMEOUT,
l'instruction bloquée est automatiquement
annulée, et un message d'erreur renvoyé à l'application.
Cette valeur est fixée à -1 au début d'une connexion.
--Fixe la valeur du lock_timeout à 1,8 seconde
SET LOCK_TIMEOUT 1800

Pour connaître la valeur courante, on utilise la


variable @@LOCK_TIMEOUT

--Consulter la valeur du lock timeout


SELECT @@LOCK_TIMEOUT AS LOCK_TIMEOUT

Je n'arrive pas à me connecter à une base de


données de mon serveur depuis un programme
client[haut]

auteur : Wolo Laurent


Si vous n'arrivez pas à vous connecter à une base de
données de votre serveur, procedez comme ci-dessous.
Si vous utilisez le nom de la machine comme source de
données, vérifiez que votre réseau
possède un serveur de nom de domaine.
Dans le cas contraire, utilisez l'adresse IP publique
de votre serveur de base de données.

Attention, il arrive que vous vous connectez sous


windows avec un utilisateur qu possède moins de droits
pour modifier les données de la base de données de ce
serveur.

Vérifiez que vous utilisez le bon type


d'autentification SQL Serveur pour votre chaîne de
connexion

Vérifiez que votre serveur ne se trouve pas derrière


un serveur proxy.
Vérifier que votre serveur ne se trouve pas derrière
un Firewall.

Comment se connecter à un serveur SQL qui se


trouve derrière un proxy ?[haut]

auteur : Wolo Laurent


Pour se connecter à un serveur SQL Serveur qui tourne
derrière un serveur proxy,
Ouvrez l'utilitaire Server Network Utility
Dans l'onglet général, cochez la case enable Winsock
proxy
puis saisissez l'adresse du serveur proxy et son port
d'écoute.

Comment se connecter à un serveur se trouvant


derrière un pare-feu ?[haut]

auteur : Wolo Laurent


Pour se connecter à un serveur dans ces conditions,
Configurez un numero de port (1433) par défaut puis
demandez à votre administrateur réseau de libérer le
trafique de ce port sur son firewall

il est déconseillé d'ouvrir une brèche dans le firewall


si MS-SQL utilise le port par défaut. reconfigurer le
port avant tout.

Quelle procédure stockée permet de limiter le


nombre de connexions simultanées ?[haut]

auteurs : drahu, Fabien Celaia

sp_configure connections, n
RECONFIGURE WITH OVERRIDE
--n est le nombre de connexions souhaitées.

Mon serveur ne démarre pas à cause du méssage :


Echec d'ouverture d'une session a empêché le
démarrage d'un service.[haut]

auteur : Wolo Laurent


Ce méssage intervient lorsque le compte configuré pour
démarrer le service MSSQLSERVER échoue dans le démarrage
d'une session Windows. Allez-y dans le Gestionnaire des
Services, Cliquez sur le service MSSQLSERVER, puis
changer le compte de démarrage du service. NB: Il est
plus simple d'utiliser le compte LocalSystem.

Comment connaître le type d'authentification


installée sur le serveur ?[haut]

auteur : Wolo Laurent

select CASE
WHEN convert(sysname, serverproperty('Edition')) IS NULL THEN 'ERREUR'
WHEN convert(sysname, serverproperty('Edition'))=0 THEN 'SECURITE INTEGREE'
WHEN convert(sysname, serverproperty('Edition'))=1 THEN 'SECURITE NON
INTEGREE'
END AS AUTHENTIFICATION

Comment resoudre le problème "Délai d'attente


expiré" lorsqu'on tente de se connecter au serveur
?[haut]

auteur : Wolo Laurent


Il s'agit d'un problème lié au DNS
Utiliser l'adresse IP\Instance du serveur à la place
de NomServeur\Instance
Dans la fenêtre de configuration IP du protocole
TCP/IP de votre carte réseau, ajouter l'adresse du DNS
primaire, en utilisant celui de votre contôleur de
domaine.

Déconnectez les utilisateurs d'une base de


données[haut]

auteurs : Frédéric Brouard, Fabien Celaia

ALTER DATABASE 'MaBase' SET SINGLE_USER WITH ROLLBACK IMMEDIATE

ALTER DATABASE 'MaBase' SET MULTI_USER

CREATE PROC sp_killuser (@MaBase varchar(30)=NULL)


AS
BEGIN
Declare @SQL_Text nvarchar(20)

declare Process_cur CURSOR FOR


select convert(varchar(5), spid)
from master..sysprocesses
where dbid = db_id(coalesce(@MaBase,db_name())
and spid <> @@spid

Declare @spid as varchar(4)

OPEN Process_cur

FETCH NEXT FROM Process_cur INTO @spid

WHILE (@@FETCH_STATUS = 0)
begin
set @SQL_Text = 'KILL '+ @spid
exec sp_executesql @T
FETCH NEXT FROM Process_cur into @spid;
end

CLOSE Process_cur
DEALLOCATE Process_cur
END
GO

Le SQL de A à Z - Fonctions SQL - Club d'entraide des développeurs francophones

1. Les fonctions dans SQL


1.1. Agrégation statistique
1.2. Fonction "système"
1.3. Fonctions générales
1.4. Fonctions de chaînes de caractères
1.5. Fonctions de chaînes de bits
1.6. Fonctions numériques
1.7. Fonctions temporelles
1.8. Prédicat, opérateurs et structures diverses
1.9. Sous requêtes
1. Les fonctions dans SQL
Légende :

O : Oui
N : Non
X : Existe mais syntaxe hors norme
! : Même nom mais fonction différente

1.1. Agrégation statistique


FonctionDescriptionNorme
SQLParadoxAccessMySQLPostGreSQLSQL
ServerOracleInterbase
AVGMoyenneOOOOOOOO
COUNTNombreOOXOOOOO
MAXMaximumOOOOOOOO
MINMinimumOOOOOOOO
SUMTotalOOOOOOOO

1.2. Fonction "système"


FonctionDescriptionNorme
SQLParadoxAccessMySQLPostGreSQLSQL
ServerOracleInterbase
CURRENT_DATEDate couranteONNOONNO
CURRENT_TIMEHeure couranteONNOONNO
CURRENT_TIMESTAMPDate et heure couranteONNOOONO
CURRENT_USERUtilisateur courantONNNOONN
SESSION_USERUtilisateur autoriséONNXOONN
SYSTEM_USERUtilisateur systèmeONNXOONN
CURDATEDate du jourNNNONNNN
CURTIMEHeure couranteNNNONNNN
DATABASENom de la bases de données
couranteNNNONOON
GETDATEHeure et date couranteNNNNNONN
NOWHeure et date couranteNOOOOOON
SYSDATEDate et/ou heure couranteNNNONNON
TODAYDate du jourNONNNNNN
USERUtilisateur courantNNNONOOO
VERSIONVersion du SGBDRNNNOONNN

1.3. Fonctions générales


FonctionDescriptionNorme
SQLParadoxAccessMySQLPostGreSQLSQL
ServerOracleInterbase
CASTTranstypageOONOOOOO
COALESCEValeur non NULLONNOOONN
NULLIFValeur NULLONNOOONN
OCTET_LENGTHLongueur en octetONNOONON
DATALENGTHLongueurNNNNNONN
DECODEFonction conditionnelleNNNNNNON
GREATESTPlus grande valeurNNNONNON
IFNULLValeur non NULLNNNOOONN
LEASTPlus petite valeurNNNNONON
LENGTHLongueurNNOOOOON
NVLValeur non NULLNNNNNNON
TO_CHARConversion de données en chaîneNNNNNNON
TO_DATEConversion en dateNNNNONON
TO_NUMBERConversion en nombreNNNNNNON

1.4. Fonctions de chaînes de caractères


FonctionDescriptionNorme
SQLParadoxAccessMySQLPostGreSQLSQL
ServerOracleInterbase
||ConcaténationOONXONOO
CHAR_LENGTHLongueur d'une chaîneONNXONNN
CHARACTER_LENGTHLongueur d'une chaîneONNOOONN
COLLATESubstitution à une séquence de
caractèresONNNNNNO
CONCATENATEConcaténationONNNNONN
CONVERTConversion de format de caractèresONNNN!OO
LIKE (prédicat)Comparaison partielleOOXOOOOO
LOWERMise en minusculeOONOOOON
POSITIONPosition d'une chaîne dans une sous
chaîneONNOONNN
SUBSTRINGExtraction d'une sous chaîneOONOONNN
TRANSLATEConversion de jeu de caractèresONNNXNXN
TO_CHARConversion de données en chaîneNNNNNNON
TRIMSuppression des caractères inutilesOONOONON
UPPERMise en majusculeOONOOOOO
CHARConversion de code en caractère ASCIINNOONONN
CHAR_OCTET_LENGTHLongueur d'une chaîne en
octetsNNNNNONN
CHARACTER_MAXIMUM_LENGTHLongueur maximum d'une
chaîneNNNNNONN
CHARACTER_OCTET_LENGTHLongueur d'une chaîne en
octetsNNNNNONN
CONCATConcaténationNNOONOON
ILIKELIKE insensible à la casseNNNNONNN
INITCAPInitiales en majusculeNNNNONON
INSTRPosition d'une chaîne dans une autreNNOONNON
LCASEMise en minusculeNNOONOON
LOCATEPosition d'une chaîne dans une autreNOOONOON
LPADRemplissage à gaucheNNNOONON
LTRIMTRIM à gaucheNOOOOOON
NCHARConversion de code en caractère
UNICODENNNNNONN
PATINDEXPosition d'un motif dans une
chaîneNNNNNONN
REPLACERemplacement de caractèresNNNONOON
REVERSERenversementNNNONOON
RPADRemplissage à droiteNNNOONON
RTRIMTRIM à droiteNNOOOOON
SPACEGénération d'espacesNNOONOON
SUBSTRExtraction d'une sous chaîneNNNNNNON
UCASEMise en majusculeNNOONOON

1.5. Fonctions de chaînes de bits


FonctionDescriptionNorme
SQLParadoxAccessMySQLPostGreSQLSQL
ServerOracleInterbase
BIT_LENGTHLongueur en bitONNNNNNN
&"et" pour bit logique NN???O??
|"ou" pour bit logiqueNN???O??
^"ou" exclusif pour bit logiqueNN???O??

1.6. Fonctions numériques


FonctionDescriptionNorme
SQLParadoxAccessMySQLPostGreSQLSQL
ServerOracleInterbase
%ModuloNNNOOONN
+ - * / ( )Opérateurs et parenthésageOOOOOOOO
ABSValeur absolueNNOOOOON
ASCIIConversion de caractère en code ASCIINNOOOOON
ASINAngle de sinusNNNOOOON
ATANAngle de tangenteNNNOOOON
CEILINGValeur approchée hauteNNOONONN
COSCosinusNNOOOOON
COTCotangenteNNOOOONN
EXPExponentielleNNOOOOON
FLOORValeur approchée basseNNOOOOON
LNLogarithme népérienNNNNNNON
LOGLogarithme népérienNNOONOON
LOG(n,m)Logarithme en base n de mNNNNONON
LOG10Logarithme décimalNNNONOON
MODModuloNNOOOOON
PIPiNNNOOOON
POWERElévation à la puissanceNNOONOON
RANDValeur aléatoireNNOONONN
ROUNDArrondiNNOOOONN
SIGNSigneNNOOOOON
SINSinusNNOOOOON
SQRTRacine carréeNNOOOONN
TANTangenteNNOOOOON
TRUNCTroncatureNNNNNNON
TRUNCATETroncatureNN OOOON
UNICODEConversion de caractère en code
UNICODENNNNNO?N

1.7. Fonctions temporelles


FonctionDescriptionNorme
SQLParadoxAccessMySQLPostGreSQLSQL
ServerOracleInterbase
EXTRACTPartie de dateOONOONON
INTERVAL (opérations sur)DuréeONNNNNON
OVERLAPS (prédicat)Recouvrement de périodeONNNONNN
ADDDATEAjout d'intervalle à une dateNNNONNNN
AGEAgeNNNNONNN
DATE_ADDAjout d'intervalle à une dateNNNONNNN
DATE_FORMATFormatage de dateNNNONNNN
DATE_PARTPartie de dateNNNNONNN
DATE_SUBRetrait d'intervalle à une dateNNNONNNN
DATEADDAjout de dateNNNNNONN
DATEDIFFRetrait de dateNNNNNONN
DATENAMENom d'une partie de dateNNNNNONN
DATEPARTPartie de dateNNNNNONN
DAYJour d'une dateNNNNNONN
DAYNAMENom du jourNNOONONN
DAYOFMONTHJour du moisNNNONNNN
DAYOFWEEKJour de la semaineNNNONNNN
DAYOFYEARJour dans l'annéeNNNONNNN
HOURExtraction de l'heureNNOONONN
LAST_DAYDernier jour du moisNNNNNNON
MINUTE NNOONONN
MONTHMois d'une dateNNOONOON
MONTH_BETWEENMONTH_BETWEENN NN
MONTHNAMENom du moisNNOONONN
NEXT_DAYProchain premier jour de la
semaineNNNNNNON
SECONDExtrait les secondesNNOONONN
SUBDATERetrait d'intervalle à une dateNNNONNNN
WEEKNuméro de la semaineNNOONOON
YEARAnnée d'une dateNNOONOON

1.8. Prédicat, opérateurs et structures diverses


FonctionDescriptionNorme
SQLParadoxAccessMySQLPostGreSQLSQL
ServerOracleInterbase
CASEStructure conditionnelleONNOOOXO
IS [NOT] TRUEVraiONNNNNNN
IS [NOT] FALSEFauxONNNNNNN
IS [NOT] UNKNOWNInconnuONNNNNNN
IS [NOT] NULLNULLOOXOOOOO
INNER JOINJointure interneOOOOOONO
LEFT, RIGHT, FULL OUTER JOINJointure
externeOOOOOONO
NATURAL JOINJointure naturelleONNOONNN
UNION JOINJointure d'unionONNNNNNN
LEFT, RIGHT, FULL OUTER NATURAL JOINJointure
naturelle externeONNXONNN
INTERSECTIntersection (ensemble)O?NNONXN
UNIONUnion (ensemble)O?ONOOOO
EXCEPTDifférence (ensemble)O?NNONNN
[NOT] INListeOOOXOOOO
[NOT] BETWEENFourchette OOOOOOO
[NOT] EXISTSExistenceO??NOOOO
ALLComparaison à toutes les valeurs d'un
ensembleO?ONOOOO
ANY / SOMEComparaison à au moins une valeur de
l'ensembleO?ONOOOO
UNIQUEExistance sans doublonsONNNNNNN
MATCH UNIQUECorrespondanceONNNNNNN
row value construteurConstruteur de ligne
valuéesONNNNNON
MINUSDifférence (ensemble)NNNNONON
LIMITEnombre de ligne
retournéeNNTOPLIMITLIMITTOPNROWS
identifiant de ligne NNN_rowidoidNrowid?
Ce que nous venons de voir concerne l'analyse conceptuelle des données, c'est à dire
un niveau d'analyse qui s'affranchi de toutes les contraintes de la base de données
sur lequel va reposer l'application. Une fois décrit sous forme graphique, ce modèle
est couramment appelé MCD pour "Modèle Conceptuel des Données".

Dès lors, tout MCD peut être tansformé en un MPD ("Modèle Physique des Données")
c'est à dire un modèle directement exploitable par la base de données que vous
voulez utiliser...

Tout l'intérêt de cet outil d'analyse est de permettre de modéliser plus aisément les
relations existant entre les entités et d'automatiser le passage du schéma muni
d'attributs aux tables de la base de données pourvues de leurs champs.

Voici maintenant les règles de base nécessaire à une bonne automatisation du


passage du MCD au MPD :

5.1. Transformation des entités (passer de l'entité à la table)


Règle n°1 : toute entité doit être représentée par une table.

5.1.1. Relation de type 1:1 (la voix de la simplicité)


Règle n°2 : Dans le cas d'entités reliées par des associations de type 1:1, les
tables doivent avoir la même clef.

Exemple :

NOTA : on aurait pu choisir l'autre clef comme clef commune, à savoir le n°


d'appartement.
Bien que certains systèmes proposent une autre solution :

Dans ce cas une étude approfondie de la solution à adopter est nécessaire, mais ce
type de relation est en général assez rare et peu performante...

5.1.2. Relation de type 1:n (maître et esclave)


Règle n°3 : Dans le cas d'entités reliées par des associations de type 1:n,
chaque table possède sa propre clef, mais la clef de l'entité côté 0,n (ou 1,n)
migre vers la table côté 0,1 (ou 1,1) et devient une clef étrangère (index
secondaire).

Exemple :
5.1.3. Relation de type n:m (plusieurs à plusieurs)
Règle n°4 : Dans le cas d'entités reliées par des associations de type n:m,
une table intermédiaire dite table de jointure, doit être créée, et doit
posséder comme clef primaire une conjonction des clefs primaires des deux
tables pour lesquelles elle sert de jointure.

Exemple :

5.2. Ou placer les attributs d'association ?


Règle n°5 : Cas des associations pourvues d'au moins un attribut :

 si le type de relation est n:m, alors les attributs de l'association


deviennent des attributs de la table de jointure.
 si le type de relation est 1:n, il convient de faire glisser les attributs
vers l’entités pourvue des cardinalités 1:1.
 si le type de relation est 1:1, il convient de faire glisser les attributs
vers l’une ou l’autre des entités.

Pour synthétiser toutes ces règles, voici un exemple de modélisation d'une


application. En l'occurrence il s'agit d'un service commercial désirant modéliser les
commandes de ses clients.
Exemple :

établi à partir de l'outil AMC (AMC*Designor ou encore Power AMC) :


Sommaire > Administration de la base de données > Connexions
au serveur
Comment fixer la durée d'attente de libération d'un
verrou sur un object de la base de données ?
Je n'arrive pas à me connecter à une base de données
de mon serveur depuis un programme client
Comment se connecter à un serveur SQL qui se trouve
derrière un proxy ?
Comment se connecter à un serveur se trouvant derrière
un pare-feu ?
Quelle procédure stockée permet de limiter le nombre
de connexions simultanées ?
Mon serveur ne démarre pas à cause du méssage : Echec
d'ouverture d'une session a empêché le démarrage d'un service.
Comment connaître le type d'authentification installée
sur le serveur ?
Comment resoudre le problème 'Délai d'attente expiré'
lorsqu'on tente de se connecter au serveur ?
Déconnectez les utilisateurs d'une base de données
Comment démarrer un serveur si la base tempdb est
corrompue

Comment fixer la durée d'attente de libération


d'un verrou sur un object de la base de données
?[haut]

auteur : Wolo Laurent


SET LOCK_TIMEOUT permet à une application de définir le
délai maximal pendant lequel
une instruction doit attendre en cas de ressource
bloquée. Si l'attente d'une instruction
dépasse la valeur du paramètre LOCK_TIMEOUT,
l'instruction bloquée est automatiquement
annulée, et un message d'erreur renvoyé à l'application.
Cette valeur est fixée à -1 au début d'une connexion.

--Fixe la valeur du lock_timeout à 1,8 seconde


SET LOCK_TIMEOUT 1800

Pour connaître la valeur courante, on utilise la


variable @@LOCK_TIMEOUT

--Consulter la valeur du lock timeout


SELECT @@LOCK_TIMEOUT AS LOCK_TIMEOUT

Je n'arrive pas à me connecter à une base de


données de mon serveur depuis un programme
client[haut]

auteur : Wolo Laurent


Si vous n'arrivez pas à vous connecter à une base de
données de votre serveur, procedez comme ci-dessous.
Si vous utilisez le nom de la machine comme source de
données, vérifiez que votre réseau
possède un serveur de nom de domaine.
Dans le cas contraire, utilisez l'adresse IP publique
de votre serveur de base de données.

Attention, il arrive que vous vous connectez sous


windows avec un utilisateur qu possède moins de droits
pour modifier les données de la base de données de ce
serveur.

Vérifiez que vous utilisez le bon type


d'autentification SQL Serveur pour votre chaîne de
connexion

Vérifiez que votre serveur ne se trouve pas derrière


un serveur proxy.
Vérifier que votre serveur ne se trouve pas derrière
un Firewall.

Comment se connecter à un serveur SQL qui se


trouve derrière un proxy ?[haut]

auteur : Wolo Laurent


Pour se connecter à un serveur SQL Serveur qui tourne
derrière un serveur proxy,
Ouvrez l'utilitaire Server Network Utility
Dans l'onglet général, cochez la case enable Winsock
proxy
puis saisissez l'adresse du serveur proxy et son port
d'écoute.

Comment se connecter à un serveur se trouvant


derrière un pare-feu ?[haut]

auteur : Wolo Laurent


Pour se connecter à un serveur dans ces conditions,
Configurez un numero de port (1433) par défaut puis
demandez à votre administrateur réseau de libérer le
trafique de ce port sur son firewall

il est déconseillé d'ouvrir une brèche dans le firewall


si MS-SQL utilise le port par défaut. reconfigurer le
port avant tout.

Quelle procédure stockée permet de limiter le


nombre de connexions simultanées ?[haut]

auteurs : drahu, Fabien Celaia

sp_configure connections, n
RECONFIGURE WITH OVERRIDE
--n est le nombre de connexions souhaitées.

Mon serveur ne démarre pas à cause du méssage :


Echec d'ouverture d'une session a empêché le
démarrage d'un service.[haut]

auteur : Wolo Laurent


Ce méssage intervient lorsque le compte configuré pour
démarrer le service MSSQLSERVER échoue dans le démarrage
d'une session Windows. Allez-y dans le Gestionnaire des
Services, Cliquez sur le service MSSQLSERVER, puis
changer le compte de démarrage du service. NB: Il est
plus simple d'utiliser le compte LocalSystem.

Comment connaître le type d'authentification


installée sur le serveur ?[haut]

auteur : Wolo Laurent

select CASE
WHEN convert(sysname, serverproperty('Edition')) IS NULL THEN 'ERREUR'
WHEN convert(sysname, serverproperty('Edition'))=0 THEN 'SECURITE INTEGREE'
WHEN convert(sysname, serverproperty('Edition'))=1 THEN 'SECURITE NON
INTEGREE'
END AS AUTHENTIFICATION

Comment resoudre le problème "Délai d'attente


expiré" lorsqu'on tente de se connecter au serveur
?[haut]
auteur : Wolo Laurent
Il s'agit d'un problème lié au DNS
Utiliser l'adresse IP\Instance du serveur à la place
de NomServeur\Instance
Dans la fenêtre de configuration IP du protocole
TCP/IP de votre carte réseau, ajouter l'adresse du DNS
primaire, en utilisant celui de votre contôleur de
domaine.

Déconnectez les utilisateurs d'une base de


données[haut]

auteurs : Frédéric Brouard, Fabien Celaia

ALTER DATABASE 'MaBase' SET SINGLE_USER WITH ROLLBACK IMMEDIATE

ALTER DATABASE 'MaBase' SET MULTI_USER

CREATE PROC sp_killuser (@MaBase varchar(30)=NULL)


AS
BEGIN
Declare @SQL_Text nvarchar(20)

declare Process_cur CURSOR FOR


select convert(varchar(5), spid)
from master..sysprocesses
where dbid = db_id(coalesce(@MaBase,db_name())
and spid <> @@spid

Declare @spid as varchar(4)

OPEN Process_cur

FETCH NEXT FROM Process_cur INTO @spid

WHILE (@@FETCH_STATUS = 0)
begin
set @SQL_Text = 'KILL '+ @spid
exec sp_executesql @T
FETCH NEXT FROM Process_cur into @spid;
end

CLOSE Process_cur
DEALLOCATE Process_cur
END
GO

Tout le monde a déjà eu affaire au moins une fois dans sa vie


à la récursion. Lorsque j'étais enfant, mes parents et moi
vivions dans un immeuble parisien où figuraient dans le hall
deux glaces se faisant face. Lorsque je passais entre ces deux
miroirs, mon image se reflétait à l'infini et j'étais assez
fier de palper le concept de récursion sur ma personne ! C'est
cela la récursion : un processus capable de se reproduire
aussi longtemps que nécessaire.
Frédéric Brouard est expert en langage SQL, SGBDR,
modélisation de données. Il enseigne à l'ISEN Toulon et aux
Arts & Métiers

Version hors-ligne (Miroir)

I. Introduction
II. Récursivité avec la norme SQL:1999
III. Une expression de table commune (CTE : Common Table
Expression)
IV. Deux astuces pour la récursion
V. Premier exemple, une hiérarchie basique
VI. Indentation hiérarchique
VI. Arbres SQL sans récursion
VI-A. PREMIÈRES IMPRESSIONS
VIII. Second exemple : un réseau complexe (et des requêtes
plus sexy !)
IX. Troisième exemple : découper une chaîne de caractères
X. Quatrième exemple : concaténer des mots pour former une
phrase
XI. Que faire de plus ?
XII. CONCLUSIONS
XIII. En bonus (CTE, requête récursive appliquée)
XIV. Bibliographie :
I. Introduction
Tout le monde a déjà eu affaire au moins une fois dans sa vie
à la récursion. Lorsque j'étais enfant, mes parents et moi
vivions dans un immeuble parisien où figuraient dans le hall
deux glaces se faisant face. Lorsque je passais entre ces deux
miroirs, mon image se reflétait à l'infini et j'étais assez
fier de palper le concept de récursion sur ma personne ! C'est
cela la récursion : un processus capable de se reproduire
aussi longtemps que nécessaire.

Mais en termes "mécaniques" nous ne pouvons accepter une


récursion infinie. Dans le monde réel, nous avons besoin que
le processus s'arrête parce que notre monde apparaît fermé.
Woody Allen, parlant de l'infini du temps, disait "l'éternité
c'est long, surtout vers la fin..." !

En informatique la récursion est une technique particulière,


capable dans certains cas de traiter avec élégance des
problèmes complexes : quelques lignes suffisent à effectuer un
travail parfois considérable. Mais la récursion induit
certains effets pervers : les ressources pour effectuer le
traitement sont maximisées par le fait que chaque appel
réentrant du processus nécessite l'ouverture d'un
environnement de travail complet, ce qui implique un coût
généralement très élevé en mémoire. Heureusement, un
mathématicien dont je ne me rappelle plus le nom, a découvert
que tout processus récursif pouvait s'écrire de manière
itérative, à condition de disposer d'une "pile"

Mais notre propos est de parler de la récursivité dans le


langage de requête SQL et en particulier de ce que fait SQL
Server 2005 au regard de la norme SQL:1999.

II. Récursivité avec la norme SQL:1999


Voici une syntaxe normative édulcorée du concept de requête
récursive :

WITH [ RECURSIVE ] <surnom_requête> [ ( <liste_colonne> ) ]


AS ( <requête_select> )
<requête_utilisant_surnom_requête>

Simple, n'est-ce pas ? En fait tout le mécanisme de


récursivité est situé dans l'écriture de la <requête_select>.
Nous allons d'abord montrer une version simplifiée, mais non
récursive d'une telle requête et, lorsque nous aurons vu ce
que nous pouvons faire avec le mot clef WITH, nous dévoilerons
un aspect plus "sexy" de SQL, utilisant la récursivité..

III. Une expression de table commune (CTE : Common Table


Expression)
L'utilisation du mot clef WITH, sans son complément RECURSIVE,
permet de construire une expression de table dite "commune",
soit en anglais "Common Table Expression" (CTE). En un sens,
la CTE est une vue exprimée spécialement pour une requête et
son usage exclusif et volatile. On peut donc parler de vue non
persistante. L'utilisation classique du concept de CTE est de
rendre plus claire l'écriture de requêtes complexes bâties à
partir de résultats d'autres requêtes.
Voici un exemple basique :
Exemple 1

-- creation de la table
CREATE TABLE T_NEWS
(NEW_ID INTEGER NOT NULL PRIMARY KEY,
NEW_FORUM VARCHAR(16),
NEW_QUESTION VARCHAR(32))
GO
-- population de la table
INSERT INTO T_NEWS VALUES (1, 'SQL', 'What is SQL ?')
INSERT INTO T_NEWS VALUES (2, 'SQL', 'What do we do now ?')
INSERT INTO T_NEWS VALUES (3, 'Microsoft', 'Is SQL 2005 ready for use ?')
INSERT INTO T_NEWS VALUES (4, 'Microsoft', 'Did SQL2000 use RECURSION ?')
INSERT INTO T_NEWS VALUES (5, 'Microsoft', 'Where am I ?')

-- la requête exprimée de manière traditionnelle :


SELECT COUNT(NEW_ID) AS NEW_NBR, NEW_FORUM
FROM T_NEWS
GROUP BY NEW_FORUM
HAVING COUNT(NEW_ID) = ( SELECT MAX(NEW_NBR)
FROM ( SELECT COUNT(NEW_ID) AS NEW_NBR, NEW_FORUM
FROM T_NEWS
GROUP BY NEW_FORUM ) T )
-- le resultat :
NEW_NBR NEW_FORUM
----------- ----------------
3 Microsoft

Cette requête est assez classique dans le cadre d'un modèle de


données de type "forum". Le but est de trouver la question qui
a provoqué le plus de réponse. Pour exprimer une telle requête
il faut faire un MAX(COUNT), ce qui n'est pas autorisé dans
SQL du fait des regroupements et doit donc être résolu par
l'utilisation de sous-requêtes. Mais notez que dans cette
écriture, deux des SELECT présentent, à peu de choses près, la
même structure :
Exemple 2

SELECT
COUNT(NEW_ID) AS NEW_NBR, NEW_FORUM
FROM T_NEWS
GROUP BY NEW_FORUM

L'utilisation du concept de CTE va rendre la requête plus


lisible :
Exemple 3

WITH
Q_COUNT_NEWS (NBR, FORUM)
AS
(SELECT COUNT(NEW_ID), NEW_FORUM
FROM T_NEWS
GROUP BY NEW_FORUM)
SELECT NBR, FORUM
FROM Q_COUNT_NEWS
WHERE NBR = (SELECT MAX(NBR)
FROM Q_COUNT_NEWS)

En fait nous utilisons la vue éphémère Q_COUNT_NEWS,


introduite par le mot-clef WITH, pour écrire d'une manière
plus élégante la solution à notre problème.

Comme dans la cadre d'une vue SQL, vous devez nommer la CTE et
vous pouvez donner des noms particuliers aux colonnes du
SELECT qui construit l'expression de la CTE, mais cette
dernière disposition n'est pas obligatoire.
Dans les faits on peut enchaîner deux, trois ou autant de CTE
que vous voulez dans une même requête, chaque CTE pouvant être
construite à partie des expression des CTE précédentes. Voici
un exemple de ce concept de CTE gigogne :
Exemple 4

WITH
Q_COUNT_NEWS (NBR, FORUM)
AS
(SELECT COUNT(NEW_ID), NEW_FORUM
FROM T_NEWS
GROUP BY NEW_FORUM),
Q_MAX_COUNT_NEWS (NBR)
AS (SELECT MAX(NBR)
FROM Q_COUNT_NEWS)
SELECT T1.*
FROM Q_COUNT_NEWS T1
INNER JOIN Q_MAX_COUNT_NEWS T2
ON T1.NBR = T2.NBR

Cette requête donne le même résultat que les précédentes : la


première CTE, Q_COUNT_NEWS est utilisée comme s'il s'agissait
d'une table dans la seconde CTE, Q_MAX_COUNT_NEWS. Ces deux
CTE sont jointes dans l'expression de requête pour donner le
résultat. Notez la virgule qui sépare les deux expressions de
table.

IV. Deux astuces pour la récursion


Pour rendre récursive une requête, deux astuces sont
nécessaires :

Premièrement, vous devez donner un point de départ au


processus de récursion. Cela doit se faire avec deux requêtes
liées. La première requête indique où l'on doit commencer et
la seconde où l'on doit se rendre ensuite. Ces deux requêtes
doivent être jointes par l'opération ensembliste UNION ALL.

Deuxièmement, vous devez effectuer une corrélation entre


l'expression de requête CTE et le code SQL qui la compose,
afin de progresser par étapes successives. Cela se fait en
référençant le <surnom_requête> à l'intérieur du code SQL
exprimant la CTE

V. Premier exemple, une hiérarchie basique


Pour cet exemple, j'ai choisi une table contenant une
typologie de véhicules :
Exemple 5

-- creation de la table
CREATE TABLE T_VEHICULE
(VHC_ID INTEGER NOT NULL PRIMARY KEY,
VHC_ID_FATHER INTEGER FOREIGN KEY REFERENCES T_VEHICULE
(VHC_ID),
VHC_NAME VARCHAR(16))
-- population
INSERT INTO T_VEHICULE VALUES (1, NULL, 'ALL')
INSERT INTO T_VEHICULE VALUES (2, 1, 'SEA')
INSERT INTO T_VEHICULE VALUES (3, 1, 'EARTH')
INSERT INTO T_VEHICULE VALUES (4, 1, 'AIR')
INSERT INTO T_VEHICULE VALUES (5, 2, 'SUBMARINE')
INSERT INTO T_VEHICULE VALUES (6, 2, 'BOAT')
INSERT INTO T_VEHICULE VALUES (7, 3, 'CAR')
INSERT INTO T_VEHICULE VALUES (8, 3, 'TWO WHEELS')
INSERT INTO T_VEHICULE VALUES (9, 3, 'TRUCK')
INSERT INTO T_VEHICULE VALUES (10, 4, 'ROCKET')
INSERT INTO T_VEHICULE VALUES (11, 4, 'PLANE')
INSERT INTO T_VEHICULE VALUES (12, 8, 'MOTORCYCLE')
INSERT INTO T_VEHICULE VALUES (13, 8, 'BICYCLE')

Habituellement, une hiérarchie se représente en utilisant une


auto-référence, c'est-à-dire à l'aide d'une clef étrangère
provenant de la clef même de la table.
Les données de cette table peuvent se représenter de la sorte
:

ALL
|--SEA
| |--SUBMARINE
| |--BOAT
|--EARTH
| |--CAR
| |--TWO WHEELS
| | |--MOTORCYCLE
| | |--BICYCLE
| |--TRUCK
|--AIR
|--ROCKET
|--PLANE

Commençons maintenant à exprimer une première interrogation et


demandons-nous d'où vient la moto ? En d'autres termes, nous
recherchons les ancêtres de MOTORCYCLE. Nous devons donc
partir de la ligne qui contient la moto :
Exemple 6

SELECT VHC_NAME, VHC_ID_FATHER


FROM T_VEHICULE
WHERE VHC_NAME = 'MOTORCYCLE'

Nous devons récupérer la valeur de l'identifiant père


(VHC_ID_FATHER) pour aller à l'étape suivante.
La seconde requête, qui avance d'un pas dans l'arbre, doit
être écrite comme suit :
Exemple 7

SELECT VHC_NAME, VHC_ID_FATHER


FROM T_VEHICULE

Comme on le voit, il n'y a aucune différence entre les deux


requêtes à l'exception du filtre WHERE qui sert de point de
départ. Souvenez-vous simplement que vous devez introduire un
UNION ALL entre les deux requêtes afin d'assurer la liaison
entre le départ et le pas suivant :
Exemple 8

SELECT VHC_NAME, VHC_ID_FATHER


FROM T_VEHICULE
WHERE VHC_NAME = 'MOTORCYCLE'
UNION ALL
SELECT VHC_NAME, VHC_ID_FATHER
FROM T_VEHICULE

Plaçons maintenant cette requête composite dans code SQL qui


bâtit l'expression de la CTE :
Exemple 9
WITH
tree (data, id)
AS (SELECT VHC_NAME, VHC_ID_FATHER
FROM T_VEHICULE
WHERE VHC_NAME = 'MOTORCYCLE'
UNION ALL
SELECT VHC_NAME, VHC_ID_FATHER
FROM T_VEHICULE)

Nous sommes maintenant près de la récursion. La dernière étape


de notre travail consiste à générer le cycle de récursion.
Cela se fait en utilisant le nom de la CTE (ici "tree") à
l'intérieur même de l'expression. Dans notre cas nous devons,
dans la seconde requête SELECT de la CTE, joindre la "table"
tree à la table T_VEHICULE et assurer le chaînage par une
jointure entre les identifiants : tree.id = (second
query).VHC_ID.

Cela se réalise ainsi :


Exemple 10

WITH
tree (data, id)
AS (SELECT VHC_NAME, VHC_ID_FATHER
FROM T_VEHICULE
WHERE VHC_NAME = 'MOTORCYCLE'
UNION ALL
SELECT VHC_NAME, VHC_ID_FATHER
FROM T_VEHICULE V
INNER JOIN tree t
ON t.id = V.VHC_ID)
SELECT *
FROM tree

Il n'y a plus rien à faire que d'exprimer une requête finale,


la plus simple possible, basée sur la CTE afin d'afficher les
données :

data id
---------------- -----------
MOTORCYCLE 8
TWO WHEELS 3
EARTH 1
ALL NULL

Regardons maintenant la façon dont nous avons lié les tables


et la CTE d'une façon pseudo-graphique :

correlation
_________________________________
| |
v |
WITH tree (data, id) |
AS (SELECT VHC_NAME, VHC_ID_FATHER |
FROM T_VEHICULE |
WHERE VHC_NAME = 'MOTORCYCLE' |
UNION ALL |
SELECT VHC_NAME, VHC_ID_FATHER |
FROM T_VEHICULE V |
INNER JOIN tree t <-----------
ON t.id = V.VHC_ID)
SELECT *
FROM tree

La question que l'on peut se poser est la suivante : qu'est ce


qui a stoppé le processus de récursion ? C'est simplement le
fait que plus rien ne peut être chaîné dès que l'identifiant
atteint le marqueur NULL, ce qui est le cas dans notre exemple
lorsque nous atteignons la ligne où VHC_NAME = "ALL".

Maintenant vous avez la technique. Veuillez simplement noter


que pour une raison que je ne m'explique pas encore, MS SQL
Server n'accepte pas la présence du mot-clef RECURSIVE après
le mot-clef WITH, alors que la norme le préconise...

VI. Indentation hiérarchique


Une chose importante et souvent réclamée avec les données
structurées sous forme arborescente, est de les présenter à la
manière d'un arbre... ce qui suppose une indentation des
items, combinée à un ordre particulier lors de la restitution
des données. Est-ce possible avec SQL ? Oui, bien sûr. Pour
réaliser cet ordonnancement des données, nous devons connaître
le cheminement dans l'arbre et le niveau du noeud, deux
informations qui nous aiderons à rajouter des espaces
d'indentation et à trier les lignes du résultat dans le bon
ordre.

Il faut donc calculer à la fois le chemin et le niveau, et


cela est possible avec la CTE :
Exemple 11

WITH tree (data, id, level, pathstr)


AS (SELECT VHC_NAME, VHC_ID, 0,
CAST('' AS VARCHAR(MAX))
FROM T_VEHICULE
WHERE VHC_ID_FATHER IS NULL
UNION ALL
SELECT VHC_NAME, VHC_ID, t.level + 1, t.pathstr + V.VHC_NAME
FROM T_VEHICULE V
INNER JOIN tree t
ON t.id = V.VHC_ID_FATHER)
SELECT SPACE(level) + data as data, id, level, pathstr
FROM tree
ORDER BY pathstr, id

data id level pathstr


------------------ ----------- ----------- --------------------------------
ALL 1 0
AIR 4 1 AIR
PLANE 11 2 AIRPLANE
ROCKET 10 2 AIRROCKET
EARTH 3 1 EARTH
CAR 7 2 EARTHCAR
TRUCK 9 2 EARTHTRUCK
TWO WHEEL S 8 2 EARTHTWO WHEELS
BICYCLE 13 3 EARTHTWO WHEELSBICYCLE
MOTORCYCLE 12 3 EARTHTWO WHEELSMOTORCYCLE
SEA 2 1 SEA
BOAT 6 2 SEABOAT
SUBMARINE 5 2 SEASUBMARINE

Pour réaliser ce tour de force, nous avons utilisé un nouveau


type de données introduit avec la version 2005 de SQL Server,
le type VARCHAR(max) afin de ne pas limiter la concaténation
des points de passage à quelques caractères. En effet, la
profondeur d'un arbre peut être importante et dans ce cas la
concaténation du nom des étapes peut conduire à saturer une
colonne traditionnellement limitée à quelques caractères.
De plus et afin d'éliminer certains effets de bord, nous vous
conseillons d'introduire un marqueur entre les noms des
différents noeuds, par exemple le caractère "virgule" suivi
d'un espace afin de lier les étapes. Cela empêchera de
confondre une étape comme MARSALES (24) avec la concaténation
de deux autres étapes comme MARS (07) et ALES (30).

VI. Arbres SQL sans récursion


Mais je dois dire que les représentations hiérarchiques par
auto-référence ne sont pas d'intéressants sujets pour la
récursion... Pourquoi ? Parce qu'il existe une possibilité de
structuration des données qui élimine tout traitement récursif
!
Souvenez-vous de ce que j'ai dit dans le chapeau de cet
article : vous pouvez vous passer de la récursion à condition
de disposer d'une pile. Est-ce possible ? Oui : il suffit de
rajouter la pile à la table... Comment ? En utilisant deux
nouvelles colonnes RIGHT_BOUND et LEFT_BOUND...
Exemple 12

ALTER TABLE T_VEHICULE ADD RIGHT_BOUND INTEGER


ALTER TABLE T_VEHICULE ADD LEFT_BOUND INTEGER

Maintenant, à la manière d'un magicien, je vais ajouter des


données a ces deux nouvelles colonnes. Des chiffres
astucieusement calculés pour rendre inutile le recours à la
récursivité pour toutes nos interrogations :
Exemple 13

UPDATE T_VEHICULE SET LEFT_BOUND = 1 , RIGHT_BOUND = 26 WHERE


VHC_ID = 1
UPDATE T_VEHICULE SET LEFT_BOUND = 2 , RIGHT_BOUND = 7 WHERE
VHC_ID = 2
UPDATE T_VEHICULE SET LEFT_BOUND = 8 , RIGHT_BOUND = 19 WHERE
VHC_ID = 3
UPDATE T_VEHICULE SET LEFT_BOUND = 20, RIGHT_BOUND = 25 WHERE
VHC_ID = 4
UPDATE T_VEHICULE SET LEFT_BOUND = 3 , RIGHT_BOUND = 4 WHERE
VHC_ID = 5
UPDATE T_VEHICULE SET LEFT_BOUND = 5 , RIGHT_BOUND = 6 WHERE
VHC_ID = 6
UPDATE T_VEHICULE SET LEFT_BOUND = 9 , RIGHT_BOUND = 10 WHERE
VHC_ID = 7
UPDATE T_VEHICULE SET LEFT_BOUND = 11, RIGHT_BOUND = 16 WHERE
VHC_ID = 8
UPDATE T_VEHICULE SET LEFT_BOUND = 17, RIGHT_BOUND = 18 WHERE
VHC_ID = 9
UPDATE T_VEHICULE SET LEFT_BOUND = 21, RIGHT_BOUND = 22 WHERE
VHC_ID = 10
UPDATE T_VEHICULE SET LEFT_BOUND = 23, RIGHT_BOUND = 24 WHERE
VHC_ID = 11
UPDATE T_VEHICULE SET LEFT_BOUND = 12, RIGHT_BOUND = 13 WHERE
VHC_ID = 12
UPDATE T_VEHICULE SET LEFT_BOUND = 14, RIGHT_BOUND = 15 WHERE
VHC_ID = 13

Et voici maintenant la requête "magique" qui donne le même


résultat que notre précédente requête exprimée sous la forme
complexe de la CTE récursive :
Exemple 14

SELECT *
FROM T_VEHICULE
WHERE RIGHT_BOUND > 12
AND LEFT_BOUND < 13

VHC_ID VHC_ID_FATHER VHC_NAME RIGHT_BOUND LEFT_BOUND


----------- ------------- ---------------- ----------- -----------
1 NULL ALL 26 1
3 1 EARTH 19 8
8 3 TWO WHEELS 16 11
12 8 MOTORCYCLE 13 12

La question est maintenant la suivante : quel est le truc ? En


fait, nous avons réalisé la pile en numérotant les données par
tranches. Comme rien ne vaut une image pour visualiser un tel
concept, voici ce que j'ai dessiné :

La seule chose que j'ai faite, c'est de numéroter


continuellement en commençant par 1 toutes les bornes droites
et gauches des empilements de données constitués par chacune
de nos données.
Afin d'obtenir la requête précédente, j'ai simplement pris les
bornes de MOTORCYCLE, c'est-à-dire 12 gauche et 13 droite afin
de les placer dans la clause WHERE et demandé d'extraire les
lignes de la table pour lesquelles les valeurs des colonnes
RIGHT BOUND étaient supérieures à 12 et LEFT BOUND inférieures
à 13.

Notez d'ailleurs que mon joli dessin serait plus


compréhensible si je le pivotais de 90° :

J'espère ainsi que vous voyez mieux les piles ! Cette


représentations est d'ailleurs connue dans la littérature
spécialisée sous le nom de "représentation intervallaire des
arborescences", en particulier dans le livre de Joe Celko "SQL
Avancé" (Vuibert éditeur) ainsi que sur mon site web SQLpro,
sur lequel vous trouverez en outre toutes les requêtes et les
procédures stockées MS SQL Server adéquates pour faire
fonctionner un tel arbre
(http://sqlpro.developpez.com/cours/arborescence/).

Dernière question sur ce modèle : pouvons6nous reproduire


l'indentation hiérarchique présentée dans la dernière requête
construite avec la CTE ? Oui bien sûr, et d'autant plus
facilement si l'on ajoute une nouvelle colonne indiquant le
niveau du noeud. C'est simple à calculer puisque le niveau
d'un noeud est celui du noeud de rattachement +1 si
l'insertion se fait en fils, ou encore le même si l'insertion
se fait en frère, et qu'à la première insertion, donc à la
racine de l'arbre, le niveau est 0.

Voici maintenant les ordres SQL modifiant notre table pour


assurer cette fonction :
Exemple 15

ALTER TABLE T_VEHICULE


ADD LEVEL INTEGER

UPDATE T_VEHICULE SET LEVEL = 0 WHERE VHC_ID = 1


UPDATE T_VEHICULE SET LEVEL = 1 WHERE VHC_ID = 2
UPDATE T_VEHICULE SET LEVEL = 1 WHERE VHC_ID = 3
UPDATE T_VEHICULE SET LEVEL = 1 WHERE VHC_ID = 4
UPDATE T_VEHICULE SET LEVEL = 2 WHERE VHC_ID = 5
UPDATE T_VEHICULE SET LEVEL = 2 WHERE VHC_ID = 6
UPDATE T_VEHICULE SET LEVEL = 2 WHERE VHC_ID = 7
UPDATE T_VEHICULE SET LEVEL = 2 WHERE VHC_ID = 8
UPDATE T_VEHICULE SET LEVEL = 2 WHERE VHC_ID = 9
UPDATE T_VEHICULE SET LEVEL = 2 WHERE VHC_ID = 10
UPDATE T_VEHICULE SET LEVEL = 2 WHERE VHC_ID = 11
UPDATE T_VEHICULE SET LEVEL = 3 WHERE VHC_ID = 12
UPDATE T_VEHICULE SET LEVEL = 3 WHERE VHC_ID = 13

Voici la requête présentant les données sous forme d'une


arborescence avec indentation hiérarchique :
Exemple 16

SELECT SPACE(LEVEL) + VHC_NAME as data


FROM T_VEHICULE
ORDER BY LEFT_BOUND

data
-----------------------
ALL
SEA
SUBMARINE
BOAT
EARTH
CAR
TWO WHEELS
MOTORCYCLE
BICYCLE
TRUCK
AIR
ROCKET
PLANE

Beaucoup plus simple, n'est-ce pas ?

VI-A. PREMIÈRES IMPRESSIONS


La seule chose à dire au sujet de ces deux techniques de
navigation dans les arbres est que le modèle par intervalle
est beaucoup plus performant pour l'extraction des données que
l'utilisation de l'expression de table récursive (CTE)
introduite avec la norme SQL:1999.
En fait la récursivité dans SQL n'est pas si intéressante que
cela dans ce cadre... Mais qu'en est-il dans un autre ? C'est
ce que nous allons voir !

VIII. Second exemple : un réseau complexe (et des requêtes


plus sexy !)
Peut être ne voyagez-vous pas assez souvent en France. En tout
cas, ce que je peux vous dire, c'est qu'à Paris il y a des
jolies filles et à Toulouse un délicieux plat appelé cassoulet
et un petit constructeur d'avion dont le nom se francise en
"bus de l'air"...
Plus sérieusement, notre problème consiste à nous rendre de
Paris à Toulouse en utilisant le réseau autoroutier :
Exemple 17

PARIS
|
------------------------------------
| | |
385 420 470
| | |
NANTES CLERMONT FERRAND LYON
| | |
| | 335 305 | 320
| ---------- -----------------
| | | |
375 | MONTPELLIER MARSEILLE
| | |
---------------------- 205
| 240 |
TOULOUSE NICE

-- creation de la table :
CREATE TABLE T_JOURNEY
(JNY_FROM_TOWN VARCHAR(32),
JNY_TO_TOWN VARCHAR(32),
JNY_KM INTEGER)
-- population :
INSERT INTO T_JOURNEY VALUES ('PARIS', 'NANTES', 385)
INSERT INTO T_JOURNEY VALUES ('PARIS', 'CLERMONT-FERRAND', 420)
INSERT INTO T_JOURNEY VALUES ('PARIS', 'LYON', 470)
INSERT INTO T_JOURNEY VALUES ('CLERMONT-FERRAND', 'MONTPELLIER',
335)
INSERT INTO T_JOURNEY VALUES ('CLERMONT-FERRAND', 'TOULOUSE', 375)
INSERT INTO T_JOURNEY VALUES ('LYON', 'MONTPELLIER', 305)
INSERT INTO T_JOURNEY VALUES ('LYON', 'MARSEILLE', 320)
INSERT INTO T_JOURNEY VALUES ('MONTPELLIER', 'TOULOUSE', 240)
INSERT INTO T_JOURNEY VALUES ('MARSEILLE', 'NICE', 205)

Essayons maintenant une simple requête donnant tous les


trajets entre deux villes :
Exemple 18

WITH journey (TO_TOWN)


AS
(SELECT DISTINCT JNY_FROM_TOWN
FROM T_JOURNEY
UNION ALL
SELECT JNY_TO_TOWN
FROM T_JOURNEY AS arrival
INNER JOIN journey AS departure
ON departure.TO_TOWN = arrival.JNY_FROM_TOWN)
SELECT *
FROM journey

TO_TOWN
--------------------------------
CLERMONT-FERRAND
LYON
MARSEILLE
MONTPELLIER
PARIS
NANTES
CLERMONT-FERRAND
LYON
MONTPELLIER
MARSEILLE
NICE
TOULOUSE
MONTPELLIER
TOULOUSE
TOULOUSE
TOULOUSE
NICE
MONTPELLIER
MARSEILLE
NICE
TOULOUSE
MONTPELLIER
TOULOUSE
TOULOUSE

Constatez avec moi que ce n'est pas très intéressant car nous
ne savons pas d'où nous venons. Seule la destination figure
dans la réponse et il est probable que plusieurs trajets
figurent pour un même voyage... Pouvons-nous avoir plus
d'informations ?

Premièrement, considérons que nous devons partir de Paris :


Exemple 19

WITH journey (TO_TOWN)


AS
(SELECT DISTINCT JNY_FROM_TOWN
FROM T_JOURNEY
WHERE JNY_FROM_TOWN = 'PARIS'
UNION ALL
SELECT JNY_TO_TOWN
FROM T_JOURNEY AS arrival
INNER JOIN journey AS departure
ON departure.TO_TOWN = arrival.JNY_FROM_TOWN)
SELECT *
FROM journey

TO_TOWN
--------------------------------
PARIS
NANTES
CLERMONT-FERRAND
LYON
MONTPELLIER
MARSEILLE
NICE
TOULOUSE
MONTPELLIER
TOULOUSE
TOULOUSE

Nous avons probablement trois trajets différents pour aller à


Toulouse. Pouvons-nous filtrer la destination ? Bien sûr :
Exemple 20

WITH journey (TO_TOWN)


AS
(SELECT DISTINCT JNY_FROM_TOWN
FROM T_JOURNEY
WHERE JNY_FROM_TOWN = 'PARIS'
UNION ALL
SELECT JNY_TO_TOWN
FROM T_JOURNEY AS arrival
INNER JOIN journey AS departure
ON departure.TO_TOWN = arrival.JNY_FROM_TOWN)
SELECT *
FROM journey
WHERE TO_TOWN = 'TOULOUSE'

TO_TOWN
--------------------------------
TOULOUSE
TOULOUSE
TOULOUSE

Nous pouvons affiner cette requête afin qu'elle nous donne le


nombre d'étapes des différents trajets, entre l'origine et la
destination :
Exemple 21

WITH journey (TO_TOWN, STEPS)


AS
(SELECT DISTINCT JNY_FROM_TOWN, 0
FROM T_JOURNEY
WHERE JNY_FROM_TOWN = 'PARIS'
UNION ALL
SELECT JNY_TO_TOWN, departure.STEPS + 1
FROM T_JOURNEY AS arrival
INNER JOIN journey AS departure
ON departure.TO_TOWN = arrival.JNY_FROM_TOWN)
SELECT *
FROM journey
WHERE TO_TOWN = 'TOULOUSE'

TO_TOWN STEPS
-------------------------------- -----------
TOULOUSE 3
TOULOUSE 2
TOULOUSE 3

La cerise sur le gâteau serait de connaître la distance des


différents trajets. Cela s'exprime comme ceci :
Exemple 22

WITH journey (TO_TOWN, STEPS, DISTANCE)


AS
(SELECT DISTINCT JNY_FROM_TOWN, 0, 0
FROM T_JOURNEY
WHERE JNY_FROM_TOWN = 'PARIS'
UNION ALL
SELECT JNY_TO_TOWN, departure.STEPS + 1
, departure.DISTANCE + arrival.JNY_KM
FROM T_JOURNEY AS arrival
INNER JOIN journey AS departure
ON departure.TO_TOWN = arrival.JNY_FROM_TOWN)
SELECT *
FROM journey
WHERE TO_TOWN = 'TOULOUSE'

TO_TOWN STEPS DISTANCE


----------------------- ----------- -----------
TOULOUSE 3 1015
TOULOUSE 2 795
TOULOUSE 3 995
La fille qui surgit du gâteau consisterait à afficher les
différentes étapes intermédiaires de chaque trajet :
Exemple 23

WITH journey (TO_TOWN, STEPS, DISTANCE, WAY)


AS
(SELECT DISTINCT JNY_FROM_TOWN, 0, 0, CAST('PARIS' AS
VARCHAR(MAX))
FROM T_JOURNEY
WHERE JNY_FROM_TOWN = 'PARIS'
UNION ALL
SELECT JNY_TO_TOWN, departure.STEPS + 1
, departure.DISTANCE + arrival.JNY_KM
, departure.WAY + ', ' + arrival.JNY_TO_TOWN
FROM T_JOURNEY AS arrival
INNER JOIN journey AS departure
ON departure.TO_TOWN = arrival.JNY_FROM_TOWN)
SELECT *
FROM journey
WHERE TO_TOWN = 'TOULOUSE'

TO_TOWN STEPS DISTANCE WAY


---------- ------- --------- ----------------------------------------------
TOULOUSE 3 1015 PARIS, LYON, MONTPELLIER, TOULOUSE
TOULOUSE 2 795 PARIS, CLERMONT-FERRAND, TOULOUSE
TOULOUSE 3 995 PARIS, CLERMONT-FERRAND, MONTPELLIER,
TOULOUSE

Et maintenant, mesdames et messieurs, la technique de la CTE


alliée à la puissance de la récursivité SQL sont fiers de vous
présenter la solution à un problème de grande complexité, le
problème du voyageur de commerce, un des casse-tête de la
recherche opérationnelle sur lequel le mathématicien Edsger
Wybe Dijkstra trouva un algorithme qui lui valut le prix
Turing en 1972... :
Exemple 24

WITH journey (TO_TOWN, STEPS, DISTANCE, WAY)


AS
(SELECT DISTINCT JNY_FROM_TOWN, 0, 0, CAST('PARIS' AS
VARCHAR(MAX))
FROM T_JOURNEY
WHERE JNY_FROM_TOWN = 'PARIS'
UNION ALL
SELECT JNY_TO_TOWN, departure.STEPS + 1,
departure.DISTANCE + arrival.JNY_KM,
departure.WAY + ', ' + arrival.JNY_TO_TOWN
FROM T_JOURNEY AS arrival
INNER JOIN journey AS departure
ON departure.TO_TOWN = arrival.JNY_FROM_TOWN)
SELECT TOP 1 *
FROM journey
WHERE TO_TOWN = 'TOULOUSE'
ORDER BY DISTANCE

TO_TOWN STEPS DISTANCE WAY


---------- ------- --------- ----------------------------------------------
TOULOUSE 2 795 PARIS, CLERMONT-FERRAND, TOULOUSE

Au fait, TOP n ne fait pas partie de la norme SQL...


haïssez-le et bénissez la CTE :
Exemple 25

WITH
journey (TO_TOWN, STEPS, DISTANCE, WAY)
AS
(SELECT DISTINCT JNY_FROM_TOWN, 0, 0, CAST('PARIS' AS
VARCHAR(MAX))
FROM T_JOURNEY
WHERE JNY_FROM_TOWN = 'PARIS'
UNION ALL
SELECT JNY_TO_TOWN, departure.STEPS + 1,
departure.DISTANCE + arrival.JNY_KM,
departure.WAY + ', ' + arrival.JNY_TO_TOWN
FROM T_JOURNEY AS arrival
INNER JOIN journey AS departure
ON departure.TO_TOWN = arrival.JNY_FROM_TOWN),
short (DISTANCE)
AS
(SELECT MIN(DISTANCE)
FROM journey
WHERE TO_TOWN = 'TOULOUSE')
SELECT *
FROM journey j
INNER JOIN short s
ON j.DISTANCE = s.DISTANCE
WHERE TO_TOWN = 'TOULOUSE'

IX. Troisième exemple : découper une chaîne de caractères

--------------------------------------------

-- soit la table :
CREATE TABLE TRolePers
(
idPers int,
Role varchar(50)
)

-- contenant :
INSERT INTO TRolePers VALUES (1, 'RespX, RespY')
INSERT INTO TRolePers VALUES (2, 'Admin')
INSERT INTO TRolePers VALUES (3, 'RespX, RespZ, RespY')

/*

Trouver une requête SQL permettant de décomposer les chaines


de caractères en colonnes, chaque mot étant délimité par la
virgule. Comment obtenir le résultat suivant ?

idPers Role
----------- --------------------------------------------------
1 RespX
1 RespY
2 Admin
3 RespX
3 respY
3 RespZ

Solution :

WITH T
AS
(
SELECT idPers,
CASE
WHEN CHARINDEX(',', Role) > 0 THEN LTRIM(SUBSTRING(Role, 1,
CHARINDEX(',', Role) - 1))
ELSE Role
END AS UnRole,
LTRIM(SUBSTRING(Role, CHARINDEX(',', Role) + 1, LEN(Role) -
CHARINDEX(',', Role))) AS LesAutresRoles

FROM TRolePers
UNION ALL
SELECT RP.idPers,
CASE
WHEN CHARINDEX(',', LesAutresRoles) > 0 THEN
LTRIM(SUBSTRING(LesAutresRoles, 1, CHARINDEX(',', LesAutresRoles) - 1))
ELSE LesAutresRoles
END,
CASE
WHEN CHARINDEX(',', LesAutresRoles) > 0 THEN
LTRIM(SUBSTRING(LesAutresRoles, CHARINDEX(',', LesAutresRoles) + 1,
LEN(LesAutresRoles) - CHARINDEX(',', LesAutresRoles)))
ELSE NULL
END
FROM TRolePers RP
INNER JOIN T
ON T.idPers = RP.idPers
WHERE LesAutresRoles IS NOT NULL
)
SELECT DISTINCT idPers, UnRole As Role
FROM T
ORDER BY 1, 2

idPers Role
----------- --------------------------------------------------
1 RespX
1 RespY
2 Admin
3 RespX
3 respY
3 RespZ

(6 ligne(s) affectée(s))
X. Quatrième exemple : concaténer des mots pour former une
phrase
Soit la table :

CREATE TABLE T_PHRASE_PHR


(PHR_ID INTEGER NOT NULL,
PHR_MOT_POSITION INTEGER NOT NULL,
PHR_MOT VARCHAR(32) NOT NULL,
CONSTRAINT PK_PHR PRIMARY KEY (PHR_ID, PHR_MOT_POSITION));

Contenant :

INSERT INTO T_PHRASE_PHR VALUES (1, 1, 'Le')


INSERT INTO T_PHRASE_PHR VALUES (1, 2, 'petit')
INSERT INTO T_PHRASE_PHR VALUES (1, 3, 'chat')
INSERT INTO T_PHRASE_PHR VALUES (1, 4, 'est')
INSERT INTO T_PHRASE_PHR VALUES (1, 5, 'mort')
INSERT INTO T_PHRASE_PHR VALUES (2, 1, 'Les')
INSERT INTO T_PHRASE_PHR VALUES (2, 2, 'sanglots')
INSERT INTO T_PHRASE_PHR VALUES (2, 3, 'longs')
INSERT INTO T_PHRASE_PHR VALUES (2, 4, 'des')
INSERT INTO T_PHRASE_PHR VALUES (2, 5, 'violons')
INSERT INTO T_PHRASE_PHR VALUES (2, 6, 'de')
INSERT INTO T_PHRASE_PHR VALUES (2, 7, 'l''')
INSERT INTO T_PHRASE_PHR VALUES (2, 8, 'automne')
INSERT INTO T_PHRASE_PHR VALUES (2, 9, 'blessent')
INSERT INTO T_PHRASE_PHR VALUES (2, 10, 'mon')
INSERT INTO T_PHRASE_PHR VALUES (2, 11, 'coeur')
INSERT INTO T_PHRASE_PHR VALUES (2, 12, 'd''')
INSERT INTO T_PHRASE_PHR VALUES (2, 13, 'une')
INSERT INTO T_PHRASE_PHR VALUES (2, 14, 'langueur')
INSERT INTO T_PHRASE_PHR VALUES (2, 15, 'monotone')

Trouver une requête SQL permettant de recomposer les chaines


de caractères en lignes, la phrase étant composée des mots
dans l'ordre de la colonne PHR_MOT_POSITION. Comment obtenir
le résultat suivant ?
id PHRASE
----------- ------------------------------------------------------------------------------------------
1 Le petit chat est mort.
2 Les sanglots longs des violons de l'automne blessent mon coeur d'une langueur
monotone.

Solution :

WITH
phrases (phrase, id, position)
AS
(
SELECT CAST(PHR_MOT AS VARCHAR(max))
+ CASE
WHEN SUBSTRING(PHR_MOT, LEN(PHR_MOT), 1) = '''' THEN ''
ELSE ' '
END, PHR_ID, PHR_MOT_POSITION
FROM T_PHRASE_PHR
WHERE PHR_MOT_POSITION = 1
UNION ALL
SELECT phrase + CAST(PHR_MOT AS VARCHAR(max))
+ CASE
WHEN SUBSTRING(PHR_MOT, LEN(PHR_MOT), 1) = '''' THEN ''
ELSE ' '
END AS PHRASE,
PHR_ID, PHR_MOT_POSITION
FROM T_PHRASE_PHR AS suiv
INNER JOIN phrases
ON suiv.PHR_ID = phrases.id
AND suiv.PHR_MOT_POSITION = phrases.position + 1
),
maxphrase
AS
(
SELECT id, MAX(position) AS maxposition
FROM phrases
GROUP BY id
)
SELECT P.id, RTRIM(phrase) + '.' AS PHRASE
FROM phrases AS P
INNER JOIN maxphrase AS M
ON P.id = M.id
AND P.position = M.maxposition
ORDER BY id
id PHRASE
----------- ------------------------------------------------------------------------------------------
1 Le petit chat est mort.
2 Les sanglots longs des violons de l'automne blessent mon coeur d'une langueur
monotone.

(2 ligne(s) affectée(s))
*/

XI. Que faire de plus ?


En fait, une chose qui a limité notre processus de recherche
dans notre réseau autoroutier, c'est que nous avons inséré les
routes en sens unique. Je veux dire par là que nos données
permettent d'aller de Paris à Lyon, mais pas de Lyon à Paris.
Pour cela nous devons ajouter les routes inverses :

JNY_FROM_TOWN JNY_TO_TOWN JNY_KM


-------------- ------------ ---------
LYON PARIS 470

Cela peut être fait par une requête on ne peut plus simple :
Exemple 26

INSERT INTO T_JOURNEY


SELECT JNY_TO_TOWN, JNY_FROM_TOWN, JNY_KM
FROM T_JOURNEY

Mais dès lors nos requêtes précédentes sont prises en défaut :

Exemple 27

WITH journey (TO_TOWN)


AS
(SELECT DISTINCT JNY_FROM_TOWN
FROM T_JOURNEY
WHERE JNY_FROM_TOWN = 'PARIS'
UNION ALL
SELECT JNY_TO_TOWN
FROM T_JOURNEY AS arrival
INNER JOIN journey AS departure
ON departure.TO_TOWN = arrival.JNY_FROM_TOWN)
SELECT *
FROM journey

TO_TOWN
--------------------------------
PARIS
NANTES
CLERMONT-FERRAND
LYON
....
LYON
MONTPELLIER
MARSEILLE
PARIS

Msg 530, Level 16, State 1, Line 1


The statement terminated. The maximum recursion 100 has been exhausted before
statement completion.

Que s'est-il passé ? Très simplement, le système essaye toutes


les routes, y compris les trajets "ping-pong" comme Paris,
Lyon, Paris, Lyon, Paris... ad infinitum... Est-il possible de
se débarrasser des trajets "cycliques" ? Sans doute. Dans
l'une de nos précédentes requête nous avons utilisé une
colonne donnant la liste des étapes du trajet. Pourquoi ne pas
l'utiliser pour empêcher un cycle de se produire ? La
condition serait : ne pas passer par une ville dont le nom se
trouve déjà dans la liste de ville du chemin (WAY). Ce qui
peut s'écrire comme ceci :
Exemple 28

WITH journey (TO_TOWN, STEPS, DISTANCE, WAY)


AS
(SELECT DISTINCT JNY_FROM_TOWN, 0, 0, CAST('PARIS' AS
VARCHAR(MAX))
FROM T_JOURNEY
WHERE JNY_FROM_TOWN = 'PARIS'
UNION ALL
SELECT JNY_TO_TOWN, departure.STEPS + 1,
departure.DISTANCE + arrival.JNY_KM,
departure.WAY + ', ' + arrival.JNY_TO_TOWN
FROM T_JOURNEY AS arrival
INNER JOIN journey AS departure
ON departure.TO_TOWN = arrival.JNY_FROM_TOWN
WHERE departure.WAY NOT LIKE '%' + arrival.JNY_TO_TOWN + '%')
SELECT *
FROM journey
WHERE TO_TOWN = 'TOULOUSE'

TO_TOWN STEPS DISTANCE WAY


-------- ----- -------- ------------------------------------------------------
TOULOUSE 3 1015 PARIS, LYON, MONTPELLIER, TOULOUSE
TOULOUSE 4 1485 PARIS, LYON, MONTPELLIER, CLERMONT-FERRAND,
TOULOUSE
TOULOUSE 2 795 PARIS, CLERMONT-FERRAND, TOULOUSE
TOULOUSE 3 995 PARIS, CLERMONT-FERRAND, MONTPELLIER,
TOULOUSE

Comme vous pouvez le constater, une nouvelle route apparaît.


C'est la plus longue, mais peut-être la plus belle !

XII. CONCLUSIONS
Avec les CTE et donc la possibilité d'écrire des requêtes
récursives, le langage SQL devient un langage complet capable
de traiter en une seule requête le problème le plus complexe
qui soit.

L'expression de table CTE peut simplifier l'écriture de


requêtes complexes. Les requêtes récursives ne doivent être
employées que lorsque la récursivité apparaît comme la seule
solution. Si vous faites une erreur dans l'écriture de votre
requête récursive, n'ayez pas peur, par défaut le nombre de
cycles de récursion est limité à 100. Vous pouvez outrepasser
cette limite en fixant vous-même la valeur à l'aide de la
clause OPTION (MAXRECURSION n), qui doit figurer en dernier
dans la requête.

Enfin, sachez que la norme SQL:1999 propose des compléments


syntaxiques pour piloter votre SQL récursif. Par exemple vous
pouvez naviguer dans les données DEPTH FIRST ou BREADTH FIRST
(en profondeur ou en largeur en premier lieu) et aussi
constituer une colonne contenant toutes les données des étapes
intermédiaires dans un tableau de ligne (ARRAY of ROW) dont la
taille doit être "suffisante" pour couvrir tous les cas de
figure.

Voici la syntaxe complète de la CTE avec récursivité :

WITH [ RECURSIVE ] [ ( <liste_colonne> ) ]


AS ( <requete_select> )
[ <clause_cycle_recherche> ]

with :
<clause_cycle_recherche> ::=
<clause_recherche>
| <clause_cycle>
| <clause_recherche> <clause_cycle>
and :
<clause_recherche> ::=
SEARCH { DEPTH FIRTS BY
| BREADTH FIRST BY } <liste_specification_ordre>
SET <colonne_sequence>

<clause_cycle> ::=
CYCLE <colonne_cycle1> [ { , <colonne_cycle2> } ... ]
SET <colonne_marquage_cycle>
TO <valeur_marque_cycle>
DEFAULT <valeur_marque_non_cycle>
USING <colonne_chemin>

XIII. En bonus (CTE, requête récursive appliquée)


Voici une procédure stockée qui donne l'ordre exact des DELETE
à effectuer dans des tables afin de vider une table de ses
données, lorsque cette dernière est liée par l'intégrité
référentielle :
Exemple 29

CREATE PROCEDURE P_WHAT_TO_DELETE_BEFORE


@TABLE_TO_DELETE VARCHAR(128), -- target table to delete
@DB VARCHAR(128), -- target database
@USR VARCHAR(128) -- target schema (dbo in most cases)
AS

WITH

T_CONTRAINTES (table_name, father_table_name)


AS (SELECT DISTINCT CTU.TABLE_NAME, TCT.TABLE_NAME
FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS RFC
INNER JOIN INFORMATION_SCHEMA.CONSTRAINT_TABLE_USAGE
CTU
ON RFC.CONSTRAINT_CATALOG = CTU.CONSTRAINT_CATALOG
AND RFC.CONSTRAINT_SCHEMA = CTU.CONSTRAINT_SCHEMA
AND RFC.CONSTRAINT_NAME = CTU.CONSTRAINT_NAME
INNER JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS TCT
ON RFC.UNIQUE_CONSTRAINT_CATALOG =
TCT.CONSTRAINT_CATALOG
AND RFC.UNIQUE_CONSTRAINT_SCHEMA =
TCT.CONSTRAINT_SCHEMA
AND RFC.UNIQUE_CONSTRAINT_NAME =
TCT.CONSTRAINT_NAME
WHERE CTU.TABLE_CATALOG = @DB
AND CTU.TABLE_SCHEMA = @USR),

T_TREE_CONTRAINTES (table_to_delete, level)


AS (SELECT DISTINCT table_name, 0
FROM T_CONTRAINTES
WHERE father_table_name = @TABLE_TO_DELETE
UNION ALL
SELECT priorT.table_name, level - 1
FROM T_CONTRAINTES priorT
INNER JOIN T_TREE_CONTRAINTES beginT
ON beginT.table_to_delete = priorT.father_table_name
WHERE priorT.father_table_name <> priorT.table_name)

SELECT DISTINCT *
FROM T_TREE_CONTRAINTES
ORDER BY level

GO
Le cas d'auto-référence est en principe intégré. Les
paramètres sont :
@DB : nom de la base,
@USR : nom du schema, par défaut dbo,
@TABLE_TO_DELETE : nom de la table que vous voulez vider

Comment insérer une valeur implicite dans un


champs auto-incrémenté ?[haut]

auteur : Wolo Laurent


SET IDENTITY_INSERT Autorise l'insertion de valeurs
explicites dans la colonne d'identité d'une table.
Exemple :

SET IDENTITY_INSERT products ON


GO
INSERT INTO products (id, product)
VALUES(3, 'garden shovel').
GO
SET IDENTITY_INSERT product OFF
GO

Cette façon de faire va à l'encontre du comportement


même de l'identity et ne devrait être utilisée
qu'exceptionnellement.

Comment connaître la valeur récente inserée dans


un champs auto-incrémenté ?[haut]

auteur : Wolo Laurent


Il suffit de consulter la valeur de la variable de
sessions @@IDENTITY juste après l'insertion, faire :

Select @@Identity as Dernière_Valeur_AutoIncrémenté


Utiliser la fonction IDENT_CURRENT pour Renvoie la
dernière valeur d'identité générée
pour une table spécifiée dans n'importe quelles sessions
et portée.

SELECT IDENT_CURRENT('t_produit')

Comment remettre à zéro la valeur d'un compteur


autoincrémenté ?[haut]

auteur : Wolo Laurent


Vous pouvez supprimer la table puis la recréer.
Mais, je préfère supprimer les données de la table puis
redéfinir la valeur de l'auto-incrément par :

DBCC CHECKIDENT ('MaTable', RESEED, 1)

Le présent tableau fait une synthèse des fonctions de la


norme SQL, mais aussi des fonctions que l'on trouve dans
les principaux dialectes des SGBDR que sont Paradox,
Access, MySQL, PostGreSQL, SQL Server, Oracle et
InterBase.
Ce tableau ne prétend pas à l'exhaustivité mais permet
la comparaison de l'implémentation des fonctions du SQL
dans différents dialectes.

1. Les fonctions dans SQL


1.1. Agrégation statistique
1.2. Fonction "système"
1.3. Fonctions générales
1.4. Fonctions de chaînes de caractères
1.5. Fonctions de chaînes de bits
1.6. Fonctions numériques
1.7. Fonctions temporelles
1.8. Prédicat, opérateurs et structures diverses
1.9. Sous requêtes
1. Les fonctions dans SQL
Légende :

O : Oui
N : Non
X : Existe mais syntaxe hors norme
! : Même nom mais fonction différente

1.1. Agrégation statistique


FonctionDescriptionNorme
SQLParadoxAccessMySQLPostGreSQLSQL
ServerOracleInterbase
AVGMoyenneOOOOOOOO
COUNTNombreOOXOOOOO
MAXMaximumOOOOOOOO
MINMinimumOOOOOOOO
SUMTotalOOOOOOOO

1.2. Fonction "système"


FonctionDescriptionNorme
SQLParadoxAccessMySQLPostGreSQLSQL
ServerOracleInterbase
CURRENT_DATEDate couranteONNOONNO
CURRENT_TIMEHeure couranteONNOONNO
CURRENT_TIMESTAMPDate et heure couranteONNOOONO
CURRENT_USERUtilisateur courantONNNOONN
SESSION_USERUtilisateur autoriséONNXOONN
SYSTEM_USERUtilisateur systèmeONNXOONN
CURDATEDate du jourNNNONNNN
CURTIMEHeure couranteNNNONNNN
DATABASENom de la bases de données
couranteNNNONOON
GETDATEHeure et date couranteNNNNNONN
NOWHeure et date couranteNOOOOOON
SYSDATEDate et/ou heure couranteNNNONNON
TODAYDate du jourNONNNNNN
USERUtilisateur courantNNNONOOO
VERSIONVersion du SGBDRNNNOONNN

1.3. Fonctions générales


FonctionDescriptionNorme
SQLParadoxAccessMySQLPostGreSQLSQL
ServerOracleInterbase
CASTTranstypageOONOOOOO
COALESCEValeur non NULLONNOOONN
NULLIFValeur NULLONNOOONN
OCTET_LENGTHLongueur en octetONNOONON
DATALENGTHLongueurNNNNNONN
DECODEFonction conditionnelleNNNNNNON
GREATESTPlus grande valeurNNNONNON
IFNULLValeur non NULLNNNOOONN
LEASTPlus petite valeurNNNNONON
LENGTHLongueurNNOOOOON
NVLValeur non NULLNNNNNNON
TO_CHARConversion de données en chaîneNNNNNNON
TO_DATEConversion en dateNNNNONON
TO_NUMBERConversion en nombreNNNNNNON

1.4. Fonctions de chaînes de caractères


FonctionDescriptionNorme
SQLParadoxAccessMySQLPostGreSQLSQL
ServerOracleInterbase
||ConcaténationOONXONOO
CHAR_LENGTHLongueur d'une chaîneONNXONNN
CHARACTER_LENGTHLongueur d'une chaîneONNOOONN
COLLATESubstitution à une séquence de
caractèresONNNNNNO
CONCATENATEConcaténationONNNNONN
CONVERTConversion de format de caractèresONNNN!OO
LIKE (prédicat)Comparaison partielleOOXOOOOO
LOWERMise en minusculeOONOOOON
POSITIONPosition d'une chaîne dans une sous
chaîneONNOONNN
SUBSTRINGExtraction d'une sous chaîneOONOONNN
TRANSLATEConversion de jeu de caractèresONNNXNXN
TO_CHARConversion de données en chaîneNNNNNNON
TRIMSuppression des caractères inutilesOONOONON
UPPERMise en majusculeOONOOOOO
CHARConversion de code en caractère ASCIINNOONONN
CHAR_OCTET_LENGTHLongueur d'une chaîne en
octetsNNNNNONN
CHARACTER_MAXIMUM_LENGTHLongueur maximum d'une
chaîneNNNNNONN
CHARACTER_OCTET_LENGTHLongueur d'une chaîne en
octetsNNNNNONN
CONCATConcaténationNNOONOON
ILIKELIKE insensible à la casseNNNNONNN
INITCAPInitiales en majusculeNNNNONON
INSTRPosition d'une chaîne dans une autreNNOONNON
LCASEMise en minusculeNNOONOON
LOCATEPosition d'une chaîne dans une autreNOOONOON
LPADRemplissage à gaucheNNNOONON
LTRIMTRIM à gaucheNOOOOOON
NCHARConversion de code en caractère
UNICODENNNNNONN
PATINDEXPosition d'un motif dans une
chaîneNNNNNONN
REPLACERemplacement de caractèresNNNONOON
REVERSERenversementNNNONOON
RPADRemplissage à droiteNNNOONON
RTRIMTRIM à droiteNNOOOOON
SPACEGénération d'espacesNNOONOON
SUBSTRExtraction d'une sous chaîneNNNNNNON
UCASEMise en majusculeNNOONOON

1.5. Fonctions de chaînes de bits


FonctionDescriptionNorme
SQLParadoxAccessMySQLPostGreSQLSQL
ServerOracleInterbase
BIT_LENGTHLongueur en bitONNNNNNN
&"et" pour bit logique NN???O??
|"ou" pour bit logiqueNN???O??
^"ou" exclusif pour bit logiqueNN???O??

1.6. Fonctions numériques


FonctionDescriptionNorme
SQLParadoxAccessMySQLPostGreSQLSQL
ServerOracleInterbase
%ModuloNNNOOONN
+ - * / ( )Opérateurs et parenthésageOOOOOOOO
ABSValeur absolueNNOOOOON
ASCIIConversion de caractère en code ASCIINNOOOOON
ASINAngle de sinusNNNOOOON
ATANAngle de tangenteNNNOOOON
CEILINGValeur approchée hauteNNOONONN
COSCosinusNNOOOOON
COTCotangenteNNOOOONN
EXPExponentielleNNOOOOON
FLOORValeur approchée basseNNOOOOON
LNLogarithme népérienNNNNNNON
LOGLogarithme népérienNNOONOON
LOG(n,m)Logarithme en base n de mNNNNONON
LOG10Logarithme décimalNNNONOON
MODModuloNNOOOOON
PIPiNNNOOOON
POWERElévation à la puissanceNNOONOON
RANDValeur aléatoireNNOONONN
ROUNDArrondiNNOOOONN
SIGNSigneNNOOOOON
SINSinusNNOOOOON
SQRTRacine carréeNNOOOONN
TANTangenteNNOOOOON
TRUNCTroncatureNNNNNNON
TRUNCATETroncatureNN OOOON
UNICODEConversion de caractère en code
UNICODENNNNNO?N

1.7. Fonctions temporelles


FonctionDescriptionNorme
SQLParadoxAccessMySQLPostGreSQLSQL
ServerOracleInterbase
EXTRACTPartie de dateOONOONON
INTERVAL (opérations sur)DuréeONNNNNON
OVERLAPS (prédicat)Recouvrement de périodeONNNONNN
ADDDATEAjout d'intervalle à une dateNNNONNNN
AGEAgeNNNNONNN
DATE_ADDAjout d'intervalle à une dateNNNONNNN
DATE_FORMATFormatage de dateNNNONNNN
DATE_PARTPartie de dateNNNNONNN
DATE_SUBRetrait d'intervalle à une dateNNNONNNN
DATEADDAjout de dateNNNNNONN
DATEDIFFRetrait de dateNNNNNONN
DATENAMENom d'une partie de dateNNNNNONN
DATEPARTPartie de dateNNNNNONN
DAYJour d'une dateNNNNNONN
DAYNAMENom du jourNNOONONN
DAYOFMONTHJour du moisNNNONNNN
DAYOFWEEKJour de la semaineNNNONNNN
DAYOFYEARJour dans l'annéeNNNONNNN
HOURExtraction de l'heureNNOONONN
LAST_DAYDernier jour du moisNNNNNNON
MINUTE NNOONONN
MONTHMois d'une dateNNOONOON
MONTH_BETWEENMONTH_BETWEENN NN
MONTHNAMENom du moisNNOONONN
NEXT_DAYProchain premier jour de la
semaineNNNNNNON
SECONDExtrait les secondesNNOONONN
SUBDATERetrait d'intervalle à une dateNNNONNNN
WEEKNuméro de la semaineNNOONOON
YEARAnnée d'une dateNNOONOON

1.8. Prédicat, opérateurs et structures diverses


FonctionDescriptionNorme
SQLParadoxAccessMySQLPostGreSQLSQL
ServerOracleInterbase
CASEStructure conditionnelleONNOOOXO
IS [NOT] TRUEVraiONNNNNNN
IS [NOT] FALSEFauxONNNNNNN
IS [NOT] UNKNOWNInconnuONNNNNNN
IS [NOT] NULLNULLOOXOOOOO
INNER JOINJointure interneOOOOOONO
LEFT, RIGHT, FULL OUTER JOINJointure
externeOOOOOONO
NATURAL JOINJointure naturelleONNOONNN
UNION JOINJointure d'unionONNNNNNN
LEFT, RIGHT, FULL OUTER NATURAL JOINJointure
naturelle externeONNXONNN
INTERSECTIntersection (ensemble)O?NNONXN
UNIONUnion (ensemble)O?ONOOOO
EXCEPTDifférence (ensemble)O?NNONNN
[NOT] INListeOOOXOOOO
[NOT] BETWEENFourchette OOOOOOO
[NOT] EXISTSExistenceO??NOOOO
ALLComparaison à toutes les valeurs d'un
ensembleO?ONOOOO
ANY / SOMEComparaison à au moins une valeur de
l'ensembleO?ONOOOO
UNIQUEExistance sans doublonsONNNNNNN
MATCH UNIQUECorrespondanceONNNNNNN
row value construteurConstruteur de ligne
valuéesONNNNNON
MINUSDifférence (ensemble)NNNNONON
LIMITEnombre de ligne
retournéeNNTOPLIMITLIMITTOPNROWS
identifiant de ligne NNN_rowidoidNrowid?

Sommaire > Administration de la base de données > Sauvegardes


et restauration
Comment sauvegarder une base de données
Comment restaurer une base de données depuis un
fichier .bak ?
Comment faire une copie de ma base de données ?
Comment savoir si je suis en mode recouvrement de type
FULL ?
Comment configurer une base de données en mode FULL
RECOVERY ?
Comment connaîte le jeu de caractère et le tri
configuré sur son serveur ?
Comment faire un export complet d'une base ( structure
avec clés, procédures etc...) y.c. des données qui sont dans
les tables.
Comment retrouver la date de la dernière restauration
d'une base ?
Comment restaurer une base de données depuis des
fichiers .mdb et .ldb ?

Comment sauvegarder une base de données [haut]

auteur : Fabien Celaia


Via la commande backup

backup database MaBase to DISK=N'c:\temp\MonFichier.bak'

lien : Déplacement, sauvegardes et restauration de


bases sous MS-SQL Server

Comment restaurer une base de données depuis un


fichier .bak ?[haut]

Via la commande restore, en ayant au préalable supprimé


toutes les connexions existantes sur la base qui sera
écrasée
restore database MaBase from DISK=N'c:\temp\MonFichier.bak'

lien : Déplacement, sauvegardes et restauration de


bases sous MS-SQL Server

Comment faire une copie de ma base de données


?[haut]

backup database MaBase to DISK=N'c:\temp\Mabase.bak'


restore database MaCopie from DISK=N'c:\temp\Mabase.bak'

lien : Déplacement, sauvegardes et restauration de


bases sous MS-SQL Server

Comment savoir si je suis en mode recouvrement de


type FULL ?[haut]

SELECT DATABASEPROPERTYEX(DB_NAME(), 'Recovery')

Comment configurer une base de données en mode


FULL RECOVERY ?[haut]

ALTER DATABASE MABASE SET RECOVERY FULL

Comment faire un export complet d'une base (


structure avec clés, procédures etc...) y.c. des
données qui sont dans les tables.[haut]
Comment retrouver la date de la dernière
restauration d'une base ?[haut]

USE VotreBase
GO
SELECT destination_database_name,
restore_date,
b.database_name,
physical_name,
backup_start_date
FROM msdb.dbo.RestoreHistory h
INNER JOIN msdb.dbo.BackupSet b ON h.backup_set_id = b.backup_set_id
INNER JOIN msdb.dbo.BackupFile f ON f.backup_set_id = b.backup_set_id
WHERE b.database_name = db_name()
GO

Si par contre vous avez effectué a restauration via la


ligne de commande ou l'assistant interactif, ne vous
restera plus qu'à aller rechercher dans les journaux

Comment restaurer une base de données depuis des


fichiers .mdb et .ldb ?[haut]

auteur : Fabien Celaia


Ces fichiers sont cex d'une base de données qui a été
détachée d'un serveur par l'ordre sp_detache_db.

Pour les rattacher à votre serveur SQL, utilisez la


procédure stockée sp_attach_db
EXEC sp_attach_db @dbname ='AdventureWorks',
@filename1 = 'D:\Program Files\Microsoft SQL
Server\MSSQL.1\MSSQL\Data\AdventureWorks_Data.mdf',
@filename2 = 'D:\Program Files\Microsoft SQL
Server\MSSQL.1\MSSQL\Data\AdventureWorks_Log.ldf'

lien : Déplacement, sauvegardes et restauration de


bases sous MS-SQL Server
lien : Attacher une base de données dont le journal de
transaction est manquant

transact sql

1. Positionnement du langage
1.1. Historique
1.2. Utilisation
1.3. Conclusion
2. Syntaxe
2.1. Identifiant
2.2. Variables
2.3. Structures basiques
2.4. Variable de retour
2.5. Variables "système"
2.6. Flags
2.7. Batch et GO
2.8. Quelques fonctions de base
2.9. Commentaires
3. UDF : fonction utilisateur
3.1. Fonction renvoyant une valeur
3.2. Fonction renvoyant une table
4. Procédures stockées
4.1. Entête de procédure
4.2. Paramètres, variables de retour et ensemble de
données
4.3. Gestion des erreurs
4.4. Procédures stockées prédéfinies
4.5. Verrouillage
4.6. Gestion de transactions
4.7. Les curseurs
5. Les triggers
5.1. Mise en place d'un trigger
5.2. Syntaxe d'un trigger MS SQL Server 2000
5.3. Eléments du langage spécifique aux triggers
5.3.1. Pseudo tables INSERTED et DELETED
5.3.2. Fonctions UPDATE et COLUMNS_UPDATED
5.3.3. Annulation des effets d'un trigger
5.4. Exemples de triggers
6. Cryptage du code, liaison au schema et recompilation
7. ANNEXE BIBLIOGRAPHIQUE

1. Positionnement du langage
Transact SQL est un langage procédural (par opposition à
SQL qui est un langage déclaratif) qui permet de
programmer des algorithmes de traitement des données au
sein des SGBDR Sybase Adaptive Server et Microsoft SQL
Server.

1.1. Historique
Il a été créé par Sybase INC. à la fin des années 80
pour répondre aux besoins d'extension de la
programmation des bases de données pour son SGBDR.

Ce langage est donc commun aux deux produits (MS SQL


Server et Sybase Adaptive) avec des différences mineures
dans les implémentations.

Il sert à programmer des procédures stockées et des


triggeurs (déclencheurs).

Transact SQL n'a aucun aspect normatif contrairement à


SQL. C'est bien un "produit" au sein commercial du
terme. En revanche depuis SQL 2 et plus fortement
maintenant, avec SQL 3, la norme SQL a prévu les
éléments de langage procédural normatif propre au
langage SQL. Mais il y a une très grande différence
entre la norme du SQL procédural et Transact SQL.

D'autres SGBDR utilisent d'autres langages procéduraux


pour l'implémentation de procédures stockées et de
triggers. C'est le cas par exemple du PL/SQL
(Programming Langage) d'Oracle.
Il existe encore peu de SGBDR dont le langage procédural
est calé sur la norme. Mais on peut citer OCELOT (SQL
3), PostGreSQL (SQL 2).

Par rapport à la concurrence voici une notation


approximative (sur 5) des différents langage procéduraux
:

SGBDRLangageRespect normeRichesse
Oracle 8PL/SQL3/55/5
MS SQL Server v7Transact SQL2/53/5
PostGreSQLSQL 24/54/5
OCELOTSQL35/54/5
InterBaseISQL3/53/5

Il s'agit bien entendu d'une notation toute personnelle


parfaitement sujette à caution !

1.2. Utilisation
Transact SQL doit être utilisé :

dans les procédures stockées, pour les calculs portant


sur des données internes à la base, sans considération
de gestion d'IHM. Exemple pré calculs d'agrégats pour
optimisation d'accès...
dans les triggers, pour définir et étendre des règles
portant sur le respect de l'intégrité des données.
Exemple formatage de données, liens d'héritage
exclusif entre tables filles...
En revanche il faut tenter de ne pas l'utiliser pour le
calculs de règles métier, l'idéal étant de déporter cela
dans des objets métiers. Enfin l'usage de trigger pour
de la réplication est à proscrire.

1.3. Conclusion
Nous retiendrons que ce qui compte dans l'étude de ce
langage, c'est plus de comprendre ses grands concepts et
son esprit afin de pouvoir le transposer sur d'autres
SGBDR plutôt que d'apprendre par cœur telle ou telle
instruction, d'autant que la documentation sur ce
langage est assez fournie.

ATTENTION : nous étudirons ici la version Microsoft du


langage Transact SQL

2. Syntaxe

2.1. Identifiant
Voici les règles de codage des identifiants (nom des
objets)
Les identifiants SQL :

ne peuvent dépasser 128 caractères.


Ils doivent commencer par une lettre ou un
"underscore".
Les caractères spéciaux et le blanc ne sont pas admis.

On se contentera d'utiliser les 37 caractères de base


: ['A'..'Z', '0'..'9', '_']
La casse n'a pas d'importance.
Le symbole @ commence le nom de toute variable.
Le symbole dédoublé @@ commence le nom des variables
globales du SGBDR.

Le symbole # commence le nom de toute table temporaire


(utilisateur)

Exemple :

SELECT CURRENT_TIMESTAMP as DateTime


INTO #tempT

SELECT *
FROM #tempTLe symbole dédoublé ## commence le nom de toute table
temporaire globale.
Conséquence : la libération des tables temporaires est
consécutif à la libération des utilisteurs.
Nota : SQL Server utilise un base particulière "tempDB"
pour stocker la définition des objets temporaires.

Attention : pour des raisons de performances il est


fortement déconseillé d'utiliser le SELECT … INTO … qui
n'est d'ailleurs pas normalisé.

Un identifiant est dit "qualifié" s'il fait l'objet


d'une notation pointée définissant le serveur, la base
cible et l'utilisateur :

serveur.base_cible.utilisateur.objetExemple :

SELECT * FROM MonServeur.MaBase.MonUser.MaTableAttention : ceci


suppose que les serveurs soient connus
les uns des autres (procédure de "linkage")
On peut ignorer le serveur, comme l'utilisateur, dans ce
cas c'est l'utilisateur courant qui est pris en compte :

base_cible..objetet au sein de la base :

objetUn identifiant ne doit pas être un mot clef de SQL, ni


un mot clef de SQL Server ni même un mot réservé. Dans
le cas contraire il faut utiliser les guillemets comme
délimiteur.

La notion de constante n'existe pas dans Transact SQL.

2.2. Variables
Les types disponibles sont ceux de SQL :

bit, int, smallint, tinyint, decimal, numeric, money, smallmoney, float, real, datetime,
smalldatetime, timestamp,
uniqueidentifier, char, varchar, text, nchar, nvarchar, ntext, binary, varbinary,
image.Auxquels il faut ajouter le type :

cursorque nous verrons plus en détail.

Les valeurs de types chaînes doivent être délimitées par


des apostrophes.

Une variable est déclarée à tout endroit du code par


l'instruction DECLARE :
Exemple :

DECLARE @maChaine char(32)Une variable est assignée par l'instruction SET :

SET @maChaine = 'toto'Avant toute assignation, une variable déclarée est


marquée à NULL.

Remarques :

il n'existe pas de type "tableau" dans le langage


Transact SQL. Cependant, une table temporaire suffit à
un tel usage.
l'ordre SQL SELECT peut aussi servir à assigner une ou
plusieurs variable, mais dans ce dernier cas il faut
veiller à ce que la réponse à la requête ne produise
qu'une seule ligne
Exemple :

SELECT @NomTable = 'TABLES', @NombreLigne = count(*)


FROM INFORMATION_SCHEMA.TABLESAttention : dans le cas d'une requête
renvoyant
plusieurs lignes, seule la valeur de la dernière ligne
sera récupéré dans la variable.

ASTUCE : si vous désirez concaténer toutes les valeurs


d'une colonne dans une seule variable, vous pouvez
utiliser la construction suivante :
DECLARE @Colonne varchar(8000)
SET @Colonne = ''
SELECT @Colonne = @Colonne +COALESCE(TABLE_NAME + ', ', '')
FROM INFORMATION_SCHEMA.TABLESNOTA : la notion de constante n'existe pas.

2.3. Structures basiques


La structure :

BEGIN
...
ENDpermet de définir des blocs d'instructions.

Les branchements possibles sont les suivants :

IF [NOT] condition instruction [ELSE instruction]


IF [NOT] EXISTS(requête select) instruction [ELSE instruction]
WHILE condition instruction
GOTO etiquette
WAITFOR [DELAY | TIME] tempsLes instructions :

BREAK
CONTINUEpermettent respectivement d'interrompre ou de continuer
autoritairement une boucle.

Remarques :

il n'y a pas de THEN dans le IF du Transact SQL, ni de


DO dans le WHILE
une seule instruction est permise pour les structure
IF et WHILE, sauf à mettre en place un bloc
d'instructions à l'aide d'une structure BEGIN / END
un branchement sur étiquette se précise avec en
suffixant l'identifiant de l'étiquette avec un
caractère deux-points ':'
Exemple :
Il s'agit de donner la liste de tous les nombres
premiers entre 1 et 5000.

Première version en code procédural :

/* recherche de tous les nombres premiers de 1 à 5000 */


/* version procédurale (itérations) */
-- création d'une table provisoire pour stockage des données
create table #n (n int)

declare @n integer, @i integer, @premier bit


set @n = 1
set nocount on
-- un nombre premier n'est divisible que par 1 et lui même
while @n < 5000
BEGIN
-- on pré suppose qu'il est premier
set @premier = 1
set @i = 2
while @i < @n
BEGIN
-- autrement dit, tout diviseur situé entre 2 et lui même moins un
-- fait que ce nombre n'est pas premier
if (@n / @i) * @i = @n
SET @premier = 0
SET @i = @i + 1
END
if @premier = 1
insert into #n VALUES (@n)
SET @n = @n + 1
END

SELECT * FROM #nPar ce code procédural, nous avons utilisé la


formulation suivante : "n est premier si aucun nombre de
2 à n-1 ne le divise".

Une autre façon de faire est de travailler en logique


ensembliste. Si nous disposons d'une table des entiers,
il est alors facile de comprendre que les nombres
premiers sont tous les nombres, moins ceux qui ne sont
pas premiers... Il s'agit ni plus ni moins que de
réaliser une différence ensembliste.
/* recherche de tous les nombres premiers de 1 à 5000 */
/* version ensembliste (requêtes) */
DECLARE @max int
SET @max = 5000
-- cet exemple utilise la logique ensembliste pour calculer tous les nombres entiers
SET NOCOUNT ON
-- table temporaire de stockage des entiers de 1 à 5000
CREATE TABLE #n (n int)
-- boucle d'insertion de 0 à 5000
DECLARE @i int
SET @i = 0
WHILE @i < @max
BEGIN
INSERT INTO #n VALUES (@i)
SET @i = @i + 1
END
-- on prend tous les entiers de la table n moins les entiers de la table n pour
-- lesquels le reste de la division entière (modulo) par un entier moindre donne 0
-- NOTA l'opération MODULO se note % dans Transact SQL
SELECT distinct n
FROM #n
WHERE n not in (SELECT distinct n1.n
FROM #n n1
CROSS JOIN #n n2
WHERE n1.n % n2.n = 0
AND n2.n BETWEEN 2 AND n1.n - 1)
ORDER BY nNotez la différence de vistesse d'exécution entre les
deux manières de faire...
Enfin, on peut encore optimiser l'une et l'autre des
procédure en limitant le diviseur au maximum à
CAST(SQRT(CAST(n2.n AS FLOAT)) AS INTEGER) + 1, car le
plus grand des diviseurs d'un nombre ne peut dépasser sa
racine carrée.

2.4. Variable de retour


Toute procédure stockée renvoie une variable de type
entier pour signaler son état. Si cette variable vaut 0,
la procédure s'est déroulée sans anomalie. Tout autre
valeur indique un problème. Les valeurs de 0 à -99 sont
réservées et celles de 0 à -14 sont prédéfinies. Par
exemple la valeur -5 signifie erreur de syntaxe.
On peut assigner une valeur de retour de procédure à
l'aide de l'instruction RETURN :

RETURN 1445

2.5. Variables "système"


SQL Server défini un grand nombre de "variables système"
c'est à dire des variables définies par le moteur.

En voici quelques unes :

VariableDescription
@@connectionsnombre de connexions actives
@@datefirtspremier jour d'une semaine (1:lundi à
7:dimanche)
@@errorcode de la dernière erreur rencontrée (0 si
aucune)
@@fetch_statusétat d'un curseur lors de la lecture
(0 si lecture proprement exécutée)
@@identitydernière valeur insérée dans une colonne
auto incrémentée pour l'utilisateur en cours
@@max_connectionsnombre maximums d'utilisateurs
concurrents
@@procididentifiant de la procédure stockée en
cours
@@rowcountnombre de lignes concernées par le
dernier ordre SQL
@@servernamenom du serveur SGBDR courant
@@spididentifiant du processus en cours
@@trancountnombre de transaction en cours

2.6. Flags
Pour manipuler les effets de certaines variables, comme
pour paramétrer la base de données, il est nécessaire de
recourir à des "flags".

SET NOCOUNT ON / OFF


empêche/oblige l'envoi au client de messages pour chaque
instruction d'une procédure stockée affichant la ligne «
nn rows affected » à la fin d'une instruction (SELECT,
INSERT, UPDATE, DELETE).
Remarque : il est recommandé de désactiver le comptage
dans les procédures stockées afin d'éviter l'envoi
intempestif de lignes non lues qui génère du trafic
réseau et rallonge les temps d'exécution.

SET ANSI_DEFAULTS ON / OFF


Conformation d'une partie de SQL Server à la norme SQL 2

SET DATEFORMAT {format de date}


Fixation du format de date par défaut

SET IDENTITY_INSERT nomTable ON / OFF


Active, désactive l'insertion automatique de colonne
auto incrémentées (identity) dans la table spécifiée.

Exemple : insertion de lignes dont la clef est spécifiée


dans une table données pourvue d'un auto incrément :

CREATE TABLE T_CLIENT


(CLI_ID INTEGER IDENTITY NOT NULL PRIMARY KEY,
CLI_NOM VARCHAR(32))

SET IDENTITY_INSERT T_CLIENT ON

INSERT INTO T_CLIENT (CLI_ID, CLI_NOM) VALUES (325, 'DUPONT')


INSERT INTO T_CLIENT (CLI_ID, CLI_NOM) VALUES (987, 'MARTIN')

SET IDENTITY_INSERT T_CLIENT OFF

INSERT INTO T_CLIENT (CLI_ID, CLI_NOM) VALUES (512, 'LEVY')


=> Serveur: Msg 544, Niveau 16, État 1, Ligne 1
Impossible d'insérer une valeur explicite dans la colonne identité de la table 'T_CLIENT'
quand IDENTITY_INSERT
est défini à OFF.

2.7. Batch et GO
Un batch est un ensemble d'ordres SQL ou Transact SQL
passé en un lot. Il peut être exécuté en lançant un
fichier vers le moteur SQL ou encore en l'exécutant dans
l'analyseur de requêtes.
la plupart du temps un batch est utilisé pour créer les
objets d'une base et lancer au coup par coup certaines
procédures lourdes (administration notamment).

Le mot clef GO permet de forcer l'exécution de


différents ordres SQL d'un traitement par lot. Hors d'un
contexte de transaction, le serveur peut choisir dans
les différents ordres qui luis ont proposés
simultanément d'exécuter telle ou telle demande dans
l'ordre que bon lui semble.
La présence du mot clef GO dans un fichier d'ordre SQL
passé au serveur permet de lui demander l'exécution
immédiate de cet ordre ou de l'ensemble d'ordres. Dans
ce, cas l'instruction GO doit être spécifié pour chaque
ordre atomique ou bien pour chaque ensemble cohérents.
En fait le mot clef GO agit comme si chaque ordre était
lancé à travers un fichier différent.

Enfin, un batch est exécuté en tout ou rien. Ainsi, en


cas d'erreur de syntaxe par exemple, même sur le dernier
ordre du lot, aucun des ordres n'est exécuté.

Attention :

Certains ordres ne peuvent être passés simultanément


dans le même lot. Par exemple la suppression d'un
objet (DROP) et sa re création immédiatement après,
(CREATE) est source de conflits.
Le mot clef GO n'est valable que pour une exécution
par lot (batch). Il n'est donc pas reconnu au sein
d'une procédure stockée ni dans le code d'un trigger

2.8. Quelques fonctions de base


USE est une instruction permettant de préciser le nom de
la base de données cible. En effet il arrive que l'on
soit obligé de travailler depuis une base pour en
traiter une autre. C'est le cas notamment lorsque l'on
veut créer une base de données et y travailler sur le
champ.
La seule base en standard dans MS SQL Server est la base
"master" qui sert de matrice à toutes les bases créées.
Elle contient les procédures inhérentes au serveur mais
aussi celles inhérentes aux bases nouvellement créées,
ainsi que les méta données spécifiques de la nouvelle
base (tables dont le nom commence par "syS...").

Exemple : création d'une base de données depuis la base


MASTER et création d'une table dans cette nouvelle base
:

CREATE DATABASE NEW_MABASE


ON
( NAME = 'NEW_MABASE_DB',
FILENAME = 'C:\MaBase.DATA',
SIZE = 100,
MAXSIZE = 500,
FILEGROWTH = 50 )
LOG ON
( NAME = 'NEW_MABASE_LOG',
FILENAME = 'C:\MaBase.LOG',
SIZE = 5,
MAXSIZE = 25,
FILEGROWTH = 5 )
GO

USE NEW_MABASE
GO

CREATE TABLE T_CLIENT


(CLI_ID INTEGER IDENTITY NOT NULL PRIMARY KEY,
CLI_NOM VARCHAR(32))
GOPRINT est une instruction permettant de générer une
ligne en sortie de procédure. Elle doit être réservée
plus à la mise au point des procédures stockées que pour
une utilisation en exploitation.

EXEC est une instruction permettant de lancer une


requête ou une procédure stockée au sein d'une procédure
ou un trigger. La plupart du temps il n'est pas
nécessaire d'utilise l'instruction EXEC, si
l'intégralité de la commande SQL ou de la procédure à
lancer est connu. Mais lorsqu'il s'agit par exemple d'un
ordre SQL contenant de nombreux paramètres, alors il est
nécessaire de le définir dynamiquement.
Exemple : voici une procédure cherchant dans toutes les
colonnes d'une table l'occurrence d'un mot :

DECLARE
@TableName Varchar(128), -- nom de la table passé en argument
@SearchWord Varchar(32) -- mot recherché

Declare @ColumnList varchar(1000) -- liste des noms de colonnes dans


-- lesquels la recherche va s'effectuer
Declare @SQL varchar(1200) -- requête de recherche

-- obtention de la liste des colonnes pour la requête de recherche


SELECT @ColumnList = COALESCE(@ColumnList + ' + COALESCE(',
'COALESCE(') + column_name +', '''')'
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = @TableName
AND DATA_TYPE LIKE '%char%'

PRINT 'INFO - @ColumnList value is : ' + @ColumnList -- juste pour voir !

-- assemblage du texte de la requête de recherche


Set @SQL = 'SELECT * FROM '+ @TableName
+ ' WHERE ' + @ColumnList
+ ' LIKE ''%' + @SearchWord +'%'''

PRINT 'INFO - @SQL value is : ' + @SQL -- juste pour voir !

-- exécution de la requête de recherche


Exec (@SQL)

2.9. Commentaires
Comme dans le cadre du langage SQL de base, pour placer
des commentaries il suffit d'utiliser les marqueurs
suivants :

DébutFinCommentaire
-- pour une ligne de commentaire
/**/pour un bloc de ligne ou de caractères

3. UDF : fonction utilisateur


Une UDF, autrement dit User Define Function ou Function
Définie par l'Utilisateur est une fonction que le
concepteur de la base écrit pour des besoins de
traitement au sein des requêtes et du code des procdures
stockées ou des triggers. Elle fait donc partie
intégrante de la base ou elle est considérée comme un
objet de la base au même titre qu'une table, une vue, un
utilisateur ou une procédure stockée.

Il existe deux grands types de fonctions : celles


renvoyant une valeur et celles renviyant un jeu de
données (table).

3.1. Fonction renvoyant une valeur


La syntaxe est assez simple :

CREATE FUNCTION [ utilisateur. ] nom_fonction


( [ { @parametre1[AS] type [ = valeur_défaut ] } [ , @parametre2 ... ] ] )

RETURNS type_résultant

[ AS ]

BEGIN

code

RETURN valeur_résultante

ENDExemple, calcul de la date de pâque pour une année


donnée :

CREATE FUNCTION FN_PAQUE (@AN INT)

RETURNS DATETIME

AS

BEGIN

-- Algorithme conçu par Claus Tøndering .


-- Copyright (C) 1998 by Claus Tondering.
-- E-mail: claus@tondering.dk.
IF @AN IS NULL
RETURN NULL

DECLARE @G INT
DECLARE @I INT
DECLARE @J INT
DECLARE @C INT
DECLARE @H INT
DECLARE @L INT
DECLARE @JourPaque INT
DECLARE @MoisPaque INT
DECLARE @DimPaque DATETIME

SET @G = @AN % 19
SET @C = @AN / 100
SET @H = (@C - @C / 4 - (8 * @C + 13) / 25 + 19 * @G + 15) % 30
SET @I = @H - (@H / 28) * (1 - (@H / 28) * (29 / (@H + 1)) * ((21 - @G) / 11))
SET @J = (@AN + @AN / 4 + @I + 2 - @C + @C / 4) % 7

SET @L = @I - @J
SET @MoisPaque = 3 + (@L + 40) / 44
SET @JourPaque = @L + 28 - 31 * (@MoisPaque / 4)

SET @DimPaque = CAST(CAST(@AN AS VARCHAR(4)) +


CASE
WHEN @MoisPaque < 10 THEN '0' + CAST(@MoisPaque AS CHAR(1))
ELSE CAST(@MoisPaque AS CHAR(2))
END +
CASE
WHEN @JourPaque < 10 THEN '0' + CAST(@JourPaque AS CHAR(1))
ELSE CAST(@JourPaque AS CHAR(2))
END
AS DATETIME)

RETURN @DimPaque

ENDOn utilise une telle fonction au sein d'une requête,


comme une fonction SQL de base, à ceci près que, pour
une raion obscure, il faut la faire précéder du nom du
propriétaire.

Exemple :

SELECT dbo.FN_PAQUE (2004) AS PAQUE_2004PAQUE_2004


-----------------------
2004-04-11 00:00:00.000

3.2. Fonction renvoyant une table


Il existe deux manières de procéder : soit par requête
directe (table en ligne), soit par construction et
alimentation d'une table (table multi instruction).

La première syntaxe (table est ligne) est très simple.


Voici un exemple, qui construit et renvoi une table des
jours de semaine :

CREATE FUNCTION FN_JOUR_SEMAINE ()


RETURNS TABLE
AS
RETURN (SELECT 1 AS N, 'Lundi' AS JOUR
UNION
SELECT 2 AS N, 'Mardi' AS JOUR
UNION
SELECT 3 AS N, 'Mercredi' AS JOUR
UNION
SELECT 4 AS N, 'Jeudi' AS JOUR
UNION
SELECT 5 AS N, 'Vendredi' AS JOUR
UNION
SELECT 2 AS N, 'Samedi' AS JOUR
UNION
SELECT 2 AS N, 'Dimanche' AS JOUR)
SELECT *
FROM dbo.FN_JOUR_SEMAINE ()
N JOUR
----------- --------
1 Lundi
2 Dimanche
2 Mardi
2 Samedi
3 Mercredi
4 Jeudi
5 Vendredi

La seconde syntaxe demande un peu plus de travail, car


elle se base sur une variable "table". En voici la
syntaxe :
CREATE FUNCTION [ utilisateur. ] nom_fonction
( [ { @parametre1[AS] type [ = valeur_défaut ] } [ , @parametre2 ... ] ] )

RETURNS type_résultant TABLE < definition_de_table >

[ AS ]

BEGIN
code

RETURN

ENDVoici un exemple, qui construit une table d'entier


limité à MaxInt et comprenant ou non le zéro (entiers
naturels) :

CREATE FUNCTION FN_ENTIERS (@MAXINT integer, @NATUREL bit = 0)


RETURNS @integers TABLE
(N int PRIMARY KEY NOT NULL)
AS
BEGIN

DECLARE @N INT
DECLARE @T TABLE (N int)
SET @N = 0

-- insertion des 10 premiers chiffres de 0 à 9


WHILE @N < 10
BEGIN
INSERT INTO @T VALUES (@N)
SET @N = @N + 1
END
SET @N = @N -1

-- si @N est supérieur à 9, alors supprimer les valeurs en trop


IF @N > @MAXINT
DELETE FROM @T WHERE N > @MAXINT
ELSE
INSERT INTO @T
SELECT DISTINCT 1 * T1.N + 10 * T2.N + + 100 * T3.N + 1000 * T4.N
FROM @T AS T1
CROSS JOIN @T AS T2
CROSS JOIN @T AS T3
CROSS JOIN @T AS T4
WHERE 1 * T1.N + 10 * T2.N + + 100 * T3.N + 1000 * T4.N BETWEEN 10 AND
@MAXINT
-- s'il s'agit d'entiers naturels, supprimer le zéro
IF @NATUREL = 1
DELETE FROM @T WHERE N = 0

-- insertion dans la variable de retour


INSERT INTO @integers
SELECT DISTINCT N FROM @T

RETURN

END
SELECT *
FROM dbo.FN_ENTIERS (13, 1)
N
-----------
1
2
3
4
5
6
7
8
9
10
11
12
13

NOTA : SQL Server interdit l'utilisation de fonctions


non déterministe au sein des fonctions utilisateur. Par
exemple, la fonction CURRENT_TIMESTAMP qui renvoie la
date/heure courante ne peut être utilisée dans le cadre
de l'écriture d'une UDF. Il y a cependant moyen de
contourner ce problème en utilisant par exemple une
vue...

CREATE VIEW V_DATEHEURE_COURANTE


AS
SELECT CURRENT_TIMESTAMP AS DHC
CREATE FUNCTION FN_DELTA_MONTH (@MaDate DATETIME)
RETURNS INT
AS
BEGIN
DECLARE @N INT

SELECT @N = DATEDIFF(MONTH, @MaDate, DHC)


FROM V_DATEHEURE_COURANTE

RETURN @N
END
SELECT dbo.FN_DELTA_MONTH('20020101')

4. Procédures stockées
Le but d'une procédure stockée est triple :

étendre les possibilités des requêtes, par exemple


lorsqu'un seul ordre INSERT ou SELECT ne suffit plus
faciliter la gestion de transactions
permettre une exécution plus rapide et plus optimisée
de calculs complexes ne portant que sur des données de
la base
Dans ce dernier cas la méthode traditionnelle
nécessitait de nombreux aller et retour entre le client
et le serveur et congestionnait le réseau.

Une procédure stockée est accessible par l'interface de


l'Entreprise Manager. Elle peut aussi être créée par
l'analyseur de requête.

En créant une nouvelle procédure stockée on se trouve


devant une fenêtre de définition de code :

ATTENTION :
Les procédures stockées (v7) sont limitées à :

128 Mo de code
1024 paramètres en argument (y compris curseurs)

4.1. Entête de procédure


Elle commence toujours pas les mots clef CREATE
PROCEDURE suivi du nom que l'on veut donner à la
procédure stockée.
Les paramètres et leur type, s'il y en as suivent le
nom.
L'entête se termine par le mot clef AS.

Exemple :

CREATE PROCEDURE SP_SEARCH_STRING_ANYFIELD


@TableName Varchar(128), -- nom de la table passé en argument
@SearchWord Varchar(32) -- mot recherché
AS
...

4.2. Paramètres, variables de retour et ensemble de


données
Une procédure stockée peut accepter de 0 à 1024
paramètres (arguments). Elle a toujours une valeur de
retour qui par défaut est le code d'exécution (entier
valant 0 en cas de succès). Elle peut retourner des
lignes de table comme une requête.

Pour déclarer les paramètres il faut les lister avec un


nom de variable et un type. Par défaut les paramètres
sont des paramètres d'entrée.
Comme pour toutes les listes le séparateur est la
virgule.
On peut déclarer des valeurs par défaut et spécifier si
le paramètre est en sortie avec le mot clef OUTPUT.

Exemple :

CREATE PROCEDURE SP_SYS_DB_TRANSACTION


@TRANS_NUM INTEGER, -- numéro de la transaction concernée
@OK BIT = 0 OUTPUT -- retour 0 OK, 1 problème
AS
...Pour récupérer la valeur de d'un paramètre OUTPUT il
faut préciser une variable de récupération de nature
OUTPUT dans le lacement de l'exécution.
Exemple :

DECLARE @RetourOK bit


EXEC(SP_SYS_DB_TRANSACTION 127, @RetourOK OUTPUT)
SELECT @RetourOKUn paramètre de type CURSOR (curseur) est un cas
particulier et ne peut être utilisé qu'en sortie et
conjointement au mot clef VARYING qui spécifie que sa
définition précise n'est pas connue au moment de la
compilation (le nombre et le type des colonnes n'est pas
défini à cet instant).

CREATE PROCEDURE SEARCH_TEXT @resultTable CURSOR VARYING OUTPUT


ASUn tel paramètre peut être réutilisé dans une autre
procédure stocké ou dans un trigger.

Comme nous l'avons vu, toute procédure stockée renvoie


une variable de type entier pour signaler son état. Si
cette variable vaut 0, la procédure s'est déroulée sans
anomalie. Tout autre valeur indique un problème. Les
valeurs de 0 à -99 sont réservées et celles de 0 à -14
sont prédéfinies. Par exemple la valeur -5 signifie
erreur de syntaxe. Bien entendu on peut assigner une
valeur de retour de procédure à l'aide de l'instruction
RETURN.

Une procédure stockée peut en outre renvoyer un jeu de


résultat sous la forme d'un ensemble de données à la
manière des résultats de requête.
Exemple :

CREATE PROCEDURE SP_LISTE_CLIENT


@CLI_ID_DEBUT INTEGER, @CLI_ID_FIN INTEGER
AS

SELECT '-- début de liste client --'


UNION ALL
SELECT CLI_NOM
FROM T_CLIENT
WHERE CLI_ID BETWEEN @CLI_ID_DEBUT AND @CLI_ID_FIN
UNION ALL
SELECT '-- fin de liste client --'
4.3. Gestion des erreurs
Il existe différents niveaux d'erreurs et différents
moyens de les gérer.
Considérons une procédure stockée qui aurait pour effet
de supprimer une ligne d'une table en s'assurant de
supprimer préalablement toutes les lignes de tous les
descendants concernés.
L'entête d'une telle procédure pourrait s'écrire :

CREATE PROCEDURE SP_DELETE_CLIENT_RECURSIVE


@CLI_ID INTEGERSi la ligne considérée n'est pas retrouvée dans la table
des clients, comme si la valeur du paramètre est NULL,
cette procédure échouera sans indiquer d'anomalie. Il
faut donc procéder à des tests préalables (nullité,
existence…). Sans cela on dit que l'on à une "exception
silencieuse".

Pour les exceptions plus flagrantes, SQL Server fournit


la variable globale @@error que l'on peut tester à tout
instant. Cette variable est mise à jour à chaque
instruction.

Le code continue de s'exécuter même après une erreur.

Pour gérer les erreurs, SQL Server dispose de la levée


des erreurs à l'aide de l'instruction RAISERROR et
l'utilisation du GOTO combiné à une étiquette de
branchement pour la reprise sur erreur, permet de
centraliser la gestion des erreurs à un seul et même
endroit du programme.

Voici un exemple mettant en œuvre ces principes :

/*---------------------------------------------------\
| recherche d'une occurrence de mot dans n'importe |
| quelle colonne de type caractères d'une table donnée |
|----------------------------------------------------- |
| Frédéric BROUARD - COMMUNICATIC SA - 2001-12-17 |
\-------------------------------------------------- */
CREATE PROCEDURE SP_SEARCH_STRING_ANYFIELD
@TableName Varchar(128), -- nom de la table passé en argument
@SearchWord Varchar(32) -- mot recherché
AS

IF @TableName IS NULL OR @SearchWord IS NULL


RAISERROR ('Paramètres NULL impossible à traiter', 16, 1)
IF @@ERROR <> 0 GOTO LBL_ERROR

-- test d'existence de la table


IF NOT EXISTS(SELECT *
FROM INFORMATION_SCHEMA.tables
WHERE TABLE_NAME = @TableName)
RAISERROR ('Références de table inconnue %s', 16, 1, @TableName)
IF @@ERROR <> 0 GOTO LBL_ERROR

Declare @ColumnList varchar(1000) -- liste des noms de colonnes dans


-- lesquels la recherche va s'effectuer
Declare @SQL varchar(1200) -- requête de recherche

-- obtention de la liste des colonnes pour la requête de recherche


SELECT @ColumnList = COALESCE(@ColumnList + ' + COALESCE(',
'COALESCE(') + column_name +', '''')'
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = @TableName
AND DATA_TYPE LIKE '%char%'

IF @ColumnList IS NULL
RAISERROR ('Aucune colonne de recherche trouvé dans la table %s',
16, 1, @TableName)
IF @@ERROR <> 0 GOTO LBL_ERROR

PRINT 'INFO - @ColumnList value is : ' + @ColumnList

-- assemblage du texte de la requête de recherche


Set @SQL =
'SELECT * FROM '+ @TableName
+ ' WHERE ' + @ColumnList
+ ' LIKE ''%' + @SearchWord +'%'''

PRINT 'INFO - @SQL value is : ' + @SQL

-- exécution de la requête de recherche


Exec (@SQL)
RETURN

LBL_ERROR:
PRINT 'ERREUR LORS DE L''EXÉCUTION DE LA PROCÉDURE STOCKÉE
SP_SEARCH_STRING_ANYFIELD'

4.4. Procédures stockées prédéfinies


SQL Server possède des procédures stockées pré établies,
un peu à la manière d'une bibliothèque d'utilisation.
Elles servent essentiellement à l'administration
(gestion des utilisateurs et des bases) et à
l'information (dictionnaire des données). Elle se
situent toutes dans la base "master".

Ainsi, la procédure stockée "sp_help" fournit une


description de n'importe quel objet de la base.

Pour appeler de telles procédures, il n'est pas


nécessaire de préciser le nom de la base "master", ce
nom est implicite.

Ces procédures stockées permettent :

de piloter le paramétrage du serveur et de la base de


données (respect des normes SQL 2, maximum
d'utilisateurs…) - exemple : sp_configure
de définir les objets de la base (table, index,
contraintes, vues…) - exemple : sp_addtype
d'administrer le serveur, les bases, les accès (rôles,
utilisateurs, login…) - exemple : sp_droplogin
d'obtenir de l'aide et de la documentation (aide en
ligne, explications) - exemple : sp_helpsort
de gérer les agents (alertes et batch) - exemple :
sp_add_job
d'assurer les réplications (entre bases et serveurs) -
exemple : sp_deleteMergeConflictRows
de monitorer le fonctionnement de SQL Server (tracer
l'exécution) - exemple : xp_sqlTrace
...
Nota : des procédures stockées dites étendues (dont le
nom commence généralement par "xp_") font appel à du
code C sous forme de DLL. Exemple : xp_cmdShell, permet
de lancer un programme en ligne de commande depuis SQL
Server (procédure dont l'utilisation est à déconseiller
!)

Exemple - création d'un type utilisateur (DOMAINE SQL 2)


pour spécification numérique de la valeur d'un mois avec
contrôle de validité :

-- ajout du type TT_MONTH en entier obligatoire


SP_addType TT_MONTH, 'INTEGER', 'NOT NULL'

-- creation de la règle R_CHECK_MONTH vérifiant


-- que la valeur soit comprise entre 1 et 12
CREATE RULE R_CHECK_MONTH AS @value BETWEEN 1 AND 12

-- liaison de la règle au type


SP_bindRule R_CHECK_MONTH, TT_MONTH

4.5. Verrouillage
Le verrouillage est la technique de base pour assurer
les concurrences d'accès aux ressources.
On peut observer les verrous posés lors des
manipulations de données à l'aide de la procédure
stockée sp_lock. De même on peut savoir qui utilise quoi
à l'aide de la procédure stockée sp_who.

SQL Server pose en principe les bons verrous lors des


requêtes. Mais il peut arriver que l'on souhaite :

soit se débarrasser des contraintes de verrouillage,


notamment pour faire du "dirty read" (lecture sale)
soit poser des verrous plus contraignant pour
s'assurer du traitement
Dans les deux cas il faut compléter les ordre SQL par
une description des verrous attendus.

Voici les paramètres de verrouillage que l'on peut


spécifier :

VerrouSELECTUPDATE
NOLOCKaucun verrouimpossible
HOLDLOCKmaintient du verrou jusqu'à la fin de la
transactionpas nécessaire
UPDLOCKpose un verrou de mise à jour au lieu d'un
verrou partagéredondant
PAGLOCKforce la pose d'un verrou de pageforce la
pose d'un verrou de page
TABLOCKforce un verrou partagé sur la tableforce
un verrou exclusif sur la table
TABLOCKXforce un verrou exclusif sur la table
pendant toute la durée de la transactionforce un
verrou exclusif sur la table pendant toute la
durée de la transaction

Ces paramètres se précisent dans un ordre SQL après le


nom de la table et le mot clef WITH en utilisant le
parenthèsage.

Exemples :

Accélération d'une extraction de données en demandant la


suppression du verrouillage :

SELECT *
FROM T_CLIENT C WITH (NOLOCK)
JOIN T_FACTURE F WITH (NOLOCK)
ON C.CLI_ID = F.CLI_IDVerrouillage exclusif de la table le temps de la mise à
jour :

UPDATE T_CLIENT WITH (TABLOCK)


SET CLI_NOM = REPLACE(CLI_NOM, ' ', '-')On peut combiner certains paramètres de
verrouillage.
Exemple :

... WITH (PAGLOCK HOLDLOCK) ...Attention : la manipulation des verrous est affaire
de
spécialistes. Une pose de verrous sans étude préalable
de la concurrence d'exécution des différentes procédures
stockées d'une base, peut conduire à des scénarios de
blocage, comme "l'étreinte fatale".
4.6. Gestion de transactions
Toute ordre SQL du DML est une transaction (SELECT
INSERT, UPDATE, DELETE).

Parce que SQL Server fonctionne en "AUTOCOMMIT", une


combinaison de différents ordres nécessite la pose
explicite d'une transaction.
Ceci se fait par l'instruction BEGIN TRANSACTION et doit
être terminé soit par un ROLLBACK, soit par un COMMIT.

Une transaction peut être anonyme ou nommée. De toute


façon SQL Server lui attribue un identifiant interne
unique.

Exemple simple. Il s'agit de réserver 3 place dans le


vol AF714 pour le client 123, s'il y a bien de la place
pour ce vol !

BEGIN TRANSACTION

-- reste t-il de la place pour ce vol ?


IF NOT EXISTS(SELECT *
FROM T_VOL_AVION
WHERE VOL_REF = 'AF714'
AND VOL_PLACE_LIBRE > 3)
THEN
BEGIN
ROLLBACK TRANSACTION
RETURN
END

-- décompte des places vendues


UPDATE T_VOL_AVION
SET VOL_PLACE_LIBRE = VOL_PLACE_LIBRE - 3
WHERE VOL_REF = 'AF714'
IF (@@ERROR <> 0) OR (@@ROWCOUNT = 0)
GOTO ROLLBACK_ON_ERROR

-- génération d'une réservation


INSERT INTO T_RESERVATION (VOL_REF, CLI_ID, PLACES)
VALUES ('AF714', 123, 3)
IF (@@ERROR <> 0) OR (@@ROWCOUNT = 0)
GOTO ROLLBACK_ON_ERROR

-- validation de l'ensemble
COMMIT TRANSACTION
RETURN

-- si branchement à l'étiquette d'erreur alors ROLLBACK


ROLLBACK_ON_ERROR:
ROLLBACK TRANSACTIONSQL Server permet aussi de définir des points de
sauvegarde, que l'on peut considérer comme un validation
partielle d'une transaction. Pour définir un point de
sauvegarde il faut utiliser l'instruction SAVE
TRANSACTION avec un nom de préférence (pas obligatoire).
Par conséquent pour faire un ROLLBACK partiel il suffit
de préciser le nom du point de sauvegarde dans l'ordre
ROLLBACK.

L'utilisation de la variable @@TranCount permet de


savoir le nombre de transaction ouvertes en cours.

Une bonne transaction ne saurait être bien gérée sans


une appréciation claire du niveau d'isolation. SQL
Server gère les 4 niveaux de la norme SQL 2. La commande
SET TRANSACTION ISOLATION LEVEL dont les options sont
READ COMMITTED, READ UNCOMMITTED, REPEATABLE READ
et
SERIALIZABLE permet de définir le niveau d'isolation
d'une transaction.

NiveauEffetsID
READ UNCOMMITTEDImplémente la lecture incorrecte,
ou le verrouillage de niveau 0, ce qui signifie
qu'aucun verrou partagé n'est généré et qu'aucun
verrou exclusif n'est respecté. Lorsque cette
option est activée, il est possible de lire des
données non validées, ou données incorrectes ; les
valeurs des données peuvent être modifiées et des
lignes peuvent apparaître ou disparaître dans le
jeu de données avant la fin de la transaction.
Cette option a le même effet que l'activation de
l'option NOLOCK dans toutes les tables de toutes
les instructions SELECT d'une transaction. Il
s'agit du niveau d'isolation le moins restrictif
parmi les quatre disponibles.0
READ COMMITTEDSpécifie que les verrous partagés
sont maintenus durant la lecture des données pour
éviter des lectures incorrectes. Les données
peuvent néanmoins être modifiées avant la fin de
la transaction, ce qui donne des lectures non
renouvelées ou des données fantômes. Cette option
est l'option SQL Server par défaut.1
REPEATABLE READDes verrous sont placés dans toutes
les données utilisées dans une requête, afin
d'empêcher les autres utilisateurs de les mettre à
jour. Toutefois, un autre utilisateur peut ajouter
de nouvelles lignes fantômes dans un jeu de
données par un utilisateur ; celles-ci seront
incluses dans des lectures ultérieures dans la
transaction courante.2
SERIALIZABLEPlace un verrou sur une plage de
données, empêchant les autres utilisateurs de les
mettre à jour ou d'insérer des lignes dans le jeu
de données, jusqu'à la fin de la transaction. Il
s'agit du niveau d'isolation le plus restrictif
parmi les quatre niveaux disponibles. Utilisez
cette option uniquement lorsque cela s'avère
nécessaire, car la concurrence d'accès est
moindre. Cette option a le même effet que
l'utilisation de l'option HOLDLOCK dans toutes les
tables de toutes les instructions SELECT d'une
transaction.3

Attention : par défaut SQL Server travaille au niveau


READ COMMITTED. Ceci explique sa rapidité comparée a des
serveurs qui fonctionnent par défaut au niveau
d'isolation maximal mais peut s'avérer catastrophique
pour l'intégité des données !!!

CONSEIL : il est toujours préférable d'utiliser la


gestion des transactions que de manipuler les verrous,
sauf en ce qui concerne la "lecture sale" notamment dans
le cadre d'une utilisation client/serveur en mode
déconnecté (par exemple dans le cadre de restitutions
documentaires pour un site web).

NOTA : SQL Server permet de gérer des transactions


distribuées et gère le "commit à deux phases" mais sans
permettre l'utilisation de points de sauvegarde.

Exemple : gestion d'un arbre modélisé par intervalle

Pour une illustration plus complète des procédures


stockées nous allons montrer les différentes procédures
nécessaires pour faire "fonctionner" un arbre modélisé
sous forme intervallaire. Pour une développement plus
complet de ce sujet, lire l'article sur la
représentation intervallaire des arborescences

Dans notre cas nous allons considérer une table


permettant de gérer le développement des projet d'une
entreprise. Voici la table du développement. Elle
contient les informations de chaque noeuds, y compris
son identifiant, sojn libellé, ses bornes droite et
gauche et son niveau :

CREATE TABLE T_DEVELOPPEMENT_DEV


(DEV_ID INTEGER IDENTITY (1, 1) NOT NULL , -- identifiant
DEV_NIVEAU SMALLINT NOT NULL , -- niveau du noeud
DEV_LIBELLE CHAR (32) NOT NULL , -- libellé
DEV_BORNE_GAUCHE INTEGER NOT NULL , -- borne gauche de l'arbre
DEV_BORNE_DROITE INTEGER NOT NULL) -- borne droite de
l'arbreTout d'abord voici la procédure d'insertion d'un fils
ainé, c'est à dire d'un fils qui sera le plus proche du
sommet au moment de l'insertion. On passe à la procédure
l'identifiant du noeud père et le libellé. Une variable
de retour permet de conaître l'état de l'insertion.

CREATE PROCEDURE SP_DEV_INSERTION_ARBRE_FILS_AINE


@id_pere integer,
@libelle varchar(32),
@id_ins integer OUTPUT AS

-- Frédéric BROUARD 17/11/2001


-- insertion sous père dans l'arbre du développement
-- paramètres : id_pere, libelle

DECLARE @bg_pere integer


DECLARE @niveau integer
DECLARE @OK integer

SET NOCOUNT ON

-- démarrage transaction
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRANSACTION INSERT_TREE_FILS_AINE

-- vérification de l'existence du père


SELECT @OK = count(*)
FROM T_DEVELOPPEMENT_DEV
WHERE DEV_ID = @id_pere

-- si élément supprimé, alors retour sans insertion avec valeur -1


IF @OK = 0
BEGIN
SELECT -1
ROLLBACK TRANSACTION INSERT_TREE_FILS_AINE
RETURN
END

-- recherche du bg_pere et calcul du niveau inférieur


SELECT @bg_pere = DEV_BORNE_GAUCHE, @niveau = DEV_NIVEAU + 1
FROM T_DEVELOPPEMENT_DEV
WHERE DEV_ID = @id_pere

-- décalage pour insertion borne droite


UPDATE T_DEVELOPPEMENT_DEV
SET DEV_BORNE_DROITE = DEV_BORNE_DROITE + 2
WHERE DEV_BORNE_DROITE > @bg_pere

IF @@ERROR <>0
BEGIN
ROLLBACK TRANSACTION INSERT_TREE_FILS_AINE
SELECT -1
RETURN
END

-- décalage pour insertion borne gauche


UPDATE T_DEVELOPPEMENT_DEV
SET DEV_BORNE_GAUCHE = DEV_BORNE_GAUCHE + 2
WHERE DEV_BORNE_GAUCHE > @bg_pere

IF @@ERROR <>0
BEGIN
ROLLBACK TRANSACTION INSERT_TREE_FILS_AINE
SELECT -1
RETURN
END

-- insertion
INSERT INTO T_DEVELOPPEMENT_DEV (DEV_NIVEAU, DEV_LIBELLE,
DEV_BORNE_GAUCHE, DEV_BORNE_DROITE)
VALUES (@niveau, @libelle, @bg_pere + 1, @bg_pere + 2)

SELECT @id_ins = @@identity

IF @@ERROR <>0
BEGIN
ROLLBACK TRANSACTION INSERT_TREE_FILS_AINE
SELECT -1
RETURN
END

COMMIT TRANSACTION INSERT_TREE_FILS_IANE

SELECT @id_ins -- renvoie de l'identifiant de l'élément inséré.Pour le fils cadet, la


procédure n'est pas plus
compliquée :

CREATE PROCEDURE SP_DEV_INSERTION_ARBRE_FILS_CADET


@id_pere integer,
@libelle varchar(32),
@id_ins integer OUTPUT AS

-- Frédéric BROUARD 17/11/2001


-- insertion sous père dans l'arbre du développement
-- paramètres : id_pere, libelle

DECLARE @bd_pere integer


DECLARE @niveau integer
DECLARE @OK integer

SET NOCOUNT ON

-- on gère une transaction qui remanie les bornes de l'arbre.


-- il ne faut pas être 'dérangé' par d'autres utilisateurs concurrents pendant cette
manoeuvre
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRANSACTION INSERT_TREE_FILS_CADET
-- vérification de l'existence du père
SELECT @OK = count(*)
FROM T_DEVELOPPEMENT_DEV
WHERE DEV_ID = @id_pere

-- si élément supprimé, alors retour sans insertion avec valeur -1


IF @OK = 0
BEGIN
SELECT -1
ROLLBACK TRANSACTION INSERT_TREE_FILS_CADET
RETURN
END

-- recherche du bg_pere et calcul du niveau inférieur


SELECT @bd_pere = DEV_BORNE_DROITE, @niveau = DEV_NIVEAU + 1
FROM T_DEVELOPPEMENT_DEV
WHERE DEV_ID = @id_pere

-- décalage pour insertion borne droite


UPDATE T_DEVELOPPEMENT_DEV
SET DEV_BORNE_DROITE = DEV_BORNE_DROITE + 2
WHERE DEV_BORNE_DROITE >= @bd_pere

IF @@ERROR <>0
BEGIN
ROLLBACK TRANSACTION INSERT_TREE_FILS_CADET
SELECT -1
RETURN
END

-- décalage pour insertion borne gauche


UPDATE T_DEVELOPPEMENT_DEV
SET DEV_BORNE_GAUCHE = DEV_BORNE_GAUCHE + 2
WHERE DEV_BORNE_GAUCHE > @bd_pere

IF @@ERROR <>0
BEGIN
ROLLBACK TRANSACTION INSERT_TREE_FILS_CADET
SELECT -1
RETURN
END

-- insertion
INSERT INTO T_DEVELOPPEMENT_DEV (DEV_NIVEAU, DEV_LIBELLE,
DEV_BORNE_GAUCHE, DEV_BORNE_DROITE)
VALUES (@niveau, @libelle, @bd_pere , @bd_pere + 1)
SELECT @id_ins = @@identity

IF @@ERROR <>0
BEGIN
ROLLBACK TRANSACTION INSERT_TREE_FILS_CADET
SELECT -1 as ID
RETURN
END

COMMIT TRANSACTION INSERT_TREE_SON

SELECT @id_insVoici maintenant comment on insère un frère à droite


d'un autre :

CREATE PROCEDURE SP_DEV_INSERTION_ARBRE_FRERE_DROIT


@id_frere integer,
@libelle varchar(32),
@id_ins integer OUTPUT AS

-- Frédéric BROUARD 17/11/2001


-- insertion latérale à droite du frère
-- paramètres : id_frere, libelle

DECLARE @bd_frere integer


DECLARE @niveau integer
DECLARE @OK integer

SET NOCOUNT ON

-- démarrage transaction
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRANSACTION INSERT_TREE_RIGHT

-- vérifie de l'existence du frère


SELECT @OK = count(*)
FROM T_DEVELOPPEMENT_DEV
WHERE DEV_ID = @id_frere

-- si élément supprimé, alors retour sans insertion avec valeur -1


IF @OK = 0
BEGIN
SELECT -1
ROLLBACK TRANSACTION INSERT_TREE_RIGHT
RETURN
END

-- recherche du bd_frere et du niveau


SELECT @bd_frere = DEV_BORNE_DROITE, @niveau = DEV_NIVEAU
FROM T_DEVELOPPEMENT_DEV
WHERE DEV_ID = @id_frere

-- décalage borne gauche pour insertion


UPDATE T_DEVELOPPEMENT_DEV
SET DEV_BORNE_GAUCHE = DEV_BORNE_GAUCHE + 2
WHERE DEV_BORNE_GAUCHE > @bd_frere

IF @@ERROR <>0
BEGIN
ROLLBACK TRANSACTION INSERT_TREE_RIGHT
SELECT -1
RETURN
END

-- décalage borne droite pour insertion


UPDATE T_DEVELOPPEMENT_DEV
SET DEV_BORNE_DROITE = DEV_BORNE_DROITE + 2
WHERE DEV_BORNE_DROITE > @bd_frere

IF @@ERROR <>0
BEGIN
ROLLBACK TRANSACTION INSERT_TREE_RIGHT
SELECT -1
RETURN
END

-- insertion
INSERT INTO T_DEVELOPPEMENT_DEV (DEV_NIVEAU, DEV_LIBELLE,
DEV_BORNE_GAUCHE, DEV_BORNE_DROITE)
VALUES (@niveau, @libelle, @bd_frere + 1, @bd_frere+2)

SELECT @id_ins = @@identity

IF @@ERROR <>0
BEGIN
ROLLBACK TRANSACTION INSERT_TREE_RIGHT
SELECT -1
RETURN
END

COMMIT TRANSACTION INSERT_TREE_RIGHT


SELECT @id_insMême insertion en frère à gauche :

CREATE PROCEDURE SP_DEV_INSERTION_ARBRE_FRERE_GAUCHE


@id_frere integer,
@libelle varchar(32),
@id_ins integer OUTPUT AS

-- Frédéric BROUARD 17/11/2001


-- insertion latérale à gauche du frère
-- paramètres : id_frere, libelle

DECLARE @bg_frere integer


DECLARE @niveau integer
DECLARE @OK integer

SET NOCOUNT ON

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE


BEGIN TRANSACTION INSERT_TREE_LEFT

-- vérification de l'existence du frère


SELECT @OK = count(*)
FROM T_DEVELOPPEMENT_DEV
WHERE DEV_ID = @id_frere

-- si élément supprimé, alors retour sans insertion avec valeur -1


IF @OK = 0
BEGIN
SELECT -1
ROLLBACK TRANSACTION INSERT_TREE_LEFT
RETURN
END

-- recherche du bg_frere
SELECT @bg_frere = DEV_BORNE_GAUCHE, @niveau = DEV_NIVEAU
FROM T_DEVELOPPEMENT_DEV
WHERE DEV_ID = @id_frere

-- décalage borne gauche pour insertion


UPDATE T_DEVELOPPEMENT_DEV
SET DEV_BORNE_GAUCHE = DEV_BORNE_GAUCHE + 2
WHERE DEV_BORNE_GAUCHE >= @bg_frere

IF @@ERROR <>0
BEGIN
ROLLBACK TRANSACTION INSERT_TREE_LEFT
SELECT -1
RETURN
END

-- décalage borne droite pour insertion


UPDATE T_DEVELOPPEMENT_DEV
SET DEV_BORNE_DROITE = DEV_BORNE_DROITE + 2
WHERE DEV_BORNE_DROITE > @bg_frere

IF @@ERROR <>0
BEGIN
ROLLBACK TRANSACTION INSERT_TREE_LEFT
SELECT -1
RETURN
END

-- insertion
INSERT INTO T_DEVELOPPEMENT_DEV (DEV_NIVEAU, DEV_LIBELLE,
DEV_BORNE_GAUCHE, DEV_BORNE_DROITE)
VALUES (@niveau, @libelle, @bg_frere, @bg_frere+1)

SELECT @id_ins = @@identity

IF @@ERROR <>0
BEGIN
ROLLBACK TRANSACTION
SELECT -1
RETURN
END

COMMIT TRANSACTION INSERT_TREE_LEFT

SELECT @id_insEnfin, comment supprimer. Notez l'argument "récursif"


passé en paramètre de procédure pour spécifier si l'on
tue toute la lignée ou si l'on laisse survivre les
descendants !

CREATE PROCEDURE SP_DEV_SUPPRESSION


@id_element integer,
@recursif bit
AS

-- Frédéric BROUARD 20/11/2001


-- suppression d'un élément (@recursif = 0) ou d'un sous arbre (@recursif = 1)
-- paramètres : id_element, @recursif

DECLARE @OK integer


DECLARE @bg_element integer
DECLARE @bd_element integer
DECLARE @intervalle integer

SET NOCOUNT ON

-- démarrage transaction
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRANSACTION DELETE_TREE

-- vérifie de l'existence de l'élément


SELECT @OK = count(*)
FROM T_DEVELOPPEMENT_DEV
WHERE DEV_ID = @id_element

-- si élément supprimé, alors retour sans insertion avec valeur -1


IF @OK = 0
BEGIN
SELECT -1
ROLLBACK TRANSACTION DELETE_TREE
RETURN
END

-- recherche des bd_element et bd_element


SELECT @bd_element = DEV_BORNE_DROITE, @bg_element =
DEV_BORNE_GAUCHE
FROM T_DEVELOPPEMENT_DEV
WHERE DEV_ID = @id_element

IF @recursif = 0
BEGIN
-- suppression de l'élément
DELETE FROM T_DEVELOPPEMENT_DEV
WHERE DEV_ID = @id_element

IF @@ERROR <>0
BEGIN
ROLLBACK TRANSACTION DELETE_TREE
SELECT -1
RETURN
END

-- décalage des bords droits et gauches des éléments du sous arbre


UPDATE T_DEVELOPPEMENT_DEV
SET DEV_BORNE_DROITE = DEV_BORNE_DROITE - 1,
DEV_BORNE_GAUCHE = DEV_BORNE_GAUCHE - 1
WHERE DEV_BORNE_GAUCHE > @bg_element AND DEV_BORNE_DROITE <
@bd_element

IF @@ERROR <>0
BEGIN
ROLLBACK TRANSACTION DELETE_TREE
SELECT -1
RETURN
END

-- décalage des bords gauches des éléments externes au sous arbre


UPDATE T_DEVELOPPEMENT_DEV
SET DEV_BORNE_GAUCHE = DEV_BORNE_GAUCHE - 2
WHERE DEV_BORNE_GAUCHE > @bg_element

IF @@ERROR <>0
BEGIN
ROLLBACK TRANSACTION DELETE_TREE
SELECT -1
RETURN
END

-- décalage des bords droits des éléments externes au sous arbre


UPDATE T_DEVELOPPEMENT_DEV
SET DEV_BORNE_DROITE = DEV_BORNE_DROITE - 2
WHERE DEV_BORNE_DROITE < @bg_element

IF @@ERROR <>0
BEGIN
ROLLBACK TRANSACTION DELETE_TREE
SELECT -1
RETURN
END
END

IF @recursif = 1
BEGIN
-- suppression des éléments du sous arbre
DELETE FROM T_DEVELOPPEMENT_DEV
WHERE DEV_BORNE_GAUCHE >= @bg_element AND DEV_BORNE_DROITE
<= @bd_element

IF @@ERROR <>0
BEGIN
ROLLBACK TRANSACTION DELETE_TREE
SELECT -1
RETURN
END

-- calcul de l'intervalle de décallage


SET @intervalle = @bd_element - @bg_element + 1

-- décalage des bords gauches des éléments externes au sous arbre


UPDATE T_DEVELOPPEMENT_DEV
SET DEV_BORNE_GAUCHE = DEV_BORNE_GAUCHE - @intervalle
WHERE DEV_BORNE_GAUCHE > @bg_element

IF @@ERROR <>0
BEGIN
ROLLBACK TRANSACTION DELETE_TREE
SELECT -1
RETURN
END

-- décalage des bords droits des éléments externes au sous arbre


UPDATE T_DEVELOPPEMENT_DEV
SET DEV_BORNE_DROITE = DEV_BORNE_DROITE - @intervalle
WHERE DEV_BORNE_DROITE < @bg_element

IF @@ERROR <>0
BEGIN
ROLLBACK TRANSACTION DELETE_TREE
SELECT -1
RETURN
END

END

COMMIT TRANSACTION DELETE_TREE

4.7. Les curseurs


Les curseurs sont des mécanismes de mémoire tampons
permettant d'accéder aux données renvoyées par une
requête et donc de parcourir les lignes du résultat.
Un curseur se définit dans une instruction DECLARE
possédant une requête de type SELECT. Il convient de
définir pour chaque colonne renvoyé une variable de type
approprié.
Pour lancer la requête associée (et donc placer les
données dans les buffers appropriés) il faut utiliser
l'instruction OPEN.
Un curseur doit être refermé avec l'instruction CLOSE.
Pour libérer la mémoire utilisée par un curseur, il faut
utiliser l'instruction DEALLOCATE.

Pour lire les données de la ligne courante et les


associées aux variables du curseur il faut utiliser
l'instruction FETCH.

Par défaut l'instruction FETCH navigue en avant d'une


ligne à chaque lecture dans l'ensemble des données du
résultat. Pour naviguer différemment, on peut qualifier
le FETCH avec les mots clef NEXT, PRIOR, FIRST, LAST,
ABSOLUTE n et RELATIVE n, mais il faut avoir déclaré le
curseur avec l'attribut SCROLL...

Enfin la variable @@fetch_Status permet de savoir si la


dernière instruction FETCH passée s'est correctement
déroulée (valeur 0), ce qui permet de tester si l'on est
arrivé en fin de parcours de l'ensemble de données.

Une boucle traditionnelle de manipulation d'un curseur


prend la forme suivante :

-- déclaration des variables de colonnes pour le curseur


DECLARE @Col1 Type1, @Col2 Type2, @Col3, Type3...

-- declaration du curseur
DECLARE MyCursor CURSOR
FOR
SELECT COL1, COL2, COL3 …
FROM MyTable

-- ouverture du curseur
OPEN MyCursor

-- lecture du premier enregistrement


FETCH MyCursor INTO @Col1, @Col2, @Col3...

-- boucle de traitement
WHILE @@fetch_Status = 0
BEGIN
traitement
-- lecture de l'enregistrement suivant
FETCH MyCursor INTO @Col1, @Col2, @Col3...
END

-- fermeture du curseur
CLOSE myCursor

-- libération de la mémoire
DEALLOCATE myCursorOn constate que l'instruction FETCH apparaît deux fois.
Une première fois avant la boucle WHILE une seconde fois
à l'intérieur et en dernière ligne de la boucle WHILE.
C'est la façon la plus classique et la plus portable
d'utiliser des curseurs.

NOTA : les performances sont en baisse lorsque l'on


utilise tout autre déplacement que le NEXT.

Remarque : il est possible d'effectuer des mises à jours


de données via des curseurs, mais cela n'est pas
conseillé. Dans ce cas il faut préciser en fin de
déclaration du curseur : FOR UPDATE OF liste_colonne

On peut aussi faire du "dirty read" avec les curseurs de


SQL Server en précisant l'attribut INSENSITIVE juste
après le nom du curseur.

La syntaxe complète SQL 2 de la déclaration d'un curseur


dans MS SQL Server est :

DECLARE nom_curseur [INSENSITIVE] [SCROLL] CURSOR


FOR requête_select
FOR {READ ONLY | UPDATE [OF colonne1 [, colonne2...]]}]La syntaxe admise par le
Transact SQL de MS SQL Server
est plus complète mais moins portable :

DECLARE nom_curseur CURSOR [LOCAL | GLOBAL] FORWARD_ONLY |


SCROLL] [STATIC | KEYSET | DYNAMIC | FAST_FORWARD] READ_ONLY |
SCROLL_LOCKS | OPTIMISTIC]
[TYPE_WARNING]
FOR requête_select
FOR UPDATE [OF colonne1 [, colonne2...]]]Exemple : voici un petit exercice consistant,
pour
chaque table de la base à en donner le nombre de ligne.
Pour cela on utilise une vue d'information de schema
pour récupérer le nom de toutes les tables et les
traiter.

/***************************************************/
-- Frédéric BROUARD - Communicatic SA - 2001-12-24 --
--=================================================--
-- Informe du nombre de ligne de chaque table de --
-- la base de données --
/***************************************************/

CREATE PROCEDURE SP_SYS_DB_DATA_ROWS


AS

-- variables locales de la procédure


DECLARE @NomTable VARCHAR(128), -- nom de la table
@SQL VARCHAR(1000) -- texte de la requête dynamique

-- pas de messages intempestifs


SET NOCOUNT ON

-- déclaration du curseur pour analyse des tables de la base


DECLARE CursBase CURSOR
FOR
SELECT TABLE_NAME
FROM INFORMATION_SCHEMA.tables
WHERE TABLE_TYPE = 'BASE TABLE'

-- gestion de la table des résultats : nom de la table temporaire


SET @NomTable = '#DATA_VOLUME'

-- vidage si existence de cette table, sinon création


IF EXISTS(SELECT * FROM INFORMATION_SCHEMA.tables WHERE
TABLE_NAME = @NomTable)
DELETE FROM #DATA_VOLUME
ELSE
CREATE TABLE #DATA_VOLUME
(TABLE_NAME VARCHAR(128),
DATA_ROWS INTEGER)

-- ouverture du curseur
OPEN CursBase

-- lecture de la première ligne


FETCH CursBase INTO @NomTable

-- boucle ligne à ligne


WHILE @@FETCH_STATUS = 0
BEGIN
-- requête d'insertion avec recherche du nombre de ligne de la table analysée
SET @SQL = 'INSERT INTO #DATA_VOLUME SELECT '''
+ @NomTable + ''', COUNT(*) FROM ' + @NomTable
EXEC(@SQL)
FETCH CursBase INTO @NomTable
END

-- fermeture et désallocation d'espace mémoire du curseur


CLOSE CursBase
DEALLOCATE CursBase

-- envoi des données


SELECT * FROM #DATA_VOLUME

-- pas de messages intempestifs


SET NOCOUNT OFFExemple : un deuxième exemple plus complexe nous montre
comment rechercher l'occurence d'un mot dans toutes les
colonnes de toutes les tables de la base. C'est une
extension de l'exemple vu au paragaphe 4.3 :

/*---------------------------------------------------\
| recherche d'une occurrence de mot dans n'importe |
| quelle colonne de type caractères de n'importe |
| quelle table de la base de données |
|----------------------------------------------------- |
| Frédéric BROUARD - COMMUNICATIC SA - 2001-12-18 |
\-------------------------------------------------- */
CREATE PROCEDURE SP_SEARCH_STRING_ANYFIELD_ANYTABLE
@SearchWord Varchar(32) -- mot recherché
AS

DECLARE @ErrMsg VARCHAR(128)

-- effet de bord 1 : pas de mot passé


IF @SearchWord IS NULL
BEGIN
SET @ErrMsg = 'Impossible de traiter cette recherche avec un argument NULL'
GOTO LBL_ERROR
END

-- effet de bord 2 : mot vide passé


IF @SearchWord = ''
BEGIN
SET @ErrMsg = 'Impossible de traiter cette recherche avec un argument vide'
GOTO LBL_ERROR
END

-- effet de bord 3 : mot contenant un caractère joker % du LIKE


IF CHARINDEX('%', @SearchWord) > 0
BEGIN
SET @ErrMsg = 'Impossible de traiter cette recherche avec un argument contenant un
caractère %'
GOTO LBL_ERROR
END

-- effet de bord 4 : mot contenant un caractère joker _ du LIKE


IF CHARINDEX('_', @SearchWord) > 0
BEGIN
SET @ErrMsg = 'Impossible de traiter cette recherche avec un argument contenant un
caractère _'
GOTO LBL_ERROR
END

-- variables de travail
DECLARE @TableName VARCHAR(128), -- nom de la table passé en argument
@ColumnList1 VARCHAR(2000),-- liste des colonnes pour clause SELECT
@ColumnList2 VARCHAR(2000),-- liste des colonnes pour clause WHERE
@SQL VARCHAR(5000) -- requête dynamique

-- curseur parcourant toutes les tables


DECLARE CurTables CURSOR
FOR
SELECT DISTINCT TABLE_NAME
FROM INFORMATION_SCHEMA.tables
WHERE TABLE_TYPE = 'BASE TABLE'
AND TABLE_NAME IS NOT NULL
-- en cas d'erreur
IF @@Error <> 0
BEGIN
SET @ErrMsg = 'Erreur dans la recherche de la lsite des tables concernées'
GOTO LBL_ERROR
END

-- ouverture du cuseur
OPEN CurTables

-- lecture de la première ligne de l'ensemble de résultat


FETCH CurTables INTO @TableName

-- la lecture est-elle correcte ? Oui, on boucle !


WHILE @@Fetch_Status = 0
BEGIN

-- les variables contenant les listes des colonnes sont initialisée à vide
SET @ColumnList1 = ''
SET @ColumnList2 = ''

-- construction des listes


SELECT @ColumnList1 = @ColumnList1 + COLUMN_NAME+', ',
@ColumnList2 = @ColumnList2 + 'COALESCE('+COLUMN_NAME+', '''') + '
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = @tableName
AND DATA_TYPE LIKE '%char%'

-- pas de colonnes cible pour la recherche, on lit l'enregistrement suivant et on boucle


IF @ColumnList1 = ''
BEGIN
FETCH CurTables INTO @TableName
CONTINUE
END

-- suppression du dernier caractère parasite des listes de colonne


SET @ColumnList1 = SUBSTRING(@ColumnList1, 1, LEN(@ColumnList1) - 1)
SET @ColumnList2 = SUBSTRING(@ColumnList2, 1, LEN(@ColumnList2) - 1)

-- création de la requête de recherche de l'ensemble des occurences


SET @SQL = 'SELECT ' +@ColumnList1
+' FROM ' +@TableName
+' WHERE ' +@ColumnList2
+' LIKE ''%'+@SearchWord+'%'''

-- exécution de la requête de recherche des occurences


EXEC(@SQL)

-- lecture de la ligne suivante


FETCH CurTables INTO @TableName
END

-- fermeture du curseur
CLOSE CurTables

-- libération de l'espace mémoire


DEALLOCATE CurTables

PRINT '*** RECHERCHE de l''occurence '+@SearchWord+ ' dans toute la base


terminée ***'

RETURN

-- gestion des erreurs


LBL_ERROR:
RAISERROR (@ErrMsg, 16, 1)Pour mettre à jour des données dans un curseur, il faut
le déclarer avec la clause FOR UPDATE et utiliser un
ordre UPDATE portant sur la table visé avec une clause
WHERE dans laquelle on référence la ligne courante du
curseur. Bien entendu la requête situé dans le curseur
ne peut porter que sur une seule table !

Exemple :

-- déclaration d'un curseur pour mise à jour


DECLARE NomCurseur CURSOR
FOR
SELECT ...
FROM LaTable
...
FOR UPDATE
...
-- exécution de la mise à jour sous curseur
UPDATE LaTable
SET Colonne1 = ..., Colonne2 = ...
WHERE CURRENT OF NomCurseur
5. Les triggers
Les triggers ou déclencheurs servent à étendre les
mécanismes de gestion de l'intégrité référentielle
(liens d'héritage par exemple) et permettre le contrôle
de saisie. Il s'agit de code déclenché lors de certains
événements de la base de données. Un trigger est
toujours rattaché à une table. Les événements qui
déclenche un trigger sont :

l'insertion de données (INSERT)


la suppression de données (DELETE)
la mise à jour (UPDATE)
Ils sont donc déclenchés systématiquement par une
requête SQL, après l'exécution de cette requête (AFTER),
ou à la place de l'exécution de cette requête (INSTEAD).
SQL Server n'implémente pas de trigger BEFORE (avant
exécution), mais nous verrons comment le simuler...

En fait le trigger correspond à la programmation des


événements des langages d'interfaces graphique, comme
Delphi ou Visual Basic.

5.1. Mise en place d'un trigger


On peut définir un trigger par l'interface de
l'Entreprise Manager comme par un batch créée, par
exemple dans l'analyseur de requête.

Et cliquer sur l'icône approprié "Trigger"

5.2. Syntaxe d'un trigger MS SQL Server 2000


CREATE TRIGGER <nom_trigger>
ON <table_ou_vue>
FOR | AFTER | INSTEAD
OF [ INSERT ] [ , ] [ UPDATE ] [ , ] [DELETE]
AS
<code>Cette syntaxe ne tient pas compte de toutes les
possibilités.

Emplois typiques :

gestion d'héritage avec lien d'exclusion


suppression, insertion et mise à jour en cascade
contrôle de validité
respect d'intégrité complexes
formatage de données
archivage automatique
...

5.3. Eléments du langage spécifique aux triggers

5.3.1. Pseudo tables INSERTED et DELETED


Les pseudo tables INSERTED et DELETED contiennent les
données respectives de l'insertion ou la mise à jour
(INSERTED) ou bien de la suppression (DELETED). On peut
les utiliser dans des requêtes comme des tables
ordinaires. La structure de ces tables est calquée sur
le structure de la table sur laquelle repose le trigger.

5.3.2. Fonctions UPDATE et COLUMNS_UPDATED


La fonction UPDATE permet de tester si une colonne est
visé par un changement de valeur. Elle s'emploie de la
manière suivante :

IF [NOT] UPDATE(<colonne>)
BEGIN
<traitement>
ENDElle ne peut être utilisée que dans les triggers de type
INSERT et UPDATE.

La fonction COLUMNS_UPDATED() permet d'interroger les


colonnes visées par un ordre INSERT ou UPDATE. Elle
utilise un masque binaire constitué par le rang ordinal
des colonnes de la table. Son emploi syntaxique est le
suivant :
IF [NOT] (COLUMNS_UPDATED() & <masque ordinal>) <comparateur> <valeur
masque attendue>
BEGIN
<traitement>
ENDUn exemple va nous permettre de préciser le
fonctionnement de ce mécanisme.
Soit une table de prospects comme suit :

CREATE TABLE T_PROSPECT


(PSP_ID INTEGER,
PSP_NOM CHAR(32),
PSP_PRENOM VARCHAR(25),
PSP_SAISIE DATETIME,
PSP_TEL VARCHAR(20))Les valeurs ordinales des colonnes de cette table (en
fait la position des colonnes lors de la construction de
la table) sont les suivantes :

PSP_ID PSP_NOM PSP_PRENOM PSP_SAISIE PSP_TEL


----------- --------------- ------------------ ---------------- --------------------
1 2 3 4 5Vous pouvez d'ailleurs retrouver la valeurs de
ces
positions ordinales par une requête dans les vue de
schéma normalisé, comme suit :

SELECT COLUMN_NAME, ORDINAL_POSITION


FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'T_PROSPECT'COLUMN_NAME
ORDINAL_POSITION
-----------------------------------
PSP_ID 1
PSP_NOM 2
PSP_PRENOM 3
PSP_SAISIE 4
PSP_TEL 5

Dès lors, si vous voulez savoir si l'ajout ou la mise à


jour concerne les colonnes PSP_NOM, PSP_PRENOM et
PSP_TEL, il faut écrire :

IF (COLUMNS_UPDATED() & 22) > 0


BEGIN
<traitement>
ENDpour savoir si au moins l'une des colonnes est
concernée
IF (COLUMNS_UPDATED() & 22) = 22
BEGIN
<traitement>
ENDpour savoir si toutes les colonnes sont
concernées

Le chiffre 22 s'obtenant par l'addition des puissances


de 2 de la position ordinale des colonnes visées, c'est
à dire :

colonne : PSP_ID PSP_NOM PSP_PRENOM PSP_SAISIE PSP_TEL


---------- ---------- ---------- ---------- ----------
ordinal : 1 2 3 4 5
puissance 2 : 2^0 = 1 2^1 = 2 2^2 = 4 2^3 = 8 2^4 = 16
retenu : non oui oui non oui
valeur :0 2 4 0 16 = 22 (SOMME de 16 + 4 + 2)

5.3.3. Annulation des effets d'un trigger


Pour empêcher un trigger de produire son effet on peut
utiliser le ROLLBACK qui dans ce cas peut porter sur la
transaction (ROLLBACK TRANSACTION celle qui a déclenchée
le trigger par exemple) ou uniquement le trigger
(ROLLBACK TRIGGER) c'est à dire sur les seuls effets de
ce dernier.

C'est par ce biais que l'on peut simuler un trigger


BEFORE : utiliser un trigger AFTER et le "rollbacker" ou
bien utiliser un trigger INSTEAD et insérer quand même
dans la table de destination.

Attention : un trigger n'est déclenché qu'une seule


fois, même si l'ordre SQL qui l'a déclenché concerne de
nombreuses lignes.

5.4. Exemples de triggers


Premier exemple - contrôle de validité de format de
données. On désire empêcher la saisie de tout numéro de
téléphone dans la table client qui possède d'autres
caractères que des chiffres (au maximum 20) et des
points de séparation :

CREATE TRIGGER E_CLI_INS


ON T_CLIENT
FOR INSERT, UPDATE
AS
-- requête de contrôle avec table d'insertion
SELECT CAST(REPLACE(CLI_TEL, '.', '') as DECIMAL(20))
FROM INSERTED
-- rollback en cas d'erreur
IF @@Error <> 0
ROLLBACK TRANSACTIONLa première tentative de modification :

UPDATE T_CLIENT
SET CLI_TEL = '01 02 03 04 05'
WHERE CLI_ID = 1
Serveur : Msg 8114, Niveau 16, État 5, Procédure E_CLI_INS, Ligne 6
Erreur de conversion du type de données varchar en numeric.provoque une erreur et
l'insertion n'a pas lieu.

Tandis que la seconde va bien produire ses effets :

UPDATE T_CLIENT
SET CLI_TEL = '91.92.93.94.95'
WHERE CLI_ID = 1Le seul inconvénient est que cette façon de procéder
rejette toutes les lignes insérées ou mise à jour sans
accepter celles qui peuvent être correctement formatées.
D'autre part on exécute cette procédure jusqu'au bout,
même si la colonne CLI_TEL ne subie aucune modification.
Néanmoins ce cas peut être résolu par un traitement
spécifique utilisant la fonction UPDATE :

CREATE TRIGGER E_CLI_INS


ON CLIENT
FOR INSERT, UPDATE
AS
-- inutile si pas d'update de la colonne visée
IF NOT UPDATE(CLI_TEL)
RETURN
-- requête de contrôle avec table d'insertion
SELECT CAST(REPLACE(CLI_TEL, '.', '') as DECIMAL(20))
FROM INSERTED
-- rollback en cas d'erreur
IF @@Error <> 0
ROLLBACK TRANSACTIONSecond exemple - L'exercice consiste maintenant à
corriger à la volée des saisie incorrectes. Tous les
caractères de séparation tel que le tiret ou l'espace
d'un numéro de téléphone devra être convertis en point.

CREATE TRIGGER E_CLI_INS


ON CLIENT
FOR UPDATE
AS
-- inutile si pas d'update de la colonne visée
IF NOT UPDATE(CLI_TEL)
RETURN
-- requête de correction avec table d'insertion
UPDATE client
SET cli_tel =
REPLACE(REPLACE(I.CLI_TEL, ' ', '.'), '-', '.')
FROM T_CLIENT C
INNER JOIN INSERTED I
ON C.CLI_ID = I.CLI_ID
-- rollback en cas d'erreur
IF @@Error <> 0
ROLLBACK TRANSACTIONAinsi l'ordre :

UPDATE T_CLIENT
SET CLI_TEL = '88 77-66 55.44'
WHERE CLI_ID = 1donne pour résultat :

cli_id cli_nom Cli_tel


----------- -------------------------------- --------------------
1 DUPONT 88.77.66.55.44et la saisie du numéro de téléphone a
été corrigé à la
volée et se trouve désormais au format voulu !

Attention : le danger réside dans l'exécution récursive


de tels triggers. Comme l'on remet à jour la table à
l'intérieur même du trigger, celui-ci est à nouveau
déclenché. Le phénomène, s'il n'était pas limité,
pourrait provoquer une famine du processus. Il faut donc
veiller à le limiter. Dans ce sens SQL Server propose
deux garde fous : le premier, intrinsèque au serveur est
de ne jamais dépasser 16 niveaux de récursion. Le second
est de proposer une limite plus restrictive à l'aide de
la procédure sp_configure, qui permet de modifier la
variable nested triggers afin d'étendre les limites
d'appel de triggers imbriqués.
De plus pour connaître le niveau d'imbrication du
trigger à l'intérieur de ce dernier il suffit de lancer
la fonction TRIGGER_NESTLEVEL() qui renvoie une variable
de niveau.
Conseil : il est préférable de ne pas utiliser de
triggers imbriqués et donc de laisser le paramètre
nested triggers de la configuration à 1.

Bien entendu ou pourrait être beaucoup plus fin dans ce


genre de contrôle et analyser puis remplacer, caractères
par caractères.
A titre de troisième exemple, nous allons réaliser un
tel trigger :

CREATE TRIGGER E_CLI_INS


ON CLIENT
FOR UPDATE
AS
-- inutile si pas d'update de la colonne visée
IF NOT UPDATE(CLI_TEL)
RETURN

-- ouverture d'un curseur sur la table INSERTED


-- pour les téléphones renseignés
DECLARE CurIns CURSOR
FOR
SELECT CLI_ID, CLI_TEL
FROM INSERTED
WHERE CLI_TEL IS NOT NULL
IF @@error <> 0 GOTO LBL_ERROR

-- variable de travail
DECLARE @IdCli int, @TelAvant VARCHAR(20), @TelApres VARCHAR(20),
@car CHAR(1), @i int, @j int

-- ouverture du curseur
OPEN CurIns
IF @@error <> 0 GOTO LBL_ERROR
-- lecture première ligne
FETCH CurIns INTO @IdCli, @TelAvant

-- boucle de lecture
WHILE @@Fetch_Status = 0
BEGIN
-- si vide reboucle immédiatement
IF @TelAvant = ''
BEGIN
FETCH CurIns INTO @IdCli, @TelAvant
CONTINUE
END
-- scrutation de la valeur du téléphone
SET @i = 1
SET @j = 0
SET @TelApres = ''
-- boucle de nettoyage sur tous les caractères
WHILE @i <= LEN(@TelAvant)
BEGIN
-- reprise du caractère d'ordre i
SET @car = SUBSTRING(@TelAvant,@i,1)
-- on ne traite que les caractères de 0 à 9
IF @car = '0' or @car = '1' or @Car = '2' or @Car = '3'
or @car = '4' or @car = '5' or @Car = '6' or @Car = '7'
or @car = '8' or @car = '9'
BEGIN
SET @TelApres = @TelApres + @Car
SET @j = @j + 1
END
SET @i =@i + 1
END
-- si vide reboucle immédiatement
IF @TelApres = ''
BEGIN
FETCH CurIns INTO @IdCli, @TelAvant
CONTINUE
END
-- découpage par tranche de 2 nombres
SET @TelAvant = @TelApres
SET @i = 1
SET @TelApres = ''
-- boucle de découpage
WHILE @i <= LEN(@TelAvant)
BEGIN
SET @car = SUBSTRING(@TelAvant,@i,1)
SET @TelApres = @TelApres + @Car
IF @i % 2 = 0
SET @TelApres = @TelApres + '-'
SET @i =@i + 1
END
-- petit effet de bord si @TelApres se termine par un nombre pair,
-- alors tiret en trop !
IF @j % 2 = 0 -- au pasage % est la fonction MODULO dans SQL Server
SET @TelApres = SUBSTRING(@TelApres, 1, LEN(@TelApres)-1)
-- mise à jour si différence
IF @TelAvant <> @TelApres
UPDATE CLIENT
SET CLI_TEL = @TelApres
WHERE CLI_ID = @IdCli
IF @@error <> 0 GOTO LBL_ERROR
FETCH CurIns INTO @IdCli, @TelAvant
END

-- fermeture du curseur et désallocation de l'espace mémoire


CLOSE CurIns
DEALLOCATE CurIns

RETURN

-- rollback en cas d'erreur


LBL_ERROR:
ROLLBACK TRANSACTIONQuatrième exemple - il s'agit maintenant de supprimer en
cascade dans différentes tables. Si un client (table
T_CLIENT) est supprimé on doit lui retirer les factures
(table T_FACTURE) qui le concerne :

CREATE TRIGGER E_DEL_CLI ON T_CLIENT


FOR DELETE
AS

DELETE FROM T_FACTURE


FROM T_FACTURE F
INNER JOIN DELETED D
ON F.CLI_ID = D.CLI_ID
IF @@ERROR <> 0
ROLLBACK TRANSACTIONBien entendu si vous avez placé de nouveau un trigger
permettant de faire de la suppression dans les lignes de
facture, alors il sera déclenché et supprimera les
occurrences désirées. C'est ce que l'on appelle un
déclenchement de triggers en cascade.
Cinquième exemple - la gestion d'un lien d'héritage
suppose souvent une exclusion mutuelle entre les fils
nous allons voir comment gérer ce cas de figure. Partons
d'une table T_VEHICULE dont la spécialisation provoque
deux tables : T_AVION et T_BATEAU. Un véhicule peut être
un avion ou bien un bateau mais pas les deux. Une valeur
de clef présente dans T_VEHICULE peut donc se retrouver
soit dans T_BATEAU soit dans T_AVION mais on doit éviter
qu'elle se retrouve dans les deux tables.

CREATE TRIGGER E_AVI_INS ON T_AVION


FOR INSERT
AS
DECLARE @rowInUse int, @rows int

-- on regarde si les clefs existent bien dans la table T_VEHICULE


SELECT @RowInUse = COUNT(*)
FROM INSERTED
SELECT @Rows = COUNT(*)
FROM T_VEHICULE V
JOIN INSERTED I
ON V.VHC_ID = I.VHC_ID
IF @RowInUse <> @Rows
BEGIN
ROLLBACK
RAISERROR ('Identifiant de l''héritant inexistant',16,1)
RETURN
END

-- on regarde si les clefs n'existent pas dans la table T_BATEAU


SELECT @Rows = COUNT(*)
FROM T_BATEAU B
JOIN INSERTED I
ON B.VHC_ID = I.VHC_ID
IF @Rows <> 0
BEGIN
ROLLBACK
RAISERROR ('Fils pré existant dans l''entité soeur BATEAU',16,1)
ENDJeu de test :

CREATE TABLE T_VEHICULE


(VHC_ID INT)
CREATE TABLE T_AVION
(VHC_ID INT,
AVI_MARQUE VARCHAR(16),
AVI_MODELE VARCHAR(16))

CREATE TABLE T_BATEAU


(VHC_ID INT,
BTO_NOM VARCHAR(16),
BTO_PORT VARCHAR(16))

INSERT INTO T_VEHICULE VALUES (1)


INSERT INTO T_VEHICULE VALUES (2)
INSERT INTO T_VEHICULE VALUES (3)

INSERT INTO T_BATEAU VALUES (2, 'Penduick', 'Lorient')


INSERT INTO T_BATEAU VALUES (3, 'Titanic', 'Liverpool')
INSERT INTO T_AVION VALUES (1, 'Boeing', '747')

INSERT INTO T_AVION VALUES (3, 'Tupolev', '144')


INSERT INTO T_AVION VALUES (5, 'Airbus', 'A320')Les deux dernières insertions
doivent être rejetées :
l'id 3 existant dans l'entité frère T_BATEAU et l'id 5
n'existant pas dans l'entité mère.

Mais cet exemple est incomplet car il faudrait créer ce


même type de trigger dans la table T_BATEAU pour
vérifier la présence de la clef dans la table père et
vérifier son absence dans la table sœur. De même qu'il
serait souhaitable de gérer une suppression en cascade
pour le père et éventuellement une modification de la
valeur de la clef en cascade ! Bref, à vous de jouer...

Sixième exemple - voici maintenant une association d'un


genre particulier. L'association 0:0 ! Comment gérer une
telle relation ? Comme à mon habitude un exemple concret
est plus compréhensible : nous voici avec un texte à
indexer mot pour mot, et pour cela nous devons classer
chaque mot rencontré dans le texte dans une table T_MOT
(MOT_MOT, MOT_REF, MOT_PAGE, MOT_LIGNE, MOT_OFFSET)
avec
la référence du texte, la page, la ligne et l'offset en
nombre de caractère. Mais il serait absurde d'indexer
tous les mots. C'est pourquoi une table
T_MOT_NOIR(MNR_MOT) de mot indésirables (les mots
"noirs") est créée, et l'on souhaite qu'aucun des mots
indexé pour le texte ne soit un mot noir, ni qu'aucun
mot noir ne se trouve dans les mots indexé. C'est donc
bien une relation d'exclusion totale, telle que
l'intersection des colonnes MOT_MOT de T_MOT et MNR_MOT
de T_MOT_NOIR produise un ensemble vide, ou plus
simplement que :

NOT EXISTS(SELECT *
FROM T_MOT MOT
JOIN T_MOT_NOIR MNR
ON MOT.MOT_MOT = MNR.MNR_MOT)Soit toujours évaluée à vrai !

Un tel trigger n'est pas difficile à écrire :

CREATE TRIGGER E_INS_MOT ON T_MOT


FOR INSERT
AS
IF EXISTS(SELECT *
FROM INSERTED I
JOIN T_MOT_NOIR M
ON I.MOT_MOT = M.MNR_MOT)
BEGIN
ROLLBACK
RAISERROR ('Insertion d''un mot noir impossible',16,1)
RETURN
ENDIl faudrait d'ailleurs penser à écrire son réciproque
dans la table T_MOT_NOIR empêchant ainsi l'insertion
d'un mot noir pré existant dans la table T_MOT.

On peut bien entendu tester un tel trigger avec le jeu


d'essai suivant :

CREATE TABLE T_MOT


(MOT_MOT CHAR(32),
MOT_REF CHAR(8),
MOT_PAGE INT,
MOT_LIGNE INT,
MOT_OFFSET INT)
CREATE TABLE T_MOT_NOIR
(MNR_MOT CHAR(32))

INSERT INTO T_MOT_NOIR VALUES ('LE')


INSERT INTO T_MOT_NOIR VALUES ('LA')
INSERT INTO T_MOT_NOIR VALUES ('LES')
INSERT INTO T_MOT_NOIR VALUES ('UN')
INSERT INTO T_MOT_NOIR VALUES ('UNE')
INSERT INTO T_MOT_NOIR VALUES ('DES')
INSERT INTO T_MOT_NOIR VALUES ('DE')

INSERT INTO T_MOT VALUES('LA', 'BIBLE', 147, 23, 14)


INSERT INTO T_MOT VALUES('VALLÉE', 'BIBLE', 147, 23, 14)
INSERT INTO T_MOT VALUES('DE', 'BIBLE', 147, 23, 14)
INSERT INTO T_MOT VALUES('LA', 'BIBLE', 147, 23, 14)
INSERT INTO T_MOT VALUES('MORT', 'BIBLE', 147, 23, 14)En conclusion nous
pouvons dire que les triggers de la
version 7 de SQL Server sont assez limités en ne
permettent pas de gérer très finement les données. Ils
ne fournissent pas un mécanisme pratique et simple
lorsque l'on veut par exemple manipuler ligne à ligne et
colonne par colonne la vailidité des données et les
rectifier à la volée avant l'insertion définitive. Il
semble que la version 2000 de SQL Server respecte plus
la norme SQL 2 sur ce point.

Septième exemple - dans une relation d'héritage, comment


insérer dans une table fille alors que l'insertion dans
la table mère est un pré requis ?
Par exemple, nous avons une table des personnes, une
table des clients et une table des employés. Ces tables
sont construites de la sorte :

CREATE TABLE T_PERSONNE_PRS


(PRS_ID INT IDENTITY NOT NULL PRIMARY KEY,
PRS_NOM CHAR(32) NOT NULL,
PRS_PRENOM VARCHAR(16))
CREATE TABLE T_EMPLOYE_EMP
(PRS_ID INT NOT NULL PRIMARY KEY REFERENCES T_PERSONNE_PRS
(PRS_ID),
EMP_MATRICULE VARCHAR(8))

On ne peut donc pas insérer directement dans


T_EMPLOYE_EMP, sauf à utiliser une vue et un trigger
INSTEAD OF...
Creation de la vue V_EMPLOYEE_EMP :

CREATE VIEW V_EMPLOYEE_EMP


AS
SELECT P.PRS_ID, P.PRS_NOM, P.PRS_PRENOM, E.EMP_MATRICULE
FROM T_PERSONNE_PRS P
INNER JOIN T_EMPLOYE_EMP E
ON P.PRS_ID = E.PRS_IDDès lors on peut créer un trigger d'insertion dans cette
vue qui va décomposer les éléments à insérer et injecter
les données dans les deux tables :

CREATE TRIGGER TRG_INS_EMPLOYE


ON V_EMPLOYEE_EMP
INSTEAD OF INSERT
AS
BEGIN
INSERT INTO T_PERSONNE_PRS (PRS_NOM, PRS_PRENOM)
SELECT PRS_NOM, PRS_PRENOM
FROM INSERTED

INSERT INTO T_EMPLOYE_EMP (PRS_ID, EMP_MATRICULE)


SELECT @@IDENTITY, EMP_MATRICULE
FROM INSERTED
ENDUtilisation :

INSERT INTO V_EMPLOYEE_EMP VALUES (1, 'DUPONT', 'Maurice', 'XF5090AZ')


SELECT * FROM T_PERSONNE_PRS
PRS_ID PRS_NOM PRS_PRENOM
----------- -------------------------------- ----------------
1 DUPONT Maurice
SELECT * FROM T_EMPLOYE_EMP
PRS_ID EMP_MATRICULE
----------- -------------
1 XF5090AZ

NOTA : voici le trigger de contrôle d'intégrité des


bornes des arborescence exprimées sous forme
intervallaire (voir l'article sur ce sujet) :

CREATE TRIGGER E_DEV_UNIQUE_BORNE ON T_DEVELOPPEMENT_DEV


FOR INSERT, UPDATE, DELETE
AS

-- vérification de l'unicité de l'ensemble des bornes (bornes gauches et bornes droite)


IF EXISTS (SELECT COUNT(*), BORNE
FROM (SELECT DEV_BORNE_DROITE AS BORNE
FROM T_DEVELOPPEMENT_DEV
UNION ALL
SELECT DEV_BORNE_GAUCHE AS BORNE
FROM T_DEVELOPPEMENT_DEV) T
GROUP BY BORNE
HAVING COUNT(*) <> 1)
ROLLBACK

-- vérification de la borne maximale comme étant deux fois le nombre de lignes de la


table
IF (SELECT MAX(BORNE)
FROM (SELECT DEV_BORNE_DROITE AS BORNE
FROM T_DEVELOPPEMENT_DEV
UNION ALL
SELECT DEV_BORNE_GAUCHE AS BORNE
FROM T_DEVELOPPEMENT_DEV) T) <> (SELECT COUNT(*) * 2
FROM T_DEVELOPPEMENT_DEV)
BEGIN
ROLLBACK
RAISERROR ('Une borne dépasse la valeur maximale attendue', 16, 1)
END

-- vérification de la borne minimale comme étant égale à un


IF (SELECT MIN(BORNE)
FROM (SELECT DEV_BORNE_DROITE AS BORNE
FROM T_DEVELOPPEMENT_DEV
UNION ALL
SELECT DEV_BORNE_GAUCHE AS BORNE
FROM T_DEVELOPPEMENT_DEV) T) <> 1
BEGIN
ROLLBACK
RAISERROR ('Une borne dépasse la valeur minimale attendue', 16, 1)
END

6. Cryptage du code, liaison au schema et recompilation


ENCRYPTION indique que SQL Server crypte l'entrée de la
table système contenant le texte de l'instruction
(procédure, fonction, trigger ou vue). L'utilisation de
cet argument permet le confidentialité du code et évite
la publication de la procédure dans le cadre de la
réplication.
C'est un moyen qui ne garantie pas qu'un utilisateur mal
intentionné n'insère des données impropres. Mais cette
technique permet de contraindre l'exécution de certains
éléments aux seuls possesseurs d'un algorithme de
vérification.
Pour comprendre comment mettre en oeuvre un tel
mécanisme, voici un exemple complet :

-- une table dont on ne désire pas que n'importe qui insére dedans
CREATE TABLE T_CRYPTEE_CRP
(CRP_ID INT NOT NULL PRIMARY KEY,
CRP_NOM CHAR(32) NOT NULL,
CRP_CONTROL VARBINARY(8))
-- un trigger qui vérifie la concordance entre le nom et le code de controle
CREATE TRIGGER TRG_CRP_INS_UPD
ON T_CRYPTEE_CRP
WITH ENCRYPTION
FOR INSERT, UPDATE
AS
IF NOT EXISTS(SELECT *
FROM INSERTED
WHERE CRP_CONTROL = CAST(SUBSTRING(CRP_NOM, 1, 8) AS
VARBINARY(8)))
ROLLBACK
-- tentative d'insertion sans connaissance de l'algoritme de controle
INSERT INTO T_CRYPTEE_CRP VALUES (1, 'Dupont', CAST(' ' AS
VARBINARY(8)))
SELECT * FROM T_CRYPTEE_CRP
CRP_ID CRP_NOM CRP_CONTROL
----------- -------------------------------- ------------------

-- aucun résultat l'insertion a été roolbacké par le trigger !


-- tentative d'insertion avec le code de controle
INSERT INTO T_CRYPTEE_CRP VALUES (1, 'Dupont', CAST('Dupont ' AS
VARBINARY(8)))
SELECT * FROM T_CRYPTEE_CRP
CRP_ID CRP_NOM CRP_CONTROL
----------- -------------------------------- ------------------
1 Dupont 0x4475706F6E742020
-- l'insertion a bien eût lieu !

Voila comment certains éditeurs de progiciel se


protègent des petits malins qui tente de "pourrir" leur
base en y insérant des données sans passer par
l'application !

RECOMPILE indique que SQL Server n'utilise pas le cache


pour le plan de la procédure (ou de la vue). Elle sera
donc recompilée à chaque l'exécution. Cela est conseillé
lorsque vous utilisez des valeurs temporaires ou
atypiques (aléatoires par exemple).

SCHEMABINDING Indique qu'une fonction (vue ou index) est


liée aux objets base de données auxquels elle fait
référence. La fonction ainsi créée ne pourra être
supprimée que si tous les objets base de données
auxquels la fonction fait référence sont supprimés
préalablement.

LES TRANSACTIONS IMBRIQUEES

I. Ce qu'est..., ce que n'est pas... une transaction


II. Modèle de transaction imbriquées
III. Le modèle asymétrique de transaction imbriqué
IV. Piloter génériquement des transactions imbriquées
V. De plus amples informations vous sont nécessaires ?

I. Ce qu'est..., ce que n'est pas... une transaction


Une transaction est un ensemble de traitements devant être
effectué en tout ou rien, en vertue du principe d'atomicité
des transaction. Par exemple un virement bancaire d'un compte
courant à un compte éparge nécessite une première requête
UPDATE pour soutirer l'argent du compte courant et une seconde
pour créditer le compte épargne. Si l'une des deux requêtes ne
s'effectue pas, alors la base devient incohérente. On dit
ainsi que la transaction assure que la base de données part
d'un état de cohérence pour arriver dans un autre état de
cohérence, les états transitoires, c'est à dire les
différentes étapes de la transaction, ne devant jamais être
présentés de quelques manière que ce soit, même en cas de
panne du système.
Mais que se passe t-il si une transaction démarre à
l'intérieur d'une autre transaction ? C'est ce que l'on apelle
"tansaction imbriquée".
Les transactions imbriquées sont le plus souvent le fait de
procédures stockées qui s'appellent les unes des autres afin
de fournir un ensemble cohérent de traitement donc chaque
partie peut en outre être individuellement appelée.

Le cas est assez classique. On le trouve par exemple lorsque


le modèle de données cartographie un objet et que différentes
procédures concourent à l'insertion de ses données comme à sa
mise à jour. Par exemple une première procédure gère la mise à
jour (INSERT / UPDATE / DELETE) d'une personne et appelle une
seconde procédure qui gère la mise à jour des adresses
relatives à cette personne. D'où deux transactions (une dans
chaque procédure) qui fatalement vont s'imbriquées.

Par exemple une procédure stockée 1 démarre une transaction et


au milieu de code, alors que la transaction 1 n'est pas
finalisée, fait appel à une autre procédure stockée qui elle
même encapsule une procédure stockée... Qui valide finalement
la transaction ? La procédure appelante ou celle qui est
appelée ? Qui annule finalement la transaction ?

Or le principe même d'une transaction imbriquée n'a pas de


sens. En effet une transaction est un ensemble cohérent.
Imaginons le scénario suivant :

BEGIN TRANSACTION A

... code a1 ...

BEGIN TRANSACTION B

... code b ...

ROLLBACK TRANSACTION B

... code a2 ...

COMMIT TRANSACTION A
La transaction B valide les parties de code a1 et a2, mais le
code b étant annulé, la transaction A est clairement
incohérente. C'est pourquoi dans le principe les transactions
imbriquées ne sont pas possible !
En fait il n'y a donc jamais qu'une seule transaction. Et
c'est toujours la première...

II. Modèle de transaction imbriquées


Dès lors deux modèles de "pseudo" transactions imbriquées sont
possibles : le modèle symétrique et le modèle asymétrique.

Dans le modèle symétrique, le premier BEGIN TRANSACTION


commence la vraie seule transaction. Chaque fois qu'un nouveau
BEGIN TRANSACTION est rencontré dans le code, la commande est
ignorée, mais un compteur de transaction est incrémenté de 1.
Chaque fois qu'un COMMIT ou ROLLBACK est rencontré, ce même
compteur est décrémenté de 1 et la commande n'a pas d'effet.
Si le compteur est à zéro, alors le COMMIT ou ROLLBACK
rencontré est réellement exécuté. Cela peut se résumé par le
script suivant :

BEGIN TRANSACTION A

... code a1 ...

BEGIN TRANSACTION B -- code ignoré, compteur "tran" à 1

... code b ...

ROLLBACK TRANSACTION B -- code ignoré, compteur "tran" à 0

... code a2 ...

COMMIT TRANSACTION A

Ainsi comme on le voit, tout le code de cette procédure est


exécuté.

Mais ce n'est pas le comportement adopté par MS SQL Server...


En effet, ce SGBDR se base sur le modèle asymétrique,
finalement bien plus fin !

III. Le modèle asymétrique de transaction imbriqué


Le principe du modèle asymétrique de transactions imbriquées
est simple, mais sa mise en œuvre réserve quelques surprises !

Voici les règles de base :


il n'y a jamais qu'une seule transaction
Le premier BEGIN TRANSACTION rencontré démarre la
transaction
tout autre BEGIN TRANSACTION que le premier ne fait
qu'incrémenter le compteur de session @@TRANCOUNT
le premier ROLLBACK TRANSACTION rencontré annule la
transaction
chaque COMMIT TRANSACTION décrémente le compteur de session
@@TRANCOUNT de 1 et si ce compteur vaut 0 alors la
transaction est finalement validée.
Voici ce qui se passe lorsque des transactions imbriquées
réussissent :

Dans cet exemple, la procédure 1 démarre une transaction avec


un BEGIN TRANSACTION et met le compteur @@TRANCOUNT à 1, puis
appelle la procédure 2 qui, voyant qu'une transaction est déjà
démarrée, ne fait que mettre le compteur @@TRANCOUT à 2, puis
appelle la procédure 3 qui, voyant qu'une transaction est déjà
démarrée, ne fait que mettre le compteur @@TRANCOUT à 3. Cette
dernière transaction réussie et ne fait que décrémenter le
compteur @@RANCOUNT qui passe de 3 à 2 puis revient en
procédure 2, qui elle même réussit aussi et ne fait que passer
le compteur @@TRANCOUNT de 2 à 1. Enfin la procédure 1 fait le
commit final qui fait passer @@TRANCOUNT de 1 à 0 et génère
réellement le COMMIT !
En tout et pour tout il n'y a eût qu'un seul BEGIN TRANSACTION
et un seul COMMIT TRANSACTION.
La notion même de transaction imbriquée n'existe donc pas...

Que se passe t-il si une transaction interne génère un


rollback ?
En fait dans ce cas le ROLLBACK est immédiatement exécuté et
le compteur @@TRANCOUNT passe à zéro.
Si jamais le code dans procédure 1 passe par le COMMIT, alors
le système ne s'y retrouve plus et génère un message d'erreur
du genre : Le compte des transactions après EXECUTE indique
qu'il manque une instruction COMMIT ou ROLLBACK TRANSACTION

Démonstration :

-- création d'une table test pour notre transaction


IF EXISTS (SELECT *
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = 'dbo'
AND TABLE_NAME = 'T_TRN')
DROP TABLE T_TRN
GO

-- table avec une contrainte de validité


CREATE TABLE T_TRN
(N INT CHECK (N >= 0))
GO

-- création d'une procédure stockée de test de transaction imbriquée


IF EXISTS (SELECT *
FROM INFORMATION_SCHEMA.ROUTINES
WHERE ROUTINE_SCHEMA = 'dbo'
AND ROUTINE_NAME = 'P_TRN_INTERNE')
DROP PROCEDURE P_TRN_INTERNE
GO

CREATE PROCEDURE P_TRN_INTERNE


AS

DECLARE @ERROR INT, @ROWCOUNT INT

BEGIN TRANSACTION

-- insertion invalide : elle doit déclencher le ROLLBACK


INSERT INTO T_TRN VALUES (-4)
SELECT @ERROR = @@ERROR, @ROWCOUNT = @@ROWCOUNT
IF @ERROR <> 0 OR @ROWCOUNT = 0
BEGIN
RAISERROR('Procédure P_TRN_INTERNE : Erreur à l''insertion', 16, 1)
GOTO LBL_ERROR
END

COMMIT TRANSACTION

RETURN (0)

LBL_ERROR:

IF @@TRANCOUNT > 1
COMMIT TRANSACTION
IF @@ROWCOUNT = 1
ROLLBACK TRANSACTION
RETURN (-1)

GO

-- création d'une procédure stockée de test de transaction imbriquée


IF EXISTS (SELECT *
FROM INFORMATION_SCHEMA.ROUTINES
WHERE ROUTINE_SCHEMA = 'dbo'
AND ROUTINE_NAME = 'P_TRN_EXTERNE')
DROP PROCEDURE P_TRN_EXTERNE
GO

CREATE PROCEDURE P_TRN_EXTERNE


AS

DECLARE @ERROR INT, @ROWCOUNT INT, @RETVAL INT

BEGIN TRANSACTION

-- insertion valide
INSERT INTO T_TRN VALUES (33)
SELECT @ERROR = @@ERROR, @ROWCOUNT = @@ROWCOUNT
IF @ERROR <> 0 OR @ROWCOUNT = 0
BEGIN
RAISERROR('Procédure P_TRN_EXTERNE : Erreur à l''insertion', 16, 1)
GOTO LBL_ERROR
END
EXEC @RETVAL = P_TRN_INTERNE
SELECT @ERROR = @@ERROR, @ROWCOUNT = @@ROWCOUNT

IF @RETVAL = -1 -- la transaction a été pseudo validée mais elle doit être annulée
BEGIN
RAISERROR('Procédure P_TRN_EXTERNE : Erreur à l''appel de la procédure
P_TRN_INTERNE', 16, 1)
GOTO LBL_ERROR
END

IF @ERROR <> 0 OR @@ROWCOUNT = 0


GOTO LBL_ERROR

COMMIT TRANSACTION

RETURN (0)

LBL_ERROR:

IF @@TRANCOUNT > 1
COMMIT TRANSACTION
IF @@ROWCOUNT = 1
ROLLBACK TRANSACTION
RETURN (-1)

GO
-- exécution teste
EXEC P_TRN_EXTERNE
GO

-- a l'issu de cet exécution aucune ligne ne doit avoir été inséré :


SELECT * FROM T_TRAN
GO

IV. Piloter génériquement des transactions imbriquées


Si vous voulez piloter proprement des transactions qui
s'emboitent dans d'autres transactions notamment lorsque vous
faîtes appel à des procédures stockées qui s'imbriques les
unes dans les autres il faut gérer le COMMIT ou le ROLLBACK en
tenant compte de la valeur du compteur @@TRANCOUNT.

Voici comment finaliser proprement une transaction quelque


soit le contexte transactionnel :

-- partie à rajouter à TOUTES les procédures (finalisation) :

-- succès
COMMIT TRANSACTION
RETURN (0)

-- échec
LBL_ERROR:
IF @@TRANCOUNT > 1
COMMIT TRANSACTION
IF @@ROWCOUNT = 1
ROLLBACK TRANSACTION
RETURN (-1)

Si vous avez besoin de rétablir le niveau d'isolation par


défaut :

...
DECLARE @RETVAL INT

...

-- succès
COMMIT TRANSACTION
SET @RETVAL = 0
GOTO RESUME

-- échec
LBL_ERROR:
IF @@TRANCOUNT > 1
COMMIT TRANSACTION
IF @@ROWCOUNT = 1
ROLLBACK TRANSACTION
SET @RETVAL = -1

LBL_RESUME:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED

LES TRIGGERS
Comment désactiver et réactiver un déclencheur ?
Comment désactiver et réactiver une contrainte ?
Comment tester qu'une colonne a été modifiée dans un
trigger Insert ou Update ?
Comment récuperer la date système dans une fonction
utilisateur ?
Comment débugger une procédure stockée ?
Comment savoir si un ordre SQL s'est bien déroulé ?
Comment retrouver le libellé d'un message d'erreur par
rapport à son n° ?
Comment requêter sur 2 tables de deux bases
différentes ?

Comment désactiver et réactiver un déclencheur


?[haut]

auteur : Wolo Laurent


MS SQL Serveur, à partir de la version 2000, prévoit une
option dans l'instruction
ALTER TABLE qui permet de désactiver et réactiver un
déclancheur.
Exemple :
L'exemple suivant utilise l'option DISABLE TRIGGER de
l'instruction ALTER TABLE pour
désactiver le déclencheur et permettre une insertion qui
devrait normalement entraîner une violation du
déclencheur.
Le déclencheur est ensuite réactivé à l'aide de l'option
ENABLE TRIGGER.

CREATE TABLE trig_example


(id INT,
name VARCHAR(10),
salary MONEY)
go
-- Creation du déclancheur
CREATE TRIGGER trig1 ON trig_example FOR INSERT
as
IF (SELECT COUNT(*) FROM INSERTED
WHERE salary > 100000) > 0
BEGIN
print "TRIG1 Error: Vous tentez d'inserer un salaire > $100,000"
ROLLBACK TRANSACTION
END
GO
-- Tentative d'insertion d'une valeur qui viole la contrainte.
INSERT INTO trig_example VALUES (1,'Pat Smith',100001)
GO
-- Nous allons maintenant désactiver le déclencheur.
ALTER TABLE trig_example DISABLE TRIGGER trig1
GO
-- Nous allons inserer une valeur qui normalement viole le déclencheur
INSERT INTO trig_example VALUES (2,'Chuck Jones',100001)
GO
-- Réactivation du déclencheur
ALTER TABLE trig_example ENABLE TRIGGER trig1
GO
-- Et l'on teste si notre déclencheur est réactivé.
INSERT INTO trig_example VALUES (3,'Mary Booth',100001)
GO

Comment désactiver et réactiver une contrainte


?[haut]

auteur : Wolo Laurent


MS SQL Serveur, à partir de la version 2000, prévoit une
option dans l'instruction
ALTER TABLE qui permet de désactiver et réactiver un
déclancheur.
Exemple :
L'exemple suivant désactive la contrainte définissant
les salaires pouvant être inclus
dans les données. L'option WITH NOCHECK CONSTRAINT est
utilisée avec ALTER TABLE pour
désactiver la contrainte et permettre une insertion qui
devrait normalement entraîner
une violation de la contrainte. WITH CHECK CONSTRAINT
réactive la contrainte.
CREATE TABLE cnst_example
(id INT NOT NULL,
name VARCHAR(10) NOT NULL,
salary MONEY NOT NULL
CONSTRAINT salary_cap CHECK (salary < 100000)
)
-- Nous commençons par inserer deux lignes
INSERT INTO cnst_example VALUES (1,'Joe Brown',65000)
INSERT INTO cnst_example VALUES (2,'Mary Smith',75000)

-- Ensuite, nous tentons de violer notre contrainte check


INSERT INTO cnst_example VALUES (3,'Pat Jones',105000)
-- Nous désactivons la contrainte puis nous relançons
ALTER TABLE cnst_example NOCHECK CONSTRAINT salary_cap
INSERT INTO cnst_example VALUES (3,'Pat Jones',105000)
-- Nous réactivons notre contrainte puis violant la requête.
ALTER TABLE cnst_example CHECK CONSTRAINT salary_cap
INSERT INTO cnst_example VALUES (4,'Eric James',110000)

Comment tester qu'une colonne a été modifiée dans


un trigger Insert ou Update ?[haut]

auteur : Wolo Laurent


Dans un trigger Insert ou Update uniquement, vous pouvez
faire usage,
de la clause IF UPDATE(Colonne1) pour tester si une
colone a été modifié par un insert ou un update.
Noter que vous pouvez étendre la construction sur
plusieurs colonnes en utilisant des opérateurs AND,OR
Exemple :

--Soit une table T_Table dont la définition est la suivante:


CREATE TABLE T_TABLE1
(
TAB_ID INT NOT NULL PRIMARY KEY,
TAB_VAL NUMERIC(32) NOT NULL
)
GO
--Nous initialisons la table
INSERT INTO T_TABLE1 VALUES(1,500)
INSERT INTO T_TABLE1 VALUES(2,1500)
INSERT INTO T_TABLE1 VALUES(3,2000)
INSERT INTO T_TABLE1 VALUES(4,1800)
GO
--Supposant que nous vonlons interdire l'ajout et la modification des valeurs dans cette
table
--Nous créons un déclencheur a cet effet.
CREATE TRIGGER TG_NO_ADD_UPDATE ON T_TABLE1
FOR INSERT,UPDATE
AS
IF UPDATE(TAB_ID)
RAISERROR ('Interdiction formelle d''ajouter ou modifier les données de cette tables',
16, 10)
go
--Tentative d'ajout d'une ligne
INSERT INTO T_TABLE1 VALUES(5,600)
GO
--Cette instruction est refoulée !!!

Comment récuperer la date système dans une


fonction utilisateur ?[haut]

auteur : Fabien Celaia


SQL server interdit l'utilisation de la fonction
getdate() dans une fonction utilisateur.
Pour contourner cette dificulté,nous créons une vue qui
renvoie la date courante

CREATE VIEW V_DateHeure_Courante


AS
SELECT CURRENT_TIMESTAMP AS DateHeure_Courante

Sélectionner cette date dans votre fonction.

SELECT DateHeure_Courante
FROM V_DateHeure_Courante

Comment débugger une procédure stockée ?[haut]

auteur : Wolo Laurent


Dans l'analyseur de requête, ouvrir une connexion sur le
serveur où se trouve la procédure et executer :
exec sp_sdidebug 'legacy_on'

Fermer la fenêtre de droite pour conserver la même


connexion, puis dans l'explorateur d'objet, faire clic
droit sur la procédure stockée puis Débogage

Comment savoir si un ordre SQL s'est bien déroulé


?[haut]

auteur : Fabien Celaia


En interrogeant la variable globale @@error, directement
après l'appel de la requête : 0=succès, sinon no
d'erreur

Comment retrouver le libellé d'un message d'erreur


par rapport à son n° ?[haut]

auteur : Fabien Celaia

SELECT description
FROM master..sysmessages
WHERE langid=@@langid
AND error= VotreNoDErreur

Comment requêter sur 2 tables de deux bases


différentes ?[haut]

auteur : Fabien Celaia


Il y a de nombreuses manières plus ou moins implicites
de déterminer une table

NomDeTable
NomDuSchema.NomDeTable
NomDeLaBase.NomDuSchema.NomDeTable
NomDeLaBase..NomDeLaTable
NomDuServeurNomDeLaBase.NomDuSchema.NomDeTable
Si l'on souhaite, dans le cas extrême, lier 2 tables de
2 bases distinctes, situées chacune sur un serveur
distinct, il faudra donc utiliser la nomenclature
complète, en prenant soin au préalable de déterminer le
serveur distant/lié
Exemple
SELECT P.Nom, P.Prenom, L.NomLocalite
FROM Personnes P INNER JOIN ServeurDistant.BaseDistante..Localites L
ON P.CodeLocalite=L.IDLocalite

LES TRIGGERS

Comment désactiver et réactiver un déclencheur ?


Comment désactiver et réactiver une contrainte ?
Comment tester qu'une colonne a été modifiée dans un
trigger Insert ou Update ?
Comment récuperer la date système dans une fonction
utilisateur ?
Comment débugger une procédure stockée ?
Comment savoir si un ordre SQL s'est bien déroulé ?
Comment retrouver le libellé d'un message d'erreur par
rapport à son n° ?
Comment requêter sur 2 tables de deux bases
différentes ?

Comment désactiver et réactiver un déclencheur


?[haut]

auteur : Wolo Laurent


MS SQL Serveur, à partir de la version 2000, prévoit une
option dans l'instruction
ALTER TABLE qui permet de désactiver et réactiver un
déclancheur.
Exemple :
L'exemple suivant utilise l'option DISABLE TRIGGER de
l'instruction ALTER TABLE pour
désactiver le déclencheur et permettre une insertion qui
devrait normalement entraîner une violation du
déclencheur.
Le déclencheur est ensuite réactivé à l'aide de l'option
ENABLE TRIGGER.

CREATE TABLE trig_example


(id INT,
name VARCHAR(10),
salary MONEY)
go
-- Creation du déclancheur
CREATE TRIGGER trig1 ON trig_example FOR INSERT
as
IF (SELECT COUNT(*) FROM INSERTED
WHERE salary > 100000) > 0
BEGIN
print "TRIG1 Error: Vous tentez d'inserer un salaire > $100,000"
ROLLBACK TRANSACTION
END
GO
-- Tentative d'insertion d'une valeur qui viole la contrainte.
INSERT INTO trig_example VALUES (1,'Pat Smith',100001)
GO
-- Nous allons maintenant désactiver le déclencheur.
ALTER TABLE trig_example DISABLE TRIGGER trig1
GO
-- Nous allons inserer une valeur qui normalement viole le déclencheur
INSERT INTO trig_example VALUES (2,'Chuck Jones',100001)
GO
-- Réactivation du déclencheur
ALTER TABLE trig_example ENABLE TRIGGER trig1
GO
-- Et l'on teste si notre déclencheur est réactivé.
INSERT INTO trig_example VALUES (3,'Mary Booth',100001)
GO

Comment désactiver et réactiver une contrainte


?[haut]

auteur : Wolo Laurent


MS SQL Serveur, à partir de la version 2000, prévoit une
option dans l'instruction
ALTER TABLE qui permet de désactiver et réactiver un
déclancheur.
Exemple :
L'exemple suivant désactive la contrainte définissant
les salaires pouvant être inclus
dans les données. L'option WITH NOCHECK CONSTRAINT est
utilisée avec ALTER TABLE pour
désactiver la contrainte et permettre une insertion qui
devrait normalement entraîner
une violation de la contrainte. WITH CHECK CONSTRAINT
réactive la contrainte.

CREATE TABLE cnst_example


(id INT NOT NULL,
name VARCHAR(10) NOT NULL,
salary MONEY NOT NULL
CONSTRAINT salary_cap CHECK (salary < 100000)
)
-- Nous commençons par inserer deux lignes
INSERT INTO cnst_example VALUES (1,'Joe Brown',65000)
INSERT INTO cnst_example VALUES (2,'Mary Smith',75000)

-- Ensuite, nous tentons de violer notre contrainte check


INSERT INTO cnst_example VALUES (3,'Pat Jones',105000)
-- Nous désactivons la contrainte puis nous relançons
ALTER TABLE cnst_example NOCHECK CONSTRAINT salary_cap
INSERT INTO cnst_example VALUES (3,'Pat Jones',105000)
-- Nous réactivons notre contrainte puis violant la requête.
ALTER TABLE cnst_example CHECK CONSTRAINT salary_cap
INSERT INTO cnst_example VALUES (4,'Eric James',110000)

Comment tester qu'une colonne a été modifiée dans


un trigger Insert ou Update ?[haut]

auteur : Wolo Laurent


Dans un trigger Insert ou Update uniquement, vous pouvez
faire usage,
de la clause IF UPDATE(Colonne1) pour tester si une
colone a été modifié par un insert ou un update.
Noter que vous pouvez étendre la construction sur
plusieurs colonnes en utilisant des opérateurs AND,OR
Exemple :

--Soit une table T_Table dont la définition est la suivante:


CREATE TABLE T_TABLE1
(
TAB_ID INT NOT NULL PRIMARY KEY,
TAB_VAL NUMERIC(32) NOT NULL
)
GO
--Nous initialisons la table
INSERT INTO T_TABLE1 VALUES(1,500)
INSERT INTO T_TABLE1 VALUES(2,1500)
INSERT INTO T_TABLE1 VALUES(3,2000)
INSERT INTO T_TABLE1 VALUES(4,1800)
GO
--Supposant que nous vonlons interdire l'ajout et la modification des valeurs dans cette
table
--Nous créons un déclencheur a cet effet.
CREATE TRIGGER TG_NO_ADD_UPDATE ON T_TABLE1
FOR INSERT,UPDATE
AS
IF UPDATE(TAB_ID)
RAISERROR ('Interdiction formelle d''ajouter ou modifier les données de cette tables',
16, 10)
go
--Tentative d'ajout d'une ligne
INSERT INTO T_TABLE1 VALUES(5,600)
GO
--Cette instruction est refoulée !!!

Comment récuperer la date système dans une


fonction utilisateur ?[haut]

auteur : Fabien Celaia


SQL server interdit l'utilisation de la fonction
getdate() dans une fonction utilisateur.
Pour contourner cette dificulté,nous créons une vue qui
renvoie la date courante

CREATE VIEW V_DateHeure_Courante


AS
SELECT CURRENT_TIMESTAMP AS DateHeure_Courante

Sélectionner cette date dans votre fonction.


SELECT DateHeure_Courante
FROM V_DateHeure_Courante

Comment débugger une procédure stockée ?[haut]

auteur : Wolo Laurent


Dans l'analyseur de requête, ouvrir une connexion sur le
serveur où se trouve la procédure et executer :

exec sp_sdidebug 'legacy_on'

Fermer la fenêtre de droite pour conserver la même


connexion, puis dans l'explorateur d'objet, faire clic
droit sur la procédure stockée puis Débogage

Comment savoir si un ordre SQL s'est bien déroulé


?[haut]

auteur : Fabien Celaia


En interrogeant la variable globale @@error, directement
après l'appel de la requête : 0=succès, sinon no
d'erreur

Comment retrouver le libellé d'un message d'erreur


par rapport à son n° ?[haut]

auteur : Fabien Celaia

SELECT description
FROM master..sysmessages
WHERE langid=@@langid
AND error= VotreNoDErreur

NomDeTable
NomDuSchema.NomDeTable
NomDeLaBase.NomDuSchema.NomDeTable
NomDeLaBase..NomDeLaTable
NomDuServeurNomDeLaBase.NomDuSchema.NomDeTable
Si l'on souhaite, dans le cas extrême, lier 2 tables de
2 bases distinctes, situées chacune sur un serveur
distinct, il faudra donc utiliser la nomenclature
complète, en prenant soin au préalable de déterminer le
serveur distant/lié
Exemple
SELECT P.Nom, P.Prenom, L.NomLocalite
FROM Personnes P INNER JOIN ServeurDistant.BaseDistante..Localites L
ON P.CodeLocalite=L.IDLocalite

LES VUES

Comment obtenir la liste des tables d'une base de


donnée ?
Comment connaître la liste des colonnes d'une table ?
Comment lister l'ensemble des vues d'une base de
données SQL Server ?
Comment lister l'ensemble des UDF d'une base de
données SQL Server ?
Comment lister l'ensemble des procédures stockées
d'une base de données SQL Server ?
Comment lister l'ensemble des déclencheurs d'une base
de données SQL Server ?
Quelle est la requête qui permet de savoir quelles
colonnes d'une table servent de clé primaire ?
Quelle commande permet d'afficher la description d'une
table sous SQLServer ?
Comment récupérer la valeur par défaut d'un champs
d'une table ?
Quelle est le nombre de ligne de chacune des tables
d'une base de données ?
Comment connaître le nom de la base de données en
cours ?
Comment afficher la liste des bases de données d'un
serveur ?
Comment changer le type de données d'une colonne ?
Comment renommer une base de données ?
Comment renommer une table ou un autre object de base
de données ?
Comment visualiser le code T-SQL d'une procédure
stocké ?
Comment récuperer un schéma de base de données sur un
serveur SQL2005 depuis une restauration d'une base de données
en version 2000 ?
Comment lister les contraintes de clés primaires et
étrangères des tables d'une base de données ?
Comment trouver la liste des tables dont dépend la vue
?
Comment comparer 2 tables ?
Comment comparer 2 bases de données ?

Comment obtenir la liste des tables d'une base de


donnée ?[haut]

auteurs : Wolo Laurent, Fabien Celaia


Vous avez beaucoups de possibilités pour connaître la
liste des tables d'une base de données. Nous vous
recommandons d'utiliser les vues d'informations de
schéma.

SELECT table_name
FROM information_schema.tables
WHERE table_type='BASE TABLE'

Vous pouvez aussi passer par la procedure stockée


sp_tables ou encore passez par les tables systèmes.

SELECT name FROM sysobjects WHERE type='U'

Comment connaître la liste des colonnes d'une


table ?[haut]
auteurs : Wolo Laurent, Fabien Celaia
Comme pour la liste des bases de données d'un serveur,
SQL Server offre trois possibilités 1-La consultation
des vues d'informations de schema

SELECT COLUMN_NAME, ORDINAL_POSITION


FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME='MA_TABLE'

2-L'utilisation de la procédure stockée sp_columns

EXEC sp_columns 'Nom_de_table'

3-L'utilisation des tables systèmes.

SELECT c.colid, c.name Colonne,


t.name Type,
CAST(c.prec as varchar(10)) +
CASE WHEN c.scale > 0 THEN ',' + CAST(c.scale as varchar(10) )
ELSE ''
END Taille
FROM syscolumns c INNER JOIN systypes t ON t.usertype=c.usertype
WHERE c.id=object_id('VotreTable')
ORDER BY c.colid

4-L'utilisation de la procédure stockée sp_help

EXEC sp_help NomTable

Comment lister l'ensemble des vues d'une base de


données SQL Server ?[haut]

auteur : Wolo Laurent


La liste des vues d'une base de données de SQL-Server
est accessible grâce à une requête
sur les tables systèmes : sysobjects, syscomments et
sysusers.

SELECT name
FROM sysobjects
WHERE type='V'

Mais il est recommandé d'utiliser les vues


d'informations de schemas.

SELECT *
FROM information_schema.views

Comment lister l'ensemble des UDF d'une base de


données SQL Server ?[haut]

auteur : Wolo Laurent


La liste des fonctions définies par l'utilisateur de
SQL-Server est accessible
grâce à une requête sur les tables systèmes :
sysobjects, syscomments et sysusers.

SELECT name
FROM sysobjects
WHERE type='FN'

Comment lister l'ensemble des procédures stockées


d'une base de données SQL Server ?[haut]

auteur : Wolo Laurent


La liste des procédures stockées de SQL-Server est
accessible grâce à une requête
sur les tables systèmes : sysobjects, syscomments et
sysusers.

SELECT name
FROM sysobjects
WHERE type='P'

On peut également utiliser la méthode des vues


d'informations de schema
SELECT *
FROM INFORMATION_SCHEMA.ROUTINES

Ou encore, utiliser la procedure stockée :


sp_stored_procedures

Comment lister l'ensemble des déclencheurs d'une


base de données SQL Server ?[haut]

auteur : spidetra
La liste des triggers de SQL-Server est accessible grâce
à une requête sur les tables systèmes : sysobjects,
syscomments et sysusers.

SELECT
o.name, o.xtype, c.text, u.name, o.crdate
FROM
dbo.sysobjects o
INNER JOIN dbo.syscomments c
ON c.id = o.id
INNER JOIN dbo.sysusers u
ON u.uid = c.uid
WHERE
xtype = 'TR'

Quelle est la requête qui permet de savoir quelles


colonnes d'une table servent de clé primaire
?[haut]

auteur : Fabien Celaia


Il existe une procédure stockée pour celà :

EXEC sp_pkeys @table_name='MaTable'

Quelle commande permet d'afficher la description


d'une table sous SQLServer ?[haut]

auteur : Wolo Laurent


sp_help MaTable

Ou

select
column_name as champ,
COALESCE(domain_name,
cast(data_type as varchar(128))+ ISNULL(' ' + cast(character_maximum_length as
varchar(10)) ,'')) as type_donnee,
CASE UPPER(IS_NULLABLE)
when 'YES' then ''
when 'NO' then 'Oui'
when Null then ''
else IS_NULLABLE
END as Obligatoire,
'' as description
from INFORMATION_SCHEMA.columns
where
table_name = 'Matable'
order by table_name, ordinal_position

Comment récupérer la valeur par défaut d'un champs


d'une table ?[haut]

auteur : Fabien Celaia

select cdefault
from syscolumns
where id = object_id('VotreTable')
and name = 'VotreColonne'

Quelle est le nombre de ligne de chacune des


tables d'une base de données ?[haut]

auteur : Wolo Laurent

Select O.Name as Table_Name, I.Rows as Rows_Count


FROM sysobjects O join sysindexes I
ON O.id=I.id
Where O.xtype='U'
Comment connaître le nom de la base de données en
cours ?[haut]

auteurs : Wolo Laurent, Fabien Celaia


Pour connaître le nom de la base de donnée en cours,
vous pouvez utiliser la fonction DB_NAME().
Base de donnée en cours

SELECT DB_NAME() AS BASE_DE_DONNEES_EN_COURS

Comment afficher la liste des bases de données


d'un serveur ?[haut]

auteur : Wolo Laurent


Vous avez trois méthodes au choix: 1- L'utilisation des
vues d'informations de schema, Exemple :
VUE D'INFORMATIONS DE SCHEMA

SELECT CATALOG_NAME
FROM INFORMATION_SCHEMA.SCHEMATA
Go

2-La consultation des tables systemes bien que non


recommandée pour des raisons de portabilité Exemple:
TABLES SYSTEMES

USE master
Go
SELECT name as BaseDedonneeDuServeur
FROM sysdatabases
Go

3-L'utilisation de la procedure stockée sp_databases


Exemple:
PROCEDURE STOCKEE SYSTEME

EXEC sp_databases
go
Comment changer le type de données d'une colonne
?[haut]

auteur : Wolo Laurent


Pour changer le type de données d'une colonne, MS SQL
Serveur fournit la clause
Alter Column
Exemple ferait l'affaire:

ALTER TABLE MyTable


ALTER COLUMN MyColumn NVARCHAR(20) NOT NULL

Vous pouvez également proceder comme ceci:

Demmarrer une transaction sérialisée;


Créer une nouvelle table avec le nouveau type de
données telle que souhaitée;
Importer les données de l'ancienne table vers la
nouvelle;
Supprimer l'ancienne table;
Renommer la nouvelle table avec l'ancien nom;

Exemple :

--Supposons que nous ayant une table T_Person dont la definition est :
CREATE TABLE Tmp_T_PERSONNE
(
PER_ID int NOT NULL,
PER_NOM varchar(50) NOT NULL,
PER_PRENOM varchar(50) NULL,
PER_NE_LE smalldatetime NOT NULL,
) ON [PRIMARY]
GO
--Et que nous voulons changer le type Per_Nom du type varchar(50) au type varchar(100)
--Nous aurons :
BEGIN TRANSACTION
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
--Créer une table temporaire ayant même structure que la première
CREATE TABLE Tmp_T_PERSONNE
(
PER_ID int NOT NULL,
PER_NOM varchar(100) NOT NULL,
PER_PRENOM varchar(50) NULL,
PER_NE_LE smalldatetime NOT NULL,
) ON [PRIMARY]
GO
-- Peupler la table
IF EXISTS(SELECT * FROM T_PERSONNE)
EXEC('INSERT INTO Tmp_T_PERSONNE (PER_ID,PER_NOM, PER_PRENOM,
PER_NE_LE, PAY_ID, PER_NE_A)
SELECT PER_ID, PER_NOM, PER_PRENOM, PER_NE_LE FROM T_PERSONNE
TABLOCKX')
GO
--Supprimer la table
DROP TABLE dbo.T_PERSONNE
GO
--Renommer la nouvelle table avec l'ancien nom
EXECUTE sp_rename N'Tmp_T_PERSONNE', N'T_PERSONNE', 'OBJECT'
GO
COMMIT

Comment renommer une base de données ?[haut]

auteur : Wolo Laurent


Pour renommer une base de données, MS SQL Server fournit
la procedure stockée sp_renamedb
Exemple :

EXEC sp_renamedb('MyOldDB','MyNiewDB')

Vous pouvez également créer une nouvelle base de


données, importez les données
par DTS de l'ancienne base de données vers la nouvelle,
puis supprimer l'ancienne base de données.

Comment renommer une table ou un autre object de


base de données ?[haut]

auteur : Wolo Laurent


Pour renommer un object d'une base de données, l'on peut
passer par la procedure stockée sp_rename.
Voici ce que l'aide en ligne de MS SQL Serveur 2000
apporte a ce sujet:
sp_rename [ @objname = ] 'object_name' ,
[ @newname = ] 'new_name'
[ , [ @objtype = ] 'object_type' ]

ou object_name désigne le nom de l'object à renommer,


new_name la nouvelle désignation de l'object et
et object_type l'une des valeurs du tableau ci-dessous.

ValeurDescription
COLUMNUne colonne qui doit être renommée..
BASE DE DONNEESBase de données définie par
l'utilisateur. Cette option est nécessaire pour
renommer une base de données.
INDEXUn index défini par l'utilisateur.
OBJECTÉlément d'un type repris dans sysobjects.
Par exemple, OBJECT peut être utilisé pour
renommer les objets dont les contraintes (CHECK,
FOREIGN KEY, PRIMARY/UNIQUE KEY), des tables
utilisateur, des affichages, des procédures
stockées, des déclencheurs et des règles.
USERDATATYPEType de données défini par
l'utilisateur ajouté en exécutant sp_addtype.

Exemple : A1-Renommer une table Dans cet exemple la


table customers est renommée custs.

EXEC sp_rename 'customers', 'custs'

A2-Renommer une colonne Dans cet exemple la colonne


contact title de la table customers est renommé title.

EXEC sp_rename 'customers.[contact title]', 'title', 'COLUMN'

Comment visualiser le code T-SQL d'une procédure


stocké ?[haut]

auteurs : Fabien Celaia, Morsi

select text
from dbo.syscomments, dbo.sysobjects
where syscomments.id = sysobjects.id
And sysobjects.xtype = 'P'
AND sysobjects.name='MaProcédure'

sp_helptext 'MaProcédure'

Comment récuperer un schéma de base de données sur


un serveur SQL2005 depuis une restauration d'une
base de données en version 2000 ?[haut]

auteur : Wolo Laurent

Database diagram support objects cannot be installed because this database does not have
a valid owner. To continue, first use the Files page of the Database Properties dialog box
or the ALTER AUTHORIZATION statement to set the database owner to a valid login,
then add the database diagram support objects.

Changer le niveau de compatibilité EXEC sp_dbcmptlevel


'database_name', '90';
Changer le propriétaire de la base de données sur le
nouveau serveur

ALTER AUTHORIZATION ON DATABASE::database_name TO valid_login

Comment lister les contraintes de clés primaires


et étrangères des tables d'une base de données
?[haut]

auteur : Rudi Bruchez

SELECT *
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS
WHERE TABLE_NAME = 'matable'

Comment trouver la liste des tables dont dépend la


vue ?[haut]
auteur : Maitrebn

SELECT DISTINCT NECESSAIRE.NAME


FROM SYSOBJECTS AS NECESSAIRE
INNER JOIN SYSDEPENDS AS DEPENDENCES
ON NECESSAIRE.ID = DEPENDENCES.depid
INNER JOIN SYSOBJECTS AS DEPENDANTE
ON DEPENDENCES.id = DEPENDANTE.id
WHERE DEPENDANTE.name='NOMDELAVUE'

Comment comparer 2 tables ?[haut]

auteurs : Frédéric Brouard, Fabien Celaia


Soit via requêtage peu aisé dans syscolumns (exemple
pour rechercher des différences de type, il faudra
complexifier avec outer jin et consors pour rechercher
les colonnes manquantes, de trop...)

select s1.name, s1.type, s2.name, s2.type


from syscolumns s1, syscolumns s2
where s1.id = object_id('MaTable1')
and s2.id = object_id('MaTable2')
and s1.name=s2.name
and s1.type<>s2.type

CONNECTIVITE A LA SGBD

Comment connaître le nom de l'utilisateur connecté au


serveur?
Comment obtenir le nombre d'utilisateurs connectés à
une base de données ?
Je n'arrive pas à créer un utilisateur, le système me
dit : user already exist ?
Quelle requête retourne les processus en train de
consommer ?
Afficher la liste des utilisateurs d'une base
spécifique ou de la base courante
Comment extirper un DDL complet pour un utilisateur
donné ?
Comment afficher les utilisateurs actifs d'une base
particulière ?
Comment changer temporairement un mot de passe que
l'on ne connaît pas ?
Comment connaître la dernière date de modification du
mot de passe des logins ?
Comment configurer une base de données en mode
utilisateur unique ?
Quel est le type de connexion le plus sûr ?

Comment connaître le nom de l'utilisateur connecté


au serveur?[haut]

auteur : Wolo Laurent


SQL Serveur fournit quatre fonctions permettants de
connaître l'utilisateur connecté au serveur pour la
session en cours. Exemple:

DECLARE @usr char(30)


SET @usr = user
SELECT 'L''utilisateur courant est : ' + @usr
GO
--Ou
SELECT 'L''utilisateur courant est : ' + SUSER_NAME()
GO
--Ou encore
SELECT 'L''utilisateur courant est : ' + SESSION_USER
GO
--Nous n'allons pas oublier la fonction Current_user
SELECT 'L''utilisateur courant est : ' + CURRENT_USER

Comment obtenir le nombre d'utilisateurs connectés


à une base de données ?[haut]

auteur : Wolo Laurent

USE MaBase
GO
SELECT COUNT(*)
FROM master..sysprocesses
WHERE dbid=db_id()
GO

ou

SELECT COUNT(*)
FROM master..sysprocesses
WHERE dbid=db_id('MabaseDeDonnée')

Pour la base courante ce sera alors :

SELECT COUNT(*)
FROM master..sysprocesses
WHERE dbid=db_id()

Je n'arrive pas à créer un utilisateur, le système


me dit : user already exist ?[haut]

auteur : Wolo Laurent


Il faut supprimer l'utilisateur et le recréer:

exec sp_dropuser 'utilisateur' -- drop le user


exec sp_adduser 'utilateur','login' -- recrée utilisateur et l'associe à login

Quelle requête retourne les processus en train de


consommer ?[haut]

auteur : Fabien Celaia

create proc sp__cpu as


/*
* Auteur : Fabien Celaia
* Date : 11/01/2002
* Desc : Affiche les processus utilisateurs en cours de traitement
* Parm : -
*/
SELECT
convert(char(4), spid) Spid,
convert(char(4), blocked) Blk,
convert(char(4), cpu) CPU,
left(loginame,15) 'Users',
left(hostname, 15) 'Host',
left(db_name(dbid),15) DB,
convert(char(20), cmd) Command,
convert(char(12), program_name) Program ,
convert(char(10), status) Status
FROM master..sysprocesses
WHERE spid <> @@spid
AND status not in ( 'BACKGROUND', 'sleeping')
ORDER BY cpu DESC
GO
GRANT execute on sp__cpu to public
GO

Afficher la liste des utilisateurs d'une base


spécifique ou de la base courante[haut]

auteur : Fabien Celaia

CREATE PROC sp__dbuser (@db varchar(30)=NULL)


AS
BEGIN
/*
* Auteur : Fabien Celaia
* Date : 3.3.2002
* Desc : Affiche la lsite des utilisateurs de la base courante ou de la base passée en
paramètre
* Parm : @db = nom de la base (optionel)
* Return : Nombre d'utilisateurs
* -1 si la base n'existe pas
*/
set nocount on

declare @i int
if @db is null
select @db=db_name()
else
if not exists(select name from master..sysdatabases where name = @db)
begin
Print 'La base '+@db+' n''existe pas dans le serveur '+ @@servername
return -1
end

select @db 'Base'


print ''

/* Nombre d'utilisateurs */
select @i=count(spid) from master..sysprocesses where dbid = db_id(@db) and status
<> 'BACKGROUND'

if @i = 0
begin
print ''
print 'Cette base est inutilisée'
print ''
end
else
begin
/* Liste des utilisateurs */
declare @snum varchar(4)
select @snum = convert(varchar(4), @i)
print ''
print @snum+' utilisateur(s) trouvés dans la base '+@db+', serveur '+ @@servername
print ''
select spid, loginame Utilisateur , cmd, program_name from master..sysprocesses
where dbid = db_id(@db) and status <> 'BACKGROUND'

/* Message informationel pour l'utilisateur courant */


if exists (select * from master..sysprocesses where spid=@@spid and
dbid=db_id(@db))
begin
print ''
print 'FYI : VOUS êtes actuellement connectés à la base '+@db+', serveur
'+@@servername
print ''
end
end
return @i
end
go
GRANT execute on sp__dbuser to public
go

Comment extirper un DDL complet pour un


utilisateur donné ?[haut]
auteur : Fabien Celaia

Create PROC sp_ddluser (@login varchar(30))


as
BEGIN
/* Auteur : Fabien Celaia
* Date : 6.6.05
* Desc : Extraction du DDL d'un utilisateur spécifique permettant sa recréation multi-
serveurs
* IParm : @login (obligatoire) : l'utilisateur à extraire
* OParm : 0 = succès
* -1 = l'utilisateur n'existe pas
*/

if not exists (select * from sysusers where name = @login)


begin
PRINT 'L''utilisateur '+@login+'n''existe pas dans la base '+db_name()+' du serveur
'+@@servername
return -1
end

if not exists (select * from master..syslogins where name = @login)


begin
/* Login inexistant => création */
select 'exec sp_addlogin '+ @login+ ', MotDePasse'
end

SELECT 'EXEC SP_DROPUSER '+@login


SELECT 'EXEC SP_ADDUSER '+@login+', '+ @login

/* membres de groupes */
select 'GRANT ROLE '+ g.name +' TO '''+u.name+''''
from sysmembers m inner join sysusers u
on m.memberuid = u.uid
inner join sysusers g
on m.groupuid=g.uid
where u.uid > 2
and u.name = @login

/*Droits*/
select
case p.protecttype
when 206 then 'REVOKE'
else 'GRANT ' end +

case p.action
when 26 then 'REFERENCES'
when 178 then 'CREATE FUNCTION'
when 193 then 'SELECT'
when 195 then 'INSERT'
when 196 then 'DELETE'
when 197 then 'UPDATE'
when 198 then 'CREATE TABLE'
when 203 then 'CREATE DATABASE'
when 207 then 'CREATE VIEW'
when 222 then 'CREATE PROCEDURE'
when 224 then 'EXECUTE'
when 228 then 'BACKUP DATABASE'
when 233 then 'CREATE DEFAULT'
when 235 then 'BACKUP LOG'
when 236 then 'CREATE RULE' end +
' ON ' + o.name +
case when p.action < 200 then
case when p.protecttype = 206 then ' FROM ' else ' TO ' END +u.name
else '' end +

case when p.protecttype = 204 then ' WITH GRANT OPTION' else '' end

from sysprotects p
inner join sysusers u on u.uid=p.uid
inner join sysobjects o on o.id=p.id
where p.columns = 0x01 OR p.columns is null
and u.name = @login
order by o.name

end

Comment afficher les utilisateurs actifs d'une


base particulière ?[haut]

auteur : Fabien Celaia

create proc sp__dbuser (@db varchar(30)=NULL)


as
begin
/*
* Auteur : Fabien Celaia
* Date : 10.8.2005
* Desc : Liste les utilisateurs actifs dans la base de données courante OU la base passée
en paramètre
* IParm : @db = Nom de la base (optionnel)
* Retour : Nombre d'utilisateurs connectés à la base
* -1 si la base passée en paramètre n'existe pas
*/
set nocount on

declare @i int
if @db is null
select @db=db_name()
else
if not exists(select name from master..sysdatabases where name = @db)
begin
Print 'La base '+@db+' n''existe pas sur le serveur SQL '+@@servername
return -1
end

select @db 'Base de données'


print ''

/* Number of users */
select @i=count(spid) from master..sysprocesses where dbid = db_id(@db) and status
<> 'background'

if @i = 0
begin
print ''
print 'Cette base est actuellement inutilisée'
print ''
end
else
begin
/* List of the users */
declare @snum varchar(4)
select @snum = convert(varchar(4), @i)
print ''
print @snum+' utilisateurs(s) actifs dans la base '+@db+' du serveur '+ @@servername
print ''
select spid, nt_username Utilisateur , cmd, program_name Programme from
master..sysprocesses
where dbid = db_id(@db) and status <> 'background'

/* Informational warning if the current user is in the database */


if exists (select * from master..sysprocesses where spid=@@spid and
dbid=db_id(@db))
begin
print ''
print 'FYI: VOUS êtes actuellement connectés sur la base '+@db+' du serveur '+
@@servername
print ''
end
end
return @i
end

Comment changer temporairement un mot de passe que


l'on ne connaît pas ?[haut]

auteur : Fabien Celaia


1) Changer de mot de passe après l'avoir sauvé

select password, name into old_login from master..sysxlogins


exec sp_password NULL, NouveauMotDePasse, MonLogin

2) Revenir à l'ancien

exec sp_configure updates,1


reconfigure with override

update master..sysxlogins
set password=O.password
from old_login O, sysxlogins L
where L.name=O.name
and O.name='MonUtilisateur'

drop table old_login

exec sp_configure updates,0

lien : Modifier temporairement un mot de passe inconnu

Comment connaître la dernière date de modification


du mot de passe des logins ? [haut]
auteur : Fabien Celaia

select name loginname, updatedate


from master..syslogins
where loginname is not null

Comment configurer une base de données en mode


utilisateur unique ?[haut]

auteur : Frédéric Brouard

ALTER DATABASE MABASE


SET SINGLE USER WITH ROLLBACK IMMEDIATE

Quel est le type de connexion le plus sûr ?[haut]

auteur : Fabien Celaia


Il y a deux types de connexion avec Micrososft SQL
Server.

le mode authentification Windows : il permet de


laisser au système la tâche d'authentifier le client.
Avantage : le mot de passe ne passe pas au travers des
paquets TDS et ne peut donc être "sniffé".
le mode authentification SQL avec un mot de passe et
un login propre au SQL server, sans lien avec le
système d'exploitation. Avantage : une distiction
claire et nette entre les droits sur la base et ceux
sur le système d'exploitation.
En terme de choix, on ne peut spécifier que
l'authentification Windows (1) ou l'authentification
mixte (1 + 2).
7. Les tables

7.1. Les contraintes de colonnes (verticales)


7.1.1. Obligatoire ([NOT] NULL)
7.1.2. Valeur par défaut (DEFAULT)
7.1.3. Séquence de collation (COLLATE)
7.1.4. Clef (PRIMARY KEY)
7.1.5. Unicité (UNIQUE)
7.1.6. Validation (CHECK)
7.1.7. Intégrité référentielle (FOREIGN KEY /
REFERENCES)
7.2. Les contraintes de table
7.2.1. Clef multicolonne (PRIMARY KEY)
7.2.2. Unicité globale (UNIQUE)
7.2.3. Validation de ligne (CHECK)
7.2.4. Integrité référentielle de table (FOREIGN
KEY / REFERENCES)
7.3. La gestion de l'intégrité référentielle
7.3.1. Mode de gestion de la la référence, clause
MATCH
7.3.2. Mode de gestion de l'intégrité clauses ON
UPDATE / ON DELETE
7.4. Mode de gestion de la déférabilité
7.5. Contraintes horizontales ou verticales ?
7.6. Alter et Drop
7.6.1. Changer le nom ou le type d'une colonne
7.6.2. Ajouter ou supprimer la contrainte NULL ou
NOT NULL
8. Les vues
9. Les informations de schéma
10. Les index
11. Résumé

6. Les assertions

Les assertions sont des contraintes dont l'étendue


dépasse les types de données, les colonnes et la
table pour permettre des règles de validation
entre différentes colonnes de différentes tables.
Les assertions au sens de la norme SQL sont donc
des objets de la base de données.
La syntaxe de création d'une assertion est la
suivante :

CREATE ASSERTION nom_assertion


CHECK (predicat)
[attribut_assertion]attribut_assertion ::
{INITIALLY DEFERRED | INITIALLY ILMMEDIATE} [ [ NOT ] DEFERRABLE ]
| [NOT] DEFERRABLE [INITIALLY DEFERRED | INITIALLY ILMMEDIATE]

NOTA : les règles de déférabilité seront discutées


lors de la partie consacrée aux contraintes de
table.

Par exemple vous pouvez définir une règle de


gestion qui indique que le montant des commandes
non réglées ne doit pas dépasser 20% du montant du
chiffre d'affaire déjà réalisé par le client.
Pour réaliser notre exemple nous avons besoin des
tables T_CLIENT, T_FACTURE et T_COMPTE, définies
de manière simpliste comme suit :

CREATE TABLE T_CLIENT


(CLI_ID INTEGER NOT NULL PRIMARY KEY,
CLI_NOM CHAR(32) NOT_NULL)

CREATE TABLE T_FACTURE


(FCT_ID INTEGER NOT NULL PRIMARY KEY,
CLI_ID INTEGER NOT NULL REFERENCES T_CLIENT (CLI_ID),
FCT_DATE DATE NOT NULL DEFAULT CURRENT_DATE,
FCT_MONTANT DECIMAL (16,2) NOT NULL,
FCT_PAYE BIT(1) NOT NULL DEFAULT 0)

CREATE TABLE T_COMPTE


(CPT_ID INTEGER NOT NULL PRIMARY KEY,
CLI_ID INTEGER NOT NULL REFERENCES T_CLIENT (CLI_ID),
CPT_CREDIT DECIMAL (16,2)
CPT_DEBIT DECIMAL (16,2)
CPT_DATE DATE NOT NULL DEFAULT CURRENT_DATE)Dans ce cas,
l'assertion prendra la forme :

Exemple 56
CREATE ASSERTION AST_VERIFACTURE
CHECK (SELECT SUM(FCT_MONTANT)
FROM T_FACTURE F
WHERE FCT_PAYE = 0
GROUP BY CLI_ID, FCT_PAYE) < (SELECT 0.2 * (SUM(CPT_DEBIT) -
SUM(CPT_CREDIT))
FROM T_COMPTE
WHERE CLI_ID = F.CLI_ID)Autre exemple, considérons que
l'unicité d'une
clef doit porter sur deux tables. Par exemple que
la clef identifiant un client ou un prospect doit
être unique au sein des deux tables afin qu'un
prospect puisse devenir un client sans changement
de clef. Dans ce cas l'assertion suivante peut
être mise en place :

Exemple 57

CREATE TABLE T_PROSPECT


(PRT_ID INTEGER NOT NULL PRIMARY KEY,
PRT_NOM CHAR(32) NOT_NULL)CREATE ASSERTION
AST_UNIQUE_ID_CLI_PRP
CHECK (NOT EXISTS (SELECT *
FROM T_CLIENT
WHERE CLI_ID IN (SELECT PRP_ID
FROM T_PROSPECT))
AND
NOT EXISTS (SELECT *
FROM T_PROSPECT
WHERE PRP_ID IN (SELECT CLI_ID
FROM T_CLIENT)))

REMARQUE : certains SGBDR n'utilisent pas les


assertions mais propose des mécanismes similaires
généralement nommés RULE (règle)...

7. Les tables

La voila la grosse partie du DDL qui vous


passionne. Alors otons nous tout de suite une
épine du pied en définissant la syntaxe de la
création de table :
CREATE [ { GLOBAL | LOCAL } TEMPORARY ] TABLE nom_table
( colonne | contrainte_de_table [ { , colonne | contrainte_de_table }... ] )
colonne ::
nom_colonne { type | domaine }
[ DEFAULT valeur_default ]
[ contrainte_de_colonne... ]
[ COLLATE collation ]
contrainte_de_colonne ::
[CONSTRAINT nom_contrainte]
[NOT] NULL
| UNIQUE | PRIMARY KEY
| CHECK ( prédicat_de_colonne )
| FOREIGN KEY [colone] REFERENCES table (colonne) spécification_référence
contrainte_de_table ::
CONSTRAINT nom_contrainte
{ UNIQUE | PRIMARY KEY ( liste_colonne )
| CHECK ( prédicat_de_table )
| FOREIGN KEY liste colonne REFERENCES nom_table (liste_colonne)
spécification_référence }

C'est une des syntaxes les plus simples que j'ai


pu trouver pour vous montrer l'ensemble des
possibilités qu'offre la norme SQL pour créer une
table.

En gros, disons que :

une table peut être créée de manière durable


(par défaut) ou temporaire et dans ce dernier
cas uniquement pour l'utilisateur et la
connexion qui l'a créé ou bien pour l'ensemble
des utilisateurs de la base
une table comporte des colonnes et des
contraintes de table
une colonne peut être spécifiée d'après un type
SQL ou un domaine créé par l'utilisateur
une colonne définie peut être dotée de
contraintes de colonnes telles que :
obligatoire, clef, unicité, intégrité
référentielle et validation
une contrainte de table porte sur une ou
plusieurs colonnes et permet l'unicité, la
validation et l'intégrité référentielle
RAPPEL : un nom de colonne doit être unique au
sein de la table

Voici quelques exemple de création de table


utilisant tantôt des types SQL tantôt des
domaines.

Exemple 58

CREATE TABLE T_CLIENT


(CLI_NOM CHAR(32),
CLI_PRENOM VARCHAR(32))Une table de clients dotée de deux colonnes avec
les nom et prénom des clients.

Exemple 59

CREATE TABLE T_CLIENT


(CLI_ID INTEGER NOT NULL PRIMARY KEY,
CLI_NOM CHAR(32) NOT NULL,
CLI_PRENOM VARCHAR(32))Une table de clients dotée de trois colonnes avec
la clef (numéro du client) les nom et prénom des
clients.

Exemple 60

CREATE DOMAIN D_NUM_ID INTEGER


CONSTRAINT C_CLEF CHECK (VALUE > 0)

CREATE DOMAIN D_ALFA_FIX_32 CHAR(32)

CREATE DOMAIN D_ALFA_VAR_32 VARCHAR(32)

CREATE TABLE T_CLIENT


(CLI_ID D_NUM_ID NOT NULL PRIMARY KEY,
CLI_NOM D_ALFA_FIX_32 NOT NULL,
CLI_PRENOM D_ALFA_VAR_32)Une table de clients similaire à l'exemple 58 à
base de domaines mais la clef ne peut être
négative.
Exemple 61

CREATE TABLE T_CLIENT


(CLI_ID INTEGER NOT NULL PRIMARY KEY,
CLI_NOM CHAR(32) NOT NULL CHECK (SUBSTRING(VALUE, 1, 1) <> ' '
AND UPPER(VALUE) = VALUE),
CLI_PRENOM VARCHAR(32) REFERENCES TR_PRENOM (PRN_PRENOM))Une
table de clients similaire à l'exemple 58 dont
le nom ne peut commencer par un blanc, doit être
en majuscule et dont le prénom doit figurer dans
la table de référence TR_PRENOM à la colonne
PRN_PRENOM.

Exemple 62

CREATE TABLE T_VOITURE


(VTR_ID INTEGER NOT NULL PRIMARY KEY,
VTR_MARQUE CHAR(32) NOT NULL,
VTR_MODELE VARCHAR(16),
VTR_IMMATRICULATION CHAR(10) NOT NULL UNIQUE,
VTR_COULEUR CHAR(16) CHECK (VALUE IN ('BLANC', 'NOIR',
'ROUGE', 'VERT', 'BLEU')))Une table de voiture avec immatriculation unique
et couleur limitée à 'BLANC', 'NOIR', 'ROUGE',
'VERT', 'BLEU'.

Exemple 63

CREATE TABLE T_CLIENT


(CLI_NOM CHAR(32) NOT NULL,
CLI_PRENOM VARCHAR(32) NOT NULL,
CONSTRAINT PK_CLIENT PRIMARY KEY (CLI_NOM, CLI_PRENOM))Une table
de clients dont la clef est le couple de
colonne NOM/PRENOM.

Nous allons maintenant détailler les différentes


contraintes dites verticales ou horizontales
suivant qu'il s'agit de contrainte de colonne ou
de contrainte de ligne. Cette notion de vertical
et horizontal fait référence à la visualisation
des données de la table :
Une contrainte de colonne est dite verticale parce
qu'elle porte sur une seule colonne. Dans la
figure ci dessus la contrainte de colonne est une
clef (PRIMARY KEY).
Une contrainte de ligne est dite horizontale parce
qu'elle porte sur plusieurs colonne et se valide
pour chaque ligne insérée.

La différence entre contrainte de ligne


(horizontale) et contrainte de colonne (verticale)
est purement terminologique puisque certaines
contraintes peuvent être définies horizontalement
comme verticalement :

contrainte[NOT] NULL
DEFAULT
COLLATE
PRIMARY KEY
UNIQUE
CHECK
FOREIGN KEY

colonne (verticale)OUIOUIOUIOUIOUIOUIOUI
ligne (horizontale)NONNONNONOUIOUIOUIOUI

REMARQUE : notons que toute contrainte peut être


déférrée, c'est à dire que ses effets peuvent être
suspendue pour ne jouer qu'à la fin d'une
transaction plutôt qu'à chaque ordre SQL sensée la
solliciter.

7.1. Les contraintes de colonnes (verticales)

Une colonne peut donc recevoir les contraintes


suivantes :

NULL / NOT NULL : précise si une valeur doit


obligatoirement être saisie dans la colonne ou
non
DEFAULT : valeur par défaut qui est placée dans
la colonne lors des insertions et de certaines
opération particulières, lorsque l'on a pas
donné de valeur explicite à la colonne
COLLATE : précise la séquence de collation,
c'est à dire l'ordre des caractères pour le tri
et les évnetuelles confusions possible
(minuscules/majuscules, caractères diacritiques
distinct ou non). Voir paragraphe 4 à ce sujet
PRIMARY KEY : précise si la colonne est la clef
de la table. ATTENTION : nécessite que la
colonne soit NOT NULL
UNIQUE : les valeurs de la colonne doivent être
unique ou NULL, c'est à dire qu'à l'exception du
marqueur NULL, il ne doit jamais y avoir plus
d'une fois la même valeur (pas de doublon)
CHECK : permet de préciser un prédicat qui
acceptera la valeur s'il est évalué à vrai
FOREIGN KEY : permet, pour les valeurs de la
colonne, de faire référence à des valeurs
préexitantes dans une colonne d'une autre table.
Ce mécanisme s'apelle intégrité référentielle
NOTA : toutes ces contraintes peuvent être placées
dans plusieurs colonnes, à l'exception de la
contrainte de clef PRIMARY KEY qui ne peut être
placée que sur une seule colonne. Pour faire de
plusieurs colonnes une clef, il faut utiliser une
contrainte de ligne (horizontale).

Lorsqu'au cours d'un ordre SQL d'insertion, de


modification ou de suppression, une contrainte
n'est pas vérifiée on dit qu'il y a "violation" de
la contrainte et les effets de l'ordre SQL sont
totalement annulé (ROLLBACK).

REMARQUE : le mot clef CONSTRAINT comme le nom de


la contrainte n'est pas obligatoire dans le cas de
contraintes de colonnes.

7.1.1. Obligatoire ([NOT] NULL)

On peut rendre la saisie d'une colonne obligatoire


en apposant le mot clef NOT NULL. Dans ce cas, il
ne sera jamais possible de faire en sorte que la
colonne soit vide. Autrement dit, la colonne devra
toujours être renseigné lors des ordres
d'insertion INSERT et de modification UPDATE.
Si l'on désire que la colonne puisse ne pas être
renseignée (donc accepter les marqueurs NULL), il
n'est pas nécessaire de préciser le mot clef NULL,
mais il est courrant qu'on le fasse par facilité
de lecture.

Exemple 64

CREATE TABLE T_PERSONNE1


(PRS_ID INTEGER NOT NULL
PRS_NOM VARCHAR(32) NOT NULL,
PRS_PRENOM VARCHAR(32) NULL,
PRS_DATE_NAISSANCE DATE)Crée une table dont les colonnes PRS_ID et
PRS_NOM
doivent obligatoirement être renseignés.

Exemple 65 - insertion et modification acceptées :

INSERT INTO T_PERSONNE1 VALUES (1, 'DUPONT', NULL, NULL)


INSERT INTO T_PERSONNE1 (PRS_ID, PRS_NOM) VALUES (2,
'DURAND')Exemple 66 - insertion et modification refusées :

INSERT INTO T_PERSONNE1 VALUES (3, NULL, 'Marcel', NULL)


INSERT INTO T_PERSONNE1 (PRS_ID, PRS_PRENOM) VALUES (4, 'Jean')NOTA :
les colonnes concourrant à la définition
d'une clef de table doivent impérativement
possèder une contrainte NOT NULL.

7.1.2. Valeur par défaut (DEFAULT)

La contrainte DEFAULT permet de préciser une


valeur qui sera automatiquement insérée en
l'absence de précision d'une valeur explicite dans
un ordre d'insertion. Certains autres ordres SQL,
comme la gestion de l'intégrité référentielle
peuvent faire référence à cette valeur par défaut.
Seule une valeur explicite, un marqueur NULL ou la
valeur retournée par les fonctions suivantes sont
acceptées : CURRENT_DATE, CURRENT_TIME[(p)],
CURRENT_TIMESTAMP[(p)], LOCALTIME[(p)],
LOCALTIMESTAMP[(p)], USER, CURRENT_USER,
SESSION_USER, SYSTEM_USER.

Exemple 67

CREATE TABLE T_PERSONNE2


(PRS_ID INTEGER,
PRS_NOM VARCHAR(32),
PRS_PRENOM VARCHAR(32),
PRS_SEXE CHAR(1) DEFAULT 'M',
PRS_DATE_NAISSANCE DATE DEFAULT CURRENT_DATE)NOTA : il n'est
pas possible de préciser une
valeur par défaut qui soit le résultat d'une
expression de requête.

Exemple 68

CREATE TABLE T_PERSONNE3


(PRS_ID INTEGER DEFAULT (SELECT MAX(PRS_ID) + 1
FROM T_PERSONNE3),
PRS_NOM VARCHAR(32),
PRS_PRENOM VARCHAR(32))Qui pourrait s'avérer bien utile pour générer de
nouvelles valeurs de clefs auto incrémentées !

7.1.3. Séquence de collation (COLLATE)

La séquence de collation permet de préciser


l'ordre positionnel des caractères et leur
éventuelle confusion, par exemple pour
s'affranchir de la différence entre majuscule et
minuscule ou encore entre caractères simples et
caractères diacritiques (accents, cédille et
autre...). La séquence de collation opére sur le
tri et la comparaison des valeurs littérales.
Exemple 69

CREATE TABLE T_PERSONNE4


(PRS_ID INTEGER
PRS_NOM VARCHAR(32) COLLATE SQL_CHARACTER,
PRS_PRENOM VARCHAR(32) COLLATE LATIN1)Rappelons qu'une séquence
de collation est
attachée à un jeu de caractère. Pour de plus
amples informations, voir "Jeu de caractères et
séquence de collation".

7.1.4. Clef (PRIMARY KEY)

Selon le docteur Codd, toute table doit être munie


d'une clef (souvent apellé à tort clef primaire en
opposition à clef étrangère...). Et toujours selon
le docteur Codd et la théorie des bases de
données, une clef doit impérativement toujours
être pourvue d'une valeur ! (sinon à quoi
servirait une clef en l'absence de serrure ?).
Lorsque la clef porte sur une seule colonne il est
possible de donner à cette colonne la contrainte
PRIMARY KEY.

Nous avons vu que la contrainte PRIMARY KEY peut


être posée sur une colonne (contrainte verticale)
ou sur plusieurs colonnes en contrainte de ligne
(horizontale). Si nous choisissons de la poser en
contrainte de colonne, alors une seule colonne de
la table peut en bénéficier.

Exemple 70

CREATE TABLE T_PERSONNE5


(PRS_ID INTEGER NOT NULL PRIMARY KEY,
PRS_NOM VARCHAR(32),
PRS_PRENOM VARCHAR(32))La contrainte PRIMARY KEY assure qu'il n'y
aura
aucune valeur redondante (doublon) dans la
colonne. La contrainte complémentaire NOT NULL
assure qu'il y aura toujours une valeur. Toute
tentative d'insérer une valeur préexistante de la
colonne se soldera par une violation de contrainte
de clef. Voici par exemple le message généré par
SQL Server dans ce cas :

Violation de la contrainte PRIMARY KEY 'PK__T_PERSONNE5__45F365D3'.


Impossible d'insérer une clé en double dans l'objet 'T_PERSONNE5'.
L'instruction a été arrêtée.NOTA : il est d'usage de placer la colonne clef en
tête de la description de la table pour des fins
de lisibilité.

Exemple 71 - clef primaire multicolonne impossible


en contrainte verticale :

CREATE TABLE T_PERSONNE5


(PRS_ID INTEGER NOT NULL PRIMARY KEY,
PRS_NOM VARCHAR(32),
PRS_PRENOM VARCHAR(32))
Impossible d'ajouter plusieurs contraintes PRIMARY KEY à la table 'T_PERSONNE6'.

7.1.5. Unicité (UNIQUE)

La contrainte d'unicité exige que toutes les


valeurs explicites contenues dans la colonne
soient uniques au sein de la table. En revanche,
la colonne peut ne pas être renseignée. En effet,
souvenez vous que les marqueurs NULL se propagent
dans les calculs et donc comparaison d'un marqueur
NULL à un ensemble de valeurs est impossible et se
solde par le renvoi d'un marqueur UNKNOW à la
place des valeurs TRUE ou FALSE attendue.

Exemple 72

CREATE TABLE T_PERSONNE7


(PRS_NOM VARCHAR(32),
PRS_PRENOM VARCHAR(32),
PRS_TELEPHONE CHAR(14) UNIQUE)
INSERT INTO T_PERSONNE7 VALUES ('Dupont', 'Marcel', '01 44 21 57 18')
INSERT INTO T_PERSONNE7 VALUES ('Duval', 'André', NULL)
INSERT INTO T_PERSONNE7 VALUES ('Durand', 'Jean', '06 11 86 46 69')
INSERT INTO T_PERSONNE7 (PRS_NOM, PRS_PRENOM) VALUES ('Dubois',
'Claude')
INSERT INTO T_PERSONNE7 VALUES ('Dugland', 'Alfred', '06 11 86 46 69')
Violation de la contrainte UNIQUE KEY 'UQ__T_PERSONNE7__47DBAE45'.
Impossible d'insérer une clé en double dans l'objet 'T_PERSONNE7'.
L'instruction a été arrêtée.
SELECT *
FROM T_PERSONNE7

PRS_NOM PRS_PRENOM PRS_TELEPHONE


-------------------------------- -------------------------------- --------------
Dupont Marcel 01 44 21 57 18
Duval André NULL
Durand Jean 06 11 86 46 69
Dubois Claude NULL

Dans cet exemple Dugland n'a pas été inséré car


son numéro de téléphone est identique à Durand.

REMARQUE : certains SGBDR comme MS SQL Server


refuse de voir la présence de plusieurs marqueurs
NULL dans la cas d'une contrainte d'unicité.
D'autres comme InterBase refusent une contrainte
d'unicité dépourvue d'une contrainte NOT NULL...

ATTENTION : vous ne pouvez pas définir une


contrainte d'unicité sur des colonnes de type BLOB

7.1.6. Validation (CHECK)

La contrainte CHECK de validation est celle qui


offre le plus de possibilité. En contre partie son
exécution est très coûteuse. Elle permet de
définir un prédicat complexe, basé sur une
comparaison pouvant contenir une requête de type
SELECT. Pour valider la contrainte, le prédicat
doit être évalué à TRUE ou UNKNOWN (présence de
NULL).

Sa syntaxe est :

CHECK ( prédicat )où prédicat peut contenir le mot clef VALUE pour
faire référence à la colonne pour laquelle la
contrainte est définie.

Exemple 73

CREATE TABLE T_PERSONNE8


(PRS_ID INTEGER CHECK (VALUE > 0),
PRS_NOM VARCHAR(32) CHECK (CHARACTER_LENGTH(VALUE) > 2),
PRS_PRENOM VARCHAR(32) CHECK (COALESCE(SUBSTRING(VALUE, 1,
1), 'X') BETWEEN 'A' AND 'Z'),
PRS_SEXE CHAR(1) CHECK (VALUE IN ('M', 'F')),
PRS_TELEPHONE CHAR(14) CHECK (SUBSTRING(VALUE, 1, 3) IN
(SELECT PREFIXE FROM T_NUM_TEL) OR IS NULL))La colonne PRS_ID ne peut
avoir de valeurs
inférieures à 0.
La colonne PRS_NOM doit avoir des valeurs
contenant au moins 2 caractères.
Le premier caractère de la colonne PRS_PRENOM, si
elle est renseigné, doit être compris entre A et
Z.
La colonne PRS_SEXE peut avoir exclusivement les
valeurs M ou F.
Les trois premiers caractères de la colonne
PRS_TELEPHONE si elle est renseignée doit
correspondre à une valeur se trouvant dans la
colonne PREFIXE de la table T_NUM_TEL.

ATTENTION : la longueur du prédicat d'une


contrainte CHECK (en nombre de caractères) peut
être limité. Il faut en effet pouvoir stocker
cette contrainte dans le dictionnaire des
informations de la base et ce dernier n'est pas
illimité.
7.1.7. Intégrité référentielle (FOREIGN KEY /
REFERENCES)

La contrainte de type FOREIGN KEY permet de mettre


en place une intégrité référentielle entre une (ou
plusieurs) colonnes d'une table et la (ou les)
colonne composant la clef d'une autre table afin
d'assurer les relations existantes et joindre les
tables dans le requête selon le modèle relationnel
que l'on a défini.
Le but de l'intégrité référentielle est de
maintenir les liens entre les tables quelque soit
les modiifications engendrées sur les données dans
l'une ou l'autre table.

Cette contrainte dans sa syntaxe complète est


assez complexe et c'est pourquoi nous allons dans
ce paragraphe donner une syntaxe très simplifié à
des fins didactiques :

FOREIGN KEY REFERENCES table (colonne)La syntaxe complète de la clause


FOREIGN KEY sera
vue au paragraphe 7.3.

ATTENTION : la colonne spécifiée comme référence


doit être une colonne clef.

Exemple 74

CREATE TABLE T_FACTURE1


(FTC_ID INTEGER,
PRS_ID INTEGER FOREIGN KEY REFERENCES T_PERSONNE5
(PRS_ID) ,
FCT_DATE DATE,
FCT_MONTANT DECIMAL(16,2))La table T_FACTURE1 est liée à la table
T_PERSONNE5 et ce lien se fait entre la clef
étrangère PRS_ID de la table T_FACTURE1 et la clef
de la table T_PERSONNE5 qui s'intitule aussi
PRS_ID.
NOTA : il est très important que les noms des
colonnes de jointure soit les mêmes dans les
différentes tables (notamment à cause du NATURAL
JOIN), mais cela n'est pas obligatoire.

Dès lors toute tentative d'insertion d'une facture


dont la référence de client est inexistante se
soldera par un échec. De même toute tentative de
supprimer un client pour lequel les données d'une
ou de plusieurs factures sont présente se soldera
par un arrêt sans effet de l'ordre SQL.

Examinons maintenant comment le SGBDR réagit pour


assurer la cohérence de la base lors d'opérations
tenant de briser les liens d'intégrité
référentielle :

Exemple 75

INSERT INTO T_PERSONNE5 VALUES (1, 'Dupont', 'Marcel')


INSERT INTO T_PERSONNE5 VALUES (2, 'Duval', 'André')
INSERT INTO T_FACTURE1 VALUES (1, 1, '2002-03-15', 1256.45)
INSERT INTO T_FACTURE1 VALUES (1, 2, '2002-04-22', 7452.89)
Tentative d'insertion d'une facture dont la personne n'est
pas référencé dans la table T_PERSONNE5 :
INSERT INTO T_FACTURE1 VALUES (1, 3, '2002-03-15', 1256.45)Conflit entre
l'instruction INSERT et
la contrainte COLUMN FOREIGN KEY
'FK__T_FACTURE__PRS_I__5165187F'.
Le conflit est survenu dans la base
de données 'DB_HOTEL',
table 'T_PERSONNE5', column 'PRS_ID'.
L'instruction a été arrêtée.
Tentative de suppression d'une personnet possédant
encore des factures
DELETE FROM T_PERSONNE5 WHERE PRS_NOM = 'Dupont'Conflit entre
l'instruction DELETE et
la contrainte COLUMN REFERENCE
'FK__T_FACTURE__PRS_I__5165187F'.
Le conflit est survenu dans la base
de données 'DB_HOTEL',
table 'T_FACTURE1', column 'PRS_ID'.
L'instruction a été arrêtée.
REMARQUE : Comme on le voit, le mécanisme
d'intégrité référentielle est un élément
indispensable au maintient des relations entre
tables. Un SGBD qui en est dépourvu ne peut pas
prétendre à gérer le relationnel. En particulier
MySQL ne peut en aucun cas prétendre être une base
de données relationnelle !

7.2. Les contraintes de table


Une table peut être pourvue des contraintes de
ligne suivante :

PRIMARY KEY : précise que la ou les colonnes


composent la clef de la table. ATTENTION :
nécessite que chaque colonne concourrant à la
clef soit NOT NULL.
UNIQUE : les valeurs de la ou les colonnes
doivent être unique ou NULL, c'est à dire qu'à
l'exception du marqueur NULL, il ne doit jamais
y avoir plus d'une fois la même valeur (pas de
doublon) au sein de l'ensemble de données formé
par les valeurs des différentes colonnes
composant la contrainte.
CHECK : permet de préciser un prédicat validant
différentes colonnes de la table et qui
accepterons les valeurs s'il est évalué à vrai.
FOREIGN KEY : permet, pour les valeurs de la ou
les colonnes, de faire référence à des valeurs
préexitantes dans une ou plusieurs colonnes
d'une autre table. Ce mécanisme s'apelle
intégrité référentielle.
Comme dans le cas des contraintes de colonne,
lorsqu'au cours d'un ordre SQL d'insertion, de
modification ou de suppression, une contrainte
n'est pas vérifiée on dit qu'il y a "violation" de
la contrainte et les effets de l'ordre SQL sont
totalement annulé (ROLLBACK).

7.2.1. Clef multicolonne (PRIMARY KEY)


La clef d'une table peut être composée de
plusieurs colonnes. Dans ce cas la syntaxe est :

CONSTRAINT nom_contrainte PRIMARY KEY (liste_colonne)Exemple 76 - clef


primaire sur PRS_NOM /
PRS_PRENOM

CREATE TABLE T_PERSONNE9


(PRS_NOM VARCHAR(32) NOT NULL,
PRS_PRENOM VARCHAR(32) NOT NULL,
PRS_TELEPHONE CHAR(14),
CONSTRAINT PK_PRS PRIMARY KEY (PRS_NOM, PRS_PRENOM))

7.2.2. Unicité globale (UNIQUE)

Un contrainte d'unicité peut être portée sur


plusieurs colonnes. Dans ce cas chaque n-uplets de
valeurs explicite doit être différents.
Dans ce cas la syntaxe est :

CONSTRAINT nom_contrainte UNIQUE (liste_colonne)Exemple 77 - définition d'une


clef unique sur
PRS_NOM / PRS_PRENOM

CREATE TABLE T_PERSONNE10


(PRS_ID INTEGER,
PRS_NOM VARCHAR(32),
PRS_PRENOM VARCHAR(32),
CONSTRAINT UNI_NOM_PRENOM UNIQUE (PRS_NOM, PRS_PRENOM))
INSERT INTO T_PERSONNE10 VALUES (1, 'Dupont', 'Marcel')
INSERT INTO T_PERSONNE10 VALUES (2, 'Duval', 'André')
INSERT INTO T_PERSONNE10 VALUES (3, 'Dupond', NULL)
INSERT INTO T_PERSONNE10 VALUES (4, NULL, NULL)
INSERT INTO T_PERSONNE10 VALUES (5, NULL, 'Alfred')
INSERT INTO T_PERSONNE10 VALUES (6, 'Duval', 'André')
Violation de la contrainte UNIQUE KEY 'UNI_NOM_PRENOM'.
Impossible d'insérer une clé en double dans l'objet 'T_PERSONNE10'.
L'instruction a été arrêtée.
SELECT *
FROM T_PERSONNE10PRS_ID PRS_NOM PRS_PRENOM
----------- -------------------------------- ----------
1 Dupont Marcel
2 Duval André
3 Dupond NULL
4 NULL NULL
5 NULL Alfred

REMARQUE : certains SGBDR comme MS SQL Server


refuse de voir la présence de plusieurs marqueurs
NULL dans la cas d'une contrainte d'unicité.
D'autres comme InterBase refusent une contrainte
d'unicité dépourvue d'une contrainte NOT NULL...

ATTENTION : vous ne pouvez pas définir une


contrainte d'unicité sur des colonnes de type BLOB

7.2.3. Validation de ligne (CHECK)

La contrainte CHECK permet d'effectuer un contrôle


de validation multicolonne au sein de la table.

Sa syntaxe est :

CONSTRAINT nom_contrainte CHECK ( prédicat )Exemple 78 - vérification de


présence
d'information dans au moins une colonne crédit ou
débit de la table compte :

CREATE TABLE T_COMPTE


(CPT_ID INTEGER,
CPT_DATE DATE,
CPT_CREDIT DECIMAL (16,2),
CPT_DEBIT DECIMAL (16,2),
CLI_ID INTEGER,
CONSTRAINT CHK_OPERATION CHECK((CPT_CREDIT >= 0 AND CPT_DEBIT
IS NULL) OR (CPT_DEBIT >= 0 AND CPT_CREDITIS NULL)))Toute tentative
d'insérer une ligne avec des
valeurs non renseignées pour les colonnes debit et
credit, ou bien avec des valeurs négative se
soldera par un refus.
7.2.4. Integrité référentielle de table (FOREIGN
KEY / REFERENCES)

Comme dans la cas d'une contrainte référentielle


de colonne, il est possible de placer une
contrainte d'intégrité portant sur plusieurs
colonne. Ceci est d'autant plus important qu'il
n'est pas rare de trouver des tables dont la clef
est composée de plusieurs colonnes. La syntaxe est
la suivante :

CONSTRAINT nom_contrainte FOREIGN KEY (liste_colonne) REFERENCES


nom_table_ref (liste_colonne_ref)Exemple 79

CREATE TABLE T_FACTURE2


(FTC_ID INTEGER,
PRS_NOM VARCHAR(32),
PRS_PRENOM VARCHAR(32),
FCT_DATE DATE,
FCT_MONTANT DECIMAL(16,2),
CONSTRAINT FK_FCT_PRS FOREIGN KEY (PRS_NOM, PRS_PRENOM)
REFERENCES T_PERSONNE9 (PRS_NOM, PRS_PRENOM))La table T_FACTURE2
est liée à la table
T_PERSONNE9 et ce lien se fait entre la clef
étrangère composite PRS_NOM / PRS_PRENOM de la
table T_FACTURE2 et la clef de la table
T_PERSONNE9 elle même composée des colonnes
PRS_NOM / PRS_PRENOM.

Examinons maintenant comment le SGBDR réagit pour


assurer la cohérence de la base lors d'opérations
tenant de briser les liens d'intégrité
référentielle :

Exemple 80

INSERT INTO T_PERSONNE9 VALUES ('Dupont', 'Marcel', '01 45 78 74 25')


INSERT INTO T_PERSONNE9 VALUES ('Duval', 'André', NULL)
INSERT INTO T_FACTURE2 VALUES (1, 'Dupont', 'Marcel', '2002-03-15', 1256.45)
INSERT INTO T_FACTURE2 VALUES (1, 'Duval', 'André', '2002-04-22', 7452.89)
Tentative d'insertion d'une facture dont la personne n'est pas référencé dans la table
T_PERSONNE5 :
INSERT INTO T_FACTURE1
VALUES (1, 'Dubois', 'Maurice', '2002-03-15', 1256.45)
Conflit entre l'instruction INSERT et la contrainte
TABLE FOREIGN KEY 'FK_FCT_PRS'. Le conflit est survenu
dans la base de données 'DB_HOTEL', table 'T_PERSONNE9'.
L'instruction a été arrêtée.
Tentative de suppression d'une personnet possédant
encore des factures
DELETE FROM T_PERSONNE5
WHERE PRS_NOM = 'Dupont' AND PRS_PRENOM = 'Marcel'
Conflit entre l'instruction DELETE et la contrainte
COLUMN REFERENCE 'FK__T_FACTURE__PRS_I__5165187F'.
Le conflit est survenu dans la base de données
'DB_HOTEL', table 'T_FACTURE1', column 'PRS_ID'.
L'instruction a été arrêtée.

7.3. La gestion de l'intégrité référentielle

Comme nous l'avions annoncé, la syntaxe de la pose


de contraintes d'intégrité est plus complexe que
ce qui vient d'être évoqué. Voici la syntaxe
complète de cette structure :

CONSTRAINT nom_contrainte
FOREIGN KEY (liste_colonne_table)
REFERENCES table_référencée (liste_colonne_référencées)
[ MATCH { FULL | PARTIAL | SIMPLE } ]
[ { ON UPDATE { NO ACTION | CASCADE | RESTRICT | SET NULL | SET
DEFAULT } ]
[ { ON DELETE { NO ACTION | CASCADE | RESTRICT | SET NULL | SET
DEFAULT } ]
[ { INITIALLY DEFERRED | INITIALLY IMMEDIATE } [ [ NOT ] DEFERRABLE ]
| [ NOT ] DEFERRABLE [ { INITIALLY DEFERRED | INITIALLY IMMEDIATE }
] ]Clause
MATCH de gestion de la référence
Clause ON UPDATE de mise à jour
Clause ON DELETE de suppression
Clause de déférabilité

Nous allons maintenant détailler les clauses


MATCH, ON UPDATE, ON DELETE et la déferabilité.
7.3.1. Mode de gestion de la la référence, clause
MATCH

Pour mieux comprendre le fonctionnement de cette


clause, voici le modèle utilisé :

Exemple 81

CREATE TABLE T_FOURNISSEUR


(FRN_NOM CHAR(16) NOT NULL,
FRN_PRENOM CHAR(16) NOT NULL,
CONSTRAINT PK_FRN PRIMARY KEY (FRN_NOM, FRN_PRENOM))
INSERT INTO T_FOURNISSEUR VALUES ('DUBOIS', 'Alain')
INSERT INTO T_FOURNISSEUR VALUES ('DURAND', 'Paula')

MATCH SIMPLE implique que :

si toutes les colonnes contraintes sont


renseignées, la contrainte s'applique
si une colonne au moins possède un marqueur
NULL, la contrainte ne s'applique pas
Exemple 82

CREATE TABLE T_COMMANDE1


(CMD_ID INTEGER NOT NULL PRIMARY KEY,
FRN_NOM CHAR(16),
FRN_PRENOM CHAR(16),
CONSTRAINT FK_CMD_FRN_MATCH_SIMPLE
FOREIGN KEY (FRN_NOM, FRN_PRENOM)
REFERENCES T_FOURNISSEUR (FRN_NOM, FRN_PRENOM)
MATCH SIMPLE)INSERT INTO T_COMMANDE1 VALUES (1, 'DUBOIS',
'Alain')
INSERT INTO T_COMMANDE1 VALUES (2, 'DUBOIS', NULL)
INSERT INTO T_COMMANDE1 VALUES (3, 'DUHAMEL', NULL)
INSERT INTO T_COMMANDE1 VALUES (4, NULL, 'Paula')
INSERT INTO T_COMMANDE1 VALUES (5, NULL, NULL)Insertion réussie
INSERT INTO T_COMMANDE1 VALUES (6, 'DUHAMEL', 'Marcel')Conflit entre
l'instruction INSERT et
la contrainte TABLE FOREIGN KEY
'FK_CMD_FRN_MATCH_SIMPLE'.
Le conflit est survenu dans la base
de données 'DB_HOTEL',
table 'T_FOURNISSEUR'.
L'instruction a été arrêtée.

MATCH FULL implique que :

la contrainte s'aplique toujours sauf si toutes


les colonnes sont pourvues d'un marqueur NULL
Par conséquent, il ne peut y avoir une colonne
renseigné et l'autre pas.

Exemple 83

CREATE TABLE T_COMMANDE2


(CMD_ID INTEGER NOT NULL PRIMARY KEY,
FRN_NOM CHAR(16),
FRN_PRENOM CHAR(16),
CONSTRAINT FK_CMD_FRN_MATCH_FULL
FOREIGN KEY (FRN_NOM, FRN_PRENOM)
REFERENCES T_FOURNISSEUR (FRN_NOM, FRN_PRENOM)
MATCH FULL)INSERT INTO T_COMMANDE1 VALUES (1, 'DUBOIS',
'Alain')
INSERT INTO T_COMMANDE1 VALUES (2, NULL, NULL)Insertion réussie
INSERT INTO T_COMMANDE1 VALUES (3, 'DUHAMEL', NULL)
INSERT INTO T_COMMANDE1 VALUES (4, NULL, 'Paula')
INSERT INTO T_COMMANDE1 VALUES (5, 'DUBOIS', NULL)
INSERT INTO T_COMMANDE1 VALUES (6, 'DUHAMEL', 'Marcel')Conflit entre
l'instruction INSERT et
la contrainte TABLE FOREIGN KEY
'FK_CMD_FRN_MATCH_FULL'.
Le conflit est survenu dans la base
de données 'DB_HOTEL',
table 'T_FOURNISSEUR'.
L'instruction a été arrêtée.

MATCH PARTIAL implique que :

La contrainte s'applique pour toutes les


colonnes renseignées
Exemple 84
CREATE TABLE T_COMMANDE2
(CMD_ID INTEGER NOT NULL PRIMARY KEY,
FRN_NOM CHAR(16),
FRN_PRENOM CHAR(16),
CONSTRAINT FK_CMD_FRN_MATCH_PARTIAL
FOREIGN KEY (FRN_NOM, FRN_PRENOM)
REFERENCES T_FOURNISSEUR (FRN_NOM, FRN_PRENOM)
MATCH PARTIAL)INSERT INTO T_COMMANDE1 VALUES (1, 'DUBOIS',
'Alain')
INSERT INTO T_COMMANDE1 VALUES (2, NULL, NULL)
INSERT INTO T_COMMANDE1 VALUES (4, NULL, 'Paula')
INSERT INTO T_COMMANDE1 VALUES (5, 'DUBOIS', NULL)Insertion réussie
INSERT INTO T_COMMANDE1 VALUES (3, 'DUHAMEL', NULL)
INSERT INTO T_COMMANDE1 VALUES (6, 'DUHAMEL', 'Marcel')Conflit entre
l'instruction INSERT et
la contrainte TABLE FOREIGN KEY
'FK_CMD_FRN_MATCH_PARTIAL'.
Le conflit est survenu dans la base
de données 'DB_HOTEL',
table 'T_FOURNISSEUR'.
L'instruction a été arrêtée.

NOTA : certains SGBDR n'ont pas implémenté le mode


de gestion de la référence. C'est le cas en
particulier de MS SQL Server et d'InterBase.

7.3.2. Mode de gestion de l'intégrité clauses ON


UPDATE / ON DELETE

Le mode de gestion de l'intégrité consiste à se


poser la question de ce que la machine doit faire
dans le cas ou l'on tente de briser une intégrité
référentielle. Nous avons vu que par défaut il
n'est pas possible de supprimer une personne ayant
encore desdonnées dans la table des factures et
qu'il n'est pas possible d'insérer une facture
pour une personne nonréférencée. Se mode est dit
en SQL : ON UPDATE NO ACTION, ON DELETE NO ACTION
ce qui signifie qu'aucune action particulière
n'est entreprise en cas de mise à jour ou
suppression.
Nous allons maintenant voir quels sont les autres
modes de gestion de l'intégrité référentielle
ATTENTION : ce mode n'a aucun effet sur le
comportement de la contrainte qui s'exerce de
toute façon en fonction de la clause MATCH

ON DELETE NO ACTION / ON UPDATE NO ACTION : aucun


traitement particulier n'est entrepris en cas de
mise à jour ou suppression d'informations
référencées. Autrement dit, il y a blocage du
traitement car le lien d'intégrité ne doit pas
être brisé. Même effets que RESTRICT, mais post
opératoire.

ON DELETE CASCADE / ON UPDATE CASCADE : en cas de


suppression d'un élément, les éléments qui le
référence sont aux aussi supprimés. En cas de
modification de la valeur de la clef, les valeurs
des clefs étrangères qui le référence sont elles
aussi modifiées afin de maintenir l'intégrité. Par
exemple en cas de suppression d'un client les
factures et commandes sont elles aussi supprimées.

NOTA : ce mode est très tentant, mais son coût de


traitement est très élevé et les performances
peuvent très rapidement se dégrader fortement.

ON DELETE SET NULL / ON UPDATE SET NULL : en cas


de suppression d'un élément, les éléments qui le
référence voit leur clef étrangère posséder le
marqueur NULL . De même en cas de modification de
la valeur de la clef. Le lien d'intégrité est
alors brisé.
L'intérêt d'une telle manoeuvre est de permettre
la suppression des lignes devenues orphelines de
manière différé, par exemple dans un batch de
nuit.

ON DELETE SET DEFAULT / ON UPDATE SET DEFAULT : en


cas de suppression comme en cas de mise à jour de
la clef référencée, la référence passe à la valeur
par défaut définie lors de la création de la
table. Ce mode permet l'insertion d'un client
générique, possédant un identifiant particulier
(par exemple 0 ou -1) afin de ne jamais briser le
lient d'intégrité référentielle. Bien entendu on
veillera ensuite à rectifier la vrai valeur du
lien au moment opportun si besoin est.

ON DELETE RESTRICT / ON UPDATE RESTRICT : mêmes


effets que NO ACTION, mais pré opératoire.

NOTA : certains SGBDR n'ont pas implémenté le mode


de gestion de l'intégrité. C'est le cas en
particulier de MS SQL Server. En revanche, il est
courant de trouver dans les SGBDR des options plus
limitées que celles fournies par la norme.

7.4. Mode de gestion de la déférabilité

La déférabilité d'une contrainte est une opération


nécessaire dès que différentes contraintes
intéragissent créant ainsi ce que l'on apelle une
"référence circulaire"...
Ainsi lorsqu'une table T1 fait référence à une
table T2 par une intégrité référentielle, se pose
le problème de la mise en place d'une intégrité
référentielle inverse de T2 vers T1...
Encore une fois nous voici confronté au problème
de l'oeuf et de la poule... C'est pour trancher ce
dilemne que la déférabilité d'une contrainte a été
défini par la norme SQL 2. Les SGBDR metant en
oeuvre cette gestion, comme ORACLE, ne courrent
pas les rues !
Tentons cependant d'y voir clair, à l'aide d'un
exemple...

Imaginons que nous voulons modéliser un client et


ses commandes et placer dans la table du client la
dernière commande servie... Le problème se pose
ainsi : comment insérer un nouveau client qui, par
définition, n'a pas encore de commande, alors que
l'on exige dans la table client de faire référence
à la dernière commande ?

Voici un premier jet du script de création de nos


deux tables :

Exemple 85

CREATE TABLE T_CLIENT


(CLI_ID INTEGER NOT NULL PRIMARY KEY,
CLI_NOM CHAR(32),
CDE_ID INTEGER NOT NULL,
CONSTRAINT FK_CDE FOREIGN KEY REFERENCES T_COMMANDE
(CDE_ID))

CREATE TABLE T_COMMANDE


(CDE_ID INTEGER NOT NULL PRIMARY KEY,
CDE_DATE DATE,
CLI_ID INTEGER NOT NULL,
CONSTRAINT FK_CLI FOREIGN KEY REFERENCES T_CLIENT (CLIK_ID))Il y a
fort à parier que ce script ne puisse être
joué sur la plupart des SGBDR...
On peut néanmoins l'amender de la manière suivante
:

Exemple 86

CREATE TABLE T_CLIENT


(CLI_ID INTEGER NOT NULL PRIMARY KEY,
CLI_NOM CHAR(32),
CDE_ID INTEGER NOT NULL)

CREATE TABLE T_COMMANDE


(CDE_ID INTEGER NOT NULL PRIMARY KEY,
CDE_DATE DATE,
CLI_ID INTEGER NOT NULL,
CONSTRAINT FK_CLI FOREIGN KEY REFERENCES T_CLIENT (CLIK_ID))

ALTER TABLE T_CLIENT


ADD CONSTRAINT FK_CDE FOREIGN KEY REFERENCES T_COMMANDE
(CDE_ID)Cependant je vous met au défi de pouvoir insérer
quoi que ce soit dans l'une quelconque des tables,
puisque l'une à besoin des informations de l'autre
et vice versa...

Pour répondre à ce cas de figure, la norme SQL 2 à


défini la "déférabilité" d'une contrainte... Au
fait savez-vous ce qu'est la déférabilité ?
Déférer quelqu'un c'est transféré la
responsabilité de cette personne à un moment
donné, à une autre instance. Ainsi un gangster
déféré au parquet voit la responsabilité de son
arrestation, passer des mains des policiers aux
mains des juges. La déférabilité est donc la
possibilité de "déférer".

Pour la norme SQL 2, la déférabilité se précise :

lors de la création du schéma


lors de l'exécution de la contrainte
Elle permet de transférer le moment ou la
validation de la contrainte va s'effectuer...

Reprenons la syntaxe de la clause de déférabilité


d'une contrainte :

[ { INITIALLY DEFERRED | INITIALLY IMMEDIATE } [ [ NOT ] DEFERRABLE ]


| [ NOT ] DEFERRABLE [ { INITIALLY DEFERRED | INITIALLY IMMEDIATE }
] ]Une contrainte peut donc être définie comme NOT
DEFERRABLE (c'est l'option par défaut) dans ce cas
elle s'apllique immédiatement (INITIALLY
IMMEDIATE).
Si elle est définie comme DEFERRABLE, alors il
convient de préciser quand :

INITIALLY DEFERRED signifie qu'elle prendra ses


effets en fin de transaction
INITIALLY IMMEDIATE signifie qu'elle appliquera
la contrainte dans l'ordre de mise à jour
(INSERT, UPDATE DELETE) sans attendre la fin de
la transaction
Pour modifier la déférabilité d'une contrainte,
SQL 2 à prévu l'ordre SET CONSTRAINTS :

SET CONSTRAINT { ALL | nom_contrainte1 [, nom_contrainte2 [...] ] }


{ IMMEDIATE | DEFFERED }Cet ordre permet de changer la déférabilité d'une
contrainte à la volée.

IMPORTANT

la déférabilité d'une contrainte est le seul


élément du langage capable de créer un "auto
rollback"
certains SGBDR valident les contraintes pour
chaque ligne ce qui n'est pas conforme à la
norme SQL 2 pour laquelle toute requête est une
transaction...
Pour résoudre notre problème, nous pouvons gérer
la déférabilité dans la construction de la table :

Exemple 87

CREATE TABLE T_CLIENT


(CLI_ID INTEGER NOT NULL PRIMARY KEY,
CLI_NOM CHAR(32),
CDE_ID INTEGER NOT NULL)

CREATE TABLE T_COMMANDE


(CDE_ID INTEGER NOT NULL PRIMARY KEY,
CDE_DATE DATE,
CLI_ID INTEGER NOT NULL,
CONSTRAINT FK_CLI FOREIGN KEY REFERENCES T_CLIENT (CLIK_ID))

ALTER TABLE T_CLIENT


ADD CONSTRAINT FK_CDE FOREIGN KEY REFERENCES T_COMMANDE
(CDE_ID)
DEFERRABLE INITIALLY DEFERRED
INSERT INTO T_CLIENT (CLI_ID, CLI_NOM)
VALUES (118, 'Dupont')

INSERT INTO T_COMMANDE (CDE_ID, CDE_DATE, CLI_ID)


VALUES (587, '2002-11-03', 118)

UPDATE T_CLIENT
SET CDE_ID = 587
WHERE CLI_ID = 118

COMMIT

Mais nous pouvons aussi piloter cette déférabilité


dans un script transactionné :

Exemple 88

CREATE TABLE T_CLIENT


(CLI_ID INTEGER NOT NULL PRIMARY KEY,
CLI_NOM CHAR(32),
CDE_ID INTEGER NOT NULL)

CREATE TABLE T_COMMANDE


(CDE_ID INTEGER NOT NULL PRIMARY KEY,
CDE_DATE DATE,
CLI_ID INTEGER NOT NULL,
CONSTRAINT FK_CLI FOREIGN KEY REFERENCES T_CLIENT (CLIK_ID))

ALTER TABLE T_CLIENT


ADD CONSTRAINT FK_CDE FOREIGN KEY REFERENCES T_COMMANDE
(CDE_ID)
DEFERRABLE INITIALLY IMMEDIATE
SET CONSTRAINTS FK_CDE DEFERRED

INSERT INTO T_CLIENT (CLI_ID, CLI_NOM)


VALUES (118, 'Dupont')

INSERT INTO T_COMMANDE (CDE_ID, CDE_DATE, CLI_ID)


VALUES (587, '2002-11-03', 118)

UPDATE T_CLIENT
SET CDE_ID = 587
WHERE CLI_ID = 118

COMMIT

Au fait, dans le principe n'importe quelle


contrainte (de colonne ou de table) peut disposer
d'une clause de déférabilité. Pas seulement les
intégrité référentielle !

7.5. Contraintes horizontales ou verticales ?


Comme nous l'avons vu, les contraintes peuvent
être définie PRIMARY KEY, UNIQUE, CHECK et FOREIGN
KEY peuvent être définies indifférement en
contraintes de colonnes comme en contrainte de
ligne. Ainsi une clef portant sur une seule
colonne peut parfaitement être définie en tant que
contrainte de table.

Ainsi les deux ordres suivants :

Exemple 89

CREATE TABLE T_CLIENT


(CLI_ID INTEGER NOT NULL PRIMARY KEY,
CLI_NOM CHAR(32))
CREATE TABLE T_CLIENT
(CLI_ID INTEGER NOT NULL,
CLI_NOM CHAR(32),
CONSTRAINT PK_CLI PRIMARY KEY (CLI_ID))

Sont strictement équivalent, même si l'un est plus


verbeux.

Mais il y a un net avantage à utiliser


systématiquemen des contraintes horizontales.
Simplement parce que :

elle sont plus lisible


elle sont nommées
elle peuvent facilement être supprimées et
réinsérées
Essayez donc dans le premier cas de l'exemple 89
de faire porter la clef primaire sur la colonne
CLI_NOM plutôt que CLI_ID...

Pour le deuxième cas, c'est bien plus simple :


Exemple 90

ALTER TABLE T_CLIENT DROP CONSTRAINT PK_CLI


ALTER TABLE T_CLIENT ADD CONSTRAINT PK_CLI PRIMARY KEY
(CLI_NOM)

7.6. Alter et Drop

Les ordres ALTER et DROP sont les ordres de


modification (ALTER pour altération) et
suppression (DROP).

L'ordre ALTER peut porter sur un domaine, une


assertion, une table, une vue, etc...

L'ordre ALTER sur une table permet de :

supprimer une colonne


supprimer une contrainte
ajouter une colonne
ajouter une contrainte
ajouter une contrainte de ligne DEFAULT
Il ne permet pas de :

changer le nom d'une colonne


changer le type d'une colonne
ajouter une contrainte de ligne NULL / NOT NULL
Syntaxe de l'ordre ALTER sur table :

ALTER TABLE nom_table


{ ADD definition_colonne
| ALTER nom_colonne { SET DEFAULT valeur_défaut | DROP DEFAULT }
| DROP nom_colonne [ CASCADE | RESTRICT ]
| ADD définition_contrainte_ligne
| DROP CONSTRAINT nom_contrainte [ CASCADE | RESTRICT ] }L'option
CASCADE / RESTRICT permet de gérer
l'intégrité de référence de la colonne ou la
contrainte.
Si RESTRICT, alors tout objet dépendant de cette
colonne ou de cette contrainte provoquera
l'annulation de l'opération de suppression.
Si CASCADE, alors tous les objets dépendant de
cette colonne ou contrainte seront supprimés.

Exemple 91

ALTER TABLE T_CLIENT


ADD CLI_PRENOM VARCHAR(25)

ALTER TABLE T_CLIENT


ADD CLI_DATE_NAISSANCE DATE,

ALTER TABLE T_CLIENT


ADD CONSTRAINT CHK_DATE_NAISSANCE CHECK (CLI_DATE_NAISSANCE
BETWEEN '1880-01-01' AND '2020-01-01')ATTENTION : ne pas tenter de rajouter une
colonne
avec l'attribut NOT NULL lorsque la table contient
déjà des lignes. Pour cette opération, veuillez
procéder en plusieurs étapes dans un script
transactionné.

DROP est l'ordre de suppression. Sa syntaxe est


onne peut plus simple :

DROP {TABLE | DOMAIN | ASSERTION | VIEW } nom_objet

7.6.1. Changer le nom ou le type d'une colonne


Ce cas n'est pas géré par un ordre simple de SQL.
En effet cette modification est trop risquée pour
être standardisée. Quid des données contenue dans
la colonne au passage de CHAR en FLOAT ? Quid des
références de cette colonne dans des vues, des
contraintes, des triggers si l'on en change le nom
?

Mais il est possible de contourner le problème en


réalisant un script transactionné. Certains SGBDR
proposent un ordre ALTER étendu ou une procédure
stockée (par exemple sp_rename de MS SQL Server).
Avant de lancer un tel script il convient de
s'assurer que le colonne ne fait l'objet d'aucune
référence interne (contraintes de table par
exemple) ou externe (vue, triggers...). Si c'est
le cas, il faut impérativement modifier,
désactiver ou supprimer ces éléments avant la
modification de la colonne.

Voici les différentes étapes du script à mettre en


oeuvre :

créer une colonne temporaire de même nom et même


type (ALTER TABLE ADD...
alimenter la colonne temporaire avec les valeurs
de l'actuelle (UPDATE ...
supprimer l'actuelle colonne (ALTER TABLE
DROP...
créer une nouvelle colonne avec le nouveau nom
et/ou le nouveau type (ALTER TABLE ADD...
alimenter la nouvelle colonne avec les données
de la colonne temporaire (UPDATE ...
supprimer la colonne temporaire (ALTER TABLE
DROP ...
Exemple 92 - modification d'une colonne CHAR(6)
contenant une date courte format FR en date SQL :

-- condition de départ

CREATE TABLE T_IMPORT


(IMP_ID INTEGER,
IMP_NOM VARCHAR(16),
IMP_DATE CHAR(6))

INSERT INTO T_IMPORT VALUES (254, 'Dupont', '251159')


INSERT INTO T_IMPORT VALUES (321, 'Durand', '130278')
INSERT INTO T_IMPORT VALUES (187, 'Dubois', '110401')

-- le script de modification

ALTER TABLE T_IMPORT


ADD TMP_IMP_DATE CHAR(6)
UPDATE T_IMPORT
SET TMP_IMP_DATE = IMP_DATE

ALTER TABLE
DROP IMP_DATE

ALTER TABLE
ADD IMP_DATE DATE

UPDATE T_IMPORT
SET IMP_DATE = CAST(CASE SUBSTRING(TMP_IMP_DATE, 5, 2)
WHEN < '03' THEN '20'
ELSE '19'
END || SUBSTRING(TMP_IMP_DATE, 5, 2)
|| '-' || SUBSTRING(TMP_IMP_DATE, 3, 2)
|| '-' || SUBSTRING(TMP_IMP_DATE, 1, 2) AS DATE)

ALTER TABLE
DROP TMP_IMP_DATE

COMMIT

-- on aura noté que le pivot de date pour changement de siècle aura été géré dans le
dernier update...

7.6.2. Ajouter ou supprimer la contrainte NULL ou


NOT NULL
Les étapes du script différent très peu. Voici un
exemple :

Exemple 93 - modification d'une colonne IMP_NOM en


plaçant la contrainte NOT NULL :

-- condition de départ

CREATE TABLE T_IMPORT


(IMP_ID INTEGER,
IMP_NOM VARCHAR(16),
IMP_DATE CHAR(6))

INSERT INTO T_IMPORT VALUES (254, 'Dupont', '251159')


INSERT INTO T_IMPORT VALUES (321, NULL, '130278')
INSERT INTO T_IMPORT VALUES (187, 'Dubois', '110401')
-- le script de modification

ALTER TABLE T_IMPORT


ADD TMP_IMP_NOM VARCHAR(16)

UPDATE T_IMPORT
SET TMP_IMP_NOM = COALESCE(IMP_NOM, '')

ALTER TABLE
DROP IMP_NOM

ALTER TABLE
ADD IMP_NOM VARCHAR(16) NOT NULL

UPDATE T_IMPORT
SET IMP_NOM = TMP_IMP_NOM

ALTER TABLE
DROP TMP_IMP_NOM

COMMIT

-- on aura noté qu'afin d'éviter un rejet massif de notre script,


-- on place un nom constitué d'une chaîne vide grace à l'opérateur
-- coalesce, dans le premier update

8. Les vues

Les vues de la norme SQL 2 ne sont autre que des


requêtes instanciées.
Elles sont nécessaires pour gérer finement les
privilèges. Elles sont utiles pour masquer la
complexité de certains modèles relationnel.

Voici la syntaxe SQL 2 pour définir une vue :

CREATE VIEW nom_vue [ ( nom_col1, [, nom_col2 ... ] ) ]


AS
requête_select
[WITH CHECK OPTIONS]Exemple 94 - vue simplifiant un modèle
-- la table suivante :
CREATE TABLE T_TARIF
(TRF_ID INTEGER PRIMARY KEY,
TRF_DATE DATE,
PRD_ID INTEGER,
TRF_VALEUR FLOAT)

-- permet de stocker l'évolution d'un tarif, sachant que celui-ci n'est applicable
-- pour un produit donné (PRD_ID) qu'à partir de la date TRF_DATE
INSERT INTO T_TARIF VALUES (1, '1996-01-01', 53, 123.45)
INSERT INTO T_TARIF VALUES (2, '1998-09-15', 53, 128.52)
INSERT INTO T_TARIF VALUES (3, '1999-12-31', 53, 147.28)
INSERT INTO T_TARIF VALUES (4, '1997-01-01', 89, 254.89)
INSERT INTO T_TARIF VALUES (5, '1999-12-31', 89, 259.99)
INSERT INTO T_TARIF VALUES (6, '1996-01-01', 97, 589.52)

-- pour des raisons de commodité d'interrogation des données, on voudrait


-- faire apparaître l'intervalle de validité du tarif plutôt que la date d'application

-- la vue suivante répond à cette attente


CREATE VIEW V_TARIF
AS
SELECT TRF_ID, PRD_ID, TRF_DATE AS TRF_DATE_DEBUT,
(SELECT COALESCE(MIN(TRF_DATE) - INTERVAL 1 DAY,
CURRENT_DATE)
FROM T_TARIF T2
WHERE T2.PRD_ID = T1.PRD_ID
AND T2.TRF_DATE > T1.TRF_DATE) AS TRF_DATE_FIN,
TRF_VALEUR
FROM T_TARIF T1
SELECT *
FROM V_TARIF
TRF_ID PRD_ID TRF_DATE_DEBUT TRF_DATE_FIN TRF_VALEUR
------- ------- --------------- ------------ ----------
1 53 1996-01-01 1998-09-14 123.45
2 53 1998-09-15 1999-12-30 128.52
3 53 1999-12-31 2002-09-03 147.28
4 89 1997-01-01 1999-12-30 254.89
5 89 1999-12-31 2002-09-03 259.99
6 97 1996-01-01 2002-09-03 589.52

Une vue peut être utilisée comme une table dans


toute requête de type SELECT. Mais à la différence
des tables, une vue peut être mise à jour (INSERT,
UPDATE, DELETE) que si elle obéit à un certain
nombre de conditions :
ne porter que sur une table (pas de jointure)
ne pas contenir de dédoublonnage (pas de mot
clef DISTINCT) si la table n'a pas de clef
contenir la clef de la table si la table en a
une
ne pas transformer les données (pas de
concaténation, addition de colonne, calcul
d'agrégat...)
ne pas contenir de clause GROUP BY ou HAVING
ne pas contenir de sous requête
répondre au filtre WHERE si la clause WITH CHECK
OPTIONS est spécifié lors de la création de la
vue
Bien évidemment une vue peut porter sur une autre
vue et pour que la nouvelle vue construite à
partir d'une autre vue puisse être modifié, il
faut que les deux vues répondent aussi à ces
critères.

En fait c'est plus simple qu'il n'y parait : il


suffit que le SGBDR puisse retrouver trace de la
ligne dans la table et de chaque valeur de chaque
colonne.

Exemple 95 - vue restreignant l'accès aux colonnes

-- soit la table suivante :


CREATE TABLE T_EMPLOYE
(EMP_ID INTEGER PRIMARY KEY,
EMP_MATRICULE CHAR(8),
EMP_TITRE VARCHAR(4),
EMP_NOM VARCHAR(32),
EMP_PRENOM VARCHAR(32),
EMP_DATE_NAIS DATE,
EMP_SALAIRE FLOAT,
EMP_STATUT CHAR(8),
EMP_MAIL VARCHAR(128),
EMP_TEL CHAR(16))
-- permet de stocker les employés de l'entreprise
-- pour le syndicat, on pourra définir la vue suivante :
CREATE VIEW V_EMP_SYNDICAT
AS
SELECT EMP_MATRICULE, EMP_TITRE, EMP_NOM, EMP_PRENOM,
EMP_DATE_NAIS, EMP_MAIL, EMP_TEL
FROM T_EMPLOYE
-- elle ne peut être mise à jour car la clef ne s'y trouve pas

-- pour le carnet d'adresse on pourra définir la vue suivante


CREATE VIEW V_EMP_SYNDICAT
AS
SELECT EMP_ID, EMP_TITRE || ' ' || EMP_PRENOM || ' ' || EMP_NOM AS
EMP_NOM_COMPLET, EMP_MAIL, EMP_TEL
FROM T_EMPLOYE
-- elle ne peut être mise à jour à cause des transformation de données (concaténation au
niveau du nom)

-- pour le service comptable, on pourra définir la vue suivante :


CREATE VIEW V_EMP_SYNDICAT
AS
SELECT EMP_ID, EMP_PRENOM, EMP_NOM, EMP_SALAIRE
FROM T_EMPLOYE
WHERE STATUT = 'ETAM'
WITH CHECK OPTIONS
-- elle pourra être mis à jour uniquement pour les salariés de type 'ETAM'La clause
WITH CHECK OPTION implique que si la vue
peut être mise à jour, alors les valeurs modifiées
insérées ou supprimées doivent répondre à la
validation de la clause WHERE comme s'il
s'agissait d'une contrainte.

Par exemple dans le cadre de la vue pour le


service comptable, il n'est pas possible de faire
:

UPDATE T_EMPLOYE
SET EMP_SALAIRE = EMP_SALAIRE + 100
WHERE STATUT = 'CADRE'

9. Les informations de schéma

Toute base de données, tout SGBDR bien constitué


permet de savoir ce qu'il contient. Ce sont les
méta données ou le dictionnaire des données
(souvent appelées à tort "tables systèmes") que la
norme apelle "information de schéma".

SQL 2 précise 23 vues permettant de connaitre les


éléments constituant l'architecture de données du
CATALOG du SGBDR. En voici la liste :

Élément du SGBDRSQL_LANGUAGESliste des langages


supportés au niveau SQL API
Élément du CATALOGSCHEMATAliste des bases
Élements d'une baseDOMAINSliste des domaines de
la base
TABLESliste des tables de la base
VIEWSliste des vues de la base
ASSERTIONSliste des contraintes de la base
CHARACTER_SETSliste des jeux de caractères de
la base
COLLATIONSliste des collations (schémas
d'équivalence de caractères) de la base
TRANSLATIONSliste des "translations" (schémas
de remplacement de caractères) de la base
Éléments d'une tableCOLUMNSliste des colonnes de
TOUTES les tables de la base
TABLE_CONSTRAINTSliste des contraintes des
tables de la base
REFERENTIAL_CONSTRAINTSliste des intégrités
référentielles de la base
CHECK_CONSTRAINTSliste des contraintes de
validité de la base
KEY_COLUMN_USAGEliste des colonnes définissant
les clefs (primaire ou étrangère) de la base
CONSTRAINT_COLUMN_USAGEliste des colonnes
définissant les contraintes de la base
CONSTRAINT_TABLE_USAGEliste des tables utilisée
par les contraintes de la base
Éléments d'une vueVIEW_TABLE_USAGEliste des
tables composant les vues de la base
VIEW_COLUMN_USAGEliste des colonnes composant
les vues de la base
Éléments d'un domaineDOMAIN_CONSTRAINTliste des
contraintes des domaines de la base
DOMAIN_COLUMN_USAGEliste des colonnes basées
sur les domaines de la base
PrivilègesTABLE_PRIVILEGESliste des privilèges
des tables de la base
COLUMN_PRIVILEGESliste des privilèges de
colonnes de table de la base
USAGE_PRIVILEGESliste des privilèges des autres
objets de la base

En sus de ces vues, la norme SQL impose une table


composée d'une unique ligne et d'une seule colonne
contenant le nom du CATALOG. Cette table se nomme
INFORMATION_SCHEMA_CATALOG_NAME.

Pour interroger ces vues, il faut en préciser


l'origine qui est par défaut "INFORMATION_SCHEMA".

Exemple 96

SELECT TC.CONSTRAINT_NAME, KCU.COLUMN_NAME


FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS TC
JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE KCU
ON TC.TABLE_NAME = KCU.TABLE_NAME
AND TC.CONSTRAINT_NAME = KCU.CONSTRAINT_NAME
WHERE TC.CONSTRAINT_TYPE = 'FOREIGN KEY'
AND TC.TABLE_NAME = 'T_EMPLOYE_EMP'Cet exemple liste les clef étrangères
et les
colonnes associées de la table T_EMPLOYE_EMP.

ATTENTION : tous les SGBDR ne proposent pas ces


vues standards pour accèder aux méta données.
Voici quelques éléments pour certains SGBDR :

DB2 : (SYSCAT.xxx) SYSCAT.TABLES,


SYSCAT.SCHEMATA, SYSCAT.REFERENCES,
SYSCAT.KEYCOLUSE...
ORACLE : USER_CATALOG, USER_TABLES, ALL_TABLES,
USER_SYNONYMS...
INFORMIX : SYSTABLES, SYSREFERENCES,
SYSSYNONYMS...
SYBASE : SYSDATABASES, SYSOBJETCS, SYSKEYS...
MS SQL SERVER : SYSDATABASES, SYSOBJETCS,
SYSFOREIGNKEYS, SYSREFERENCES...
INTERBASE : RDB$RELATIONS, RDB$FIELDS,
RDB$DATABASE...
Dans tous les cas, si votre SGBDR supporte les
vues standard de SQL 2 il vaut mieux les utiliser.
Dans le cas contraire, les créer semble un moindre
mal ! En effet, les vues sont garanties par la
norme tandis que les tables "systèmes" peuvent
évoluer d'une version à l'autre du SGBDR...

NOTA : tous les objets d'une base ne sont pas


toujours tous accessibles par les vues
normalisées. Voici un exemple de requête
interrogeant directement les tables systèmes d'une
base MS SQL Server à la recherche des objets
"tiggers", "fonctions", "procédures stockées" et
"vues" créées par l'utilisateur :

Exemple 97

-- les triggers
SELECT CAST('TRIGGER' AS VARCHAR(32)) AS TYPE_OBJET,
o1.name AS NOM_OBJET, o2.name AS TABLE_ASSOCIEE
FROM sysobjects o1
JOIN sysobjects o2
ON o1.parent_obj = o2.id
WHERE o1.xtype = 'TR'
AND o1.status >= 0
AND o1.category = 0
UNION
-- les fonctions
SELECT CAST('FONCTION' AS VARCHAR(32)),
name, ''
FROM sysobjects
WHERE xtype = 'FN'
AND status >= 0
AND category = 0
UNION
-- les procédures stockées
SELECT CAST('PROCEDURE' AS VARCHAR(32)),
name, ''
FROM sysobjects
WHERE xtype = 'P'
AND status >= 0
AND category = 0
UNION
-- les vues
SELECT CAST('VUE' AS VARCHAR(32)),
name, ''
FROM sysobjects
WHERE xtype = 'V'
AND status >= 0
AND category = 0
ORDER BY TYPE_OBJET, NOM_OBJET, TABLE_ASSOCIEE

10. Les index


Contrairement à une idée reçue, les index ne font
nullement partie du SQL. Ce sont en revanche des
éléments indispensables à une exploitation
performante de base de données. En effet un index
permet de spécifier au SGBDR qu'il convient de
créer une structure de données adéquate afin de
stocker les données dans un ordre précis. Par
conséquent les recherches et en particuliers les
comparaisons, notamment pour les jointures, sont
notablement accélérées. Dans le principe le gain
de temps espéré est quadratique. Par exemple si
une recherche sur une colonne dépourvue d'index
met 144 secondes, avec un index cette même
recherche sera supposée mettre 12 seconde (racine
carré de 144) !

Différents types d'index sont généralement


proposés. Voici quelques exemples de techniques
d'indexation :

index en cluster : l'ordre des données répond à


un ordre physique d'insertion, convient
particulièrement pour les clefs numériques auto
incrémentées (dans ce cas une table ne peut
recevoir qu'un seul index de ce type)
index en arbre équilibré : convient pour la
plupart des types de données
index en clef de hachage : convient pour des
colonnes dont la dispersion est très importante.
Un algorithme de hachage est mis en place (il
s'agit en général d'une transformation
injective)
index bitmap : convient pour des colonnes à
faible dispersion (ideal pour des colonnes
booléennes)
En règle général les fabriquants de SGBDR
proposent un mécanisme de création d'index dont la
syntaxe est proche des ordres basiques du SQL.
C'est en général l'ordre CREATE INDEX.

Voici la syntaxe d'un tel ordre pour MS SQL Server


:

CREATE [UNIQUE] [CLUSTERED | NONCLUSTERED] INDEX nom_index


ON nom_table (col1 [, col2 ...] )
[WITH
[PAD_INDEX]
[[,] FILLFACTOR = facteur_de_remplissage]
[[,] IGNORE_DUP_KEY]
[[,] DROP_EXISTING]
[[,] STATISTICS_NORECOMPUTE]
]
[ON groupe_de_fichiers]La plupart du temps lorsque vous créez une
contrainte de clef primaire, étrangère ou une
contrainte d'unicité, le SGBDR implante
automatiquement un index pour assurer la mécanisme
de contrainte avec des performances correctes. En
effet une contrainte d'unicité est facilité si un
tri sur les données de la colonne peut être activé
très rapidement.

CONSEIL : pour une table donnée, il convient


d'indexer dans l'ordre :

les colonnes composant la clef


les colonnes composant les clefs étrangères
les colonnes composant les contraintes d'unicité

les colonnes dotées de contraintes de validité


les colonnes fréquemment mises en relation,
indépendemenent des jointures naturelles
les colonnes les plus sollicitées par les
recherches
Dans la mesure du possible on placera des index à
ordre descendant pour les colonnes de type DATE,
TIME et DATETIME.

11. Résumé
partie en construction

Voici les différences entre les moteurs des bases


de données :

Mise à jour des donnéesParadoxAccessSybaseSQL


ServerOracleMySQLInterBasePostGreSQL
noms normatifsNon OonNon
CONNEXIONNon Non
SESSIONNon Non
CATALOGNon Oui
SCHEMAOui Oui
jeu de caractèresOui Oui
collationsOui Oui (v2000)
translationNon Non
CHARNon Oui
VARCHAROui Oui
NCHARNon Oui
NVARCHARNon Oui
BITNon Non (1)
VARBINARYOui Oui
INTOui Oui
SMALLINTOui Oui
FLOATOui Oui
DOUBLE PRECISIONNon Oui
DECIMALOui (BCD) Oui
NUMERICNon Oui
TIMESTAMPOui Oui
TIMEOui
DATEOui
INTERVALNon
BOOLEANOui Non
BLOBOui Oui
CLOB / NCLOBOui/Non Oui
ARRAYNon Non
ROWNon Non
REFNon Non
DOMAINNon Non (2)
ASSERTIONNon Non (2)

(1) le BIT SQL est limité à 1 BIT valant 0 ou 1


(2) non, mais mécanisme similaire

La SECURITE DES DONNEES SOUS SQL

Certains chef de projet oublient d'intégrer au


développement la sécurité des accès aux bases de données
exploitées par les applicatifs. Le but de ce document
est d'étudier les moyens de mettre en place cette
sécurité "a posteriori".

1. Constat
2. Le remède
2.1. Détection des failles
2.1.1. Accès serveur par compte par défaut ("sa")
2.1.2. Comptes d'accès serveur non sécurisés
2.1.3. Rôles et utilisateurs de la base non sécurisés
2.2. Création de mots de passe
2.3. Sécurisation de l'accès au serveur
2.4. Stratégie SQL Server de gestion de la sécurité
2.5. La connexion
3. Création de la sécurité
3.1. Protection du compte "sa"
3.2. Ajout d'utilisateurs et de connexions
4. La problématique de mise en oeuvre
4.1. Faille dans le système
4.2. Mise en place à travers les applications clientes

1. Constat
La plupart du temps, l'accès au serveur se fait par le
compte d'administration système (SA) sans aucun mot de
passe.

Aucun utilisateur ni rôle n'ayant été créé pour


l'exploitation des données c'est donc le propriétaire
des bases par défaut (dbo) qui exerce ses droits.
Ceux-ci étant illimités il est possible pour les
utilisateur finaux de modifier supprimer ou insérer dans
toutes les bases y compris les bases systèmes, les
schémas, les données comme le code (procédures stockées
et triggers notamment).

Bien évidemment, cet état de fait laisse la porte grande


ouverte aux attaques de serveurs SGBD, SQL Server
notamment, en particulier lorsque le réseau du client
accède à Internet.

Sur la sécurité en général dans SQL Server, voir le site


:
http://www.sqlsecurity.com/

2. Le remède
Le remède consiste à établir :

un accès sécurisé au serveur


plusieurs utilisateurs dotés de privilèges différents
pour l'exploitation des données et des procédures.

2.1. Détection des failles


Voici les manières les plus simples pour détecter les
failles de sécurité du serveur et de la base de données
cible.

2.1.1. Accès serveur par compte par défaut ("sa")


Lors de l'accès au serveur, par exemple en appelant
l'analyseur de requête depuis le menu Window, la boîte
de dialogue suivante apparaît :

La faille de sécurité à ce niveau est l'autorisation


d'accès avec un mot de passe vide.

NOTA : si ce n'est la boîte de dialogue de SQL Server,


le mécanisme est le même dans une application écrite en
langage hôte.

2.1.2. Comptes d'accès serveur non sécurisés


Une fois connecté au serveur, exécuter la requête
suivante sur la base master (à défaut, exécutez
préalablement USE MASTER GO ) :

SELECT NAME, PASSWORD


FROM SYSLOGINS
WHERE (PASSWORD IS NULL AND NAME IS NOT NULL)
OR (PASSWORD = '' AND NAME IS NOT NULL)La faille de sécurité à ce niveau
est la présence de
lignes dans la table. Ces lignes indiquent quels comptes
d'accès au serveur sont dépourvus de mot de passe ou
bien dotés d'un mot de passe constitué par une chaîne de
vide.

2.1.3. Rôles et utilisateurs de la base non sécurisés


Toujours dans la connexion ouverte, exécuter la requête
suivante sur la base master (à défaut, exécutez
préalablement USE MASTER GO ) :

select *
from sysusers
where (password is null) or (password = CAST('' as VARBINARY(128)))La faille de
sécurité à ce niveau est la présence de
lignes dans la table. Ces lignes indiquent quels
utilisateurs des différentes bases sont dépourvus de mot
de passe ou bien dotés d'un mot de passe constitué par
une chaîne de vide.

2.2. Création de mots de passe


Un bon mot de passe, est un mot de passe :

qui comporte un minimum de 6 à 8 caractères


ne comporte pas de caractères au delà des 128 premiers
du jeu de base hormis les caractères non imprimables
comporte un mélange de minuscule, majuscule, de
lettres et de chiffre, et au moins un signe de
ponctuation.
est changé fréquemment (plusieurs fois par an)
est généré par un générateur automatique de mots de
passe utilisant un générateur aléatoire de caractères
Il existe des sites de pirates informatiques recensant
les quelques centaines de mots de passe les plus
utilisés et certains programmes sont disponibles sur
Internet pour utiliser une telle liste afin d'accéder à
certains logiciels.

On devra donc veiller à mettre des mots de passe


suffisamment sûrs.

Liste des mots de passe des constructeurs :

http://www.astalavista.com/library/auditing/password/lists/defaultpasswords.shtml
http://www.phenoelit.de/dpl/dpl.html

2.3. Sécurisation de l'accès au serveur


L'accès au serveur doit se faire par un compte autre que
"sa" et avec un mot de passe sur le compte. Le compte
"sa" doit être réservé aux opérations de
d'administration et maintenance lourdes telles que la
sauvegarde et la restauration de données.

NOTA : les mots de passe ne sont pas visibles par


requêtes même lorsque l'on est administrateur de la base
de données.

Exemple :

SELECT NAME, PASSWORD


FROM SYSLOGINSDonne :

NAME PASSWORD
------------------------------ -------------------------------
sa ????????Tant est si bien que la perte de mots de passe est une
opération non récupérable sauf depuis une sauvegarde.

ATTENTION : si le paramétrage par défaut de SQL Server


est en casse insensible (option à déconseillée), alors
le mot de passe pourra être saisi indifféremment en
majuscule ou minuscule.

NOTA : la sécurité d'accès effectuée au niveau de la


base est la plus sûre qui puisse se faire. Gérer la
sécurité uniquement à travers une application est une
utopie puisque de nombreux outils peuvent accéder au
middleware ODBC, notamment l'ensemble des outils
Microsoft Office.

2.4. Stratégie SQL Server de gestion de la sécurité


SQL Server propose deux stratégies de gestion des
droits. L'une est intégrée à l'OS Windows NT et se
repose sur les comptes d'utilisateurs système. L'autre
est basée sur une technique plus proche de ce que la
norme SQL propose, c'est à dire une grande indépendance
entre les utilisateurs du système et les utilisateurs
des base de données.

ATTENTION : la stratégie de droits reposant sur les


droits des utilisateurs Système est a déconseillé
totalement : elle n'est absolument pas portable (donc
impossible pour un éditeur ou un prestataire à mettre en
œuvre chez son client) et à causé de multiples problèmes
dans les versions précédentes. De fait, Microsoft a
réintroduit la seconde solution qui était celle de
Sybase au départ lorsque Microsoft à racheté le produit
SQL Server à cet éditeur.

Alors que la norme SQL 2 ne propose pas plus que la


définition d'utilisateurs et l'octroit (GRANT, REVOKE)
de privilèges, MS SQL serveur y rajoute l'octroit DENY
et propose la notion de rôles de base de données et de
serveur, très proche de la norme SQL3 (pour plus
d'information concernant la gestion des privilèges SQL,
"lire La Gestion des Privilèges".

Un "rôle" est en fait un groupe d'utilisateur possédant


des droits communs.

Un utilisateur est un accès à la base et doit appartenir


au moins à un rôle.

ATTENTION : une mauvaise manipulation de ces rôles et


utilisateurs peut faire perdre définitivement tout accès
à certaines bases voire au serveur. L'important et de ne
modifier en aucun cas, ni le compte ni les droits
attribués au compte "sa".

SQL Server propose des rôles pré établis, présent dans


toutes les bases de données. Il s'agit des rôles
suivants :

roleutilisation
publicpar défaut
db_ownercréateur par défaut des objets de la base,
il possède toutes les autorisations sur la base de
données.
db_accessadmingère les accès : ajoute ou supprime
des ID utilisateur.
db_securituadmingère les droits : accès,
propriétés d'objet, rôles et membres des rôles
db_ddladmingestion des droits au niveau du sous
ensemble DDl du SQL : lance l'instruction ALL DDL
mais pas les instructions GRANT, REVOKE ou DENY
db_backupoperatoropérateur de sauvegarde (mais pas
de restauration !). Lance les instructions DBCC,
CHECKPOINT et BACKUP.
db_datareaderdroit de consultation des données de
la base (lecture uniquement). Sélectionne toutes
les données de toutes les tables utilisateur dans
la base de données.
db_datawriterdroit en lecture, écriture des
données de la base. Modifie les données de toutes
les tables utilisateur dans la base de données.
db_denydatareaderrévocation des droits en lecture
sur tous les objets de la base
db_denydatawriterrévocation des droits en écriture
sur tous les objets de la base

En principe il n'y a pas lieu de créer de nouveau rôles,


mais simplement de créer des utilisateurs et d'y
affecter un ou plusieurs rôles parmi ceux pré établis.

Les rôles db_deny... permettent de placer des


utilisateurs dans ces rôles afin d'interdire
immédiatement certains droits.

ATTENTION : une confusion classique consiste à


considérer l'utilisateur SQL comme une personne
physique, un utilisateur de PC par exemple. C'est un non
sens absolu. L'utilisateur SQL, n'est autre en général
qu'une application cliente...

2.5. La connexion
En définitive, ce que voit l'utilisateur de
l'application, comme ce que fait l'utilisateur d'un
SGBDR MS SQL Server, c'est d'activer sa connexion. Il
faut donc automatiser la gestion des droits par le biais
de la connexion.

Le nom d'une connexion ne peut :

contenir de barres obliques inversées (\)


correspondre à un nom réservé, par exemple "sa" ou
"public" ou bien déjà exister
avoir la valeur NULL ou être une chaîne vide ('')
NOTA : l'élément clef de la gestion de la sécurité au
sein de SQL Server est la connexion : la connexion
défini la base par défaut et l'utilisateur par défaut
dotés de ses privilèges.

3. Création de la sécurité
La sécurité doit être mise en place en deux phases :
protection du compte "sa" puis création de nouvelles
connexions dotés de droits.

3.1. Protection du compte "sa"


Le première phase consiste à doter le compte "sa" d'un
mot de passe difficile à chercher.
Une fois connecté au compte "sa" (sans mot de passe), il
faut ajouter le mot de passe que l'on veut donner à ce
compte à l'aide de la procédure stockée sp_password,
dont la syntaxe est :

sp_password 'mot_de_passe_actuel', 'nouveau_mot_de_passe', 'connexion'Exemple :

Use master
sp_password NULL, 'd1A*cv4:', 'sa'Donne le mot de passe "d1A*cv4:" au compte "sa".

Les utilisateurs déjà connectés en "sa" continuent de


voir la base de données, tandis que les nouveaux
accédants doivent impérativement donner le nouveau mot
de passe.

3.2. Ajout d'utilisateurs et de connexions


L'ajout d'un utilisateur affectés à des rôles
prédéterminés se fait en deux temps :

création de la connexion
ajout d'un utilisateur et de son rôle
Les procédures stockées à utiliser sont les suivantes :

sp_addlogin 'connexion', 'mot_de_passe', 'base_cible'


sp_adduser 'connexion', 'utilisateur', 'rôle'

NOTA : il est nécessaire d'utiliser la base de données


cible.

Exemple : création d'un utilisateur nommé usr_lecteur


dont la connexion est log_lecteur, le mot de passé
kw7E.6J et possédant le rôle de lecteur uniquement :
use maBase
go

exec sp_addlogin 'log_lecteur', 'kw7E.6J', 'maBase'


go

sp_adduser 'log_lecteur', 'usr_lecteur', 'db_datareader'


goPour tester la validité de cette sécurité, il suffit de
se connecter au serveur avec le compte d'utilisateur
log_lecteur et le mot de passe kw7E.6J. Dès lors toute
tentative d'insertion de mise à jour ou de suppression
se soldera par une inaction accompagné d'un message
d'erreur tel que :

Serveur: Msg 229, Niveau 14, État 5, Ligne 1


Autorisation DELETE refusée sur l'objet 'maTable', base de données 'maBase',
propriétaire 'dbo'.De même un changement de base cible provoquera
l'apparition d'un message d'erreur, comme celui-ci :

4. La problématique de mise en oeuvre


Différents problèmes vont rentrer en jeu à la fois lors
de la mise en œuvre sur le plan pratique de la sécurité,
mais aussi tout au long de la vie du serveur et des
bases de données qu'il contient, tout particulièrement
en ce qui concerne l'évolution de la structure de la
base et la maintenance du serveur.

4.1. Faille dans le système


Une importante faille existe sur SQL Server. En effet
lors de la sauvegarde d'une base de données les rôles et
les utilisateurs ne sont pas stockées. Ce qui fait qu'en
cas de restauration d'une base, celle-ci est déprotégée
!

Il convient donc de remettre en place systématiquement


ces éléments de sécurité.
C'est aussi pourquoi il faut se poser la question de la
responsabilité de la détention du compte "sa" et de sa
communication aux utilisateurs, mêmes les plus avertis.
Le cas de l'éditeur de progiciel est particulier. La
pratique consiste à sécuriser la base suffisamment
fortement pour que, au quotidien :

le client ait tous les droits de lecture sur toutes


les informations stockées dans la base
le client n'ait aucun droit de modification des
données de la base, sauf à passer par l'application
le client puisse effectuer quotidiennement les
sauvegardes
en cas de restauration, une procédure particulière
permette cette reprise de données, même en cas de
défaillance de l'éditeur, mais que cette procédure
lève la responsabilité de l'éditeur tant que ce
dernier n'a pas été informé et protège à nouveau la
base
Par exemple cette procédure peut consister d'une part en
une enveloppe cachetée comprenant le mot de passe
d'accès au compte "sa" et d'autre part l'automatisation
dans l'application de la reprotection automatique en cas
de défaillance des mots de passe habituels.

L'interdiction de modification des données, peut se


faire par un trigger contrôlant un code de cohérence
dont l'algorithme est tenu secret. pour ce faire le mode
de cryptage est requis pour la création de ces triggers.

4.2. Mise en place à travers les applications clientes


La mise en place d'une sécurité a posteriori comporte
quelques écueils qu'il convient d'éviter notamment
lorsque l'application est distribuée et qu'une télé
action systématique sur tous les postes de travail est
difficilement envisageable.
Dans ce cas, il convient de prévoir l'auto gestion
systématique de la mise en place de la sécurité.

L'organigramme fonctionnel peut se résumer dans ce cas


aux étapes suivantes :

l'applicatif tente de se connecter avec un compte en


lecture écriture seulement
il y parvient => FIN
il n'y parvient pas => l'applicatif se connecte avec
le compte administrateur ancien non protégé, puis
applique le script de protection
L'inconvénient est que ce mode peut prendre un temps
assez important notamment si le "time out" de connexion
a une durée assez longue.

Le mot de passe devra être stocké dans l'applicatif,


mais de façon non compréhensible notamment si l'outil de
développement produit du code interprété (cas de Windev,
Visual Basic...).
Il pourra aussi être stocké de manière externe, dans un
fichier de ressources par exemple (*.ini ou registre de
Windows) notamment si les applicatifs sont personnalisée
par poste ou par utilisateurs système.

On aura tout intérêt à crypter la chaîne de caractères


contenant le mot de passe, à l'aide d'une fonction
bijective, par exemple de la manière suivante :
mot de passe original : 123456789
mot de passe stockée : 192837465
en reprenant le premier puis le dernier caractère et
ainsi de suite par élimination des caractères déjà
repris.

Enfin, à chaque fois qu'il faudra modifier le schéma de


la base, il faudra prévoir dans le code applicatif
l'usage du compte "sa" et donc son mot de passe. Cela
laisse entendre que l'application doit être découpée en
deux parties : un exécutable à destination de
l'utilisateur et un autre exécutable (pouvant être lancé
par le premier) à destination du serveur de base de
données pour applications des modifications de structure
de la base.

S-ar putea să vă placă și