Aller au contenu principal

Bases de données & SQL · L2 · Section 10/11

Annales corrigées

Progression

Points d’expérience : XPSérie de jours consécutifs : · —Progression du module : — / —compris

#Annales corrigées

Cette page rassemble des sujets réellement posés en contrôle continu, en contrôle terminal et en travaux dirigés dans l'UE Base de données de L2 Informatique (UCA, campus Valrose), accompagnés de leurs corrections détaillées.

Provenance des sujets. Les contrôles continus portent la marque de leurs sessions : un CC d'octobre 2015 (durée 50 minutes, questions de cours et algèbre relationnelle sur une compagnie maritime), un CC de novembre 2020 (durée 50 minutes, base d'hôtels), un CC n°2 de novembre 2013 (durée 1h30, CarTrack puis requêtes SQL Armazon), et un contrôle continu d'avril 2012 en trois heures sur la base Apple Store. Le contrôle terminal de décembre 2016 nous est parvenu sous forme d'extraits : questions de cours sur le verrouillage en deux phases et l'ordre de traitement des clauses SQL, modélisation E-A d'une base Zoo, graphe d'attente et normalisation. Les exercices de TD 9 (normalisation et transformation E-A) et TD 10 (contrôle de concurrence) complètent l'ensemble, avec leurs corrigés.

Ce que les sujets testent, dans l'ordre de fréquence. L'identification des clés étrangères d'un schéma donné ; l'écriture de requêtes en algèbre relationnelle, dont au moins une question en « tous les » qui appelle la division ; les mêmes requêtes en SQL ; la normalisation d'une table plate jusqu'en 3NF ; et, quand la session porte sur les transactions, la construction d'un graphe de précédence ou d'un graphe d'attente. Trois questions de cours reviennent régulièrement : le rôle des clés étrangères, la différence entre algèbre relationnelle et SQL, et l'utilité du verrouillage en deux phases.

#Exercice 1 — Compagnie maritime : questions de cours et algèbre (CC octobre 2015)

Base de données d'une compagnie maritime. Les clés primaires sont soulignées en sujet.

texttext

1NAVIRE(NUMERO, NOM, TYPE, CAPACITE, DATE_FABRICATION)2CAPITAINE(NUMERO, NOM, SALAIRE)3TRAVERSEE(NUMERO, CAPITAINE, NAVIRE, VILLE_DEPART, VILLE_ARRIVE, HEURE_DEPART, HEURE_ARRIVE)
  1. Dans une base de données relationnelle, quel est le rôle des clés étrangères ?
  2. En algèbre relationnelle, l'opérateur « projection » peut-il réduire la cardinalité d'une relation ? Pourquoi ? Y a-t-il un moyen de garantir que la cardinalité reste invariée ?
  3. Quelles opérations requièrent la compatibilité envers l'union ? Qu'est-ce que la compatibilité envers l'union ?
  4. a) Quelles sont les clés étrangères ?
  5. Réaliser les requêtes suivantes en algèbre relationnelle :
    • b) le nom des capitaines ;
    • c) le nom des capitaines percevant un salaire inférieur à 2000 ;
    • d) le nom des navires fabriqués avant le 1/1/99 et dont la capacité est supérieure à 100 ;
    • e) le nom des capitaines effectuant une traversée en direction de NICE ;
    • f) les capitaines (nom) percevant un salaire supérieur à 2500 et effectuant une traversée ;
    • g) les navires (numéro) dont la capacité est inférieure à 50 ou effectuant une traversée à destination de NICE ;
    • h) les capitaines effectuant toutes les traversées au départ de NICE ;
    • i) les capitaines effectuant toutes les traversées au départ de NICE et dont le nom est différent de « De Maria » ;
    • l) les navires (nom) fabriqués après le navire « Casanova ».
Correction détaillée

1. Rôle des clés étrangères. Elles garantissent l'intégrité référentielle : les valeurs d'une clé étrangère doivent correspondre à une valeur d'une clé candidate d'un tuple de la relation référencée, ou être complètement nulles. Elles matérialisent les associations du modèle E-A et interdisent les liens orphelins — un TRAVERSEE pointant vers un capitaine inexistant est refusé par le moteur.

2. La projection et la cardinalité. Oui, la projection peut réduire la cardinalité : elle supprime les doublons. Si deux tuples ne différaient que par des attributs non projetés, ils deviennent identiques après projection et se confondent en un seul — c'est la sémantique ensembliste de l'opérateur. Pour garantir que la cardinalité reste invariée, il suffit de projeter un ensemble d'attributs qui contient une clé candidate (a fortiori la clé primaire) : chaque tuple reste alors distinguable, donc aucune fusion n'est possible.

3. Compatibilité envers l'union. Les opérations qui la requièrent sont les opérateurs ensemblistes binaires : union, intersection et différence. Elle signifie deux choses : les deux relations ont le même degré, et leurs attributs sont compatibles deux à deux (le i-ème attribut de l'une a le même domaine que le i-ème attribut de l'autre). Les noms d'attributs, eux, peuvent différer. Le produit cartésien, en revanche, n'exige aucune compatibilité : il accepte des schémas quelconques.

4. a) Clés étrangères.

texttext

1TRAVERSEE.CAPITAINE -> CAPITAINE.NUMERO2TRAVERSEE.NAVIRE    -> NAVIRE.NUMERO

b) Le nom des capitaines.

texttext

1π_NOM ( CAPITAINE )

c) Salaire inférieur à 2000.

texttext

1π_NOM ( σ_SALAIRE < 2000 ( CAPITAINE ) )

d) Navires fabriqués avant le 1/1/99 et de capacité supérieure à 100.

texttext

1π_NOM ( σ_DATE_FABRICATION < '1/1/99' ∧ CAPACITE > 100 ( NAVIRE ) )

e) Capitaines effectuant une traversée en direction de NICE. « En direction de » désigne la destination, donc VILLE_ARRIVE. On joint après avoir réduit TRAVERSEE à ce qui sert :

texttext

1π_NOM ( CAPITAINE ⋈_CAPITAINE.NUMERO = TRAVERSEE.CAPITAINE π_CAPITAINE ( σ_VILLE_ARRIVE = 'NICE' ( TRAVERSEE ) ) )

f) Salaire supérieur à 2500 et effectuant une traversée.

texttext

1π_NOM ( σ_SALAIRE > 2500 ( CAPITAINE ) ⋈_CAPITAINE.NUMERO = TRAVERSEE.CAPITAINE π_CAPITAINE ( TRAVERSEE ) )

L'ordre compte ici pour l'efficacité, pas pour le résultat : filtrer le salaire avant la jointure réduit le volume intermédiaire. Les deux écritures sont acceptées, la première est meilleure.

g) Navires de capacité inférieure à 50 ou effectuant une traversée à destination de NICE. C'est le « ou » qui impose l'union, et c'est aussi la question qui teste la compatibilité envers l'union :

texttext

1π_NUMERO ( σ_CAPACITE < 50 ( NAVIRE ) ) ∪ π_NAVIRE ( σ_VILLE_ARRIVE = 'NICE' ( TRAVERSEE ) )

Les deux opérandes ont le même degré (1) et des domaines compatibles (un numéro de navire des deux côtés). Une jointure ou une sélection n'aurait pas suffi : aucun navire ne satisfait nécessairement les deux conditions à la fois, il faut la réunion.

h) Capitaines effectuant toutes les traversées au départ de NICE. Question en « tous les » : division.

texttext

1R = π_CAPITAINE, NUMERO ( σ_VILLE_DEPART = 'NICE' ( TRAVERSEE ) )2S = π_NUMERO ( σ_VILLE_DEPART = 'NICE' ( TRAVERSEE ) )3π_CAPITAINE ( R ÷ S )

i) Les mêmes, de nom différent de « De Maria ». On compose la division avec une sélection sur CAPITAINE :

texttext

1π_NOM ( σ_NOM ≠ 'De Maria' ( CAPITAINE ) ⋈ ( R ÷ S ) )

l) Navires fabriqués après le navire « Casanova ». Il faut d'abord extraire la date de fabrication de « Casanova » dans une relation à un seul attribut renommé, puis comparer :

texttext

1C = ρ_DATE_CASANOVA ← DATE_FABRICATION ( π_DATE_FABRICATION ( σ_NOM = 'Casanova' ( NAVIRE ) ) )2π_NOM ( σ_NAVIRE.DATE_FABRICATION > C.DATE_CASANOVA ( NAVIRE × C ) )

Le renommage n'est pas décoratif : sans lui, les deux attributs s'appelleraient DATE_FABRICATION et la condition de comparaison serait ambiguë.

#Exercice 2 — Base d'hôtels (CC novembre 2020)

texttext

1HOTEL(nom, nomPropriétaire, ville, catégorie, wifi, restaurant)2CHAMBRE(identifiantChambre, numéro, surface, nombreCouchage, minibar, nomHôtel)3EQUIPE_SERVICE(numéro, nbrPersonnes, corpsDeMétier, nomHôtel)4INTERVENTION(numéro, identifiantChambre, numéroEquipeService, raisonIntervention, date, heureDébut, heureFin)5RESERVATION(numéro, nomClient, dateDébut, dateFin, identifiantChambre)

Un hôtel a un nom (supposé suffisant pour l'identifier), un propriétaire, une ville, une catégorie (simple, de charme, luxe, exceptionnel), et offre ou non le wifi et la restauration. Un hôtel a des chambres, caractérisées par un nombre de couchages, une surface et la présence d'un minibar. Chaque chambre a un numéro dans l'hôtel et un identifiant unique sur l'ensemble des hôtels. Une équipe d'intervention est forcément associée à un hôtel et identifiée par un numéro ; on note son nombre de personnes et son corps de métier. Chaque intervention est réalisée par une seule équipe et ne concerne qu'une chambre.

  1. Quelles sont les principales différences entre l'algèbre relationnelle et le langage SQL ?
  2. a) Identifiez les clés étrangères de la base.
  3. En algèbre relationnelle :
    • b) la liste des chambres réservées par le client de nom « Dupont », avec pour chacune le nom de l'hôtel ;
    • c) la liste des équipes d'intervention qui sont intervenues au moins une fois dans toutes les chambres.
  4. En SQL :
    • d) pour chaque hôtel de catégorie « luxe », donner le nombre de chambres ;
    • e) la liste des chambres qui n'ont jamais été réservées ;
    • f) dans la requête suivante, la sous-requête est-elle indépendante ou corrélée ? Pourquoi ? Quel est le résultat de cette requête ?
sqlsql

1select CHAMBRE.identifiantChambre2from CHAMBRE3where CHAMBRE.surface > (select AVG(C.surface)4                         from CHAMBRE C5                         where CHAMBRE.nomHôtel = C.nomHôtel)
Correction détaillée

1. Algèbre relationnelle contre SQL. Quatre différences structurelles.

  • Deux syntaxes : l'algèbre est symbolique et fonctionnelle, SQL est textuel et en clauses.
  • SQL est informatique, l'algèbre est formelle : SQL nomme explicitement les relations et les attributs, l'algèbre raisonne sur des symboles.
  • Sacs contre ensembles : sans DISTINCT, une projection SQL conserve les doublons, alors que la projection algébrique les élimine par définition. L'algèbre est ensembliste, SQL est multi-ensembliste.
  • Composition : toute opération d'algèbre rend une relation, donc s'imbrique librement (propriété de fermeture). SQL n'offre cette composition que par les sous-requêtes et les CTE.

2. a) Clés étrangères.

texttext

1CHAMBRE.nomHôtel                    -> HOTEL.nom2EQUIPE_SERVICE.nomHôtel             -> HOTEL.nom3INTERVENTION.identifiantChambre     -> CHAMBRE.identifiantChambre4INTERVENTION.numéroEquipeService    -> EQUIPE_SERVICE.numéro5RESERVATION.identifiantChambre      -> CHAMBRE.identifiantChambre

Notez que INTERVENTION porte deux clés étrangères et aucune vers HOTEL : l'hôtel est atteignable par transitivité, via la chambre ou via l'équipe.

b) Chambres réservées par « Dupont », avec le nom de l'hôtel.

texttext

1π_CHAMBRE.numéro, HOTEL.nom ( σ_nomClient = 'Dupont' ( RESERVATION )2                              ⋈_RESERVATION.identifiantChambre = CHAMBRE.identifiantChambre CHAMBRE3                              ⋈_CHAMBRE.nomHôtel = HOTEL.nom HOTEL )

On filtre d'abord (le client), on joint ensuite : le prédicat le plus sélectif va le plus près possible des feuilles.

c) Équipes intervenues dans toutes les chambres. Division, à nouveau :

texttext

1R = π_numéroEquipeService, identifiantChambre ( INTERVENTION )2S = π_identifiantChambre ( CHAMBRE )3π_numéroEquipeService ( R ÷ S )

Une équipe qui n'est jamais intervenue n'apparaît pas dans INTERVENTION : elle ne peut donc pas figurer dans le résultat, ce qui est bien la réponse attendue.

d) Nombre de chambres par hôtel de catégorie « luxe ».

sqlsql

1select h.nom, count(c.identifiantChambre) as nbChambres2from HOTEL h3join CHAMBRE c on c.nomHôtel = h.nom4where h.catégorie = 'luxe'5group by h.nom;

group by h.nom suffit (le nom identifie l'hôtel). Un hôtel de catégorie « luxe » sans chambre n'apparaîtrait pas avec un join : si l'énoncé attend 0, il faut un left join.

e) Chambres jamais réservées.

sqlsql

1select c.identifiantChambre2from CHAMBRE c3where not exists (select 1 from RESERVATION r4                  where r.identifiantChambre = c.identifiantChambre);

Variante équivalente : left join RESERVATION ... where r.numéro is null.

f) La sous-requête est corrélée. Elle référence CHAMBRE.nomHôtel, attribut de la requête externe : elle ne peut pas être évaluée une fois pour toutes, elle doit être réévaluée pour chaque ligne de CHAMBRE, avec une moyenne calculée sur les chambres du même hôtel. C'est ce qui la distingue d'une sous-requête indépendante, qui ne dépend pas de la ligne courante et que le moteur peut calculer une seule fois.

Résultat : la requête renvoie les identifiants des chambres dont la surface est supérieure à la surface moyenne des chambres de leur propre hôtel. Une chambre de 30 m² dans un hôtel dont la moyenne est de 20 m² sort ; la même chambre dans un hôtel dont la moyenne est de 40 m² ne sort pas. C'est un classement relatif, jamais absolu.

#Exercice 3 — Apple Store (contrôle continu avril 2012, 3 heures)

On souhaite représenter une version simplifiée du système de gestion des données de l'Apple Store.

Le magasin propose des applications téléchargeables classées par catégories (Actualité, Productivité, Jeux, Météo, Style de vie). Chaque catégorie a une description brève. Chaque application a un identifiant unique interne, un nom, un prix en dollars, une date de sortie et le nom du développeur. Une application est associée à une catégorie principale et possiblement à une catégorie secondaire.

Chaque utilisateur doit créer un compte, identifié par son email. On connaît aussi son nom, son prénom, sa date de naissance, son numéro de carte bancaire et la date d'expiration de celle-ci. Le pictogramme de vérification ne doit surtout pas être enregistré. Le compte est protégé par un mot de passe. Chaque utilisateur peut lister les terminaux Apple (iPhone, iPad, iPod) qu'il possède ; ces terminaux sont attachés à ce compte et à aucun autre. Pour chaque terminal, on renseigne son numéro de série (unique), sa date d'achat et un nom d'usage.

Chaque utilisateur peut acheter des applications. Chaque achat est référencé par son numéro de facture, et la date d'achat est importante. Une application peut être achetée par plusieurs utilisateurs. Enfin, chaque application possédée par un utilisateur peut être installée sur un ou plusieurs de ses terminaux, avec une date d'installation ; seules les applications achetées par l'utilisateur peuvent être installées. La solution doit contenir six tables.

Partie 1 — conception. Concevez la base et donnez son équivalent SQL.

Partie 2 — requêtes de consultation.

  1. Lister les noms d'applications dont la catégorie secondaire n'est pas spécifiée.
  2. Lister les utilisateurs (email, nom, prénom) ayant acheté au moins une application.
  3. Pour chaque catégorie principale, donner le nombre d'applications achetées par l'utilisateur ayant pour email jean-vivien.millo@inria.fr.
  4. Lister les utilisateurs (nom, prénom) qui n'ont pas acheté l'application « Angry birds ».
  5. Lister les utilisateurs qui ont acheté toutes les applications.
  6. Donner l'email des personnes ainsi que les noms des applications associées dont le prix dépasse celui de l'application la plus chère de la catégorie « Sport ».
  7. Lister les utilisateurs (nom et prénom) qui possèdent des applications mais qui ne les ont pas installées.

Partie 3 — requêtes de mise à jour.

  1. Ajouter une information indiquant la taille en kilo-octet d'une application.
  2. Baisser de 10 centimes le prix de toutes les applications qui coûtent plus de 2 dollars.
  3. Créer une nouvelle table, par une requête unique, constituée du nom et du prénom de l'utilisateur, du numéro de facture et de la date d'achat.
Correction détaillée

Partie 1 — schéma (six tables).

sqlsql

1create table Categorie (2  numCat integer primary key,3  libelle text unique not null,4  description text5);6 7create table Application (8  idApp integer primary key,9  nom text not null,10  prix real not null,11  dateSortie text,12  developpeur text,13  catPrincipale integer not null references Categorie(numCat),14  catSecondaire integer references Categorie(numCat)   -- nullable : catégorie secondaire facultative

Trois points de conception que l'énoncé fait travailler :

  • catSecondaire doit accepter NULL : une application peut ne pas avoir de catégorie secondaire, et c'est la question 1 de la partie 2 qui l'exploite.
  • La contrainte « seules les applications achetées peuvent être installées » est encodée par la clé étrangère composite de Installation vers Achat(email, idApp). Pour que ce soit possible, il faut déclarer une contrainte d'unicité sur (email, idApp) dans Achat — la clé primaire numFacture ne suffit pas à servir de cible.
  • « attachés à ce compte et à aucun autre » interdit de mettre numSerie dans Utilisateur (colonne multivaluée) : d'où la table Terminal avec sa clé étrangère.

Partie 2 — requêtes.

sqlsql

1-- 1. Applications sans catégorie secondaire2select nom from Application where catSecondaire is null;3 4-- 2. Utilisateurs ayant acheté au moins une application5select distinct u.email, u.nom, u.prenom6from Utilisateur u7join Achat a on a.email = u.email;8 9-- 3. Nombre d'applications achetées par catégorie principale, pour un utilisateur donné10select ap.catPrincipale, count(*) as nb11from Achat a12join Application ap on ap.idApp = a.idApp13where a.email = 'jean-vivien.millo@inria.fr'14group by ap.catPrincipale;

Deux remarques de fond.

  • La question 4 se résout par une anti-jointure : « n'a pas acheté » couvre aussi les utilisateurs qui n'ont rien acheté du tout. Un join puis where ap.nom <> 'Angry birds' donnerait une réponse fausse, puisqu'il exclurait silencieusement les clients sans achat.
  • Les questions 5 et 6 sont les deux formes de la question en « tous les » et de la comparaison à un agrégat global. La question 5 est une division (double not exists) ; la question 6 est une sous-requête scalaire dans le WHERE, qui peut être non corrélée parce que le maximum de la catégorie « Sport » est une constante.

Partie 3 — mises à jour.

sqlsql

1-- 1. Ajouter la taille en kilo-octets2alter table Application add column tailleKo integer;3 4-- 2. Baisser de 10 centimes les applications à plus de 2 dollars5update Application set prix = prix - 0.10 where prix > 2;6 7-- 3. Créer une table à partir d'une requête (CREATE TABLE ... AS SELECT)8create table AchatUtilisateur as9select u.nom, u.prenom, a.numFacture, a.dateAchat10from Utilisateur u11join Achat a on a.email = u.email;

La requête 3 illustre la différence entre LDD et LMD : create table ... as select est une instruction de définition de données qui se nourrit d'une instruction de manipulation. C'est le pont entre les deux langages d'un SGBD.

#Exercice 4 — CarTrack et Armazon (CC n°2, novembre 2013)

Partie 1 — conception (10 pts). CarTrack est une startup qui fournit des services de sécurité aux voitures : suivi 24h/24, blocage à distance, récupération de véhicules volés, déverrouillage à distance.

Pour chaque voiture, on note la marque, le modèle, la couleur, la plaque, le propriétaire (un client), la date d'immatriculation et le code du système installé. Une voiture a un seul système et un système est associé à une seule voiture ; il est toutefois conseillé de répertorier les systèmes dans une table à part. Pour chaque client, on stocke le nom, le prénom, l'adresse et le numéro de téléphone.

Quand le système central envoie une requête sur l'état de la voiture, le système du véhicule retourne les coordonnées GPS (latitude, longitude), l'état de l'alarme (active ou inactive) et l'état des portes (déverrouillées ou verrouillées). Une fois l'information reçue, le système central inscrit la date, l'heure et les données reçues.

L'entreprise a un groupe de partenaires qui installent son système dans les voitures. Pour chaque partenaire, on enregistre son nom, son adresse, son numéro de téléphone et le code des systèmes installés. Pour chaque système installé par un partenaire, le test d'installation doit être enregistré.

Partie 2 — requêtes SQL (10 pts). Armazon vend tout type de produits. Schéma partiel :

texttext

1Client(code, nom, prenom, codeAdresse, telephone)2Categorie(code, nom)3Produit(code, nom, description, codeCategorie, prix)4Stock(date, codeProduit, unites)5Commande(code, date, codeClient, codeProduit, quantite)
  1. Nombre d'unités disponibles du ou des produits « Ventilateur ».
  2. Nombre de commandes de livres effectués par « Marie Pascal » (Livre est une catégorie).
  3. Les clients (code) qui ont acheté pendant le mois de novembre 2013, avec pour chacun la quantité totale de produits achetés, le résultat étant ordonné selon la quantité décroissante.
  4. Noms des produits qui ne sont pas achetés.
  5. Les produits dont le prix est supérieur au prix moyen des produits de la même catégorie.
Correction détaillée

Partie 1 — schéma CarTrack. L'énoncé laisse volontairement des zones d'ambiguïté ; l'attendu est un schéma cohérent accompagné de ses justifications.

sqlsql

1create table Client (2  numClient integer primary key,3  nom text not null, prenom text not null,4  adresse text, telephone text5);6 7create table Voiture (8  plaque text primary key,9  marque text, modele text, couleur text,10  dateImmatriculation text,11  numClient integer not null references Client(numClient)12);13 14create table Systeme (

Les décisions à justifier :

  • Le 1-1 est modélisé par une table Systeme séparée, comme le suggère l'énoncé, avec une contrainte unique sur plaque : c'est cette unicité qui encode le « un seul système par voiture ». Sans elle, la relation serait 1-N.
  • Les relevés sont dans une table à part parce qu'un système produit une quantité non bornée de mesures : les stocker en colonnes de Systeme serait un attribut multivalué.
  • Le lien partenaire-système est N-N (un partenaire installe plusieurs systèmes, un système est installé par un partenaire), et il porte des attributs (la date et le résultat du test d'installation). C'est le cas typique qui impose la table d'association.
  • Le test d'installation appartient à l'association, pas au système seul : deux partenaires peuvent avoir installé le même modèle de système et obtenu des résultats différents.

Partie 2 — requêtes SQL.

sqlsql

1-- 1. Unités disponibles du ou des produits « Ventilateur »2select p.code, p.nom, sum(s.unites) as unitesDisponibles3from Produit p4join Stock s on s.codeProduit = p.code5where p.nom = 'Ventilateur'6group by p.code, p.nom;

Le group by est nécessaire : Stock est historisé par date, et le même produit peut apparaître sur plusieurs lignes. Sans regroupement, on obtiendrait une ligne par relevé de stock.

sqlsql

1-- 2. Nombre de commandes de livres effectués par Marie Pascal2select count(*) as nbCommandes3from Commande c4join Client cl on cl.code = c.codeClient5join Produit p on p.code = c.codeProduit6join Categorie cat on cat.code = p.codeCategorie7where cl.nom = 'Pascal' and cl.prenom = 'Marie' and cat.nom = 'Livre';

Trois jointures sont inévitables : le nom du client est dans Client, la catégorie du produit dans Categorie, et le lien passe par Produit. La comparaison porte sur nom et prenom : rien ne garantit l'unicité du nom de famille.

sqlsql

1-- 3. Clients ayant acheté en novembre 2013, avec quantité totale, tri décroissant2select c.codeClient, sum(c.quantite) as quantiteTotale3from Commande c4where c.date >= '2013-11-01' and c.date < '2013-12-01'5group by c.codeClient6order by quantiteTotale desc;

Le filtre de date s'écrit en bornes (>= et <) plutôt qu'avec between ou month() : c'est à la fois SARGable (un index sur date reste utilisable) et correct sur les bornes — between '2013-11-01' and '2013-11-30' raterait les commandes du 30 novembre à 14h si la colonne contenait une heure.

sqlsql

1-- 4. Produits jamais achetés2select p.nom3from Produit p4where not exists (select 1 from Commande c where c.codeProduit = p.code);5 6-- 5. Produits plus chers que la moyenne de leur catégorie7select p.nom, p.prix, p.codeCategorie8from Produit p9where p.prix > (select avg(p2.prix)10                from Produit p211                where p2.codeCategorie = p.codeCategorie);

La question 5 est le cas d'école de la sous-requête corrélée : la moyenne dépend de la catégorie de la ligne courante, donc elle est recalculée pour chaque produit. Une version non corrélée serait fausse, puisqu'elle comparerait chaque produit à la moyenne globale du catalogue. Une variante par jointure sur une table dérivée donne le même résultat :

sqlsql

1select p.nom, p.prix2from Produit p3join (select codeCategorie, avg(prix) as moyenne from Produit group by codeCategorie) m4  on m.codeCategorie = p.codeCategorie5where p.prix > m.moyenne;

#Exercice 5 — Modélisation E-A d'une base Zoo (extrait du CT décembre 2016)

Chaque animal est identifié par son numéro, a un nom, appartient à une espèce et est parqué dans un emplacement numéroté. Un emplacement peut être vide ou accueillir plusieurs animaux. Chaque emplacement, identifié par son numéro, appartient à un type (volière, cage, îlot). À chaque emplacement sont affectés un ou plusieurs gardiens, et un gardien peut être affecté à plusieurs emplacements. Chaque gardien a un numéro d'identification, un nom, un salaire et un remplaçant, qui est lui aussi un gardien. Un gardien peut être le remplaçant de plusieurs autres gardiens.

  1. Concevez un modèle E-A pour cette base (notation UML ou Chen, au choix).
  2. Transformez le modèle E-A obtenu en un ensemble de tables, avec clés primaires et étrangères définies.
Correction détaillée

1. Modèle E-A. Quatre entités et deux associations, plus une association réflexive.

texttext

1Animal(numAnimal, nom, espece)          -- association « parqué dans » vers Emplacement, 1..1 / 0..*2Emplacement(numEmplacement, type)       -- association « affecté à » vers Gardien, 1..* / 0..*3Gardien(numGardien, nom, salaire)       -- association réflexive « remplace », 0..* / 0..1
  • Animal — parqué dans — Emplacement : cardinalités 1..1 du côté animal (un animal est dans exactement un emplacement) et 0..* du côté emplacement (un emplacement peut être vide). C'est un 1-N.
  • Emplacement — affecté à — Gardien : 1..* des deux côtés. C'est un N-N, donc une table d'association.
  • Gardien — remplace — Gardien : association réflexive, 0..1 du côté « remplaçant » et 0..* du côté « remplacé ». Un gardien a au plus un remplaçant ; il peut être le remplaçant de plusieurs.

2. Tables.

texttext

1Animal(numAnimal, nom, espece, numEmplacement#)2Emplacement(numEmplacement, type)3Gardien(numGardien, nom, salaire, numRemplacant#)4Affectation(numEmplacement#, numGardien#)

Application des trois règles de passage : l'association 1-N Animal–Emplacement place la clé étrangère du côté « 1 », c'est-à-dire dans Animal ; l'association N-N Emplacement–Gardien devient la table Affectation à clé primaire composite ; l'association réflexive se traduit par une clé étrangère de Gardien vers Gardien, numRemplacant étant nullable puisqu'un gardien peut n'avoir aucun remplaçant.

La contrainte « il ne peut pas être son propre remplaçant » n'est pas exprimable par une simple clé étrangère : elle demande un check (numRemplacant <> numGardien) — et le fait de pouvoir nommer précisément cette limite est souvent ce qui distingue une bonne copie.

#Exercice 6 — Transformation E-A d'un fragment enseignant-chercheur (TD 9)

Transformez en schéma relationnel ce fragment de schéma E-A du département d'Informatique de l'Université Côte d'Azur.

texttext

1Enseignant(Numéro, Nom, Age)   -- Donne, 1..1 / 0..* -->  Cours(Code, Intitulé, Nb_heures)2Enseignant                     -- Recherche pour, 1..* / 0..* --> Projet(Code, Intitulé, Description)

Chaque enseignant donne aucun ou plusieurs cours. Chaque cours est enseigné par un seul enseignant. Chaque enseignant fait de la recherche pour aucun ou plusieurs projets. Chaque projet est géré par un ou plusieurs enseignants.

Correction détaillée
texttext

1Enseignant(Numéro, Nom, Age)2Cours(Code, Intitulé, Nb_heures, num_enseignant#)3Projet(Code, Intitulé, Description)4EnseignantProjet(num_enseignant#, code_projet#)

Association « Donne » (1-N). La cardinalité de (Enseignant, Donne) vaut 1 : la clé étrangère va du côté « 1 », donc dans Cours. Mettre num_cours dans Enseignant serait une faute : un enseignant donne plusieurs cours, donc la colonne serait multivaluée — exactement ce que la 1NF interdit. C'est l'erreur que le corrigé du TD signale en premier.

Association « Recherche pour » (N-N). Aucune des deux cardinalités ne vaut 1 : impossible de placer la clé étrangère d'un côté ou de l'autre sans créer de doublons ou violer la 1NF. Il faut la table intermédiaire EnseignantProjet, dont la clé primaire est composite — les deux clés étrangères — ce qui interdit d'associer deux fois le même couple enseignant-projet.

Le critère de décision, énoncé une fois pour toutes : si la cardinalité d'une entité dans l'association vaut 1, la clé étrangère va chez elle ; sinon, l'association devient une table.

#Exercice 7 — Planifications et graphes de précédence (TD 10)

Pour chacune des planifications suivantes, tracez un graphe de précédence et indiquez si la planification garantit la sérialisation des conflits. Pour les planifications sérialisables, donnez une planification sérielle équivalente.

texttext

1a) L(T1,x), L(T2,x), E(T1,x), E(T2,x), val(T1), val(T2)2b) L(T1,x), L(T2,y), E(T3,x), E(T1,y), val(T1), val(T2), val(T3)3c) L(T1,x), E(T2,x), E(T1,x), avort(T2), val(T1)4d) E(T1,x), L(T2,x), E(T1,x), val(T2), avort(T1)5e) L(T1,x), E(T2,x), E(T1,x), L(T3,x), val(T1), val(T2), val(T3)
Correction détaillée

Méthode : écrire d'abord le tableau des temps (chaque action correspond à un temps), puis chercher les couples lecture/écriture sur la même donnée, puis chercher un cycle.

a) T1 lit x (t1), T2 lit x (t2), T1 écrit x (t3), T2 écrit x (t4).

  • T2 lit x avant que T1 ne l'écrive : flèche T2T1T_2 \to T_1.
  • T1 écrit x avant que T2 ne l'écrive : flèche T1T2T_1 \to T_2.

Le graphe est cyclique : la planification n'est pas sérialisable.

b) T1 lit x (t1), T2 lit y (t2), T3 écrit x (t3), T1 écrit y (t4).

  • T1 a lu x avant l'écriture de T3 : flèche T1T3T_1 \to T_3.
  • T2 a lu y avant l'écriture de T1 : flèche T2T1T_2 \to T_1.

Le graphe est acyclique : la planification est sérialisable. L'ordre topologique T2,T1,T3T_2, T_1, T_3 fournit la planification sérielle équivalente : les opérations de T2 d'abord, puis celles de T1, puis celles de T3.

c) T1 lit x (t1), T2 écrit x (t2), T1 écrit x (t3), T2 avorte (t4), T1 valide (t5).

  • T1 lit x avant que T2 ne l'écrive : flèche T1T2T_1 \to T_2.
  • T2 écrit x avant que T1 ne l'écrive : flèche T2T1T_2 \to T_1.

Le graphe brut est cyclique. Mais T2 avorte : ses effets sont annulés (c'est l'atomicité), et ses opérations ne participent donc plus à l'exécution finale. Une fois T2 retirée du graphe, il ne reste que T1 : la planification est sérialisable, et la planification sérielle équivalente se réduit à T1 seule. Le point à retenir : une transaction avortée ne peut pas figurer dans l'ordre sériel équivalent.

d) T1 écrit x (t1), T2 lit x (t2), T1 écrit x (t3), T2 valide (t4), T1 avorte (t5).

  • T1 a écrit x avant que T2 ne le lise : flèche T1T2T_1 \to T_2.
  • T2 a lu x avant que T1 ne le réécrive : flèche T2T1T_2 \to T_1.

Le graphe est cyclique : la planification n'est pas sérialisable. Le fait que T1 avorte ensuite ne rachète rien : T2 a validé sur la base d'une valeur que T1 a ensuite annulée — c'est exactement le problème de la dépendance non validée. C'est la différence essentielle avec le cas c.

e) T1 lit x (t1), T2 écrit x (t2), T1 écrit x (t3), T3 lit x (t4).

  • T1 lit x avant l'écriture de T2 : T1T2T_1 \to T_2.
  • T2 écrit x avant l'écriture de T1 : T2T1T_2 \to T_1.
  • T1 écrit x avant la lecture de T3 : T1T3T_1 \to T_3 ; T2 aussi : T2T3T_2 \to T_3.

Le cycle T1T2T_1 \leftrightarrow T_2 subsiste : la planification n'est pas sérialisable. L'ajout de T3, qui ne fait que lire après coup, ne change rien au diagnostic.

#Exercice 8 — Sérialisabilité d'une longue planification (TD 10)

Déterminez si la planification suivante est sérialisable au niveau des conflits :

texttext

1P = [ L1(Z), L2(Y), E2(Y), L3(Y), L1(X), E1(X), E1(Z), E3(Y), L2(X), L1(Y), E1(Y), E2(X), L3(W), E3(W) ]

Li(Z)L_i(Z) et Ei(Z)E_i(Z) désignent une lecture et une écriture de la donnée Z par la transaction ii.

Correction détaillée

On parcourt la séquence dans l'ordre et on enregistre, pour chaque donnée, le dernier écrivain et les lecteurs depuis cette écriture. Une flèche TiTjT_i \to T_j apparaît dès que TjT_j écrit une donnée que TiT_i a lue, ou que TjT_j lit une donnée que TiT_i a écrite.

TempsActionConséquence
t1L1(Z)L_1(Z)Z lu par T1
t2L2(Y)L_2(Y)Y lu par T2
t3E2(Y)E_2(Y)Y écrit par T2
t4L3(Y)L_3(Y)T2T3T_2 \to T_3 : T3 lit Y écrit par T2
t5L1(X)L_1(X)X lu par T1
t6E1(X)E_1(X)X écrit par T1
t7E1(Z)E_1(Z)Z écrit par T1 (T1 était seule lectrice)
t8E3(Y)E_3(Y)T2T3T_2 \to T_3 : T3 écrit Y après T2
t9L2(X)L_2(X)T1T2T_1 \to T_2 : T2 lit X écrit par T1
t10L1(Y)L_1(Y)Y lu par T1
t11E1(Y)E_1(Y)T2T1T_2 \to T_1 et T3T1T_3 \to T_1 : T1 écrit Y lu par T2 et T3
t12E2(X)E_2(X)T1T2T_1 \to T_2 : T2 écrit X après T1
t13L3(W)L_3(W)W lu par T3
t14E3(W)E_3(W)W écrit par T3

Ensemble des flèches : T2T3T_2 \to T_3, T1T2T_1 \to T_2, T2T1T_2 \to T_1, T3T1T_3 \to T_1.

Le graphe contient le cycle T1T2T3T1T_1 \to T_2 \to T_3 \to T_1 : la planification n'est pas sérialisable au niveau des conflits.

#Exercice 9 — Graphe d'attente et verrous indéfinis (TD 10 / extrait du CT 2016)

Produisez un graphe d'attente pour le scénario de transactions suivant et déterminez si des verrous indéfinis y apparaissent.

TransactionDonnées verrouilléesDonnées attendues
T1x2x1, x3
T2x3, x10x7, x8
T3x8x4, x5
T4x7x1
T5x1, x5x3
T6x4, x9x6
T7x6x5
Correction détaillée

Construction du graphe. Une flèche TiTjT_i \to T_j existe quand TiT_i attend un élément actuellement verrouillé par TjT_j. On croise la colonne « attend » avec la colonne « verrouille » :

AttenteDétenu parFlèche
T1 attend x1T5T1T5T_1 \to T_5
T1 attend x3T2T1T2T_1 \to T_2
T2 attend x7T4T2T4T_2 \to T_4
T2 attend x8T3T2T3T_2 \to T_3
T3 attend x4T6T3T6T_3 \to T_6
T3 attend x5T5T3T5T_3 \to T_5
T4 attend x1T5T4T5T_4 \to T_5
T5 attend x3T2T5T2T_5 \to T_2
T6 attend x6T7T6T7T_6 \to T_7
T7 attend x5T5T7T5T_7 \to T_5
texttext

1T1 ──→ T5          T5 ──→ T22T1 ──→ T2          T6 ──→ T73T2 ──→ T4          T7 ──→ T54T2 ──→ T35T3 ──→ T66T3 ──→ T57T4 ──→ T5

Détection. Par le théorème de Holt, un verrou indéfini existe si et seulement si le graphe des attentes contient un cycle. Les cycles orientés simples sont au nombre de trois :

texttext

1T2 → T3 → T5 → T22T2 → T4 → T5 → T23T2 → T3 → T6 → T7 → T5 → T2

Conclusion : le graphe contient des cycles, donc il y a interblocage. Les transactions concernées sont T2, T3, T4, T5, T6 et T7 ; T1 attend un verrou détenu par T5 mais personne n'attend un verrou détenu par T1, elle n'est donc pas dans un cycle — elle subit l'attente sans participer à l'impasse.

#Exercice 10 — Mise à jour perdue et protocole V2P (TD 10)

Déterminez le problème de la planification suivante et résolvez-le à l'aide du protocole de verrouillage en deux phases.

TempsT1T2
t1verrou-écriture(qte)verrou-écriture(qte)
t2lire qteattente
t3qte ← qte + 3000attente
t4validation / déverrouillagelire qte
t5qte ← qte + 500
t6validation / déverrouillage
Correction détaillée

Le problème : la mise à jour perdue. Sans verrouillage, les deux transactions liraient la même valeur initiale de qte et écriraient chacune une valeur calculée à partir de cette valeur initiale. La seconde écriture écraserait la première : sur un stock de 1000, on attendrait 4500 et on obtiendrait 1500 ou 4000 selon l'ordre. C'est exactement « une opération de modification apparemment achevée avec succès est écrasée par un autre utilisateur ».

La résolution par le verrouillage. Le tableau montre le mécanisme en action :

  • t1 : T1 obtient le verrou d'écriture sur qte. T2 demande le même verrou et doit attendre : un verrou exclusif n'est pas compatible avec un autre verrou exclusif.
  • t2 et t3 : T1 est seule à pouvoir lire et écrire. Elle lit la valeur, calcule qte + 3000.
  • t4 : T1 valide et déverrouille. Ce n'est qu'à ce moment que les effets de l'écriture deviennent visibles aux autres transactions.
  • t4 à t6 : T2 lit alors la valeur déjà modifiée par T1, et calcule qte + 500 à partir d'elle.

Le résultat final est la somme des deux mises à jour, ce qui est le comportement attendu. Le protocole de verrouillage en deux phases est ce qui garantit que T2 ne peut pas acquérir son verrou après en avoir libéré un, ni libérer un verrou avant d'avoir acquis tous ceux dont elle a besoin : les opérations de verrouillage précèdent toutes la première opération de déverrouillage.

Pourquoi le verrouillage seul ne suffit pas. Le cours donne une planification de verrouillage incorrecte pour le prouver : T9 déverrouille soldex avant d'avoir fini sa phase de croissance, ce qui laisse T10 s'insérer au milieu et produit un résultat qui ne correspond à aucune exécution sérielle (soldex = 220 et soldey = 340 alors que les deux ordres sériels donnent respectivement 220/330 et 210/340). C'est la discipline en deux phases qui garantit la sérialisabilité, pas la présence de verrous.

#Exercice 11 — Bibliothèque de DVD (TD 1 / TP 1)

Schéma fourni par l'énoncé (les clés primaires sont soulignées en sujet, les clés étrangères en italique) :

texttext

1Societe(numS, nomS, rueS, villeS, directeurS)2Dvd(numD, titreD, auteurD, anneeD, categorieD, dateAchatD, nombreD, societeD)3Acteur(numA, nomA, prenomA, ageA, sexeA)4Casting(dvdC, acteurC, roleC)5Emprunt(dvdE, dateE, clientE, dureeE)

Chaque société de location a un nom et une adresse, ainsi qu'un nom de directeur ; plusieurs sociétés peuvent porter le même nom. Un DVD a un titre, un auteur, une année de sortie, un numéro d'identification, une catégorie, une date d'acquisition, un nombre d'exemplaires achetés et la société qui l'a acquis. Pour un acteur, on mémorise son nom, son prénom, son âge et son sexe ; pour chaque DVD et chaque acteur principal, on mémorise le rôle tenu. Un DVD est emprunté par un client (seul son numéro est mémorisé) à une date donnée et pour une durée déterminée. Un même client ne peut pas emprunter un même DVD plus d'une fois par jour.

Mettez en œuvre les requêtes suivantes :

  1. La liste des DVD acquis par la société « DVDStore ».
  2. La liste des DVD acquis par la société « DVDStore », avec pour chacun le nom et le prénom des acteurs qui ont participé au film.
  3. La liste des acteurs de chaque DVD.
  4. La liste des clients ayant loué un DVD dont un des acteurs a « Xavier » pour prénom.
Correction détaillée

Un point de conception à comprendre d'abord. « Plusieurs sociétés de location peuvent porter le même nom » : nomS ne peut donc pas être la clé primaire de Societe, d'où numS. De même, Dvd possède son propre identifiant numD : un même film (même titre, même année) peut exister chez deux sociétés, et ce sont deux lignes différentes, donc deux DVD différents. C'est ce que le corrigé du TD souligne : bien comprendre le rôle de l'identifiant unique. Conséquence directe sur la question 4 : un même film loué chez deux sociétés n'est pas le même DVD.

Requête 1.

sqlsql

1select d.numD, d.titreD2from Dvd d3join Societe s on s.numS = d.societeD4where s.nomS = 'DVDStore';

La jointure est indispensable : le nom de la société n'est pas dans Dvd, seulement sa clé étrangère.

Requête 2.

sqlsql

1select d.numD, d.titreD, a.nomA, a.prenomA2from Dvd d3join Societe s on s.numS = d.societeD4join Casting c on c.dvdC = d.numD5join Acteur a on a.numA = c.acteurC6where s.nomS = 'DVDStore'7order by d.numD, a.nomA;

Trois jointures en chaîne, et le rôle (roleC) est disponible dans Casting si l'énoncé le demande. Attention à la cardinalité : un DVD avec cinq acteurs produit cinq lignes. Si l'on veut une ligne par DVD avec la liste des acteurs, il faut agréger — group_concat en SQLite et MySQL, string_agg en PostgreSQL.

Requête 3.

sqlsql

1select d.titreD, a.nomA, a.prenomA, c.roleC2from Dvd d3join Casting c on c.dvdC = d.numD4join Acteur a on a.numA = c.acteurC5order by d.titreD, a.nomA;

C'est la même jointure que la question 2 sans le filtre sur la société : Casting est la table d'association qui porte la relation N-N entre Dvd et Acteur, et c'est elle qui porte aussi le rôle — attribut de l'association, pas de l'acteur.

Requête 4.

sqlsql

1select distinct e.clientE2from Emprunt e3join Casting c on c.dvdC = e.dvdE4join Acteur a on a.numA = c.acteurC5where a.prenomA = 'Xavier';

Le distinct est nécessaire : un client qui a loué trois films avec Xavier apparaîtrait trois fois. Comme toujours quand la question porte sur l'existence d'une correspondance, la semi-jointure est plus fidèle :

sqlsql

1select e.clientE2from Emprunt e3where exists (4  select 1 from Casting c5  join Acteur a on a.numA = c.acteurC6  where c.dvdC = e.dvdE and a.prenomA = 'Xavier'7);

#Exercice 12 — Questions de cours du contrôle terminal (décembre 2016)

  1. Quelle est l'utilité du protocole de verrouillage en deux phases ?
  2. En SQL, quel est l'ordre de traitement des clauses SELECT, FROM, WHERE, GROUP BY, HAVING, ORDER BY ? Quelle est la logique de cet ordre ?
Correction détaillée

1. Utilité du V2P. Il garantit la sérialisabilité des conflits : il évite l'incohérence des données lorsque deux transactions effectuent des opérations de lecture et d'écriture sur une même valeur. Le protocole impose que toutes les opérations de verrouillage d'une transaction précèdent sa première opération de déverrouillage. La transaction se décompose alors en une phase de croissance (elle acquiert tous ses verrous, n'en libère aucun) et une phase de résorption (elle libère ses verrous, n'en acquiert plus).

Le mécanisme pratique : la première transaction arrivée obtient le verrou exclusif, ce qui restreint la lecture et l'écriture de la valeur à elle seule ; les autres transactions doivent attendre la fin de la transaction détentrice. Une fois celle-ci validée, elle cède sa place et la valeur finale reste cohérente. C'est le « premier arrivé, premier servi » — mais c'est la discipline en deux phases, et non le verrouillage en soi, qui empêche une transaction de s'insérer au milieu d'une autre.

2. Ordre de traitement des clauses. L'ordre est :

texttext

1FROM  ->  WHERE  ->  GROUP BY  ->  HAVING  ->  sous-requête  ->  SELECT  ->  ORDER BY  ->  LIMIT

La logique est celle d'un pipeline : chaque étape ne peut utiliser que ce que les étapes précédentes ont produit.

  • FROM lit les tables : c'est la source.
  • WHERE applique une ou des conditions sur les lignes issues de ces tables.
  • GROUP BY regroupe les lignes selon un attribut — les lignes regroupées sont celles qui ont passé le WHERE.
  • HAVING n'affiche les groupes que selon une condition de calcul sur un attribut.
  • La sous-requête récupère un jeu de données, qui peut ensuite alimenter le SELECT.
  • SELECT sélectionne les attributs.
  • ORDER BY fixe l'ordre d'affichage.
  • LIMIT borne le nombre de lignes affichées.

Deux conséquences pratiques découlent de cet ordre : un alias défini dans le SELECT n'est pas utilisable dans le WHERE (évalué avant) mais l'est dans le ORDER BY (évalué après) ; et un agrégat comme AVG(...) ne peut pas apparaître dans un WHERE, puisqu'il n'existe pas encore à ce stade.

#Synthèse : ce qu'il faut savoir faire sans hésiter

CompétenceOù elle est évaluéeSection du module
Énumérer les clés étrangères d'un schémaTous les sujetsModélisation
Traduire un énoncé en E-A puis en tablesCT 2016, TD 9Modélisation
Écrire une requête en algèbre relationnelleCC 2015, CC 2020, TD 3Algèbre relationnelle
Reconnaître une question en « tous les » (division)CC 2015, CC 2020, CC 2012Algèbre relationnelle
Traduire une expression algébrique en SQL et inversementCC 2015, TD 3Algèbre relationnelle
Écrire des requêtes SQL de consultation, jointures comprisesCC 2012, CC 2013, CC 2020SELECT, JOIN
Agréger et filtrer des groupesTD 5, CC 2013Agrégations
Distinguer sous-requête corrélée et indépendanteCC 2020, CC 2013SELECT, JOIN
Normaliser une table plate jusqu'en 3NFCT 2016, TD 9Normalisation
Diagnostiquer les anomalies d'une table dénormaliséeTD 9Normalisation
Construire un graphe de précédence et conclureTD 10Transactions
Construire un graphe d'attente et détecter un interblocageCT 2016, TD 10Transactions
Expliquer l'utilité du V2PCT 2016Transactions
Dans un sujet d'examen, une question demande « les X qui ont fait tous les Y ». Quelle construction d'algèbre relationnelle faut-il mobiliser ?
Dans un sujet d'examen, une question demande « les X qui ont fait tous les Y ». Quelle construction d'algèbre relationnelle faut-il mobiliser ?
Une planification non sérielle est sérialisable si et seulement si...
Une planification non sérielle est sérialisable si et seulement si...