Les bases de données (database en anglais) sont une forme d’organisation des données. Elles permettent de centraliser différentes données en évitant les duplications inutiles et en garantissant un accès contrôlé évitant les corruptions. Une base de données peut gérer des données organisées suivant différents modèles : navigationnel, hierarchique, relationnel, post-relationnel… C’est le modèle relationnel, le plus utilisé, qui va nous intéresser ici.
Dans un modèle relationnel, les données sont organisées dans des tables (des tableaux à deux dimensions) ayant des relations entre elles via des clés étrangères.
Une base de données contient généralement plusieurs tables (comme un classeur contenant plusieurs feuilles).
Un serveur héberge généralement plusieurs bases (comme une armoire contenant plusieurs classeurs).
Un logiciel permettant d’interagir avec les bases et les tables est un système de gestion de bases de données (SGBD et DBMS en anglais). MySQL et SQLite sont des exemples de SGBD gratuits.
On interagit avec un SGBD par l’intermédiaire de lignes de commandes. On peut aussi utiliser une surcouche graphique (SQLiteStudio par exemple).
Les tables (ou relations) sont des tableaux à deux dimensions.
L’ensemble des valeurs permises pour les champs d’un attribut s’appelle un domaine.
Exemples de domaines : Entiers (INTEGER
ou INT
), chaînes de caractères de taille fixe (CHAR(X)
), chaîne de caractères de taille maximale fixée (VARCHAR(X)
), chaîne de caractères de taille libre (TEXT
).
Une clé est un groupe d’attributs (colonnes) minimum qui permet d’identifier de façon univoque un enregistrement (un tuple) dans la table.
Toute table doit comporter au moins une clé, ce qui implique qu’elle ne peut pas contenir deux enregistrements identiques.
Si plusieurs clés existent dans une relation, on en choisit une parmi celles-ci. Cette clé est appelée clé primaire.
La clé primaire est généralement choisie de façon à ce qu’elle soit la plus simple, c’est à dire portant sur le moins d’attributs possibles et sur les attributs ayant des domaines le plus basique possible (entiers ou chaînes courtes typiquement).
On appelle clés candidates l’ensemble des clés d’une relation qui n’ont pas été choisies comme clé primaire (elles étaient candidates à cette fonction).
Rq : il y a plusieurs autres clés candidates : les différentes combinaisons des attributs aboutissant à une concaténation unique.
S’il est impossible de trouver une clé primaire, ou que les clés candidates sont trop complexes, on peut faire appel à une clé artificielle (qui s’oppose à clé naturelle). Une clé artificielle est un attribut supplémentaire ajouté à la table, qui n’est lié à aucune signification, et qui sert uniquement à identifier de façon unique les enregistrements et/ou à simplifier les références de clés étrangères.
C’est généralement ce qui a été fait lorsque les valeurs d’un attribut correspondent au numéro de la ligne (index).
Une clé étrangère est un attribut ou un groupe d’attributs d’une table A apparaissant comme clé primaire dans une table B. Elle matérialise une référence entre les enregistrements de A et de B.
Une clé étrangère d’un enregistrement référence une clé primaire d’un autre enregistrement.
Seule une clé primaire peut être référencée par une clé étrangère, c’est même le seule fonction de la clé primaire : être la clé qui peut être référencée par les clés étrangères.
On peut représenter les relations entre tables grâce à des diagrammes sagitaux comme dans l’exemple ci-dessous.
Les clés primaires sont représentées en rouge et les clés étrangères en vert.
Dans cet exemple, l’attribut CODCOM
est insuffisant pour servir de clé primaire à la table Communes
(car il y a des valeurs redondantes), mais la combinaison des attributs CODCOM
et CODDEP
donne bien un code unique et donc convient.
Dans une base de données relationnelle, on extrait et on traite les données en utilisant un jeu d’opérations mathématiques. Il y a huit opérations principales, que l’on peut répartir en opérations ensemblistes et opérations relationnelles.
Le nombre de lignes de la table résultat vaut le produit des nombres de lignes de chacune des tables.
C’est l’opération inverse du produit cartésien. Pour trouver le quotient de la division de la table 1 par la table 2, on cherche avec quoi il faudrait faire le produit de la table 2 pour obtenir les lignes de la table 1 contenant les champs de la table 2.
La projection extrait une ou plusieurs colonnes (attributs) d’une table.
La restriction (aussi appelée sélection) extrait une ou plusieurs lignes (enregistrements) d’une table.
Une jointure interne permet de combiner deux tables en se servant d’une colonne dans chaque table ayant des valeurs en commun. On suture alors sur ces valeurs communes et on laisse tomber les lignes n’ayant pas de correspondance d’une table à l’autre.
Si on joint sur la clé étrangère d’une table correspondant à la clé primaire de l’autre table, toutes les lignes seront présentes dans le résultat !
Pour dialoguer avec une base de données relationnelle, on utilise le langage SQL (Structured Query Language).
On entre des requêtes (ou instructions, query en anglais), qui ressemble à des phrases (terminées par des points-virgules). Chaque requête est constituée de clauses faites de commandes (ou mots clefs) suivies d’arguments dont certains peuvent être remplacés par des jokers.
Les commandes sont des mots anglais, ce qui donne à SQL l’apparence d’une langue naturelle.
Pour illustrer les différentes requêtes, on va utiliser la base de données dont le modèle a été aperçu plus haut :
Et voici un extrait des trois tables :
Rq : NBCOM
désigne le nombre de communes (dans un département ou dans une région) et POP
la population (dans une commune, un département, ou une région).
Cette base est issue des données “Populations légales 2019” de l’INSEE publiées le 12/01/2022. Elle a été construite via SQLite et elle est intégrée au repo du TP si vous voulez interagir avec et reproduire les exemples.
C’est la commande la plus importante ! Elle permet de rechercher des données dans une table (ou plusieurs) en précisant des conditions.
SELECT
opère un mélange entre une projection et une restriction.
La syntaxe de base est :
SELECT
liste d’attributs projeté
FROM
liste de tables
WHERE
condition de la restriction
SELECT
indique le sous-ensemble des attributs qui doivent apparaître dans la réponse.FROM
décrit les tables (relations) qui sont utilisables dans la requête (c’est à dire l’ensemble des attributs que l’on peut utiliser).WHERE
exprime les conditions que doivent respecter les attributs d’une ligne (d’un tupe) pour pouvoir être dans la réponse. Une condition est un prédicat et par conséquent renvoie un booléen. Cette partie est optionnelle.Pour récupérer toutes les colonnes de la table commune :
SELECT *
FROM Communes;
le symbole *
sert de joker.
Pour récupérer seulement les colonnes CODCOM
et POP
de la table commune (on opère ainsi une projection) :
SELECT CODCOM, POP
FROM Communes;
On peut aussi très simplement opérer le produit cartésien de deux tables en sélectionnant des attributs de chacune des tables :
SELECT Regions.REG, Departements.CODDEP
FROM Regions, Departements;
Pour récupérer la colonne COM
triée suivant la colonne POP
en ordre ascendant (par défaut), on utilise la clause ORDER BY
:
SELECT COM
FROM Communes
ORDER BY POP;
Aucun habitant à Cumières-le-Mort-Homme… le nom semble approprié 😨
Et pout un tri en ordre descendant, on ajoute le mot clef DESC
:
SELECT COM
FROM Communes
ORDER BY POP DESC;
Où sont passées Lyon, Paris, Marseille ?
Pour récupérer seulement un nombre limité d’enregistrements (de lignes), on utilise le mot clef LIMIT
:
SELECT CODDEP, POP
FROM Departements
LIMIT 5;
Et on peut aussi décaler l’ensemble des résultats d’un certain nombre de lignes grâce au mot clef OFFSET
.
SELECT CODDEP, POP
FROM Departements
LIMIT 5 OFFSET 3;
Utilions maintenant WHERE
pour filtrer les résultats (on opère alors une restriction/sélection).
Sélectionnons les codes départements des communes de plus de $50\,000$ habitants :
SELECT CODDEP
FROM Communes
WHERE POP > 50000;
On remarque sur le résultat précédent qu’il y a des résultats redondants… Pour éliminer les doublons, on peut utiliser SELECT DISTINCT
.
SELECT DISTINCT CODDEP
FROM Communes
WHERE POP > 50000;
La clause WHERE
d’une instruction de sélection est définie par une condition. Une telle condition s’exprime à l’aide d’opérateurs de comparaison et d’opérateurs logiques. Le résultat d’une expression de condition est toujours un booléen.
Les opérateurs au programme :
Opérateur | Signification |
---|---|
= |
égal à |
<> |
différent de |
< |
inférieur à |
<= |
inférieur ou égal à |
> |
supérieur à |
>= |
supérieur ou égal à |
AND |
et |
OR |
ou (inclusif) |
NOT |
non |
IN |
appartient à |
Lorsque la condition porte sur une chaîne de caractères, deux jokers sont utilisable : %
pour désigner une chaîne quelconque de taille non fixée et _
pour désigner un unique caractère quelconque. Mais il faut alors utiliser l’opérateur de comparaison LIKE
plutôt que =
. C’est a priori hors programme.
SELECT COM
FROM Communes
WHERE COM LIKE 'Paris%';
SELECT COM
FROM Communes
WHERE COM LIKE '%Arrondissement';
SELECT COM
FROM Communes
WHERE COM LIKE '_ours';
Afin de décrire un attribut d’une table en particulier (dans le cas d’une requête portant sur plusieurs tables notamment), on utilise la notation table.attribut
.
SELECT Communes.COM, Departements.DEP, Regions.REG
FROM Communes, Departements, Regions
WHERE Communes.CODDEP = Departements.CODDEP AND Communes.CODREG = Departements.CODREG;
On peut aussi renommer les tables et les attributs par des alias grâce au mot clef AS
afin d’en simplifier la syntaxe.
Réécrivons par exemple la requête précédente. Les alias servent à la fois à simplifier le nom des tables (raccourcis définis dans le FROM
, mais qu’on peut déjà utiliser dans le SELECT
) ou rendre le nom des attributs plus parlants (dans le SELECT
).
SELECT Co.COM AS ville, De.DEP AS departement, Re.REG AS region
FROM Communes AS Co, Departements AS De, Regions AS Re
WHERE Co.CODDEP = De.CODDEP AND Co.CODREG = Re.CODREG;
Les fonctions d’agrégation sont des fonctions de type statistique qui prennent en argument un ou plusieurs attributs et qui s’appliquent à l’ensemble des champs ainsi sélectionnés.
Les fonctions d’agrégation ont pour résultat une valeur atomique (pas un groupe) comme un nombre ou une chaîne.
Les fonctions au programme sont :
Fonction | renvoie |
---|---|
MIN |
la valeur minimale |
MAX |
la valeur maximale |
SUM |
la somme |
AVG |
la moyenne |
COUNT |
le nombre d’enregistrements |
SELECT COUNT(*) AS nb_communes
FROM Communes;
SELECT MIN(POP), MAX(POP)
FROM Regions;
GROUP BY
Grâce à la clause GROUP BY
, on peut grouper ensemble (partitionner) des lignes qui ont les mêmes valeurs dans une ou plusieurs colonne.
Mais c’est l’application d’une fonction sur chaque agregat obtenu qui en fait tout son intérêt.
Syntaxe générale d’une agrégation :
SELECT
liste ordonnée d’attributs de partionnement, liste d’application de fonctions sur d’autres attributs
FROM
liste de tables
WHERE
condition qui filtre les tables
GROUP BY
liste ordonnée d’attributs de partitionnement
HAVING
condition qui filtre les agrégats
Montrons comment on peut retrouver les attributs NBCOM
et POP
de la table Departements
directement à partir de la table Communes
:
SELECT CODDEP, COUNT(*) AS NBCOM_DEP, SUM(POP) AS POP_DEP
FROM Communes
GROUP BY CODDEP;
On retrouve bien les mêmes valeurs que dans la table Departements
.
Supposons maintenant que l’on veuille connaître la population moyenne par commune dans chacune des régions :
SELECT CODREG, AVG(POP) AS POP_MOY_REG
FROM Communes
GROUP BY CODREG;
La clause HAVING
permet d’effectuer une restriction sur les résultats de l’agrégation grâce à une condition.
Reprenons l’exemple des populations par département, mais n’affichons plus que les départements ayant une population supérieure à deux million d’habitant.
SELECT CODDEP, COUNT(*) AS NBCOM_DEP, SUM(POP) AS POP_DEP
FROM Communes
GROUP BY CODDEP
HAVING SUM(POP) > 2000000;
On a vu lors des exemples de tri que Paris, Marseille et Lyon n’apparaissent pas parmi les communes les plus peuplées, ce qui s’explique par le fait que chacune de ces villes sont découpées administrativement en arrondissements.
Essayons maintenant de regrouper ces arrondissements :
SELECT COM, SUM(POP) AS POP_TOT
FROM Communes
WHERE COM like '%Arrondissement'
GROUP BY CODDEP;
Et si on voulait opérer une sélection supplémentaire sur cet aggrégat de 3 villes, c’est un HAVING
que l’on utiliserait. Par exemple :
SELECT COM, SUM(POP) AS POP_TOT
FROM Communes
WHERE COM like '%Arrondissement'
GROUP BY CODDEP
HAVING COM like 'Lyon%'
WHERE
est un filtre sur les données ⇒ il s’applique avant l’agrégation par GROUP BY
.
HAVING
est un filtre sur les résultats d’un regroupement ⇒ il s’applique après l’agrégation par GROUP BY
.
On peut utiliser les opérations ensemblistes vues plus haut grâce aux opérateurs SQL suivants :
Opérateur SQL | Opération |
---|---|
UNION |
union |
INTERSECT |
intersection |
EXCEPT /MINUS |
différence |
La requête suivante donne les noms de communes qui sont aussi des noms de départements :
SELECT COM
FROM Communes
INTERSECT
SELECT DEP
FROM Departements;
On peut enchasser des requêtes les unes dans les autres en utilisant des parenthèses.
Cette requête affiche le département ayant les communes les plus faiblement peuplées en moyenne.
SELECT DEP
FROM Departements
WHERE CODDEP = (SELECT CODDEP
FROM (SELECT *,AVG(POP) AS Moy
FROM Communes
GROUP BY CODDEP)
WHERE Moy = (SELECT Min(Moy)
FROM (SELECT *,AVG(POP) AS Moy
FROM Communes
GROUP BY CODDEP)))
Pour traduire de telles requêtes enchassées les unes dans les autres, le plus simple est de partir de la dernière et remonter.
Essayons maintenant d’intégrer nos villes à arrondissements dans le classement général des 10 communes les plus peuplées :
SELECT COM,POP
FROM (SELECT *
FROM Communes
WHERE COM NOT like '%Arrondissement'
UNION
SELECT CODREG,CODDEP,CODCOM,COM,SUM(POP)
FROM Communes
WHERE COM like '%Arrondissement'
GROUP BY CODDEP)
ORDER BY POP DESC
LIMIT 10;
Pour que les 3 premiers résultats aient des noms corrects, on peut utiliser une commande CASE
comme ci-dessous. Ce n’est pas au programme.
SELECT
(CASE
WHEN COM LIKE 'Paris%' THEN 'Paris'
WHEN COM LIKE 'Marseille%' THEN 'Marseille'
WHEN COM LIKE 'Lyon%' THEN 'Lyon'
ELSE COM
END) AS COM, POP
FROM (SELECT COM,POP
FROM (SELECT *
FROM Communes
WHERE COM NOT like '%Arrondissement'
UNION
SELECT CODREG,CODDEP,CODCOM,COM,SUM(POP)
FROM Communes
WHERE COM like '%Arrondissement'
GROUP BY CODDEP)
ORDER BY POP DESC
LIMIT 10);
Joindre deux tables peut s’avérer très utile et s’opère grâce à la commande table1 JOIN table2 ON condition
.
Après le mot clef ON
, on précise le critère de jointure. S’il s’agit d’un critère d’égalité (du type colonneX.table1 = colonneY.table2
, alors on parle d’équi-jointure.
Les jointures évoquées ici sont des jointures internes. Ce sont de loin les plus utilisées et le programme de CPGE se limite à elles. De même le programme n’évoque que les équi-jointures.
On a obtenu plus haut un ensemble de communes dont le nom est aussi un nom de département. Associons-leur le nom du département où elles se trouvent grâce à une jointure :
SELECT DEP, Communes.COM
FROM Communes
JOIN (SELECT COM
FROM Communes
INTERSECT
SELECT DEP
FROM Departements) AS Inter
ON Inter.COM = Communes.COM
JOIN Departements ON Communes.CODDEP = Departements.CODDEP;
La première jointure sert à simplifier la table Communes
de manière à ce qu’elle ne contienne plus que les communes ayant des noms de départements.
Et la deuxième jointure sert à associer le nom du département où elle se trouve à chacune de ces communes.
On constate ainsi que le Doubs, la Corrèze et la Mayenne ont une structure récursive…
On aurait pu obtenir le même résultat en joignant d’abord les tables Communes
et Departements
, puis en réalisant une auto-jointure sur le résultat de cette première jointure.
Une auto-jointure consiste à associer ensemble deux colonnes de la même table. Cela nécessite d’utiliser deux alias différents pour désigner la table, sinon le moteur SQL est tout perdu (erreur ambiguous columnn name
)…
SELECT DISTINCT DEP, COM
FROM (SELECT * FROM Communes
JOIN Departements
ON Communes.CODDEP = Departements.CODDEP) AS ComDep1
JOIN (SELECT * FROM Communes
JOIN Departements
ON Communes.CODDEP = Departements.CODDEP) AS ComDep2
ON ComDep1.COM = ComDep2.DEP;